349 lines
14 KiB
JavaScript
349 lines
14 KiB
JavaScript
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>
|
|
);
|
|
}
|