Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const state = {
seeds: [], // [{ tmdb_id, media_type, title }]
editMode: false, // editing an existing profile (vs. first-time onboarding)
mediaType: "movie",
platforms: new Set(), // selected streaming platforms (empty = any); creator-only
room: null,
soloMode: false,
deck: [],
Expand Down Expand Up @@ -321,6 +322,26 @@ function setupMediaToggle() {
});
}

// Multi-select streaming platforms; empty set = "Any". Results include titles on ANY
// picked platform. The "Any" chip (data-platform="") clears the selection.
function syncPlatformChips() {
document.querySelectorAll("#platform-chips .chip").forEach((b) => {
const p = b.dataset.platform;
b.classList.toggle("on", p === "" ? state.platforms.size === 0 : state.platforms.has(p));
});
}
function setupPlatformChips() {
document.querySelectorAll("#platform-chips .chip").forEach((btn) => {
btn.onclick = () => {
const p = btn.dataset.platform;
if (p === "") state.platforms.clear(); // "Any" resets to no filter
else if (state.platforms.has(p)) state.platforms.delete(p);
else state.platforms.add(p);
syncPlatformChips();
};
});
}

async function handleCreateRoom() {
const err = $("lobby-error");
err.textContent = "";
Expand All @@ -330,7 +351,7 @@ async function handleCreateRoom() {
body: JSON.stringify({
user_id: state.userId,
media_type: state.mediaType,
platform_filter: $("platform-select").value || null,
platform_filter: [...state.platforms].join(",") || null, // CSV; empty = any
solo: $("solo-check").checked,
}),
});
Expand Down Expand Up @@ -444,10 +465,17 @@ async function maybeTopUp() {
}
}

// The creator (user_a) owns the shared movie/tv choice; solo user is their own creator.
function isRoomCreator() {
return !!state.room && state.userId === state.room.user_a_id;
}

function syncMediaToggle() {
document.querySelectorAll("#swipe-media-toggle .toggle-opt").forEach((b) => {
b.classList.toggle("active", b.dataset.media === state.mediaType);
});
// Only the creator can switch type (shared pool); hide the control for the joiner.
$("swipe-media-toggle").classList.toggle("hidden", !isRoomCreator());
}

