gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit54070a4e54070a4etracker#8: /my/unwatched + /my/schedule (301 from old), S01E01, title (year), 1 episode, watched-set lookup (unwatched 15s -> 1s)mre54070a4e/components/myshows.hl

4.3 KB

  1. // components/myshows.hl — `/my/shows` (tracker.worldapi.org#7): every show the signed-in user follows,
  2. // the one followed last on top. Creator's words: "trackers /my/shows that just ists the shows in desc
  3. // order i followed them, small poster, title, last episode like s08e35". Each row: the small poster (the
  4. // same `/posters/<name>` route + placeholder as the show page, shows.hl `posterUrlOf`: one `/posters/none` URL when the file is missing), the title linking
  5. // to `/shows/<slug>`, and the LAST WATCHED episode as `S08E35` — the architect's reading (in the ticket):
  6. // the highest season/episode number the user has watched of that show; nothing watched → no episode.
  7. //
  8. // Read-only page (no check icons, no click), so like /schedule it needs no `on server` face — `rows` is
  9. // one server-side render from `session`. Reused (not copied): `followsOfUser` (follows.hl),
  10. // `watchesOfUser` (watches.hl), `showById`/`episodeById`/`posterUrlOf`/`episodeCode`/`titleWithYear` (shows.hl).
  11. //
  12. // COST: only the user's own follows and watches are touched — one fetch per followed show and one fetch
  13. // per watched EPISODE (never a scan of all episodes/seasons of a followed show, unlike /unwatched): the
  14. // followed shows go into a map first, then each episode watch's episode is fetched once and kept only
  15. // if it is higher than that show's current best (measured on a copy of the real data, STATUS.md).
  16. parent './main.hl'
  17. import { appTitle } from '../project.hl'
  18. import { userIdOfSession } from '../users.hl'
  19. import { followsOfUser } from '../follows.hl'
  20. import { showById, episodeById, posterUrlOf, episodeCode, titleWithYear } from '../shows.hl'
  21. import { watchesOfUser } from '../watches.hl'
  22. // ---- pure helpers (static: an mpackdb-backed helper must be `static`, hybriel#115/#116) ------
  23. // insertion sort, hand-written (no list sort(), hybriel#1) — newest follow first; `at` is epoch ms
  24. // (migrated `_createdAt`, or now() for a new follow), a missing one counts as the oldest
  25. static insertByFollowDesc = (sorted, row) => {
  26. let out = []
  27. let inserted = false
  28. for (x of sorted) {
  29. if (!inserted && row.followedAt >= x.followedAt) { out.push(row) inserted = true }
  30. out.push(x)
  31. }
  32. if (!inserted) { out.push(row) }
  33. return out
  34. }
  35. // show id → the highest watched episode of it { seasonNumber, episodeNumber }, only for shows in `followed`
  36. static lastWatchedOf = (userId, followed) => {
  37. let best = {}
  38. for (w of watchesOfUser(userId)) {
  39. if (w.targetType == 'episode' && w.target != null) {
  40. let e = episodeById(w.target)
  41. if (e != null && e.show != null && followed[e.show] == true && e.seasonNumber != null && e.episodeNumber != null) {
  42. let b = best[e.show]
  43. if (b == null || e.seasonNumber > b.seasonNumber || (e.seasonNumber == b.seasonNumber && e.episodeNumber > b.episodeNumber)) {
  44. best[e.show] = { seasonNumber = e.seasonNumber episodeNumber = e.episodeNumber }
  45. }
  46. }
  47. }
  48. }
  49. return best
  50. }
  51. static myShowsRowsOf = (userId) => {
  52. if (userId == null) { return [] }
  53. let follows = followsOfUser(userId)
  54. let followed = {}
  55. for (f of follows) { followed[f.show] = true }
  56. let last = lastWatchedOf(userId, followed)
  57. let out = []
  58. for (f of follows) {
  59. let show = showById(f.show)
  60. if (show != null) {
  61. let lw = last[show.id]
  62. let hasEpisode = lw != null
  63. out = insertByFollowDesc(out, {
  64. id = show.id title = titleWithYear(show) href = '/shows/' + show.urlSegment
  65. posterUrl = posterUrlOf(show)
  66. hasEpisode = hasEpisode
  67. episodeLabel = hasEpisode ? episodeCode(lw.seasonNumber, lw.episodeNumber) : ''
  68. followedAt = f.at != null ? f.at : 0
  69. })
  70. }
  71. }
  72. return out
  73. }
  74. // ---- reactive members -----------------------------------------------------------------------
  75. session = null
  76. userId = userIdOfSession(session)
  77. signedIn = userId != null
  78. rows = signedIn ? myShowsRowsOf(userId) : []
  79. hasRows = rows.length > 0
  80. noRows = !hasRows
  81. emptyText = signedIn ? 'You follow no shows yet.' : 'Sign in to see the shows you follow.'
  82. __title = 'My shows — ' + appTitle
  83. View {
  84. myShowsPage {
  85. h1 { "My shows" }
  86. if (noRows) { p { class = "muted" emptyText } }
  87. if (hasRows) {
  88. myShowsRows {
  89. for (row of rows) {
  90. myShowRow {
  91. img { class = "myshow-poster" src = row.posterUrl alt = row.title }
  92. a { class = "myshow-title" href = row.href row.title }
  93. if (row.hasEpisode) { myShowEpisode { row.episodeLabel } }
  94. }
  95. }
  96. }
  97. }
  98. }
  99. }

Branches

Latest commits

  • 54070a4etracker#8: /my/unwatched + /my/schedule (301 from old), S01E01, title (year), 1 episode, watched-set lookup (unwatched 15s -> 1s)mre
  • 3251488atracker#7: /my/shows (followed shows, newest follow first, poster, title, last watched SxxEyy); gate can take screenshots (TRACKER_GATE_SHOTS)mre
  • 44b7d9f9tracker#6: /schedule — upcoming episodes of followed shows, soonest firstmre
  • 91c9fc8ctracker#5: /unwatched — unwatched released episodes of followed shows, newest firstmre
  • a97c0295tracker#4: show page /shows/:slug (header, seasons, episodes, watch checks) + tools/relink-episode-seasons.hlmre
  • 05f407c5tracker: no border on any button except inverted ones (Log out, ident status and identities too); header brand weight 100mre
  • 17375427tracker#2: tools/migrate.hl + tools/verify.hl — old MongoDB data into mpackdb with new idsmre
  • 31aac936tracker#3: no border on the header and on filled buttons; inverted buttons keep theirsmre
  • 2ad9d29ctracker#1: login exactly like calendar (identity selector in the header, empty homepage)mre
  • 3691e176tracker#1: empty tracker with the ident login (state of 2026-09-27)mre