feat: add SPA, serve at /app/, update Dockerfile and README

- React + TanStack Router + shadcn/ui SPA under spa/
- serve spa/dist at /app/ with index.html fallback for client routing
- Dockerfile: node build stage for SPA, copy dist into runtime image
- README: document SPA, CORS_ORIGINS env var, architecture entry
- vite base set to /app/, manifest.json paths fixed
This commit is contained in:
2026-06-04 04:20:15 +02:00
parent 15dc0e526b
commit b9c0b10740
153 changed files with 24329 additions and 1 deletions

87
spa/src/lib/api/search.ts Normal file
View File

@@ -0,0 +1,87 @@
import { z } from "zod"
import { paginatedSchema } from "./common"
import { get } from "./client"
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(),
})
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>
export function search(params: SearchQueryParams) {
return get<SearchResponse>("/search", params)
}
export function getPerson(id: string) {
return get<PersonDto>(`/people/${id}`)
}
export function getPersonCredits(id: string) {
return get<PersonCreditsDto>(`/people/${id}/credits`)
}