feat: Add configurable deposit percentage, FAQ management, and fix PayID copy button

This commit is contained in:
Gemini CLI committed 2026-06-03 20:05:21 +10:00
1 parent d34950a91b
commit 27f3d73d01
12 files changed
+1111 -334

No files matched your search

-1
View File
@@ -1813,7 +1813,6 @@
"integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/config": "6.19.3",
"@prisma/engines": "6.19.3"
@@ -0,0 +1,42 @@
-- AlterTable
ALTER TABLE "StockItem" ADD COLUMN "maxStock" REAL;
-- CreateTable
CREATE TABLE "FAQ" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"question" TEXT NOT NULL,
"answer" TEXT NOT NULL,
"order" INTEGER NOT NULL DEFAULT 0
);
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Config" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT DEFAULT 1,
"adminCode" TEXT NOT NULL DEFAULT '1234',
"finalDepositDate" TEXT,
"cookDay" TEXT,
"payIdInfo" TEXT DEFAULT 'Your PayID Here',
"termsOfService" TEXT DEFAULT 'To secure your order, a 30% non-refundable deposit is required upfront before smoking begins. Payments can be made via PayID or Cash. The remaining balance is payable upon pickup.',
"orderingPolicy" TEXT DEFAULT 'Deposits cover material costs and are final. PayID details provided after ordering.'
);
INSERT INTO "new_Config" ("adminCode", "id") SELECT "adminCode", "id" FROM "Config";
DROP TABLE "Config";
ALTER TABLE "new_Config" RENAME TO "Config";
CREATE TABLE "new_Order" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"orderNumber" TEXT,
"name" TEXT NOT NULL,
"phone" TEXT NOT NULL,
"items" TEXT NOT NULL,
"totalWeight" REAL NOT NULL,
"estimatedTotal" REAL NOT NULL,
"status" TEXT NOT NULL DEFAULT 'Pending',
"timestamp" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "new_Order" ("estimatedTotal", "id", "items", "name", "phone", "timestamp", "totalWeight") SELECT "estimatedTotal", "id", "items", "name", "phone", "timestamp", "totalWeight" FROM "Order";
DROP TABLE "Order";
ALTER TABLE "new_Order" RENAME TO "Order";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
@@ -0,0 +1,18 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Config" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT DEFAULT 1,
"adminCode" TEXT NOT NULL DEFAULT '1234',
"finalDepositDate" TEXT,
"cookDay" TEXT,
"payIdInfo" TEXT DEFAULT 'Your PayID Here',
"termsOfService" TEXT DEFAULT 'To secure your order, a 30% non-refundable deposit is required upfront before smoking begins. Payments can be made via PayID or Cash. The remaining balance is payable upon pickup.',
"orderingPolicy" TEXT DEFAULT 'Deposits cover material costs and are final. PayID details provided after ordering.',
"depositPercentage" INTEGER NOT NULL DEFAULT 30
);
INSERT INTO "new_Config" ("adminCode", "cookDay", "finalDepositDate", "id", "orderingPolicy", "payIdInfo", "termsOfService") SELECT "adminCode", "cookDay", "finalDepositDate", "id", "orderingPolicy", "payIdInfo", "termsOfService" FROM "Config";
DROP TABLE "Config";
ALTER TABLE "new_Config" RENAME TO "Config";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;
+10
View File
@@ -43,4 +43,14 @@ model Config {
finalDepositDate String?
cookDay String?
payIdInfo String? @default("Your PayID Here")
termsOfService String? @default("To secure your order, a 30% non-refundable deposit is required upfront before smoking begins. Payments can be made via PayID or Cash. The remaining balance is payable upon pickup.")
orderingPolicy String? @default("Deposits cover material costs and are final. PayID details provided after ordering.")
depositPercentage Int @default(30)
}
model FAQ {
id Int @id @default(autoincrement())
question String
answer String
order Int @default(0)
}
+72 -5
View File
@@ -13,12 +13,13 @@ app.use(express.json());
// API Routes
// Fetch all data (Stock, Orders, Config)
// Fetch all data (Stock, Orders, Config, FAQs)
app.get('/api/data', async (req, res) => {
try {
let stock = await prisma.stockItem.findMany();
const orders = await prisma.order.findMany();
const config = await prisma.config.findFirst();
const faqs = await prisma.fAQ.findMany({ orderBy: { order: 'asc' } });
// Parse order items from string to JSON
const parsedOrders = orders.map(order => ({
@@ -64,7 +65,7 @@ app.get('/api/data', async (req, res) => {
// Security: Don't send the admin code to the client
const configResponse = config ? { ...config, adminCode: undefined } : null;
res.json({ stock: stockWithRemaining, orders: parsedOrders, config: configResponse });
res.json({ stock: stockWithRemaining, orders: parsedOrders, config: configResponse, faqs });
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to fetch data' });
@@ -75,13 +76,17 @@ app.get('/api/data', async (req, res) => {
app.post('/api/login', async (req, res) => {
try {
const { code } = req.body;
console.log(`Login attempt with code: ${code}`);
const config = await prisma.config.findFirst();
if (config && config.adminCode === code) {
console.log('Login successful');
res.json({ success: true });
} else {
console.log(`Login failed. Expected: ${config?.adminCode}, Got: ${code}`);
res.status(401).json({ error: 'Invalid admin code' });
}
} catch (error) {
console.error('Login error:', error);
res.status(500).json({ error: 'Login failed' });
}
});
@@ -236,12 +241,15 @@ app.delete('/api/stock/:id', async (req, res) => {
// Update Config
app.post('/api/config', async (req, res) => {
try {
const { adminCode, finalDepositDate, cookDay, payIdInfo } = req.body;
const { adminCode, finalDepositDate, cookDay, payIdInfo, termsOfService, orderingPolicy, depositPercentage } = req.body;
const data: any = {};
if (adminCode !== undefined) data.adminCode = adminCode;
if (finalDepositDate !== undefined) data.finalDepositDate = finalDepositDate;
if (cookDay !== undefined) data.cookDay = cookDay;
if (payIdInfo !== undefined) data.payIdInfo = payIdInfo;
if (termsOfService !== undefined) data.termsOfService = termsOfService;
if (orderingPolicy !== undefined) data.orderingPolicy = orderingPolicy;
if (depositPercentage !== undefined) data.depositPercentage = parseInt(depositPercentage);
// Use upsert to ensure we always use ID 1
await prisma.config.upsert({
@@ -257,19 +265,58 @@ app.post('/api/config', async (req, res) => {
}
});
// Update or Create FAQ Item
app.post('/api/faqs', async (req, res) => {
try {
const item = req.body;
if (item.id) {
const updated = await prisma.fAQ.update({
where: { id: item.id },
data: {
...item,
id: undefined // Don't update ID
}
});
res.json(updated);
} else {
const created = await prisma.fAQ.create({ data: item });
res.json(created);
}
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to update FAQ' });
}
});
// Delete FAQ Item
app.delete('/api/faqs/:id', async (req, res) => {
try {
const { id } = req.params;
await prisma.fAQ.delete({ where: { id: parseInt(id) } });
res.sendStatus(200);
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Failed to delete FAQ item' });
}
});
// Reset Database to defaults
app.post('/api/reset', async (req, res) => {
try {
await prisma.order.deleteMany();
await prisma.stockItem.deleteMany();
await prisma.config.deleteMany();
await prisma.fAQ.deleteMany();
// Seed default admin code and limits with ID 1
await prisma.config.create({
data: {
id: 1,
adminCode: '1234',
payIdInfo: 'Your PayID Here'
payIdInfo: 'Your PayID Here',
termsOfService: 'To secure your order, a 30% non-refundable deposit is required upfront before smoking begins. Payments can be made via PayID or Cash. The remaining balance is payable upon pickup.',
orderingPolicy: 'Deposits cover material costs and are final. PayID details provided after ordering.',
depositPercentage: 30
}
});
@@ -280,6 +327,14 @@ app.post('/api/reset', async (req, res) => {
{ name: 'Pork Shoulder', category: 'Pork', price: 35, unit: 'kg', description: 'Pulled pork perfection', available: true }
]
});
// Seed some default FAQs
await prisma.fAQ.createMany({
data: [
{ question: 'When is pickup?', answer: 'Pickups are usually scheduled for the afternoon of the Cook Day. We will contact you to confirm the exact time.', order: 1 },
{ question: 'How do I pay the deposit?', answer: 'Once you submit your order, you can pay the 30% deposit via PayID or Cash.', order: 2 }
]
});
res.sendStatus(200);
} catch (error) {
@@ -299,6 +354,18 @@ app.get(/^\/(?!api).*/, (req, res) => {
res.sendFile(path.join(frontendPath, 'index.html'));
});
app.listen(PORT, () => {
app.listen(PORT, async () => {
// Ensure a default config exists
const config = await prisma.config.findFirst();
if (!config) {
await prisma.config.create({
data: {
id: 1,
adminCode: '1234',
payIdInfo: 'Your PayID Here'
}
});
console.log('Default config seeded');
}
console.log(`Server is running on http://localhost:${PORT}`);
});
+779 -308
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -21,7 +21,7 @@
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitejs/plugin-react": "^4.7.0",
"autoprefixer": "^10.5.0",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
@@ -32,6 +32,6 @@
"tailwindcss": "^4.3.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12"
"vite": "^5.4.21"
}
}
+13 -3
View File
@@ -10,6 +10,7 @@ function App() {
const [view, setView] = useState<'customer' | 'admin'>('customer');
const [stock, setStock] = useState<any[]>([]);
const [orders, setOrders] = useState<any[]>([]);
const [faqs, setFaqs] = useState<any[]>([]);
const [cart, setCart] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [showCartModal, setShowCartModal] = useState(false);
@@ -25,19 +26,28 @@ function App() {
const data = await API.fetchData();
setStock(data.stock);
setOrders(data.orders.sort((a: any, b: any) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()));
setFaqs(data.faqs || []);
if (data.config) {
setConfig(data.config);
}
setIsLoading(false);
} catch (err) {
console.error("Server connection failed.", err);
}
};
useEffect(() => {
// Force a minimum loading time of 5 seconds
const timer = setTimeout(() => {
setIsLoading(false);
}, 5000);
refreshData();
const intervalId = setInterval(refreshData, 5000);
return () => clearInterval(intervalId);
return () => {
clearTimeout(timer);
clearInterval(intervalId);
};
}, []);
const showNotification = (msg: string) => {
setNotification(msg);
@@ -158,7 +168,7 @@ function App() {
<main className="max-w-6xl mx-auto px-4 py-8">
{notification && <div className="fixed bottom-4 right-4 bg-red-600 text-white px-6 py-3 rounded-lg shadow-2xl animate-bounce z-50 font-bold tracking-wide border border-red-400">{notification}</div>}
{view === 'customer' ? <CustomerView stock={stock} onAddToCart={(item: any) => setPickerItem(item)} config={config} /> : <AdminDashboard stock={stock} orders={orders} config={config} refreshData={refreshData} notify={showNotification} promptConfirm={promptConfirm} />}
{view === 'customer' ? <CustomerView stock={stock} faqs={faqs} onAddToCart={(item: any) => setPickerItem(item)} config={config} /> : <AdminDashboard stock={stock} orders={orders} config={config} faqs={faqs} refreshData={refreshData} notify={showNotification} promptConfirm={promptConfirm} />}
</main>
{showCartModal && <CartModal cart={cart} config={config} onClose={() => setShowCartModal(false)} onRemove={removeFromCart} onUpdateQty={updateCartQty} onSubmit={submitOrder} />}
+137 -3
View File
@@ -2,8 +2,8 @@ import { useState, useEffect } from 'react';
import { Icons } from './Icons';
import { API } from '../services/api';
export default function AdminDashboard({ stock, orders, config, refreshData, notify, promptConfirm }: any) {
const [tab, setTab] = useState<'orders' | 'stock' | 'settings'>('orders');
export default function AdminDashboard({ stock, orders, config, faqs, refreshData, notify, promptConfirm }: any) {
const [tab, setTab] = useState<'orders' | 'stock' | 'faqs' | 'settings'>('orders');
const [newAdminCode, setNewAdminCode] = useState('');
const [settingsForm, setSettingsForm] = useState<any>(null);
@@ -12,7 +12,10 @@ export default function AdminDashboard({ stock, orders, config, refreshData, not
setSettingsForm({
finalDepositDate: config.finalDepositDate || '',
cookDay: config.cookDay || '',
payIdInfo: config.payIdInfo || ''
payIdInfo: config.payIdInfo || '',
termsOfService: config.termsOfService || '',
orderingPolicy: config.orderingPolicy || '',
depositPercentage: config.depositPercentage || 30
});
}
}, [config, settingsForm]);
@@ -72,6 +75,32 @@ export default function AdminDashboard({ stock, orders, config, refreshData, not
});
};
const addNewFAQ = async () => {
const newFAQ = {
question: "New Question?",
answer: "Answer here...",
order: faqs.length + 1
};
await API.updateFAQ(newFAQ);
refreshData();
notify("New FAQ Added");
};
const updateFAQ = async (id: number, updates: any) => {
const currentFAQ = faqs.find((f: any) => f.id === id) || { id };
await API.updateFAQ({ ...currentFAQ, ...updates });
refreshData();
notify("FAQ updated");
};
const deleteFAQ = (id: number) => {
promptConfirm("Delete this FAQ?", async () => {
await API.deleteFAQ(id);
refreshData();
notify("FAQ deleted");
});
};
const clearAllOrders = () => {
promptConfirm("WARNING: This will delete ALL current order history. Proceed?", async () => {
await API.clearOrders();
@@ -93,6 +122,7 @@ export default function AdminDashboard({ stock, orders, config, refreshData, not
<div className="flex border-b border-stone-800 bg-stone-950">
<button onClick={() => setTab('orders')} className={`flex-1 py-4 font-bold text-center transition-colors uppercase tracking-wider text-sm ${tab === 'orders' ? 'bg-stone-900 text-red-500 border-t-2 border-red-500' : 'text-stone-600 hover:text-stone-300'}`}>Requests</button>
<button onClick={() => setTab('stock')} className={`flex-1 py-4 font-bold text-center transition-colors uppercase tracking-wider text-sm ${tab === 'stock' ? 'bg-stone-900 text-red-500 border-t-2 border-red-500' : 'text-stone-600 hover:text-stone-300'}`}>Inventory</button>
<button onClick={() => setTab('faqs')} className={`flex-1 py-4 font-bold text-center transition-colors uppercase tracking-wider text-sm ${tab === 'faqs' ? 'bg-stone-900 text-red-500 border-t-2 border-red-500' : 'text-stone-600 hover:text-stone-300'}`}>FAQ</button>
<button onClick={() => setTab('settings')} className={`flex-1 py-4 font-bold text-center transition-colors uppercase tracking-wider text-sm ${tab === 'settings' ? 'bg-stone-900 text-red-500 border-t-2 border-red-500' : 'text-stone-600 hover:text-stone-300'}`}>Settings</button>
</div>
@@ -114,6 +144,23 @@ export default function AdminDashboard({ stock, orders, config, refreshData, not
</div>
)}
{tab === 'faqs' && (
<div className="space-y-6">
<div className="flex justify-between items-end border-b border-stone-800 pb-4">
<h3 className="text-xl font-bold text-white">FAQ Management</h3>
<p className="text-xs text-stone-500">Edit frequently asked questions</p>
</div>
<div className="grid gap-4">
{faqs.map((faq: any) => (
<AdminFAQItem key={faq.id} item={faq} onUpdate={(data: any) => updateFAQ(faq.id, data)} onDelete={() => deleteFAQ(faq.id)} />
))}
</div>
<div className="flex justify-between items-center pt-10 border-t border-stone-800 mt-10">
<button onClick={addNewFAQ} className="bg-stone-800 hover:bg-stone-700 text-white px-4 py-2 rounded font-bold flex items-center gap-2 border border-stone-700"><Icons.Plus /> Add New FAQ</button>
</div>
</div>
)}
{tab === 'orders' && (
<div className="space-y-6">
<div className="grid grid-cols-3 gap-4 mb-8">
@@ -227,6 +274,35 @@ export default function AdminDashboard({ stock, orders, config, refreshData, not
className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none"
/>
</div>
<div>
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">Deposit Percentage (%)</label>
<input
type="number"
min="0"
max="100"
value={settingsForm.depositPercentage}
onChange={(e) => setSettingsForm({ ...settingsForm, depositPercentage: e.target.value })}
className="w-32 bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none"
/>
</div>
<h4 className="text-sm font-bold text-red-500 uppercase tracking-widest border-b border-stone-800 pb-2 pt-4">Policies & TOS</h4>
<div>
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">Terms of Service</label>
<textarea
value={settingsForm.termsOfService}
onChange={(e) => setSettingsForm({ ...settingsForm, termsOfService: e.target.value })}
className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none h-24 text-sm"
/>
</div>
<div>
<label className="block text-xs font-bold text-stone-500 uppercase mb-2">Ordering Policy Footer</label>
<textarea
value={settingsForm.orderingPolicy}
onChange={(e) => setSettingsForm({ ...settingsForm, orderingPolicy: e.target.value })}
className="w-full bg-stone-900 border border-stone-700 rounded p-3 text-white focus:border-red-600 outline-none h-24 text-sm"
/>
</div>
<button
onClick={handleSaveSettings}
@@ -256,6 +332,64 @@ export default function AdminDashboard({ stock, orders, config, refreshData, not
);
}
function AdminFAQItem({ item, onUpdate, onDelete }: any) {
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState<any>({});
useEffect(() => {
if (!isEditing) {
setEditData({
question: item.question || "",
answer: item.answer || "",
order: item.order || 0
});
}
}, [item, isEditing]);
const handleSave = () => {
onUpdate({
...editData,
order: parseInt(editData.order)
});
setIsEditing(false);
};
return (
<div className="bg-stone-950 p-4 rounded-lg border border-stone-800 flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1">
{!isEditing ? (
<div className="flex-1">
<p className="font-bold text-white text-lg">{item.question}</p>
<p className="text-sm text-stone-400">{item.answer}</p>
<p className="text-[10px] text-stone-600 mt-2 uppercase font-bold">Display Order: {item.order}</p>
</div>
) : (
<div className="flex-1 flex flex-col gap-2 mr-4">
<input type="text" value={editData.question} onChange={e => setEditData({ ...editData, question: e.target.value })} className="bg-black text-white border border-stone-700 rounded px-2 py-1 font-bold w-full" placeholder="Question" />
<textarea value={editData.answer} onChange={e => setEditData({ ...editData, answer: e.target.value })} className="bg-black text-stone-400 border border-stone-700 rounded px-2 py-1 text-sm w-full h-24" placeholder="Answer" />
<div className="flex items-center gap-2">
<label className="text-[10px] text-stone-500 uppercase font-bold">Order:</label>
<input type="number" value={editData.order} onChange={e => setEditData({ ...editData, order: e.target.value })} className="bg-black text-white border border-stone-700 rounded px-2 py-0.5 text-xs w-20" />
</div>
</div>
)}
</div>
<div className="flex items-center gap-2">
{isEditing ? (
<>
<button onClick={handleSave} className="p-2 bg-green-600 hover:bg-green-500 text-white rounded"><Icons.Save /></button>
<button onClick={onDelete} className="p-2 bg-red-900 hover:bg-red-700 text-white rounded"><Icons.Trash /></button>
</>
) : (
<button onClick={() => setIsEditing(true)} className="p-2 bg-stone-800 hover:bg-stone-700 text-stone-400 hover:text-white rounded"><Icons.Edit /></button>
)}
</div>
</div>
</div>
);
}
function AdminStockItem({ item, onUpdate, onDelete }: any) {
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState<any>({});
+15 -7
View File
@@ -31,7 +31,8 @@ export default function CartModal({ cart, onClose, onRemove, onUpdateQty, onSubm
}, 0);
const totalWeight = cart.reduce((acc, i) => acc + i.qty, 0);
const depositAmount = total * 0.3;
const depositPct = config?.depositPercentage ?? 30;
const depositAmount = total * (depositPct / 100);
const handleSubmit = async () => {
if (!name || !phone) return;
@@ -62,14 +63,21 @@ export default function CartModal({ cart, onClose, onRemove, onUpdateQty, onSubm
<div className="space-y-4">
<div className="flex gap-3">
<div className="text-stone-500 mt-1"><Icons.Alert /></div>
<p className="text-sm text-stone-300">A <span className="text-white font-bold">30% deposit of ${depositAmount.toFixed(2)}</span> is required before we start the smoker.</p>
<p className="text-sm text-stone-300">A <span className="text-white font-bold">{depositPct}% deposit of ${depositAmount.toFixed(2)}</span> is required before we start the smoker.</p>
</div>
<div className="space-y-2">
<p className="text-xs font-bold text-stone-500 uppercase">Option 1: PayID (Instant)</p>
<div className="bg-stone-900 p-3 rounded border border-red-900/30 text-white font-mono flex justify-between items-center group">
<button
onClick={() => {
const payId = config?.payIdInfo || '0400 000 000';
navigator.clipboard.writeText(payId);
// Optional: Add UI feedback here if needed, but the user just wanted it to work
}}
className="w-full bg-stone-900 p-3 rounded border border-red-900/30 text-white font-mono flex justify-between items-center group hover:border-red-600 transition-colors"
>
<span>{config?.payIdInfo || '0400 000 000'}</span>
<span className="text-[8px] uppercase bg-red-900/50 px-1 rounded text-red-400">Copy</span>
</div>
<span className="text-[8px] uppercase bg-red-900/50 px-1 rounded text-red-400 group-hover:bg-red-600 group-hover:text-white transition-colors">Copy</span>
</button>
<p className="text-[10px] text-stone-500 italic">Please use "{submittedOrder.orderNumber}" as the payment description.</p>
</div>
<div className="space-y-2">
@@ -159,14 +167,14 @@ export default function CartModal({ cart, onClose, onRemove, onUpdateQty, onSubm
<span>${total.toFixed(2)}</span>
</div>
<div className="flex justify-between text-red-500 text-2xl font-black italic uppercase tracking-tighter">
<span>30% Deposit Due</span>
<span>{depositPct}% Deposit Due</span>
<span>${depositAmount.toFixed(2)}</span>
</div>
</div>
<div className="space-y-4">
<div className="text-xs bg-stone-950 p-3 rounded border border-stone-800 text-stone-400">
<p className="font-bold text-red-500 mb-1 uppercase tracking-widest">Deposit Required</p>
<p>Order will not be cooked until the 30% deposit is paid. We accept PayID and Cash.</p>
<p>Order will not be cooked until the {depositPct}% deposit is paid. We accept PayID and Cash.</p>
</div>
<div className="space-y-3">
<div>
+17 -5
View File
@@ -78,7 +78,7 @@ function StockCard({ item, onAdd, disabled }: { item: any, onAdd?: () => void, d
);
}
export default function CustomerView({ stock, onAddToCart, config }: { stock: any[], onAddToCart: (item: any) => void, config?: any }) {
export default function CustomerView({ stock, faqs, onAddToCart, config }: { stock: any[], faqs: any[], onAddToCart: (item: any) => void, config?: any }) {
const availableStock = stock.filter(item => item.available);
const soldOutStock = stock.filter(item => !item.available);
@@ -114,9 +114,7 @@ export default function CustomerView({ stock, onAddToCart, config }: { stock: an
Terms of Service & Ordering Policy
</p>
<p className="text-stone-300 text-sm leading-relaxed">
To secure your order, a <span className="text-white font-bold underline">30% non-refundable deposit</span> is required upfront before smoking begins.
Payments can be made via <span className="text-red-400 font-bold">PayID</span> or <span className="text-red-400 font-bold">Cash</span>.
The remaining balance is payable upon pickup.
{config?.termsOfService || `To secure your order, a ${config?.depositPercentage || 30}% non-refundable deposit is required upfront before smoking begins. Payments can be made via PayID or Cash. The remaining balance is payable upon pickup.`}
</p>
</div>
@@ -132,7 +130,7 @@ export default function CustomerView({ stock, onAddToCart, config }: { stock: an
</div>
<div className="text-[10px] text-stone-500 italic flex items-center gap-1">
<Icons.Check /> Deposits cover material costs and are final. PayID details provided after ordering.
<Icons.Check /> {config?.orderingPolicy || 'Deposits cover material costs and are final. PayID details provided after ordering.'}
</div>
</div>
</div>
@@ -157,6 +155,20 @@ export default function CustomerView({ stock, onAddToCart, config }: { stock: an
</div>
</div>
)}
{faqs && faqs.length > 0 && (
<div className="pt-12 border-t border-stone-800 mt-20">
<h3 className="text-2xl font-bold text-stone-300 mb-8 flex items-center gap-2 uppercase italic tracking-tighter"><Icons.Alert /> Frequently Asked Questions</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{faqs.map(faq => (
<div key={faq.id} className="space-y-2">
<h4 className="font-bold text-red-500 text-lg uppercase tracking-tight">{faq.question}</h4>
<p className="text-stone-400 text-sm leading-relaxed">{faq.answer}</p>
</div>
))}
</div>
</div>
)}
</div>
);
}
+6
View File
@@ -36,6 +36,12 @@ export const API = {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cfg)
}),
updateFAQ: (faq: any) => fetch(`${API_BASE}/api/faqs`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(faq)
}).then(res => res.json()),
deleteFAQ: (id: number) => fetch(`${API_BASE}/api/faqs/${id}`, { method: 'DELETE' }),
resetDB: () => fetch(`${API_BASE}/api/reset`, { method: 'POST' }),
login: async (code: string) => {
const res = await fetch(`${API_BASE}/api/login`, {