EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
page.tsx
Go to the documentation of this file.
1"use client";
2import { useEffect, useMemo, useState } from "react";
3import { fieldpine } from "@/lib/fieldpine";
4import { Icon } from '@/contexts/IconContext';
5
6interface ProductOption {
7 id: number;
8 name: string;
9 price: number;
10 description?: string;
11}
12
13interface FormatOption {
14 id: string;
15 name: string;
16 type: "A4 Sign" | "Shelf Label";
17 content: string;
18}
19
20const fallbackProducts: ProductOption[] = [
21 { id: 101, name: "Organic Pears 1kg", description: "Crisp and sweet", price: 2.95 },
22 { id: 205, name: "Honeycrisp Apples", description: "Seasonal promo", price: 3.49 },
23 { id: 330, name: "Bananas", description: "Everyday price", price: 1.99 },
24 { id: 441, name: "Almond Milk 1L", description: "Dairy free", price: 4.5 },
25];
26
27const starterExamples: Record<number, string> = {
28 1: "SPECIAL\n\nPEARS\n$2.95\n\nToday Only",
29 2: "SPECIAL\n\nPEARS\n>color red\n$2.95\n>color black\n\n\nToday Only",
30 3: ">fontsize 30mm\nSPECIAL\n\nPEARS\n>background red white\n$2.95\n>background white\n>color black\n\nToday Only\n>left\n>fontsize 8mm\nLimit 3 per customer, while stocks last, no rainchecks",
31};
32
33const initialFormats: FormatOption[] = [
34 { id: "a4-summer", name: "Summer A4", type: "A4 Sign", content: starterExamples[1] },
35 { id: "shelf-std", name: "Standard Shelf", type: "Shelf Label", content: "PRICE\n>fontsize 14mm\n>left\nProduct Name" },
36];
37
38function mmToPx(input: string) {
39 const numeric = parseFloat(input);
40 if (Number.isNaN(numeric)) return undefined;
41 return numeric * 3.78; // rough mm to px conversion
42}
43
44export default function SignsAndLabelsPage() {
45 const [signText, setSignText] = useState(starterExamples[1]);
46 const [products, setProducts] = useState<ProductOption[]>(fallbackProducts);
47 const [selectedProduct, setSelectedProduct] = useState<ProductOption>(fallbackProducts[0]);
48 const [productQuery, setProductQuery] = useState("");
49 const [selectedQueue, setSelectedQueue] = useState("Main Label Printer");
50 const [queues] = useState(["Main Label Printer", "Office Laser", "Back Office Zebra"]);
51 const [formats, setFormats] = useState<FormatOption[]>(initialFormats);
52 const [selectedFormatId, setSelectedFormatId] = useState(initialFormats[0].id);
53 const [saveMessage, setSaveMessage] = useState("");
54 const [newFormatName, setNewFormatName] = useState("");
55 const [newFormatType, setNewFormatType] = useState<FormatOption["type"]>("A4 Sign");
56 const [printMessage, setPrintMessage] = useState("");
57 const [isPrinting, setIsPrinting] = useState(false);
58 const [productsLoading, setProductsLoading] = useState(false);
59 const [productError, setProductError] = useState<string | null>(null);
60 const [includeName, setIncludeName] = useState(true);
61 const [includeDescription, setIncludeDescription] = useState(false);
62 const [includePrice, setIncludePrice] = useState(false);
63 const [pdfMessage, setPdfMessage] = useState<string | null>(null);
64
65 useEffect(() => {
66 const savedQueue = typeof window !== "undefined" ? localStorage.getItem("signs.queue") : null;
67 if (savedQueue) setSelectedQueue(savedQueue);
68 }, []);
69
70 useEffect(() => {
71 if (typeof window !== "undefined") {
72 localStorage.setItem("signs.queue", selectedQueue);
73 }
74 }, [selectedQueue]);
75
76 useEffect(() => {
77 let active = true;
78 const fetchProducts = async () => {
79 setProductsLoading(true);
80 setProductError(null);
81 try {
82 const result = await fieldpine.getProducts({ limit: 50, search: productQuery || undefined });
83 const mapped: ProductOption[] = (result.products || []).map((p: any) => ({
84 id: p.pid ?? p.id ?? p.productId ?? 0,
85 name: p.description ?? p.name ?? `Product ${p.pid ?? p.id ?? ""}`,
86 description: p.longDescription ?? p.description ?? p.name,
87 price: Number(p.sellprice ?? p.sellPrice ?? p.price ?? 0),
88 })).filter((p: ProductOption) => p.id !== 0);
89 if (!active) return;
90 if (mapped.length) {
91 setProducts(mapped);
92 setSelectedProduct((prev) => {
93 const stillExists = mapped.find((m) => m.id === prev?.id);
94 return stillExists ?? mapped[0];
95 });
96 } else {
97 setProducts(fallbackProducts);
98 setSelectedProduct(fallbackProducts[0]);
99 }
100 } catch (error: any) {
101 if (!active) return;
102 setProductError(error?.message || "Unable to load products");
103 setProducts(fallbackProducts);
104 setSelectedProduct(fallbackProducts[0]);
105 } finally {
106 if (active) setProductsLoading(false);
107 }
108 };
109
110 const timer = setTimeout(fetchProducts, 250);
111 return () => {
112 active = false;
113 clearTimeout(timer);
114 };
115 }, [productQuery]);
116
117 const filteredProducts = useMemo(() => {
118 if (!productQuery.trim()) return products;
119 const q = productQuery.toLowerCase();
120 return products.filter((p) =>
121 p.name.toLowerCase().includes(q) ||
122 p.description?.toLowerCase().includes(q) ||
123 p.id.toString() === q
124 );
125 }, [productQuery, products]);
126
127 const parsedLines = useMemo(() => {
128 const autoLines: string[] = [];
129 if (includeName && selectedProduct?.name) autoLines.push(selectedProduct.name);
130 if (includeDescription && selectedProduct?.description) autoLines.push(selectedProduct.description);
131 if (includePrice && Number.isFinite(selectedProduct?.price)) autoLines.push(`$${selectedProduct.price.toFixed(2)}`);
132
133 const lines = [...autoLines, ...signText.split(/\r?\n/)] as string[];
134 const out: { id: number; text: string; style: React.CSSProperties }[] = [];
135 let currentColor = "#0f172a";
136 let currentBackground = "white";
137 let currentAlign: React.CSSProperties["textAlign"] = "center";
138 let currentFontSize = 28;
139
140 lines.forEach((raw, idx) => {
141 if (raw.startsWith(">")) {
142 const parts = raw.slice(1).trim().split(/\s+/);
143 const [command, ...rest] = parts;
144 if (command === "color" && rest[0]) currentColor = rest[0];
145 if (command === "background" && rest[0]) currentBackground = rest[0];
146 if (command === "fontsize" && rest[0]) currentFontSize = mmToPx(rest[0]) ?? currentFontSize;
147 if (command === "left") currentAlign = "left";
148 if (command === "center") currentAlign = "center";
149 if (command === "right") currentAlign = "right";
150 return;
151 }
152
153 out.push({
154 id: idx,
155 text: raw.length ? raw : "\u00a0",
156 style: {
157 color: currentColor,
158 background: currentBackground,
159 textAlign: currentAlign,
160 fontSize: currentFontSize,
161 lineHeight: 1.05,
162 fontWeight: "bold",
163 padding: "4px 6px",
164 },
165 });
166 });
167 return out;
168 }, [signText]);
169
170 const handleExample = (n: number) => {
171 setSignText(starterExamples[n]);
172 setPrintMessage("");
173 };
174
175 const handleLoadFormat = () => {
176 const fmt = formats.find((f) => f.id === selectedFormatId);
177 if (!fmt) return;
178 setSignText(fmt.content);
179 setSaveMessage(`Loaded ${fmt.name}`);
180 setTimeout(() => setSaveMessage(""), 2000);
181 };
182
183 const handleSaveFormat = () => {
184 setFormats((prev) => prev.map((f) => (f.id === selectedFormatId ? { ...f, content: signText } : f)));
185 const fmt = formats.find((f) => f.id === selectedFormatId);
186 setSaveMessage(fmt ? `${fmt.name} saved` : "Saved changes");
187 setTimeout(() => setSaveMessage(""), 2200);
188 };
189
190 const handleSaveNew = () => {
191 if (!newFormatName.trim()) {
192 setSaveMessage("Enter a name to save a new format");
193 return;
194 }
195 const id = `custom-${Date.now()}`;
196 const next: FormatOption = { id, name: newFormatName.trim(), type: newFormatType, content: signText };
197 setFormats((prev) => [...prev, next]);
198 setSelectedFormatId(id);
199 setNewFormatName("");
200 setSaveMessage(`Saved new format '${next.name}'`);
201 setTimeout(() => setSaveMessage(""), 2200);
202 };
203
204 const handlePrint = () => {
205 setIsPrinting(true);
206 setPrintMessage("Sending to printer queue...");
207 setTimeout(() => {
208 setIsPrinting(false);
209 setPrintMessage(`Queued to ${selectedQueue} for ${selectedProduct.name}`);
210 }, 900);
211 };
212
213 const handlePrintToPdf = () => {
214 if (typeof window === "undefined") return;
215 setPdfMessage(null);
216 const html = `<!doctype html>
217 <html>
218 <head>
219 <meta charset="utf-8" />
220 <title>Sign Preview</title>
221 <style>
222 body { margin: 0; padding: 24px; font-family: 'Inter', system-ui, sans-serif; background: #f8fafc; }
223 .page { width: 210mm; height: 297mm; padding: 16mm; box-sizing: border-box; background: white; border: 1px solid #cbd5e1; box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12); }
224 .header { display: flex; justify-content: space-between; font-size: 12px; color: #475569; margin-bottom: 12px; }
225 .lines { display: flex; flex-direction: column; gap: 8px; align-items: stretch; }
226 </style>
227 </head>
228 <body>
229 <div class="page">
230 <div class="lines">
231 ${parsedLines.map((line) => `<div style="${[
232 `color:${line.style.color ?? "#0f172a"}`,
233 `background:${line.style.background ?? "white"}`,
234 `text-align:${line.style.textAlign ?? "center"}`,
235 `font-size:${typeof line.style.fontSize === "number" ? `${line.style.fontSize}px` : line.style.fontSize ?? "28px"}`,
236 `line-height:${line.style.lineHeight ?? 1.05}`,
237 `font-weight:${line.style.fontWeight ?? "bold"}`,
238 `padding:${line.style.padding ?? "4px 6px"}`
239 ].join(";")}">${line.text || "&nbsp;"}</div>`).join("")}
240 </div>
241 </div>
242 <script>window.onload = () => { window.focus(); window.print(); };</script>
243 </body>
244 </html>`;
245
246 try {
247 const blob = new Blob([html], { type: "text/html" });
248 const url = URL.createObjectURL(blob);
249 const iframe = document.createElement("iframe");
250 iframe.style.position = "fixed";
251 iframe.style.right = "0";
252 iframe.style.bottom = "0";
253 iframe.style.width = "0";
254 iframe.style.height = "0";
255 iframe.style.border = "0";
256 iframe.src = url;
257 iframe.onload = () => {
258 try {
259 iframe.contentWindow?.focus();
260 iframe.contentWindow?.print();
261 setPdfMessage("Opened browser print dialog — choose Save as PDF.");
262 } catch (err) {
263 setPdfMessage("Unable to open print dialog. Allow popups or try again.");
264 }
265 };
266 document.body.appendChild(iframe);
267 setTimeout(() => {
268 document.body.removeChild(iframe);
269 URL.revokeObjectURL(url);
270 }, 10_000);
271 } catch (err) {
272 setPdfMessage("Unable to generate PDF preview.");
273 }
274 };
275
276 const handleSelectProduct = (id: number) => {
277 const match = products.find((p) => p.id === id);
278 if (match) setSelectedProduct(match);
279 };
280
281 return (
282 <div className="p-8 space-y-6">
283 <div className="flex items-center justify-between gap-4">
284 <div>
285 <p className="text-sm text-muted mb-1">Products / Signs & Labels</p>
286 <h1 className="text-3xl font-bold text-text">Signs and Labels</h1>
287 <p className="text-sm text-muted">Draft, preview, and send signage to your label printers.</p>
288 </div>
289 <div className="hidden md:flex items-center gap-3 bg-surface border border-border rounded-lg px-4 py-2 shadow-sm">
290 <div className="text-xs text-muted">Queue</div>
291 <select
292 className="border border-border rounded px-3 py-1 text-sm"
293 value={selectedQueue}
294 onChange={(e) => setSelectedQueue(e.target.value)}
295 >
296 {queues.map((q) => (
297 <option key={q} value={q}>{q}</option>
298 ))}
299 </select>
300 </div>
301 </div>
302
303 <div className="grid gap-6 lg:grid-cols-3">
304 <div className="lg:col-span-2 space-y-6">
305 <div className="bg-surface border border-border rounded-xl shadow-sm p-4 sm:p-6">
306 <div className="flex flex-col gap-3">
307 <div className="flex items-center justify-between gap-3">
308 <div>
309 <h2 className="text-xl font-semibold text-text">Sign details</h2>
310 <p className="text-sm text-muted">Write your sign text. Use &gt; commands for quick styling.</p>
311 </div>
312 <div className="flex gap-2">
313 {[1, 2, 3].map((n) => (
314 <button
315 key={n}
316 onClick={() => handleExample(n)}
317 className="px-3 py-2 text-sm bg-surface-2 hover:bg-surface rounded border border-border"
318 >
319 Example #{n}
320 </button>
321 ))}
322 </div>
323 </div>
324
325 <div className="flex flex-wrap gap-3 text-sm text-muted">
326 <label className="inline-flex items-center gap-2">
327 <input type="checkbox" checked={includeName} onChange={(e) => setIncludeName(e.target.checked)} />
328 Include product name
329 </label>
330 <label className="inline-flex items-center gap-2">
331 <input type="checkbox" checked={includeDescription} onChange={(e) => setIncludeDescription(e.target.checked)} />
332 Include description
333 </label>
334 <label className="inline-flex items-center gap-2">
335 <input type="checkbox" checked={includePrice} onChange={(e) => setIncludePrice(e.target.checked)} />
336 Include price
337 </label>
338 </div>
339 </div>
340 <textarea
341 value={signText}
342 onChange={(e) => setSignText(e.target.value)}
343 rows={16}
344 className="w-full border border-border rounded-lg p-3 font-mono text-sm outline-none focus:ring-2 focus:ring-brand/50"
345 aria-label="Sign text"
346 />
347 <div className="mt-2 text-xs text-muted">
348 Commands: <code>{">color red"}</code>, <code>{">fontsize 30mm"}</code>, <code>{">left|center|right"}</code>, <code>{">background yellow"}</code>
349 </div>
350 </div>
351
352 <div className="bg-surface border border-border rounded-xl shadow-sm p-4 sm:p-6 flex flex-col gap-4">
353 <div className="flex items-center justify-between">
354 <div>
355 <h3 className="text-lg font-semibold text-text">Sample product</h3>
356 <p className="text-sm text-muted">Used for preview and when sending to the printer queue.</p>
357 </div>
358 <div className="text-sm text-muted">${selectedProduct.price.toFixed(2)}</div>
359 </div>
360 <div className="flex flex-col gap-3">
361 <input
362 className="border border-border rounded px-3 py-2 text-sm"
363 placeholder="Search product by name or ID"
364 value={productQuery}
365 onChange={(e) => setProductQuery(e.target.value)}
366 />
367 {productError && <div className="text-xs text-red-600">{productError}</div>}
368 <div className="flex flex-wrap gap-2">
369 {productsLoading ? (
370 <div className="text-sm text-muted">Loading products…</div>
371 ) : filteredProducts.length === 0 ? (
372 <div className="text-sm text-muted">No products found</div>
373 ) : (
374 filteredProducts.map((p) => (
375 <button
376 key={p.id}
377 onClick={() => handleSelectProduct(p.id)}
378 className={`px-3 py-2 rounded border text-sm ${selectedProduct.id === p.id ? "bg-brand text-white border-brand" : "bg-surface border-border hover:border-brand"}`}
379 >
380 <div className="font-medium">{p.name}</div>
381 {p.description && <div className="text-xs text-muted">{p.description}</div>}
382 </button>
383 ))
384 )}
385 </div>
386 </div>
387 </div>
388 </div>
389
390 <div className="space-y-6">
391 <div className="bg-surface border border-border rounded-xl shadow-sm p-4 sm:p-6">
392 <div className="flex items-center justify-between mb-3">
393 <div>
394 <h3 className="text-lg font-semibold text-text">Quick preview</h3>
395 <p className="text-xs text-muted">Preview is indicative. Print to see final layout.</p>
396 </div>
397 <span className="text-xs px-2 py-1 rounded bg-surface-2 border border-border">A4</span>
398 </div>
399 <div className="border border-border rounded-lg bg-surface overflow-hidden">
400 <div className="bg-surface-2 px-3 py-2 text-xs text-muted flex justify-between">
401 <span>{selectedProduct.name}</span>
402 <span>Queue: {selectedQueue}</span>
403 </div>
404 <div className="p-4 min-h-[420px] flex items-center justify-center">
405 <div className="w-full max-w-[360px] border border-border shadow-inner rounded-lg bg-surface aspect-[1/1.414] flex flex-col items-stretch justify-center p-4 gap-2">
406 {parsedLines.map((line) => (
407 <div key={line.id} style={line.style}>
408 {line.text}
409 </div>
410 ))}
411 </div>
412 </div>
413 </div>
414 <button
415 onClick={handlePrint}
416 disabled={isPrinting}
417 className="w-full mt-4 px-4 py-3 rounded-lg bg-brand text-white font-semibold hover:bg-brand/90 disabled:opacity-60"
418 >
419 {isPrinting ? "Sending..." : "Print Large Sign"}
420 </button>
421 {printMessage && <div className="mt-2 text-sm text-brand">{printMessage}</div>}
422 <button
423 onClick={handlePrintToPdf}
424 className="w-full mt-2 px-4 py-3 rounded-lg border border-border bg-surface text-text font-semibold hover:bg-surface-2"
425 >
426 Save/Print as PDF (browser)
427 </button>
428 {pdfMessage && <div className="mt-1 text-xs text-muted">{pdfMessage}</div>}
429 </div>
430
431 <div className="bg-surface border border-border rounded-xl shadow-sm p-4 sm:p-6 space-y-4">
432 <div className="flex items-center justify-between">
433 <h3 className="text-lg font-semibold text-text">Design & Save</h3>
434 <select
435 className="border border-border rounded px-2 py-1 text-sm"
436 value={selectedQueue}
437 onChange={(e) => setSelectedQueue(e.target.value)}
438 >
439 {queues.map((q) => (
440 <option key={q} value={q}>{q}</option>
441 ))}
442 </select>
443 </div>
444
445 <div className="space-y-3">
446 <label className="text-sm font-medium text-text">Load an existing format</label>
447 <div className="flex gap-2">
448 <select
449 className="flex-1 border border-border rounded px-3 py-2 text-sm"
450 value={selectedFormatId}
451 onChange={(e) => setSelectedFormatId(e.target.value)}
452 >
453 {formats.map((fmt) => (
454 <option key={fmt.id} value={fmt.id}>{fmt.name} ({fmt.type})</option>
455 ))}
456 </select>
457 <button
458 onClick={handleLoadFormat}
459 className="px-3 py-2 rounded bg-surface-2 border border-border text-sm hover:bg-surface"
460 >
461 Load
462 </button>
463 <button
464 onClick={handleSaveFormat}
465 className="px-3 py-2 rounded bg-brand text-white text-sm hover:bg-brand/90"
466 >
467 Save Changes
468 </button>
469 </div>
470 </div>
471
472 <div className="border-t border-border pt-3 space-y-3">
473 <div className="text-sm font-medium text-text">Save as new</div>
474 <div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
475 <input
476 className="border border-border rounded px-3 py-2 text-sm"
477 placeholder="Format name"
478 value={newFormatName}
479 onChange={(e) => setNewFormatName(e.target.value)}
480 />
481 <select
482 className="border border-border rounded px-3 py-2 text-sm"
483 value={newFormatType}
484 onChange={(e) => setNewFormatType(e.target.value as FormatOption["type"])}
485 >
486 <option value="A4 Sign">A4 Sign</option>
487 <option value="Shelf Label">Shelf Label</option>
488 </select>
489 </div>
490 <button
491 onClick={handleSaveNew}
492 className="w-full px-3 py-2 rounded bg-surface-2 border border-border text-sm hover:bg-surface"
493 >
494 Save New
495 </button>
496 </div>
497
498 {saveMessage && <div className="text-sm text-brand">{saveMessage}</div>}
499 </div>
500 </div>
501 </div>
502 </div>
503 );
504}