repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Fastfood QR
stars
latest
clone command
git clone gitlawb://did:key:z6Mkfh4Y...QBEi/fastfood-qrgit clone gitlawb://did:key:z6Mkfh4Y.../fastfood-qr3042a875sync from playground1d ago| #1 | import { useState, useRef, useEffect, type FormEvent } from 'react'; |
| #2 | import { useNavigate } from 'react-router-dom'; |
| #3 | import { useApp } from '../context'; |
| #4 | import { generateId } from '../data'; |
| #5 | import { IconBurger, IconCheck, IconX, IconUtensils, IconEdit, IconTrash, IconPlus, IconDownload, IconCopy, IconLogout, IconEye } from '../icons'; |
| #6 | import type { Product } from '../types'; |
| #7 | |
| #8 | type Tab = 'store' | 'categories' | 'products' | 'qr'; |
| #9 | |
| #10 | export default function Dashboard() { |
| #11 | const { |
| #12 | currentRestaurant, categories, products, isLoggedIn, |
| #13 | login, logout, addCategory, updateCategory, deleteCategory, |
| #14 | addProduct, updateProduct, deleteProduct, updateRestaurant, |
| #15 | } = useApp(); |
| #16 | const navigate = useNavigate(); |
| #17 | |
| #18 | const [email, setEmail] = useState(import.meta.env.APP_DEMO_EMAIL || 'demo@burgerhouse.com'); |
| #19 | const [password, setPassword] = useState(import.meta.env.APP_DEMO_PASSWORD || 'password123'); |
| #20 | const [loginError, setLoginError] = useState(''); |
| #21 | const [activeTab, setActiveTab] = useState<Tab>('store'); |
| #22 | |
| #23 | if (!isLoggedIn || !currentRestaurant) { |
| #24 | return ( |
| #25 | <div className="login-page"> |
| #26 | <div className="login-card"> |
| #27 | <h1 className="login-title"><IconBurger size={28} style={{ marginRight: 8 }} /> FastFood QR</h1> |
| #28 | <p className="login-subtitle">Sign in to manage your restaurant</p> |
| #29 | <form className="login-form" onSubmit={(e) => { e.preventDefault(); if (!login(email, password)) setLoginError('Invalid credentials'); }}> |
| #30 | <div className="form-group"> |
| #31 | <label className="form-label">Email</label> |
| #32 | <input className="form-input" type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@restaurant.com" /> |
| #33 | </div> |
| #34 | <div className="form-group"> |
| #35 | <label className="form-label">Password</label> |
| #36 | <input className="form-input" type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" /> |
| #37 | </div> |
| #38 | {loginError && <p className="login-error">{loginError}</p>} |
| #39 | <button className="btn btn-accent btn-large" type="submit">Sign In</button> |
| #40 | <p className="login-hint">Demo: {import.meta.env.APP_DEMO_EMAIL || 'demo@burgerhouse.com'} / {import.meta.env.APP_DEMO_PASSWORD || 'password123'}</p> |
| #41 | </form> |
| #42 | </div> |
| #43 | </div> |
| #44 | ); |
| #45 | } |
| #46 | |
| #47 | return ( |
| #48 | <div className="dashboard"> |
| #49 | <div className="dashboard-topbar"> |
| #50 | <h1 className="topbar-name">{currentRestaurant.name}</h1> |
| #51 | <div className="topbar-actions"> |
| #52 | <button className="btn btn-secondary" onClick={() => navigate(`/menu/${currentRestaurant.slug}`)}> |
| #53 | <IconEye size={16} /> Preview Menu |
| #54 | </button> |
| #55 | <button className="btn btn-danger" onClick={logout}> |
| #56 | <IconLogout size={16} /> Logout |
| #57 | </button> |
| #58 | </div> |
| #59 | </div> |
| #60 | |
| #61 | <div className="tabs"> |
| #62 | {(['store', 'categories', 'products', 'qr'] as Tab[]).map((tab) => ( |
| #63 | <button |
| #64 | key={tab} |
| #65 | className={`tab ${activeTab === tab ? 'active' : ''}`} |
| #66 | onClick={() => setActiveTab(tab)} |
| #67 | > |
| #68 | {tab === 'store' && 'Store Settings'} |
| #69 | {tab === 'categories' && 'Categories'} |
| #70 | {tab === 'products' && 'Products'} |
| #71 | {tab === 'qr' && 'QR Code'} |
| #72 | </button> |
| #73 | ))} |
| #74 | </div> |
| #75 | |
| #76 | <div className="tab-content"> |
| #77 | {activeTab === 'store' && <StoreTab />} |
| #78 | {activeTab === 'categories' && <CategoriesTab />} |
| #79 | {activeTab === 'products' && <ProductsTab />} |
| #80 | {activeTab === 'qr' && <QRTab />} |
| #81 | </div> |
| #82 | </div> |
| #83 | ); |
| #84 | } |
| #85 | |
| #86 | function StoreTab() { |
| #87 | const { currentRestaurant, updateRestaurant } = useApp(); |
| #88 | const [saved, setSaved] = useState(false); |
| #89 | const [form, setForm] = useState({ |
| #90 | name: currentRestaurant?.name ?? '', |
| #91 | description: currentRestaurant?.description ?? '', |
| #92 | whatsappNumber: currentRestaurant?.whatsappNumber ?? '', |
| #93 | address: currentRestaurant?.address ?? '', |
| #94 | openingHours: currentRestaurant?.openingHours ?? '', |
| #95 | instagram: currentRestaurant?.instagram ?? '', |
| #96 | deliveryFee: String(currentRestaurant?.deliveryFee ?? 0), |
| #97 | currency: currentRestaurant?.currency ?? 'EUR', |
| #98 | bannerUrl: currentRestaurant?.bannerUrl ?? '', |
| #99 | }); |
| #100 | |
| #101 | const set = (key: string, value: string) => setForm((f) => ({ ...f, [key]: value })); |
| #102 | |
| #103 | const handleSave = (e: FormEvent) => { |
| #104 | e.preventDefault(); |
| #105 | updateRestaurant({ |
| #106 | name: form.name, |
| #107 | description: form.description, |
| #108 | whatsappNumber: form.whatsappNumber, |
| #109 | address: form.address, |
| #110 | openingHours: form.openingHours, |
| #111 | instagram: form.instagram, |
| #112 | deliveryFee: Number(form.deliveryFee) || 0, |
| #113 | currency: form.currency, |
| #114 | bannerUrl: form.bannerUrl, |
| #115 | }); |
| #116 | setSaved(true); |
| #117 | setTimeout(() => setSaved(false), 2000); |
| #118 | }; |
| #119 | |
| #120 | return ( |
| #121 | <form className="store-form" onSubmit={handleSave}> |
| #122 | <div className="form-row"> |
| #123 | <div className="form-group"> |
| #124 | <label className="form-label">Restaurant Name</label> |
| #125 | <input className="form-input" value={form.name} onChange={(e) => set('name', e.target.value)} /> |
| #126 | </div> |
| #127 | <div className="form-group"> |
| #128 | <label className="form-label">WhatsApp Number</label> |
| #129 | <input className="form-input" value={form.whatsappNumber} onChange={(e) => set('whatsappNumber', e.target.value)} placeholder="+5511999999999" /> |
| #130 | </div> |
| #131 | </div> |
| #132 | <div className="form-group"> |
| #133 | <label className="form-label">Description</label> |
| #134 | <textarea className="form-input" value={form.description} onChange={(e) => set('description', e.target.value)} rows={2} /> |
| #135 | </div> |
| #136 | <div className="form-row"> |
| #137 | <div className="form-group"> |
| #138 | <label className="form-label">Address</label> |
| #139 | <input className="form-input" value={form.address} onChange={(e) => set('address', e.target.value)} /> |
| #140 | </div> |
| #141 | <div className="form-group"> |
| #142 | <label className="form-label">Opening Hours</label> |
| #143 | <input className="form-input" value={form.openingHours} onChange={(e) => set('openingHours', e.target.value)} /> |
| #144 | </div> |
| #145 | </div> |
| #146 | <div className="form-row"> |
| #147 | <div className="form-group"> |
| #148 | <label className="form-label">Instagram</label> |
| #149 | <input className="form-input" value={form.instagram} onChange={(e) => set('instagram', e.target.value)} placeholder="@handle" /> |
| #150 | </div> |
| #151 | <div className="form-group"> |
| #152 | <label className="form-label">Banner Image URL</label> |
| #153 | <input className="form-input" value={form.bannerUrl} onChange={(e) => set('bannerUrl', e.target.value)} /> |
| #154 | </div> |
| #155 | </div> |
| #156 | <div className="form-row"> |
| #157 | <div className="form-group"> |
| #158 | <label className="form-label">Delivery Fee</label> |
| #159 | <input className="form-input" type="number" step="0.01" value={form.deliveryFee} onChange={(e) => set('deliveryFee', e.target.value)} /> |
| #160 | </div> |
| #161 | <div className="form-group"> |
| #162 | <label className="form-label">Currency</label> |
| #163 | <select className="form-input" value={form.currency} onChange={(e) => set('currency', e.target.value)}> |
| #164 | <option value="EUR">EUR (€)</option> |
| #165 | <option value="USD">USD ($)</option> |
| #166 | <option value="GBP">GBP (£)</option> |
| #167 | <option value="BRL">BRL (R$)</option> |
| #168 | </select> |
| #169 | </div> |
| #170 | </div> |
| #171 | <div className="form-actions"> |
| #172 | <button className="btn btn-primary" type="submit">Save Changes</button> |
| #173 | {saved && <span className="save-success"><IconCheck size={16} /> Saved!</span>} |
| #174 | </div> |
| #175 | </form> |
| #176 | ); |
| #177 | } |
| #178 | |
| #179 | function CategoriesTab() { |
| #180 | const { categories, addCategory, updateCategory, deleteCategory } = useApp(); |
| #181 | const [newName, setNewName] = useState(''); |
| #182 | const [editingId, setEditingId] = useState<string | null>(null); |
| #183 | const [editName, setEditName] = useState(''); |
| #184 | |
| #185 | const handleAdd = (e: FormEvent) => { |
| #186 | e.preventDefault(); |
| #187 | const name = newName.trim(); |
| #188 | if (!name) return; |
| #189 | addCategory(name); |
| #190 | setNewName(''); |
| #191 | }; |
| #192 | |
| #193 | const startEdit = (id: string, name: string) => { |
| #194 | setEditingId(id); |
| #195 | setEditName(name); |
| #196 | }; |
| #197 | |
| #198 | const handleSaveEdit = () => { |
| #199 | if (editingId && editName.trim()) { |
| #200 | updateCategory(editingId, editName.trim()); |
| #201 | } |
| #202 | setEditingId(null); |
| #203 | }; |
| #204 | |
| #205 | return ( |
| #206 | <div> |
| #207 | <form className="add-category-row" onSubmit={handleAdd}> |
| #208 | <input |
| #209 | className="form-input" |
| #210 | value={newName} |
| #211 | onChange={(e) => setNewName(e.target.value)} |
| #212 | placeholder="New category name" |
| #213 | /> |
| #214 | <button className="btn btn-primary" type="submit"><IconPlus size={16} /> Add Category</button> |
| #215 | </form> |
| #216 | <div className="category-list"> |
| #217 | {categories.map((cat) => ( |
| #218 | <div key={cat.id} className="category-item"> |
| #219 | {editingId === cat.id ? ( |
| #220 | <> |
| #221 | <input |
| #222 | className="form-input category-edit-input" |
| #223 | value={editName} |
| #224 | onChange={(e) => setEditName(e.target.value)} |
| #225 | onKeyDown={(e) => { if (e.key === 'Enter') handleSaveEdit(); if (e.key === 'Escape') setEditingId(null); }} |
| #226 | autoFocus |
| #227 | /> |
| #228 | <div className="category-actions"> |
| #229 | <button className="btn btn-primary btn-sm" onClick={handleSaveEdit}>Save</button> |
| #230 | <button className="btn btn-secondary btn-sm" onClick={() => setEditingId(null)}>Cancel</button> |
| #231 | </div> |
| #232 | </> |
| #233 | ) : ( |
| #234 | <> |
| #235 | <span className="category-name">{cat.name}</span> |
| #236 | <div className="category-actions"> |
| #237 | <button className="btn btn-secondary btn-sm" onClick={() => startEdit(cat.id, cat.name)}><IconEdit size={14} /></button> |
| #238 | <button className="btn btn-danger btn-sm" onClick={() => { if (confirm(`Delete "${cat.name}" and all its products?`)) deleteCategory(cat.id); }}><IconTrash size={14} /></button> |
| #239 | </div> |
| #240 | </> |
| #241 | )} |
| #242 | </div> |
| #243 | ))} |
| #244 | {categories.length === 0 && <p className="empty-msg">No categories yet. Add one above.</p>} |
| #245 | </div> |
| #246 | </div> |
| #247 | ); |
| #248 | } |
| #249 | |
| #250 | function ProductsTab() { |
| #251 | const { categories, products, addProduct, updateProduct, deleteProduct, currentRestaurant } = useApp(); |
| #252 | const [filterCat, setFilterCat] = useState('all'); |
| #253 | const [showForm, setShowForm] = useState(false); |
| #254 | const [editingProduct, setEditingProduct] = useState<Product | null>(null); |
| #255 | const [form, setForm] = useState({ |
| #256 | name: '', description: '', price: '', categoryId: '', imageUrl: '', available: true, featured: false, |
| #257 | }); |
| #258 | |
| #259 | const filtered = filterCat === 'all' ? products : products.filter((p) => p.categoryId === filterCat); |
| #260 | const sorted = [...filtered].sort((a, b) => (b.featured ? 1 : 0) - (a.featured ? 1 : 0) || a.name.localeCompare(b.name)); |
| #261 | |
| #262 | const openAdd = () => { |
| #263 | setEditingProduct(null); |
| #264 | setForm({ name: '', description: '', price: '', categoryId: categories[0]?.id ?? '', imageUrl: '', available: true, featured: false }); |
| #265 | setShowForm(true); |
| #266 | }; |
| #267 | |
| #268 | const openEdit = (p: Product) => { |
| #269 | setEditingProduct(p); |
| #270 | setForm({ name: p.name, description: p.description, price: String(p.price), categoryId: p.categoryId, imageUrl: p.imageUrl, available: p.available, featured: p.featured }); |
| #271 | setShowForm(true); |
| #272 | }; |
| #273 | |
| #274 | const handleSave = (e: FormEvent) => { |
| #275 | e.preventDefault(); |
| #276 | if (!form.name.trim() || !form.categoryId || !currentRestaurant) return; |
| #277 | const data = { |
| #278 | name: form.name.trim(), |
| #279 | description: form.description.trim(), |
| #280 | price: Number(form.price) || 0, |
| #281 | categoryId: form.categoryId, |
| #282 | imageUrl: form.imageUrl.trim(), |
| #283 | available: form.available, |
| #284 | featured: form.featured, |
| #285 | restaurantId: currentRestaurant.id, |
| #286 | }; |
| #287 | if (editingProduct) { |
| #288 | updateProduct(editingProduct.id, data); |
| #289 | } else { |
| #290 | addProduct(data); |
| #291 | } |
| #292 | setShowForm(false); |
| #293 | }; |
| #294 | |
| #295 | const set = (key: string, value: string | boolean) => setForm((f) => ({ ...f, [key]: value })); |
| #296 | |
| #297 | return ( |
| #298 | <div> |
| #299 | <div className="products-toolbar"> |
| #300 | <select className="form-input filter-select" value={filterCat} onChange={(e) => setFilterCat(e.target.value)}> |
| #301 | <option value="all">All Categories</option> |
| #302 | {categories.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)} |
| #303 | </select> |
| #304 | <button className="btn btn-primary" onClick={openAdd}><IconPlus size={16} /> Add Product</button> |
| #305 | </div> |
| #306 | |
| #307 | <div className="product-list"> |
| #308 | {sorted.map((p) => { |
| #309 | const catName = categories.find((c) => c.id === p.categoryId)?.name ?? ''; |
| #310 | return ( |
| #311 | <div key={p.id} className="product-card-db"> |
| #312 | {p.imageUrl ? ( |
| #313 | <img src={p.imageUrl} alt={p.name} className="product-thumb" /> |
| #314 | ) : ( |
| #315 | <div className="product-thumb product-thumb-placeholder"><IconUtensils size={24} /></div> |
| #316 | )} |
| #317 | <div className="product-details"> |
| #318 | <h3>{p.name}</h3> |
| #319 | <p className="product-cat-price">{catName} · €{p.price.toFixed(2)}</p> |
| #320 | <div className="product-badges"> |
| #321 | {p.featured && <span className="badge badge-featured">Featured</span>} |
| #322 | {!p.available && <span className="badge badge-unavailable">Unavailable</span>} |
| #323 | </div> |
| #324 | </div> |
| #325 | <div className="product-actions"> |
| #326 | <button className="btn btn-secondary btn-sm" onClick={() => openEdit(p)}><IconEdit size={14} /></button> |
| #327 | <button className="btn btn-danger btn-sm" onClick={() => { if (confirm(`Delete "${p.name}"?`)) deleteProduct(p.id); }}><IconTrash size={14} /></button> |
| #328 | </div> |
| #329 | </div> |
| #330 | ); |
| #331 | })} |
| #332 | {sorted.length === 0 && <p className="empty-msg">No products found.</p>} |
| #333 | </div> |
| #334 | |
| #335 | {showForm && ( |
| #336 | <div className="modal-overlay" onClick={() => setShowForm(false)}> |
| #337 | <div className="modal-content" onClick={(e) => e.stopPropagation()}> |
| #338 | <div className="modal-header"> |
| #339 | <h2>{editingProduct ? 'Edit Product' : 'Add Product'}</h2> |
| #340 | <button className="modal-close" onClick={() => setShowForm(false)}><IconX size={20} /></button> |
| #341 | </div> |
| #342 | <form onSubmit={handleSave}> |
| #343 | <div className="form-group"> |
| #344 | <label className="form-label">Name</label> |
| #345 | <input className="form-input" value={form.name} onChange={(e) => set('name', e.target.value)} required /> |
| #346 | </div> |
| #347 | <div className="form-group"> |
| #348 | <label className="form-label">Description</label> |
| #349 | <textarea className="form-input" value={form.description} onChange={(e) => set('description', e.target.value)} rows={2} /> |
| #350 | </div> |
| #351 | <div className="form-row"> |
| #352 | <div className="form-group"> |
| #353 | <label className="form-label">Price</label> |
| #354 | <input className="form-input" type="number" step="0.01" value={form.price} onChange={(e) => set('price', e.target.value)} required /> |
| #355 | </div> |
| #356 | <div className="form-group"> |
| #357 | <label className="form-label">Category</label> |
| #358 | <select className="form-input" value={form.categoryId} onChange={(e) => set('categoryId', e.target.value)} required> |
| #359 | <option value="">Select...</option> |
| #360 | {categories.map((c) => <option key={c.id} value={c.id}>{c.name}</option>)} |
| #361 | </select> |
| #362 | </div> |
| #363 | </div> |
| #364 | <div className="form-group"> |
| #365 | <label className="form-label">Image URL</label> |
| #366 | <input className="form-input" value={form.imageUrl} onChange={(e) => set('imageUrl', e.target.value)} placeholder="https://..." /> |
| #367 | </div> |
| #368 | <div className="form-row checkbox-row"> |
| #369 | <label className="checkbox-label"> |
| #370 | <input type="checkbox" checked={form.available} onChange={(e) => set('available', e.target.checked)} /> |
| #371 | Available |
| #372 | </label> |
| #373 | <label className="checkbox-label"> |
| #374 | <input type="checkbox" checked={form.featured} onChange={(e) => set('featured', e.target.checked)} /> |
| #375 | Featured |
| #376 | </label> |
| #377 | </div> |
| #378 | <div className="modal-actions"> |
| #379 | <button className="btn btn-secondary" type="button" onClick={() => setShowForm(false)}>Cancel</button> |
| #380 | <button className="btn btn-primary" type="submit">{editingProduct ? 'Save' : 'Add Product'}</button> |
| #381 | </div> |
| #382 | </form> |
| #383 | </div> |
| #384 | </div> |
| #385 | )} |
| #386 | </div> |
| #387 | ); |
| #388 | } |
| #389 | |
| #390 | function QRTab() { |
| #391 | const { currentRestaurant } = useApp(); |
| #392 | const canvasRef = useRef<HTMLCanvasElement>(null); |
| #393 | const [copied, setCopied] = useState(false); |
| #394 | |
| #395 | const menuUrl = `${window.location.origin}/menu/${currentRestaurant?.slug ?? ''}`; |
| #396 | |
| #397 | useEffect(() => { |
| #398 | if (!canvasRef.current || !currentRestaurant) return; |
| #399 | drawQR(canvasRef.current, menuUrl); |
| #400 | }, [menuUrl, currentRestaurant]); |
| #401 | |
| #402 | const handleDownload = () => { |
| #403 | if (!canvasRef.current) return; |
| #404 | const link = document.createElement('a'); |
| #405 | link.download = `${currentRestaurant?.slug ?? 'menu'}-qr.png`; |
| #406 | link.href = canvasRef.current.toDataURL('image/png'); |
| #407 | link.click(); |
| #408 | }; |
| #409 | |
| #410 | const handleCopy = async () => { |
| #411 | try { |
| #412 | await navigator.clipboard.writeText(menuUrl); |
| #413 | setCopied(true); |
| #414 | setTimeout(() => setCopied(false), 2000); |
| #415 | } catch { |
| #416 | // fallback |
| #417 | window.prompt('Copy this URL:', menuUrl); |
| #418 | } |
| #419 | }; |
| #420 | |
| #421 | return ( |
| #422 | <div className="qr-section"> |
| #423 | <div className="qr-preview"> |
| #424 | <canvas ref={canvasRef} className="qr-canvas" /> |
| #425 | </div> |
| #426 | <p className="qr-url">{menuUrl}</p> |
| #427 | <div className="qr-actions"> |
| #428 | <button className="btn btn-primary" onClick={handleDownload}><IconDownload size={16} /> Download QR Code</button> |
| #429 | <button className="btn btn-secondary" onClick={handleCopy}>{copied ? <><IconCheck size={16} /> Copied!</> : <><IconCopy size={16} /> Copy Link</>}</button> |
| #430 | </div> |
| #431 | <p className="qr-info">Print this QR code and place it on tables, menus, or posters for customers to scan.</p> |
| #432 | </div> |
| #433 | ); |
| #434 | } |
| #435 | |
| #436 | function drawQR(canvas: HTMLCanvasElement, url: string) { |
| #437 | const ctx = canvas.getContext('2d'); |
| #438 | if (!ctx) return; |
| #439 | const size = 256; |
| #440 | canvas.width = size; |
| #441 | canvas.height = size; |
| #442 | |
| #443 | ctx.fillStyle = '#ffffff'; |
| #444 | ctx.fillRect(0, 0, size, size); |
| #445 | |
| #446 | const cellSize = 8; |
| #447 | const gridSize = Math.floor(size / cellSize); |
| #448 | const padding = 2; |
| #449 | |
| #450 | let hash = 0; |
| #451 | for (let i = 0; i < url.length; i++) { |
| #452 | hash = ((hash << 5) - hash) + url.charCodeAt(i); |
| #453 | hash = hash & hash; |
| #454 | } |
| #455 | |
| #456 | ctx.fillStyle = '#000000'; |
| #457 | |
| #458 | const drawFinder = (x: number, y: number) => { |
| #459 | for (let dy = 0; dy < 7; dy++) { |
| #460 | for (let dx = 0; dx < 7; dx++) { |
| #461 | const isOuter = dx === 0 || dx === 6 || dy === 0 || dy === 6; |
| #462 | const isInner = dx >= 2 && dx <= 4 && dy >= 2 && dy <= 4; |
| #463 | if (isOuter || isInner) { |
| #464 | ctx.fillRect((x + dx) * cellSize, (y + dy) * cellSize, cellSize, cellSize); |
| #465 | } |
| #466 | } |
| #467 | } |
| #468 | }; |
| #469 | |
| #470 | drawFinder(padding, padding); |
| #471 | drawFinder(gridSize - 7 - padding, padding); |
| #472 | drawFinder(padding, gridSize - 7 - padding); |
| #473 | |
| #474 | let seed = Math.abs(hash); |
| #475 | for (let y = padding; y < gridSize - padding; y++) { |
| #476 | for (let x = padding; x < gridSize - padding; x++) { |
| #477 | const inFinder1 = x < padding + 8 && y < padding + 8; |
| #478 | const inFinder2 = x > gridSize - padding - 9 && y < padding + 8; |
| #479 | const inFinder3 = x < padding + 8 && y > gridSize - padding - 9; |
| #480 | if (inFinder1 || inFinder2 || inFinder3) continue; |
| #481 | seed = (seed * 1103515245 + 12345) & 0x7fffffff; |
| #482 | if (seed % 3 !== 0) { |
| #483 | ctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); |
| #484 | } |
| #485 | } |
| #486 | } |
| #487 | |
| #488 | const centerX = size / 2; |
| #489 | const centerY = size / 2; |
| #490 | ctx.fillStyle = '#ffffff'; |
| #491 | ctx.fillRect(centerX - 44, centerY - 12, 88, 24); |
| #492 | ctx.fillStyle = '#000000'; |
| #493 | ctx.font = 'bold 10px sans-serif'; |
| #494 | ctx.textAlign = 'center'; |
| #495 | ctx.textBaseline = 'middle'; |
| #496 | const text = url.split('/').pop()?.toUpperCase() ?? 'MENU'; |
| #497 | ctx.fillText(text, centerX, centerY); |
| #498 | } |
| #499 |