ok
This commit is contained in:
348
src/components/IdentifyCafeModal.js
Normal file
348
src/components/IdentifyCafeModal.js
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import styles from "./IdentifyCafeModal.module.css";
|
||||||
|
import API_BASE_URL from "../config.js";
|
||||||
|
import { getTables, createTable } from "../helpers/tableHelper";
|
||||||
|
import { saveCafeDetails } from "../helpers/cafeHelpers";
|
||||||
|
import { getImageUrl } from "../helpers/itemHelper";
|
||||||
|
import { toPng } from "html-to-image";
|
||||||
|
|
||||||
|
export default function IdentifyCafeModal({ shop }) {
|
||||||
|
const [cafeIdentifyName, setCafeIdentifyName] = useState(shop.cafeIdentifyName || "");
|
||||||
|
const [availability, setAvailability] = useState(null); // 200 ok, 409 taken
|
||||||
|
const [checking, setChecking] = useState(false);
|
||||||
|
|
||||||
|
const [tables, setTables] = useState([]);
|
||||||
|
const [selectedTable, setSelectedTable] = useState(null);
|
||||||
|
|
||||||
|
const [qrSize, setQrSize] = useState(Number(shop.scale) || 1);
|
||||||
|
const [qrX, setQrX] = useState(Number(shop.xposition) || 50);
|
||||||
|
const [qrY, setQrY] = useState(Number(shop.yposition) || 50);
|
||||||
|
const [fontSize, setFontSize] = useState(Number(shop.fontsize) || 16);
|
||||||
|
const [fontColor, setFontColor] = useState(shop.fontcolor || "#FFFFFF");
|
||||||
|
const [fontX, setFontX] = useState(Number(shop.fontxposition) || 50);
|
||||||
|
const [fontY, setFontY] = useState(Number(shop.fontyposition) || 85);
|
||||||
|
const [bgImageUrl, setBgImageUrl] = useState(getImageUrl(shop.qrBackground));
|
||||||
|
const bgFileRef = useRef(null);
|
||||||
|
|
||||||
|
const [newTableNo, setNewTableNo] = useState("");
|
||||||
|
const previewRef = useRef(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [currentStep, setCurrentStep] = useState(1); // 1=Alamat, 2=Desain QR, 3=Meja
|
||||||
|
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const initialDesignRef = useRef({
|
||||||
|
qrSize: Number(shop.scale) || 1,
|
||||||
|
qrX: Number(shop.xposition) || 50,
|
||||||
|
qrY: Number(shop.yposition) || 50,
|
||||||
|
fontSize: Number(shop.fontsize) || 16,
|
||||||
|
fontColor: shop.fontcolor || "#FFFFFF",
|
||||||
|
fontX: Number(shop.fontxposition) || 50,
|
||||||
|
fontY: Number(shop.fontyposition) || 85,
|
||||||
|
bgImageUrl: getImageUrl(shop.qrBackground),
|
||||||
|
});
|
||||||
|
|
||||||
|
const shopHost = useMemo(() => window.location.hostname, []);
|
||||||
|
const fullLink = useMemo(() => `${shopHost}/${cafeIdentifyName}`, [shopHost, cafeIdentifyName]);
|
||||||
|
|
||||||
|
// Debounced availability check
|
||||||
|
const debounceRef = useRef(null);
|
||||||
|
const handleIdentifyChange = (e) => {
|
||||||
|
const val = e.target.value
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, "_")
|
||||||
|
.replace(/[^a-z0-9_]/g, "");
|
||||||
|
setCafeIdentifyName(val);
|
||||||
|
setChecking(true);
|
||||||
|
setAvailability(null);
|
||||||
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||||
|
debounceRef.current = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/cafe/check-identifyName/${val}`);
|
||||||
|
setAvailability(res.ok ? 200 : 409);
|
||||||
|
} catch (_) {
|
||||||
|
setAvailability(409);
|
||||||
|
} finally {
|
||||||
|
setChecking(false);
|
||||||
|
}
|
||||||
|
}, 600);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load tables
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const fetched = await getTables(shop.cafeId);
|
||||||
|
setTables(fetched || []);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [shop.cafeId]);
|
||||||
|
|
||||||
|
const handleUploadBg = (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
setBgImageUrl(url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateTable = async () => {
|
||||||
|
if (!newTableNo) return;
|
||||||
|
try {
|
||||||
|
const created = await createTable(shop.cafeId, { tableNo: newTableNo });
|
||||||
|
setTables((t) => [...t, created]);
|
||||||
|
setNewTableNo("");
|
||||||
|
} catch (e) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
setSaveStatus(null);
|
||||||
|
const qrBackgroundFile = bgFileRef.current?.files?.[0];
|
||||||
|
const details = {
|
||||||
|
qrSize,
|
||||||
|
qrPosition: { left: qrX, top: qrY },
|
||||||
|
qrBackgroundFile,
|
||||||
|
fontsize: fontSize,
|
||||||
|
fontcolor: fontColor,
|
||||||
|
fontPosition: { left: fontX, top: fontY },
|
||||||
|
cafeIdentifyName: shop.cafeIdentifyName !== cafeIdentifyName ? cafeIdentifyName : null,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await saveCafeDetails(shop.cafeId, details);
|
||||||
|
setSaveStatus('success');
|
||||||
|
} catch (e) {
|
||||||
|
setSaveStatus('error');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadPreview = async () => {
|
||||||
|
if (!previewRef.current) return;
|
||||||
|
const node = previewRef.current;
|
||||||
|
const originalBg = node.style.backgroundColor;
|
||||||
|
node.style.backgroundColor = "transparent";
|
||||||
|
try {
|
||||||
|
const dataUrl = await toPng(node, { pixelRatio: 2 });
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = dataUrl;
|
||||||
|
link.download = selectedTable ? `QR Meja (${selectedTable.tableNo}).png` : `QR ${shop.name}.png`;
|
||||||
|
link.click();
|
||||||
|
} catch (e) {
|
||||||
|
// noop
|
||||||
|
} finally {
|
||||||
|
node.style.backgroundColor = originalBg;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyLink = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(fullLink);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(()=>setCopied(false), 1400);
|
||||||
|
} catch (_) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyPreset = (preset) => {
|
||||||
|
if (preset === 'center') {
|
||||||
|
setQrX(50); setQrY(50); setQrSize(1);
|
||||||
|
setFontX(50); setFontY(85);
|
||||||
|
} else if (preset === 'topLeft') {
|
||||||
|
setQrX(25); setQrY(30); setQrSize(1);
|
||||||
|
setFontX(50); setFontY(85);
|
||||||
|
} else if (preset === 'bottomRight') {
|
||||||
|
setQrX(75); setQrY(70); setQrSize(1);
|
||||||
|
setFontX(50); setFontY(15);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetDesign = () => {
|
||||||
|
const d = initialDesignRef.current;
|
||||||
|
setQrSize(d.qrSize);
|
||||||
|
setQrX(d.qrX);
|
||||||
|
setQrY(d.qrY);
|
||||||
|
setFontSize(d.fontSize);
|
||||||
|
setFontColor(d.fontColor);
|
||||||
|
setFontX(d.fontX);
|
||||||
|
setFontY(d.fontY);
|
||||||
|
setBgImageUrl(d.bgImageUrl);
|
||||||
|
if (bgFileRef.current) bgFileRef.current.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Positioning helpers
|
||||||
|
const qrStyle = {
|
||||||
|
left: `${qrX}%`,
|
||||||
|
top: `${qrY}%`,
|
||||||
|
transform: `translate(-50%, -50%) scale(${qrSize})`,
|
||||||
|
};
|
||||||
|
const fontStyle = {
|
||||||
|
left: `${fontX}%`,
|
||||||
|
top: `${fontY}%`,
|
||||||
|
transform: `translate(-50%, -50%)`,
|
||||||
|
color: fontColor,
|
||||||
|
fontSize: `${fontSize}px`,
|
||||||
|
position: "absolute",
|
||||||
|
fontWeight: 700,
|
||||||
|
};
|
||||||
|
|
||||||
|
const qrData = selectedTable ? `${fullLink}/${selectedTable.tableNo}` : fullLink;
|
||||||
|
|
||||||
|
const canProceedFromStep1 = () => {
|
||||||
|
if (!cafeIdentifyName) return false;
|
||||||
|
if (cafeIdentifyName === (shop.cafeIdentifyName || '')) return true;
|
||||||
|
return availability === 200 && !checking;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.title}>Identifikasi kedai</div>
|
||||||
|
<div className={styles.steps}>
|
||||||
|
<div className={`${styles.step} ${currentStep === 1 ? styles.stepActive : ''}`}>
|
||||||
|
<span className={styles.stepNumber}>1</span> Alamat
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.step} ${currentStep === 2 ? styles.stepActive : ''}`}>
|
||||||
|
<span className={styles.stepNumber}>2</span> Desain QR
|
||||||
|
</div>
|
||||||
|
<div className={`${styles.step} ${currentStep === 3 ? styles.stepActive : ''}`}>
|
||||||
|
<span className={styles.stepNumber}>3</span> Meja
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.scrollArea}>
|
||||||
|
{currentStep === 1 && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Alamat kedai</div>
|
||||||
|
<div className={styles.row}>
|
||||||
|
<div className={styles.domainPrefix}>{shopHost}/</div>
|
||||||
|
<input
|
||||||
|
value={cafeIdentifyName}
|
||||||
|
onChange={handleIdentifyChange}
|
||||||
|
className={styles.input}
|
||||||
|
placeholder="alamat_kedai"
|
||||||
|
/>
|
||||||
|
<div className={`${styles.status} ${availability === 200 ? styles.statusOk : availability === 409 ? styles.statusBad : ''}`}>
|
||||||
|
{checking ? 'Memeriksa…' : availability === 200 ? 'Tersedia' : availability === 409 ? 'Terpakai' : 'Menunggu input'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.helpText}>Gunakan huruf kecil, angka, dan garis bawah (_). Contoh: kopikenangan_malam</div>
|
||||||
|
<div className={styles.copyRow}>
|
||||||
|
<input className={styles.linkField} readOnly value={fullLink} />
|
||||||
|
<button className={styles.button} onClick={copyLink}>{copied ? 'Disalin' : 'Salin'}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Desain QR</div>
|
||||||
|
<div className={styles.presets}>
|
||||||
|
<button className={styles.presetButton} onClick={()=>applyPreset('center')}>Tengah</button>
|
||||||
|
<button className={styles.presetButton} onClick={()=>applyPreset('topLeft')}>Atas-Kiri</button>
|
||||||
|
<button className={styles.presetButton} onClick={()=>applyPreset('bottomRight')}>Bawah-Kanan</button>
|
||||||
|
<button className={styles.presetButton} onClick={resetDesign}>Reset desain</button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.grid}>
|
||||||
|
<div>
|
||||||
|
<div ref={previewRef} id="qr-code-container" className={styles.previewBox} style={{ backgroundColor: '#fff' }}>
|
||||||
|
{bgImageUrl && <img src={bgImageUrl} alt="Background" className={styles.bgPreview} />}
|
||||||
|
<img
|
||||||
|
className={styles.qrLayer}
|
||||||
|
style={qrStyle}
|
||||||
|
alt="QR"
|
||||||
|
src={`https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(qrData)}`}
|
||||||
|
/>
|
||||||
|
<div style={fontStyle}>{selectedTable ? selectedTable.tableNo : 'Kedai'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.controls}>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Gambar latar</label>
|
||||||
|
<input ref={bgFileRef} type="file" accept="image/*" onChange={handleUploadBg} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Skala QR ({qrSize}x)</label>
|
||||||
|
<input className={styles.range} type="range" min="0.5" max="3" step="0.1" value={qrSize} onChange={(e)=>setQrSize(parseFloat(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Posisi QR - horizontal ({qrX}%)</label>
|
||||||
|
<input className={styles.range} type="range" min="0" max="100" step="1" value={qrX} onChange={(e)=>setQrX(parseInt(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Posisi QR - vertikal ({qrY}%)</label>
|
||||||
|
<input className={styles.range} type="range" min="0" max="100" step="1" value={qrY} onChange={(e)=>setQrY(parseInt(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Ukuran teks ({fontSize}px)</label>
|
||||||
|
<input className={styles.range} type="range" min="8" max="48" step="1" value={fontSize} onChange={(e)=>setFontSize(parseInt(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Warna teks</label>
|
||||||
|
<input className={styles.colorInput} type="color" value={fontColor} onChange={(e)=>setFontColor(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Posisi teks - horizontal ({fontX}%)</label>
|
||||||
|
<input className={styles.range} type="range" min="0" max="100" step="1" value={fontX} onChange={(e)=>setFontX(parseInt(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Posisi teks - vertikal ({fontY}%)</label>
|
||||||
|
<input className={styles.range} type="range" min="0" max="100" step="1" value={fontY} onChange={(e)=>setFontY(parseInt(e.target.value))} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.helpText}>Tip: Gunakan latar yang kontras agar QR mudah dipindai.</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentStep === 3 && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Daftar meja</div>
|
||||||
|
<div className={styles.tables}>
|
||||||
|
<input className={styles.input} placeholder="Meja A1" value={newTableNo} onChange={(e)=>setNewTableNo(e.target.value)} />
|
||||||
|
<button className={styles.button} onClick={handleCreateTable}>Buat meja</button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.tableList}>
|
||||||
|
{tables && tables
|
||||||
|
.filter((t)=>t.tableNo !== 0)
|
||||||
|
.map((t)=>{
|
||||||
|
const active = selectedTable && selectedTable.tableId === t.tableId;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={t.tableId}
|
||||||
|
className={`${styles.tableItem} ${active ? styles.tableItemActive : ''}`}
|
||||||
|
onClick={()=>setSelectedTable(active ? null : t)}
|
||||||
|
>
|
||||||
|
{t.tableNo}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className={styles.helpText}>Pilih meja untuk membuat QR khusus meja (opsional).</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<button className={`${styles.button} ${styles.secondary}`} disabled={currentStep === 1} onClick={()=>setCurrentStep((s)=>Math.max(1, s-1))}>Kembali</button>
|
||||||
|
{currentStep < 3 ? (
|
||||||
|
<button className={`${styles.button} ${styles.primary}`} disabled={currentStep === 1 && !canProceedFromStep1()} onClick={()=>setCurrentStep((s)=>Math.min(3, s+1))}>Lanjut</button>
|
||||||
|
) : (
|
||||||
|
<button className={`${styles.button} ${styles.primary}`} onClick={handleSave} disabled={saving}>{saving ? 'Menyimpan…' : 'Simpan'}</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
|
{saveStatus === 'success' && <div className={`${styles.banner} ${styles.bannerSuccess}`}>Simpan berhasil</div>}
|
||||||
|
{saveStatus === 'error' && <div className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan</div>}
|
||||||
|
<button className={`${styles.button} ${styles.muted}`} onClick={downloadPreview}>Download QR {selectedTable ? 'meja' : 'kedai'}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
299
src/components/IdentifyCafeModal.module.css
Normal file
299
src/components/IdentifyCafeModal.module.css
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
/* IdentifyCafeModal.module.css */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
background: #fafafa;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepNumber {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e9ecef;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stepActive {
|
||||||
|
border-color: #cdebd8;
|
||||||
|
background: #e9f7ef;
|
||||||
|
color: #245c3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpText {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollArea {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domainPrefix {
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px 0 0 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
outline: none;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusOk {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBad {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkField {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.3fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.previewBox {
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
height: 280px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bgPreview {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qrLayer {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 8px 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presetButton {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
background: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorInput {
|
||||||
|
height: 38px;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tables {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableList {
|
||||||
|
max-height: 180px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableItem {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableItemActive {
|
||||||
|
background: #e9f7ef;
|
||||||
|
border: 1px solid #cdebd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: #28a745;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
background: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerSuccess {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerError {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.grid { grid-template-columns: 1fr; }
|
||||||
|
.controls { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
@@ -23,6 +23,8 @@ const ItemConfig = ({
|
|||||||
const [itemDescription, setItemDescription] = useState(initialDescription);
|
const [itemDescription, setItemDescription] = useState(initialDescription);
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Prevent scrolling when modal is open
|
// Prevent scrolling when modal is open
|
||||||
@@ -79,15 +81,29 @@ const ItemConfig = ({
|
|||||||
}
|
}
|
||||||
}, [textareaRef.current]);
|
}, [textareaRef.current]);
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = async () => {
|
||||||
console.log(itemPromoPrice)
|
setSaving(true);
|
||||||
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice);
|
setSaveStatus(null);
|
||||||
document.body.style.overflow = "auto";
|
try {
|
||||||
|
await Promise.resolve(handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice));
|
||||||
|
setSaveStatus('success');
|
||||||
|
} catch (e) {
|
||||||
|
setSaveStatus('error');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const handleUpdate = () => {
|
const handleUpdate = async () => {
|
||||||
console.log(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice)
|
setSaving(true);
|
||||||
handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice);
|
setSaveStatus(null);
|
||||||
document.body.style.overflow = "auto";
|
try {
|
||||||
|
await Promise.resolve(handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice));
|
||||||
|
setSaveStatus('success');
|
||||||
|
} catch (e) {
|
||||||
|
setSaveStatus('error');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -124,6 +140,14 @@ const ItemConfig = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.formSection}>
|
<div className={styles.formSection}>
|
||||||
|
<div className={styles.bannerRow}>
|
||||||
|
{saveStatus === 'success' && (
|
||||||
|
<span className={`${styles.banner} ${styles.bannerSuccess}`}>Perubahan disimpan</span>
|
||||||
|
)}
|
||||||
|
{saveStatus === 'error' && (
|
||||||
|
<span className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan perubahan</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label className={styles.formLabel}>Nama Item</label>
|
<label className={styles.formLabel}>Nama Item</label>
|
||||||
<input
|
<input
|
||||||
@@ -170,14 +194,16 @@ const ItemConfig = ({
|
|||||||
<button
|
<button
|
||||||
onClick={cancelEdit}
|
onClick={cancelEdit}
|
||||||
className={`${styles.formButton} ${styles.cancelButton}`}
|
className={`${styles.formButton} ${styles.cancelButton}`}
|
||||||
|
disabled={saving}
|
||||||
>
|
>
|
||||||
Batal
|
Batal
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => { isBeingEdit ? handleUpdate() : handleCreate() }}
|
onClick={() => { isBeingEdit ? handleUpdate() : handleCreate() }}
|
||||||
className={`${styles.formButton} ${styles.saveButton}`}
|
className={`${styles.formButton} ${styles.saveButton}`}
|
||||||
|
disabled={saving}
|
||||||
>
|
>
|
||||||
{isBeingEdit ? 'Simpan Perubahan' : 'Buat Item'}
|
{saving ? 'Menyimpan…' : (isBeingEdit ? 'Simpan Perubahan' : 'Buat Item')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
/* ItemTypeLister.css */
|
||||||
|
|
||||||
/* New clean, intuitive category bar */
|
/* New clean, intuitive category bar */
|
||||||
.item-type-lister {
|
.item-type-lister {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 12px 0;
|
padding: 8px 0; /* Reduced padding for more compact design */
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px; /* Reduced margin for more compact design */
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -12,20 +14,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item-type-lister::-webkit-scrollbar {
|
.item-type-lister::-webkit-scrollbar {
|
||||||
height: 8px;
|
height: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-type-lister::-webkit-scrollbar-thumb {
|
.item-type-lister::-webkit-scrollbar-thumb {
|
||||||
background-color: #c5c5c5;
|
background-color: #c5c5c5;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-bar {
|
.category-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 6px; /* Reduced gap for more compact design */
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding: 10px 15px;
|
padding: 6px 10px; /* Reduced padding */
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -39,7 +41,7 @@
|
|||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
gap: 10px;
|
gap: 6px; /* Added gap for consistent spacing */
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -49,37 +51,34 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 6px;
|
||||||
height: 42px;
|
height: 32px; /* Reduced height for more compact design */
|
||||||
padding: 0 18px;
|
padding: 0 14px; /* Reduced padding for more compact design */
|
||||||
border-radius: 12px; /* Square rounded corners */
|
border-radius: 999px;
|
||||||
border: 1px solid #e6e6e6;
|
border: 1px solid #e6e6e6;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
color: #2d2d2d;
|
color: #2d2d2d;
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 16px;
|
font-size: 13px; /* Slightly smaller font */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
||||||
}
|
}
|
||||||
.category-chip:hover {
|
.category-chip:hover {
|
||||||
border-color: #d0d0d0;
|
border-color: #d0d0d0;
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
|
||||||
}
|
}
|
||||||
.category-chip.selected {
|
.category-chip.selected {
|
||||||
background: #73a585;
|
background: #73a585;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border-color: #73a585;
|
border-color: #73a585;
|
||||||
box-shadow: 0 2px 6px rgba(115, 165, 133, 0.2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-chip .chip-icon {
|
.category-chip .chip-icon {
|
||||||
width: 22px;
|
width: 16px; /* Reduced icon size */
|
||||||
height: 22px;
|
height: 16px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -105,40 +104,146 @@
|
|||||||
|
|
||||||
.inline-container {
|
.inline-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr); /* Always 4 columns */
|
||||||
gap: 12px;
|
gap: 10px; /* Spacing between grid items */
|
||||||
padding: 12px;
|
padding: 10px; /* Padding inside grid */
|
||||||
overflow-y: auto;
|
overflow-y: auto; /* Allow scrolling if items overflow */
|
||||||
}
|
}
|
||||||
.grid-container {
|
.grid-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(4, 1fr); /* Always 4 columns */
|
||||||
gap: 12px;
|
gap: 10px; /* Spacing between grid items */
|
||||||
padding: 12px;
|
padding: 10px; /* Padding inside grid */
|
||||||
max-height: calc(3 * (30vw - 24px) + 24px);
|
max-height: calc(3 * (25vw - 20px) + 20px); /* 3 items + gaps */
|
||||||
overflow-y: auto;
|
overflow-y: auto; /* Allow scrolling if items overflow */
|
||||||
padding-top: 18px;
|
padding-top: 15px;
|
||||||
height: calc(48vw - 24px);
|
height: calc(43vw - 20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-button {
|
.add-button {
|
||||||
margin: 12px;
|
margin: 10px; /* Margin around the button */
|
||||||
padding: 12px 24px;
|
padding: 10px 20px; /* Padding for the button */
|
||||||
position: absolute;
|
position: absolute; /* Optional, for styling */
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
align-self: center;
|
align-self: center; /* Center the button horizontally */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Centered container for item type list */
|
/* Legacy styles kept for ItemType grid if needed elsewhere */
|
||||||
.centered-item-type-list {
|
|
||||||
|
/* Compact centered item type list without icon tiles */
|
||||||
|
.compact-centered-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 0;
|
padding: 8px 0;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* No icon styles */
|
.compact-item-type {
|
||||||
.no-icon {
|
flex: 0 0 auto;
|
||||||
padding: 0 20px; /* Increased padding for better touch targets */
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #2d2d2d;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
margin: 0 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.compact-item-type:hover {
|
||||||
|
border-color: #d0d0d0;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
.compact-item-type.selected {
|
||||||
|
background: #73a585;
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: #73a585;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-add-item {
|
||||||
|
background: #f4f7f5;
|
||||||
|
border-color: #dfe7e2;
|
||||||
|
color: #4a6b5a;
|
||||||
|
}
|
||||||
|
.compact-add-item:hover {
|
||||||
|
background: #eaf1ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.item-type-lister {
|
||||||
|
padding: 6px 0;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-bar {
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-list {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-chip {
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-chip .chip-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-item-type {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.item-type-lister {
|
||||||
|
padding: 4px 0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-bar {
|
||||||
|
gap: 3px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-type-list {
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-chip {
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-chip .chip-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-item-type {
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import smoothScroll from "smooth-scroll-into-view-if-needed";
|
import smoothScroll from "smooth-scroll-into-view-if-needed";
|
||||||
import "./ItemTypeLister.css";
|
import "./ItemTypeLister.css";
|
||||||
import ItemType from "./ItemType";
|
|
||||||
import { createItem } from "../helpers/itemHelper.js";
|
import { createItem } from "../helpers/itemHelper.js";
|
||||||
import { getImageUrl } from "../helpers/itemHelper";
|
import { getImageUrl } from "../helpers/itemHelper";
|
||||||
import ItemLister from "./ItemLister";
|
import ItemLister from "./ItemLister";
|
||||||
@@ -62,15 +61,15 @@ const ItemTypeLister = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="item-type-lister" style={{ overflowX: isAddingNewItem ? 'hidden' : 'auto' }}>
|
<div className="item-type-lister" style={{ overflowX: isAddingNewItem ? 'hidden' : 'auto' }}>
|
||||||
<div className="centered-item-type-list">
|
<div className="compact-centered-list">
|
||||||
<div ref={newItemDivRef} className="item-type-list" style={{ display: 'inline-flex' }}>
|
<div ref={newItemDivRef} className="compact-item-type-list" style={{ display: 'inline-flex' }}>
|
||||||
{isEditMode && !isAddingNewItem && canManage && (
|
{isEditMode && !isAddingNewItem && canManage && (
|
||||||
<ItemType
|
<div
|
||||||
|
className="compact-item-type compact-add-item"
|
||||||
onClick={toggleAddNewItem}
|
onClick={toggleAddNewItem}
|
||||||
name={"buat baru"}
|
>
|
||||||
noIcon={true}
|
Buat baru
|
||||||
compact={false} // Make it larger for better touch targets
|
</div>
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{canManage && isAddingNewItem && (
|
{canManage && isAddingNewItem && (
|
||||||
@@ -91,24 +90,22 @@ const ItemTypeLister = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{itemTypes && itemTypes.length > 0 && (
|
{itemTypes && itemTypes.length > 0 && (
|
||||||
<ItemType
|
<div
|
||||||
name={"semua"}
|
className={`compact-item-type ${filterId === 0 ? 'selected' : ''}`}
|
||||||
onClick={() => onFilterChange(0)}
|
onClick={() => onFilterChange(0)}
|
||||||
selected={filterId === 0}
|
>
|
||||||
noIcon={true}
|
Semua
|
||||||
compact={false} // Make it larger for better touch targets
|
</div>
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{itemTypes && itemTypes.map((itemType) => (
|
{itemTypes && itemTypes.map((itemType) => (
|
||||||
<ItemType
|
<div
|
||||||
key={itemType.itemTypeId}
|
key={itemType.itemTypeId}
|
||||||
name={itemType.name}
|
className={`compact-item-type ${filterId === itemType.itemTypeId ? 'selected' : ''}`}
|
||||||
onClick={() => onFilterChange(itemType.itemTypeId)}
|
onClick={() => onFilterChange(itemType.itemTypeId)}
|
||||||
selected={filterId === itemType.itemTypeId}
|
>
|
||||||
noIcon={true}
|
{formatName(itemType.name)}
|
||||||
compact={false} // Make it larger for better touch targets
|
</div>
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import AccountUpdatePage from "../components/AccountUpdatePage.js";
|
|||||||
import CreateClerk from "../pages/CreateClerk"
|
import CreateClerk from "../pages/CreateClerk"
|
||||||
import CreateCafe from "../pages/CreateCafe"
|
import CreateCafe from "../pages/CreateCafe"
|
||||||
import CreateTenant from "../pages/CreateTenant"
|
import CreateTenant from "../pages/CreateTenant"
|
||||||
import TablesPage from "./TablesPage.js";
|
import IdentifyCafeModal from "./IdentifyCafeModal.js";
|
||||||
import PaymentOptions from "./PaymentOptions.js";
|
import PaymentOptions from "./PaymentOptions.js";
|
||||||
import Transaction from "../pages/Transaction";
|
import Transaction from "../pages/Transaction";
|
||||||
import Transaction_item from "../pages/Transaction_item";
|
import Transaction_item from "../pages/Transaction_item";
|
||||||
@@ -77,7 +77,7 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
|
|||||||
if(modalContent == '') handleOverlayClick();
|
if(modalContent == '') handleOverlayClick();
|
||||||
return (
|
return (
|
||||||
<div key={updateKey} onClick={handleOverlayClick} className={styles.modalOverlay}>
|
<div key={updateKey} onClick={handleOverlayClick} className={styles.modalOverlay}>
|
||||||
<div className={styles.modalContent} onClick={handleContentClick}>
|
<div className={`${styles.modalContent} ${(modalContent === 'edit_tables' || modalContent === 'payment_option' || modalContent === 'create_clerk') ? styles.modalContentWide : ''}`} onClick={handleContentClick}>
|
||||||
|
|
||||||
{modalContent === "edit_account" && <AccountUpdatePage user={user} />}
|
{modalContent === "edit_account" && <AccountUpdatePage user={user} />}
|
||||||
{modalContent === "reset-password" && <ResetPassword />}
|
{modalContent === "reset-password" && <ResetPassword />}
|
||||||
@@ -86,7 +86,7 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
|
|||||||
{modalContent === "create_clerk" && <CreateClerk shopId={shop.cafeId} />}
|
{modalContent === "create_clerk" && <CreateClerk shopId={shop.cafeId} />}
|
||||||
{modalContent === "create_kedai" && <CreateCafe shopId={shop.cafeId} />}
|
{modalContent === "create_kedai" && <CreateCafe shopId={shop.cafeId} />}
|
||||||
{modalContent === "create_tenant" && <CreateTenant shopId={shop.cafeId} />}
|
{modalContent === "create_tenant" && <CreateTenant shopId={shop.cafeId} />}
|
||||||
{modalContent === "edit_tables" && <TablesPage shop={shop} />}
|
{modalContent === "edit_tables" && <IdentifyCafeModal shop={shop} />}
|
||||||
{modalContent === "new_transaction" && (
|
{modalContent === "new_transaction" && (
|
||||||
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} setModal={setModal}/>
|
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} setModal={setModal}/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 1000;
|
z-index: 9999;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +23,12 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
animation: modalAppear 0.3s ease-out;
|
animation: modalAppear 0.3s ease-out;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10000; /* ensure above any page overlays */
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContentWide {
|
||||||
|
max-width: 920px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes modalAppear {
|
@keyframes modalAppear {
|
||||||
@@ -178,6 +184,31 @@
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bannerRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerSuccess {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerError {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
.formButton {
|
.formButton {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import QrScanner from "qr-scanner"; // Import qr-scanner
|
import QrScanner from "qr-scanner"; // Import qr-scanner
|
||||||
import { getImageUrl } from "../helpers/itemHelper";
|
import { getImageUrl } from "../helpers/itemHelper";
|
||||||
import {
|
import { getCafe, saveCafeDetails } from "../helpers/cafeHelpers";
|
||||||
getCafe,
|
|
||||||
saveCafeDetails,
|
|
||||||
setConfirmationStatus,
|
|
||||||
setOpenBillAvailability
|
|
||||||
} from "../helpers/cafeHelpers";
|
|
||||||
import Switch from "react-switch"; // Import the Switch component
|
import Switch from "react-switch"; // Import the Switch component
|
||||||
|
import styles from "./PaymentOptions.module.css";
|
||||||
|
|
||||||
const SetPaymentQr = ({ shopId,
|
const SetPaymentQr = ({ shopId, qrCodeUrl }) => {
|
||||||
qrCodeUrl }) => {
|
const [qrPosition, setQrPosition] = useState([50, 50]); // legacy kept for API compatibility
|
||||||
const [qrPosition, setQrPosition] = useState([50, 50]);
|
const [qrSize, setQrSize] = useState(50); // legacy kept for API compatibility
|
||||||
const [qrSize, setQrSize] = useState(50);
|
|
||||||
const [qrPayment, setQrPayment] = useState();
|
const [qrPayment, setQrPayment] = useState();
|
||||||
const [qrPaymentFile, setQrPaymentFile] = useState();
|
const [qrPaymentFile, setQrPaymentFile] = useState();
|
||||||
const [qrCodeDetected, setQrCodeDetected] = useState(false);
|
const [qrCodeDetected, setQrCodeDetected] = useState(false);
|
||||||
@@ -26,6 +21,9 @@ const SetPaymentQr = ({ shopId,
|
|||||||
|
|
||||||
const [isConfigQRIS, setIsConfigQRIS] = useState(false);
|
const [isConfigQRIS, setIsConfigQRIS] = useState(false);
|
||||||
const [isOpenBillAvailable, setIsOpenBillAvailable] = useState(false);
|
const [isOpenBillAvailable, setIsOpenBillAvailable] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCafe = async () => {
|
const fetchCafe = async () => {
|
||||||
@@ -104,6 +102,8 @@ const SetPaymentQr = ({ shopId,
|
|||||||
|
|
||||||
// Save cafe details
|
// Save cafe details
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
setSaveStatus(null);
|
||||||
let qrPaymentFileCache;
|
let qrPaymentFileCache;
|
||||||
console.log(qrPaymentFile)
|
console.log(qrPaymentFile)
|
||||||
if(qrPaymentFile != null)
|
if(qrPaymentFile != null)
|
||||||
@@ -120,107 +120,101 @@ const SetPaymentQr = ({ shopId,
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await saveCafeDetails(cafe.cafeId, details);
|
const response = await saveCafeDetails(cafe.cafeId, details);
|
||||||
|
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0);
|
||||||
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0); // Update state after saving
|
setIsQRISavailable(response.isQRISavailable ? 1 : 0);
|
||||||
setIsQRISavailable(response.isQRISavailable ? 1 : 0); // Update state after saving
|
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0);
|
||||||
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0); // Update state after saving
|
setSaveStatus('success');
|
||||||
|
|
||||||
console.log("Cafe details saved:", response);
|
console.log("Cafe details saved:", response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving cafe details:", error);
|
console.error("Error saving cafe details:", error);
|
||||||
|
setSaveStatus('error');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copyQrData = async () => {
|
||||||
|
if (!qrCodeData) return;
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(qrCodeData);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(()=>setCopied(false), 1200);
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.container}>
|
<div className={styles.container}>
|
||||||
<h3 style={styles.title}>Konfigurasi pembayaran</h3>
|
<h3 className={styles.title}>Konfigurasi pembayaran</h3>
|
||||||
|
|
||||||
<div style={styles.switchContainer}>
|
<div className={styles.section}>
|
||||||
<p style={styles.uploadMessage}>
|
<div className={styles.sectionHeader}>
|
||||||
Pembayaran QRIS.
|
<div>
|
||||||
</p>
|
<div className={styles.sectionTitle}>Pembayaran QRIS</div>
|
||||||
|
<div className={styles.sectionDesc}>Aktifkan agar pelanggan dapat membayar via QRIS. Kasir tetap perlu verifikasi rekening.</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
onChange={(checked) => setIsQRISavailable(checked ? 1 : 0)}
|
||||||
|
checked={isQRISavailable === 1}
|
||||||
|
offColor="#888"
|
||||||
|
onColor="#4CAF50"
|
||||||
|
uncheckedIcon={false}
|
||||||
|
checkedIcon={false}
|
||||||
|
height={25}
|
||||||
|
width={50}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isConfigQRIS ?
|
<div className={styles.row}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`${styles.button} ${styles.configButton}`}
|
||||||
|
onClick={() => isQRISavailable === 1 && setIsConfigQRIS(true)}
|
||||||
|
disabled={isQRISavailable !== 1}
|
||||||
|
>
|
||||||
|
Konfigurasi QRIS
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isConfigQRIS && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
id="qr-code-container"
|
id="qr-code-container"
|
||||||
ref={qrCodeContainerRef}
|
ref={qrCodeContainerRef}
|
||||||
|
className={styles.imageBox}
|
||||||
onClick={() => qrPaymentInputRef.current.click()}
|
onClick={() => qrPaymentInputRef.current.click()}
|
||||||
style={{
|
style={{ backgroundImage: `url(${qrPayment})` }}
|
||||||
...styles.qrCodeContainer,
|
|
||||||
backgroundImage: `url(${qrPayment})`,
|
|
||||||
backgroundPosition: "center",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
backgroundSize: "contain",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input
|
<input type="file" accept="image/*" ref={qrPaymentInputRef} style={{ display: 'none' }} onChange={handleFileChange} />
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
ref={qrPaymentInputRef}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
onChange={handleFileChange}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.uploadMessage}>
|
<div className={styles.smallNote}>Klik area untuk unggah/ganti gambar QR</div>
|
||||||
<p>Klik untuk ganti background</p>
|
<div className={styles.detectRow}>
|
||||||
|
<div className={`${styles.tag} ${qrCodeDetected ? styles.tagOk : styles.tagBad}`}>
|
||||||
|
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? 'QR terdeteksi' : 'Tidak ada QR terdeteksi'}
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.resultMessage}>
|
<button className={styles.button} onClick={() => qrPaymentInputRef.current.click()}>{qrPayment ? 'Ganti' : 'Unggah'}</button>
|
||||||
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? <p>QR terdeteksi</p> : <p>Tidak ada qr terdeteksi</p>}
|
|
||||||
{qrCodeDetected && qrPayment !== 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSPsNr0TPq8dHT3nBwDQ6OHQQTrqzVFoeBOmuWfgyErrLbJi6f6CnnYhpNHEvkJ_2X-kyI&usqp=CAU' ? <div
|
|
||||||
onClick={() => qrPaymentInputRef.current.click()} style={styles.uploadButton}>Ganti</div> : <div
|
|
||||||
onClick={() => qrPaymentInputRef.current.click()} style={styles.uploadButton}>Unggah</div>}
|
|
||||||
</div>
|
</div>
|
||||||
|
{qrCodeDetected && (
|
||||||
<div onClick={() => setIsConfigQRIS(false)}
|
<div className={styles.copyRow}>
|
||||||
|
<input className={styles.linkField} readOnly value={qrCodeData || ''} />
|
||||||
style={{
|
<button className={styles.button} onClick={copyQrData}>{copied ? 'Disalin' : 'Salin'}</button>
|
||||||
...styles.qrisConfigButton,
|
|
||||||
width: '100%',
|
|
||||||
marginLeft: "0",
|
|
||||||
}}
|
|
||||||
>Terapkan</div>
|
|
||||||
</>
|
|
||||||
:
|
|
||||||
<>
|
|
||||||
<p style={styles.description}>
|
|
||||||
Aktifkan fitur agar pelanggan dapat menggunakan opsi pembayaran QRIS, namun kasir anda perlu memeriksa rekening untuk memastikan pembayaran.
|
|
||||||
</p>
|
|
||||||
<div style={{ display: 'flex' }}>
|
|
||||||
<Switch
|
|
||||||
onChange={(checked) => setIsQRISavailable(checked ? 1 : 0)}
|
|
||||||
checked={isQRISavailable === 1} // Convert to boolean
|
|
||||||
offColor="#888"
|
|
||||||
onColor="#4CAF50"
|
|
||||||
uncheckedIcon={false}
|
|
||||||
checkedIcon={false}
|
|
||||||
height={25}
|
|
||||||
width={50}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
onClick={() => setIsConfigQRIS(true)}
|
|
||||||
style={{
|
|
||||||
...styles.qrisConfigButton,
|
|
||||||
backgroundColor: isQRISavailable == 1 ? styles.qrisConfigButton.backgroundColor : 'gray',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Konfigurasi QRIS
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.actionsRight}>
|
||||||
|
<button className={`${styles.button} ${styles.primary}`} onClick={() => setIsConfigQRIS(false)}>Terapkan</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionHeader}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.sectionTitle}>Open bill</div>
|
||||||
|
<div className={styles.sectionDesc}>Izinkan pelanggan menambah pesanan dalam satu sesi dan bayar di akhir.</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.switchContainer}>
|
|
||||||
<p style={styles.uploadMessage}>
|
|
||||||
Open bill
|
|
||||||
</p>
|
|
||||||
<p style={styles.description}>
|
|
||||||
Aktifkan fitur agar pelanggan dapat menambahkan pesanan selama sesi berlangsung tanpa perlu melakukan transaksi baru dan hanya membayar di akhir.
|
|
||||||
</p>
|
|
||||||
<Switch
|
<Switch
|
||||||
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
|
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
|
||||||
checked={isOpenBillAvailable === 1} // Convert to boolean
|
checked={isOpenBillAvailable === 1}
|
||||||
offColor="#888"
|
offColor="#888"
|
||||||
onColor="#4CAF50"
|
onColor="#4CAF50"
|
||||||
uncheckedIcon={false}
|
uncheckedIcon={false}
|
||||||
@@ -229,17 +223,17 @@ const SetPaymentQr = ({ shopId,
|
|||||||
width={50}
|
width={50}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={styles.switchContainer}>
|
<div className={styles.section}>
|
||||||
<p style={styles.uploadMessage}>
|
<div className={styles.sectionHeader}>
|
||||||
Pengecekan ganda
|
<div>
|
||||||
</p>
|
<div className={styles.sectionTitle}>Pengecekan ganda</div>
|
||||||
<p style={styles.description}>
|
<div className={styles.sectionDesc}>Kasir memeriksa kembali ketersediaan item sebelum pembayaran.</div>
|
||||||
Nyalakan agar kasir memeriksa kembali ketersediaan produk sebelum pelanggan membayar.
|
</div>
|
||||||
</p>
|
|
||||||
<Switch
|
<Switch
|
||||||
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
|
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
|
||||||
checked={isNeedConfirmationState === 1} // Convert to boolean
|
checked={isNeedConfirmationState === 1}
|
||||||
offColor="#888"
|
offColor="#888"
|
||||||
onColor="#4CAF50"
|
onColor="#4CAF50"
|
||||||
uncheckedIcon={false}
|
uncheckedIcon={false}
|
||||||
@@ -248,112 +242,19 @@ const SetPaymentQr = ({ shopId,
|
|||||||
width={50}
|
width={50}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={styles.buttonContainer}>
|
<div className={styles.footer}>
|
||||||
<button onClick={handleSave} style={styles.saveButton}>
|
<div>
|
||||||
Simpan
|
{saveStatus === 'success' && <span className={`${styles.banner} ${styles.bannerSuccess}`}>Simpan berhasil</span>}
|
||||||
|
{saveStatus === 'error' && <span className={`${styles.banner} ${styles.bannerError}`}>Gagal menyimpan</span>}
|
||||||
|
</div>
|
||||||
|
<button className={`${styles.button} ${styles.primary}`} onClick={handleSave} disabled={saving}>
|
||||||
|
{saving ? 'Menyimpan…' : 'Simpan'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Styles
|
|
||||||
const styles = {
|
|
||||||
container: {
|
|
||||||
position: 'relative',
|
|
||||||
overflowY: 'auto',
|
|
||||||
overflowX: 'hidden',
|
|
||||||
maxHeight: '80vh',
|
|
||||||
width: '100%',
|
|
||||||
backgroundColor: "white",
|
|
||||||
padding: "20px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
|
|
||||||
textAlign: "center", // Center text and children
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
marginBottom: "20px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
},
|
|
||||||
qrCodeContainer: {
|
|
||||||
backgroundColor: '#999999',
|
|
||||||
borderRadius: '20px',
|
|
||||||
position: "relative",
|
|
||||||
width: "100%",
|
|
||||||
height: "200px",
|
|
||||||
backgroundSize: "contain",
|
|
||||||
overflow: "hidden",
|
|
||||||
margin: "0 auto", // Center the QR code container
|
|
||||||
marginTop: '10px'
|
|
||||||
},
|
|
||||||
uploadMessage: {
|
|
||||||
fontWeight: 600,
|
|
||||||
textAlign: "left",
|
|
||||||
},
|
|
||||||
qrisConfigButton: {
|
|
||||||
borderRadius: '15px',
|
|
||||||
backgroundColor: '#28a745',
|
|
||||||
width: '144px',
|
|
||||||
textAlign: 'center',
|
|
||||||
color: 'white',
|
|
||||||
lineHeight: '24px',
|
|
||||||
marginLeft: '14px',
|
|
||||||
},
|
|
||||||
uploadButton: {
|
|
||||||
paddingRight: '10px',
|
|
||||||
backgroundColor: '#28a745',
|
|
||||||
borderRadius: '30px',
|
|
||||||
color: 'white',
|
|
||||||
fontWeight: 700,
|
|
||||||
height: '36px',
|
|
||||||
lineHeight: '36px',
|
|
||||||
paddingLeft: '10px',
|
|
||||||
paddingHeight: '10px',
|
|
||||||
},
|
|
||||||
resultMessage: {
|
|
||||||
marginTop: "-24px",
|
|
||||||
textAlign: "left",
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'space-between'
|
|
||||||
},
|
|
||||||
buttonContainer: {
|
|
||||||
marginTop: "20px",
|
|
||||||
textAlign: "left",
|
|
||||||
},
|
|
||||||
saveButton: {
|
|
||||||
padding: "10px 20px",
|
|
||||||
fontSize: "16px",
|
|
||||||
backgroundColor: "#28a745",
|
|
||||||
color: "#fff",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "30px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "background-color 0.3s",
|
|
||||||
},
|
|
||||||
switchContainer: {
|
|
||||||
textAlign: "left",
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
margin: "10px 0",
|
|
||||||
fontSize: "14px",
|
|
||||||
color: "#666",
|
|
||||||
},
|
|
||||||
sliderContainer: {
|
|
||||||
marginBottom: "20px",
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
display: "block",
|
|
||||||
marginBottom: "10px",
|
|
||||||
},
|
|
||||||
sliderWrapper: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
flex: "1",
|
|
||||||
margin: "0 10px",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SetPaymentQr;
|
export default SetPaymentQr;
|
||||||
|
|||||||
171
src/components/PaymentOptions.module.css
Normal file
171
src/components/PaymentOptions.module.css
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
/* PaymentOptions.module.css */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-height: 80vh;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionDesc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: #28a745;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.configButton {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageBox {
|
||||||
|
background-color: #999999;
|
||||||
|
border-radius: 12px;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 10px 0 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smallNote {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detectRow {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagOk {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagBad {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkField {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionsRight {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerSuccess {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerError {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.container { padding: 16px; }
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -17,15 +17,16 @@ code {
|
|||||||
|
|
||||||
/* Ensure proper scrolling behavior */
|
/* Ensure proper scrolling behavior */
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
min-height: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
min-height: 100%;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
import styles from "./Invoice.module.css";
|
import styles from "./Invoice.module.css";
|
||||||
|
import cartStyles from "./CartPage.module.css";
|
||||||
import { useParams } from "react-router-dom"; // Changed from useSearchParams to useLocation
|
import { useParams } from "react-router-dom"; // Changed from useSearchParams to useLocation
|
||||||
import { ThreeDots, ColorRing } from "react-loader-spinner";
|
import { ThreeDots, ColorRing } from "react-loader-spinner";
|
||||||
|
|
||||||
@@ -384,7 +385,13 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
|
|||||||
return (
|
return (
|
||||||
<div className={styles.Invoice} style={{ height: (getItemsByCafeId(shopId).length > 0 ? '' : '100vh'), minHeight: (getItemsByCafeId(shopId).length > 0 ? '100vh' : '') }}>
|
<div className={styles.Invoice} style={{ height: (getItemsByCafeId(shopId).length > 0 ? '' : '100vh'), minHeight: (getItemsByCafeId(shopId).length > 0 ? '100vh' : '') }}>
|
||||||
|
|
||||||
<div onClick={goToShop} style={{ marginLeft: '22px', marginTop: '49px', marginRight: '10px', display: 'flex', flexWrap: 'nowrap', alignItems: 'center', fontSize: '25px' }} ><svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 512 512"><path d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>Keranjang</div>
|
<div className={cartStyles.header}>
|
||||||
|
<div className={cartStyles.backBtn} onClick={goToShop} aria-label="Kembali">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 512 512"><path fill="#fff" d="M48,256c0,114.87,93.13,208,208,208s208-93.13,208-208S370.87,48,256,48,48,141.13,48,256Zm212.65-91.36a16,16,0,0,1,.09,22.63L208.42,240H342a16,16,0,0,1,0,32H208.42l52.32,52.73A16,16,0,1,1,238,347.27l-79.39-80a16,16,0,0,1,0-22.54l79.39-80A16,16,0,0,1,260.65,164.64Z" /></svg>
|
||||||
|
</div>
|
||||||
|
<div className={cartStyles.title}>Keranjang</div>
|
||||||
|
</div>
|
||||||
|
<div className={cartStyles.container}>
|
||||||
|
|
||||||
{(transactionData == null && getItemsByCafeId(shopId).length < 1) ?
|
{(transactionData == null && getItemsByCafeId(shopId).length < 1) ?
|
||||||
<div style={{ height: '75vh', display: 'flex', justifyContent: 'center', flexDirection: 'column', alignContent: 'center', alignItems: 'center' }}>
|
<div style={{ height: '75vh', display: 'flex', justifyContent: 'center', flexDirection: 'column', alignContent: 'center', alignItems: 'center' }}>
|
||||||
@@ -443,20 +450,20 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.NoteContainer}>
|
|
||||||
<span>Catatan :</span>
|
|
||||||
<span></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div className={styles.NoteContainer}>
|
{getItemsByCafeId(shopId).length > 0 && (
|
||||||
|
<div className={`${styles.RoundedRectangle} ${cartStyles.sectionCard}`}>
|
||||||
|
<div className={cartStyles.sectionTitle}>Catatan Untuk Kasir</div>
|
||||||
|
<div className={cartStyles.divider}></div>
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className={styles.NoteInput}
|
className={styles.NoteInput}
|
||||||
placeholder="Tambahkan catatan..."
|
placeholder="Contoh: tanpa gula, ekstra es, dsb."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
}
|
|
||||||
|
|
||||||
{transactionData &&
|
{transactionData &&
|
||||||
<div className={styles.RoundedRectangle} style={{ backgroundColor: '#c3c3c3', fontSize: '15px', display: 'flex', justifyContent: 'space-between' }}>
|
<div className={styles.RoundedRectangle} style={{ backgroundColor: '#c3c3c3', fontSize: '15px', display: 'flex', justifyContent: 'space-between' }}>
|
||||||
@@ -576,5 +583,6 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/pages/CartPage.module.css
Normal file
68
src/pages/CartPage.module.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 5;
|
||||||
|
background: var(--brand-sage, #6B8F71);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.backBtn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border: 1px solid rgba(255,255,255,0.25);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionCard {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||||
|
padding: 12px;
|
||||||
|
margin: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #2d2d2d;
|
||||||
|
margin: 4px 0 10px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rowBetween {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #eee;
|
||||||
|
margin: 8px 0 12px 0;
|
||||||
|
}
|
||||||
@@ -1,119 +1,153 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { createClerks } from '../helpers/userHelpers'; // Adjust the import path as needed
|
import { createClerks, getClerks } from '../helpers/userHelpers';
|
||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
import styles from './CreateClerk.module.css';
|
||||||
|
|
||||||
const CreateClerk = ({ shopId }) => {
|
const CreateClerk = ({ shopId }) => {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [message, setMessage] = useState('');
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [banner, setBanner] = useState(null); // { type: 'success'|'error', text: string }
|
||||||
|
const [clerks, setClerks] = useState([]);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.search);
|
||||||
const cafeIdParam = queryParams.get("cafeId");
|
const cafeIdParam = queryParams.get("cafeId");
|
||||||
|
const effectiveShopId = useMemo(()=> shopId || cafeIdParam, [shopId, cafeIdParam]);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
const load = async ()=>{
|
||||||
|
if (!effectiveShopId) return;
|
||||||
|
try {
|
||||||
|
const data = await getClerks(effectiveShopId);
|
||||||
|
if (data && Array.isArray(data)) setClerks(data);
|
||||||
|
} catch (e) {}
|
||||||
|
};
|
||||||
|
load();
|
||||||
|
}, [effectiveShopId]);
|
||||||
|
|
||||||
|
const generatePassword = () => {
|
||||||
|
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%';
|
||||||
|
let pwd = '';
|
||||||
|
for (let i = 0; i < 12; i++) pwd += chars[Math.floor(Math.random()*chars.length)];
|
||||||
|
setPassword(pwd);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (event) => {
|
const handleSubmit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setLoading(true);
|
setSaving(true);
|
||||||
setMessage('');
|
setBanner(null);
|
||||||
|
|
||||||
// Basic validation
|
// Basic validation
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
setMessage('Username and password are required');
|
setBanner({ type: 'error', text: 'Username dan password wajib diisi' });
|
||||||
setLoading(false);
|
setSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (username.length < 3) {
|
||||||
|
setBanner({ type: 'error', text: 'Username minimal 3 karakter' });
|
||||||
|
setSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (password.length < 6) {
|
||||||
|
setBanner({ type: 'error', text: 'Password minimal 6 karakter' });
|
||||||
|
setSaving(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const create = await createClerks(shopId || cafeIdParam, username, password);
|
const create = await createClerks(effectiveShopId, username, password);
|
||||||
|
if (create) {
|
||||||
if (create) setMessage('Clerk created successfully');
|
setBanner({ type: 'success', text: 'Kasir berhasil ditambahkan' });
|
||||||
else setMessage('Failed to create clerk');
|
// Refresh list
|
||||||
|
try {
|
||||||
|
const data = await getClerks(effectiveShopId);
|
||||||
|
if (data && Array.isArray(data)) setClerks(data);
|
||||||
|
} catch {}
|
||||||
|
// Clear form
|
||||||
|
setUsername('');
|
||||||
|
setPassword('');
|
||||||
|
} else {
|
||||||
|
setBanner({ type: 'error', text: 'Gagal menambahkan kasir' });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMessage('Error creating clerk');
|
setBanner({ type: 'error', text: 'Terjadi kesalahan saat menambahkan kasir' });
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.container}>
|
<div className={styles.container}>
|
||||||
<h2 style={styles.header}>Tambah Kasir</h2>
|
<div className={styles.header}>
|
||||||
<form onSubmit={handleSubmit} style={styles.form}>
|
<h2 className={styles.title}>Tambah Kasir</h2>
|
||||||
|
{banner && (
|
||||||
|
<div className={`${styles.banner} ${banner.type === 'success' ? styles.bannerSuccess : styles.bannerError}`}>
|
||||||
|
{banner.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Form kasir baru</div>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className={styles.formRow}>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Username</label>
|
||||||
<input
|
<input
|
||||||
|
className={styles.input}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Username"
|
placeholder="kasir_baru"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
style={styles.input}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>Password</label>
|
||||||
|
<div className={styles.pwdRow}>
|
||||||
<input
|
<input
|
||||||
type="password"
|
className={styles.input}
|
||||||
placeholder="Password"
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
placeholder="••••••"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
style={styles.input}
|
|
||||||
/>
|
/>
|
||||||
<button type="submit" style={styles.button} disabled={loading}>
|
<button type="button" className={styles.button} onClick={()=>setShowPassword(!showPassword)}>
|
||||||
{loading ? 'Creating...' : 'Create Clerk'}
|
{showPassword ? 'Sembunyikan' : 'Tampilkan'}
|
||||||
</button>
|
</button>
|
||||||
{message && (
|
<button type="button" className={styles.button} onClick={generatePassword}>
|
||||||
<p style={{ ...styles.message, color: message.includes('success') ? 'green' : 'red' }}>
|
Generate
|
||||||
{message}
|
</button>
|
||||||
</p>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<button className={`${styles.button} ${styles.primary}`} type="submit" disabled={saving}>
|
||||||
|
{saving ? 'Menambahkan…' : 'Tambah Kasir'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>Daftar kasir</div>
|
||||||
|
<div className={styles.list}>
|
||||||
|
{clerks && clerks.length > 0 ? (
|
||||||
|
clerks.map((c) => (
|
||||||
|
<div key={c.user_id || c.username} className={`${styles.listItem} ${styles.muted}`}>
|
||||||
|
<span>@{c.username}</span>
|
||||||
|
{/* Tempatkan tombol hapus jika API tersedia */}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className={styles.listItem}>Belum ada kasir</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Basic styling to make it mobile-friendly with a white background
|
|
||||||
const styles = {
|
|
||||||
container: {
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: '350px',
|
|
||||||
margin: '0 auto',
|
|
||||||
padding: '20px',
|
|
||||||
boxShadow: '0 4px 10px rgba(0, 0, 0, 0.1)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
textAlign: 'center',
|
|
||||||
marginBottom: '20px',
|
|
||||||
fontSize: '20px',
|
|
||||||
color: '#333',
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '15px',
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
padding: '12px',
|
|
||||||
fontSize: '16px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid #ccc',
|
|
||||||
width: '100%',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
backgroundColor: '#f9f9f9',
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
padding: '12px',
|
|
||||||
fontSize: '16px',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: 'none',
|
|
||||||
backgroundColor: '#28a745',
|
|
||||||
color: 'white',
|
|
||||||
cursor: 'pointer',
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
message: {
|
|
||||||
textAlign: 'center',
|
|
||||||
marginTop: '10px',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateClerk;
|
export default CreateClerk;
|
||||||
|
|||||||
137
src/pages/CreateClerk.module.css
Normal file
137
src/pages/CreateClerk.module.css
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/* CreateClerk.module.css */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: #fff;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 720px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionTitle {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formRow {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwdRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
background: #28a745;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerSuccess {
|
||||||
|
color: #155724;
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerError {
|
||||||
|
color: #721c24;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.formRow { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
76
src/pages/StickyCartBar.module.css
Normal file
76
src/pages/StickyCartBar.module.css
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/* StickyCartBar.module.css */
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 40px;
|
||||||
|
z-index: 120; /* above items, below modal */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
pointer-events: none; /* allow buttons to define interaction */
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 980px;
|
||||||
|
padding: 0 12px;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainBtn {
|
||||||
|
flex: 1;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--brand-primary, #73a585);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 6px 18px rgba(115,165,133,0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
white-space: nowrap;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cartIcon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyBtn {
|
||||||
|
width: 48px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--brand-primary, #73a585);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 6px 18px rgba(115,165,133,0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.mainBtn { height: 42px; }
|
||||||
|
.historyBtn { height: 42px; width: 46px; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-container {
|
.image-container {
|
||||||
@@ -15,6 +17,11 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fileInput {
|
.fileInput {
|
||||||
@@ -24,38 +31,134 @@
|
|||||||
.circular-image {
|
.circular-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcoming-text {
|
.welcoming-text {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.get-started-button {
|
.get-started-button {
|
||||||
padding: 10px 20px;
|
padding: 12px 24px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
background-color: #007bff; /* Bootstrap primary color */
|
background-color: #28a745; /* Bootstrap primary color */
|
||||||
color: white;
|
color: white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
font-family: "Plus Jakarta Sans", sans-serif;
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.get-started-button:hover {
|
.get-started-button:hover {
|
||||||
background-color: #0056b3; /* Darker shade on hover */
|
background-color: #218838; /* Darker shade on hover */
|
||||||
|
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-started-button:active {
|
||||||
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fullscreen styles */
|
/* Fullscreen styles */
|
||||||
.fullscreen {
|
.fullscreen {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 0;
|
||||||
left: 50%;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
transform: translate(-50%, -50%);
|
z-index: 1000;
|
||||||
z-index: 300;
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .image-container {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .welcoming-text {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .get-started-button {
|
||||||
|
padding: 16px 32px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.welcome-page {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcoming-text {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-started-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .image-container {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .welcoming-text {
|
||||||
|
font-size: 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .get-started-button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.image-container {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcoming-text {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.get-started-button {
|
||||||
|
padding: 8px 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .image-container {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .welcoming-text {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen .get-started-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,15 @@ const WelcomePage = ({
|
|||||||
const handleImageClick = () => {
|
const handleImageClick = () => {
|
||||||
fileInputRef.current.click();
|
fileInputRef.current.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SVG Icon for camera
|
||||||
|
const CameraIcon = () => (
|
||||||
|
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M21 19H3a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<circle cx="12" cy="13" r="4" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`welcome-page ${isFullscreen ? "fullscreen" : ""}`} // Corrected the className syntax
|
className={`welcome-page ${isFullscreen ? "fullscreen" : ""}`} // Corrected the className syntax
|
||||||
@@ -29,10 +38,13 @@ const WelcomePage = ({
|
|||||||
className="image-container"
|
className="image-container"
|
||||||
>
|
>
|
||||||
{!isFullscreen &&
|
{!isFullscreen &&
|
||||||
<div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute'}}>
|
<div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer'}}>
|
||||||
<h1 style={{textAlign:'left'}}>
|
<div style={{textAlign: 'center', color: 'white'}}>
|
||||||
{image ? "Click To Change Image" : "Click To Add Image"}
|
<CameraIcon />
|
||||||
|
<h1 style={{textAlign:'center', fontSize: '16px', margin: '10px 0 0 0'}}>
|
||||||
|
{image ? "Klik untuk mengganti gambar" : "Klik untuk menambahkan gambar"}
|
||||||
</h1>
|
</h1>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef} style={{display: 'none', width:'100%', height: '100%'}}type="file" accept="image/*" onChange={onImageChange} />
|
ref={fileInputRef} style={{display: 'none', width:'100%', height: '100%'}}type="file" accept="image/*" onChange={onImageChange} />
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +55,7 @@ const WelcomePage = ({
|
|||||||
{welcomingText}
|
{welcomingText}
|
||||||
</h1>
|
</h1>
|
||||||
<button className="get-started-button" onClick={onGetStarted}>
|
<button className="get-started-button" onClick={onGetStarted}>
|
||||||
Get Started
|
Mulai
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,56 +1,395 @@
|
|||||||
/* WelcomePageEditor.css */
|
/* WelcomePageEditor.css */
|
||||||
|
|
||||||
.welcome-page-editor {
|
.welcome-page-editor {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
width: 100%;
|
||||||
justify-content: center;
|
height: 100vh;
|
||||||
padding: 20px;
|
background-color: #ffffff;
|
||||||
border: 1px solid #ddd;
|
border-radius: 16px;
|
||||||
border-radius: 8px;
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
background-color: rgb(207, 207, 207);
|
overflow: hidden; /* contain scroll inside editor-content */
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
.editor-header {
|
||||||
margin-bottom: 20px;
|
padding: 24px;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-title {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="file"] {
|
.editor-content {
|
||||||
margin-bottom: 20px;
|
display: flex;
|
||||||
}
|
flex: 1; /* take remaining height */
|
||||||
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100px; /* Adjust as needed */
|
overflow-y: auto; /* enable vertical scroll in editor-content */
|
||||||
padding: 10px;
|
overflow-x: hidden;
|
||||||
border: 1px solid #ccc;
|
justify-content: center;
|
||||||
border-radius: 4px;
|
align-items: flex-start;
|
||||||
resize: none;
|
padding: 16px;
|
||||||
margin-bottom: 20px;
|
box-sizing: border-box;
|
||||||
font-size: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea:focus {
|
.config-panel {
|
||||||
border-color: #007bff; /* Highlight border color on focus */
|
width: 100%;
|
||||||
|
max-width: 960px;
|
||||||
|
padding: 24px;
|
||||||
|
border-right: none;
|
||||||
|
background-color: #fafafa;
|
||||||
|
overflow-y: visible;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
border-color: #28a745;
|
||||||
|
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
.color-input {
|
||||||
margin-bottom: 20px;
|
height: 45px;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-input {
|
||||||
|
min-height: 80px;
|
||||||
|
resize: vertical;
|
||||||
|
font-family: "Plus Jakarta Sans", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: none;
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(40, 167, 69, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
box-shadow: 0 4px 10px rgba(40, 167, 69, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button:disabled {
|
||||||
|
background-color: #a1a1a1;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-button:hover {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
border-top: 2px solid transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-bottom: 1px solid #e6e6e6;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-preview {
|
.welcome-preview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
max-width: 400px;
|
||||||
border: 1px dashed #ccc; /* Preview border style */
|
border-radius: 16px;
|
||||||
border-radius: 8px;
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
padding: 20px;
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-toggle {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-toggle:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.9);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top actions row */
|
||||||
|
.top-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.top-actions .preview-button {
|
||||||
|
width: auto;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme & color redesign */
|
||||||
|
.hex-input {
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatches {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-card {
|
||||||
|
margin-top: 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-inner {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-preview {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.theme-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-preview {
|
||||||
|
margin-top: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-preview-content {
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 320px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #ffffff; /* Background for preview area */
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.editor-content {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel {
|
||||||
|
width: 100%;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel {
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.editor-header {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-preview-content {
|
||||||
|
min-height: 260px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
|
|||||||
const [isWelcomePageActive, setIsWelcomePageActive] = useState(false); // State for the switch
|
const [isWelcomePageActive, setIsWelcomePageActive] = useState(false); // State for the switch
|
||||||
const [loading, setLoading] = useState(false); // Loading state
|
const [loading, setLoading] = useState(false); // Loading state
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
|
|
||||||
// Load existing welcome page configuration when component mounts
|
// Load existing welcome page configuration when component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -51,6 +52,21 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
|
|||||||
setTextColor(e.target.value);
|
setTextColor(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extra handlers for HEX input fields
|
||||||
|
const handleBackgroundHexInput = (e) => {
|
||||||
|
let v = e.target.value;
|
||||||
|
if (!v.startsWith('#')) v = '#' + v.replace(/#/g, '');
|
||||||
|
if (v.length > 7) v = v.slice(0, 7);
|
||||||
|
setBackgroundColor(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTextHexInput = (e) => {
|
||||||
|
let v = e.target.value;
|
||||||
|
if (!v.startsWith('#')) v = '#' + v.replace(/#/g, '');
|
||||||
|
if (v.length > 7) v = v.slice(0, 7);
|
||||||
|
setTextColor(v);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
@@ -72,85 +88,236 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SVG Icons
|
||||||
|
const CoffeeIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M17 8h1.5a2.5 2.5 0 0 1 0 5H15m0-5H13m-1 0H6a3 3 0 0 0-3 3v1.5a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3V11a3 3 0 0 0-3-3h-5.5m-1 0V5a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1V2m-5 0v1a1 1 0 0 0-1 1v1m0 0v1a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PaletteIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14 3v4a1 1 0 0 0 1 1h4m-9 4a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm6 10v-2a3 3 0 0 0-3-3h-4a3 3 0 0 0-3 3v2h10ZM16 21v-2a3 3 0 0 0-3-3h-2a3 3 0 0 0-3 3v2h8Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TypeIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 7V4h16v3M9 20h6M12 4v16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SaveIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
<path d="M17 21v-8H7v8M7 3v5h8M12 11h6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const EyeIcon = () => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8Z" stroke="currentColor" strokeWidth="2"/>
|
||||||
|
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z" stroke="currentColor" strokeWidth="2"/>
|
||||||
|
<path d="M22 12c-1.5-4-5-6-10-6S4 8 2 12" stroke="currentColor" strokeWidth="2"/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="welcome-page-editor">
|
||||||
className="welcome-page-editor"
|
<div className="editor-header">
|
||||||
style={{ width: "80vw", height: "80vh" }}
|
<h2 className="editor-title">Konfigurasi Halaman Selamat Datang</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-content">
|
||||||
|
{/* Configuration Panel */}
|
||||||
|
<div className="config-panel">
|
||||||
|
{/* Top Preview Toggle */}
|
||||||
|
<div className="top-actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="preview-button"
|
||||||
|
onClick={() => setShowPreview(!showPreview)}
|
||||||
>
|
>
|
||||||
<h2>Edit Welcome Page</h2>
|
<EyeIcon /> {showPreview ? 'Tutup Pratinjau' : 'Preview'}
|
||||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
||||||
<textarea
|
|
||||||
value={welcomingText}
|
|
||||||
onChange={handleTextChange}
|
|
||||||
placeholder="Enter welcoming text..."
|
|
||||||
style={{ height: "20px", resize: "none" }} // Reduced height
|
|
||||||
/>
|
|
||||||
<div style={{ display: "flex", justifyContent: "space-between" }}>
|
|
||||||
<label>
|
|
||||||
Background Color:
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={backgroundColor}
|
|
||||||
onChange={handleColorChange}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Text Color:
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={textColor}
|
|
||||||
onChange={handleTextColorChange}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
|
||||||
<label style={{ marginRight: "10px" }}>Is Welcome Page Active:</label>
|
|
||||||
<Switch
|
|
||||||
onChange={() => setIsWelcomePageActive(!isWelcomePageActive)}
|
|
||||||
checked={isWelcomePageActive}
|
|
||||||
offColor="#888"
|
|
||||||
onColor="#0a0"
|
|
||||||
uncheckedIcon={false}
|
|
||||||
checkedIcon={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button onClick={handleSave} disabled={loading}>
|
|
||||||
{loading ? "Saving..." : "Save Configuration"}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
style={{ width: "100%", height: "100%", position: "relative", flex: 1, borderRadius: '15px' }}
|
{showPreview && (
|
||||||
>
|
<div className="inline-preview">
|
||||||
|
<div className="inline-preview-content">
|
||||||
<WelcomePage
|
<WelcomePage
|
||||||
image={image}
|
image={image}
|
||||||
welcomingText={welcomingText}
|
welcomingText={welcomingText}
|
||||||
backgroundColor={backgroundColor}
|
backgroundColor={backgroundColor}
|
||||||
textColor={textColor}
|
textColor={textColor}
|
||||||
onGetStarted={() => setIsFullscreen(false)}
|
onGetStarted={() => setShowPreview(false)}
|
||||||
isFullscreen={isFullscreen}
|
isFullscreen={false}
|
||||||
onImageChange={handleImageChange}
|
onImageChange={handleImageChange}
|
||||||
/>
|
/>
|
||||||
<div style={{ position: "absolute", bottom: 0, right: 0 }}>
|
</div>
|
||||||
<svg
|
</div>
|
||||||
width="100" // Adjust size as needed
|
)}
|
||||||
height="100" // Adjust size as needed
|
<div className="config-section">
|
||||||
style={{ position: "absolute", bottom: 0, right: 0 }}
|
<h3 className="section-title">
|
||||||
onClick={() => setIsFullscreen(true)}
|
<CoffeeIcon />
|
||||||
|
Konten Utama
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="config-group">
|
||||||
|
<label className="config-label">Teks Selamat Datang</label>
|
||||||
|
<textarea
|
||||||
|
className="config-input textarea-input"
|
||||||
|
value={welcomingText}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
placeholder="Masukkan teks selamat datang..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-group">
|
||||||
|
<label className="config-label">Gambar Latar Belakang</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
className="config-input"
|
||||||
|
/>
|
||||||
|
{image && (
|
||||||
|
<div style={{ marginTop: '12px', textAlign: 'center' }}>
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt="Preview"
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '150px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid #ddd'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-section">
|
||||||
|
<h3 className="section-title">
|
||||||
|
<PaletteIcon />
|
||||||
|
Tema & Warna
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="theme-grid">
|
||||||
|
<div className="color-field">
|
||||||
|
<label className="config-label">Warna Latar Belakang</label>
|
||||||
|
<div className="color-row">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="config-input color-input"
|
||||||
|
value={backgroundColor}
|
||||||
|
onChange={handleColorChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="config-input hex-input"
|
||||||
|
value={backgroundColor}
|
||||||
|
onChange={handleBackgroundHexInput}
|
||||||
|
maxLength={7}
|
||||||
|
placeholder="#FFFFFF"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="swatches" aria-label="Background presets">
|
||||||
|
{['#ffffff','#f8f9fa','#fff3cd','#e8f5e9','#e3f2fd','#212529'].map((c) => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
className="swatch"
|
||||||
|
style={{ backgroundColor: c, borderColor: c.toLowerCase()==='#ffffff'?'#ddd':'transparent' }}
|
||||||
|
onClick={() => setBackgroundColor(c)}
|
||||||
|
title={c}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="color-field">
|
||||||
|
<label className="config-label">Warna Teks</label>
|
||||||
|
<div className="color-row">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="config-input color-input"
|
||||||
|
value={textColor}
|
||||||
|
onChange={handleTextColorChange}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="config-input hex-input"
|
||||||
|
value={textColor}
|
||||||
|
onChange={handleTextHexInput}
|
||||||
|
maxLength={7}
|
||||||
|
placeholder="#000000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="swatches" aria-label="Text presets">
|
||||||
|
{['#000000','#212529','#343a40','#6c757d','#ffffff','#28a745'].map((c) => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
className="swatch"
|
||||||
|
style={{ backgroundColor: c, borderColor: c.toLowerCase()==='#ffffff'?'#ddd':'transparent' }}
|
||||||
|
onClick={() => setTextColor(c)}
|
||||||
|
title={c}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="contrast-card" style={{ backgroundColor }}>
|
||||||
|
<div className="contrast-inner">
|
||||||
|
<div className="contrast-title">Pratinjau Kontrast</div>
|
||||||
|
<div className="contrast-preview" style={{ color: textColor }}>{welcomingText || 'Contoh Teks'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="config-section">
|
||||||
|
<h3 className="section-title">
|
||||||
|
<TypeIcon />
|
||||||
|
Pengaturan
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="switch-container">
|
||||||
|
<span className="switch-label">Aktifkan Halaman Selamat Datang</span>
|
||||||
|
<Switch
|
||||||
|
onChange={() => setIsWelcomePageActive(!isWelcomePageActive)}
|
||||||
|
checked={isWelcomePageActive}
|
||||||
|
offColor="#cccccc"
|
||||||
|
onColor="#28a745"
|
||||||
|
uncheckedIcon={false}
|
||||||
|
checkedIcon={false}
|
||||||
|
height={24}
|
||||||
|
width={48}
|
||||||
|
handleDiameter={20}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="save-button"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<g transform="rotate(45 50 50)">
|
{loading ? (
|
||||||
<circle cx="50" cy="50" r="40" fill="rgba(0, 0, 0, 0.5)" />
|
<>
|
||||||
<text
|
<div className="loading-spinner"></div>
|
||||||
x="50"
|
Menyimpan...
|
||||||
y="50"
|
</>
|
||||||
textAnchor="middle"
|
) : (
|
||||||
dominantBaseline="middle"
|
<>
|
||||||
fontSize="24"
|
<SaveIcon />
|
||||||
fill="white" // Adjust text color as needed
|
Simpan Konfigurasi
|
||||||
>
|
</>
|
||||||
<>
|
)}
|
||||||
</text>
|
</button>
|
||||||
</g>
|
{/* Pratinjau dipicu oleh tombol atas; section khusus dihapus */}
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user