refactor(spa): collapse api/hooks telescope, merge review sheets, extract tab+list modules

- merge lib/api/* + hooks/use-* → features/* domain modules
- log-sheet + edit-review-sheet → review-sheet with mode discriminant
- index.tsx → feed-tab, watchlist-tab, queue-tab
- social actor-list pattern → ActorList component
- movie detail inline sections → community-reviews, viewing-history
This commit is contained in:
2026-07-10 17:38:58 +02:00
parent 89045414cf
commit 0ee2e04fe5
61 changed files with 1753 additions and 1810 deletions

145
spa/src/features/search.ts Normal file
View File

@@ -0,0 +1,145 @@
import { z } from "zod"
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"
import { paginatedSchema } from "@/lib/api/common"
import { get } from "@/lib/api/client"
const PAGE_SIZE = 20
export const searchQueryParamsSchema = z.object({
q: z.string().optional(),
genre: z.string().optional(),
year: z.number().optional(),
person_id: z.string().uuid().optional(),
department: z.string().optional(),
language: z.string().optional(),
limit: z.number().optional(),
offset: z.number().optional(),
})
export type SearchQueryParams = z.infer<typeof searchQueryParamsSchema>
export const movieSearchHitDtoSchema = z.object({
movie_id: z.string().uuid(),
title: z.string(),
release_year: z.number().optional(),
director: z.string().optional(),
poster_path: z.string().optional(),
genres: z.array(z.string()),
})
export type MovieSearchHitDto = z.infer<typeof movieSearchHitDtoSchema>
export const personSearchHitDtoSchema = z.object({
person_id: z.string().uuid(),
name: z.string(),
known_for_department: z.string().optional(),
profile_path: z.string().optional(),
known_for_titles: z.array(z.string()),
})
export type PersonSearchHitDto = z.infer<typeof personSearchHitDtoSchema>
export const searchResponseSchema = z.object({
movies: paginatedSchema(movieSearchHitDtoSchema),
people: paginatedSchema(personSearchHitDtoSchema),
})
export type SearchResponse = z.infer<typeof searchResponseSchema>
export const personDtoSchema = z.object({
id: z.string().uuid(),
external_id: z.string(),
name: z.string(),
known_for_department: z.string().optional(),
profile_path: z.string().optional(),
biography: z.string().optional(),
birthday: z.string().optional(),
deathday: z.string().optional(),
place_of_birth: z.string().optional(),
also_known_as: z.array(z.string()).default([]),
homepage: z.string().optional(),
imdb_url: z.string().optional(),
enriched: z.boolean().default(false),
})
export type PersonDto = z.infer<typeof personDtoSchema>
export const castCreditDtoSchema = z.object({
movie_id: z.string().uuid(),
title: z.string(),
release_year: z.number().optional(),
character: z.string(),
poster_path: z.string().optional(),
})
export type CastCreditDto = z.infer<typeof castCreditDtoSchema>
export const crewCreditDtoSchema = z.object({
movie_id: z.string().uuid(),
title: z.string(),
release_year: z.number().optional(),
job: z.string(),
department: z.string(),
poster_path: z.string().optional(),
})
export type CrewCreditDto = z.infer<typeof crewCreditDtoSchema>
export const personCreditsDtoSchema = z.object({
person: personDtoSchema,
cast: z.array(castCreditDtoSchema),
crew: z.array(crewCreditDtoSchema),
})
export type PersonCreditsDto = z.infer<typeof personCreditsDtoSchema>
function search(params: SearchQueryParams) {
return get<SearchResponse>("/search", params)
}
function getPerson(id: string) {
return get<PersonDto>(`/people/${id}`)
}
function getPersonCredits(id: string) {
return get<PersonCreditsDto>(`/people/${id}/credits`)
}
export const searchKeys = {
all: ["search"] as const,
query: (params: SearchQueryParams) => [...searchKeys.all, params] as const,
person: (id: string) => ["people", id] as const,
personCredits: (id: string) => ["people", id, "credits"] as const,
}
export function useSearch(params: SearchQueryParams) {
return useQuery({
queryKey: searchKeys.query(params),
queryFn: () => search(params),
enabled: !!params.q || !!params.genre || !!params.person_id,
})
}
export function useInfiniteSearch(
params: Omit<SearchQueryParams, "limit" | "offset">,
) {
return useInfiniteQuery({
queryKey: searchKeys.query(params),
queryFn: ({ pageParam = 0 }) =>
search({ ...params, limit: PAGE_SIZE, offset: pageParam }),
initialPageParam: 0,
getNextPageParam: (last) => {
const next = last.movies.offset + last.movies.limit
return next < last.movies.total_count ? next : undefined
},
enabled: !!params.q || !!params.genre || !!params.person_id,
})
}
export function usePerson(id: string) {
return useQuery({
queryKey: searchKeys.person(id),
queryFn: () => getPerson(id),
enabled: !!id,
})
}
export function usePersonCredits(id: string) {
return useQuery({
queryKey: searchKeys.personCredits(id),
queryFn: () => getPersonCredits(id),
enabled: !!id,
})
}