SolLunar-Kitchen/backups/index.html.pre-cosmic-20260831-050953

848 lines
53 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#FFFFFF">
<link rel="icon" type="image/png" sizes="64x64" href="icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="icon-192.png">
<link rel="icon" type="image/png" sizes="512x512" href="icon-512.png">
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png">
<link rel="manifest" href="manifest.webmanifest">
<title>FEST 484 · SolLunar KITCHEN</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<style>
:root{
--bg:#FFFFFF; --bg2:#f8fafc;
--ink:#0A0A0A; --ink2:#475569; --muted:#94a3b8;
--line:#e2e8f0; --line2:#cbd5e1;
--blue:#3b82f6; --blue-soft:#eff6ff;
--pink:#d946ef; --pink-soft:#fdf4ff;
--gold:#eab308; --gold-soft:#fefce8;
--green:#22c55e; --green-soft:#f0fdf4;
--red:#ef4444; --red-soft:#fef2f2;
--black:#0a0a0a;
--radius:18px;
--shadow:0 1px 2px rgb(10 10 10 / .05), 0 8px 24px -12px rgb(10 10 10 / .12);
--font:-apple-system,'Avenir Next','Avenir',system-ui,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
--display:'Orbitron',var(--font);
}
*{box-sizing:border-box;margin:0;padding:0}
html{line-height:1.5}
body{font-family:var(--font);background:var(--bg2);color:var(--ink);-webkit-font-smoothing:antialiased}
#app{max-width:520px;margin:0 auto;min-height:100dvh;background:var(--bg);display:flex;flex-direction:column;position:relative;box-shadow:0 0 40px rgb(0 0 0/.06)}
/* ---------- header ---------- */
header{position:sticky;top:0;z-index:40;background:rgb(255 255 255/.85);backdrop-filter:blur(12px);border-bottom:1px solid var(--line);padding:12px 16px;display:flex;align-items:center;gap:10px}
.logo{width:44px;height:44px;flex:0 0 44px;object-fit:contain}
.brand{display:flex;flex-direction:column;line-height:1.15}
.brand b{font-family:var(--display);font-weight:900;font-size:15px;letter-spacing:2px}
.brand span{font-size:10px;color:var(--muted);letter-spacing:1.5px}
.header-right{margin-left:auto;display:flex;align-items:center;gap:8px}
.clock{font-family:var(--display);font-size:12px;font-weight:700;color:var(--ink2);letter-spacing:1px;background:var(--bg2);border:1px solid var(--line);border-radius:999px;padding:4px 10px}
.icon-btn{width:36px;height:36px;border-radius:999px;border:1px solid var(--line);background:#fff;color:var(--ink2);display:grid;place-items:center;cursor:pointer;transition:all .15s}
.icon-btn:active{transform:scale(.92)}
.icon-btn svg{width:17px;height:17px}
/* ---------- hero ---------- */
.hero{padding:26px 18px 18px;background:linear-gradient(180deg,#fff 0%,var(--bg2) 100%)}
.hero h1{font-family:var(--display);font-weight:900;font-size:26px;letter-spacing:3px;margin:10px 0 4px}
.hero h1 em{font-style:normal;background:linear-gradient(90deg,var(--blue),var(--pink));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.hero p{color:var(--ink2);font-size:13px;max-width:360px}
.chips{display:flex;flex-wrap:wrap;gap:6px;margin:14px 0 16px}
.chip{font-size:10px;font-weight:700;letter-spacing:1px;text-transform:uppercase;padding:5px 10px;border-radius:999px;border:1px solid}
.chip.c1{color:var(--ink);border-color:var(--ink);background:var(--black);color:#fff}
.chip.c2{color:#166534;border-color:#bbf7d0;background:var(--green-soft)}
.chip.c3{color:#86198f;border-color:#f5d0fe;background:var(--pink-soft)}
.chip.c4{color:#854d0e;border-color:#fde68a;background:var(--gold-soft)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:none;cursor:pointer;font-family:var(--font);font-weight:700;font-size:14px;border-radius:14px;padding:13px 22px;transition:transform .12s,box-shadow .12s}
.btn:active{transform:scale(.97)}
.btn-primary{background:var(--black);color:#fff;width:100%;box-shadow:var(--shadow)}
.btn-primary:disabled{opacity:.35;cursor:not-allowed}
.btn-ghost{background:#fff;color:var(--ink);border:1px solid var(--line2)}
.btn-blue{background:var(--blue);color:#fff;width:100%}
.btn-pink{background:linear-gradient(90deg,var(--blue),var(--pink));color:#fff;width:100%}
.btn-sm{padding:9px 14px;font-size:12px;border-radius:11px;width:auto}
/* ---------- day tabs ---------- */
.daytabs{display:flex;gap:8px;padding:4px 16px 12px;position:sticky;top:69px;background:rgb(255 255 255/.9);backdrop-filter:blur(8px);z-index:30}
.daytab{flex:1;border:1px solid var(--line);background:#fff;border-radius:14px;padding:9px 6px;cursor:pointer;text-align:center;transition:all .15s}
.daytab .d1{font-family:var(--display);font-weight:700;font-size:11px;letter-spacing:1px}
.daytab .d2{font-size:10px;color:var(--muted)}
.daytab.active{background:var(--black);border-color:var(--black);color:#fff;box-shadow:var(--shadow)}
.daytab.active .d2{color:#a3a3a3}
.daytab.today::after{content:'TODAY';display:block;font-size:8px;font-weight:900;letter-spacing:1px;color:var(--pink);margin-top:2px}
.daytab.active.today::after{color:var(--pink)}
/* ---------- timeline ---------- */
.timeline{padding:8px 16px 24px;position:relative}
.tl-head{display:flex;justify-content:space-between;align-items:baseline;margin:6px 2px 10px}
.tl-head h2{font-size:14px;font-weight:800;letter-spacing:.5px;text-transform:uppercase}
.tl-head span{font-size:11px;color:var(--muted)}
.tl-wrap{position:relative;padding-left:26px}
.tl-rail{position:absolute;left:9px;top:6px;bottom:6px;width:2px;background:var(--line);border-radius:2px}
.nowline{position:absolute;left:-4px;right:-8px;height:2px;background:var(--red);border-radius:2px;z-index:5}
.nowline::before{content:'NOW';position:absolute;left:-2px;top:-16px;font-size:8px;font-weight:900;letter-spacing:1px;color:var(--red);background:#fff;padding:1px 4px;border-radius:4px}
.nowline::after{content:'';position:absolute;left:-5px;top:-2px;width:8px;height:8px;border-radius:99px;background:var(--red);animation:pulse 1.6s infinite}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(1.5)}}
.item-card{position:relative;background:#fff;border:1px solid var(--line);border-radius:var(--radius);padding:14px;margin-bottom:12px;box-shadow:0 1px 2px rgb(10 10 10/.04);transition:border-color .2s,box-shadow .2s}
.item-card.is-open{border-color:var(--green);box-shadow:0 0 0 3px var(--green-soft),var(--shadow)}
.item-card.is-soon{border-color:#bfdbfe}
.item-card.is-ended{opacity:.62}
.tl-dot{position:absolute;left:-21px;top:22px;width:12px;height:12px;border-radius:99px;background:#fff;border:3px solid var(--line2);z-index:6}
.item-card.is-open .tl-dot{border-color:var(--green);background:var(--green)}
.item-card.is-soon .tl-dot{border-color:var(--blue)}
.tl-time{position:absolute;left:-26px;top:18px;transform:translateX(-100%);width:52px;text-align:right;font-family:var(--display);font-size:9px;font-weight:700;color:var(--muted);letter-spacing:.5px;line-height:1.3}
.item-top{display:flex;gap:12px;align-items:flex-start}
.item-emoji{width:46px;height:46px;flex:0 0 46px;border-radius:14px;background:var(--bg2);border:1px solid var(--line);display:grid;place-items:center;font-size:24px}
.item-name{font-size:15px;font-weight:800;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.item-desc{font-size:12px;color:var(--ink2);margin-top:2px}
.item-price{margin-left:auto;font-family:var(--display);font-weight:900;font-size:16px;white-space:nowrap}
.item-foot{display:flex;align-items:center;gap:8px;margin-top:12px;flex-wrap:wrap}
.status-pill{font-size:10px;font-weight:800;letter-spacing:.8px;text-transform:uppercase;padding:5px 10px;border-radius:999px;display:inline-flex;align-items:center;gap:6px}
.status-pill .dot{width:6px;height:6px;border-radius:99px;background:currentColor}
.sp-open{background:var(--green-soft);color:#15803d}
.sp-open .dot{animation:pulse 1.4s infinite}
.sp-soon{background:var(--blue-soft);color:#1d4ed8}
.sp-upcoming{background:var(--bg2);color:var(--muted);border:1px solid var(--line)}
.sp-ended{background:var(--bg2);color:var(--muted)}
.tags{display:flex;gap:4px}
.tag{font-size:9px;font-weight:800;letter-spacing:.5px;padding:3px 7px;border-radius:7px;background:var(--bg2);border:1px solid var(--line);color:var(--ink2)}
.add-btn{margin-left:auto;border:1px solid var(--black);background:#fff;color:var(--ink);font-weight:800;font-size:12px;border-radius:11px;padding:8px 14px;cursor:pointer;transition:all .12s;white-space:nowrap}
.add-btn:active{transform:scale(.94);background:var(--black);color:#fff}
.add-btn.in-cart{background:var(--green-soft);border-color:#bbf7d0;color:#15803d}
/* ---------- orders ---------- */
.orders{padding:16px}
.orders h2{font-size:14px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;margin-bottom:4px}
.orders .sub{font-size:12px;color:var(--muted);margin-bottom:14px}
.empty{text-align:center;padding:56px 20px;color:var(--muted)}
.empty .big{font-size:40px;margin-bottom:10px}
.order-card{background:#fff;border:1px solid var(--line);border-radius:var(--radius);padding:14px;margin-bottom:12px;box-shadow:0 1px 2px rgb(10 10 10/.04)}
.order-top{display:flex;align-items:center;gap:10px}
.order-code{font-family:var(--display);font-weight:900;font-size:15px;letter-spacing:1px}
.badge{font-size:9px;font-weight:900;letter-spacing:1px;padding:4px 9px;border-radius:99px;text-transform:uppercase}
.b-paid{background:var(--green-soft);color:#15803d}
.b-pending{background:var(--gold-soft);color:#854d0e}
.b-failed{background:var(--red-soft);color:#b91c1c}
.b-demo{background:var(--pink-soft);color:#86198f}
.order-meta{font-size:11px;color:var(--muted);margin:6px 0}
.order-items{font-size:12px;color:var(--ink2);border-top:1px dashed var(--line);padding-top:8px;margin-top:8px}
.order-total{display:flex;justify-content:space-between;font-size:13px;font-weight:800;margin-top:6px}
.order-total .btc{font-family:var(--display);font-size:11px;color:var(--blue);font-weight:700}
.order-actions{display:flex;gap:8px;margin-top:10px}
/* ---------- bottom nav + fab ---------- */
nav.bottom{position:sticky;bottom:0;z-index:40;background:rgb(255 255 255/.92);backdrop-filter:blur(12px);border-top:1px solid var(--line);display:flex;padding:8px 12px calc(10px + env(safe-area-inset-bottom))}
.nav-btn{flex:1;border:none;background:none;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:3px;padding:7px;border-radius:12px;font-size:10px;font-weight:700;letter-spacing:.5px;color:var(--muted);transition:all .15s}
.nav-btn svg{width:20px;height:20px}
.nav-btn.active{color:var(--ink);background:var(--bg2)}
.fab{position:absolute;right:16px;bottom:calc(74px + env(safe-area-inset-bottom));z-index:45;width:58px;height:58px;border-radius:20px;border:none;cursor:pointer;background:var(--black);color:#fff;display:grid;place-items:center;box-shadow:0 10px 24px -8px rgb(10 10 10/.5);transition:transform .15s}
.fab:active{transform:scale(.92)}
.fab svg{width:24px;height:24px}
.fab .count{position:absolute;top:-7px;right:-7px;min-width:22px;height:22px;border-radius:99px;background:var(--pink);color:#fff;font-size:11px;font-weight:900;display:grid;place-items:center;padding:0 5px;box-shadow:0 2px 6px rgb(0 0 0/.3)}
.fab.hidden{display:none}
/* ---------- sheet / modal ---------- */
.overlay{position:fixed;inset:0;background:rgb(10 10 10/.45);backdrop-filter:blur(3px);z-index:100;display:none;align-items:flex-end;justify-content:center}
.overlay.show{display:flex}
.sheet{width:100%;max-width:520px;background:#fff;border-radius:24px 24px 0 0;padding:10px 18px calc(22px + env(safe-area-inset-bottom));max-height:88dvh;overflow-y:auto;animation:slideUp .3s cubic-bezier(.16,1,.3,1)}
@keyframes slideUp{from{transform:translateY(60px);opacity:0}to{transform:translateY(0);opacity:1}}
.grab{width:40px;height:4px;border-radius:4px;background:var(--line2);margin:4px auto 14px}
.sheet h3{font-family:var(--display);font-weight:900;font-size:16px;letter-spacing:1.5px;margin-bottom:4px}
.sheet .sub{font-size:12px;color:var(--muted);margin-bottom:14px}
.cart-row{display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px solid var(--line)}
.cart-row .em{width:40px;height:40px;border-radius:12px;background:var(--bg2);display:grid;place-items:center;font-size:20px;border:1px solid var(--line)}
.cart-row .nm{font-size:13px;font-weight:700}
.cart-row .pt{font-size:11px;color:var(--muted)}
.qty{display:flex;align-items:center;gap:10px;margin-left:auto}
.qty button{width:28px;height:28px;border-radius:9px;border:1px solid var(--line2);background:#fff;font-size:15px;font-weight:800;cursor:pointer;color:var(--ink)}
.qty button:active{transform:scale(.9)}
.qty span{font-family:var(--display);font-weight:700;font-size:13px;min-width:18px;text-align:center}
.cart-row .lp{font-family:var(--display);font-weight:900;font-size:13px;width:52px;text-align:right}
.total-row{display:flex;justify-content:space-between;align-items:baseline;padding:14px 0 4px}
.total-row .t{font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:1px}
.total-row .v{font-family:var(--display);font-weight:900;font-size:20px}
.btc-est{font-family:var(--display);font-size:11px;color:var(--blue);font-weight:700;margin-bottom:14px;text-align:right}
.btc-est small{color:var(--muted);font-family:var(--font);font-weight:400}
.field{margin-bottom:12px}
.field label{display:block;font-size:11px;font-weight:800;letter-spacing:.5px;text-transform:uppercase;color:var(--ink2);margin-bottom:6px}
.field input,.field select,.field textarea{width:100%;border:1px solid var(--line2);border-radius:12px;padding:12px 14px;font-family:var(--font);font-size:14px;background:var(--bg2);color:var(--ink);outline:none;transition:border-color .15s}
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--blue)}
.field textarea{resize:none;height:70px}
.hint{font-size:11px;color:var(--muted);margin:-6px 0 12px}
.pay-box{border:1px solid var(--line);border-radius:var(--radius);background:var(--bg2);padding:16px;text-align:center;margin-bottom:14px}
.pay-amount{font-family:var(--display);font-weight:900;font-size:28px;margin:6px 0 2px}
.pay-btc{font-family:var(--display);font-weight:700;font-size:13px;color:var(--blue);margin-bottom:12px}
.qr-wrap{background:#fff;border:1px solid var(--line);border-radius:16px;padding:12px;display:inline-block}
.qr-wrap canvas,.qr-wrap img{display:block;border-radius:8px}
.addr-row{display:flex;align-items:center;gap:8px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin-top:12px}
.addr-row code{flex:1;font-size:10px;word-break:break-all;color:var(--ink2);font-family:ui-monospace,Menlo,monospace}
.copy-btn{border:1px solid var(--line2);background:var(--bg2);border-radius:9px;font-size:11px;font-weight:800;padding:7px 12px;cursor:pointer;white-space:nowrap}
.pay-status{display:flex;align-items:center;justify-content:center;gap:8px;font-size:12px;font-weight:700;color:var(--ink2);margin:12px 0}
.spinner{width:14px;height:14px;border:2px solid var(--line);border-top-color:var(--blue);border-radius:99px;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.pay-status.ok{color:#15803d}
.pay-status.err{color:#b91c1c}
.pay-tabs{display:flex;gap:6px;margin-bottom:12px}
.pay-tab{flex:1;border:1px solid var(--line);background:#fff;border-radius:11px;padding:9px;font-size:12px;font-weight:800;cursor:pointer;color:var(--muted)}
.pay-tab.active{background:var(--black);color:#fff;border-color:var(--black)}
.demo-note{background:var(--gold-soft);border:1px solid #fde68a;color:#854d0e;font-size:11px;font-weight:600;border-radius:12px;padding:10px 12px;margin-bottom:12px}
.success{text-align:center;padding:18px 0 6px}
.success .check{width:74px;height:74px;border-radius:99px;background:var(--green-soft);border:2px solid #bbf7d0;display:grid;place-items:center;margin:0 auto 14px;font-size:34px;animation:pop .5s cubic-bezier(.16,1,.3,1)}
@keyframes pop{from{transform:scale(.4);opacity:0}to{transform:scale(1);opacity:1}}
.success h3{margin-bottom:6px}
.success .code{font-family:var(--display);font-weight:900;font-size:24px;letter-spacing:3px;background:var(--bg2);border:1px dashed var(--line2);border-radius:14px;padding:12px;margin:14px 0}
.success .pick{font-size:13px;color:var(--ink2);margin-bottom:16px}
.success .pick b{color:var(--ink)}
/* ---------- settings ---------- */
.modal{position:fixed;inset:0;background:rgb(10 10 10/.45);backdrop-filter:blur(3px);z-index:110;display:none;align-items:center;justify-content:center;padding:20px}
.modal.show{display:flex}
.modal-box{width:100%;max-width:460px;background:#fff;border-radius:22px;padding:20px;max-height:86dvh;overflow-y:auto;animation:slideUp .3s cubic-bezier(.16,1,.3,1)}
.modal-box h3{font-family:var(--display);font-weight:900;font-size:15px;letter-spacing:1.5px;margin-bottom:10px}
.test-result{font-size:11px;font-weight:700;border-radius:10px;padding:9px 12px;margin-bottom:12px;display:none}
.test-result.ok{display:block;background:var(--green-soft);color:#15803d}
.test-result.err{display:block;background:var(--red-soft);color:#b91c1c}
footer{padding:18px;text-align:center;font-size:10px;color:var(--muted);letter-spacing:1px;text-transform:uppercase;border-top:1px solid var(--line);margin-top:auto}
.toast{position:fixed;left:50%;bottom:100px;transform:translateX(-50%) translateY(20px);background:var(--black);color:#fff;font-size:12px;font-weight:700;padding:10px 18px;border-radius:99px;opacity:0;transition:all .25s;z-index:200;pointer-events:none;white-space:nowrap}
.toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
</style>
</head>
<body>
<div id="app">
<header>
<img class="logo" src="logo.png" alt="FEST 484">
<div class="brand"><b>FEST 484</b><span>SolLunar KITCHEN</span></div>
<div class="header-right">
<a class="icon-btn" href="home.html" aria-label="Home"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l9-8 9 8"/><path d="M5 10v10h5v-6h4v6h5V10"/></svg></a>
<div class="clock" id="clock">--:--</div>
<button class="icon-btn" id="btn-settings" aria-label="Settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51h.01a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</button>
</div>
</header>
<main style="flex:1;display:flex;flex-direction:column">
<!-- SCHEDULE VIEW -->
<section id="view-schedule">
<div class="hero">
<div class="chips">
<span class="chip c1">NO SEED OILS</span>
<span class="chip c2">GRASS FED</span>
<span class="chip c3">VEG FRIENDLY</span>
<span class="chip c4">CASHLESS · BTC</span>
</div>
<h1>WHEN'S <em>FOOD</em> ON?</h1>
<p>Live schedule for the SolLunar kitchen. See when each stand is serving, pre-order for a window, and pay with Bitcoin — no cash, no cards.</p>
</div>
<div class="daytabs" id="daytabs"></div>
<div class="timeline" id="timeline">
<div class="tl-head"><h2>Serving schedule</h2><span id="tl-sub"></span></div>
<div class="tl-wrap" id="tl-wrap">
<div class="tl-rail"></div>
</div>
</div>
</section>
<!-- ORDERS VIEW -->
<section id="view-orders" hidden>
<div class="orders">
<h2>My orders</h2>
<div class="sub">Show the code at the pickup stand. Paid orders are confirmed on-chain.</div>
<div id="orders-list"></div>
</div>
</section>
<footer>FEST 484 · Off-grid · Zero waste · Powered by BTCPay</footer>
</main>
<nav class="bottom">
<button class="nav-btn active" data-view="schedule">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 3"/></svg>
SCHEDULE
</button>
<button class="nav-btn" data-view="orders">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="M9 12h6M9 16h4"/></svg>
ORDERS
</button>
</nav>
<button class="fab hidden" id="fab" aria-label="Cart">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>
<span class="count" id="fab-count">0</span>
</button>
</div>
<!-- SHEET -->
<div class="overlay" id="sheet-overlay"><div class="sheet" id="sheet"></div></div>
<!-- SETTINGS MODAL -->
<div class="modal" id="settings-modal">
<div class="modal-box">
<h3>⚙️ PAYMENT SETTINGS</h3>
<p style="font-size:12px;color:var(--ink2);margin-bottom:14px">Connect a <b>BTCPay Server</b> store to receive real Bitcoin payments. Find your Store ID in the store's settings on your BTCPay instance, and generate a store API key with "Create invoice" permission.</p>
<div class="field"><label>BTCPay server URL</label><input id="set-url" type="url" placeholder="https://btcpay.yourhost.com"></div>
<div class="field"><label>Store ID</label><input id="set-store" type="text" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"></div>
<div class="field"><label>Store API key (optional but recommended)</label><input id="set-key" type="password" placeholder="lk_… / bk_…"></div>
<div class="test-result" id="test-result"></div>
<div style="display:flex;gap:8px;margin-bottom:10px">
<button class="btn btn-ghost btn-sm" id="btn-test">Test connection</button>
<button class="btn btn-primary btn-sm" id="btn-save-settings" style="margin-left:auto">Save</button>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-ghost btn-sm" id="btn-clear-orders">Clear orders</button>
<button class="btn btn-ghost btn-sm" style="margin-left:auto" id="btn-close-settings">Close</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<script>
"use strict";
window.__errs=[];
window.addEventListener('error',e=>{
window.__errs.push(String(e.message)+' @'+(e.lineno||'?'));
let el=document.getElementById('__errbox');
if(!el){el=document.createElement('div');el.id='__errbox';el.style.cssText='position:fixed;top:0;left:0;right:0;z-index:9999;background:#7f1d1d;color:#fff;font:11px monospace;padding:8px;max-height:40vh;overflow:auto';document.body.appendChild(el);}
el.textContent=window.__errs.join('\n');
});
/* ============================== DATA ============================== */
const FEST_START=new Date(2026,9,8); /* Oct 8, 2026 — festival start (coincides with first page) */
function dayLabel(offset){
const d=new Date(FEST_START); d.setDate(d.getDate()+offset);
return {
name:d.toLocaleDateString('en-US',{weekday:'short'}),
date:d.toLocaleDateString('en-US',{month:'short',day:'numeric'}),
iso:d.toISOString().slice(0,10)
};
}
const FEST={
days:[
{ tag:'DAY 1', items:[
{id:'d1-coffee', name:'Coffee Station', emoji:'☕', price:4, start:540, end:1080, tags:['VG'], desc:'Slow-drip single origin + oat milk latte, served from the wood-fired cart.'},
{id:'d1-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1320, tags:['V','GF'], desc:'Fresh fruit skewers, sorbet cups, and festival brownies.'},
{id:'d1-taco', name:'Taco Stand', emoji:'🌮', price:8, start:720, end:870, tags:['GF'], desc:'Hand-pressed corn tortillas, slow-braised grass-fed filling, house salsa.'},
{id:'d1-bbq', name:'BBQ Pit', emoji:'🍖', price:12, start:780, end:930, tags:[], desc:'12-hour offset-smoked brisket & ribs. No seed oils — rendered tallow only.'},
{id:'d1-chicken',name:'Chicken Grill', emoji:'🍗', price:9, start:1050, end:1230, tags:['GF'], desc:'Buttermilk fried chicken (tallow-fried) with slaw.'},
{id:'d1-pasta', name:'Pasta Bar', emoji:'🍝', price:10, start:1080, end:1200, tags:['V'], desc:'Fresh-cut daily pasta, garden vegetables, house olive oil.'},
{id:'d1-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1320, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'}
]},
{ tag:'DAY 2', items:[
{id:'d2-coffee', name:'Coffee Station', emoji:'☕', price:4, start:570, end:1020, tags:['VG'], desc:'Slow-drip single origin + oat milk latte.'},
{id:'d2-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1260, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, brownies.'},
{id:'d2-burger', name:'Garden Burgers', emoji:'🍔', price:11, start:720, end:900, tags:[], desc:'Grass-fed beef patty, brioche, charred onion, herb aioli.'},
{id:'d2-grill', name:'Grill 484', emoji:'🔥', price:9, start:750, end:960, tags:['GF'], desc:'Skewered vegetables & chicken over open flame.'},
{id:'d2-ramen', name:'Ramen Tent', emoji:'🍜', price:12, start:1020, end:1200, tags:['V'], desc:'Miso broth simmered all day, soft egg, scallion, pickled ginger.'},
{id:'d2-drinks', name:'Drinks', emoji:'🥤', price:3, start:570, end:1290, tags:['VG'], desc:'Cold brew, kombucha, herbal tea, lemonade.'}
]},
{ tag:'DAY 3', items:[
{id:'d3-coffee', name:'Coffee Station', emoji:'☕', price:4, start:540, end:1050, tags:['VG'], desc:'Slow-drip single origin, Saturday long pour.'},
{id:'d3-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1320, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, Saturday brownie batch.'},
{id:'d3-flatbread',name:'Flatbread Oven', emoji:'🫓', price:9, start:720, end:930, tags:['V','GF'], desc:'Wood-fired flatbreads — charred vegetables, whipped feta, house olive oil.'},
{id:'d3-bbq', name:'BBQ Pit', emoji:'🍖', price:12, start:780, end:960, tags:[], desc:'Offset-smoked brisket & ribs. The weekend is for the pit.'},
{id:'d3-dumplings',name:'Dumpling Bar', emoji:'🥟', price:10, start:1020, end:1230, tags:['GF'], desc:'Pan-seared vegetable & chicken dumplings, chili crisp.'},
{id:'d3-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1320, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'}
]},
{ tag:'DAY 4', items:[
{id:'d4-coffee', name:'Coffee Station', emoji:'☕', price:4, start:540, end:1020, tags:['VG'], desc:'Slow-drip single origin + oat milk latte.'},
{id:'d4-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1290, tags:['V','GF'], desc:'Sorbet cups, fruit skewers, brownies.'},
{id:'d4-taco', name:'Taco Stand', emoji:'🌮', price:8, start:720, end:870, tags:['GF'], desc:'Corn tortillas, slow-braised grass-fed filling, house salsa.'},
{id:'d4-pasta', name:'Pasta Bar', emoji:'🍝', price:10, start:900, end:1140, tags:['V'], desc:'Sunday pasta — fresh-cut, garden vegetables, house olive oil.'},
{id:'d4-grill', name:'Grill 484', emoji:'🔥', price:9, start:960, end:1170, tags:['GF'], desc:'Skewered vegetables & chicken over open flame.'},
{id:'d4-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1290, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'}
]},
{ tag:'FINAL DAY', items:[
{id:'d5-coffee', name:'Coffee Station', emoji:'☕', price:4, start:600, end:960, tags:['VG'], desc:'One last pour. Single origin, slow drip.'},
{id:'d5-sweets', name:'Sweets Cart', emoji:'🍭', price:5, start:660, end:1200, tags:['V','GF'], desc:'Sorbet, fruit, brownies — farewell batch.'},
{id:'d5-taco', name:'Taco Stand', emoji:'🌮', price:8, start:720, end:840, tags:['GF'], desc:'Corn tortillas, slow-braised filling.'},
{id:'d5-feast', name:'Farewell Feast · BBQ', emoji:'🍖', price:14, start:720, end:960, tags:[], desc:'The full spread: brisket, ribs, tallow fries, slaw, bread.'},
{id:'d5-pasta', name:'Pasta Bar', emoji:'🍝', price:10, start:1020, end:1170, tags:['V'], desc:'Fresh pasta, garden vegetables, olive oil.'},
{id:'d5-drinks', name:'Drinks', emoji:'🥤', price:3, start:600, end:1230, tags:['VG'], desc:'Cold brew, kombucha, iced herbal tea, lemonade.'}
]}
]
};
FEST.days.forEach((d,i)=>{ const L=dayLabel(i); d.name=L.name; d.date=L.date; d.iso=L.iso; });
/* ============================== STATE ============================== */
const LS={cart:'fest484_cart',orders:'fest484_orders',btcpay:'fest484_btcpay'};
let state={
view:'schedule',
dayIdx:0,
cart:JSON.parse(localStorage.getItem(LS.cart)||'{}'),
orders:JSON.parse(localStorage.getItem(LS.orders)||'[]'),
settings:JSON.parse(localStorage.getItem(LS.btcpay)||'null'),
sheetStep:null,
btcPrice:null,
pay:null
};
const saveCart=()=>localStorage.setItem(LS.cart,JSON.stringify(state.cart));
const saveOrders=()=>localStorage.setItem(LS.orders,JSON.stringify(state.orders));
const saveSettings=()=>localStorage.setItem(LS.btcpay,JSON.stringify(state.settings));
const $=s=>document.querySelector(s);
const fmt$=n=>'$'+n.toFixed(2);
const fmtT=min=>{let h=Math.floor(min/60),m=min%60,ap=h>=12?'PM':'AM';h=h%12||12;return h+':'+String(m).padStart(2,'0')+' '+ap;};
const fmtDur=min=>{const h=Math.floor(min/60),m=Math.round(min%60);return h? (h+'h'+(m?' '+m+'m':'')) : m+'m';};
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function toast(msg){const t=$('#toast');t.textContent=msg;t.classList.add('show');clearTimeout(t._x);t._x=setTimeout(()=>t.classList.remove('show'),2200);}
function cartTotal(){let t=0;for(const id in state.cart){const it=findItem(id);if(it)t+=it.price*state.cart[id];}return t;}
function cartCount(){return Object.values(state.cart).reduce((a,b)=>a+b,0);}
function findItem(id){for(const d of FEST.days)for(const it of d.items)if(it.id===id)return it;return null;}
function findDayOf(id){for(let i=0;i<FEST.days.length;i++)if(FEST.days[i].items.some(x=>x.id===id))return i;}
/* ============================== STATUS ============================== */
function nowMin(){const d=new Date();return d.getHours()*60+d.getMinutes();}
function itemStatus(i,item){
if(i>0)return{state:'upcoming',label:FEST.days[i].name+' · '+FEST.days[i].date};
const n=nowMin();
if(n<item.start)return{state:'soon',label:'Starts in '+fmtDur(item.start-n)};
if(n<item.end)return{state:'open',label:'Available now'};
return{state:'ended',label:'Ended'};
}
/* ============================== SCHEDULE RENDER ============================== */
function renderDayTabs(){
$('#daytabs').innerHTML=FEST.days.map((d,i)=>
'<button class="daytab'+(i===state.dayIdx?' active':'')+(i===0?' today':'')+'" data-day="'+i+'">'+
'<div class="d1">'+d.tag+'</div><div class="d2">'+d.name+' '+d.date+'</div></button>').join('');
document.querySelectorAll('.daytab').forEach(b=>b.onclick=()=>{state.dayIdx=+b.dataset.day;renderDayTabs();renderTimeline();});
}
function renderTimeline(){
const day=FEST.days[state.dayIdx];
const items=[...day.items].sort((a,b)=>a.start-b.start);
const d0=Math.min(...items.map(x=>x.start))-30;
const d1=Math.max(...items.map(x=>x.end))+30;
const isToday=state.dayIdx===0;
const n=nowMin();
const nowLine=(isToday&&n>=d0&&n<=d1)
?'<div class="nowline" style="top:'+(((n-d0)/(d1-d0))*100).toFixed(2)+'%"></div>':'';
$('#tl-sub').textContent=isToday?'Live · updates automatically':'Pre-orders open';
$('#tl-wrap').innerHTML='<div class="tl-rail"></div>'+nowLine+items.map(item=>{
const st=itemStatus(state.dayIdx,item);
const cls=st.state==='open'?'is-open':st.state==='soon'?'is-soon':(st.state==='upcoming'?'':'is-ended');
const pill={open:'sp-open',soon:'sp-soon',upcoming:'sp-upcoming',ended:'sp-ended'}[st.state];
const inCart=state.cart[item.id]>0;
return '<div class="item-card '+cls+'" data-id="'+item.id+'">'+
'<div class="tl-dot"></div>'+
'<div class="tl-time">'+fmtT(item.start).replace(' ','<br>')+'</div>'+
'<div class="item-top">'+
'<div class="item-emoji">'+item.emoji+'</div>'+
'<div><div class="item-name">'+item.name+' <span class="tags">'+item.tags.map(t=>'<span class="tag">'+t+'</span>').join('')+'</span></div>'+
'<div class="item-desc">'+item.desc+'</div></div>'+
'<div class="item-price">'+fmt$(item.price)+'</div>'+
'</div>'+
'<div class="item-foot">'+
'<span class="status-pill '+pill+'"><span class="dot"></span>'+st.label+'</span>'+
'<span class="tag" style="margin-left:auto">'+fmtT(item.start)+' – '+fmtT(item.end)+'</span>'+
'<button class="add-btn'+(inCart?' in-cart':'')+'" data-add="'+item.id+'">'+(inCart?'✓ Added · '+state.cart[item.id]:'+ Add to order')+'</button>'+
'</div></div>';
}).join('');
document.querySelectorAll('[data-add]').forEach(b=>b.onclick=()=>{
const id=b.dataset.add;
state.cart[id]=(state.cart[id]||0)+1;
saveCart();renderFab();renderTimeline();
toast(findItem(id).emoji+' '+findItem(id).name+' added');
});
}
function refreshStatuses(){
document.querySelectorAll('#tl-wrap .item-card').forEach(card=>{
const id=card.dataset.id, it=findItem(id);
const st=itemStatus(state.dayIdx,it);
const pill=card.querySelector('.status-pill');
pill.className='status-pill '+( {open:'sp-open',soon:'sp-soon',upcoming:'sp-upcoming',ended:'sp-ended'}[st.state] );
pill.innerHTML='<span class="dot"></span>'+st.label;
card.classList.remove('is-open','is-soon','is-ended');
if(st.state==='open')card.classList.add('is-open');
else if(st.state==='soon')card.classList.add('is-soon');
else if(st.state==='ended')card.classList.add('is-ended');
});
}
/* ============================== FAB / NAV ============================== */
function renderFab(){
const c=cartCount();
$('#fab').classList.toggle('hidden',c===0);
$('#fab-count').textContent=c;
document.title=c>0?('FEST 484 · '+c+' in cart'):('FEST 484 · SolLunar KITCHEN');
let d=document.getElementById('debugbar');
if(!d){d=document.createElement('div');d.id='debugbar';d.style.cssText='position:fixed;bottom:0;left:0;right:0;z-index:9998;background:#0A0A0A;color:#4ade80;font:10px monospace;padding:4px 8px;pointer-events:none';document.body.appendChild(d);}
d.textContent='cart='+JSON.stringify(state.cart)+' errs='+(window.__errs?window.__errs.length:0);
}
document.querySelectorAll('.nav-btn').forEach(b=>b.onclick=()=>{
state.view=b.dataset.view;
document.querySelectorAll('.nav-btn').forEach(x=>x.classList.toggle('active',x===b));
$('#view-schedule').hidden=state.view!=='schedule';
$('#view-orders').hidden=state.view!=='orders';
if(state.view==='orders')renderOrders();
});
$('#fab').onclick=()=>openSheet('cart');
/* ============================== SHEET STEPS ============================== */
function openSheet(step){state.sheetStep=step;renderSheet();$('#sheet-overlay').classList.add('show');}
function closeSheet(){
if(state.pay&&state.pay.timer){clearInterval(state.pay.timer);state.pay=null;}
state.sheetStep=null;$('#sheet-overlay').classList.remove('show');
}
$('#sheet-overlay').addEventListener('mousedown',e=>{$('#sheet-overlay')._down=e.target.id==='sheet-overlay';});
$('#sheet-overlay').addEventListener('click',e=>{if($('#sheet-overlay')._down&&e.target.id==='sheet-overlay')closeSheet();});
function renderSheet(){
const s=$('#sheet');
if(state.sheetStep==='cart')s.innerHTML=stepCart();
else if(state.sheetStep==='checkout')s.innerHTML=stepCheckout();
else if(state.sheetStep==='payment')s.innerHTML=stepPayment();
else if(state.sheetStep==='success')s.innerHTML=stepSuccess();
wireSheet();
}
function stepCart(){
const rows=Object.keys(state.cart).map(id=>{
const it=findItem(id);if(!it)return'';
return '<div class="cart-row" data-row="'+id+'">'+
'<div class="em">'+it.emoji+'</div>'+
'<div><div class="nm">'+it.name+'</div><div class="pt">'+fmtT(it.start)+' – '+fmtT(it.end)+'</div></div>'+
'<div class="qty"><button data-dec="'+id+'">−</button><span>'+state.cart[id]+'</span><button data-inc="'+id+'">+</button></div>'+
'<div class="lp">'+fmt$(it.price*state.cart[id])+'</div></div>';
}).join('');
const total=cartTotal();
const btc=state.btcPrice?'≈ '+(total/state.btcPrice).toFixed(6)+' BTC <small>@ $'+state.btcPrice.toFixed(0)+'</small>':'';
return '<div class="grab"></div><h3>YOUR ORDER</h3><div class="sub">SolLunar KITCHEN · pickup at the stand window</div>'+
(rows||'<div class="empty" style="padding:30px"><div class="big">🧺</div>Nothing here yet — add something from the schedule.</div>')+
'<div class="total-row"><span class="t">Total</span><span class="v">'+fmt$(total)+'</span></div>'+
'<div class="btc-est">'+(btc||'Fetching BTC price…')+'</div>'+
'<button class="btn btn-primary" id="to-checkout" '+(cartCount()===0?'disabled':'')+'>Checkout →</button>';
}
function stepCheckout(){
const days=[...new Set(Object.keys(state.cart).map(findDayOf))].map(i=>FEST.days[i]);
const windows=[...new Set(days.flatMap(d=>d.items.map(it=>it.id)))]
.map(id=>({id,start:findItem(id).start,end:findItem(id).end,name:findItem(id).name}))
.sort((a,b)=>a.start-b.start);
const n=nowMin();
const def=windows.find(w=>w.end>n)||windows[0];
const opts=windows.map(w=>'<option value="'+w.start+'-'+w.end+'" '+(def&&w.start===def.start&&w.end===def.end?'selected':'')+'>'+
fmtT(w.start)+' – '+fmtT(w.end)+' · '+w.name+'</option>').join('');
return '<div class="grab"></div><h3>CHECKOUT</h3><div class="sub">Who is picking this up?</div>'+
'<div class="field"><label>Name</label><input id="co-name" type="text" placeholder="Your name at the stand" maxlength="40"></div>'+
'<div class="field"><label>Pickup window</label><select id="co-window">'+opts+'</select></div>'+
'<div class="field"><label>Notes (optional)</label><textarea id="co-notes" placeholder="Allergies, extras, anything the kitchen should know…"></textarea></div>'+
'<div class="total-row"><span class="t">Total · '+cartCount()+' items</span><span class="v">'+fmt$(cartTotal())+'</span></div>'+
'<button class="btn btn-pink" id="to-payment" style="margin-top:8px">₿&nbsp; Pay with Bitcoin</button>'+
'<button class="btn btn-ghost" style="width:100%;margin-top:8px" id="back-cart">← Back to cart</button>';
}
function stepPayment(){
const p=state.pay;
if(!p)return'<div class="grab"></div><div class="empty">Payment not started.</div>';
const addr=p.method==='bolt11'?p.bolt11:p.address;
const tabs=(p.real&&p.bolt11)?'<div class="pay-tabs">'+
'<button class="pay-tab'+(p.method==='btc'?' active':'')+'" data-pm="btc">₿ Bitcoin</button>'+
'<button class="pay-tab'+(p.method==='bolt11'?' active':'')+'" data-pm="bolt11">⚡ Lightning</button></div>':'';
return '<div class="grab"></div><h3>PAYMENT</h3>'+
'<div class="sub">'+(p.real?'BTCPay invoice '+p.invoiceId:'Demo invoice — no real transaction')+'</div>'+
(p.real?'':'<div class="demo-note">⚠️ <b>Demo mode.</b> No BTCPay Server configured — open ⚙️ settings to connect a real store. This QR is a placeholder.</div>')+
tabs+
'<div class="pay-box">'+
'<div style="font-size:11px;letter-spacing:1px;color:var(--muted);text-transform:uppercase">Send exactly</div>'+
'<div class="pay-amount">'+fmt$(p.amountUsd)+'</div>'+
'<div class="pay-btc">'+(state.btcPrice?'≈ '+(p.amountUsd/state.btcPrice).toFixed(6)+' BTC':'≈ BTC price unavailable')+'</div>'+
'<div class="qr-wrap" id="qr-wrap"></div>'+
'<div class="addr-row"><code id="pay-addr">'+esc(addr)+'</code><button class="copy-btn" id="copy-addr">Copy</button></div>'+
'</div>'+
'<div class="pay-status" id="pay-status"><span class="spinner"></span> Waiting for payment…</div>'+
(p.real?'<a class="btn btn-ghost" style="width:100%;text-decoration:none;color:var(--ink)" href="'+p.checkoutUrl+'" target="_blank" rel="noopener">Open BTCPay checkout →</a>':
'<button class="btn btn-blue" id="simulate-pay" style="margin-top:4px">⚡ Simulate payment received (demo)</button>')+
'<button class="btn btn-ghost" style="width:100%;margin-top:8px" id="cancel-pay">Cancel</button>';
}
function stepSuccess(){
const o=state.pay&&state.pay.order;
if(!o)return'';
return '<div class="grab"></div><div class="success">'+
'<div class="check">✓</div>'+
'<h3>ORDER CONFIRMED</h3>'+
'<div class="sub" style="margin-bottom:0">'+(o.status==='PAID (DEMO)'?'Demo payment recorded — connect BTCPay for real orders':'Payment detected')+'</div>'+
'<div class="code">'+o.code+'</div>'+
'<div class="pick">Pickup: <b>'+o.pickupLabel+'</b><br>'+o.itemsSummary+'</div>'+
'<div class="pay-amount" style="font-size:20px">'+fmt$(o.totalUsd)+'</div>'+
'<div class="pay-btc" style="margin-bottom:18px">'+(o.btcEstimate||'')+'</div>'+
'<button class="btn btn-primary" id="success-done">Done</button>'+
'</div>';
}
function wireSheet(){
const s=$('#sheet');
s.querySelectorAll('[data-inc]').forEach(b=>b.onclick=()=>{state.cart[b.dataset.inc]++;afterCartChange();});
s.querySelectorAll('[data-dec]').forEach(b=>b.onclick=()=>{
const id=b.dataset.dec;state.cart[id]--;
if(state.cart[id]<=0)delete state.cart[id];
afterCartChange();
});
const tc=s.querySelector('#to-checkout');if(tc)tc.onclick=()=>openSheet('checkout');
const bc=s.querySelector('#back-cart');if(bc)bc.onclick=()=>openSheet('cart');
const tp=s.querySelector('#to-payment');
if(tp)tp.onclick=async()=>{
const name=$('#co-name').value.trim();
if(!name){toast('Please add a pickup name');$('#co-name').focus();return;}
const win=$('#co-window').value.split('-').map(Number);
const notes=$('#co-notes').value.trim();
await startPayment({name,window:win,notes});
};
s.querySelectorAll('[data-pm]').forEach(b=>b.onclick=()=>{
state.pay.method=b.dataset.pm;
const addr=state.pay.method==='bolt11'?state.pay.bolt11:state.pay.address;
$('#pay-addr').textContent=addr;
s.querySelectorAll('[data-pm]').forEach(x=>x.classList.toggle('active',x===b));
drawQR($('#qr-wrap'),addr);
});
const cp=s.querySelector('#copy-addr');
if(cp)cp.onclick=async()=>{
const t=$('#pay-addr').textContent;
try{await navigator.clipboard.writeText(t);toast('Copied');}
catch(e){const ta=document.createElement('textarea');ta.value=t;document.body.appendChild(ta);ta.select();document.execCommand('copy');ta.remove();toast('Copied');}
};
const sim=s.querySelector('#simulate-pay');
if(sim)sim.onclick=()=>completeOrder('PAID (DEMO)');
const cx=s.querySelector('#cancel-pay');
if(cx)cx.onclick=()=>{
if(state.pay&&state.pay.timer)clearInterval(state.pay.timer);
state.pay=null;openSheet('checkout');
};
const done=s.querySelector('#success-done');
if(done)done.onclick=()=>{
closeSheet();
document.querySelector('[data-view="orders"]').click();
};
}
function afterCartChange(){saveCart();renderFab();renderTimeline();renderSheet();}
/* ============================== QR ============================== */
function drawQR(el,data){
if(!el)return;
el.innerHTML='';
try{
if(typeof QRCode!=='undefined'){
new QRCode(el,{text:data,width:190,height:190,colorDark:'#0A0A0A',colorLight:'#ffffff',correctLevel:QRCode.CorrectLevel.M});
}else{throw new Error('no lib');}
}catch(e){
el.innerHTML='<div style="width:190px;height:190px;display:grid;place-items:center;background:var(--bg2);border-radius:8px;color:var(--muted);font-size:11px">QR unavailable<br>offline</div>';
}
}
/* ============================== PAYMENT ============================== */
function demoAddress(){
const chars='023456789acdefghjklmnpqrstuvwxyz';
let s='bc1q';
for(let i=0;i<38;i++)s+=chars[Math.floor(Math.random()*chars.length)];
return s;
}
async function fetchBtcPrice(){
try{
const r=await fetch('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd',{cache:'no-store'});
const j=await r.json();
if(j.bitcoin&&j.bitcoin.usd){state.btcPrice=j.bitcoin.usd;return;}
}catch(e){}
if(!state.btcPrice)state.btcPrice=65000; // offline fallback
}
function btcpayHeaders(){
const s=state.settings||{};
return s.apiKey?{'Authorization':'Bearer '+s.apiKey}:{};
}
async function startPayment(info){
const usd=cartTotal();
const day=FEST.days.find(d=>d.items.some(it=>info.window&&Math.abs(it.start-info.window[0])<1&&Math.abs(it.end-info.window[1])<1))||FEST.days[state.dayIdx];
const pickupLabel=fmtT(info.window[0])+' – '+fmtT(info.window[1]);
const itemsSummary=Object.keys(state.cart).map(id=>{const it=findItem(id);return state.cart[id]+'× '+it.name;}).join(', ');
const order={
id:crypto.randomUUID?crypto.randomUUID():String(Date.now()),
code:'F484-'+Math.random().toString(36).slice(2,6).toUpperCase(),
name:info.name,window:info.window,notes:info.notes,
items:JSON.parse(JSON.stringify(state.cart)),
itemsSummary, totalUsd:usd,
btcEstimate:state.btcPrice?((usd/state.btcPrice).toFixed(6)+' BTC'):null,
pickupLabel, day:day.name+' '+day.date,
status:'PENDING', ts:Date.now(), real:false, address:null
};
const p=state.pay={order,amountUsd:usd,method:'btc',address:null,bolt11:null,real:false,invoiceId:null,checkoutUrl:null,timer:null};
const s=state.settings;
if(s&&s.url&&s.store){
try{
const res=await fetch(s.url.replace(/\/+$/,'')+'/v1/stores/'+s.store+'/invoices',{
method:'POST',
headers:Object.assign({'Content-Type':'application/json'},btcpayHeaders()),
body:JSON.stringify({
amount:Math.round(usd*100)/100, currency:'USD',
description:'FEST 484 order '+order.code+' — pickup '+pickupLabel,
metadata:{orderId:order.code,pickup:pickupLabel}
})
});
if(!res.ok)throw new Error('HTTP '+res.status);
const inv=await res.json();
p.real=true;order.real=true;
p.invoiceId=inv.id;
p.checkoutUrl=inv.checkoutUrl||inv.paymentUrl||'';
let addr=null,bolt11=null;
if(Array.isArray(inv.paymentMethods)){
const btcPM=(inv.paymentMethods.find(pm=>pm.cryptoCode==='BTC')||{});
const lnPM=(inv.paymentMethods.find(pm=>pm.cryptoCode==='LIGHTNING')||{});
addr=btcPM.address||null; bolt11=lnPM.bolt11||lnPM.address||null;
}
if(!addr&&inv.paymentAddresses){
addr=typeof inv.paymentAddresses==='string'?inv.paymentAddresses:(inv.paymentAddresses.BTC||null);
}
if(!addr)throw new Error('No BTC address in invoice');
p.address=addr;order.address=addr;p.bolt11=bolt11;
startPolling(order);
}catch(e){
p.real=false;order.real=false;
p.address=demoAddress();order.address=p.address;
toast('BTCPay unavailable ('+e.message+') — demo invoice shown');
}
}else{
p.address=demoAddress();order.address=p.address;
}
openSheet('payment');
}
function startPolling(order){
if(state.pay.timer)clearInterval(state.pay.timer);
const s=state.settings;
let ticks=0;
state.pay.timer=setInterval(async()=>{
ticks++;
try{
const r=await fetch(s.url.replace(/\/+$/,'')+'/v1/invoices/'+state.pay.invoiceId,{headers:btcpayHeaders()});
if(!r.ok)return;
const inv=await r.json();
const st=(inv.status||'').toLowerCase();
if(['paid','confirmed','settled'].includes(st)){
clearInterval(state.pay.timer);state.pay.timer=null;
completeOrder('PAID');
}else if(st==='expired'||st==='failed'){
clearInterval(state.pay.timer);state.pay.timer=null;
const ps=$('#pay-status');
if(ps){ps.className='pay-status err';ps.textContent='Invoice expired — please go back and retry.';}
}
}catch(e){ if(ticks%12===0){const ps=$('#pay-status');if(ps)ps.textContent='Still waiting… (reconnecting)';} }
},5000);
}
function completeOrder(status){
const p=state.pay;if(!p||!p.order)return;
clearInterval(p.timer);p.timer=null;
p.order.status=status;
if(!state.orders.some(o=>o.id===p.order.id))state.orders.unshift(p.order);
saveOrders();
state.cart={};saveCart();renderFab();renderTimeline();
openSheet('success');
}
/* ============================== ORDERS VIEW ============================== */
function renderOrders(){
const list=$('#orders-list');
if(!state.orders.length){
list.innerHTML='<div class="empty"><div class="big">🧾</div>No orders yet.<br>Check the schedule and grab something good.</div>';
return;
}
list.innerHTML=state.orders.map(o=>{
const badge=o.status==='PAID'?'<span class="badge b-paid">PAID</span>'
:o.status==='PAID (DEMO)'?'<span class="badge b-demo">PAID · DEMO</span>'
:o.status==='FAILED'?'<span class="badge b-failed">FAILED</span>'
:'<span class="badge b-pending">PENDING</span>';
return '<div class="order-card">'+
'<div class="order-top"><span class="order-code">'+o.code+'</span>'+badge+'</div>'+
'<div class="order-meta">'+o.day+' · pickup '+o.pickupLabel+' · '+esc(o.name)+(o.notes?' · “'+esc(o.notes)+'”':'')+'</div>'+
'<div class="order-items">'+o.itemsSummary+'</div>'+
'<div class="order-total"><span>'+fmt$(o.totalUsd)+'</span><span class="btc">'+(o.btcEstimate||'')+'</span></div>'+
(o.real&&o.status!=='PAID'&&o.status!=='FAILED'?'<div class="order-actions"><button class="btn btn-ghost btn-sm" data-recheck="'+o.id+'">Re-check status</button></div>':'')+
'</div>';
}).join('');
list.querySelectorAll('[data-recheck]').forEach(b=>b.onclick=async()=>{
const o=state.orders.find(x=>x.id===b.dataset.recheck);
const s=state.settings;if(!s)return;
b.disabled=true;b.textContent='Checking…';
try{
const r=await fetch(s.url.replace(/\/+$/,'')+'/v1/invoices/'+o.invoiceId,{headers:btcpayHeaders()});
const inv=await r.json();
const st=(inv.status||'').toLowerCase();
if(['paid','confirmed','settled'].includes(st)){o.status='PAID';saveOrders();toast('Payment confirmed 🎉');}
}catch(e){}
renderOrders();
});
}
/* ============================== SETTINGS ============================== */
$('#btn-settings').onclick=()=>{
const s=state.settings||{};
$('#set-url').value=s.url||'';$('#set-store').value=s.store||'';$('#set-key').value=s.apiKey||'';
const tr=$('#test-result');tr.className='test-result';
$('#settings-modal').classList.add('show');
};
$('#btn-close-settings').onclick=()=>$('#settings-modal').classList.remove('show');
$('#settings-modal').addEventListener('mousedown',e=>{$('#settings-modal')._down=e.target.id==='settings-modal';});
$('#settings-modal').addEventListener('click',e=>{if(e.target.id==='settings-modal'&&$('#settings-modal')._down)e.target.classList.remove('show');});
$('#btn-save-settings').onclick=()=>{
state.settings={url:$('#set-url').value.trim(),store:$('#set-store').value.trim(),apiKey:$('#set-key').value.trim()};
saveSettings();
$('#settings-modal').classList.remove('show');
toast(state.settings.url?'BTCPay saved':'Settings cleared');
};
/* Legacy direct-BTCPay test handler retained below only as migration reference.
$('#btn-test').onclick=async()=>{
const tr=$('#test-result');
tr.className='test-result';
tr.textContent='Testing payment proxy…';
try{
const r=await fetch(url.replace(/\/+$/,'')+'/v1/stores/'+store,{headers:$('#set-key').value.trim()?{'Authorization':'Bearer '+$('#set-key').value.trim()}:{}});
if(r.ok){
const j=await r.json();
tr.className='test-result ok';tr.textContent='✓ Connected — store: '+(j.name||j.id);
}else{
tr.className='test-result err';tr.textContent='✗ HTTP '+r.status+(r.status===401?' — add a store API key with read access.':'');
}
}catch(e){tr.className='test-result err';tr.textContent='✗ Could not reach server: '+e.message;}
};
*/
/* Test the local proxy; BTCPay credentials must never be sent from this page. */
$('#btn-test').onclick=async()=>{
const tr=$('#test-result');tr.className='test-result';tr.textContent='Testing payment proxy…';
try{
const r=await fetch('/api/settings');
const j=await r.json().catch(()=>({}));
if(!r.ok)throw new Error('HTTP '+r.status);
tr.className=j.configured?'test-result ok':'test-result err';
tr.textContent=j.configured?'✓ Payment proxy connected — '+(j.store||'store configured'):'✗ Payment proxy is not configured.';
}catch(e){tr.className='test-result err';tr.textContent='✗ Could not reach payment proxy: '+e.message;}
};
$('#btn-clear-orders').onclick=()=>{
if(!state.orders.length)return;
if(confirm('Clear all saved orders?')){state.orders=[];saveOrders();$('#settings-modal').classList.remove('show');renderOrders();}
};
/* ============================== CLOCK / LOOP ============================== */
function tickClock(){
const d=new Date();
$('#clock').textContent=String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');
}
setInterval(()=>{tickClock();if(!$('#view-schedule').hidden)refreshStatuses();},30000);
/* ============================== INIT ============================== */
(async function init(){
tickClock();
renderDayTabs();
renderTimeline();
renderFab();
try{
const r=await fetch('/api/settings');
if(r.ok){const j=await r.json();if(j.configured){state.settings={configured:true,url:j.url,store:j.store};saveSettings();}}
}catch(e){}
await fetchBtcPrice();
setInterval(fetchBtcPrice,5*60*1000);
})();
</script>
</body>
</html>