gitoriaLog in with ident

tracker

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commit3251488a3251488atracker#7: /my/shows (followed shows, newest follow first, poster, title, last watched SxxEyy); gate can take screenshots (TRACKER_GATE_SHOTS)mre3251488a/components/myshows.hl

4.4 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 `posterName`), 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`/`posterName` (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, posterName } from '../shows.hl'
  21. import { watchesOfUser } from '../watches.hl'
  22. // ---- pure helpers (static: an mpackdb-backed helper must be `static`, hybriel#115/#116) ------
  23. static pad2 = (n) => {
  24. let s = '' + n
  25. while (s.length < 2) { s = '0' + s }
  26. return s
  27. }
  28. // insertion sort, hand-written (no list sort(), hybriel#1) — newest follow first; `at` is epoch ms
  29. // (migrated `_createdAt`, or now() for a new follow), a missing one counts as the oldest
  30. static insertByFollowDesc = (sorted, row) => {
  31. let out = []
  32. let inserted = false
  33. for (x of sorted) {
  34. if (!inserted && row.followedAt >= x.followedAt) { out.push(row) inserted = true }
  35. out.push(x)
  36. }
  37. if (!inserted) { out.push(row) }
  38. return out
  39. }
  40. // show id → the highest watched episode of it { seasonNumber, episodeNumber }, only for shows in `followed`
  41. static lastWatchedOf = (userId, followed) => {
  42. let best = {}
  43. for (w of watchesOfUser(userId)) {
  44. if (w.targetType == 'episode' && w.target != null) {
  45. let e = episodeById(w.target)
  46. if (e != null && e.show != null && followed[e.show] == true && e.seasonNumber != null && e.episodeNumber != null) {
  47. let b = best[e.show]
  48. if (b == null || e.seasonNumber > b.seasonNumber || (e.seasonNumber == b.seasonNumber && e.episodeNumber > b.episodeNumber)) {
  49. best[e.show] = { seasonNumber = e.seasonNumber episodeNumber = e.episodeNumber }
  50. }
  51. }
  52. }
  53. }
  54. return best
  55. }
  56. static myShowsRowsOf = (userId) => {
  57. if (userId == null) { return [] }
  58. let follows = followsOfUser(userId)
  59. let followed = {}
  60. for (f of follows) { followed[f.show] = true }
  61. let last = lastWatchedOf(userId, followed)
  62. let out = []
  63. for (f of follows) {
  64. let show = showById(f.show)
  65. if (show != null) {
  66. let poster = posterName(show)
  67. let lw = last[show.id]
  68. let hasEpisode = lw != null
  69. out = insertByFollowDesc(out, {
  70. id = show.id title = show.title href = '/shows/' + show.urlSegment
  71. // no poster name at all → still the route's placeholder (any unknown safe name falls back to it)
  72. posterUrl = poster != null ? '/posters/' + poster : '/posters/none'
  73. hasEpisode = hasEpisode
  74. episodeLabel = hasEpisode ? 'S' + pad2(lw.seasonNumber) + 'E' + pad2(lw.episodeNumber) : ''
  75. followedAt = f.at != null ? f.at : 0
  76. })
  77. }
  78. }
  79. return out
  80. }
  81. // ---- reactive members -----------------------------------------------------------------------
  82. session = null
  83. userId = userIdOfSession(session)
  84. signedIn = userId != null
  85. rows = signedIn ? myShowsRowsOf(userId) : []
  86. hasRows = rows.length > 0
  87. noRows = !hasRows
  88. emptyText = signedIn ? 'You follow no shows yet.' : 'Sign in to see the shows you follow.'
  89. __title = 'My shows — ' + appTitle
  90. View {
  91. myShowsPage {
  92. h1 { "My shows" }
  93. if (noRows) { p { class = "muted" emptyText } }
  94. if (hasRows) {
  95. myShowsRows {
  96. for (row of rows) {
  97. myShowRow {
  98. img { class = "myshow-poster" src = row.posterUrl alt = row.title }
  99. a { class = "myshow-title" href = row.href row.title }
  100. if (row.hasEpisode) { myShowEpisode { row.episodeLabel } }
  101. }
  102. }
  103. }
  104. }
  105. }
  106. }

Branches

Latest commits

  • 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