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

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

View File

@@ -23,6 +23,8 @@ const ItemConfig = ({
const [itemDescription, setItemDescription] = useState(initialDescription);
const fileInputRef = useRef(null);
const textareaRef = useRef(null);
const [saving, setSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState(null); // 'success' | 'error'
useEffect(() => {
// Prevent scrolling when modal is open
@@ -79,15 +81,29 @@ const ItemConfig = ({
}
}, [textareaRef.current]);
const handleCreate = () => {
console.log(itemPromoPrice)
handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice);
document.body.style.overflow = "auto";
const handleCreate = async () => {
setSaving(true);
setSaveStatus(null);
try {
await Promise.resolve(handleCreateItem(itemName, itemPrice, selectedImage, previewUrl, itemDescription, itemPromoPrice));
setSaveStatus('success');
} catch (e) {
setSaveStatus('error');
} finally {
setSaving(false);
}
};
const handleUpdate = () => {
console.log(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice)
handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice);
document.body.style.overflow = "auto";
const handleUpdate = async () => {
setSaving(true);
setSaveStatus(null);
try {
await Promise.resolve(handleUpdateItem(itemName, itemPrice, selectedImage, itemDescription, itemPromoPrice));
setSaveStatus('success');
} catch (e) {
setSaveStatus('error');
} finally {
setSaving(false);
}
};
return (
@@ -124,6 +140,14 @@ const ItemConfig = ({
</div>
<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}>
<label className={styles.formLabel}>Nama Item</label>
<input
@@ -167,17 +191,19 @@ const ItemConfig = ({
</div>
<div className={styles.formActions}>
<button
onClick={cancelEdit}
<button
onClick={cancelEdit}
className={`${styles.formButton} ${styles.cancelButton}`}
disabled={saving}
>
Batal
</button>
<button
onClick={() => {isBeingEdit ? handleUpdate() : handleCreate()}}
<button
onClick={() => { isBeingEdit ? handleUpdate() : handleCreate() }}
className={`${styles.formButton} ${styles.saveButton}`}
disabled={saving}
>
{isBeingEdit ? 'Simpan Perubahan' : 'Buat Item'}
{saving ? 'Menyimpan…' : (isBeingEdit ? 'Simpan Perubahan' : 'Buat Item')}
</button>
</div>
</div>
@@ -186,4 +212,4 @@ const ItemConfig = ({
);
};
export default ItemConfig;
export default ItemConfig;

View File

@@ -1,10 +1,12 @@
/* ItemTypeLister.css */
/* New clean, intuitive category bar */
.item-type-lister {
width: 100%;
overflow-x: auto;
white-space: nowrap;
padding: 12px 0;
margin-bottom: 12px;
padding: 8px 0; /* Reduced padding for more compact design */
margin-bottom: 8px; /* Reduced margin for more compact design */
scrollbar-width: thin;
display: flex;
justify-content: center;
@@ -12,20 +14,20 @@
}
.item-type-lister::-webkit-scrollbar {
height: 8px;
height: 6px;
}
.item-type-lister::-webkit-scrollbar-thumb {
background-color: #c5c5c5;
border-radius: 4px;
border-radius: 3px;
}
.category-bar {
display: flex;
align-items: center;
gap: 10px;
gap: 6px; /* Reduced gap for more compact design */
overflow-x: auto;
padding: 10px 15px;
padding: 6px 10px; /* Reduced padding */
-ms-overflow-style: none;
scrollbar-width: none;
justify-content: center;
@@ -39,7 +41,7 @@
-ms-overflow-style: none;
scrollbar-width: none;
overflow-y: hidden;
gap: 10px;
gap: 6px; /* Added gap for consistent spacing */
justify-content: center;
align-items: center;
width: 100%;
@@ -49,37 +51,34 @@
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 10px;
height: 42px;
padding: 0 18px;
border-radius: 12px; /* Square rounded corners */
gap: 6px;
height: 32px; /* Reduced height for more compact design */
padding: 0 14px; /* Reduced padding for more compact design */
border-radius: 999px;
border: 1px solid #e6e6e6;
background: #ffffff;
color: #2d2d2d;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 500;
font-size: 16px;
font-size: 13px; /* Slightly smaller font */
cursor: pointer;
user-select: none;
transition: all 0.2s ease;
justify-content: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.category-chip:hover {
border-color: #d0d0d0;
background-color: #f8f8f8;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.category-chip.selected {
background: #73a585;
color: #ffffff;
border-color: #73a585;
box-shadow: 0 2px 6px rgba(115, 165, 133, 0.2);
}
.category-chip .chip-icon {
width: 22px;
height: 22px;
width: 16px; /* Reduced icon size */
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
@@ -105,40 +104,146 @@
.inline-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 12px;
overflow-y: auto;
grid-template-columns: repeat(4, 1fr); /* Always 4 columns */
gap: 10px; /* Spacing between grid items */
padding: 10px; /* Padding inside grid */
overflow-y: auto; /* Allow scrolling if items overflow */
}
.grid-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 12px;
max-height: calc(3 * (30vw - 24px) + 24px);
overflow-y: auto;
padding-top: 18px;
height: calc(48vw - 24px);
grid-template-columns: repeat(4, 1fr); /* Always 4 columns */
gap: 10px; /* Spacing between grid items */
padding: 10px; /* Padding inside grid */
max-height: calc(3 * (25vw - 20px) + 20px); /* 3 items + gaps */
overflow-y: auto; /* Allow scrolling if items overflow */
padding-top: 15px;
height: calc(43vw - 20px);
}
.add-button {
margin: 12px;
padding: 12px 24px;
position: absolute;
margin: 10px; /* Margin around the button */
padding: 10px 20px; /* Padding for the button */
position: absolute; /* Optional, for styling */
bottom: 0;
align-self: center;
align-self: center; /* Center the button horizontally */
}
/* Centered container for item type list */
.centered-item-type-list {
/* Legacy styles kept for ItemType grid if needed elsewhere */
/* Compact centered item type list without icon tiles */
.compact-centered-list {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 12px 0;
padding: 8px 0;
overflow-x: auto;
}
/* No icon styles */
.no-icon {
padding: 0 20px; /* Increased padding for better touch targets */
.compact-item-type {
flex: 0 0 auto;
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;
}
}

View File

@@ -1,7 +1,6 @@
import React, { useState, useRef, useEffect } from "react";
import smoothScroll from "smooth-scroll-into-view-if-needed";
import "./ItemTypeLister.css";
import ItemType from "./ItemType";
import { createItem } from "../helpers/itemHelper.js";
import { getImageUrl } from "../helpers/itemHelper";
import ItemLister from "./ItemLister";
@@ -62,15 +61,15 @@ const ItemTypeLister = ({
return (
<div className="item-type-lister" style={{ overflowX: isAddingNewItem ? 'hidden' : 'auto' }}>
<div className="centered-item-type-list">
<div ref={newItemDivRef} className="item-type-list" style={{ display: 'inline-flex' }}>
<div className="compact-centered-list">
<div ref={newItemDivRef} className="compact-item-type-list" style={{ display: 'inline-flex' }}>
{isEditMode && !isAddingNewItem && canManage && (
<ItemType
<div
className="compact-item-type compact-add-item"
onClick={toggleAddNewItem}
name={"buat baru"}
noIcon={true}
compact={false} // Make it larger for better touch targets
/>
>
Buat baru
</div>
)}
{canManage && isAddingNewItem && (
@@ -91,24 +90,22 @@ const ItemTypeLister = ({
)}
{itemTypes && itemTypes.length > 0 && (
<ItemType
name={"semua"}
<div
className={`compact-item-type ${filterId === 0 ? 'selected' : ''}`}
onClick={() => onFilterChange(0)}
selected={filterId === 0}
noIcon={true}
compact={false} // Make it larger for better touch targets
/>
>
Semua
</div>
)}
{itemTypes && itemTypes.map((itemType) => (
<ItemType
<div
key={itemType.itemTypeId}
name={itemType.name}
className={`compact-item-type ${filterId === itemType.itemTypeId ? 'selected' : ''}`}
onClick={() => onFilterChange(itemType.itemTypeId)}
selected={filterId === itemType.itemTypeId}
noIcon={true}
compact={false} // Make it larger for better touch targets
/>
>
{formatName(itemType.name)}
</div>
))}
</div>
</div>

View File

@@ -5,7 +5,7 @@ import AccountUpdatePage from "../components/AccountUpdatePage.js";
import CreateClerk from "../pages/CreateClerk"
import CreateCafe from "../pages/CreateCafe"
import CreateTenant from "../pages/CreateTenant"
import TablesPage from "./TablesPage.js";
import IdentifyCafeModal from "./IdentifyCafeModal.js";
import PaymentOptions from "./PaymentOptions.js";
import Transaction from "../pages/Transaction";
import Transaction_item from "../pages/Transaction_item";
@@ -77,7 +77,7 @@ const Modal = ({ user, shop, isOpen, onClose, modalContent, deviceType, setModal
if(modalContent == '') handleOverlayClick();
return (
<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 === "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_kedai" && <CreateCafe 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" && (
<Transaction propsShopId={shop.cafeId} handleMoveToTransaction={handleMoveToTransaction} depth={depth} shopImg={shopImg} setModal={setModal}/>
)}

View File

@@ -8,7 +8,7 @@
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
z-index: 9999;
padding: 20px;
}
@@ -23,6 +23,12 @@
flex-direction: column;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
animation: modalAppear 0.3s ease-out;
position: relative;
z-index: 10000; /* ensure above any page overlays */
}
.modalContentWide {
max-width: 920px;
}
@keyframes modalAppear {
@@ -178,6 +184,31 @@
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 {
flex: 1;
padding: 14px 16px;
@@ -262,4 +293,4 @@
.formActions {
flex-direction: column;
}
}
}

View File

@@ -1,18 +1,13 @@
import React, { useState, useRef, useEffect } from "react";
import QrScanner from "qr-scanner"; // Import qr-scanner
import { getImageUrl } from "../helpers/itemHelper";
import {
getCafe,
saveCafeDetails,
setConfirmationStatus,
setOpenBillAvailability
} from "../helpers/cafeHelpers";
import { getCafe, saveCafeDetails } from "../helpers/cafeHelpers";
import Switch from "react-switch"; // Import the Switch component
import styles from "./PaymentOptions.module.css";
const SetPaymentQr = ({ shopId,
qrCodeUrl }) => {
const [qrPosition, setQrPosition] = useState([50, 50]);
const [qrSize, setQrSize] = useState(50);
const SetPaymentQr = ({ shopId, qrCodeUrl }) => {
const [qrPosition, setQrPosition] = useState([50, 50]); // legacy kept for API compatibility
const [qrSize, setQrSize] = useState(50); // legacy kept for API compatibility
const [qrPayment, setQrPayment] = useState();
const [qrPaymentFile, setQrPaymentFile] = useState();
const [qrCodeDetected, setQrCodeDetected] = useState(false);
@@ -26,6 +21,9 @@ const SetPaymentQr = ({ shopId,
const [isConfigQRIS, setIsConfigQRIS] = 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(() => {
const fetchCafe = async () => {
@@ -104,6 +102,8 @@ const SetPaymentQr = ({ shopId,
// Save cafe details
const handleSave = async () => {
setSaving(true);
setSaveStatus(null);
let qrPaymentFileCache;
console.log(qrPaymentFile)
if(qrPaymentFile != null)
@@ -120,240 +120,141 @@ const SetPaymentQr = ({ shopId,
try {
const response = await saveCafeDetails(cafe.cafeId, details);
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0); // Update state after saving
setIsQRISavailable(response.isQRISavailable ? 1 : 0); // Update state after saving
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0); // Update state after saving
setIsNeedConfirmationState(response.needsConfirmation ? 1 : 0);
setIsQRISavailable(response.isQRISavailable ? 1 : 0);
setIsOpenBillAvailable(response.isOpenBillAvailable ? 1 : 0);
setSaveStatus('success');
console.log("Cafe details saved:", response);
} catch (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 (
<div style={styles.container}>
<h3 style={styles.title}>Konfigurasi pembayaran</h3>
<div className={styles.container}>
<h3 className={styles.title}>Konfigurasi pembayaran</h3>
<div style={styles.switchContainer}>
<p style={styles.uploadMessage}>
Pembayaran QRIS.
</p>
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div>
<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
id="qr-code-container"
ref={qrCodeContainerRef}
className={styles.imageBox}
onClick={() => qrPaymentInputRef.current.click()}
style={{
...styles.qrCodeContainer,
backgroundImage: `url(${qrPayment})`,
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
backgroundSize: "contain",
}}
style={{ backgroundImage: `url(${qrPayment})` }}
>
<input
type="file"
accept="image/*"
ref={qrPaymentInputRef}
style={{ display: "none" }}
onChange={handleFileChange}
/>
<input type="file" accept="image/*" ref={qrPaymentInputRef} style={{ display: 'none' }} onChange={handleFileChange} />
</div>
<div style={styles.uploadMessage}>
<p>Klik untuk ganti background</p>
</div>
<div style={styles.resultMessage}>
{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 onClick={() => setIsConfigQRIS(false)}
style={{
...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 className={styles.smallNote}>Klik area untuk unggah/ganti gambar QR</div>
<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>
<button className={styles.button} onClick={() => qrPaymentInputRef.current.click()}>{qrPayment ? 'Ganti' : 'Unggah'}</button>
</div>
{qrCodeDetected && (
<div className={styles.copyRow}>
<input className={styles.linkField} readOnly value={qrCodeData || ''} />
<button className={styles.button} onClick={copyQrData}>{copied ? 'Disalin' : 'Salin'}</button>
</div>
)}
<div className={styles.actionsRight}>
<button className={`${styles.button} ${styles.primary}`} onClick={() => setIsConfigQRIS(false)}>Terapkan</button>
</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
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
checked={isOpenBillAvailable === 1} // Convert to boolean
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
)}
</div>
<div style={styles.switchContainer}>
<p style={styles.uploadMessage}>
Pengecekan ganda
</p>
<p style={styles.description}>
Nyalakan agar kasir memeriksa kembali ketersediaan produk sebelum pelanggan membayar.
</p>
<Switch
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
checked={isNeedConfirmationState === 1} // Convert to boolean
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
<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>
<Switch
onChange={(checked) => setIsOpenBillAvailable(checked ? 1 : 0)}
checked={isOpenBillAvailable === 1}
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
</div>
</div>
<div style={styles.buttonContainer}>
<button onClick={handleSave} style={styles.saveButton}>
Simpan
<div className={styles.section}>
<div className={styles.sectionHeader}>
<div>
<div className={styles.sectionTitle}>Pengecekan ganda</div>
<div className={styles.sectionDesc}>Kasir memeriksa kembali ketersediaan item sebelum pembayaran.</div>
</div>
<Switch
onChange={(checked) => setIsNeedConfirmationState(checked ? 1 : 0)}
checked={isNeedConfirmationState === 1}
offColor="#888"
onColor="#4CAF50"
uncheckedIcon={false}
checkedIcon={false}
height={25}
width={50}
/>
</div>
</div>
<div className={styles.footer}>
<div>
{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>
</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;

View 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

View File

@@ -17,15 +17,16 @@ code {
/* Ensure proper scrolling behavior */
html, body {
height: 100%;
width: 100%;
overflow: hidden;
min-height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
#root {
height: 100%;
width: 100%;
overflow: hidden;
min-height: 100%;
overflow: visible;
}
/* Custom scrollbar */
@@ -95,4 +96,4 @@ html, body {
max-height: 80vh;
overflow-y: auto;
}
}
}

View File

@@ -1,5 +1,6 @@
import React, { useRef, useEffect, useState } from "react";
import styles from "./Invoice.module.css";
import cartStyles from "./CartPage.module.css";
import { useParams } from "react-router-dom"; // Changed from useSearchParams to useLocation
import { ThreeDots, ColorRing } from "react-loader-spinner";
@@ -384,7 +385,13 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
return (
<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) ?
<div style={{ height: '75vh', display: 'flex', justifyContent: 'center', flexDirection: 'column', alignContent: 'center', alignItems: 'center' }}>
@@ -443,21 +450,21 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
</div>
)}
<div className={styles.NoteContainer}>
<span>Catatan :</span>
<span></span>
</div>
<div className={styles.NoteContainer}>
<textarea
ref={textareaRef}
className={styles.NoteInput}
placeholder="Tambahkan catatan..."
/>
</div>
</div>
}
{getItemsByCafeId(shopId).length > 0 && (
<div className={`${styles.RoundedRectangle} ${cartStyles.sectionCard}`}>
<div className={cartStyles.sectionTitle}>Catatan Untuk Kasir</div>
<div className={cartStyles.divider}></div>
<textarea
ref={textareaRef}
className={styles.NoteInput}
placeholder="Contoh: tanpa gula, ekstra es, dsb."
/>
</div>
)}
{transactionData &&
<div className={styles.RoundedRectangle} style={{ backgroundColor: '#c3c3c3', fontSize: '15px', display: 'flex', justifyContent: 'space-between' }}>
{transactionData.payment_type != 'paylater' ?
@@ -575,6 +582,7 @@ export default function Invoice({ shopId, setModal, table, sendParam, deviceType
</>
)
}
</div>
</div>
);
}

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

View File

@@ -1,119 +1,153 @@
import React, { useState } from 'react';
import { createClerks } from '../helpers/userHelpers'; // Adjust the import path as needed
import React, { useEffect, useMemo, useState } from 'react';
import { createClerks, getClerks } from '../helpers/userHelpers';
import { useLocation } from "react-router-dom";
import styles from './CreateClerk.module.css';
const CreateClerk = ({ shopId }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [saving, setSaving] = useState(false);
const [banner, setBanner] = useState(null); // { type: 'success'|'error', text: string }
const [clerks, setClerks] = useState([]);
const location = useLocation();
const queryParams = new URLSearchParams(location.search);
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) => {
event.preventDefault();
setLoading(true);
setMessage('');
setSaving(true);
setBanner(null);
// Basic validation
if (!username || !password) {
setMessage('Username and password are required');
setLoading(false);
setBanner({ type: 'error', text: 'Username dan password wajib diisi' });
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;
}
try {
const create = await createClerks(shopId || cafeIdParam, username, password);
if (create) setMessage('Clerk created successfully');
else setMessage('Failed to create clerk');
const create = await createClerks(effectiveShopId, username, password);
if (create) {
setBanner({ type: 'success', text: 'Kasir berhasil ditambahkan' });
// 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) {
setMessage('Error creating clerk');
setBanner({ type: 'error', text: 'Terjadi kesalahan saat menambahkan kasir' });
} finally {
setLoading(false);
setSaving(false);
}
};
return (
<div style={styles.container}>
<h2 style={styles.header}>Tambah Kasir</h2>
<form onSubmit={handleSubmit} style={styles.form}>
<input
type="text"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
style={styles.input}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
style={styles.input}
/>
<button type="submit" style={styles.button} disabled={loading}>
{loading ? 'Creating...' : 'Create Clerk'}
</button>
{message && (
<p style={{ ...styles.message, color: message.includes('success') ? 'green' : 'red' }}>
{message}
</p>
<div className={styles.container}>
<div className={styles.header}>
<h2 className={styles.title}>Tambah Kasir</h2>
{banner && (
<div className={`${styles.banner} ${banner.type === 'success' ? styles.bannerSuccess : styles.bannerError}`}>
{banner.text}
</div>
)}
</form>
</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
className={styles.input}
type="text"
placeholder="kasir_baru"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className={styles.field}>
<label className={styles.label}>Password</label>
<div className={styles.pwdRow}>
<input
className={styles.input}
type={showPassword ? 'text' : 'password'}
placeholder="••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="button" className={styles.button} onClick={()=>setShowPassword(!showPassword)}>
{showPassword ? 'Sembunyikan' : 'Tampilkan'}
</button>
<button type="button" className={styles.button} onClick={generatePassword}>
Generate
</button>
</div>
</div>
</div>
<div className={styles.footer}>
<button className={`${styles.button} ${styles.primary}`} type="submit" disabled={saving}>
{saving ? 'Menambahkan…' : 'Tambah Kasir'}
</button>
</div>
</form>
</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;

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

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

View File

@@ -7,6 +7,8 @@
width: 100%;
height: 100%;
text-align: center;
padding: 20px;
box-sizing: border-box;
}
.image-container {
@@ -15,6 +17,11 @@
overflow: hidden;
border-radius: 50%;
margin-bottom: 20px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: #f0f0f0;
}
.fileInput {
@@ -24,38 +31,134 @@
.circular-image {
width: 100%;
height: 100%;
object-fit: contain;
object-fit: cover;
border-radius: 50%;
}
.welcoming-text {
font-size: 24px;
margin-bottom: 20px;
font-family: "Plus Jakarta Sans", sans-serif;
font-weight: 600;
color: #333;
text-align: center;
max-width: 80%;
}
.get-started-button {
padding: 10px 20px;
padding: 12px 24px;
border: none;
border-radius: 25px;
background-color: #007bff; /* Bootstrap primary color */
background-color: #28a745; /* Bootstrap primary color */
color: white;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
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 {
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 {
position: fixed;
top: 50%;
left: 50%;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
transform: translate(-50%, -50%);
z-index: 300;
z-index: 1000;
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;
}
}

View File

@@ -1,5 +1,5 @@
// WelcomePage.js
import React,{useRef} from "react";
import React, { useRef } from "react";
import "./WelcomePage.css";
const WelcomePage = ({
@@ -15,6 +15,15 @@ const WelcomePage = ({
const handleImageClick = () => {
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 (
<div
className={`welcome-page ${isFullscreen ? "fullscreen" : ""}`} // Corrected the className syntax
@@ -29,10 +38,13 @@ const WelcomePage = ({
className="image-container"
>
{!isFullscreen &&
<div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute'}}>
<h1 style={{textAlign:'left'}}>
{image ? "Click To Change Image" : "Click To Add Image"}
</h1>
<div onClick={handleImageClick} style={{width: '100%', height: '100%', position:'absolute', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer'}}>
<div style={{textAlign: 'center', color: 'white'}}>
<CameraIcon />
<h1 style={{textAlign:'center', fontSize: '16px', margin: '10px 0 0 0'}}>
{image ? "Klik untuk mengganti gambar" : "Klik untuk menambahkan gambar"}
</h1>
</div>
<input
ref={fileInputRef} style={{display: 'none', width:'100%', height: '100%'}}type="file" accept="image/*" onChange={onImageChange} />
</div>
@@ -43,10 +55,10 @@ const WelcomePage = ({
{welcomingText}
</h1>
<button className="get-started-button" onClick={onGetStarted}>
Get Started
Mulai
</button>
</div>
);
};
export default WelcomePage;
export default WelcomePage;

View File

@@ -1,56 +1,395 @@
/* WelcomePageEditor.css */
.welcome-page-editor {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: rgb(207, 207, 207);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
width: 100%;
height: 100vh;
background-color: #ffffff;
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden; /* contain scroll inside editor-content */
font-family: "Plus Jakarta Sans", sans-serif;
}
h2 {
margin-bottom: 20px;
.editor-header {
padding: 24px;
border-bottom: 1px solid #e6e6e6;
background-color: #f8f9fa;
}
.editor-title {
margin: 0;
font-weight: 700;
font-size: 24px;
color: #333;
}
input[type="file"] {
margin-bottom: 20px;
}
textarea {
.editor-content {
display: flex;
flex: 1; /* take remaining height */
width: 100%;
height: 100px; /* Adjust as needed */
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
resize: none;
margin-bottom: 20px;
font-size: 16px;
overflow-y: auto; /* enable vertical scroll in editor-content */
overflow-x: hidden;
justify-content: center;
align-items: flex-start;
padding: 16px;
box-sizing: border-box;
}
textarea:focus {
border-color: #007bff; /* Highlight border color on focus */
.config-panel {
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;
border-color: #28a745;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.1);
}
label {
margin-bottom: 20px;
.color-input {
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;
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 {
width: 100%;
height: 100%;
border: 1px dashed #ccc; /* Preview border style */
border-radius: 8px;
padding: 20px;
max-width: 400px;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
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;
justify-content: 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;
}
}

View File

@@ -14,6 +14,7 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
const [isWelcomePageActive, setIsWelcomePageActive] = useState(false); // State for the switch
const [loading, setLoading] = useState(false); // Loading state
const [isFullscreen, setIsFullscreen] = useState(false);
const [showPreview, setShowPreview] = useState(false);
// Load existing welcome page configuration when component mounts
useEffect(() => {
@@ -51,6 +52,21 @@ const WelcomePageEditor = ({ cafeId, welcomePageConfig }) => {
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 () => {
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 (
<div
className="welcome-page-editor"
style={{ width: "80vw", height: "80vh" }}
>
<h2>Edit Welcome Page</h2>
<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>
<div className="welcome-page-editor">
<div className="editor-header">
<h2 className="editor-title">Konfigurasi Halaman Selamat Datang</h2>
</div>
<div
style={{ width: "100%", height: "100%", position: "relative", flex: 1, borderRadius: '15px' }}
>
<WelcomePage
image={image}
welcomingText={welcomingText}
backgroundColor={backgroundColor}
textColor={textColor}
onGetStarted={() => setIsFullscreen(false)}
isFullscreen={isFullscreen}
onImageChange={handleImageChange}
/>
<div style={{ position: "absolute", bottom: 0, right: 0 }}>
<svg
width="100" // Adjust size as needed
height="100" // Adjust size as needed
style={{ position: "absolute", bottom: 0, right: 0 }}
onClick={() => setIsFullscreen(true)}
<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)}
>
<EyeIcon /> {showPreview ? 'Tutup Pratinjau' : 'Preview'}
</button>
</div>
{showPreview && (
<div className="inline-preview">
<div className="inline-preview-content">
<WelcomePage
image={image}
welcomingText={welcomingText}
backgroundColor={backgroundColor}
textColor={textColor}
onGetStarted={() => setShowPreview(false)}
isFullscreen={false}
onImageChange={handleImageChange}
/>
</div>
</div>
)}
<div className="config-section">
<h3 className="section-title">
<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)">
<circle cx="50" cy="50" r="40" fill="rgba(0, 0, 0, 0.5)" />
<text
x="50"
y="50"
textAnchor="middle"
dominantBaseline="middle"
fontSize="24"
fill="white" // Adjust text color as needed
>
&lt;&gt;
</text>
</g>
</svg>
{loading ? (
<>
<div className="loading-spinner"></div>
Menyimpan...
</>
) : (
<>
<SaveIcon />
Simpan Konfigurasi
</>
)}
</button>
{/* Pratinjau dipicu oleh tombol atas; section khusus dihapus */}
</div>
</div>
</div>