/* eslint-disable no-undef */
const { useState, useEffect, useRef, useMemo, useCallback } = React;
/* =========================================================
Small visual atoms
========================================================= */
function Star({ filled, onClick, size = 30 }) {
// Chunky 5-point star. Use a path that feels hand-cut, not generic 5-point math.
return (
);
}
function StarsInput({ value, onChange }) {
return (
{[1,2,3,4,5].map(n => (
onChange(value === n ? 0 : n)}
/>
))}
);
}
function MiniStars({ value }) {
if (!value) return null;
return {"★".repeat(value)}{"☆".repeat(5 - value)} ;
}
function Badges({ gf, v, compact }) {
if (!gf && !v) return null;
return (
{gf && GF }
{v && VG }
);
}
function Avatar({ friend, size = 26 }) {
if (!friend) return null;
const initials = (friend.name || "?").trim().split(/\s+/).map(s => s[0]).slice(0,2).join("").toUpperCase() || "?";
return (
{initials}
);
}
function Drip({ className }) {
// hand-drawn pink drip ribbon similar to the poster corners
return (
);
}
/* =========================================================
Flavor row inside the NOW card
========================================================= */
function FlavorRow({ flavor, log, onLogChange, toppingName, toppingTags, sprinklesAlways }) {
// For the "now" card we let each friend pick which flavor(s) they got — so the
// log isn't per-row. We just render the flavor info + badges here.
return (
);
}
/* =========================================================
Entry card — the per-friend logging form
========================================================= */
function EntryCard({ week, friend, entry, onChange, onDelete, onClose }) {
const [draftComment, setDraftComment] = useState(entry?.comment || "");
useEffect(() => { setDraftComment(entry?.comment || ""); }, [friend?.id, week.id]);
const flavorIds = week.flavors.map(f => f.name);
const got = entry?.flavors || [];
const toggleFlavor = (name) => {
const next = got.includes(name) ? got.filter(f => f !== name) : [...got, name];
onChange({ ...entry, flavors: next });
};
return (
Got which?
{week.flavors.map(f => {
const on = got.includes(f.name);
return (
toggleFlavor(f.name)}
type="button"
>
{f.name}
);
})}
onChange({ ...entry, flavors: got.length === 2 ? [] : flavorIds })}
type="button"
>
🍦 Both / swirl
Topping?
onChange({ ...entry, topping: !entry?.topping })}
type="button"
>
{week.topping.name}
{(week.topping.gf || week.topping.v) && (
)}
onChange({ ...entry, sprinkles: !entry?.sprinkles })}
type="button"
>
🌈 Rainbow sprinkles
How was it?
onChange({ ...entry, stars })} />
Note
{ onChange({ ...entry, comment: draftComment.trim() }); onClose(); }}>
Save
Delete
);
}
/* =========================================================
Friend chip strip
========================================================= */
function FriendStrip({ friends, weekLogs, activeFriendId, onPickFriend, onManageFriends }) {
return (
{friends.map(f => {
const entry = weekLogs?.[f.id];
const hasEntry = !!entry && (entry.flavors?.length || entry.stars || entry.comment || entry.topping || entry.sprinkles);
return (
onPickFriend(f.id)}
type="button"
>
{f.name}
{hasEntry && entry.stars > 0 && }
);
})}
+
Edit crew
);
}
/* =========================================================
Manage friends modal
========================================================= */
const FRIEND_COLORS = ["#FCD86A", "#EE6388", "#7EE0C5", "#FFB87A", "#C7B6FF", "#FF8DBE", "#9FE0FF", "#FFE08A"];
function ManageFriendsModal({ friends, onClose, onSave }) {
const [list, setList] = useState(friends);
const [newName, setNewName] = useState("");
const update = (id, patch) => setList(list.map(f => f.id === id ? { ...f, ...patch } : f));
const remove = (id) => setList(list.filter(f => f.id !== id));
const add = () => {
const name = newName.trim();
if (!name) return;
const usedColors = new Set(list.map(f => f.color));
const color = FRIEND_COLORS.find(c => !usedColors.has(c)) || FRIEND_COLORS[list.length % FRIEND_COLORS.length];
setList([...list, { id: "f_" + Date.now(), name, color }]);
setNewName("");
};
return (
{ if (e.target === e.currentTarget) onClose(); }}>
Your softserve crew
{list.map(f => (
))}
setNewName(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") add(); }}
maxLength={20}
/>
ADD
{ onSave(list); onClose(); }}
>
DONE
);
}
/* =========================================================
Week row in the season list
========================================================= */
function WeekRow({ week, isCurrent, isPast, friends, weekLogs, onTap }) {
return (
{week.range}
{week.flavors.map((f, i) => (
{f.name}
))}
+ topping
{week.topping.name}
{(week.topping.gf || week.topping.v) && }
{weekLogs && Object.keys(weekLogs).length > 0 && (
{friends.map(f => {
const e = weekLogs[f.id];
if (!e || (!e.stars && !e.flavors?.length && !e.comment)) return null;
return (
{f.name[0].toUpperCase()}
{e.stars > 0 && {"★".repeat(e.stars)} }
);
})}
)}
);
}
/* expose to window for app.jsx */
Object.assign(window, {
Star, StarsInput, MiniStars, Badges, Avatar, Drip,
FlavorRow, EntryCard, FriendStrip, ManageFriendsModal, WeekRow,
FRIEND_COLORS,
});