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: add person detail pages with filmography grid and watch toggle

+1365 -535
+511 -279
apps/mobile/app/person/[id].tsx
··· 3 3 PersonFilmographyItemDto, 4 4 TmdbPersonDetailDto, 5 5 } from "@opnshelf/api"; 6 - import { peopleControllerGetPersonDetailsOptions } from "@opnshelf/api"; 7 - import { useQuery } from "@tanstack/react-query"; 8 - import { Image } from "expo-image"; 6 + import { 7 + authControllerMeOptions, 8 + moviesControllerGetUserMoviesOptions, 9 + moviesControllerGetUserMoviesQueryKey, 10 + moviesControllerMarkWatchedMutation, 11 + moviesControllerUnmarkWatchedMutation, 12 + peopleControllerGetPersonDetailsOptions, 13 + peopleControllerGetPersonFilmographyInfiniteOptions, 14 + showsControllerGetUserShowsOptions, 15 + showsControllerGetUserShowsQueryKey, 16 + showsControllerMarkShowWatchedMutation, 17 + showsControllerUnmarkWatchedMutation, 18 + } from "@opnshelf/api"; 19 + import { FlashList } from "@shopify/flash-list"; 20 + import { 21 + useInfiniteQuery, 22 + useMutation, 23 + useQuery, 24 + useQueryClient, 25 + } from "@tanstack/react-query"; 9 26 import { useLocalSearchParams, useRouter } from "expo-router"; 10 - import { useMemo } from "react"; 27 + import { useMemo, useState } from "react"; 11 28 import { 29 + ActivityIndicator, 30 + Dimensions, 12 31 RefreshControl, 13 - ScrollView, 14 32 StyleSheet, 15 33 Text, 16 34 TouchableOpacity, 17 35 View, 18 36 } from "react-native"; 19 37 import { SafeAreaView } from "react-native-safe-area-context"; 38 + import { AddToListModal } from "@/components/AddToListModal"; 20 39 import { DetailHero } from "@/components/detail"; 40 + import { MovieItem } from "@/components/MovieItem"; 21 41 import { ScrollRevealHeader } from "@/components/ScrollRevealHeader"; 42 + import { ShowItem, type ShowItemData } from "@/components/ShowItem"; 22 43 import { borderRadius, spacing } from "@/constants/spacing"; 23 44 import { useTheme } from "@/contexts/theme"; 24 45 import { useScrollRevealHeader } from "@/hooks/useScrollRevealHeader"; 25 - import { getTmdbPosterUrl } from "@/lib/utils"; 46 + import { invalidateUserShelfQueries } from "@/lib/invalidate-shelf"; 26 47 27 48 const POSTER_BASE_URL = "https://image.tmdb.org/t/p/w92"; 49 + const SCREEN_WIDTH = Dimensions.get("window").width; 50 + const GAP = spacing.md; 51 + const H_PADDING = spacing.lg; 52 + const COLUMNS = 2; 53 + const ITEM_MARGIN = GAP / 2; 54 + const ITEM_WIDTH = (SCREEN_WIDTH - H_PADDING * 2) / COLUMNS - ITEM_MARGIN * 2; 28 55 29 56 function formatDate(dateString?: string): string | null { 30 57 if (!dateString) return null; ··· 45 72 return `${birthYear} - Present`; 46 73 } 47 74 75 + // Convert filmography item to MovieItem format 76 + function toMovieItem(item: PersonFilmographyItemDto): { 77 + id: number; 78 + title: string; 79 + poster_path?: string | null; 80 + release_date?: string; 81 + } { 82 + return { 83 + id: item.id, 84 + title: item.title, 85 + poster_path: item.poster_path, 86 + release_date: item.release_date, 87 + }; 88 + } 89 + 90 + // Convert filmography item to ShowItem format 91 + function toShowItem(item: PersonFilmographyItemDto): ShowItemData { 92 + return { 93 + id: item.id, 94 + name: item.title, 95 + poster_path: item.poster_path, 96 + first_air_date: item.first_air_date, 97 + }; 98 + } 99 + 48 100 export default function PersonDetailScreen() { 49 101 const { id: personId } = useLocalSearchParams<{ id: string }>(); 50 102 const router = useRouter(); 51 103 const { colors } = useTheme(); 52 104 const { showCompactHeader, onScroll } = useScrollRevealHeader(); 105 + const queryClient = useQueryClient(); 106 + 107 + const { data: user } = useQuery({ 108 + ...authControllerMeOptions(), 109 + staleTime: 5 * 60 * 1000, 110 + retry: false, 111 + }); 112 + const userDid = user?.did || ""; 53 113 54 114 const { 55 115 data: personData, ··· 62 122 }), 63 123 }); 64 124 125 + const { 126 + data: filmographyData, 127 + fetchNextPage, 128 + hasNextPage, 129 + isFetchingNextPage, 130 + } = useInfiniteQuery({ 131 + ...peopleControllerGetPersonFilmographyInfiniteOptions({ 132 + path: { personId }, 133 + }), 134 + getNextPageParam: (lastPage) => { 135 + if (lastPage.page < lastPage.totalPages) { 136 + return lastPage.page + 1; 137 + } 138 + return undefined; 139 + }, 140 + }); 141 + 142 + // Fetch user's tracked movies and shows 143 + const { data: trackedMovies } = useQuery({ 144 + ...moviesControllerGetUserMoviesOptions({ path: { userDid } }), 145 + enabled: !!userDid, 146 + }); 147 + 148 + const { data: trackedShows } = useQuery({ 149 + ...showsControllerGetUserShowsOptions({ path: { userDid } }), 150 + enabled: !!userDid, 151 + }); 152 + 65 153 const person = personData as TmdbPersonDetailDto | undefined; 66 154 67 155 const profileUrl = person?.profile_path ··· 86 174 await refetch(); 87 175 }; 88 176 177 + // Flatten all filmography items 178 + const filmographyItems = useMemo(() => { 179 + return filmographyData?.pages.flatMap((page) => page.items) ?? []; 180 + }, [filmographyData]); 181 + 182 + const totalFilmographyCount = filmographyData?.pages[0]?.total ?? 0; 183 + 184 + // Create lookup sets for watched items 185 + const watchedMovieIds = useMemo(() => { 186 + if (!trackedMovies) return new Set<string>(); 187 + return new Set(trackedMovies.map((m) => m.movieId)); 188 + }, [trackedMovies]); 189 + 190 + const watchedShowIds = useMemo(() => { 191 + if (!trackedShows) return new Set<string>(); 192 + return new Set(trackedShows.map((s) => s.showId)); 193 + }, [trackedShows]); 194 + 195 + // Calculate watched count 196 + const watchedCount = useMemo(() => { 197 + return filmographyItems.filter((item) => { 198 + if (item.media_type === "movie") { 199 + return watchedMovieIds.has(String(item.id)); 200 + } 201 + return watchedShowIds.has(String(item.id)); 202 + }).length; 203 + }, [filmographyItems, watchedMovieIds, watchedShowIds]); 204 + 205 + // Mutations 206 + const markMovieMutation = useMutation({ 207 + mutationKey: ["movies", "markWatched"], 208 + ...moviesControllerMarkWatchedMutation(), 209 + onSuccess: () => { 210 + queryClient.invalidateQueries({ 211 + queryKey: moviesControllerGetUserMoviesQueryKey({ path: { userDid } }), 212 + }); 213 + invalidateUserShelfQueries(queryClient, userDid); 214 + }, 215 + }); 216 + 217 + const unmarkMovieMutation = useMutation({ 218 + mutationKey: ["movies", "unmarkWatched"], 219 + ...moviesControllerUnmarkWatchedMutation(), 220 + onSuccess: () => { 221 + queryClient.invalidateQueries({ 222 + queryKey: moviesControllerGetUserMoviesQueryKey({ path: { userDid } }), 223 + }); 224 + invalidateUserShelfQueries(queryClient, userDid); 225 + }, 226 + }); 227 + 228 + const markShowMutation = useMutation({ 229 + mutationKey: ["shows", "markShowWatched"], 230 + ...showsControllerMarkShowWatchedMutation(), 231 + onSuccess: () => { 232 + queryClient.invalidateQueries({ 233 + queryKey: showsControllerGetUserShowsQueryKey({ path: { userDid } }), 234 + }); 235 + invalidateUserShelfQueries(queryClient, userDid); 236 + }, 237 + }); 238 + 239 + const unmarkShowMutation = useMutation({ 240 + mutationKey: ["shows", "unmarkWatched"], 241 + ...showsControllerUnmarkWatchedMutation(), 242 + onSuccess: () => { 243 + queryClient.invalidateQueries({ 244 + queryKey: showsControllerGetUserShowsQueryKey({ path: { userDid } }), 245 + }); 246 + invalidateUserShelfQueries(queryClient, userDid); 247 + }, 248 + }); 249 + 250 + // Modal state for add to list 251 + const [activeListModal, setActiveListModal] = useState<{ 252 + mediaType: "movie" | "show"; 253 + mediaId: string; 254 + title: string; 255 + } | null>(null); 256 + 257 + const handleToggleWatched = (item: PersonFilmographyItemDto) => { 258 + if (!user) return; 259 + 260 + const isMovie = item.media_type === "movie"; 261 + const mediaId = String(item.id); 262 + const isWatched = isMovie 263 + ? watchedMovieIds.has(mediaId) 264 + : watchedShowIds.has(mediaId); 265 + 266 + if (isWatched) { 267 + // Unmark immediately 268 + if (isMovie) { 269 + unmarkMovieMutation.mutate({ 270 + path: { movieId: mediaId }, 271 + query: { mode: "all" }, 272 + }); 273 + } else { 274 + unmarkShowMutation.mutate({ 275 + path: { showId: mediaId }, 276 + query: { mode: "all" }, 277 + }); 278 + } 279 + } else { 280 + // Mark with current date 281 + const now = new Date().toISOString(); 282 + if (isMovie) { 283 + markMovieMutation.mutate({ 284 + body: { movieId: mediaId, watchedAt: now }, 285 + }); 286 + } else { 287 + markShowMutation.mutate({ 288 + body: { showId: mediaId, watchedAt: now }, 289 + }); 290 + } 291 + } 292 + }; 293 + 294 + const _handleAddToList = (item: PersonFilmographyItemDto) => { 295 + if (!user) return; 296 + setActiveListModal({ 297 + mediaType: item.media_type === "movie" ? "movie" : "show", 298 + mediaId: String(item.id), 299 + title: item.title, 300 + }); 301 + }; 302 + 89 303 const handleNavigateToMedia = (item: PersonFilmographyItemDto) => { 90 304 if (item.media_type === "movie") { 91 305 router.push({ 92 306 pathname: "/movie/[id]", 93 - params: { 94 - id: String(item.id), 95 - title: item.title, 96 - }, 307 + params: { id: String(item.id), title: item.title }, 97 308 }); 98 309 } else { 99 310 router.push({ 100 311 pathname: "/show/[id]", 101 - params: { 102 - id: String(item.id), 103 - title: item.title, 104 - }, 312 + params: { id: String(item.id), title: item.title }, 105 313 }); 106 314 } 107 315 }; 108 316 317 + const renderFilmographyItem = ({ 318 + item, 319 + }: { 320 + item: PersonFilmographyItemDto; 321 + }) => { 322 + const isMovie = item.media_type === "movie"; 323 + const mediaId = String(item.id); 324 + const isWatched = isMovie 325 + ? watchedMovieIds.has(mediaId) 326 + : watchedShowIds.has(mediaId); 327 + 328 + const isMarking = isMovie 329 + ? markMovieMutation.isPending && 330 + markMovieMutation.variables?.body?.movieId === mediaId 331 + : markShowMutation.isPending && 332 + markShowMutation.variables?.body?.showId === mediaId; 333 + 334 + const isUnmarking = isMovie 335 + ? unmarkMovieMutation.isPending && 336 + unmarkMovieMutation.variables?.path?.movieId === mediaId 337 + : unmarkShowMutation.isPending && 338 + unmarkShowMutation.variables?.path?.showId === mediaId; 339 + 340 + const metaText = item.character || item.job || undefined; 341 + 342 + if (isMovie) { 343 + return ( 344 + <MovieItem 345 + movie={toMovieItem(item)} 346 + isWatched={user ? isWatched : undefined} 347 + isMarking={user ? isMarking : undefined} 348 + isUnmarking={user ? isUnmarking : undefined} 349 + onToggle={user ? () => handleToggleWatched(item) : undefined} 350 + onPress={() => handleNavigateToMedia(item)} 351 + width={ITEM_WIDTH} 352 + /> 353 + ); 354 + } 355 + 356 + return ( 357 + <ShowItem 358 + show={toShowItem(item)} 359 + isWatched={user ? isWatched : undefined} 360 + isMarking={user ? isMarking : undefined} 361 + isUnmarking={user ? isUnmarking : undefined} 362 + onToggle={user ? () => handleToggleWatched(item) : undefined} 363 + onPress={() => handleNavigateToMedia(item)} 364 + metaText={metaText} 365 + width={ITEM_WIDTH} 366 + /> 367 + ); 368 + }; 369 + 370 + const keyExtractor = (item: PersonFilmographyItemDto) => 371 + `${item.media_type}-${item.id}`; 372 + 109 373 return ( 110 374 <SafeAreaView 111 375 style={[styles.container, { backgroundColor: colors.background }]} 112 376 > 113 - <ScrollView 114 - contentContainerStyle={styles.scrollContent} 377 + <FlashList 378 + data={filmographyItems} 379 + renderItem={renderFilmographyItem} 380 + keyExtractor={keyExtractor} 381 + numColumns={COLUMNS} 382 + contentContainerStyle={styles.listContent} 383 + columnWrapperStyle={styles.columnWrapper} 115 384 onScroll={onScroll} 116 385 scrollEventThrottle={16} 117 386 refreshControl={ ··· 123 392 progressBackgroundColor={colors.surfaceContainerHigh} 124 393 /> 125 394 } 126 - > 127 - <DetailHero 128 - title={person?.name || "Person"} 129 - subtitle={subtitle} 130 - backdropUrl={null} 131 - posterUrl={profileUrl} 132 - colors={{ 133 - primary: colors.primary, 134 - secondary: colors.secondary, 135 - accent: colors.tertiary, 136 - muted: colors.surfaceContainerHighest, 137 - }} 138 - onBack={() => router.back()} 139 - isLoading={isLoading} 140 - /> 395 + ListHeaderComponent={ 396 + <View> 397 + <DetailHero 398 + title={person?.name || "Person"} 399 + subtitle={subtitle} 400 + backdropUrl={null} 401 + posterUrl={profileUrl} 402 + colors={{ 403 + primary: colors.primary, 404 + secondary: colors.secondary, 405 + accent: colors.tertiary, 406 + muted: colors.surfaceContainerHighest, 407 + }} 408 + onBack={() => router.back()} 409 + isLoading={isLoading} 410 + /> 141 411 142 - <View style={styles.content}> 143 - {/* Personal Info Section */} 144 - <View 145 - style={[ 146 - styles.infoCard, 147 - { backgroundColor: colors.surfaceContainer }, 148 - ]} 149 - > 150 - <Text style={[styles.infoTitle, { color: colors.primary }]}> 151 - Personal Info 152 - </Text> 153 - <View style={styles.infoItems}> 154 - {person?.birthday && ( 155 - <View style={styles.infoItem}> 156 - <Ionicons 157 - name="calendar-outline" 158 - size={16} 159 - color={colors.onSurfaceVariant} 160 - /> 412 + <View style={styles.content}> 413 + {/* Personal Info Section */} 414 + <View style={styles.section}> 415 + <Text style={[styles.sectionTitle, { color: colors.primary }]}> 416 + Personal Info 417 + </Text> 418 + <View style={styles.infoItems}> 419 + {person?.birthday && ( 420 + <View style={styles.infoItem}> 421 + <Ionicons 422 + name="calendar-outline" 423 + size={16} 424 + color={colors.onSurfaceVariant} 425 + /> 426 + <Text 427 + style={[ 428 + styles.infoText, 429 + { color: colors.onSurfaceVariant }, 430 + ]} 431 + > 432 + {person.deathday 433 + ? `Born: ${formatDate(person.birthday)}` 434 + : `Birthday: ${formatDate(person.birthday)}`} 435 + </Text> 436 + </View> 437 + )} 438 + {person?.deathday && ( 439 + <View style={styles.infoItem}> 440 + <Ionicons 441 + name="calendar-outline" 442 + size={16} 443 + color={colors.onSurfaceVariant} 444 + /> 445 + <Text 446 + style={[ 447 + styles.infoText, 448 + { color: colors.onSurfaceVariant }, 449 + ]} 450 + > 451 + Died: {formatDate(person.deathday)} 452 + </Text> 453 + </View> 454 + )} 455 + {person?.place_of_birth && ( 456 + <View style={styles.infoItem}> 457 + <Ionicons 458 + name="location-outline" 459 + size={16} 460 + color={colors.onSurfaceVariant} 461 + /> 462 + <Text 463 + style={[ 464 + styles.infoText, 465 + { color: colors.onSurfaceVariant }, 466 + ]} 467 + > 468 + {person.place_of_birth} 469 + </Text> 470 + </View> 471 + )} 472 + {person?.popularity !== undefined && ( 473 + <View style={styles.infoItem}> 474 + <Ionicons 475 + name="star-outline" 476 + size={16} 477 + color={colors.onSurfaceVariant} 478 + /> 479 + <Text 480 + style={[ 481 + styles.infoText, 482 + { color: colors.onSurfaceVariant }, 483 + ]} 484 + > 485 + Popularity: {person.popularity.toFixed(1)} 486 + </Text> 487 + </View> 488 + )} 489 + </View> 490 + </View> 491 + 492 + {/* Watched Progress Card */} 493 + {user && totalFilmographyCount > 0 && ( 494 + <View style={styles.section}> 161 495 <Text 162 - style={[ 163 - styles.infoText, 164 - { color: colors.onSurfaceVariant }, 165 - ]} 496 + style={[styles.sectionTitle, { color: colors.primary }]} 166 497 > 167 - {person.deathday 168 - ? `Born: ${formatDate(person.birthday)}` 169 - : `Birthday: ${formatDate(person.birthday)}`} 498 + Your Progress 170 499 </Text> 171 - </View> 172 - )} 173 - {person?.deathday && ( 174 - <View style={styles.infoItem}> 175 - <Ionicons 176 - name="calendar-outline" 177 - size={16} 178 - color={colors.onSurfaceVariant} 179 - /> 180 500 <Text 181 501 style={[ 182 - styles.infoText, 502 + styles.progressText, 183 503 { color: colors.onSurfaceVariant }, 184 504 ]} 185 505 > 186 - Died: {formatDate(person.deathday)} 506 + <Text style={{ fontWeight: "600", color: colors.primary }}> 507 + {watchedCount} 508 + </Text>{" "} 509 + of{" "} 510 + <Text style={{ fontWeight: "500" }}> 511 + {totalFilmographyCount} 512 + </Text>{" "} 513 + titles watched 187 514 </Text> 188 - </View> 189 - )} 190 - {person?.place_of_birth && ( 191 - <View style={styles.infoItem}> 192 - <Ionicons 193 - name="location-outline" 194 - size={16} 195 - color={colors.onSurfaceVariant} 196 - /> 197 - <Text 515 + <View 198 516 style={[ 199 - styles.infoText, 200 - { color: colors.onSurfaceVariant }, 517 + styles.progressBar, 518 + { backgroundColor: colors.surfaceContainerHigh }, 201 519 ]} 202 520 > 203 - {person.place_of_birth} 204 - </Text> 521 + <View 522 + style={[ 523 + styles.progressFill, 524 + { 525 + width: `${Math.min(100, (watchedCount / totalFilmographyCount) * 100)}%`, 526 + backgroundColor: colors.primary, 527 + }, 528 + ]} 529 + /> 530 + </View> 205 531 </View> 206 532 )} 207 - {person?.popularity !== undefined && ( 208 - <View style={styles.infoItem}> 209 - <Ionicons 210 - name="star-outline" 211 - size={16} 212 - color={colors.onSurfaceVariant} 213 - /> 533 + 534 + {/* Biography Section */} 535 + {person?.biography && ( 536 + <View style={styles.section}> 537 + <Text 538 + style={[styles.sectionTitle, { color: colors.primary }]} 539 + > 540 + Biography 541 + </Text> 214 542 <Text 215 543 style={[ 216 - styles.infoText, 544 + styles.biography, 217 545 { color: colors.onSurfaceVariant }, 218 546 ]} 219 547 > 220 - Popularity: {person.popularity.toFixed(1)} 548 + {person.biography} 221 549 </Text> 222 550 </View> 223 551 )} 552 + 553 + {/* Filmography Section Header */} 554 + <View style={styles.filmographyHeader}> 555 + <Text style={[styles.sectionTitle, { color: colors.primary }]}> 556 + Filmography 557 + <Text 558 + style={[styles.count, { color: colors.onSurfaceVariant }]} 559 + > 560 + ({totalFilmographyCount > 0 ? totalFilmographyCount : "..."}{" "} 561 + titles) 562 + </Text> 563 + </Text> 564 + </View> 224 565 </View> 225 566 </View> 226 - 227 - {/* Biography Section */} 228 - {person?.biography && ( 229 - <View style={styles.section}> 230 - <Text style={[styles.sectionTitle, { color: colors.primary }]}> 231 - Biography 232 - </Text> 567 + } 568 + ListFooterComponent={ 569 + hasNextPage ? ( 570 + <View style={styles.loadMoreContainer}> 571 + {isFetchingNextPage ? ( 572 + <ActivityIndicator color={colors.primary} /> 573 + ) : ( 574 + <TouchableOpacity 575 + onPress={() => fetchNextPage()} 576 + style={[ 577 + styles.showMoreButton, 578 + { backgroundColor: colors.surfaceContainer }, 579 + ]} 580 + activeOpacity={0.8} 581 + > 582 + <Text 583 + style={[styles.showMoreText, { color: colors.primary }]} 584 + > 585 + Show more 586 + </Text> 587 + </TouchableOpacity> 588 + )} 589 + </View> 590 + ) : filmographyItems.length > 0 ? ( 591 + <View style={styles.loadMoreContainer}> 233 592 <Text 234 - style={[styles.biography, { color: colors.onSurfaceVariant }]} 593 + style={[ 594 + styles.allLoadedText, 595 + { color: colors.onSurfaceVariant }, 596 + ]} 235 597 > 236 - {person.biography} 598 + Showing all {filmographyItems.length} titles 237 599 </Text> 238 600 </View> 239 - )} 240 - 241 - {/* Filmography Section */} 242 - <View style={styles.section}> 243 - <Text style={[styles.sectionTitle, { color: colors.primary }]}> 244 - Filmography 245 - <Text style={[styles.count, { color: colors.onSurfaceVariant }]}> 246 - {" "} 247 - ({person?.filmography?.length || 0} titles) 248 - </Text> 249 - </Text> 250 - <View style={styles.filmographyList}> 251 - {person?.filmography?.map((item) => ( 252 - <FilmographyItem 253 - key={`${item.media_type}-${item.id}-${item.character || item.job || ""}`} 254 - item={item} 255 - onPress={() => handleNavigateToMedia(item)} 256 - colors={colors} 257 - /> 258 - ))} 259 - </View> 260 - </View> 261 - </View> 262 - </ScrollView> 601 + ) : null 602 + } 603 + estimatedItemSize={200} 604 + /> 263 605 264 606 <ScrollRevealHeader 265 607 visible={showCompactHeader} 266 608 onBack={() => router.back()} 267 609 title={person?.name || "Person"} 268 610 /> 611 + 612 + {/* Add to List Modal */} 613 + {activeListModal && user && ( 614 + <AddToListModal 615 + open={!!activeListModal} 616 + onOpenChange={(open) => !open && setActiveListModal(null)} 617 + mediaType={activeListModal.mediaType} 618 + mediaId={activeListModal.mediaId} 619 + mediaTitle={activeListModal.title} 620 + user={user} 621 + /> 622 + )} 269 623 </SafeAreaView> 270 624 ); 271 625 } 272 626 273 - interface FilmographyItemProps { 274 - item: PersonFilmographyItemDto; 275 - onPress: () => void; 276 - colors: { 277 - surfaceContainer: string; 278 - onSurface: string; 279 - onSurfaceVariant: string; 280 - outline: string; 281 - primary: string; 282 - }; 283 - } 284 - 285 - function FilmographyItem({ item, onPress, colors }: FilmographyItemProps) { 286 - const year = item.release_date 287 - ? new Date(item.release_date).getFullYear() 288 - : item.first_air_date 289 - ? new Date(item.first_air_date).getFullYear() 290 - : null; 291 - 292 - const posterUrl = getTmdbPosterUrl(item.poster_path, "w92"); 293 - 294 - return ( 295 - <TouchableOpacity 296 - onPress={onPress} 297 - style={[ 298 - styles.filmographyItem, 299 - { backgroundColor: colors.surfaceContainer }, 300 - ]} 301 - activeOpacity={0.8} 302 - > 303 - {/* Poster */} 304 - <View style={styles.posterContainer}> 305 - {posterUrl ? ( 306 - <Image 307 - source={{ uri: posterUrl }} 308 - style={styles.poster} 309 - contentFit="cover" 310 - /> 311 - ) : ( 312 - <View 313 - style={[ 314 - styles.posterPlaceholder, 315 - { backgroundColor: colors.surfaceContainer }, 316 - ]} 317 - > 318 - <Ionicons 319 - name={item.media_type === "movie" ? "film-outline" : "tv-outline"} 320 - size={24} 321 - color={colors.onSurfaceVariant} 322 - /> 323 - </View> 324 - )} 325 - </View> 326 - 327 - {/* Info */} 328 - <View style={styles.filmographyInfo}> 329 - <Text 330 - style={[styles.filmographyTitle, { color: colors.onSurface }]} 331 - numberOfLines={1} 332 - > 333 - {item.title} 334 - </Text> 335 - <Text 336 - style={[styles.filmographyRole, { color: colors.onSurfaceVariant }]} 337 - numberOfLines={1} 338 - > 339 - {item.character || item.job || ""} 340 - {(item.character || item.job) && item.department && ( 341 - <Text style={{ color: colors.outline }}> • </Text> 342 - )} 343 - {item.department} 344 - </Text> 345 - </View> 346 - 347 - {/* Type Badge & Year */} 348 - <View style={styles.filmographyMeta}> 349 - <View 350 - style={[ 351 - styles.typeBadge, 352 - { backgroundColor: colors.surfaceContainer }, 353 - ]} 354 - > 355 - <Text 356 - style={[styles.typeBadgeText, { color: colors.onSurfaceVariant }]} 357 - > 358 - {item.media_type === "movie" ? "Movie" : "TV"} 359 - </Text> 360 - </View> 361 - {year && ( 362 - <Text style={[styles.yearText, { color: colors.primary }]}> 363 - {year} 364 - </Text> 365 - )} 366 - </View> 367 - </TouchableOpacity> 368 - ); 369 - } 370 - 371 627 const styles = StyleSheet.create({ 372 628 container: { 373 629 flex: 1, 374 630 }, 375 - scrollContent: { 631 + listContent: { 632 + paddingHorizontal: H_PADDING - ITEM_MARGIN, 376 633 paddingBottom: spacing.xxl, 377 634 }, 635 + columnWrapper: { 636 + gap: GAP, 637 + justifyContent: "space-between", 638 + }, 378 639 content: { 379 640 paddingHorizontal: spacing.md, 380 641 paddingTop: spacing.lg, 381 642 gap: spacing.lg, 382 643 }, 383 - infoCard: { 384 - padding: spacing.md, 385 - borderRadius: borderRadius.lg, 386 - }, 387 - infoTitle: { 388 - fontSize: 16, 389 - fontWeight: "600", 390 - marginBottom: spacing.sm, 391 - }, 392 644 infoItems: { 393 645 gap: spacing.xs, 394 646 }, ··· 400 652 infoText: { 401 653 fontSize: 14, 402 654 }, 655 + progressText: { 656 + fontSize: 14, 657 + marginBottom: spacing.xs, 658 + }, 659 + progressBar: { 660 + height: 8, 661 + borderRadius: borderRadius.full, 662 + overflow: "hidden", 663 + }, 664 + progressFill: { 665 + height: "100%", 666 + }, 403 667 section: { 404 - gap: spacing.sm, 668 + gap: spacing.xs, 669 + }, 670 + filmographyHeader: { 671 + marginTop: spacing.sm, 672 + marginBottom: spacing.md, 405 673 }, 406 674 sectionTitle: { 407 675 fontSize: 18, ··· 415 683 fontSize: 15, 416 684 lineHeight: 22, 417 685 }, 418 - filmographyList: { 419 - gap: spacing.sm, 420 - }, 421 - filmographyItem: { 422 - flexDirection: "row", 686 + loadMoreContainer: { 687 + padding: spacing.md, 423 688 alignItems: "center", 424 - padding: spacing.sm, 689 + }, 690 + showMoreButton: { 691 + paddingVertical: spacing.sm, 692 + paddingHorizontal: spacing.lg, 425 693 borderRadius: borderRadius.md, 426 - }, 427 - posterContainer: { 428 - width: 48, 429 - height: 72, 430 - borderRadius: borderRadius.sm, 431 - overflow: "hidden", 432 - marginRight: spacing.md, 433 - }, 434 - poster: { 435 - width: 48, 436 - height: 72, 437 - }, 438 - posterPlaceholder: { 439 - width: 48, 440 - height: 72, 441 - justifyContent: "center", 442 694 alignItems: "center", 443 - }, 444 - filmographyInfo: { 445 - flex: 1, 446 695 justifyContent: "center", 447 696 }, 448 - filmographyTitle: { 697 + showMoreText: { 449 698 fontSize: 14, 450 699 fontWeight: "500", 451 - marginBottom: 2, 452 700 }, 453 - filmographyRole: { 454 - fontSize: 12, 455 - }, 456 - filmographyMeta: { 457 - alignItems: "flex-end", 458 - gap: spacing.xs, 459 - }, 460 - typeBadge: { 461 - paddingHorizontal: spacing.sm, 462 - paddingVertical: 4, 463 - borderRadius: borderRadius.full, 464 - }, 465 - typeBadgeText: { 466 - fontSize: 11, 467 - fontWeight: "500", 468 - }, 469 - yearText: { 470 - fontSize: 13, 471 - fontWeight: "600", 701 + allLoadedText: { 702 + fontSize: 14, 703 + textAlign: "center", 472 704 }, 473 705 });
+193
apps/web/src/components/ErrorPage.tsx
··· 1 + import type { ErrorComponentProps } from "@tanstack/react-router"; 2 + import { AlertTriangle, ArrowLeft, Home, RefreshCw } from "lucide-react"; 3 + import * as React from "react"; 4 + import { M3Button } from "@/components/ui/m3-button"; 5 + import { 6 + M3Card, 7 + M3CardContent, 8 + M3CardDescription, 9 + M3CardHeader, 10 + M3CardTitle, 11 + } from "@/components/ui/m3-card"; 12 + 13 + export function ErrorPage({ error, reset }: ErrorComponentProps) { 14 + const [canGoBack, setCanGoBack] = React.useState(false); 15 + 16 + React.useEffect(() => { 17 + setCanGoBack(window.history.length > 1); 18 + }, []); 19 + 20 + // Get error message based on error type 21 + const errorMessage = React.useMemo(() => { 22 + if (error instanceof Error) { 23 + return error.message; 24 + } 25 + if (typeof error === "string") { 26 + return error; 27 + } 28 + if (error && typeof error === "object" && "message" in error) { 29 + return String(error.message); 30 + } 31 + return "An unexpected error occurred"; 32 + }, [error]); 33 + 34 + // Get error status/code if available (from API errors, etc.) 35 + const errorStatus = React.useMemo(() => { 36 + if ( 37 + error && 38 + typeof error === "object" && 39 + "status" in error && 40 + typeof error.status === "number" 41 + ) { 42 + return error.status; 43 + } 44 + return null; 45 + }, [error]); 46 + 47 + return ( 48 + <section className="relative isolate flex flex-1 items-center overflow-hidden"> 49 + <div 50 + className="pointer-events-none absolute inset-0" 51 + style={{ 52 + background: 53 + "radial-gradient(circle at top left, color-mix(in srgb, var(--md-sys-color-error) 14%, transparent), transparent 34%), radial-gradient(circle at bottom right, color-mix(in srgb, var(--md-sys-color-error) 8%, transparent), transparent 28%)", 54 + }} 55 + /> 56 + 57 + <div className="container relative mx-auto flex w-full max-w-7xl flex-1 px-4 py-12 md:py-20"> 58 + <div className="grid w-full gap-6 lg:grid-cols-[1.1fr_0.9fr] lg:items-center"> 59 + <div className="max-w-2xl"> 60 + <div 61 + className="mb-5 inline-flex items-center gap-2 rounded-full border px-4 py-2" 62 + style={{ 63 + backgroundColor: 64 + "color-mix(in srgb, var(--md-sys-color-error) 10%, transparent)", 65 + borderColor: 66 + "color-mix(in srgb, var(--md-sys-color-error) 24%, transparent)", 67 + color: "var(--md-sys-color-error)", 68 + }} 69 + > 70 + <AlertTriangle className="size-4" /> 71 + <span className="md-label-large"> 72 + {errorStatus ? `${errorStatus} · ` : ""}Something went wrong 73 + </span> 74 + </div> 75 + 76 + <h1 className="md-display-small mb-4 max-w-xl"> 77 + This page hit a technical snag. 78 + </h1> 79 + 80 + <p 81 + className="md-title-large max-w-xl" 82 + style={{ color: "var(--md-sys-color-on-surface-variant)" }} 83 + > 84 + {errorMessage || 85 + "An unexpected error occurred while loading this page. The issue may be temporary or related to your connection."} 86 + </p> 87 + 88 + <div className="mt-8 flex flex-wrap gap-3"> 89 + <M3Button variant="filled" size="lg" asChild> 90 + <a href="/"> 91 + <Home className="size-5" /> 92 + Go home 93 + </a> 94 + </M3Button> 95 + 96 + <M3Button variant="tonal" size="lg" onClick={reset}> 97 + <RefreshCw className="size-5" /> 98 + Try again 99 + </M3Button> 100 + 101 + {canGoBack ? ( 102 + <M3Button 103 + variant="text" 104 + size="lg" 105 + type="button" 106 + onClick={() => window.history.back()} 107 + > 108 + <ArrowLeft className="size-5" /> 109 + Go back 110 + </M3Button> 111 + ) : null} 112 + </div> 113 + </div> 114 + 115 + <M3Card 116 + variant="elevated" 117 + className="rounded-xl border" 118 + style={{ borderColor: "var(--md-sys-color-outline-variant)" }} 119 + > 120 + <M3CardHeader className="gap-3 p-6 pb-4"> 121 + <div 122 + className="flex size-14 items-center justify-center rounded-full" 123 + style={{ 124 + backgroundColor: "var(--md-sys-color-error-container)", 125 + color: "var(--md-sys-color-on-error-container)", 126 + }} 127 + > 128 + <AlertTriangle className="size-7" /> 129 + </div> 130 + <M3CardTitle className="md-headline-small"> 131 + Let&apos;s get you back on track 132 + </M3CardTitle> 133 + <M3CardDescription className="md-body-large"> 134 + Choose a recovery path below to continue using OpnShelf. 135 + </M3CardDescription> 136 + </M3CardHeader> 137 + 138 + <M3CardContent className="space-y-4 p-6 pt-0"> 139 + <div 140 + className="rounded-xl border p-4" 141 + style={{ 142 + backgroundColor: "var(--md-sys-color-surface-container)", 143 + borderColor: "var(--md-sys-color-outline-variant)", 144 + }} 145 + > 146 + <p className="mb-1 text-sm font-semibold text-(--md-sys-color-on-surface)"> 147 + Return to the dashboard 148 + </p> 149 + <p className="text-sm text-(--md-sys-color-on-surface-variant)"> 150 + Start fresh from home and access your shelf, lists, and queue. 151 + </p> 152 + </div> 153 + 154 + <div 155 + className="rounded-xl border p-4" 156 + style={{ 157 + backgroundColor: "var(--md-sys-color-surface-container)", 158 + borderColor: "var(--md-sys-color-outline-variant)", 159 + }} 160 + > 161 + <p className="mb-1 text-sm font-semibold text-(--md-sys-color-on-surface)"> 162 + Try reloading the page 163 + </p> 164 + <p className="text-sm text-(--md-sys-color-on-surface-variant)"> 165 + If this is a temporary glitch, the Try again button above will 166 + reload the route data. 167 + </p> 168 + </div> 169 + 170 + <div 171 + className="rounded-xl border p-4" 172 + style={{ 173 + backgroundColor: 174 + "color-mix(in srgb, var(--md-sys-color-error) 10%, transparent)", 175 + borderColor: 176 + "color-mix(in srgb, var(--md-sys-color-error) 24%, transparent)", 177 + }} 178 + > 179 + <p className="mb-1 text-sm font-semibold text-(--md-sys-color-on-surface)"> 180 + Check your connection 181 + </p> 182 + <p className="text-sm text-(--md-sys-color-on-surface-variant)"> 183 + If the error persists, it may be due to a network or server 184 + issue. 185 + </p> 186 + </div> 187 + </M3CardContent> 188 + </M3Card> 189 + </div> 190 + </div> 191 + </section> 192 + ); 193 + }
+2
apps/web/src/routes/__root.tsx
··· 13 13 import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; 14 14 import React from "react"; 15 15 import { Toaster } from "sonner"; 16 + import { ErrorPage } from "@/components/ErrorPage"; 16 17 import { NotFoundPage } from "@/components/NotFoundPage"; 17 18 import { TraktImportStatusToast } from "@/components/TraktImportStatusToast"; 18 19 import { ThemeProvider } from "@/components/theme-provider"; ··· 64 65 65 66 component: RootComponent, 66 67 notFoundComponent: NotFoundPage, 68 + errorComponent: ErrorPage, 67 69 shellComponent: RootDocument, 68 70 }); 69 71
+359 -126
apps/web/src/routes/person.$personId.$name.tsx
··· 1 1 import { 2 + authControllerMeOptions, 3 + moviesControllerGetUserMoviesOptions, 4 + moviesControllerGetUserMoviesQueryKey, 5 + moviesControllerMarkWatchedMutation, 6 + moviesControllerUnmarkWatchedMutation, 2 7 type PersonFilmographyItemDto, 3 8 peopleControllerGetPersonDetailsOptions, 9 + peopleControllerGetPersonFilmographyInfiniteOptions, 10 + showsControllerGetUserShowsOptions, 11 + showsControllerGetUserShowsQueryKey, 12 + showsControllerMarkShowWatchedMutation, 13 + showsControllerUnmarkWatchedMutation, 4 14 type TmdbPersonDetailDto, 5 15 } from "@opnshelf/api"; 6 - import { useQuery } from "@tanstack/react-query"; 7 - import { createFileRoute, Link, useRouter } from "@tanstack/react-router"; 8 - import { Calendar, Film, MapPin, Star, Tv } from "lucide-react"; 9 - import { useMemo } from "react"; 16 + import { 17 + useInfiniteQuery, 18 + useMutation, 19 + useQuery, 20 + useQueryClient, 21 + } from "@tanstack/react-query"; 22 + import { createFileRoute, useRouter } from "@tanstack/react-router"; 23 + import { Calendar, MapPin, Star } from "lucide-react"; 24 + import { useMemo, useState } from "react"; 25 + import { DatePickerModal } from "@/components/DatePickerModal"; 10 26 import { DetailHero } from "@/components/detail"; 27 + import { MediaPosterCard } from "@/components/MediaPosterCard"; 11 28 import { useTheme } from "@/components/theme-provider"; 12 - import { getTmdbPosterUrl, getTmdbProfileUrl } from "@/lib/utils"; 29 + import { createTitleSlug, getTmdbProfileUrl } from "@/lib/utils"; 13 30 14 31 export const Route = createFileRoute("/person/$personId/$name")({ 15 32 loader: async ({ params, context }) => { ··· 87 104 const { personId, name } = Route.useParams(); 88 105 const router = useRouter(); 89 106 const { seedColor } = useTheme(); 107 + const queryClient = useQueryClient(); 108 + 109 + const { data: user } = useQuery({ 110 + ...authControllerMeOptions(), 111 + staleTime: 5 * 60 * 1000, 112 + retry: false, 113 + }); 114 + const userDid = user?.did || ""; 90 115 91 116 const { data: personData, isLoading: isPersonLoading } = useQuery({ 92 117 ...peopleControllerGetPersonDetailsOptions({ ··· 94 119 }), 95 120 }); 96 121 122 + const { 123 + data: filmographyData, 124 + fetchNextPage, 125 + hasNextPage, 126 + isFetchingNextPage, 127 + status, 128 + } = useInfiniteQuery({ 129 + ...peopleControllerGetPersonFilmographyInfiniteOptions({ 130 + path: { personId }, 131 + }), 132 + getNextPageParam: (lastPage) => { 133 + if (lastPage.page < lastPage.totalPages) { 134 + return lastPage.page + 1; 135 + } 136 + return undefined; 137 + }, 138 + }); 139 + 140 + // Fetch user's tracked movies and shows for watch status 141 + const { data: trackedMovies } = useQuery({ 142 + ...moviesControllerGetUserMoviesOptions({ 143 + path: { userDid }, 144 + }), 145 + enabled: !!userDid, 146 + }); 147 + 148 + const { data: trackedShows } = useQuery({ 149 + ...showsControllerGetUserShowsOptions({ 150 + path: { userDid }, 151 + }), 152 + enabled: !!userDid, 153 + }); 154 + 97 155 const person = personData as TmdbPersonDetailDto | undefined; 98 156 99 157 const colors = { ··· 156 214 return items; 157 215 }, [person]); 158 216 217 + // Flatten all filmography items from infinite query pages 218 + const filmographyItems = useMemo(() => { 219 + return filmographyData?.pages.flatMap((page) => page.items) ?? []; 220 + }, [filmographyData]); 221 + 222 + const totalFilmographyCount = filmographyData?.pages[0]?.total ?? 0; 223 + 224 + // Create lookup sets for watched items 225 + const watchedMovieIds = useMemo(() => { 226 + if (!trackedMovies) return new Set<string>(); 227 + return new Set(trackedMovies.map((m) => m.movieId)); 228 + }, [trackedMovies]); 229 + 230 + const watchedShowIds = useMemo(() => { 231 + if (!trackedShows) return new Set<string>(); 232 + return new Set(trackedShows.map((s) => s.showId)); 233 + }, [trackedShows]); 234 + 235 + // Calculate watched count 236 + const watchedCount = useMemo(() => { 237 + return filmographyItems.filter((item) => { 238 + if (item.media_type === "movie") { 239 + return watchedMovieIds.has(String(item.id)); 240 + } 241 + return watchedShowIds.has(String(item.id)); 242 + }).length; 243 + }, [filmographyItems, watchedMovieIds, watchedShowIds]); 244 + 245 + // Mark watched mutations 246 + const markMovieMutation = useMutation({ 247 + mutationKey: ["movies", "markWatched"], 248 + ...moviesControllerMarkWatchedMutation(), 249 + onSuccess: () => { 250 + queryClient.invalidateQueries({ 251 + queryKey: moviesControllerGetUserMoviesQueryKey({ path: { userDid } }), 252 + }); 253 + }, 254 + }); 255 + 256 + const unmarkMovieMutation = useMutation({ 257 + mutationKey: ["movies", "unmarkWatched"], 258 + ...moviesControllerUnmarkWatchedMutation(), 259 + onSuccess: () => { 260 + queryClient.invalidateQueries({ 261 + queryKey: moviesControllerGetUserMoviesQueryKey({ path: { userDid } }), 262 + }); 263 + }, 264 + }); 265 + 266 + const markShowMutation = useMutation({ 267 + mutationKey: ["shows", "markShowWatched"], 268 + ...showsControllerMarkShowWatchedMutation(), 269 + onSuccess: () => { 270 + queryClient.invalidateQueries({ 271 + queryKey: showsControllerGetUserShowsQueryKey({ path: { userDid } }), 272 + }); 273 + }, 274 + }); 275 + 276 + const unmarkShowMutation = useMutation({ 277 + mutationKey: ["shows", "unmarkWatched"], 278 + ...showsControllerUnmarkWatchedMutation(), 279 + onSuccess: () => { 280 + queryClient.invalidateQueries({ 281 + queryKey: showsControllerGetUserShowsQueryKey({ path: { userDid } }), 282 + }); 283 + }, 284 + }); 285 + 286 + // Modal states 287 + const [datePickerModal, setDatePickerModal] = useState<{ 288 + mediaType: "movie" | "show"; 289 + mediaId: string; 290 + title: string; 291 + isWatched: boolean; 292 + } | null>(null); 293 + 294 + const handleToggleWatched = (item: PersonFilmographyItemDto) => { 295 + if (!user) return; 296 + 297 + const isMovie = item.media_type === "movie"; 298 + const mediaId = String(item.id); 299 + const isWatched = isMovie 300 + ? watchedMovieIds.has(mediaId) 301 + : watchedShowIds.has(mediaId); 302 + 303 + if (isWatched) { 304 + // Unmark 305 + if (isMovie) { 306 + unmarkMovieMutation.mutate({ 307 + path: { movieId: mediaId }, 308 + query: { mode: "all" }, 309 + }); 310 + } else { 311 + unmarkShowMutation.mutate({ 312 + path: { showId: mediaId }, 313 + query: { mode: "all" }, 314 + }); 315 + } 316 + } else { 317 + // Open date picker for marking 318 + setDatePickerModal({ 319 + mediaType: item.media_type, 320 + mediaId, 321 + title: item.title, 322 + isWatched: false, 323 + }); 324 + } 325 + }; 326 + 327 + const handleMarkWithDate = (date: Date) => { 328 + if (!datePickerModal || !user) return; 329 + 330 + const { mediaType, mediaId } = datePickerModal; 331 + 332 + if (mediaType === "movie") { 333 + markMovieMutation.mutate({ 334 + body: { 335 + movieId: mediaId, 336 + watchedAt: date.toISOString(), 337 + }, 338 + }); 339 + } else { 340 + markShowMutation.mutate({ 341 + body: { 342 + showId: mediaId, 343 + watchedAt: date.toISOString(), 344 + }, 345 + }); 346 + } 347 + 348 + setDatePickerModal(null); 349 + }; 350 + 159 351 return ( 160 352 <div 161 353 className="min-h-screen m3-background m3-on-background" ··· 177 369 <div className="container mx-auto px-4 py-6 max-w-7xl"> 178 370 <div className="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-8 min-w-0"> 179 371 <div className="space-y-4 min-w-0"> 180 - {/* Sidebar content - could add actions later */} 372 + {/* Sidebar content */} 181 373 <div className="m3-surface-container rounded-xl p-4"> 182 374 <h3 183 375 className="m3-title-medium mb-3" ··· 198 390 ))} 199 391 </div> 200 392 </div> 393 + 394 + {/* Watched Stats Card */} 395 + {user && filmographyItems.length > 0 && ( 396 + <div className="m3-surface-container rounded-xl p-4"> 397 + <h3 398 + className="m3-title-medium mb-2" 399 + style={{ color: colors.primary }} 400 + > 401 + Your Progress 402 + </h3> 403 + <div 404 + className="text-sm" 405 + style={{ color: "var(--md-sys-color-on-surface-variant)" }} 406 + > 407 + <span 408 + className="font-semibold" 409 + style={{ color: colors.primary }} 410 + > 411 + {watchedCount} 412 + </span>{" "} 413 + of{" "} 414 + <span className="font-medium">{totalFilmographyCount}</span>{" "} 415 + titles watched 416 + </div> 417 + {totalFilmographyCount > 0 && ( 418 + <div className="mt-3 h-2 rounded-full bg-(--md-sys-color-surface-container-high)"> 419 + <div 420 + className="h-full rounded-full transition-all" 421 + style={{ 422 + width: `${Math.min(100, (watchedCount / totalFilmographyCount) * 100)}%`, 423 + backgroundColor: colors.primary, 424 + }} 425 + /> 426 + </div> 427 + )} 428 + </div> 429 + )} 201 430 </div> 202 431 203 432 <div className="space-y-6 min-w-0"> ··· 218 447 219 448 {/* Filmography Section */} 220 449 <section> 221 - <h2 222 - className="text-xl font-semibold mb-4" 223 - style={{ color: colors.primary }} 224 - > 225 - Filmography 226 - <span className="ml-2 text-sm font-normal text-(--md-sys-color-on-surface-variant)"> 227 - ({person?.filmography?.length || 0} titles) 228 - </span> 229 - </h2> 230 - <div className="space-y-3"> 231 - {person?.filmography?.map((item: PersonFilmographyItemDto) => ( 232 - <FilmographyItem 233 - key={`${item.media_type}-${item.id}-${item.character || item.job || ""}`} 234 - item={item} 235 - colors={colors} 236 - /> 237 - ))} 450 + <div className="flex items-center justify-between mb-4"> 451 + <h2 452 + className="text-xl font-semibold" 453 + style={{ color: colors.primary }} 454 + > 455 + Filmography 456 + <span className="ml-2 text-sm font-normal text-(--md-sys-color-on-surface-variant)"> 457 + ({totalFilmographyCount > 0 ? totalFilmographyCount : "..."}{" "} 458 + titles) 459 + </span> 460 + </h2> 461 + </div> 462 + 463 + {/* Filmography grid - first page loads automatically */} 464 + <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"> 465 + {filmographyItems.map((item: PersonFilmographyItemDto) => { 466 + const isMovie = item.media_type === "movie"; 467 + const id = String(item.id); 468 + const year = isMovie 469 + ? item.release_date?.split("-")[0] 470 + : item.first_air_date?.split("-")[0]; 471 + const isWatched = isMovie 472 + ? watchedMovieIds.has(id) 473 + : watchedShowIds.has(id); 474 + const isShelfPending = isMovie 475 + ? (markMovieMutation.isPending && 476 + markMovieMutation.variables?.body?.movieId === id) || 477 + (unmarkMovieMutation.isPending && 478 + unmarkMovieMutation.variables?.path?.movieId === id) 479 + : (markShowMutation.isPending && 480 + markShowMutation.variables?.body?.showId === id) || 481 + (unmarkShowMutation.isPending && 482 + unmarkShowMutation.variables?.path?.showId === id); 483 + 484 + return ( 485 + <MediaPosterCard 486 + key={`${item.media_type}-${item.id}`} 487 + posterPath={item.poster_path} 488 + title={item.title} 489 + subtitle={year} 490 + to={ 491 + isMovie 492 + ? "/movies/$movieId/$title" 493 + : "/shows/$showId/$title" 494 + } 495 + params={ 496 + isMovie 497 + ? { 498 + movieId: id, 499 + title: createTitleSlug(item.title), 500 + } 501 + : { 502 + showId: id, 503 + title: createTitleSlug(item.title), 504 + } 505 + } 506 + user={user} 507 + isOnShelf={isWatched} 508 + isShelfPending={isShelfPending} 509 + onToggleShelf={() => handleToggleWatched(item)} 510 + listMedia={ 511 + user 512 + ? { 513 + type: isMovie ? "movie" : "show", 514 + id, 515 + title: item.title, 516 + } 517 + : undefined 518 + } 519 + /> 520 + ); 521 + })} 522 + </div> 523 + 524 + {/* Show more button / loading state */} 525 + <div className="py-6"> 526 + {isFetchingNextPage ? ( 527 + <div className="flex items-center justify-center gap-2 text-(--md-sys-color-on-surface-variant)"> 528 + <div 529 + className="animate-spin rounded-full h-5 w-5 border-b-2" 530 + style={{ borderColor: colors.primary }} 531 + /> 532 + <span className="text-sm">Loading...</span> 533 + </div> 534 + ) : hasNextPage ? ( 535 + <button 536 + type="button" 537 + onClick={() => fetchNextPage()} 538 + className="w-full py-3 px-4 rounded-lg bg-(--md-sys-color-surface-container) hover:bg-(--md-sys-color-surface-container-high) transition-colors text-sm font-medium" 539 + style={{ color: colors.primary }} 540 + > 541 + Show more 542 + </button> 543 + ) : filmographyItems.length > 0 ? ( 544 + <p className="text-center text-sm text-(--md-sys-color-on-surface-variant)"> 545 + Showing all {filmographyItems.length} titles 546 + </p> 547 + ) : status === "success" ? ( 548 + <p className="text-center text-sm text-(--md-sys-color-on-surface-variant)"> 549 + No filmography available 550 + </p> 551 + ) : null} 238 552 </div> 239 553 </section> 240 554 </div> 241 555 </div> 242 556 </div> 243 557 558 + {/* Date Picker Modal */} 559 + {datePickerModal && user && ( 560 + <DatePickerModal 561 + open={!!datePickerModal} 562 + onClose={() => setDatePickerModal(null)} 563 + mode={datePickerModal.mediaType === "movie" ? "movie" : "show"} 564 + movieId={ 565 + datePickerModal.mediaType === "movie" 566 + ? datePickerModal.mediaId 567 + : undefined 568 + } 569 + showId={ 570 + datePickerModal.mediaType === "show" 571 + ? datePickerModal.mediaId 572 + : undefined 573 + } 574 + userDid={user.did} 575 + modalTitle={`Mark "${datePickerModal.title}" as watched`} 576 + onSelect={handleMarkWithDate} 577 + /> 578 + )} 579 + 244 580 {isPersonLoading && ( 245 581 <div 246 582 className="fixed inset-0 flex items-center justify-center z-50" ··· 257 593 </div> 258 594 ); 259 595 } 260 - 261 - interface FilmographyItemProps { 262 - item: PersonFilmographyItemDto; 263 - colors: { primary: string }; 264 - } 265 - 266 - function FilmographyItem({ item, colors }: FilmographyItemProps) { 267 - const year = item.release_date 268 - ? new Date(item.release_date).getFullYear() 269 - : item.first_air_date 270 - ? new Date(item.first_air_date).getFullYear() 271 - : null; 272 - 273 - const posterUrl = getTmdbPosterUrl(item.poster_path, "w92"); 274 - 275 - const role = item.character || item.job || ""; 276 - const department = item.department || ""; 277 - 278 - // Determine the route based on media type 279 - const routeTo = 280 - item.media_type === "movie" 281 - ? { 282 - to: "/movies/$movieId/$title", 283 - params: { 284 - movieId: String(item.id), 285 - title: item.title.toLowerCase().replace(/\s+/g, "-"), 286 - }, 287 - } 288 - : { 289 - to: "/shows/$showId/$title", 290 - params: { 291 - showId: String(item.id), 292 - title: item.title.toLowerCase().replace(/\s+/g, "-"), 293 - }, 294 - }; 295 - 296 - return ( 297 - <Link 298 - to={routeTo.to} 299 - params={routeTo.params} 300 - className="flex gap-4 p-3 rounded-lg transition-colors hover:bg-(--md-sys-color-surface-container) group" 301 - > 302 - {/* Poster */} 303 - <div className="shrink-0 w-16 aspect-2/3 rounded-md overflow-hidden bg-(--md-sys-color-surface-container-high)"> 304 - {posterUrl ? ( 305 - <img 306 - src={posterUrl} 307 - alt={item.title} 308 - className="w-full h-full object-cover" 309 - loading="lazy" 310 - /> 311 - ) : ( 312 - <div className="w-full h-full flex items-center justify-center"> 313 - {item.media_type === "movie" ? ( 314 - <Film className="w-6 h-6 text-(--md-sys-color-on-surface-variant)" /> 315 - ) : ( 316 - <Tv className="w-6 h-6 text-(--md-sys-color-on-surface-variant)" /> 317 - )} 318 - </div> 319 - )} 320 - </div> 321 - 322 - {/* Info */} 323 - <div className="flex-1 min-w-0"> 324 - <div className="flex items-start justify-between gap-2"> 325 - <div className="flex-1 min-w-0"> 326 - <h3 className="font-medium line-clamp-1 group-hover:text-(--md-sys-color-primary) transition-colors"> 327 - {item.title} 328 - </h3> 329 - <p className="text-sm text-(--md-sys-color-on-surface-variant) line-clamp-1"> 330 - {role && <span>{role}</span>} 331 - {role && department && ( 332 - <span className="text-(--md-sys-color-outline)"> • </span> 333 - )} 334 - {department && <span>{department}</span>} 335 - </p> 336 - </div> 337 - <div className="flex items-center gap-2 shrink-0"> 338 - {/* Media Type Badge */} 339 - <span 340 - className="text-xs px-2 py-1 rounded-full" 341 - style={{ 342 - backgroundColor: "var(--md-sys-color-surface-container-high)", 343 - color: "var(--md-sys-color-on-surface-variant)", 344 - }} 345 - > 346 - {item.media_type === "movie" ? "Movie" : "TV"} 347 - </span> 348 - {/* Year */} 349 - {year && ( 350 - <span 351 - className="text-sm font-medium" 352 - style={{ color: colors.primary }} 353 - > 354 - {year} 355 - </span> 356 - )} 357 - </div> 358 - </div> 359 - </div> 360 - </Link> 361 - ); 362 - }
+29
backend/src/people/dto/person.dto.ts
··· 77 77 @ApiProperty({ type: [PersonFilmographyItemDto] }) 78 78 filmography: PersonFilmographyItemDto[]; 79 79 } 80 + 81 + export class PersonFilmographyResponseDto { 82 + @ApiProperty({ type: [PersonFilmographyItemDto] }) 83 + items: PersonFilmographyItemDto[]; 84 + 85 + @ApiProperty() 86 + total: number; 87 + 88 + @ApiProperty() 89 + page: number; 90 + 91 + @ApiProperty() 92 + pageSize: number; 93 + 94 + @ApiProperty() 95 + totalPages: number; 96 + } 97 + 98 + export class PersonFilmographyQueryDto { 99 + @ApiPropertyOptional({ description: "Page number (1-based)", default: 1 }) 100 + @IsOptional() 101 + @IsNumber() 102 + page?: number = 1; 103 + 104 + @ApiPropertyOptional({ description: "Items per page", default: 20 }) 105 + @IsOptional() 106 + @IsNumber() 107 + pageSize?: number = 20; 108 + }
+64 -3
backend/src/people/people.controller.ts
··· 1 - import { Controller, Get, NotFoundException, Param } from "@nestjs/common"; 2 - import { ApiOperation, ApiParam, ApiResponse, ApiTags } from "@nestjs/swagger"; 3 - import { TmdbPersonDetailDto } from "./dto/person.dto"; 1 + import { 2 + Controller, 3 + Get, 4 + NotFoundException, 5 + Param, 6 + Query, 7 + } from "@nestjs/common"; 8 + import { 9 + ApiOperation, 10 + ApiParam, 11 + ApiQuery, 12 + ApiResponse, 13 + ApiTags, 14 + } from "@nestjs/swagger"; 15 + import { 16 + TmdbPersonDetailDto, 17 + PersonFilmographyResponseDto, 18 + } from "./dto/person.dto"; 4 19 import { PeopleService } from "./people.service"; 5 20 6 21 @ApiTags("People") ··· 26 41 ): Promise<TmdbPersonDetailDto> { 27 42 try { 28 43 return await this.peopleService.getPersonDetails(personId); 44 + } catch (error) { 45 + if (error instanceof Error && error.message === "Person not found") { 46 + throw new NotFoundException("Person not found"); 47 + } 48 + throw error; 49 + } 50 + } 51 + 52 + @Get("tmdb/:personId/filmography") 53 + @ApiOperation({ summary: "Get paginated person filmography from TMDB" }) 54 + @ApiParam({ 55 + name: "personId", 56 + description: "TMDB person ID", 57 + type: String, 58 + }) 59 + @ApiQuery({ 60 + name: "page", 61 + required: false, 62 + description: "Page number (1-based)", 63 + type: Number, 64 + }) 65 + @ApiQuery({ 66 + name: "pageSize", 67 + required: false, 68 + description: "Items per page", 69 + type: Number, 70 + }) 71 + @ApiResponse({ 72 + status: 200, 73 + description: "Filmography retrieved successfully", 74 + type: PersonFilmographyResponseDto, 75 + }) 76 + @ApiResponse({ status: 404, description: "Person not found" }) 77 + async getPersonFilmography( 78 + @Param("personId") personId: string, 79 + @Query("page") page?: string, 80 + @Query("pageSize") pageSize?: string, 81 + ): Promise<PersonFilmographyResponseDto> { 82 + try { 83 + const pageNum = page ? parseInt(page, 10) : 1; 84 + const pageSizeNum = pageSize ? parseInt(pageSize, 10) : 20; 85 + return await this.peopleService.getPersonFilmography( 86 + personId, 87 + pageNum, 88 + pageSizeNum, 89 + ); 29 90 } catch (error) { 30 91 if (error instanceof Error && error.message === "Person not found") { 31 92 throw new NotFoundException("Person not found");
+29 -6
backend/src/people/people.service.ts
··· 1 1 import { Injectable } from "@nestjs/common"; 2 2 import { PeopleTmdbService } from "./people-tmdb.service"; 3 - import type { TmdbPersonDetailDto } from "./dto/person.dto"; 3 + import type { 4 + TmdbPersonDetailDto, 5 + PersonFilmographyResponseDto, 6 + } from "./dto/person.dto"; 4 7 5 8 @Injectable() 6 9 export class PeopleService { 7 10 constructor(private readonly peopleTmdbService: PeopleTmdbService) {} 8 11 9 12 async getPersonDetails(personId: string): Promise<TmdbPersonDetailDto> { 10 - const [person, filmography] = await Promise.all([ 11 - this.peopleTmdbService.getPersonDetails(personId), 12 - this.peopleTmdbService.getCombinedFilmography(personId), 13 - ]); 13 + const person = await this.peopleTmdbService.getPersonDetails(personId); 14 14 15 15 return { 16 16 id: person.id, ··· 22 22 place_of_birth: person.place_of_birth, 23 23 known_for_department: person.known_for_department, 24 24 popularity: person.popularity, 25 - filmography, 25 + filmography: [], // Filmography is now fetched separately with pagination 26 + }; 27 + } 28 + 29 + async getPersonFilmography( 30 + personId: string, 31 + page: number = 1, 32 + pageSize: number = 20, 33 + ): Promise<PersonFilmographyResponseDto> { 34 + const filmography = 35 + await this.peopleTmdbService.getCombinedFilmography(personId); 36 + 37 + const total = filmography.length; 38 + const totalPages = Math.ceil(total / pageSize); 39 + const startIndex = (page - 1) * pageSize; 40 + const endIndex = startIndex + pageSize; 41 + const paginatedItems = filmography.slice(startIndex, endIndex); 42 + 43 + return { 44 + items: paginatedItems, 45 + total, 46 + page, 47 + pageSize, 48 + totalPages, 26 49 }; 27 50 } 28 51 }
+65 -2
packages/api/src/generated/@tanstack/react-query.gen.ts
··· 3 3 import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; 4 4 5 5 import { client } from '../client.gen'; 6 - import { authControllerBlueskyProfileStatus, authControllerCallback, authControllerGetClientMetadata, authControllerLogin, authControllerLogout, authControllerMe, authControllerSignup, authControllerSuggestions, listsControllerAddItemToList, listsControllerCreateList, listsControllerDeleteList, listsControllerGetList, listsControllerGetListsForItem, listsControllerGetPublicUserList, listsControllerGetPublicUserLists, listsControllerGetUserLists, listsControllerRemoveItemFromList, listsControllerUpdateList, moviesControllerDeleteWatchHistoryEntry, moviesControllerDiscoverMovies, moviesControllerGetMovie, moviesControllerGetMovieDetails, moviesControllerGetMovieWatchHistory, moviesControllerGetUserMovies, moviesControllerGetUserMoviesPaginated, moviesControllerMarkWatched, moviesControllerSearchMovies, moviesControllerUnmarkWatched, type Options, searchControllerDiscoverAll, searchControllerSearchAll, shelfControllerGetUserActivitySummary, shelfControllerGetUserShelf, showsControllerDeleteEpisodeWatchHistoryEntry, showsControllerDiscoverShows, showsControllerGetEpisodeDetails, showsControllerGetLocalEpisodes, showsControllerGetLocalSeasons, showsControllerGetSeasonDetails, showsControllerGetShow, showsControllerGetShowDetails, showsControllerGetShowWatchHistory, showsControllerGetUserEpisodesPaginated, showsControllerGetUserReleaseCalendar, showsControllerGetUserShows, showsControllerGetUserUpNext, showsControllerMarkSeasonWatched, showsControllerMarkShowWatched, showsControllerMarkWatched, showsControllerSearchShows, showsControllerUnmarkWatched, socialControllerFollow, socialControllerGetFeed, socialControllerGetFollowers, socialControllerGetFollowing, socialControllerGetRelationship, socialControllerGetWatchers, socialControllerSearchPeople, socialControllerUnfollow, usersControllerCompleteOnboarding, usersControllerDeleteMyAccount, usersControllerDeleteMyAvatar, usersControllerFetchMyTraktPublicHistory, usersControllerGetAvatar, usersControllerGetMyAccountDeletion, usersControllerGetMyCurrentTraktImport, usersControllerGetMySettings, usersControllerGetPublicProfile, usersControllerImportMyBlueskyFollows, usersControllerImportMyHistory, usersControllerStartMyTraktImport, usersControllerUpdateMyProfile, usersControllerUpdateMySettings, usersControllerUploadMyAvatar } from '../sdk.gen'; 7 - import type { AuthControllerBlueskyProfileStatusData, AuthControllerBlueskyProfileStatusResponse, AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerMeData, AuthControllerMeResponse, AuthControllerSignupData, AuthControllerSuggestionsData, ListsControllerAddItemToListData, ListsControllerCreateListData, ListsControllerCreateListResponse, ListsControllerDeleteListData, ListsControllerGetListData, ListsControllerGetListResponse, ListsControllerGetListsForItemData, ListsControllerGetListsForItemResponse, ListsControllerGetPublicUserListData, ListsControllerGetPublicUserListResponse, ListsControllerGetPublicUserListsData, ListsControllerGetPublicUserListsResponse, ListsControllerGetUserListsData, ListsControllerGetUserListsResponse, ListsControllerRemoveItemFromListData, ListsControllerUpdateListData, ListsControllerUpdateListResponse, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryResponse, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponse, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponse, MoviesControllerGetMovieResponse, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryResponse, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponse, MoviesControllerGetUserMoviesResponse, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedResponse, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponse, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedResponse, SearchControllerDiscoverAllData, SearchControllerDiscoverAllResponse, SearchControllerSearchAllData, SearchControllerSearchAllResponse, ShelfControllerGetUserActivitySummaryData, ShelfControllerGetUserActivitySummaryResponse, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponse, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryResponse, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponse, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponse, ShowsControllerGetLocalEpisodesData, ShowsControllerGetLocalEpisodesResponse, ShowsControllerGetLocalSeasonsData, ShowsControllerGetLocalSeasonsResponse, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponse, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponse, ShowsControllerGetShowResponse, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryResponse, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponse, ShowsControllerGetUserReleaseCalendarData, ShowsControllerGetUserReleaseCalendarResponse, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponse, ShowsControllerGetUserUpNextData, ShowsControllerGetUserUpNextResponse, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedResponse, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedResponse, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedResponse, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponse, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponse, SocialControllerFollowData, SocialControllerFollowResponse, SocialControllerGetFeedData, SocialControllerGetFeedResponse, SocialControllerGetFollowersData, SocialControllerGetFollowersResponse, SocialControllerGetFollowingData, SocialControllerGetFollowingResponse, SocialControllerGetRelationshipData, SocialControllerGetRelationshipResponse, SocialControllerGetWatchersData, SocialControllerGetWatchersResponse, SocialControllerSearchPeopleData, SocialControllerSearchPeopleResponse, SocialControllerUnfollowData, SocialControllerUnfollowResponse, UsersControllerCompleteOnboardingData, UsersControllerCompleteOnboardingResponse, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountResponse, UsersControllerDeleteMyAvatarData, UsersControllerDeleteMyAvatarResponse, UsersControllerFetchMyTraktPublicHistoryData, UsersControllerFetchMyTraktPublicHistoryResponse, UsersControllerGetAvatarData, UsersControllerGetMyAccountDeletionData, UsersControllerGetMyAccountDeletionResponse, UsersControllerGetMyCurrentTraktImportData, UsersControllerGetMyCurrentTraktImportResponse, UsersControllerGetMySettingsData, UsersControllerGetMySettingsResponse, UsersControllerGetPublicProfileData, UsersControllerGetPublicProfileResponse, UsersControllerImportMyBlueskyFollowsData, UsersControllerImportMyBlueskyFollowsResponse, UsersControllerImportMyHistoryData, UsersControllerImportMyHistoryResponse, UsersControllerStartMyTraktImportData, UsersControllerStartMyTraktImportResponse, UsersControllerUpdateMyProfileData, UsersControllerUpdateMyProfileResponse, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsResponse, UsersControllerUploadMyAvatarData, UsersControllerUploadMyAvatarResponse } from '../types.gen'; 6 + import { authControllerBlueskyProfileStatus, authControllerCallback, authControllerGetClientMetadata, authControllerLogin, authControllerLogout, authControllerMe, authControllerSignup, authControllerSuggestions, listsControllerAddItemToList, listsControllerCreateList, listsControllerDeleteList, listsControllerGetList, listsControllerGetListsForItem, listsControllerGetPublicUserList, listsControllerGetPublicUserLists, listsControllerGetUserLists, listsControllerRemoveItemFromList, listsControllerUpdateList, moviesControllerDeleteWatchHistoryEntry, moviesControllerDiscoverMovies, moviesControllerGetMovie, moviesControllerGetMovieDetails, moviesControllerGetMovieWatchHistory, moviesControllerGetUserMovies, moviesControllerGetUserMoviesPaginated, moviesControllerMarkWatched, moviesControllerSearchMovies, moviesControllerUnmarkWatched, type Options, peopleControllerGetPersonDetails, peopleControllerGetPersonFilmography, searchControllerDiscoverAll, searchControllerSearchAll, shelfControllerGetUserActivitySummary, shelfControllerGetUserShelf, showsControllerDeleteEpisodeWatchHistoryEntry, showsControllerDiscoverShows, showsControllerGetEpisodeDetails, showsControllerGetLocalEpisodes, showsControllerGetLocalSeasons, showsControllerGetSeasonDetails, showsControllerGetShow, showsControllerGetShowDetails, showsControllerGetShowWatchHistory, showsControllerGetUserEpisodesPaginated, showsControllerGetUserReleaseCalendar, showsControllerGetUserShows, showsControllerGetUserUpNext, showsControllerMarkSeasonWatched, showsControllerMarkShowWatched, showsControllerMarkWatched, showsControllerSearchShows, showsControllerUnmarkWatched, socialControllerFollow, socialControllerGetFeed, socialControllerGetFollowers, socialControllerGetFollowing, socialControllerGetRelationship, socialControllerGetWatchers, socialControllerSearchPeople, socialControllerUnfollow, usersControllerCompleteOnboarding, usersControllerDeleteMyAccount, usersControllerDeleteMyAvatar, usersControllerFetchMyTraktPublicHistory, usersControllerGetAvatar, usersControllerGetMyAccountDeletion, usersControllerGetMyCurrentTraktImport, usersControllerGetMySettings, usersControllerGetPublicProfile, usersControllerImportMyBlueskyFollows, usersControllerImportMyHistory, usersControllerStartMyTraktImport, usersControllerUpdateMyProfile, usersControllerUpdateMySettings, usersControllerUploadMyAvatar } from '../sdk.gen'; 7 + import type { AuthControllerBlueskyProfileStatusData, AuthControllerBlueskyProfileStatusResponse, AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerMeData, AuthControllerMeResponse, AuthControllerSignupData, AuthControllerSuggestionsData, ListsControllerAddItemToListData, ListsControllerCreateListData, ListsControllerCreateListResponse, ListsControllerDeleteListData, ListsControllerGetListData, ListsControllerGetListResponse, ListsControllerGetListsForItemData, ListsControllerGetListsForItemResponse, ListsControllerGetPublicUserListData, ListsControllerGetPublicUserListResponse, ListsControllerGetPublicUserListsData, ListsControllerGetPublicUserListsResponse, ListsControllerGetUserListsData, ListsControllerGetUserListsResponse, ListsControllerRemoveItemFromListData, ListsControllerUpdateListData, ListsControllerUpdateListResponse, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryResponse, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponse, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponse, MoviesControllerGetMovieResponse, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryResponse, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponse, MoviesControllerGetUserMoviesResponse, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedResponse, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponse, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedResponse, PeopleControllerGetPersonDetailsData, PeopleControllerGetPersonDetailsResponse, PeopleControllerGetPersonFilmographyData, PeopleControllerGetPersonFilmographyResponse, SearchControllerDiscoverAllData, SearchControllerDiscoverAllResponse, SearchControllerSearchAllData, SearchControllerSearchAllResponse, ShelfControllerGetUserActivitySummaryData, ShelfControllerGetUserActivitySummaryResponse, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponse, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryResponse, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponse, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponse, ShowsControllerGetLocalEpisodesData, ShowsControllerGetLocalEpisodesResponse, ShowsControllerGetLocalSeasonsData, ShowsControllerGetLocalSeasonsResponse, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponse, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponse, ShowsControllerGetShowResponse, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryResponse, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponse, ShowsControllerGetUserReleaseCalendarData, ShowsControllerGetUserReleaseCalendarResponse, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponse, ShowsControllerGetUserUpNextData, ShowsControllerGetUserUpNextResponse, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedResponse, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedResponse, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedResponse, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponse, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponse, SocialControllerFollowData, SocialControllerFollowResponse, SocialControllerGetFeedData, SocialControllerGetFeedResponse, SocialControllerGetFollowersData, SocialControllerGetFollowersResponse, SocialControllerGetFollowingData, SocialControllerGetFollowingResponse, SocialControllerGetRelationshipData, SocialControllerGetRelationshipResponse, SocialControllerGetWatchersData, SocialControllerGetWatchersResponse, SocialControllerSearchPeopleData, SocialControllerSearchPeopleResponse, SocialControllerUnfollowData, SocialControllerUnfollowResponse, UsersControllerCompleteOnboardingData, UsersControllerCompleteOnboardingResponse, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountResponse, UsersControllerDeleteMyAvatarData, UsersControllerDeleteMyAvatarResponse, UsersControllerFetchMyTraktPublicHistoryData, UsersControllerFetchMyTraktPublicHistoryResponse, UsersControllerGetAvatarData, UsersControllerGetMyAccountDeletionData, UsersControllerGetMyAccountDeletionResponse, UsersControllerGetMyCurrentTraktImportData, UsersControllerGetMyCurrentTraktImportResponse, UsersControllerGetMySettingsData, UsersControllerGetMySettingsResponse, UsersControllerGetPublicProfileData, UsersControllerGetPublicProfileResponse, UsersControllerImportMyBlueskyFollowsData, UsersControllerImportMyBlueskyFollowsResponse, UsersControllerImportMyHistoryData, UsersControllerImportMyHistoryResponse, UsersControllerStartMyTraktImportData, UsersControllerStartMyTraktImportResponse, UsersControllerUpdateMyProfileData, UsersControllerUpdateMyProfileResponse, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsResponse, UsersControllerUploadMyAvatarData, UsersControllerUploadMyAvatarResponse } from '../types.gen'; 8 8 9 9 export type QueryKey<TOptions extends Options> = [ 10 10 Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & { ··· 1679 1679 }, 1680 1680 queryKey: searchControllerDiscoverAllInfiniteQueryKey(options) 1681 1681 }); 1682 + 1683 + export const peopleControllerGetPersonDetailsQueryKey = (options: Options<PeopleControllerGetPersonDetailsData>) => createQueryKey('peopleControllerGetPersonDetails', options); 1684 + 1685 + /** 1686 + * Get person details from TMDB 1687 + */ 1688 + export const peopleControllerGetPersonDetailsOptions = (options: Options<PeopleControllerGetPersonDetailsData>) => queryOptions<PeopleControllerGetPersonDetailsResponse, DefaultError, PeopleControllerGetPersonDetailsResponse, ReturnType<typeof peopleControllerGetPersonDetailsQueryKey>>({ 1689 + queryFn: async ({ queryKey, signal }) => { 1690 + const { data } = await peopleControllerGetPersonDetails({ 1691 + ...options, 1692 + ...queryKey[0], 1693 + signal, 1694 + throwOnError: true 1695 + }); 1696 + return data; 1697 + }, 1698 + queryKey: peopleControllerGetPersonDetailsQueryKey(options) 1699 + }); 1700 + 1701 + export const peopleControllerGetPersonFilmographyQueryKey = (options: Options<PeopleControllerGetPersonFilmographyData>) => createQueryKey('peopleControllerGetPersonFilmography', options); 1702 + 1703 + /** 1704 + * Get paginated person filmography from TMDB 1705 + */ 1706 + export const peopleControllerGetPersonFilmographyOptions = (options: Options<PeopleControllerGetPersonFilmographyData>) => queryOptions<PeopleControllerGetPersonFilmographyResponse, DefaultError, PeopleControllerGetPersonFilmographyResponse, ReturnType<typeof peopleControllerGetPersonFilmographyQueryKey>>({ 1707 + queryFn: async ({ queryKey, signal }) => { 1708 + const { data } = await peopleControllerGetPersonFilmography({ 1709 + ...options, 1710 + ...queryKey[0], 1711 + signal, 1712 + throwOnError: true 1713 + }); 1714 + return data; 1715 + }, 1716 + queryKey: peopleControllerGetPersonFilmographyQueryKey(options) 1717 + }); 1718 + 1719 + export const peopleControllerGetPersonFilmographyInfiniteQueryKey = (options: Options<PeopleControllerGetPersonFilmographyData>): QueryKey<Options<PeopleControllerGetPersonFilmographyData>> => createQueryKey('peopleControllerGetPersonFilmography', options, true); 1720 + 1721 + /** 1722 + * Get paginated person filmography from TMDB 1723 + */ 1724 + export const peopleControllerGetPersonFilmographyInfiniteOptions = (options: Options<PeopleControllerGetPersonFilmographyData>) => infiniteQueryOptions<PeopleControllerGetPersonFilmographyResponse, DefaultError, InfiniteData<PeopleControllerGetPersonFilmographyResponse>, QueryKey<Options<PeopleControllerGetPersonFilmographyData>>, number | Pick<QueryKey<Options<PeopleControllerGetPersonFilmographyData>>[0], 'body' | 'headers' | 'path' | 'query'>>( 1725 + // @ts-ignore 1726 + { 1727 + queryFn: async ({ pageParam, queryKey, signal }) => { 1728 + // @ts-ignore 1729 + const page: Pick<QueryKey<Options<PeopleControllerGetPersonFilmographyData>>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { 1730 + query: { 1731 + page: pageParam 1732 + } 1733 + }; 1734 + const params = createInfiniteParams(queryKey, page); 1735 + const { data } = await peopleControllerGetPersonFilmography({ 1736 + ...options, 1737 + ...params, 1738 + signal, 1739 + throwOnError: true 1740 + }); 1741 + return data; 1742 + }, 1743 + queryKey: peopleControllerGetPersonFilmographyInfiniteQueryKey(options) 1744 + });
+2 -2
packages/api/src/generated/index.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 - export { authControllerBlueskyProfileStatus, authControllerCallback, authControllerGetClientMetadata, authControllerLogin, authControllerLogout, authControllerMe, authControllerSignup, authControllerSuggestions, listsControllerAddItemToList, listsControllerCreateList, listsControllerDeleteList, listsControllerGetList, listsControllerGetListsForItem, listsControllerGetPublicUserList, listsControllerGetPublicUserLists, listsControllerGetUserLists, listsControllerRemoveItemFromList, listsControllerUpdateList, moviesControllerDeleteWatchHistoryEntry, moviesControllerDiscoverMovies, moviesControllerGetMovie, moviesControllerGetMovieDetails, moviesControllerGetMovieWatchHistory, moviesControllerGetUserMovies, moviesControllerGetUserMoviesPaginated, moviesControllerMarkWatched, moviesControllerSearchMovies, moviesControllerUnmarkWatched, type Options, searchControllerDiscoverAll, searchControllerSearchAll, shelfControllerGetUserActivitySummary, shelfControllerGetUserShelf, showsControllerDeleteEpisodeWatchHistoryEntry, showsControllerDiscoverShows, showsControllerGetEpisodeDetails, showsControllerGetLocalEpisodes, showsControllerGetLocalSeasons, showsControllerGetSeasonDetails, showsControllerGetShow, showsControllerGetShowDetails, showsControllerGetShowWatchHistory, showsControllerGetUserEpisodesPaginated, showsControllerGetUserReleaseCalendar, showsControllerGetUserShows, showsControllerGetUserUpNext, showsControllerMarkSeasonWatched, showsControllerMarkShowWatched, showsControllerMarkWatched, showsControllerSearchShows, showsControllerUnmarkWatched, socialControllerFollow, socialControllerGetFeed, socialControllerGetFollowers, socialControllerGetFollowing, socialControllerGetRelationship, socialControllerGetWatchers, socialControllerSearchPeople, socialControllerUnfollow, usersControllerCompleteOnboarding, usersControllerDeleteMyAccount, usersControllerDeleteMyAvatar, usersControllerFetchMyTraktPublicHistory, usersControllerGetAvatar, usersControllerGetMyAccountDeletion, usersControllerGetMyCurrentTraktImport, usersControllerGetMySettings, usersControllerGetPublicProfile, usersControllerImportMyBlueskyFollows, usersControllerImportMyHistory, usersControllerStartMyTraktImport, usersControllerUpdateMyProfile, usersControllerUpdateMySettings, usersControllerUploadMyAvatar } from './sdk.gen'; 4 - export type { AccountDeletionJobDto, AddToListDto, AuthControllerBlueskyProfileStatusData, AuthControllerBlueskyProfileStatusErrors, AuthControllerBlueskyProfileStatusResponse, AuthControllerBlueskyProfileStatusResponses, AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerGetClientMetadataResponses, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerLogoutResponses, AuthControllerMeData, AuthControllerMeErrors, AuthControllerMeResponse, AuthControllerMeResponses, AuthControllerSignupData, AuthControllerSuggestionsData, AuthControllerSuggestionsResponses, BlueskyProfileStatusDto, ClientOptions, CompleteOnboardingResponseDto, CreateListDto, DeleteUserAccountDto, EpisodeContextDto, EpisodeHistoryItemDto, EpisodeReferenceDto, FetchTraktPublicHistoryDto, FetchTraktPublicHistoryResponseDto, FollowedActivityFeedDto, FollowedActivityItemDto, FollowedWatcherActorDto, FollowedWatcherDto, FollowedWatchersDto, ImportBlueskyFollowsResponseDto, ImportErrorDto, ImportHistoryDto, ImportHistoryResponseDto, ImportSkipDto, ListDto, ListsControllerAddItemToListData, ListsControllerAddItemToListErrors, ListsControllerAddItemToListResponses, ListsControllerCreateListData, ListsControllerCreateListErrors, ListsControllerCreateListResponse, ListsControllerCreateListResponses, ListsControllerDeleteListData, ListsControllerDeleteListErrors, ListsControllerDeleteListResponses, ListsControllerGetListData, ListsControllerGetListErrors, ListsControllerGetListResponse, ListsControllerGetListResponses, ListsControllerGetListsForItemData, ListsControllerGetListsForItemErrors, ListsControllerGetListsForItemResponse, ListsControllerGetListsForItemResponses, ListsControllerGetPublicUserListData, ListsControllerGetPublicUserListErrors, ListsControllerGetPublicUserListResponse, ListsControllerGetPublicUserListResponses, ListsControllerGetPublicUserListsData, ListsControllerGetPublicUserListsResponse, ListsControllerGetPublicUserListsResponses, ListsControllerGetUserListsData, ListsControllerGetUserListsErrors, ListsControllerGetUserListsResponse, ListsControllerGetUserListsResponses, ListsControllerRemoveItemFromListData, ListsControllerRemoveItemFromListErrors, ListsControllerRemoveItemFromListResponses, ListsControllerUpdateListData, ListsControllerUpdateListErrors, ListsControllerUpdateListResponse, ListsControllerUpdateListResponses, ListsForItemDto, ListSummaryDto, ListWithItemsDto, LocalEpisodeDto, LocalSeasonDto, MarkedEpisodesResponseDto, MarkEpisodeWatchedDto, MarkSeasonWatchedDto, MarkShowWatchedDto, MediaInListDto, MovieColorsDto, MovieDto, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryErrors, MoviesControllerDeleteWatchHistoryEntryResponse, MoviesControllerDeleteWatchHistoryEntryResponses, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponse, MoviesControllerDiscoverMoviesResponses, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponse, MoviesControllerGetMovieDetailsResponses, MoviesControllerGetMovieResponse, MoviesControllerGetMovieResponses, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryErrors, MoviesControllerGetMovieWatchHistoryResponse, MoviesControllerGetMovieWatchHistoryResponses, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponse, MoviesControllerGetUserMoviesPaginatedResponses, MoviesControllerGetUserMoviesResponse, MoviesControllerGetUserMoviesResponses, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedErrors, MoviesControllerMarkWatchedResponse, MoviesControllerMarkWatchedResponses, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponse, MoviesControllerSearchMoviesResponses, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedErrors, MoviesControllerUnmarkWatchedResponse, MoviesControllerUnmarkWatchedResponses, NormalizedImportItemDto, PaginatedEpisodesResponseDto, PaginatedMoviesResponseDto, PaginatedSocialUsersDto, PaginatedUpNextResponseDto, PublicUserProfileDto, ReleaseCalendarItemDto, ReleaseCalendarResponseDto, SearchControllerDiscoverAllData, SearchControllerDiscoverAllResponse, SearchControllerDiscoverAllResponses, SearchControllerSearchAllData, SearchControllerSearchAllResponse, SearchControllerSearchAllResponses, SearchResultsDto, SearchShowsResultsDto, ShelfActivityBucketDto, ShelfActivitySummaryDto, ShelfControllerGetUserActivitySummaryData, ShelfControllerGetUserActivitySummaryResponse, ShelfControllerGetUserActivitySummaryResponses, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponse, ShelfControllerGetUserShelfResponses, ShelfResponseDto, ShowDto, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryErrors, ShowsControllerDeleteEpisodeWatchHistoryEntryResponse, ShowsControllerDeleteEpisodeWatchHistoryEntryResponses, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponse, ShowsControllerDiscoverShowsResponses, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponse, ShowsControllerGetEpisodeDetailsResponses, ShowsControllerGetLocalEpisodesData, ShowsControllerGetLocalEpisodesResponse, ShowsControllerGetLocalEpisodesResponses, ShowsControllerGetLocalSeasonsData, ShowsControllerGetLocalSeasonsResponse, ShowsControllerGetLocalSeasonsResponses, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponse, ShowsControllerGetSeasonDetailsResponses, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponse, ShowsControllerGetShowDetailsResponses, ShowsControllerGetShowResponse, ShowsControllerGetShowResponses, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryErrors, ShowsControllerGetShowWatchHistoryResponse, ShowsControllerGetShowWatchHistoryResponses, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponse, ShowsControllerGetUserEpisodesPaginatedResponses, ShowsControllerGetUserReleaseCalendarData, ShowsControllerGetUserReleaseCalendarResponse, ShowsControllerGetUserReleaseCalendarResponses, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponse, ShowsControllerGetUserShowsResponses, ShowsControllerGetUserUpNextData, ShowsControllerGetUserUpNextResponse, ShowsControllerGetUserUpNextResponses, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedErrors, ShowsControllerMarkSeasonWatchedResponse, ShowsControllerMarkSeasonWatchedResponses, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedErrors, ShowsControllerMarkShowWatchedResponse, ShowsControllerMarkShowWatchedResponses, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedErrors, ShowsControllerMarkWatchedResponse, ShowsControllerMarkWatchedResponses, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponse, ShowsControllerSearchShowsResponses, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponse, ShowsControllerUnmarkWatchedResponses, SocialActorDto, SocialControllerFollowData, SocialControllerFollowResponse, SocialControllerFollowResponses, SocialControllerGetFeedData, SocialControllerGetFeedResponse, SocialControllerGetFeedResponses, SocialControllerGetFollowersData, SocialControllerGetFollowersResponse, SocialControllerGetFollowersResponses, SocialControllerGetFollowingData, SocialControllerGetFollowingResponse, SocialControllerGetFollowingResponses, SocialControllerGetRelationshipData, SocialControllerGetRelationshipResponse, SocialControllerGetRelationshipResponses, SocialControllerGetWatchersData, SocialControllerGetWatchersResponse, SocialControllerGetWatchersResponses, SocialControllerSearchPeopleData, SocialControllerSearchPeopleResponse, SocialControllerSearchPeopleResponses, SocialControllerUnfollowData, SocialControllerUnfollowResponse, SocialControllerUnfollowResponses, SocialUserCardDto, StartTraktImportDto, StartTraktImportResponseDto, TmdbCastDto, TmdbCreditsDto, TmdbCrewDto, TmdbEpisodeDto, TmdbGenreDto, TmdbMovieDetailDto, TmdbMovieResultDto, TmdbNetworkDto, TmdbSeasonDetailDto, TmdbSeasonSummaryDto, TmdbShowDetailDto, TmdbShowResultDto, TmdbTrailerDto, TrackedEpisodeDto, TrackedMovieDto, TrackedShowSummaryDto, TraktHistoryPreviewItemDto, TraktImportJobDto, TraktPublicProfileDto, UnifiedDiscoverResponseDto, UnifiedSearchResponseDto, UnifiedSearchResultDto, UpdateListDto, UpdateUserProfileDto, UpdateUserSettingsDto, UpNextEpisodeDto, UpNextShowDto, UserDto, UserProfileDto, UserRelationshipDto, UsersControllerCompleteOnboardingData, UsersControllerCompleteOnboardingErrors, UsersControllerCompleteOnboardingResponse, UsersControllerCompleteOnboardingResponses, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountErrors, UsersControllerDeleteMyAccountResponse, UsersControllerDeleteMyAccountResponses, UsersControllerDeleteMyAvatarData, UsersControllerDeleteMyAvatarResponse, UsersControllerDeleteMyAvatarResponses, UsersControllerFetchMyTraktPublicHistoryData, UsersControllerFetchMyTraktPublicHistoryErrors, UsersControllerFetchMyTraktPublicHistoryResponse, UsersControllerFetchMyTraktPublicHistoryResponses, UsersControllerGetAvatarData, UsersControllerGetAvatarResponses, UsersControllerGetMyAccountDeletionData, UsersControllerGetMyAccountDeletionErrors, UsersControllerGetMyAccountDeletionResponse, UsersControllerGetMyAccountDeletionResponses, UsersControllerGetMyCurrentTraktImportData, UsersControllerGetMyCurrentTraktImportErrors, UsersControllerGetMyCurrentTraktImportResponse, UsersControllerGetMyCurrentTraktImportResponses, UsersControllerGetMySettingsData, UsersControllerGetMySettingsErrors, UsersControllerGetMySettingsResponse, UsersControllerGetMySettingsResponses, UsersControllerGetPublicProfileData, UsersControllerGetPublicProfileErrors, UsersControllerGetPublicProfileResponse, UsersControllerGetPublicProfileResponses, UsersControllerImportMyBlueskyFollowsData, UsersControllerImportMyBlueskyFollowsErrors, UsersControllerImportMyBlueskyFollowsResponse, UsersControllerImportMyBlueskyFollowsResponses, UsersControllerImportMyHistoryData, UsersControllerImportMyHistoryErrors, UsersControllerImportMyHistoryResponse, UsersControllerImportMyHistoryResponses, UsersControllerStartMyTraktImportData, UsersControllerStartMyTraktImportErrors, UsersControllerStartMyTraktImportResponse, UsersControllerStartMyTraktImportResponses, UsersControllerUpdateMyProfileData, UsersControllerUpdateMyProfileErrors, UsersControllerUpdateMyProfileResponse, UsersControllerUpdateMyProfileResponses, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsErrors, UsersControllerUpdateMySettingsResponse, UsersControllerUpdateMySettingsResponses, UsersControllerUploadMyAvatarData, UsersControllerUploadMyAvatarResponse, UsersControllerUploadMyAvatarResponses, UserSettingsDto, WatchHistoryItemDto } from './types.gen'; 3 + export { authControllerBlueskyProfileStatus, authControllerCallback, authControllerGetClientMetadata, authControllerLogin, authControllerLogout, authControllerMe, authControllerSignup, authControllerSuggestions, listsControllerAddItemToList, listsControllerCreateList, listsControllerDeleteList, listsControllerGetList, listsControllerGetListsForItem, listsControllerGetPublicUserList, listsControllerGetPublicUserLists, listsControllerGetUserLists, listsControllerRemoveItemFromList, listsControllerUpdateList, moviesControllerDeleteWatchHistoryEntry, moviesControllerDiscoverMovies, moviesControllerGetMovie, moviesControllerGetMovieDetails, moviesControllerGetMovieWatchHistory, moviesControllerGetUserMovies, moviesControllerGetUserMoviesPaginated, moviesControllerMarkWatched, moviesControllerSearchMovies, moviesControllerUnmarkWatched, type Options, peopleControllerGetPersonDetails, peopleControllerGetPersonFilmography, searchControllerDiscoverAll, searchControllerSearchAll, shelfControllerGetUserActivitySummary, shelfControllerGetUserShelf, showsControllerDeleteEpisodeWatchHistoryEntry, showsControllerDiscoverShows, showsControllerGetEpisodeDetails, showsControllerGetLocalEpisodes, showsControllerGetLocalSeasons, showsControllerGetSeasonDetails, showsControllerGetShow, showsControllerGetShowDetails, showsControllerGetShowWatchHistory, showsControllerGetUserEpisodesPaginated, showsControllerGetUserReleaseCalendar, showsControllerGetUserShows, showsControllerGetUserUpNext, showsControllerMarkSeasonWatched, showsControllerMarkShowWatched, showsControllerMarkWatched, showsControllerSearchShows, showsControllerUnmarkWatched, socialControllerFollow, socialControllerGetFeed, socialControllerGetFollowers, socialControllerGetFollowing, socialControllerGetRelationship, socialControllerGetWatchers, socialControllerSearchPeople, socialControllerUnfollow, usersControllerCompleteOnboarding, usersControllerDeleteMyAccount, usersControllerDeleteMyAvatar, usersControllerFetchMyTraktPublicHistory, usersControllerGetAvatar, usersControllerGetMyAccountDeletion, usersControllerGetMyCurrentTraktImport, usersControllerGetMySettings, usersControllerGetPublicProfile, usersControllerImportMyBlueskyFollows, usersControllerImportMyHistory, usersControllerStartMyTraktImport, usersControllerUpdateMyProfile, usersControllerUpdateMySettings, usersControllerUploadMyAvatar } from './sdk.gen'; 4 + export type { AccountDeletionJobDto, AddToListDto, AuthControllerBlueskyProfileStatusData, AuthControllerBlueskyProfileStatusErrors, AuthControllerBlueskyProfileStatusResponse, AuthControllerBlueskyProfileStatusResponses, AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerGetClientMetadataResponses, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerLogoutResponses, AuthControllerMeData, AuthControllerMeErrors, AuthControllerMeResponse, AuthControllerMeResponses, AuthControllerSignupData, AuthControllerSuggestionsData, AuthControllerSuggestionsResponses, BlueskyProfileStatusDto, ClientOptions, CompleteOnboardingResponseDto, CreateListDto, DeleteUserAccountDto, EpisodeContextDto, EpisodeHistoryItemDto, EpisodeReferenceDto, FetchTraktPublicHistoryDto, FetchTraktPublicHistoryResponseDto, FollowedActivityFeedDto, FollowedActivityItemDto, FollowedWatcherActorDto, FollowedWatcherDto, FollowedWatchersDto, ImportBlueskyFollowsResponseDto, ImportErrorDto, ImportHistoryDto, ImportHistoryResponseDto, ImportSkipDto, ListDto, ListsControllerAddItemToListData, ListsControllerAddItemToListErrors, ListsControllerAddItemToListResponses, ListsControllerCreateListData, ListsControllerCreateListErrors, ListsControllerCreateListResponse, ListsControllerCreateListResponses, ListsControllerDeleteListData, ListsControllerDeleteListErrors, ListsControllerDeleteListResponses, ListsControllerGetListData, ListsControllerGetListErrors, ListsControllerGetListResponse, ListsControllerGetListResponses, ListsControllerGetListsForItemData, ListsControllerGetListsForItemErrors, ListsControllerGetListsForItemResponse, ListsControllerGetListsForItemResponses, ListsControllerGetPublicUserListData, ListsControllerGetPublicUserListErrors, ListsControllerGetPublicUserListResponse, ListsControllerGetPublicUserListResponses, ListsControllerGetPublicUserListsData, ListsControllerGetPublicUserListsResponse, ListsControllerGetPublicUserListsResponses, ListsControllerGetUserListsData, ListsControllerGetUserListsErrors, ListsControllerGetUserListsResponse, ListsControllerGetUserListsResponses, ListsControllerRemoveItemFromListData, ListsControllerRemoveItemFromListErrors, ListsControllerRemoveItemFromListResponses, ListsControllerUpdateListData, ListsControllerUpdateListErrors, ListsControllerUpdateListResponse, ListsControllerUpdateListResponses, ListsForItemDto, ListSummaryDto, ListWithItemsDto, LocalEpisodeDto, LocalSeasonDto, MarkedEpisodesResponseDto, MarkEpisodeWatchedDto, MarkSeasonWatchedDto, MarkShowWatchedDto, MediaInListDto, MovieColorsDto, MovieDto, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryErrors, MoviesControllerDeleteWatchHistoryEntryResponse, MoviesControllerDeleteWatchHistoryEntryResponses, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponse, MoviesControllerDiscoverMoviesResponses, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponse, MoviesControllerGetMovieDetailsResponses, MoviesControllerGetMovieResponse, MoviesControllerGetMovieResponses, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryErrors, MoviesControllerGetMovieWatchHistoryResponse, MoviesControllerGetMovieWatchHistoryResponses, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponse, MoviesControllerGetUserMoviesPaginatedResponses, MoviesControllerGetUserMoviesResponse, MoviesControllerGetUserMoviesResponses, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedErrors, MoviesControllerMarkWatchedResponse, MoviesControllerMarkWatchedResponses, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponse, MoviesControllerSearchMoviesResponses, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedErrors, MoviesControllerUnmarkWatchedResponse, MoviesControllerUnmarkWatchedResponses, NormalizedImportItemDto, PaginatedEpisodesResponseDto, PaginatedMoviesResponseDto, PaginatedSocialUsersDto, PaginatedUpNextResponseDto, PeopleControllerGetPersonDetailsData, PeopleControllerGetPersonDetailsErrors, PeopleControllerGetPersonDetailsResponse, PeopleControllerGetPersonDetailsResponses, PeopleControllerGetPersonFilmographyData, PeopleControllerGetPersonFilmographyErrors, PeopleControllerGetPersonFilmographyResponse, PeopleControllerGetPersonFilmographyResponses, PersonFilmographyItemDto, PersonFilmographyResponseDto, PublicUserProfileDto, ReleaseCalendarItemDto, ReleaseCalendarResponseDto, SearchControllerDiscoverAllData, SearchControllerDiscoverAllResponse, SearchControllerDiscoverAllResponses, SearchControllerSearchAllData, SearchControllerSearchAllResponse, SearchControllerSearchAllResponses, SearchResultsDto, SearchShowsResultsDto, ShelfActivityBucketDto, ShelfActivitySummaryDto, ShelfControllerGetUserActivitySummaryData, ShelfControllerGetUserActivitySummaryResponse, ShelfControllerGetUserActivitySummaryResponses, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponse, ShelfControllerGetUserShelfResponses, ShelfResponseDto, ShowDto, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryErrors, ShowsControllerDeleteEpisodeWatchHistoryEntryResponse, ShowsControllerDeleteEpisodeWatchHistoryEntryResponses, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponse, ShowsControllerDiscoverShowsResponses, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponse, ShowsControllerGetEpisodeDetailsResponses, ShowsControllerGetLocalEpisodesData, ShowsControllerGetLocalEpisodesResponse, ShowsControllerGetLocalEpisodesResponses, ShowsControllerGetLocalSeasonsData, ShowsControllerGetLocalSeasonsResponse, ShowsControllerGetLocalSeasonsResponses, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponse, ShowsControllerGetSeasonDetailsResponses, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponse, ShowsControllerGetShowDetailsResponses, ShowsControllerGetShowResponse, ShowsControllerGetShowResponses, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryErrors, ShowsControllerGetShowWatchHistoryResponse, ShowsControllerGetShowWatchHistoryResponses, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponse, ShowsControllerGetUserEpisodesPaginatedResponses, ShowsControllerGetUserReleaseCalendarData, ShowsControllerGetUserReleaseCalendarResponse, ShowsControllerGetUserReleaseCalendarResponses, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponse, ShowsControllerGetUserShowsResponses, ShowsControllerGetUserUpNextData, ShowsControllerGetUserUpNextResponse, ShowsControllerGetUserUpNextResponses, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedErrors, ShowsControllerMarkSeasonWatchedResponse, ShowsControllerMarkSeasonWatchedResponses, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedErrors, ShowsControllerMarkShowWatchedResponse, ShowsControllerMarkShowWatchedResponses, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedErrors, ShowsControllerMarkWatchedResponse, ShowsControllerMarkWatchedResponses, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponse, ShowsControllerSearchShowsResponses, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponse, ShowsControllerUnmarkWatchedResponses, SocialActorDto, SocialControllerFollowData, SocialControllerFollowResponse, SocialControllerFollowResponses, SocialControllerGetFeedData, SocialControllerGetFeedResponse, SocialControllerGetFeedResponses, SocialControllerGetFollowersData, SocialControllerGetFollowersResponse, SocialControllerGetFollowersResponses, SocialControllerGetFollowingData, SocialControllerGetFollowingResponse, SocialControllerGetFollowingResponses, SocialControllerGetRelationshipData, SocialControllerGetRelationshipResponse, SocialControllerGetRelationshipResponses, SocialControllerGetWatchersData, SocialControllerGetWatchersResponse, SocialControllerGetWatchersResponses, SocialControllerSearchPeopleData, SocialControllerSearchPeopleResponse, SocialControllerSearchPeopleResponses, SocialControllerUnfollowData, SocialControllerUnfollowResponse, SocialControllerUnfollowResponses, SocialUserCardDto, StartTraktImportDto, StartTraktImportResponseDto, TmdbCastDto, TmdbCreditsDto, TmdbCrewDto, TmdbEpisodeDto, TmdbGenreDto, TmdbMovieDetailDto, TmdbMovieResultDto, TmdbNetworkDto, TmdbPersonDetailDto, TmdbSeasonDetailDto, TmdbSeasonSummaryDto, TmdbShowDetailDto, TmdbShowResultDto, TmdbTrailerDto, TrackedEpisodeDto, TrackedMovieDto, TrackedShowSummaryDto, TraktHistoryPreviewItemDto, TraktImportJobDto, TraktPublicProfileDto, UnifiedDiscoverResponseDto, UnifiedSearchResponseDto, UnifiedSearchResultDto, UpdateListDto, UpdateUserProfileDto, UpdateUserSettingsDto, UpNextEpisodeDto, UpNextShowDto, UserDto, UserProfileDto, UserRelationshipDto, UsersControllerCompleteOnboardingData, UsersControllerCompleteOnboardingErrors, UsersControllerCompleteOnboardingResponse, UsersControllerCompleteOnboardingResponses, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountErrors, UsersControllerDeleteMyAccountResponse, UsersControllerDeleteMyAccountResponses, UsersControllerDeleteMyAvatarData, UsersControllerDeleteMyAvatarResponse, UsersControllerDeleteMyAvatarResponses, UsersControllerFetchMyTraktPublicHistoryData, UsersControllerFetchMyTraktPublicHistoryErrors, UsersControllerFetchMyTraktPublicHistoryResponse, UsersControllerFetchMyTraktPublicHistoryResponses, UsersControllerGetAvatarData, UsersControllerGetAvatarResponses, UsersControllerGetMyAccountDeletionData, UsersControllerGetMyAccountDeletionErrors, UsersControllerGetMyAccountDeletionResponse, UsersControllerGetMyAccountDeletionResponses, UsersControllerGetMyCurrentTraktImportData, UsersControllerGetMyCurrentTraktImportErrors, UsersControllerGetMyCurrentTraktImportResponse, UsersControllerGetMyCurrentTraktImportResponses, UsersControllerGetMySettingsData, UsersControllerGetMySettingsErrors, UsersControllerGetMySettingsResponse, UsersControllerGetMySettingsResponses, UsersControllerGetPublicProfileData, UsersControllerGetPublicProfileErrors, UsersControllerGetPublicProfileResponse, UsersControllerGetPublicProfileResponses, UsersControllerImportMyBlueskyFollowsData, UsersControllerImportMyBlueskyFollowsErrors, UsersControllerImportMyBlueskyFollowsResponse, UsersControllerImportMyBlueskyFollowsResponses, UsersControllerImportMyHistoryData, UsersControllerImportMyHistoryErrors, UsersControllerImportMyHistoryResponse, UsersControllerImportMyHistoryResponses, UsersControllerStartMyTraktImportData, UsersControllerStartMyTraktImportErrors, UsersControllerStartMyTraktImportResponse, UsersControllerStartMyTraktImportResponses, UsersControllerUpdateMyProfileData, UsersControllerUpdateMyProfileErrors, UsersControllerUpdateMyProfileResponse, UsersControllerUpdateMyProfileResponses, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsErrors, UsersControllerUpdateMySettingsResponse, UsersControllerUpdateMySettingsResponses, UsersControllerUploadMyAvatarData, UsersControllerUploadMyAvatarResponse, UsersControllerUploadMyAvatarResponses, UserSettingsDto, WatchHistoryItemDto } from './types.gen';
+11 -1
packages/api/src/generated/sdk.gen.ts
··· 2 2 3 3 import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client'; 4 4 import { client } from './client.gen'; 5 - import type { AuthControllerBlueskyProfileStatusData, AuthControllerBlueskyProfileStatusErrors, AuthControllerBlueskyProfileStatusResponses, AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerGetClientMetadataResponses, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerLogoutResponses, AuthControllerMeData, AuthControllerMeErrors, AuthControllerMeResponses, AuthControllerSignupData, AuthControllerSuggestionsData, AuthControllerSuggestionsResponses, ListsControllerAddItemToListData, ListsControllerAddItemToListErrors, ListsControllerAddItemToListResponses, ListsControllerCreateListData, ListsControllerCreateListErrors, ListsControllerCreateListResponses, ListsControllerDeleteListData, ListsControllerDeleteListErrors, ListsControllerDeleteListResponses, ListsControllerGetListData, ListsControllerGetListErrors, ListsControllerGetListResponses, ListsControllerGetListsForItemData, ListsControllerGetListsForItemErrors, ListsControllerGetListsForItemResponses, ListsControllerGetPublicUserListData, ListsControllerGetPublicUserListErrors, ListsControllerGetPublicUserListResponses, ListsControllerGetPublicUserListsData, ListsControllerGetPublicUserListsResponses, ListsControllerGetUserListsData, ListsControllerGetUserListsErrors, ListsControllerGetUserListsResponses, ListsControllerRemoveItemFromListData, ListsControllerRemoveItemFromListErrors, ListsControllerRemoveItemFromListResponses, ListsControllerUpdateListData, ListsControllerUpdateListErrors, ListsControllerUpdateListResponses, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryErrors, MoviesControllerDeleteWatchHistoryEntryResponses, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponses, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponses, MoviesControllerGetMovieResponses, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryErrors, MoviesControllerGetMovieWatchHistoryResponses, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponses, MoviesControllerGetUserMoviesResponses, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedErrors, MoviesControllerMarkWatchedResponses, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponses, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedErrors, MoviesControllerUnmarkWatchedResponses, SearchControllerDiscoverAllData, SearchControllerDiscoverAllResponses, SearchControllerSearchAllData, SearchControllerSearchAllResponses, ShelfControllerGetUserActivitySummaryData, ShelfControllerGetUserActivitySummaryResponses, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponses, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryErrors, ShowsControllerDeleteEpisodeWatchHistoryEntryResponses, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponses, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponses, ShowsControllerGetLocalEpisodesData, ShowsControllerGetLocalEpisodesResponses, ShowsControllerGetLocalSeasonsData, ShowsControllerGetLocalSeasonsResponses, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponses, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponses, ShowsControllerGetShowResponses, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryErrors, ShowsControllerGetShowWatchHistoryResponses, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponses, ShowsControllerGetUserReleaseCalendarData, ShowsControllerGetUserReleaseCalendarResponses, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponses, ShowsControllerGetUserUpNextData, ShowsControllerGetUserUpNextResponses, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedErrors, ShowsControllerMarkSeasonWatchedResponses, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedErrors, ShowsControllerMarkShowWatchedResponses, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedErrors, ShowsControllerMarkWatchedResponses, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponses, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponses, SocialControllerFollowData, SocialControllerFollowResponses, SocialControllerGetFeedData, SocialControllerGetFeedResponses, SocialControllerGetFollowersData, SocialControllerGetFollowersResponses, SocialControllerGetFollowingData, SocialControllerGetFollowingResponses, SocialControllerGetRelationshipData, SocialControllerGetRelationshipResponses, SocialControllerGetWatchersData, SocialControllerGetWatchersResponses, SocialControllerSearchPeopleData, SocialControllerSearchPeopleResponses, SocialControllerUnfollowData, SocialControllerUnfollowResponses, UsersControllerCompleteOnboardingData, UsersControllerCompleteOnboardingErrors, UsersControllerCompleteOnboardingResponses, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountErrors, UsersControllerDeleteMyAccountResponses, UsersControllerDeleteMyAvatarData, UsersControllerDeleteMyAvatarResponses, UsersControllerFetchMyTraktPublicHistoryData, UsersControllerFetchMyTraktPublicHistoryErrors, UsersControllerFetchMyTraktPublicHistoryResponses, UsersControllerGetAvatarData, UsersControllerGetAvatarResponses, UsersControllerGetMyAccountDeletionData, UsersControllerGetMyAccountDeletionErrors, UsersControllerGetMyAccountDeletionResponses, UsersControllerGetMyCurrentTraktImportData, UsersControllerGetMyCurrentTraktImportErrors, UsersControllerGetMyCurrentTraktImportResponses, UsersControllerGetMySettingsData, UsersControllerGetMySettingsErrors, UsersControllerGetMySettingsResponses, UsersControllerGetPublicProfileData, UsersControllerGetPublicProfileErrors, UsersControllerGetPublicProfileResponses, UsersControllerImportMyBlueskyFollowsData, UsersControllerImportMyBlueskyFollowsErrors, UsersControllerImportMyBlueskyFollowsResponses, UsersControllerImportMyHistoryData, UsersControllerImportMyHistoryErrors, UsersControllerImportMyHistoryResponses, UsersControllerStartMyTraktImportData, UsersControllerStartMyTraktImportErrors, UsersControllerStartMyTraktImportResponses, UsersControllerUpdateMyProfileData, UsersControllerUpdateMyProfileErrors, UsersControllerUpdateMyProfileResponses, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsErrors, UsersControllerUpdateMySettingsResponses, UsersControllerUploadMyAvatarData, UsersControllerUploadMyAvatarResponses } from './types.gen'; 5 + import type { AuthControllerBlueskyProfileStatusData, AuthControllerBlueskyProfileStatusErrors, AuthControllerBlueskyProfileStatusResponses, AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerGetClientMetadataResponses, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerLogoutResponses, AuthControllerMeData, AuthControllerMeErrors, AuthControllerMeResponses, AuthControllerSignupData, AuthControllerSuggestionsData, AuthControllerSuggestionsResponses, ListsControllerAddItemToListData, ListsControllerAddItemToListErrors, ListsControllerAddItemToListResponses, ListsControllerCreateListData, ListsControllerCreateListErrors, ListsControllerCreateListResponses, ListsControllerDeleteListData, ListsControllerDeleteListErrors, ListsControllerDeleteListResponses, ListsControllerGetListData, ListsControllerGetListErrors, ListsControllerGetListResponses, ListsControllerGetListsForItemData, ListsControllerGetListsForItemErrors, ListsControllerGetListsForItemResponses, ListsControllerGetPublicUserListData, ListsControllerGetPublicUserListErrors, ListsControllerGetPublicUserListResponses, ListsControllerGetPublicUserListsData, ListsControllerGetPublicUserListsResponses, ListsControllerGetUserListsData, ListsControllerGetUserListsErrors, ListsControllerGetUserListsResponses, ListsControllerRemoveItemFromListData, ListsControllerRemoveItemFromListErrors, ListsControllerRemoveItemFromListResponses, ListsControllerUpdateListData, ListsControllerUpdateListErrors, ListsControllerUpdateListResponses, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryErrors, MoviesControllerDeleteWatchHistoryEntryResponses, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponses, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponses, MoviesControllerGetMovieResponses, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryErrors, MoviesControllerGetMovieWatchHistoryResponses, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponses, MoviesControllerGetUserMoviesResponses, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedErrors, MoviesControllerMarkWatchedResponses, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponses, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedErrors, MoviesControllerUnmarkWatchedResponses, PeopleControllerGetPersonDetailsData, PeopleControllerGetPersonDetailsErrors, PeopleControllerGetPersonDetailsResponses, PeopleControllerGetPersonFilmographyData, PeopleControllerGetPersonFilmographyErrors, PeopleControllerGetPersonFilmographyResponses, SearchControllerDiscoverAllData, SearchControllerDiscoverAllResponses, SearchControllerSearchAllData, SearchControllerSearchAllResponses, ShelfControllerGetUserActivitySummaryData, ShelfControllerGetUserActivitySummaryResponses, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponses, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryErrors, ShowsControllerDeleteEpisodeWatchHistoryEntryResponses, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponses, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponses, ShowsControllerGetLocalEpisodesData, ShowsControllerGetLocalEpisodesResponses, ShowsControllerGetLocalSeasonsData, ShowsControllerGetLocalSeasonsResponses, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponses, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponses, ShowsControllerGetShowResponses, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryErrors, ShowsControllerGetShowWatchHistoryResponses, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponses, ShowsControllerGetUserReleaseCalendarData, ShowsControllerGetUserReleaseCalendarResponses, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponses, ShowsControllerGetUserUpNextData, ShowsControllerGetUserUpNextResponses, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedErrors, ShowsControllerMarkSeasonWatchedResponses, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedErrors, ShowsControllerMarkShowWatchedResponses, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedErrors, ShowsControllerMarkWatchedResponses, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponses, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponses, SocialControllerFollowData, SocialControllerFollowResponses, SocialControllerGetFeedData, SocialControllerGetFeedResponses, SocialControllerGetFollowersData, SocialControllerGetFollowersResponses, SocialControllerGetFollowingData, SocialControllerGetFollowingResponses, SocialControllerGetRelationshipData, SocialControllerGetRelationshipResponses, SocialControllerGetWatchersData, SocialControllerGetWatchersResponses, SocialControllerSearchPeopleData, SocialControllerSearchPeopleResponses, SocialControllerUnfollowData, SocialControllerUnfollowResponses, UsersControllerCompleteOnboardingData, UsersControllerCompleteOnboardingErrors, UsersControllerCompleteOnboardingResponses, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountErrors, UsersControllerDeleteMyAccountResponses, UsersControllerDeleteMyAvatarData, UsersControllerDeleteMyAvatarResponses, UsersControllerFetchMyTraktPublicHistoryData, UsersControllerFetchMyTraktPublicHistoryErrors, UsersControllerFetchMyTraktPublicHistoryResponses, UsersControllerGetAvatarData, UsersControllerGetAvatarResponses, UsersControllerGetMyAccountDeletionData, UsersControllerGetMyAccountDeletionErrors, UsersControllerGetMyAccountDeletionResponses, UsersControllerGetMyCurrentTraktImportData, UsersControllerGetMyCurrentTraktImportErrors, UsersControllerGetMyCurrentTraktImportResponses, UsersControllerGetMySettingsData, UsersControllerGetMySettingsErrors, UsersControllerGetMySettingsResponses, UsersControllerGetPublicProfileData, UsersControllerGetPublicProfileErrors, UsersControllerGetPublicProfileResponses, UsersControllerImportMyBlueskyFollowsData, UsersControllerImportMyBlueskyFollowsErrors, UsersControllerImportMyBlueskyFollowsResponses, UsersControllerImportMyHistoryData, UsersControllerImportMyHistoryErrors, UsersControllerImportMyHistoryResponses, UsersControllerStartMyTraktImportData, UsersControllerStartMyTraktImportErrors, UsersControllerStartMyTraktImportResponses, UsersControllerUpdateMyProfileData, UsersControllerUpdateMyProfileErrors, UsersControllerUpdateMyProfileResponses, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsErrors, UsersControllerUpdateMySettingsResponses, UsersControllerUploadMyAvatarData, UsersControllerUploadMyAvatarResponses } from './types.gen'; 6 6 7 7 export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & { 8 8 /** ··· 481 481 * Discover popular movies and shows from TMDB 482 482 */ 483 483 export const searchControllerDiscoverAll = <ThrowOnError extends boolean = false>(options?: Options<SearchControllerDiscoverAllData, ThrowOnError>) => (options?.client ?? client).get<SearchControllerDiscoverAllResponses, unknown, ThrowOnError>({ url: '/search/discover', ...options }); 484 + 485 + /** 486 + * Get person details from TMDB 487 + */ 488 + export const peopleControllerGetPersonDetails = <ThrowOnError extends boolean = false>(options: Options<PeopleControllerGetPersonDetailsData, ThrowOnError>) => (options.client ?? client).get<PeopleControllerGetPersonDetailsResponses, PeopleControllerGetPersonDetailsErrors, ThrowOnError>({ url: '/people/tmdb/{personId}', ...options }); 489 + 490 + /** 491 + * Get paginated person filmography from TMDB 492 + */ 493 + export const peopleControllerGetPersonFilmography = <ThrowOnError extends boolean = false>(options: Options<PeopleControllerGetPersonFilmographyData, ThrowOnError>) => (options.client ?? client).get<PeopleControllerGetPersonFilmographyResponses, PeopleControllerGetPersonFilmographyErrors, ThrowOnError>({ url: '/people/tmdb/{personId}/filmography', ...options });
+100
packages/api/src/generated/types.gen.ts
··· 1069 1069 page: number; 1070 1070 }; 1071 1071 1072 + export type PersonFilmographyItemDto = { 1073 + id: number; 1074 + media_type: string; 1075 + title: string; 1076 + poster_path?: string; 1077 + release_date?: string; 1078 + first_air_date?: string; 1079 + character?: string; 1080 + job?: string; 1081 + department?: string; 1082 + order?: number; 1083 + vote_average?: number; 1084 + }; 1085 + 1086 + export type TmdbPersonDetailDto = { 1087 + id: number; 1088 + name: string; 1089 + profile_path?: string; 1090 + biography?: string; 1091 + birthday?: string; 1092 + deathday?: string; 1093 + place_of_birth?: string; 1094 + known_for_department?: string; 1095 + popularity?: number; 1096 + filmography: Array<PersonFilmographyItemDto>; 1097 + }; 1098 + 1099 + export type PersonFilmographyResponseDto = { 1100 + items: Array<PersonFilmographyItemDto>; 1101 + total: number; 1102 + page: number; 1103 + pageSize: number; 1104 + totalPages: number; 1105 + }; 1106 + 1072 1107 export type MoviesControllerSearchMoviesData = { 1073 1108 body?: never; 1074 1109 path?: never; ··· 2659 2694 }; 2660 2695 2661 2696 export type SearchControllerDiscoverAllResponse = SearchControllerDiscoverAllResponses[keyof SearchControllerDiscoverAllResponses]; 2697 + 2698 + export type PeopleControllerGetPersonDetailsData = { 2699 + body?: never; 2700 + path: { 2701 + /** 2702 + * TMDB person ID 2703 + */ 2704 + personId: string; 2705 + }; 2706 + query?: never; 2707 + url: '/people/tmdb/{personId}'; 2708 + }; 2709 + 2710 + export type PeopleControllerGetPersonDetailsErrors = { 2711 + /** 2712 + * Person not found 2713 + */ 2714 + 404: unknown; 2715 + }; 2716 + 2717 + export type PeopleControllerGetPersonDetailsResponses = { 2718 + /** 2719 + * Person details retrieved successfully 2720 + */ 2721 + 200: TmdbPersonDetailDto; 2722 + }; 2723 + 2724 + export type PeopleControllerGetPersonDetailsResponse = PeopleControllerGetPersonDetailsResponses[keyof PeopleControllerGetPersonDetailsResponses]; 2725 + 2726 + export type PeopleControllerGetPersonFilmographyData = { 2727 + body?: never; 2728 + path: { 2729 + /** 2730 + * TMDB person ID 2731 + */ 2732 + personId: string; 2733 + }; 2734 + query?: { 2735 + /** 2736 + * Page number (1-based) 2737 + */ 2738 + page?: number; 2739 + /** 2740 + * Items per page 2741 + */ 2742 + pageSize?: number; 2743 + }; 2744 + url: '/people/tmdb/{personId}/filmography'; 2745 + }; 2746 + 2747 + export type PeopleControllerGetPersonFilmographyErrors = { 2748 + /** 2749 + * Person not found 2750 + */ 2751 + 404: unknown; 2752 + }; 2753 + 2754 + export type PeopleControllerGetPersonFilmographyResponses = { 2755 + /** 2756 + * Filmography retrieved successfully 2757 + */ 2758 + 200: PersonFilmographyResponseDto; 2759 + }; 2760 + 2761 + export type PeopleControllerGetPersonFilmographyResponse = PeopleControllerGetPersonFilmographyResponses[keyof PeopleControllerGetPersonFilmographyResponses];
-14
packages/api/src/index.ts
··· 46 46 isKnownTraktImportStatus, 47 47 isTerminalTraktImportStatus, 48 48 } from "./trakt-import-status"; 49 - 50 - // TODO: Remove these manual exports after running `pnpm generate:api` 51 - // People API - temporary manual implementation until backend codegen is run 52 - export type { 53 - PersonFilmographyItemDto, 54 - TmdbPersonDetailDto, 55 - PeopleControllerGetPersonDetailsData, 56 - PeopleControllerGetPersonDetailsResponse, 57 - } from "./people-temp"; 58 - export { peopleControllerGetPersonDetails } from "./people-temp"; 59 - export { 60 - peopleControllerGetPersonDetailsOptions, 61 - peopleControllerGetPersonDetailsQueryKey, 62 - } from "./people-temp";
-102
packages/api/src/people-temp.ts
··· 1 - // TODO: Regenerate API client when backend is ready - this is a temporary manual implementation 2 - // Run: pnpm generate:api 3 - 4 - import { client } from "./generated/client.gen"; 5 - import { type DefaultError, queryOptions } from "@tanstack/react-query"; 6 - 7 - // Types 8 - export type PersonFilmographyItemDto = { 9 - id: number; 10 - media_type: "movie" | "tv"; 11 - title: string; 12 - poster_path?: string; 13 - release_date?: string; 14 - first_air_date?: string; 15 - character?: string; 16 - job?: string; 17 - department?: string; 18 - order?: number; 19 - vote_average?: number; 20 - }; 21 - 22 - export type TmdbPersonDetailDto = { 23 - id: number; 24 - name: string; 25 - profile_path?: string; 26 - biography?: string; 27 - birthday?: string; 28 - deathday?: string; 29 - place_of_birth?: string; 30 - known_for_department?: string; 31 - popularity?: number; 32 - filmography: PersonFilmographyItemDto[]; 33 - }; 34 - 35 - export type PeopleControllerGetPersonDetailsData = { 36 - body?: never; 37 - path: { 38 - personId: string; 39 - }; 40 - query?: never; 41 - url: "/people/tmdb/{personId}"; 42 - }; 43 - 44 - export type PeopleControllerGetPersonDetailsResponse = TmdbPersonDetailDto; 45 - 46 - // API Client 47 - export const peopleControllerGetPersonDetails = async ( 48 - options: PeopleControllerGetPersonDetailsData, 49 - ): Promise<{ data: PeopleControllerGetPersonDetailsResponse }> => { 50 - const response = await (options.client ?? client).get<{ 51 - 200: PeopleControllerGetPersonDetailsResponse; 52 - }>({ 53 - url: "/people/tmdb/{personId}", 54 - ...options, 55 - }); 56 - 57 - if (!response.data) { 58 - throw new Error("Failed to fetch person details"); 59 - } 60 - 61 - return { data: response.data }; 62 - }; 63 - 64 - // Query Key 65 - type QueryKey<T> = [ 66 - T & { 67 - _id: string; 68 - tags?: ReadonlyArray<string>; 69 - }, 70 - ]; 71 - 72 - const createQueryKey = <T>(id: string, options?: T): [QueryKey<T>[0]] => { 73 - const params = { _id: id } as QueryKey<T>[0]; 74 - if (options && "path" in (options as Record<string, unknown>)) { 75 - (params as Record<string, unknown>).path = (options as Record<string, unknown>).path; 76 - } 77 - return [params]; 78 - }; 79 - 80 - export const peopleControllerGetPersonDetailsQueryKey = ( 81 - options: PeopleControllerGetPersonDetailsData, 82 - ) => createQueryKey("peopleControllerGetPersonDetails", options); 83 - 84 - // React Query Options 85 - export const peopleControllerGetPersonDetailsOptions = ( 86 - options: PeopleControllerGetPersonDetailsData, 87 - ) => 88 - queryOptions< 89 - PeopleControllerGetPersonDetailsResponse, 90 - DefaultError, 91 - PeopleControllerGetPersonDetailsResponse, 92 - ReturnType<typeof peopleControllerGetPersonDetailsQueryKey> 93 - >({ 94 - queryFn: async ({ queryKey }) => { 95 - const { data } = await peopleControllerGetPersonDetails({ 96 - ...options, 97 - ...queryKey[0], 98 - }); 99 - return data; 100 - }, 101 - queryKey: peopleControllerGetPersonDetailsQueryKey(options), 102 - });