async function toggleMedia(media) {
Expand Down Expand Up @@ -1022,6 +1050,7 @@ function init() {
renderAvoidChips();
setupSeedSearch();
setupMediaToggle();
setupPlatformChips();
setupGestures();

$("onboarding-continue").onclick = handleOnboarding;
Expand Down
18 changes: 9 additions & 9 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ <h3>Create a room</h3>
<button class="toggle-opt" data-media="tv">📺 TV shows</button>
</div>
</div>
<label class="muted">Platform (optional)</label>
<select id="platform-select" class="input">
<option value="">Any / Plex</option>
<option value="netflix">Netflix</option>
<option value="disney">Disney+</option>
<option value="hbo">HBO Max</option>
<option value="prime">Prime Video</option>
<option value="apple">Apple TV+</option>
</select>
<label class="muted">Platforms <span class="hint">(pick any you have)</span></label>
<div id="platform-chips" class="chips">
<button type="button" class="chip on" data-platform="">Any</button>
<button type="button" class="chip" data-platform="netflix">Netflix</button>
<button type="button" class="chip" data-platform="disney">Disney+</button>
<button type="button" class="chip" data-platform="hbo">HBO Max</button>
<button type="button" class="chip" data-platform="prime">Prime Video</button>
<button type="button" class="chip" data-platform="apple">Apple TV+</button>
</div>
<label class="check-row"><input type="checkbox" id="solo-check" /> Solo mode (just me)</label>
<button id="create-room-btn" class="btn btn-primary mt">Create</button>
</div>
Expand Down
5 changes: 5 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ select.input {
border-color: var(--nope);
color: #fff;
}
.chip.on {
background: linear-gradient(135deg, var(--accent), var(--accent-dim));
border-color: transparent;
color: #fff;
}
.swipe-links {
display: flex;
justify-content: center;
Expand Down
6 changes: 4 additions & 2 deletions src/routes/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ rooms.get("/:id/ws", async (c) => {
});

// PATCH /api/rooms/:id — live movie/tv toggle: change media_type + reset the pool.
// Creator-only: media_type is a shared, room-level setting (both users draw from ONE
// pool), so only the room creator (user_a) decides it; the joiner inherits it.
// Body: { user_id, media_type }
rooms.patch("/:id", async (c) => {
const id = c.req.param("id");
Expand All @@ -327,8 +329,8 @@ rooms.patch("/:id", async (c) => {
if (!row) return c.json({ error: "room_not_found" }, 404);
const room = mapRoom(row);

// AUTH GATING: only room members can change the type.
if (userId !== room.user_a_id && userId !== room.user_b_id) {
// AUTH GATING: only the room creator (user_a) may change the shared media_type.
if (userId !== room.user_a_id) {
return c.json({ error: "forbidden" }, 403);
}

Expand Down
20 changes: 17 additions & 3 deletions src/services/tmdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,20 @@ const PROVIDER_IDS: Record<string, number> = {
"apple tv": 350,
};

/**
* Map a platform filter (single key or CSV like "netflix,prime") to TMDb provider ids,
* joined with "|" (OR) for with_watch_providers. Unknown/empty → null. Pure → unit-tested.
*/
export function platformProviderParam(platform?: string | null): string | null {
if (!platform) return null;
const ids = new Set<number>();
for (const raw of platform.split(",")) {
const id = PROVIDER_IDS[raw.trim().toLowerCase()];
if (id) ids.add(id);
}
return ids.size > 0 ? [...ids].join("|") : null;
}

function dateToYear(date?: string): number | null {
if (!date || date.length < 4) return null;
const y = Number(date.slice(0, 4));
Expand Down Expand Up @@ -156,9 +170,9 @@ export async function discoverTitles(env: Env, opts: DiscoverOptions): Promise<D
params.without_genres = opts.avoidGenres.join(","); // exclude any of these
}

const providerId = opts.platform ? PROVIDER_IDS[opts.platform.toLowerCase()] : undefined;
if (providerId) {
params.with_watch_providers = String(providerId);
const providers = platformProviderParam(opts.platform); // "8|119" for netflix+prime
if (providers) {
params.with_watch_providers = providers;
params.watch_region = TMDB_REGION;
}

Expand Down
19 changes: 19 additions & 0 deletions test/unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { genJoinCode, genId } from "../src/lib/ids";
import { mapProfile, mapRoom } from "../src/lib/mappers";
import { topupCursor, rejectAvoidGenres } from "../src/lib/deck";
import { platformProviderParam } from "../src/services/tmdb";

describe("ids", () => {
it("genJoinCode is 6 chars from the safe alphabet", () => {
Expand Down Expand Up @@ -77,3 +78,21 @@ describe("rejectAvoidGenres (adaptive top-up filter)", () => {
expect(rejectAvoidGenres(cards, [])).toHaveLength(2);
});
});

describe("platformProviderParam (multi-select platforms → TMDb providers)", () => {
it("maps a single platform to its provider id", () => {
expect(platformProviderParam("netflix")).toBe("8");
});
it("maps a CSV of platforms to OR-joined ids (netflix + prime)", () => {
expect(platformProviderParam("netflix,prime")).toBe("8|119");
});
it("treats empty / null as 'any' (no filter)", () => {
expect(platformProviderParam("")).toBeNull();
expect(platformProviderParam(null)).toBeNull();
expect(platformProviderParam(undefined)).toBeNull();
});
it("ignores unknown keys and dedupes, trimming/casing", () => {
expect(platformProviderParam(" Netflix , bogus ")).toBe("8");
expect(platformProviderParam("prime,amazon")).toBe("119"); // both → 119, deduped
});
});
Loading