diff --git a/public/app.js b/public/app.js
index 7981143..afb8c86 100644
--- a/public/app.js
+++ b/public/app.js
@@ -40,6 +40,7 @@ const state = {
genreLevels: {},
avoidGenres: new Set(),
seeds: [], // [{ tmdb_id, media_type, title }]
+ editMode: false, // editing an existing profile (vs. first-time onboarding)
mediaType: "movie",
room: null,
soloMode: false,
@@ -90,18 +91,20 @@ function renderGenreChips() {
GENRES.forEach((g) => {
const chip = document.createElement("button");
chip.className = "chip";
- chip.textContent = g.name;
+ const paint = (level) => {
+ chip.classList.toggle("like", level === 1);
+ chip.classList.toggle("love", level === 2);
+ chip.textContent = g.name + (level === 2 ? " ♥♥" : level === 1 ? " ♥" : "");
+ };
+ paint(state.genreLevels[g.id] || 0); // reflect current state (for edit prefill)
chip.onclick = () => {
const level = (state.genreLevels[g.id] || 0) + 1;
if (level > 2) {
delete state.genreLevels[g.id];
- chip.classList.remove("like", "love");
- chip.textContent = g.name;
+ paint(0);
} else {
state.genreLevels[g.id] = level;
- chip.classList.toggle("like", level === 1);
- chip.classList.toggle("love", level === 2);
- chip.textContent = g.name + (level === 2 ? " ♥♥" : " ♥");
+ paint(level);
}
};
box.appendChild(chip);
@@ -116,6 +119,7 @@ function renderAvoidChips() {
const chip = document.createElement("button");
chip.className = "chip";
chip.textContent = g.name;
+ chip.classList.toggle("avoid", state.avoidGenres.has(g.id)); // reflect state (edit prefill)
chip.onclick = () => {
if (state.avoidGenres.has(g.id)) {
state.avoidGenres.delete(g.id);
@@ -217,17 +221,20 @@ function renderSeedChips() {
async function handleOnboarding() {
const err = $("onboarding-error");
err.textContent = "";
- const username = $("username-input").value.trim();
- if (!username) {
- err.textContent = "Please enter a name.";
- return;
- }
try {
- const user = await api("/api/users", { method: "POST", body: JSON.stringify({ username }) });
- state.userId = user.id;
- state.username = user.username;
- localStorage.setItem("cinemate_user_id", user.id);
- localStorage.setItem("cinemate_username", user.username);
+ // First-time onboarding creates the user; edit mode reuses the current one.
+ if (!state.editMode) {
+ const username = $("username-input").value.trim();
+ if (!username) {
+ err.textContent = "Please enter a name.";
+ return;
+ }
+ const user = await api("/api/users", { method: "POST", body: JSON.stringify({ username }) });
+ state.userId = user.id;
+ state.username = user.username;
+ localStorage.setItem("cinemate_user_id", user.id);
+ localStorage.setItem("cinemate_username", user.username);
+ }
const genreScores = {};
for (const [id, level] of Object.entries(state.genreLevels)) {
@@ -236,13 +243,20 @@ async function handleOnboarding() {
await api("/api/profile/quiz", {
method: "POST",
body: JSON.stringify({
- user_id: user.id,
+ user_id: state.userId,
genre_scores: genreScores,
avoid_genres: [...state.avoidGenres],
- seeds: state.seeds.map((s) => ({ tmdb_id: s.tmdb_id, media_type: s.media_type })),
+ seeds: state.seeds.map((s) => ({ tmdb_id: s.tmdb_id, media_type: s.media_type, title: s.title })),
}),
});
+ if (state.editMode) {
+ exitEditMode();
+ toast("Preferences saved");
+ showScreen("screen-lobby");
+ return;
+ }
+
updateUserBadge();
if (state.pendingCode) {
$("join-code-input").value = state.pendingCode;
@@ -256,6 +270,44 @@ async function handleOnboarding() {
}
}
+// Open the quiz pre-filled with the current profile, to edit without a full reset.
+async function openEditPrefs() {
+ try {
+ const p = await api(`/api/profile/${state.userId}`);
+ state.genreLevels = {};
+ for (const [id, score] of Object.entries(p.genre_scores || {})) {
+ state.genreLevels[id] = Number(score) >= 0.9 ? 2 : 1;
+ }
+ state.avoidGenres = new Set((p.prefs?.avoid_genres || []).map(Number));
+ state.seeds = (p.prefs?.seeds || []).map((s) => ({
+ tmdb_id: s.tmdb_id,
+ media_type: s.media_type,
+ title: s.title || `#${s.tmdb_id}`,
+ }));
+ } catch {
+ state.genreLevels = {};
+ state.avoidGenres = new Set();
+ state.seeds = [];
+ }
+ state.editMode = true;
+ $("name-field").classList.add("hidden");
+ $("onboarding-title").textContent = "Edit preferences";
+ $("onboarding-continue").textContent = "Save";
+ renderGenreChips();
+ renderAvoidChips();
+ renderSeedChips();
+ hideSuggestions();
+ $("onboarding-error").textContent = "";
+ showScreen("screen-onboarding");
+}
+
+function exitEditMode() {
+ state.editMode = false;
+ $("name-field").classList.remove("hidden");
+ $("onboarding-title").textContent = "Welcome 👋";
+ $("onboarding-continue").textContent = "Continue";
+}
+
// ── Lobby ────────────────────────────────────────────────────────────────────
function setupMediaToggle() {
document.querySelectorAll("#media-toggle .toggle-opt").forEach((btn) => {
@@ -690,6 +742,7 @@ function resetData() {
closeWs();
state.userId = null;
state.username = null;
+ exitEditMode();
state.genreLevels = {};
state.avoidGenres = new Set();
state.seeds = [];
@@ -747,6 +800,7 @@ function init() {
$("match-continue-btn").onclick = () => showScreen("screen-swipe");
$("copy-link-btn").onclick = () => copyText(inviteLink(), "Invite link");
$("user-badge").onclick = showProfile;
+ $("edit-prefs-btn").onclick = openEditPrefs;
$("reset-btn").onclick = resetData;
$("profile-back-btn").onclick = () => showScreen("screen-lobby");
$("trailer-close").onclick = closeTrailer;
diff --git a/public/index.html b/public/index.html
index e95b173..a9b9dd2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -16,9 +16,11 @@
Cinemate
-
Welcome 👋
-
What's your name?
-
+
Welcome 👋
+
Which genres do you like? (tap once = like, twice = love)
@@ -143,8 +145,9 @@
My watchlist
Profile
-
Starting over clears your data on this device and re-runs the quiz.
-
+
+
Starting over clears your data on this device and re-runs the quiz.
+
diff --git a/src/index.ts b/src/index.ts
index 042c1f3..dc11a99 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,7 +10,7 @@ export { Room } from "./durable-objects/room";
const app = new Hono<{ Bindings: Env }>();
-app.get("/api/health", (c) => c.json({ ok: true, service: "cinemate", version: "v2.8" }));
+app.get("/api/health", (c) => c.json({ ok: true, service: "cinemate", version: "v2.9" }));
app.route("/api/users", users);
app.route("/api/profile", profile);
diff --git a/src/lib/mappers.ts b/src/lib/mappers.ts
index 4000ad6..720e7db 100644
--- a/src/lib/mappers.ts
+++ b/src/lib/mappers.ts
@@ -1,6 +1,6 @@
// Map D1 rows (Record) → application types.
-import type { MediaType, Profile, ProfilePrefs, Room, RoomStatus, SeedTitle } from "../types";
+import type { MediaType, Profile, ProfilePrefs, Room, RoomStatus } from "../types";
function parseJsonObject(value: unknown): Record {
if (typeof value !== "string") return {};
@@ -53,9 +53,10 @@ function parsePrefs(value: unknown): ProfilePrefs {
.filter((s): s is Record => !!s && typeof s === "object")
.map((s) => ({
tmdb_id: Number(s.tmdb_id),
- media_type: s.media_type === "tv" ? "tv" : "movie",
+ media_type: s.media_type === "tv" ? ("tv" as const) : ("movie" as const),
+ title: typeof s.title === "string" ? s.title : undefined,
}))
- .filter((s): s is SeedTitle => Number.isFinite(s.tmdb_id) && s.tmdb_id > 0);
+ .filter((s) => Number.isFinite(s.tmdb_id) && s.tmdb_id > 0);
}
return out;
} catch {
diff --git a/src/routes/profile.ts b/src/routes/profile.ts
index ec5af0b..624ea1e 100644
--- a/src/routes/profile.ts
+++ b/src/routes/profile.ts
@@ -10,13 +10,16 @@ function toNumberArray(value: unknown): number[] {
return value.map(Number).filter((n) => Number.isFinite(n));
}
-function toSeeds(value: unknown): { tmdb_id: number; media_type: "movie" | "tv" }[] {
+function toSeeds(
+ value: unknown,
+): { tmdb_id: number; media_type: "movie" | "tv"; title?: string }[] {
if (!Array.isArray(value)) return [];
return value
.filter((s): s is Record => !!s && typeof s === "object")
.map((s) => ({
tmdb_id: Number(s.tmdb_id),
media_type: s.media_type === "tv" ? ("tv" as const) : ("movie" as const),
+ ...(typeof s.title === "string" ? { title: s.title.slice(0, 120) } : {}),
}))
.filter((s) => Number.isInteger(s.tmdb_id) && s.tmdb_id > 0)
.slice(0, 6);
diff --git a/src/types.ts b/src/types.ts
index 2dd63d7..75be54d 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -29,6 +29,7 @@ export interface User {
export interface SeedTitle {
tmdb_id: number;
media_type: MediaType;
+ title?: string; // kept for display when editing preferences
}
/** Quiz preferences stored as JSON in profiles.prefs. Each is optional (empty = no filter). */