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.

chore: typecheck

+94 -47
+40 -3
apps/web/src/routeTree.gen.ts
··· 13 13 import { Route as SearchRouteImport } from './routes/search' 14 14 import { Route as LoginRouteImport } from './routes/login' 15 15 import { Route as IndexRouteImport } from './routes/index' 16 + import { Route as MoviesMovieIdRouteImport } from './routes/movies.$movieId' 16 17 import { Route as AuthCompleteRouteImport } from './routes/auth/complete' 17 18 18 19 const ShelfRoute = ShelfRouteImport.update({ ··· 33 34 const IndexRoute = IndexRouteImport.update({ 34 35 id: '/', 35 36 path: '/', 37 + getParentRoute: () => rootRouteImport, 38 + } as any) 39 + const MoviesMovieIdRoute = MoviesMovieIdRouteImport.update({ 40 + id: '/movies/$movieId', 41 + path: '/movies/$movieId', 36 42 getParentRoute: () => rootRouteImport, 37 43 } as any) 38 44 const AuthCompleteRoute = AuthCompleteRouteImport.update({ ··· 47 53 '/search': typeof SearchRoute 48 54 '/shelf': typeof ShelfRoute 49 55 '/auth/complete': typeof AuthCompleteRoute 56 + '/movies/$movieId': typeof MoviesMovieIdRoute 50 57 } 51 58 export interface FileRoutesByTo { 52 59 '/': typeof IndexRoute ··· 54 61 '/search': typeof SearchRoute 55 62 '/shelf': typeof ShelfRoute 56 63 '/auth/complete': typeof AuthCompleteRoute 64 + '/movies/$movieId': typeof MoviesMovieIdRoute 57 65 } 58 66 export interface FileRoutesById { 59 67 __root__: typeof rootRouteImport ··· 62 70 '/search': typeof SearchRoute 63 71 '/shelf': typeof ShelfRoute 64 72 '/auth/complete': typeof AuthCompleteRoute 73 + '/movies/$movieId': typeof MoviesMovieIdRoute 65 74 } 66 75 export interface FileRouteTypes { 67 76 fileRoutesByFullPath: FileRoutesByFullPath 68 - fullPaths: '/' | '/login' | '/search' | '/shelf' | '/auth/complete' 77 + fullPaths: 78 + | '/' 79 + | '/login' 80 + | '/search' 81 + | '/shelf' 82 + | '/auth/complete' 83 + | '/movies/$movieId' 69 84 fileRoutesByTo: FileRoutesByTo 70 - to: '/' | '/login' | '/search' | '/shelf' | '/auth/complete' 71 - id: '__root__' | '/' | '/login' | '/search' | '/shelf' | '/auth/complete' 85 + to: 86 + | '/' 87 + | '/login' 88 + | '/search' 89 + | '/shelf' 90 + | '/auth/complete' 91 + | '/movies/$movieId' 92 + id: 93 + | '__root__' 94 + | '/' 95 + | '/login' 96 + | '/search' 97 + | '/shelf' 98 + | '/auth/complete' 99 + | '/movies/$movieId' 72 100 fileRoutesById: FileRoutesById 73 101 } 74 102 export interface RootRouteChildren { ··· 77 105 SearchRoute: typeof SearchRoute 78 106 ShelfRoute: typeof ShelfRoute 79 107 AuthCompleteRoute: typeof AuthCompleteRoute 108 + MoviesMovieIdRoute: typeof MoviesMovieIdRoute 80 109 } 81 110 82 111 declare module '@tanstack/react-router' { ··· 109 138 preLoaderRoute: typeof IndexRouteImport 110 139 parentRoute: typeof rootRouteImport 111 140 } 141 + '/movies/$movieId': { 142 + id: '/movies/$movieId' 143 + path: '/movies/$movieId' 144 + fullPath: '/movies/$movieId' 145 + preLoaderRoute: typeof MoviesMovieIdRouteImport 146 + parentRoute: typeof rootRouteImport 147 + } 112 148 '/auth/complete': { 113 149 id: '/auth/complete' 114 150 path: '/auth/complete' ··· 125 161 SearchRoute: SearchRoute, 126 162 ShelfRoute: ShelfRoute, 127 163 AuthCompleteRoute: AuthCompleteRoute, 164 + MoviesMovieIdRoute: MoviesMovieIdRoute, 128 165 } 129 166 export const routeTree = rootRouteImport 130 167 ._addFileChildren(rootRouteChildren)
+54 -44
apps/web/src/routes/movies.$movieId.tsx
··· 12 12 import { useMemo } from "react"; 13 13 import { usePosterColors } from "../hooks/usePosterColors"; 14 14 15 + // TMDB Movie Detail type based on API response 16 + interface TMDBMovieDetail { 17 + id: number; 18 + title: string; 19 + poster_path?: string; 20 + backdrop_path?: string; 21 + release_date?: string; 22 + overview?: string; 23 + runtime?: number; 24 + vote_average?: number; 25 + vote_count?: number; 26 + genres?: Array<{ id: number; name: string }>; 27 + } 28 + 15 29 export const Route = createFileRoute("/movies/$movieId")({ 16 30 component: MovieDetailPage, 17 31 }); ··· 28 42 }); 29 43 30 44 // Fetch movie details 31 - const { data: movie, isLoading: isMovieLoading } = useQuery({ 45 + const { data: movieData, isLoading: isMovieLoading } = useQuery({ 32 46 ...moviesControllerGetMovieDetailsOptions({ 33 47 path: { movieId }, 34 48 }), 35 49 }); 36 50 51 + const movie = movieData as TMDBMovieDetail | undefined; 52 + 37 53 // Fetch user's tracked movies 38 54 const { data: trackedMovies } = useQuery({ 39 55 ...moviesControllerGetUserMoviesOptions({ ··· 49 65 }, [trackedMovies, movieId]); 50 66 51 67 // Extract accent colors from poster 52 - const colors = usePosterColors(movie?.poster_path as string); 68 + const colors = usePosterColors(movie?.poster_path); 53 69 54 70 // Mutations for watchlist 55 71 const markMutation = useMutation({ ··· 89 105 unmarkMutation.variables?.path?.movieId === movieId); 90 106 91 107 const releaseYear = movie?.release_date 92 - ? new Date(movie.release_date as string).getFullYear() 108 + ? new Date(movie.release_date).getFullYear() 93 109 : null; 94 110 95 111 const backdropUrl = movie?.backdrop_path ··· 137 153 {/* Back button */} 138 154 <Link 139 155 to="/search" 156 + search={{ q: "" }} 140 157 className="absolute top-4 left-4 z-10 p-2 rounded-full bg-black/50 hover:bg-black/70 transition-colors" 141 158 > 142 159 <ArrowLeft className="w-5 h-5" /> ··· 157 174 {posterUrl ? ( 158 175 <img 159 176 src={posterUrl} 160 - alt={movie?.title as string} 177 + alt={movie?.title} 161 178 className="w-full aspect-2/3 object-cover" 162 179 /> 163 180 ) : ( ··· 219 236 > 220 237 <img 221 238 src={posterUrl} 222 - alt={movie?.title as string} 239 + alt={movie?.title} 223 240 className="w-full aspect-2/3 object-cover" 224 241 /> 225 242 </div> ··· 355 372 Release Date 356 373 </span> 357 374 <span className="text-gray-200 font-medium"> 358 - {new Date(movie.release_date as string).toLocaleDateString( 359 - "en-US", 360 - { 361 - year: "numeric", 362 - month: "long", 363 - day: "numeric", 364 - }, 365 - )} 375 + {new Date(movie.release_date).toLocaleDateString("en-US", { 376 + year: "numeric", 377 + month: "long", 378 + day: "numeric", 379 + })} 366 380 </span> 367 381 </div> 368 382 )} ··· 385 399 className="font-medium" 386 400 style={{ color: colors.accent }} 387 401 > 388 - {(movie.vote_average as number).toFixed(1)}/10 402 + {movie.vote_average.toFixed(1)}/10 389 403 </span> 390 404 </div> 391 405 )} ··· 395 409 Votes 396 410 </span> 397 411 <span className="text-gray-200 font-medium"> 398 - {(movie.vote_count as number).toLocaleString()} 412 + {movie.vote_count.toLocaleString()} 399 413 </span> 400 414 </div> 401 415 )} 402 416 </section> 403 417 404 418 {/* Genres */} 405 - {movie?.genres && 406 - (movie.genres as Array<{ id: number; name: string }>).length > 407 - 0 && ( 408 - <section> 409 - <h2 410 - className="text-xl font-semibold mb-3" 411 - style={{ color: colors.primary }} 412 - > 413 - Genres 414 - </h2> 415 - <div className="flex flex-wrap gap-2"> 416 - {(movie.genres as Array<{ id: number; name: string }>).map( 417 - (genre) => ( 418 - <span 419 - key={genre.id} 420 - className="px-4 py-2 rounded-full text-sm font-medium" 421 - style={{ 422 - backgroundColor: `${colors.primary}20`, 423 - color: colors.accent, 424 - border: `1px solid ${colors.primary}40`, 425 - }} 426 - > 427 - {genre.name} 428 - </span> 429 - ), 430 - )} 431 - </div> 432 - </section> 433 - )} 419 + {movie?.genres && movie.genres.length > 0 && ( 420 + <section> 421 + <h2 422 + className="text-xl font-semibold mb-3" 423 + style={{ color: colors.primary }} 424 + > 425 + Genres 426 + </h2> 427 + <div className="flex flex-wrap gap-2"> 428 + {movie.genres.map((genre) => ( 429 + <span 430 + key={genre.id} 431 + className="px-4 py-2 rounded-full text-sm font-medium" 432 + style={{ 433 + backgroundColor: `${colors.primary}20`, 434 + color: colors.accent, 435 + border: `1px solid ${colors.primary}40`, 436 + }} 437 + > 438 + {genre.name} 439 + </span> 440 + ))} 441 + </div> 442 + </section> 443 + )} 434 444 </div> 435 445 </div> 436 446 </div>