This commit is contained in:
karyamanswasta
2025-08-27 05:54:02 +07:00
parent f58b40c70d
commit df203447a9
20 changed files with 2344 additions and 1701 deletions

View 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>
);
}