A personal media tracker built on the AT Protocol opnshelf.xyz
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: complete calendar redesign with week-based selection and database-backed queries

Frontend changes:
- Redesigned day cells to show release titles with colored dot indicators (blue for movies, purple for TV)
- Implemented week-based selection with dimming effect (non-selected days at 40% opacity)
- Added 'Go to today' link below month header for quick navigation
- Sidebar now displays all releases for selected week sorted by date
- Removed week/month/list view toggle in favor of unified month view
- Added placeholder cells at end of calendar grid to complete final week
- Fetches 3 months of data with React Query placeholderData for smooth transitions
- Fixed React key collisions by including season/episode numbers

Backend changes:
- Release calendar API now accepts startDate and endDate query parameters
- Service layer queries database directly for episodes and watchlist items
- Eliminated per-show TMDB API calls - single database query for date range
- Updated DTOs, controller, service, and all related tests

+251 -184
+65 -40
apps/web/src/routes/calendar.tsx
··· 38 38 function CalendarPage() { 39 39 const user = useUser(); 40 40 const [currentDate, setCurrentDate] = useState(new Date()); 41 - const [viewMode, setViewMode] = useState<"month" | "week" | "list">("month"); 42 41 const [selectedWeekStart, setSelectedWeekStart] = useState<Date | null>(null); 43 42 44 - // Fetch release calendar data 43 + // Calculate date range for 3 months (prev, current, next) 44 + const dateRange = useMemo(() => { 45 + const year = currentDate.getFullYear(); 46 + const month = currentDate.getMonth(); 47 + 48 + // Previous month 49 + const prevMonth = new Date(year, month - 1, 1); 50 + // Next month 51 + const nextMonth = new Date(year, month + 2, 0); // Last day of next month 52 + 53 + const startDate = prevMonth.toISOString().split("T")[0]; 54 + const endDate = nextMonth.toISOString().split("T")[0]; 55 + 56 + return { startDate, endDate }; 57 + }, [currentDate]); 58 + 59 + // Fetch release calendar data with date range 45 60 const { data: calendarData, isLoading } = useQuery({ 46 61 ...showsControllerGetUserReleaseCalendarOptions({ 47 62 path: { userDid: user?.did || "" }, 63 + query: dateRange, 48 64 }), 49 65 enabled: !!user?.did, 66 + // Keep previous data while fetching new data for smooth transitions 67 + placeholderData: (previousData) => previousData, 68 + staleTime: 5 * 60 * 1000, // 5 minutes 50 69 }); 51 70 52 71 // Transform API data into date-keyed format ··· 95 114 setSelectedWeekStart(null); 96 115 }; 97 116 117 + const goToToday = () => { 118 + const today = new Date(); 119 + setCurrentDate(today); 120 + setSelectedWeekStart(getWeekStart(today)); 121 + }; 122 + 98 123 const formatDateKey = (day: number) => { 99 124 const year = currentDate.getFullYear(); 100 125 const month = String(currentDate.getMonth() + 1).padStart(2, "0"); ··· 175 200 for (let i = 0; i < 7; i++) { 176 201 const date = new Date(selectedWeekStart); 177 202 date.setDate(selectedWeekStart.getDate() + i); 178 - const dateKey = date.toISOString().split("T")[0]; 203 + // Use local date components to match the formatDateKey function 204 + const year = date.getFullYear(); 205 + const month = String(date.getMonth() + 1).padStart(2, "0"); 206 + const day = String(date.getDate()).padStart(2, "0"); 207 + const dateKey = `${year}-${month}-${day}`; 179 208 const dayReleases = releases[dateKey] || []; 180 209 for (const release of dayReleases) { 181 210 weekReleases.push({ ...release, date: dateKey }); ··· 229 258 return ( 230 259 <div className="container-app py-8"> 231 260 {/* Header */} 232 - <div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> 233 - <div> 234 - <h1 className="text-display-2 mb-2">Release Calendar</h1> 235 - <p className="text-[var(--foreground-muted)]"> 236 - Track upcoming movies and TV shows you're following. 237 - </p> 238 - </div> 239 - 240 - {/* View Toggle */} 241 - <div className="flex items-center gap-2"> 242 - <div className="flex rounded-lg border border-[var(--border)] bg-[var(--background-elevated)] p-1"> 243 - {(["month", "week", "list"] as const).map((mode) => ( 244 - <button 245 - key={mode} 246 - type="button" 247 - onClick={() => setViewMode(mode)} 248 - className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${ 249 - viewMode === mode 250 - ? "bg-[var(--accent)] text-white" 251 - : "text-[var(--foreground-muted)] hover:text-[var(--foreground)]" 252 - }`} 253 - > 254 - {mode.charAt(0).toUpperCase() + mode.slice(1)} 255 - </button> 256 - ))} 257 - </div> 258 - </div> 261 + <div className="mb-8"> 262 + <h1 className="text-display-2 mb-2">Release Calendar</h1> 263 + <p className="text-[var(--foreground-muted)]"> 264 + Track upcoming movies and TV shows you're following. 265 + </p> 259 266 </div> 260 267 261 268 {/* Calendar Navigation */} ··· 265 272 Previous 266 273 </button> 267 274 268 - <h2 className="text-display-3"> 269 - {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()} 270 - </h2> 275 + <div className="flex flex-col items-center"> 276 + <h2 className="text-display-3"> 277 + {monthNames[currentDate.getMonth()]} {currentDate.getFullYear()} 278 + </h2> 279 + <button 280 + type="button" 281 + onClick={goToToday} 282 + className="text-sm text-[var(--foreground-muted)] hover:text-[var(--foreground)] transition-colors mt-1" 283 + > 284 + Go to today 285 + </button> 286 + </div> 271 287 272 288 <button type="button" onClick={nextMonth} className="btn btn-secondary"> 273 289 Next ··· 315 331 day, 316 332 ).toDateString(); 317 333 const inSelectedWeek = isInSelectedWeek(day); 334 + const isDimmed = selectedWeekStart && !inSelectedWeek; 318 335 319 336 return ( 320 337 <button ··· 334 351 : isToday 335 352 ? "border-[var(--accent)] bg-[var(--accent-subtle)]" 336 353 : "border-[var(--border)] bg-[var(--background-elevated)] hover:border-[var(--border-strong)]" 337 - }`} 354 + } ${isDimmed ? "opacity-40" : ""}`} 338 355 > 339 356 <span 340 357 className={`text-sm font-medium ${ ··· 350 367 <div className="mt-1 flex flex-col gap-0.5 w-full"> 351 368 {dayReleases.slice(0, 2).map((release) => ( 352 369 <div 353 - key={`${release.showId || release.movieId || release.title}-${release.releaseDate}`} 370 + key={`${release.showId || release.movieId || release.title}-${release.releaseDate}-${release.seasonNumber}-${release.episodeNumber}`} 354 371 className="flex items-center gap-1.5 overflow-hidden" 355 372 > 356 373 <div ··· 372 389 )} 373 390 </div> 374 391 )} 375 - {/* Selected week indicator */} 376 - {inSelectedWeek && ( 377 - <div className="absolute bottom-1.5 left-1.5 right-1.5 h-0.5 rounded-full bg-[var(--accent)]" /> 378 - )} 379 392 </button> 380 393 ); 381 394 })} 395 + 396 + {/* Empty cells for days after the last day of month to complete the grid */} 397 + {Array.from({ 398 + length: (7 - ((firstDayOfMonth + daysInMonth) % 7)) % 7, 399 + }).map((_, index) => ( 400 + <div 401 + // biome-ignore lint/suspicious/noArrayIndexKey: Empty calendar placeholder cells 402 + key={`calendar-end-empty-${index}`} 403 + className="h-24 rounded-lg bg-[var(--background-subtle)]" 404 + aria-hidden="true" 405 + /> 406 + ))} 382 407 </div> 383 408 </div> 384 409 ··· 412 437 <div className="space-y-3"> 413 438 {selectedWeekReleases.map((release) => ( 414 439 <Link 415 - key={`${release.showId || release.movieId}-${release.releaseDate}-${release.date}`} 440 + key={`${release.showId || release.movieId}-${release.releaseDate}-${release.date}-${release.seasonNumber}-${release.episodeNumber}`} 416 441 to={getItemUrl(release)} 417 442 className="card card-interactive flex items-center gap-3 p-3" 418 443 >
+18
backend/src/shows/dto/show.dto.ts
··· 611 611 total: number; 612 612 } 613 613 614 + export class ReleaseCalendarQueryDto { 615 + @ApiPropertyOptional({ 616 + description: "Start date for release calendar range (YYYY-MM-DD)", 617 + example: "2026-01-01", 618 + }) 619 + @IsOptional() 620 + @IsDateString() 621 + startDate?: string; 622 + 623 + @ApiPropertyOptional({ 624 + description: "End date for release calendar range (YYYY-MM-DD)", 625 + example: "2026-03-31", 626 + }) 627 + @IsOptional() 628 + @IsDateString() 629 + endDate?: string; 630 + } 631 + 614 632 export class MarkSeasonWatchedDto { 615 633 @ApiProperty({ description: "TMDB show ID" }) 616 634 @IsString()
+6 -1
backend/src/shows/shows.controller.spec.ts
··· 277 277 mockReleaseCalendar, 278 278 ); 279 279 280 - const result = await controller.getUserReleaseCalendar("did:plc:abc123"); 280 + const query = { startDate: "2026-01-01", endDate: "2026-03-31" }; 281 + const result = await controller.getUserReleaseCalendar( 282 + "did:plc:abc123", 283 + query, 284 + ); 281 285 282 286 expect(result).toEqual(mockReleaseCalendar); 283 287 expect(mockShowsService.getUserReleaseCalendar).toHaveBeenCalledWith( 284 288 "did:plc:abc123", 289 + query, 285 290 ); 286 291 }); 287 292 });
+7 -3
backend/src/shows/shows.controller.ts
··· 35 35 PaginatedEpisodesResponseDto, 36 36 PaginatedUpNextQueryDto, 37 37 PaginatedUpNextResponseDto, 38 + ReleaseCalendarQueryDto, 38 39 ReleaseCalendarResponseDto, 39 40 SearchShowsResultsDto, 40 41 TMDBEpisodeDto, ··· 204 205 @Get("user/:userDid/release-calendar") 205 206 @ApiOperation({ 206 207 summary: 207 - "Get upcoming releases for watched shows and future-dated watchlist items", 208 + "Get releases for watched shows and watchlist items within a date range", 208 209 }) 209 210 @ApiResponse({ status: 200, type: ReleaseCalendarResponseDto }) 210 - async getUserReleaseCalendar(@Param("userDid") userDid: string) { 211 - return this.showsService.getUserReleaseCalendar(userDid); 211 + async getUserReleaseCalendar( 212 + @Param("userDid") userDid: string, 213 + @Query() query: ReleaseCalendarQueryDto, 214 + ) { 215 + return this.showsService.getUserReleaseCalendar(userDid, query); 212 216 } 213 217 214 218 @Get("user/:userDid/episodes")
+26 -33
backend/src/shows/shows.service.spec.ts
··· 487 487 488 488 describe("getUserReleaseCalendar", () => { 489 489 it("should return upcoming tracked-show airings and future watchlist releases", async () => { 490 - const showsTmdb = ( 491 - service as unknown as { 492 - showsTmdb: { 493 - getShowDetails: (showId: string) => Promise<unknown>; 494 - }; 495 - } 496 - ).showsTmdb; 497 - const getShowDetailsSpy = jest.spyOn(showsTmdb, "getShowDetails"); 498 - 490 + // Mock tracked episodes to get the shows the user is watching 499 491 mockPrismaService.trackedEpisode.findMany.mockResolvedValue([ 500 492 { 501 493 id: "tracked-1", ··· 504 496 episodeNumber: 2, 505 497 watchedDate: new Date("2024-01-10T00:00:00.000Z"), 506 498 createdAt: new Date("2024-01-10T00:00:00.000Z"), 507 - show: { 508 - showId: "show-1", 509 - title: "Tracked Show", 510 - posterPath: "/tracked-show.jpg", 511 - backdropPath: "/tracked-show-backdrop.jpg", 512 - firstAirYear: 2024, 513 - firstAirDate: new Date("2024-01-01T00:00:00.000Z"), 514 - overview: "Tracked show overview", 515 - colors: { primary: "#111111" }, 516 - }, 517 499 }, 518 500 ]); 519 501 520 - getShowDetailsSpy.mockResolvedValue({ 521 - id: 1, 522 - name: "Tracked Show", 523 - popularity: 1, 524 - vote_average: 1, 525 - vote_count: 1, 526 - next_episode_to_air: { 527 - id: 101, 502 + // Mock episodes from watched shows with air dates in range 503 + mockPrismaService.episode.findMany.mockResolvedValue([ 504 + { 505 + id: "episode-1", 506 + tmdbId: 101, 507 + showId: "show-1", 508 + seasonNumber: 2, 509 + episodeNumber: 5, 528 510 name: "Broadcast Episode", 529 - season_number: 2, 530 - episode_number: 5, 531 - air_date: "2099-01-12", 511 + airDate: new Date("2099-01-12T00:00:00.000Z"), 532 512 overview: "Broadcast overview", 513 + season: { 514 + id: "season-1", 515 + show: { 516 + showId: "show-1", 517 + title: "Tracked Show", 518 + posterPath: "/tracked-show.jpg", 519 + backdropPath: "/tracked-show-backdrop.jpg", 520 + firstAirYear: 2024, 521 + firstAirDate: new Date("2024-01-01T00:00:00.000Z"), 522 + overview: "Tracked show overview", 523 + colors: { primary: "#111111" }, 524 + }, 525 + }, 533 526 }, 534 - }); 527 + ]); 535 528 536 529 mockPrismaService.show.findUnique.mockResolvedValue({ 537 530 posterPath: "/tracked-show.jpg", ··· 593 586 source: "watchlist", 594 587 mediaType: "movie", 595 588 releaseKind: "movie", 596 - releaseDate: "2099-01-10T00:00:00.000Z", 589 + releaseDate: "2099-01-10", 597 590 title: "Future Movie", 598 591 subtitle: "Watchlist movie release", 599 592 overview: "Movie overview", ··· 606 599 source: "watchlist", 607 600 mediaType: "show", 608 601 releaseKind: "show", 609 - releaseDate: "2099-01-11T00:00:00.000Z", 602 + releaseDate: "2099-01-11", 610 603 title: "Future Show", 611 604 subtitle: "Watchlist series release", 612 605 overview: "Show overview",
+117 -104
backend/src/shows/shows.service.ts
··· 1 1 import { Agent } from "@atproto/api"; 2 2 import { TID } from "@atproto/common"; 3 3 import { Injectable, Logger } from "@nestjs/common"; 4 - import { parseScopedShowMediaId } from "../lists/list-media-id.util"; 5 4 import { 6 5 $nsid as COLLECTION, 7 6 main as episodeSchema, 8 7 } from "../lexicons/xyz/opnshelf/episode"; 9 8 import type { Main as EpisodeRecord } from "../lexicons/xyz/opnshelf/episode.defs"; 9 + import { parseScopedShowMediaId } from "../lists/list-media-id.util"; 10 10 import { ColorExtractionService } from "../movies/color-extraction.service"; 11 11 import { PrismaService } from "../prisma/prisma.service"; 12 12 import { Prisma } from "../generated/client"; ··· 650 650 }; 651 651 } 652 652 653 - async getUserReleaseCalendar(userDid: string) { 654 - const trackedEpisodes = (await this.prisma.trackedEpisode.findMany({ 653 + async getUserReleaseCalendar( 654 + userDid: string, 655 + query?: { startDate?: string; endDate?: string }, 656 + ) { 657 + // Parse date range or default to all upcoming dates 658 + const startDate = query?.startDate 659 + ? new Date(query.startDate) 660 + : new Date("1970-01-01"); 661 + const endDate = query?.endDate 662 + ? new Date(query.endDate) 663 + : new Date("2099-12-31"); 664 + 665 + // Get shows the user is watching (has tracked episodes for) 666 + const trackedEpisodes = await this.prisma.trackedEpisode.findMany({ 655 667 where: { userDid }, 656 - include: { show: true }, 657 - orderBy: [{ watchedDate: "desc" }, { createdAt: "desc" }], 658 - })) as TrackedEpisodeWithShow[]; 668 + select: { showId: true }, 669 + distinct: ["showId"], 670 + }); 659 671 660 - const trackedShows = new Map< 661 - string, 662 - { 663 - showId: string; 664 - show: TrackedEpisodeWithShow["show"]; 665 - } 666 - >(); 672 + const showIds = trackedEpisodes.map((t) => t.showId); 667 673 668 - for (const tracked of trackedEpisodes) { 669 - const existing = trackedShows.get(tracked.showId); 670 - if (!existing) { 671 - trackedShows.set(tracked.showId, { 672 - showId: tracked.showId, 673 - show: tracked.show, 674 - }); 675 - } 676 - } 674 + // Query episodes from watched shows with air dates in range 675 + const episodes = await this.prisma.episode.findMany({ 676 + where: { 677 + showId: { in: showIds }, 678 + airDate: { 679 + gte: startDate, 680 + lte: endDate, 681 + }, 682 + }, 683 + include: { 684 + season: { 685 + include: { 686 + show: true, 687 + }, 688 + }, 689 + }, 690 + orderBy: { airDate: "asc" }, 691 + }); 677 692 678 - const watchingItems: Array<ReleaseCalendarItem | null> = await Promise.all( 679 - Array.from(trackedShows.values()).map(async ({ showId, show }) => { 680 - try { 681 - const showDetails = await this.showsTmdb.getShowDetails(showId); 682 - const nextEpisode = showDetails.next_episode_to_air; 693 + const watchingItems: ReleaseCalendarItem[] = await Promise.all( 694 + episodes.map(async (episode) => { 695 + const show = episode.season.show; 696 + const colors = await this.ensureShowHasColors(show.showId); 683 697 684 - if ( 685 - !nextEpisode?.air_date || 686 - !this.isUpcomingDate(nextEpisode.air_date) 687 - ) { 688 - return null; 689 - } 690 - 691 - const colors = await this.ensureShowHasColors(showId); 692 - 693 - return { 694 - source: "watching" as const, 695 - mediaType: "show" as const, 696 - releaseKind: "episode" as const, 697 - releaseDate: nextEpisode.air_date, 698 - title: show.title, 699 - subtitle: `S${nextEpisode.season_number} E${nextEpisode.episode_number} · ${nextEpisode.name || "Next airing episode"}`, 700 - overview: nextEpisode.overview ?? show.overview ?? undefined, 701 - posterPath: show.posterPath ?? undefined, 702 - backdropPath: show.backdropPath ?? undefined, 703 - showId, 704 - seasonNumber: nextEpisode.season_number, 705 - episodeNumber: nextEpisode.episode_number, 706 - colors: (colors ?? show.colors ?? undefined) as 707 - | ReleaseCalendarColors 708 - | undefined, 709 - }; 710 - } catch (error) { 711 - this.logger.warn( 712 - `Failed to compute release calendar entry for show ${showId}: ${error instanceof Error ? error.message : String(error)}`, 713 - ); 714 - return null; 715 - } 698 + return { 699 + source: "watching" as const, 700 + mediaType: "show" as const, 701 + releaseKind: "episode" as const, 702 + releaseDate: episode.airDate?.toISOString().split("T")[0] ?? "", 703 + title: show.title, 704 + subtitle: `S${episode.seasonNumber} E${episode.episodeNumber} · ${episode.name}`, 705 + overview: episode.overview ?? show.overview ?? undefined, 706 + posterPath: show.posterPath ?? undefined, 707 + backdropPath: show.backdropPath ?? undefined, 708 + showId: show.showId, 709 + seasonNumber: episode.seasonNumber, 710 + episodeNumber: episode.episodeNumber, 711 + colors: (colors ?? show.colors ?? undefined) as 712 + | ReleaseCalendarColors 713 + | undefined, 714 + }; 716 715 }), 717 716 ); 718 717 718 + // Get watchlist 719 719 const watchlist = await this.prisma.list.findFirst({ 720 720 where: { userDid, slug: "watchlist" }, 721 721 select: { 722 722 items: { 723 - orderBy: { createdAt: "desc" }, 723 + where: { 724 + OR: [ 725 + { 726 + mediaType: "movie", 727 + movie: { 728 + releaseDate: { 729 + gte: startDate, 730 + lte: endDate, 731 + }, 732 + }, 733 + }, 734 + { 735 + mediaType: "show", 736 + show: { 737 + firstAirDate: { 738 + gte: startDate, 739 + lte: endDate, 740 + }, 741 + }, 742 + }, 743 + ], 744 + }, 724 745 select: { 725 746 mediaType: true, 726 747 mediaId: true, ··· 756 777 ).flatMap((item): ReleaseCalendarItem[] => { 757 778 const watchlistItem = item as WatchlistReleaseItem; 758 779 759 - if (watchlistItem.mediaType === "movie") { 760 - const releaseDate = watchlistItem.movie?.releaseDate; 761 - if (!releaseDate || !this.isUpcomingDate(releaseDate)) { 762 - return []; 763 - } 764 - 780 + if (watchlistItem.mediaType === "movie" && watchlistItem.movie) { 765 781 return [ 766 782 { 767 783 source: "watchlist" as const, 768 784 mediaType: "movie" as const, 769 785 releaseKind: "movie" as const, 770 - releaseDate: releaseDate.toISOString(), 771 - title: watchlistItem.movie?.title ?? "Untitled", 786 + releaseDate: 787 + watchlistItem.movie.releaseDate?.toISOString().split("T")[0] ?? 788 + "", 789 + title: watchlistItem.movie.title, 772 790 subtitle: "Watchlist movie release", 773 - overview: watchlistItem.movie?.overview ?? undefined, 774 - posterPath: watchlistItem.movie?.posterPath ?? undefined, 775 - backdropPath: watchlistItem.movie?.backdropPath ?? undefined, 776 - movieId: watchlistItem.movie?.movieId ?? watchlistItem.mediaId, 777 - colors: watchlistItem.movie?.colors as 791 + overview: watchlistItem.movie.overview ?? undefined, 792 + posterPath: watchlistItem.movie.posterPath ?? undefined, 793 + backdropPath: watchlistItem.movie.backdropPath ?? undefined, 794 + movieId: watchlistItem.movie.movieId, 795 + colors: watchlistItem.movie.colors as 778 796 | ReleaseCalendarColors 779 797 | undefined, 780 798 }, 781 799 ]; 782 800 } 783 801 784 - if (parseScopedShowMediaId(watchlistItem.mediaId)) { 785 - return []; 786 - } 802 + if (watchlistItem.mediaType === "show" && watchlistItem.show) { 803 + // Skip scoped shows (season/episode specific) 804 + if (parseScopedShowMediaId(watchlistItem.mediaId)) { 805 + return []; 806 + } 787 807 788 - const releaseDate = watchlistItem.show?.firstAirDate; 789 - if (!releaseDate || !this.isUpcomingDate(releaseDate)) { 790 - return []; 808 + return [ 809 + { 810 + source: "watchlist" as const, 811 + mediaType: "show" as const, 812 + releaseKind: "show" as const, 813 + releaseDate: 814 + watchlistItem.show.firstAirDate?.toISOString().split("T")[0] ?? 815 + "", 816 + title: watchlistItem.show.title, 817 + subtitle: "Watchlist series release", 818 + overview: watchlistItem.show.overview ?? undefined, 819 + posterPath: watchlistItem.show.posterPath ?? undefined, 820 + backdropPath: watchlistItem.show.backdropPath ?? undefined, 821 + showId: watchlistItem.show.showId, 822 + colors: watchlistItem.show.colors as 823 + | ReleaseCalendarColors 824 + | undefined, 825 + }, 826 + ]; 791 827 } 792 828 793 - return [ 794 - { 795 - source: "watchlist" as const, 796 - mediaType: "show" as const, 797 - releaseKind: "show" as const, 798 - releaseDate: releaseDate.toISOString(), 799 - title: watchlistItem.show?.title ?? "Untitled", 800 - subtitle: "Watchlist series release", 801 - overview: watchlistItem.show?.overview ?? undefined, 802 - posterPath: watchlistItem.show?.posterPath ?? undefined, 803 - backdropPath: watchlistItem.show?.backdropPath ?? undefined, 804 - showId: watchlistItem.show?.showId ?? watchlistItem.mediaId, 805 - colors: watchlistItem.show?.colors as 806 - | ReleaseCalendarColors 807 - | undefined, 808 - }, 809 - ]; 829 + return []; 810 830 }); 811 831 812 832 const items: ReleaseCalendarItem[] = [...watchingItems, ...watchlistItems] 813 - .filter((item): item is ReleaseCalendarItem => item !== null) 833 + .filter((item) => item.releaseDate) 814 834 .sort((left, right) => { 815 835 const releaseDateCompare = 816 836 this.parseReleaseDate(left.releaseDate).getTime() - ··· 1336 1356 } 1337 1357 1338 1358 return new Date(value); 1339 - } 1340 - 1341 - private isUpcomingDate(value: string | Date) { 1342 - const releaseDate = this.parseReleaseDate(value); 1343 - const today = new Date(); 1344 - today.setUTCHours(0, 0, 0, 0); 1345 - return releaseDate.getTime() >= today.getTime(); 1346 1359 } 1347 1360 }
+1 -1
packages/api/src/generated/@tanstack/react-query.gen.ts
··· 607 607 export const showsControllerGetUserReleaseCalendarQueryKey = (options: Options<ShowsControllerGetUserReleaseCalendarData>) => createQueryKey('showsControllerGetUserReleaseCalendar', options); 608 608 609 609 /** 610 - * Get upcoming releases for watched shows and future-dated watchlist items 610 + * Get releases for watched shows and watchlist items within a date range 611 611 */ 612 612 export const showsControllerGetUserReleaseCalendarOptions = (options: Options<ShowsControllerGetUserReleaseCalendarData>) => queryOptions<ShowsControllerGetUserReleaseCalendarResponse, DefaultError, ShowsControllerGetUserReleaseCalendarResponse, ReturnType<typeof showsControllerGetUserReleaseCalendarQueryKey>>({ 613 613 queryFn: async ({ queryKey, signal }) => {
+1 -1
packages/api/src/generated/sdk.gen.ts
··· 161 161 export const showsControllerGetUserUpNext = <ThrowOnError extends boolean = false>(options: Options<ShowsControllerGetUserUpNextData, ThrowOnError>) => (options.client ?? client).get<ShowsControllerGetUserUpNextResponses, unknown, ThrowOnError>({ url: '/shows/user/{userDid}/up-next', ...options }); 162 162 163 163 /** 164 - * Get upcoming releases for watched shows and future-dated watchlist items 164 + * Get releases for watched shows and watchlist items within a date range 165 165 */ 166 166 export const showsControllerGetUserReleaseCalendar = <ThrowOnError extends boolean = false>(options: Options<ShowsControllerGetUserReleaseCalendarData, ThrowOnError>) => (options.client ?? client).get<ShowsControllerGetUserReleaseCalendarResponses, unknown, ThrowOnError>({ url: '/shows/user/{userDid}/release-calendar', ...options }); 167 167
+10 -1
packages/api/src/generated/types.gen.ts
··· 1665 1665 path: { 1666 1666 userDid: string; 1667 1667 }; 1668 - query?: never; 1668 + query?: { 1669 + /** 1670 + * Start date for release calendar range (YYYY-MM-DD) 1671 + */ 1672 + startDate?: string; 1673 + /** 1674 + * End date for release calendar range (YYYY-MM-DD) 1675 + */ 1676 + endDate?: string; 1677 + }; 1669 1678 url: '/shows/user/{userDid}/release-calendar'; 1670 1679 }; 1671 1680