尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證一個與你一起觀看的人的追蹤工具:個人清單、兩人共享清單,以及即時的觀看庫。React、Supabase、TMDB。
AI 翻譯
A watch tracker you keep with the people you watch with: a personal list, a shared list for two, and a live library of what to watch. React, Supabase, TMDB.
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
A watch tracker you keep with the people you watch with. Every film and series you have seen or mean to, with your own rating and notes, plus a second list you share with a friend (or a few) where everyone adds titles and rates them. Real posters, cast, and runtimes come from TMDB; accounts and data live on Supabase.
Web first (React), with the backend kept as a plain API so a native iOS client can reuse it later. Every user's data is isolated at the database level, so this runs as a real multi-user app, not a single-owner tool.
Built by Anwar Creative Studio.檢視完整 README ↗A watch tracker you keep with the people you watch with. Every film and series you have seen or mean to, with your own rating and notes, plus a second list you share with a friend (or a few) where everyone adds titles and rates them. Real posters, cast, and runtimes come from TMDB; accounts and data live on Supabase.
Web first (React), with the backend kept as a plain API so a native iOS client can reuse it later. Every user's data is isolated at the database level, so this runs as a real multi-user app, not a single-owner tool.
Built by Anwar Creative Studio.
A list entry has an owner_type of either user or space. A personal list and a shared list run through the same code, they just point at a different owner. Ratings are their own table, one row per person per entry, which is why a shared entry can hold two ratings at once.
Titles are cached in a local titles table when first added, so a list points at a stable row and your history does not shift if TMDB changes.
Postgres row level security does the access control. A signed-in user can read and write their own user entries and any space entry for a space they belong to. Ratings are readable by anyone who can see the parent entry and writable only for your own row. Neither list_entries nor ratings nor the RLS policies that gate them cap a space's membership; the two security definer functions that grow one do that instead. Starting a space (create_couple_space) and adding another member to an existing one (add_space_member) both first check the friendship being relied on is accepted — and specifically the acting user's own friendship with whoever is being added, not any existing member's.
/api/* Vercel Edge functions proxy TMDB, OMDb and The Guardian: the keys stay server-side and responses are cached on the CDN, so the free-tier limits are shared across everyone rather than spent per visitThe browser never talks to TMDB, OMDb or The Guardian directly. It calls
/api/tmdb, /api/omdb and /api/guardian, which:
VITE_ prefix, never bundled),Cache-Control: s-maxage so Vercel's edge caches the response — title
detail for a day, discovery feeds for 15 minutes, the press feed for 30,
IMDb/RT scores for a week,So the first person to open "trending" this hour pays the upstream calls; the
next few thousand are served from the edge. During vite dev a small plugin
in vite.config.ts runs the same handler modules so /api/* works locally.
Each function also runs a best-effort per-IP rate limit (in the edge instance's memory, ceilings well above any real session) so one client cannot run the free-tier budgets down. For a hard limit, add a rule in the Vercel firewall.
/api/poster is opt-in (see the env table). When it and VITE_POSTER_CACHE
are set, it copies each TMDB poster into a public Supabase Storage bucket the
first time a card renders it, and the grid serves the Storage copy from then
on. Without it, cards use TMDB image URLs as before.
Letterboxd is a public diary. A group text is where "what should we watch" goes to die. Stub is the small private middle: one list that is yours, one that is ours, and a real catalogue to pull from, with no feed and no account required from anyone you are not already watching with.
npm install
supabase/migrations/ in order (0001 through 0008), or supabase link and supabase db push.anon public key.eyJ).cp .env.example .env
# then fill in the values
| var | side | required | what |
|---|---|---|---|
VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY | client | yes | the database and auth |
TMDB_ACCESS_TOKEN | server (/api/*) | yes | titles, artwork, discovery |
OMDB_API_KEY | server | no | IMDb / Rotten Tomatoes / Metacritic scores |
GUARDIAN_API_KEY | server | no | the "In the press" feed |
SUPABASE_SERVICE_ROLE_KEY | server | no | lets /api/poster write to Storage |
VITE_POSTER_CACHE | client | no | set to 1 to serve posters from Storage |
VITE_SENTRY_DSN | client | no | a Sentry DSN turns on crash reporting |
The server vars have no VITE_ prefix on purpose, so they never reach the
browser. On Vercel, add them as plain (not VITE_) environment variables;
vite dev reads them from .env for the local /api plugin. For the poster
cache, also run supabase/migrations/0005_posters_bucket.sql and set
VITE_POSTER_CACHE=1.
npm run dev
Open the local URL Vite prints, create an account, and start adding titles from the Library. Until the two Supabase values are set, the app shows a setup screen instead of booting.
| Table | Purpose |
|---|---|
profiles | One row per auth user, created by a trigger on sign up. Holds the username and display name. |
titles | Local cache of TMDB media so lists reference a stable row. |
list_entries | A title on a list. owner_type is user or space; carries status, note, and watched date. |
ratings | One row per person per entry. This is how everyone's ratings on a shared entry sit side by side. |
friendships | Requester, addressee, and status (pending or accepted). |
spaces + space_members | A shared list and who belongs to it. create_couple_space(friend) starts one between two accepted friends; add_space_member(space, friend) grows an existing one, up to 8 people, gated on the adder's own friendship with whoever they're adding. |
notifications | One row per (recipient, entry, kind). Triggers on ratings and list_entries.note write these for the other space member; a repeat rating or note edit refreshes the row instead of adding another. Delivered live over Supabase realtime. |
npm run dev # dev server
npm run build # typecheck, then production build
npm run typecheck # types only
npm test # unit tests (Vitest)
npm run test:db # row-level-security tests (needs a local Supabase: supabase start)
npm run preview # serve the build
npm run icons # regenerate the PNG icons from the mark
npm test covers the pure logic: OMDb score parsing, Guardian article mapping, TMDB normalisation and the upcoming/classic filters, list averages, and theme persistence.
supabase/tests/rls_test.sql is a pgTAP suite that runs against a throwaway database (supabase start && npm run test:db, or the CI database job). It asserts that one user cannot read or write another's personal list or ratings or profile, that a shared space is visible only to its members, that a space can grow past a pair but only when the acting member has their own accepted friendship with whoever they're adding (not merely riding on a fellow member's), that friend_activity and list_compare only return data between accepted friends, and that delete_own_account removes exactly the caller and their personal rows.
CI (.github/workflows/ci.yml) runs typecheck, unit tests and the build on every push, plus the database tests on a fresh Supabase stack.
/api/* proxy that keeps the third-party keys server-side and caches their responses on the CDN./api/poster caches TMDB poster art into Supabase Storage so the grid does not depend on TMDB's CDN. Off until SUPABASE_SERVICE_ROLE_KEY and VITE_POSTER_CACHE are set./api/* function, and crash reporting to Sentry when VITE_SENTRY_DSN is set.prototype/archive.html is the original single file version that seeded the idea: one HTML file, no accounts, list kept in the page itself.