/* eslint-disable no-undef */
const { useState, useEffect, useRef, useMemo, useCallback } = React;
/* =========================================================
STORAGE
========================================================= */
const LS_FRIENDS = "porco.friends.v1";
const LS_LOGS = "porco.logs.v1";
const DEFAULT_FRIENDS = [
{ id: "f_you", name: "You", color: "#FCD86A" },
{ id: "f_pal1", name: "Pal", color: "#EE6388" },
{ id: "f_pal2", name: "Bestie", color: "#7EE0C5" },
];
const loadJSON = (k, fb) => {
try { const v = JSON.parse(localStorage.getItem(k)); return v == null ? fb : v; } catch { return fb; }
};
const saveJSON = (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)); } catch {} };
/* =========================================================
DATE HELPERS — pick the active week from today.
The system date is set to 2026-05-15 for this build but the
logic works for any date.
========================================================= */
function findCurrentWeekIdx(weeks, today = new Date()) {
// First, any week where start ≤ today ≤ end
for (let i = 0; i < weeks.length; i++) {
const s = new Date(weeks[i].start + "T00:00:00");
const e = new Date(weeks[i].end + "T23:59:59");
if (today >= s && today <= e) return i;
}
// Else: next upcoming week
for (let i = 0; i < weeks.length; i++) {
if (today < new Date(weeks[i].start + "T00:00:00")) return i;
}
// Else: most recent past
return weeks.length - 1;
}
/* =========================================================
APP
========================================================= */
function App() {
const weeks = window.WEEKS;
const [friends, setFriends] = useState(() => loadJSON(LS_FRIENDS, DEFAULT_FRIENDS));
const [logs, setLogs] = useState(() => loadJSON(LS_LOGS, {}));
const [activeTab, setActiveTab] = useState("now"); // "now" | "season"
const [manageOpen, setManageOpen] = useState(false);
const [editing, setEditing] = useState(null); // { weekId, friendId }
const [sort, setSort] = useState("chrono"); // "chrono" | "rated" | "visited"
useEffect(() => saveJSON(LS_FRIENDS, friends), [friends]);
useEffect(() => saveJSON(LS_LOGS, logs), [logs]);
const currentIdx = useMemo(() => findCurrentWeekIdx(weeks), [weeks]);
const currentWeek = weeks[currentIdx];
const todayISO = useMemo(() => new Date().toISOString().slice(0, 10), []);
// ---- mutate helpers ----
const updateEntry = useCallback((weekId, friendId, patch) => {
setLogs(prev => {
const next = { ...prev };
next[weekId] = { ...(next[weekId] || {}) };
next[weekId][friendId] = { ...(next[weekId][friendId] || {}), ...patch, ts: Date.now() };
return next;
});
}, []);
const deleteEntry = useCallback((weekId, friendId) => {
setLogs(prev => {
const next = { ...prev };
if (!next[weekId]) return prev;
const { [friendId]: _, ...rest } = next[weekId];
if (Object.keys(rest).length === 0) {
delete next[weekId];
} else {
next[weekId] = rest;
}
return next;
});
}, []);
const onPickFriend = (weekId, friendId) => {
setEditing(prev =>
prev && prev.weekId === weekId && prev.friendId === friendId
? null
: { weekId, friendId }
);
};
// ---- tweaks ----
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"theme": "indigo",
"density": "comfy",
"drips": "on",
"sort": "chrono"
}/*EDITMODE-END*/;
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
useEffect(() => {
document.body.dataset.theme = t.theme;
document.body.dataset.density = t.density;
document.body.dataset.drips = t.drips;
}, [t.theme, t.density, t.drips]);
// Sort weeks for the season list
const sortedWeeks = useMemo(() => {
const arr = [...weeks];
if (t.sort === "rated") {
const score = (w) => {
const wl = logs[w.id];
if (!wl) return -1;
const ratings = Object.values(wl).map(e => e.stars || 0).filter(s => s > 0);
if (!ratings.length) return -1;
return ratings.reduce((a,b) => a + b, 0) / ratings.length;
};
arr.sort((a, b) => score(b) - score(a));
} else if (t.sort === "visited") {
const has = (w) => logs[w.id] && Object.values(logs[w.id]).some(e => e.stars || e.flavors?.length || e.comment);
arr.sort((a, b) => Number(has(b)) - Number(has(a)));
} else {
// chrono
}
return arr;
}, [weeks, logs, t.sort]);
return (
<>
{activeTab === "now" && (
onPickFriend(currentWeek.id, fid)}
onChange={(fid, patch) => updateEntry(currentWeek.id, fid, patch)}
onDelete={(fid) => { deleteEntry(currentWeek.id, fid); setEditing(null); }}
onCloseEntry={() => setEditing(null)}
onManageFriends={() => setManageOpen(true)}
currentIdx={currentIdx}
totalWeeks={weeks.length}
/>
)}
{activeTab === "season" && (
{ deleteEntry(wid, fid); setEditing(null); }}
onCloseEntry={() => setEditing(null)}
onManageFriends={() => setManageOpen(true)}
sortLabel={t.sort}
/>
)}
{manageOpen && (
setManageOpen(false)}
onSave={setFriends}
/>
)}
setTweak("theme", v)} options={[
{ value: "indigo", label: "Indigo" },
{ value: "midnight", label: "Midnight" },
{ value: "cherry", label: "Cherry" },
{ value: "pistachio", label: "Pistachio" },
]} />
setTweak("density", v)} options={[
{ value: "comfy", label: "Comfy" },
{ value: "compact", label: "Compact" },
]} />
setTweak("drips", v)} options={[
{ value: "off", label: "Off" },
{ value: "on", label: "Subtle" },
{ value: "heavy", label: "Heavy" },
]} />
setTweak("sort", v)} options={[
{ value: "chrono", label: "Chronological" },
{ value: "rated", label: "Highest rated" },
{ value: "visited", label: "Visited first" },
]} />
{ if (confirm("Clear all logs?")) { setLogs({}); } }} />
>
);
}
/* =========================================================
Header lockup
========================================================= */
function Header() {
return (
);
}
/* =========================================================
Tabs
========================================================= */
function Tabs({ active, onChange }) {
return (
onChange("now")}>This week
onChange("season")}>Full season
);
}
/* =========================================================
NOW VIEW
========================================================= */
function NowView({ week, friends, weekLogs, editing, onPickFriend, onChange, onDelete, onCloseEntry, onManageFriends, currentIdx, totalWeeks }) {
const activeFriend = editing && friends.find(f => f.id === editing.friendId);
return (
{week.range}
Week {currentIdx + 1} of {totalWeeks}
{week.flavors.map(f => (
))}
+ topping
{week.topping.name}
{editing && activeFriend && (
onChange(activeFriend.id, patch)}
onDelete={() => onDelete(activeFriend.id)}
onClose={onCloseEntry}
/>
)}
Notes & ratings
);
}
function NotesPreview({ weekLogs, friends }) {
const entries = Object.entries(weekLogs || {}).filter(([_, e]) => e && (e.stars || e.comment || e.flavors?.length));
if (!entries.length) {
return No one's logged this week yet — tap a friend chip above to start.
;
}
return (
{entries.map(([fid, e]) => {
const f = friends.find(x => x.id === fid);
if (!f) return null;
return (
{f.name}
{e.stars > 0 && }
{e.flavors?.length > 0 && (
Got: {e.flavors.join(" + ")}
{e.topping && " + topping"}
{e.sprinkles && " + sprinkles"}
)}
{e.comment && (
“{e.comment}”
)}
);
})}
);
}
/* =========================================================
SEASON VIEW
========================================================= */
function SeasonView({ weeks, currentWeekId, todayISO, friends, logs, editing, onPickFriend, onChange, onDelete, onCloseEntry, onManageFriends, sortLabel }) {
return (
2026 season
{weeks.map(w => {
const isCurrent = w.id === currentWeekId;
const isPast = w.end < todayISO && !isCurrent;
const isEditingHere = editing?.weekId === w.id;
return (
onPickFriend(w.id, editing?.weekId === w.id && editing?.friendId ? editing.friendId : friends[0].id)}
/>
{isEditingHere && (
onPickFriend(w.id, fid)}
onManageFriends={onManageFriends}
/>
{editing.friendId && (() => {
const f = friends.find(x => x.id === editing.friendId);
if (!f) return null;
return (
onChange(w.id, f.id, patch)}
onDelete={() => onDelete(w.id, f.id)}
onClose={onCloseEntry}
/>
);
})()}
)}
);
})}
);
}
/* =========================================================
Legend (matches poster footer)
========================================================= */
function Legend() {
return (
🌈 RAINBOW SPRINKLES ALWAYS AVAILABLE 🌈
GF Gluten-Free · VG Vegan
);
}
/* =========================================================
Patch: useTweaks doesn't return setTweak inline on object —
provide a small wrapper so we can call t.setTweak() above.
========================================================= */
// useTweaks is already a hook from tweaks-panel.jsx; just make sure we use it.
// (no-op — useTweaks already returns the tweaks object with `setTweak` attached if the starter exposes it.)
ReactDOM.createRoot(document.getElementById("root")).render( );