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:
@@ -1,55 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { API_URL, post } from "./client"
|
||||
|
||||
export const loginRequestSchema = z.object({
|
||||
email: z.string(),
|
||||
password: z.string(),
|
||||
})
|
||||
export type LoginRequest = z.infer<typeof loginRequestSchema>
|
||||
|
||||
export const loginResponseSchema = z.object({
|
||||
token: z.string(),
|
||||
refresh_token: z.string(),
|
||||
user_id: z.string().uuid(),
|
||||
email: z.string(),
|
||||
role: z.string(),
|
||||
expires_at: z.string(),
|
||||
})
|
||||
export type LoginResponse = z.infer<typeof loginResponseSchema>
|
||||
|
||||
export const registerRequestSchema = z.object({
|
||||
email: z.string(),
|
||||
username: z.string(),
|
||||
password: z.string(),
|
||||
})
|
||||
export type RegisterRequest = z.infer<typeof registerRequestSchema>
|
||||
|
||||
export function login(data: LoginRequest) {
|
||||
return post<LoginResponse>("/auth/login", data)
|
||||
}
|
||||
|
||||
export function register(data: RegisterRequest) {
|
||||
return post("/auth/register", data)
|
||||
}
|
||||
|
||||
export type RefreshResponse = {
|
||||
token: string
|
||||
refresh_token: string
|
||||
expires_at: string
|
||||
}
|
||||
|
||||
export async function refreshToken(
|
||||
refresh_token: string,
|
||||
): Promise<RefreshResponse> {
|
||||
const res = await fetch(`${API_URL}/api/v1/auth/refresh`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ refresh_token }),
|
||||
})
|
||||
if (!res.ok) throw new Error("refresh failed")
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export function apiLogout(refresh_token: string) {
|
||||
return post("/auth/logout", { refresh_token })
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import type { DiaryEntryDto, Paginated } from "./common"
|
||||
import { diaryEntryDtoSchema, movieDtoSchema, paginatedSchema, reviewDtoSchema } from "./common"
|
||||
import { del, get, patch, post } from "./client"
|
||||
|
||||
export const diaryQueryParamsSchema = z.object({
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
sort_by: z.string().optional(),
|
||||
movie_id: z.string().uuid().optional(),
|
||||
user_id: z.string().uuid().optional(),
|
||||
})
|
||||
export type DiaryQueryParams = z.infer<typeof diaryQueryParamsSchema>
|
||||
|
||||
export const diaryResponseSchema = paginatedSchema(diaryEntryDtoSchema)
|
||||
export type DiaryResponse = Paginated<DiaryEntryDto>
|
||||
|
||||
export const logReviewRequestSchema = z.object({
|
||||
external_metadata_id: z.string().optional(),
|
||||
manual_title: z.string().optional(),
|
||||
manual_release_year: z.number().optional(),
|
||||
manual_director: z.string().optional(),
|
||||
rating: z.number(),
|
||||
comment: z.string().optional(),
|
||||
watched_at: z.string(),
|
||||
watch_medium: z.string().optional(),
|
||||
})
|
||||
export type LogReviewRequest = z.infer<typeof logReviewRequestSchema>
|
||||
|
||||
export const editReviewRequestSchema = z.object({
|
||||
rating: z.number().optional(),
|
||||
comment: z.string().nullable().optional(),
|
||||
watched_at: z.string().optional(),
|
||||
watch_medium: z.string().nullable().optional(),
|
||||
})
|
||||
export type EditReviewRequest = z.infer<typeof editReviewRequestSchema>
|
||||
|
||||
export const feedEntryDtoSchema = z.object({
|
||||
movie: movieDtoSchema,
|
||||
review: reviewDtoSchema,
|
||||
user_id: z.string().uuid(),
|
||||
user_display_name: z.string(),
|
||||
is_federated: z.boolean(),
|
||||
actor_url: z.string().optional(),
|
||||
})
|
||||
export type FeedEntryDto = z.infer<typeof feedEntryDtoSchema>
|
||||
|
||||
export const activityFeedQueryParamsSchema = z.object({
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
sort_by: z.string().optional(),
|
||||
})
|
||||
export type ActivityFeedQueryParams = z.infer<typeof activityFeedQueryParamsSchema>
|
||||
|
||||
export const activityFeedResponseSchema = paginatedSchema(feedEntryDtoSchema)
|
||||
export type ActivityFeedResponse = Paginated<FeedEntryDto>
|
||||
|
||||
export const exportQueryParamsSchema = z.object({
|
||||
format: z.string().optional(),
|
||||
})
|
||||
export type ExportQueryParams = z.infer<typeof exportQueryParamsSchema>
|
||||
|
||||
export function getDiary(params?: DiaryQueryParams) {
|
||||
return get<DiaryResponse>("/diary", params)
|
||||
}
|
||||
|
||||
export function logReview(data: LogReviewRequest) {
|
||||
return post("/reviews", data)
|
||||
}
|
||||
|
||||
export function editReview(id: string, data: EditReviewRequest) {
|
||||
return patch(`/reviews/${id}`, data)
|
||||
}
|
||||
|
||||
export function deleteReview(id: string) {
|
||||
return del(`/reviews/${id}`)
|
||||
}
|
||||
|
||||
export function getActivityFeed(params?: ActivityFeedQueryParams) {
|
||||
return get<ActivityFeedResponse>("/activity-feed", params)
|
||||
}
|
||||
|
||||
export function exportDiary(params?: ExportQueryParams) {
|
||||
return get<Blob>("/diary/export", params)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { get, post, put, del } from "./client"
|
||||
import { goalDtoSchema } from "./users"
|
||||
|
||||
export const goalsResponseSchema = z.object({
|
||||
goals: z.array(goalDtoSchema),
|
||||
})
|
||||
export type GoalsResponse = z.infer<typeof goalsResponseSchema>
|
||||
|
||||
export type CreateGoalRequest = {
|
||||
year: number
|
||||
target_count: number
|
||||
}
|
||||
|
||||
export type UpdateGoalRequest = {
|
||||
target_count: number
|
||||
}
|
||||
|
||||
export const userSettingsDtoSchema = z.object({
|
||||
federate_goals: z.boolean(),
|
||||
federate_reviews: z.boolean(),
|
||||
federate_watchlist: z.boolean(),
|
||||
})
|
||||
export type UserSettingsDto = z.infer<typeof userSettingsDtoSchema>
|
||||
|
||||
export type UpdateUserSettingsRequest = {
|
||||
federate_goals: boolean
|
||||
federate_reviews: boolean
|
||||
federate_watchlist: boolean
|
||||
}
|
||||
|
||||
export function getGoals() {
|
||||
return get<GoalsResponse>("/goals")
|
||||
}
|
||||
|
||||
export function getUserGoals(userId: string) {
|
||||
return get<GoalsResponse>(`/users/${userId}/goals`)
|
||||
}
|
||||
|
||||
export function createGoal(data: CreateGoalRequest) {
|
||||
return post<z.infer<typeof goalDtoSchema>>("/goals", data)
|
||||
}
|
||||
|
||||
export function updateGoal(year: number, data: UpdateGoalRequest) {
|
||||
return put<z.infer<typeof goalDtoSchema>>(`/goals/${year}`, data)
|
||||
}
|
||||
|
||||
export function deleteGoal(year: number) {
|
||||
return del(`/goals/${year}`)
|
||||
}
|
||||
|
||||
export function getSettings() {
|
||||
return get<UserSettingsDto>("/settings")
|
||||
}
|
||||
|
||||
export function updateSettings(data: UpdateUserSettingsRequest) {
|
||||
return put("/settings", data)
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { del, get, post, put, uploadWithFields } from "./client"
|
||||
|
||||
export const sessionCreatedResponseSchema = z.object({
|
||||
session_id: z.string(),
|
||||
columns: z.array(z.string()),
|
||||
sample_rows: z.array(z.array(z.string())),
|
||||
})
|
||||
export type SessionCreatedResponse = z.infer<typeof sessionCreatedResponseSchema>
|
||||
|
||||
export const sessionStateResponseSchema = z.object({
|
||||
session_id: z.string(),
|
||||
columns: z.array(z.string()),
|
||||
has_mappings: z.boolean(),
|
||||
row_count: z.number(),
|
||||
})
|
||||
export type SessionStateResponse = z.infer<typeof sessionStateResponseSchema>
|
||||
|
||||
export const apiFieldMappingSchema = z.object({
|
||||
source_column: z.string(),
|
||||
domain_field: z.string(),
|
||||
rating_scale: z.number().optional(),
|
||||
date_format: z.string().optional(),
|
||||
})
|
||||
export type ApiFieldMapping = z.infer<typeof apiFieldMappingSchema>
|
||||
|
||||
export const applyMappingRequestSchema = z.object({
|
||||
mappings: z.array(apiFieldMappingSchema),
|
||||
})
|
||||
export type ApplyMappingRequest = z.infer<typeof applyMappingRequestSchema>
|
||||
|
||||
export const confirmRequestSchema = z.object({
|
||||
confirmed_indices: z.array(z.number()),
|
||||
})
|
||||
export type ConfirmRequest = z.infer<typeof confirmRequestSchema>
|
||||
|
||||
export const saveProfileRequestSchema = z.object({
|
||||
session_id: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
export type SaveProfileRequest = z.infer<typeof saveProfileRequestSchema>
|
||||
|
||||
export function createImportSession(file: File) {
|
||||
const ext = file.name.split(".").pop()?.toLowerCase()
|
||||
const format = ext === "json" ? "json" : "csv"
|
||||
return uploadWithFields<SessionCreatedResponse>("/import/sessions", file, { format })
|
||||
}
|
||||
|
||||
export function getImportSession(id: string) {
|
||||
return get<SessionStateResponse>(`/import/sessions/${id}`)
|
||||
}
|
||||
|
||||
export type PreviewRow = {
|
||||
index: number
|
||||
status: string
|
||||
title?: string
|
||||
release_year?: string
|
||||
director?: string
|
||||
rating?: string
|
||||
watched_at?: string
|
||||
comment?: string
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
export type PreviewResponse = {
|
||||
rows: PreviewRow[]
|
||||
}
|
||||
|
||||
export function getImportPreview(id: string) {
|
||||
return get<PreviewResponse>(`/import/sessions/${id}/preview`)
|
||||
}
|
||||
|
||||
export function applyMapping(sessionId: string, data: ApplyMappingRequest) {
|
||||
return put(`/import/sessions/${sessionId}/mapping`, data)
|
||||
}
|
||||
|
||||
export function confirmImport(sessionId: string, data: ConfirmRequest) {
|
||||
return post(`/import/sessions/${sessionId}/confirm`, data)
|
||||
}
|
||||
|
||||
export type ImportProfile = {
|
||||
id: string
|
||||
name: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export function getImportProfiles() {
|
||||
return get<ImportProfile[]>("/import/profiles")
|
||||
}
|
||||
|
||||
export function saveImportProfile(data: SaveProfileRequest) {
|
||||
return post<{ id: string }>("/import/profiles", data)
|
||||
}
|
||||
|
||||
export function deleteImportProfile(id: string) {
|
||||
return del(`/import/profiles/${id}`)
|
||||
}
|
||||
|
||||
export function applyImportProfile(sessionId: string, profileId: string) {
|
||||
return put<{ row_count: number }>(`/import/sessions/${sessionId}/profile/${profileId}`)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
export { ApiError } from "./client"
|
||||
export * from "./common"
|
||||
export * from "./auth"
|
||||
export * from "./diary"
|
||||
export * from "./movies"
|
||||
export * from "./users"
|
||||
export * from "./search"
|
||||
export * from "./watchlist"
|
||||
export * from "./webhooks"
|
||||
export * from "./imports"
|
||||
export * from "./wrapup"
|
||||
export * from "./social"
|
||||
@@ -1,121 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import type { Paginated } from "./common"
|
||||
import { movieDtoSchema, paginatedSchema, reviewDtoSchema } from "./common"
|
||||
import { get, post } from "./client"
|
||||
|
||||
export const moviesQueryParamsSchema = z.object({
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
search: z.string().optional(),
|
||||
genre: z.string().optional(),
|
||||
language: z.string().optional(),
|
||||
})
|
||||
export type MoviesQueryParams = z.infer<typeof moviesQueryParamsSchema>
|
||||
|
||||
export const moviesResponseSchema = paginatedSchema(movieDtoSchema)
|
||||
export type MoviesResponse = z.infer<typeof moviesResponseSchema>
|
||||
|
||||
export const movieStatsDtoSchema = z.object({
|
||||
total_count: z.number(),
|
||||
avg_rating: z.number().optional(),
|
||||
federated_count: z.number(),
|
||||
rating_histogram: z.array(z.number()),
|
||||
})
|
||||
export type MovieStatsDto = z.infer<typeof movieStatsDtoSchema>
|
||||
|
||||
export const socialReviewDtoSchema = z.object({
|
||||
user_display: z.string(),
|
||||
rating: z.number(),
|
||||
comment: z.string().optional(),
|
||||
watched_at: z.string(),
|
||||
is_federated: z.boolean(),
|
||||
watch_medium: z.string().optional(),
|
||||
})
|
||||
export type SocialReviewDto = z.infer<typeof socialReviewDtoSchema>
|
||||
|
||||
export const socialFeedResponseSchema = paginatedSchema(socialReviewDtoSchema)
|
||||
export type SocialFeedResponse = Paginated<SocialReviewDto>
|
||||
|
||||
export const movieDetailResponseSchema = z.object({
|
||||
movie: movieDtoSchema,
|
||||
stats: movieStatsDtoSchema,
|
||||
reviews: socialFeedResponseSchema,
|
||||
})
|
||||
export type MovieDetailResponse = z.infer<typeof movieDetailResponseSchema>
|
||||
|
||||
export const reviewHistoryResponseSchema = z.object({
|
||||
movie: movieDtoSchema,
|
||||
viewings: z.array(reviewDtoSchema),
|
||||
trend: z.string(),
|
||||
})
|
||||
export type ReviewHistoryResponse = z.infer<typeof reviewHistoryResponseSchema>
|
||||
|
||||
export const genreDtoSchema = z.object({
|
||||
tmdb_id: z.number(),
|
||||
name: z.string(),
|
||||
})
|
||||
|
||||
export const keywordDtoSchema = z.object({
|
||||
tmdb_id: z.number(),
|
||||
name: z.string(),
|
||||
})
|
||||
|
||||
export const castMemberDtoSchema = z.object({
|
||||
person_id: z.string(),
|
||||
tmdb_person_id: z.number(),
|
||||
name: z.string(),
|
||||
character: z.string(),
|
||||
billing_order: z.number(),
|
||||
profile_path: z.string().optional(),
|
||||
})
|
||||
export type CastMemberDto = z.infer<typeof castMemberDtoSchema>
|
||||
|
||||
export const crewMemberDtoSchema = z.object({
|
||||
person_id: z.string(),
|
||||
tmdb_person_id: z.number(),
|
||||
name: z.string(),
|
||||
job: z.string(),
|
||||
department: z.string(),
|
||||
profile_path: z.string().optional(),
|
||||
})
|
||||
export type CrewMemberDto = z.infer<typeof crewMemberDtoSchema>
|
||||
|
||||
export const movieProfileResponseSchema = z.object({
|
||||
tmdb_id: z.number(),
|
||||
imdb_id: z.string().optional(),
|
||||
overview: z.string().optional(),
|
||||
tagline: z.string().optional(),
|
||||
runtime_minutes: z.number().optional(),
|
||||
budget_usd: z.number().optional(),
|
||||
revenue_usd: z.number().optional(),
|
||||
vote_average: z.number().optional(),
|
||||
vote_count: z.number().optional(),
|
||||
original_language: z.string().optional(),
|
||||
collection_name: z.string().optional(),
|
||||
genres: z.array(genreDtoSchema),
|
||||
keywords: z.array(keywordDtoSchema),
|
||||
cast: z.array(castMemberDtoSchema),
|
||||
crew: z.array(crewMemberDtoSchema),
|
||||
enriched_at: z.string(),
|
||||
})
|
||||
export type MovieProfileResponse = z.infer<typeof movieProfileResponseSchema>
|
||||
|
||||
export function getMovies(params?: MoviesQueryParams) {
|
||||
return get<MoviesResponse>("/movies", params)
|
||||
}
|
||||
|
||||
export function getMovie(id: string) {
|
||||
return get<MovieDetailResponse>(`/movies/${id}`)
|
||||
}
|
||||
|
||||
export function getMovieHistory(id: string) {
|
||||
return get<ReviewHistoryResponse>(`/movies/${id}/history`)
|
||||
}
|
||||
|
||||
export function getMovieProfile(id: string) {
|
||||
return get<MovieProfileResponse>(`/movies/${id}/profile`)
|
||||
}
|
||||
|
||||
export function syncPoster(id: string) {
|
||||
return post(`/movies/${id}/sync-poster`)
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
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(),
|
||||
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>
|
||||
|
||||
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`)
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { del, get, post } from "./client"
|
||||
|
||||
export const remoteActorDtoSchema = z.object({
|
||||
handle: z.string(),
|
||||
display_name: z.string().optional(),
|
||||
url: z.string(),
|
||||
})
|
||||
export type RemoteActorDto = z.infer<typeof remoteActorDtoSchema>
|
||||
|
||||
export const actorListResponseSchema = z.object({
|
||||
actors: z.array(remoteActorDtoSchema),
|
||||
})
|
||||
export type ActorListResponse = z.infer<typeof actorListResponseSchema>
|
||||
|
||||
export const followRequestSchema = z.object({
|
||||
handle: z.string(),
|
||||
})
|
||||
export type FollowRequest = z.infer<typeof followRequestSchema>
|
||||
|
||||
export const actorUrlRequestSchema = z.object({
|
||||
actor_url: z.string(),
|
||||
})
|
||||
export type ActorUrlRequest = z.infer<typeof actorUrlRequestSchema>
|
||||
|
||||
export const blockedDomainResponseSchema = z.object({
|
||||
domain: z.string(),
|
||||
reason: z.string().optional(),
|
||||
blocked_at: z.string(),
|
||||
})
|
||||
export type BlockedDomainResponse = z.infer<typeof blockedDomainResponseSchema>
|
||||
|
||||
export const addBlockedDomainRequestSchema = z.object({
|
||||
domain: z.string(),
|
||||
reason: z.string().optional(),
|
||||
})
|
||||
export type AddBlockedDomainRequest = z.infer<typeof addBlockedDomainRequestSchema>
|
||||
|
||||
export const blockedActorResponseSchema = z.object({
|
||||
url: z.string(),
|
||||
handle: z.string(),
|
||||
display_name: z.string().optional(),
|
||||
avatar_url: z.string().optional(),
|
||||
})
|
||||
export type BlockedActorResponse = z.infer<typeof blockedActorResponseSchema>
|
||||
|
||||
export function getFollowing() {
|
||||
return get<ActorListResponse>("/social/following")
|
||||
}
|
||||
|
||||
export function getFollowers() {
|
||||
return get<ActorListResponse>("/social/followers")
|
||||
}
|
||||
|
||||
export function getUserFollowing(userId: string) {
|
||||
return get<ActorListResponse>(`/users/${userId}/following`)
|
||||
}
|
||||
|
||||
export function getUserFollowers(userId: string) {
|
||||
return get<ActorListResponse>(`/users/${userId}/followers`)
|
||||
}
|
||||
|
||||
export function getPendingFollowers() {
|
||||
return get<ActorListResponse>("/social/followers/pending")
|
||||
}
|
||||
|
||||
export function follow(data: FollowRequest) {
|
||||
return post("/social/follow", data)
|
||||
}
|
||||
|
||||
export function unfollow(data: ActorUrlRequest) {
|
||||
return post("/social/unfollow", data)
|
||||
}
|
||||
|
||||
export function acceptFollower(data: ActorUrlRequest) {
|
||||
return post("/social/followers/accept", data)
|
||||
}
|
||||
|
||||
export function rejectFollower(data: ActorUrlRequest) {
|
||||
return post("/social/followers/reject", data)
|
||||
}
|
||||
|
||||
export function removeFollower(data: ActorUrlRequest) {
|
||||
return post("/social/followers/remove", data)
|
||||
}
|
||||
|
||||
export function getBlockedDomains() {
|
||||
return get<BlockedDomainResponse[]>("/admin/blocked-domains")
|
||||
}
|
||||
|
||||
export function addBlockedDomain(data: AddBlockedDomainRequest) {
|
||||
return post("/admin/blocked-domains", data)
|
||||
}
|
||||
|
||||
export function removeBlockedDomain(domain: string) {
|
||||
return del(`/admin/blocked-domains/${domain}`)
|
||||
}
|
||||
|
||||
export function blockActor(data: ActorUrlRequest) {
|
||||
return post("/social/block", data)
|
||||
}
|
||||
|
||||
export function unblockActor(data: ActorUrlRequest) {
|
||||
return post("/social/unblock", data)
|
||||
}
|
||||
|
||||
export function getBlockedActors() {
|
||||
return get<BlockedActorResponse[]>("/social/blocked")
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { diaryEntryDtoSchema, paginatedSchema } from "./common"
|
||||
import { get, post, put, putForm } from "./client"
|
||||
|
||||
export const userSummaryDtoSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
email: z.string(),
|
||||
username: z.string(),
|
||||
display_name: z.string().optional(),
|
||||
total_movies: z.number(),
|
||||
avg_rating: z.number().optional(),
|
||||
})
|
||||
export type UserSummaryDto = z.infer<typeof userSummaryDtoSchema>
|
||||
|
||||
export const usersResponseSchema = z.object({
|
||||
users: z.array(userSummaryDtoSchema),
|
||||
})
|
||||
export type UsersResponse = z.infer<typeof usersResponseSchema>
|
||||
|
||||
export const userProfileQueryParamsSchema = z.object({
|
||||
view: z.string().optional(),
|
||||
limit: z.number().optional(),
|
||||
offset: z.number().optional(),
|
||||
search: z.string().optional(),
|
||||
})
|
||||
export type UserProfileQueryParams = z.infer<typeof userProfileQueryParamsSchema>
|
||||
|
||||
export const userStatsDtoSchema = z.object({
|
||||
total_movies: z.number(),
|
||||
avg_rating: z.number().optional(),
|
||||
favorite_director: z.string().optional(),
|
||||
most_active_month: z.string().optional(),
|
||||
})
|
||||
export type UserStatsDto = z.infer<typeof userStatsDtoSchema>
|
||||
|
||||
export const monthlyRatingDtoSchema = z.object({
|
||||
year_month: z.string(),
|
||||
month_label: z.string(),
|
||||
avg_rating: z.number(),
|
||||
count: z.number(),
|
||||
})
|
||||
export type MonthlyRatingDto = z.infer<typeof monthlyRatingDtoSchema>
|
||||
|
||||
export const directorStatDtoSchema = z.object({
|
||||
director: z.string(),
|
||||
count: z.number(),
|
||||
})
|
||||
export type DirectorStatDto = z.infer<typeof directorStatDtoSchema>
|
||||
|
||||
export const userTrendsDtoSchema = z.object({
|
||||
monthly_ratings: z.array(monthlyRatingDtoSchema),
|
||||
top_directors: z.array(directorStatDtoSchema),
|
||||
max_director_count: z.number(),
|
||||
})
|
||||
export type UserTrendsDto = z.infer<typeof userTrendsDtoSchema>
|
||||
|
||||
export const monthActivityDtoSchema = z.object({
|
||||
year_month: z.string(),
|
||||
month_label: z.string(),
|
||||
count: z.number(),
|
||||
entries: z.array(diaryEntryDtoSchema),
|
||||
})
|
||||
export type MonthActivityDto = z.infer<typeof monthActivityDtoSchema>
|
||||
|
||||
const userDiaryResponseSchema = paginatedSchema(diaryEntryDtoSchema)
|
||||
|
||||
export const goalDtoSchema = z.object({
|
||||
year: z.number(),
|
||||
target_count: z.number(),
|
||||
current_count: z.number(),
|
||||
percentage: z.number(),
|
||||
is_complete: z.boolean(),
|
||||
goal_type: z.string(),
|
||||
})
|
||||
export type GoalDto = z.infer<typeof goalDtoSchema>
|
||||
|
||||
export const userProfileResponseSchema = z.object({
|
||||
user_id: z.string().uuid(),
|
||||
username: z.string(),
|
||||
avatar_url: z.string().optional(),
|
||||
banner_url: z.string().optional(),
|
||||
stats: userStatsDtoSchema,
|
||||
following_count: z.number(),
|
||||
followers_count: z.number(),
|
||||
entries: userDiaryResponseSchema.optional(),
|
||||
history: z.array(monthActivityDtoSchema).optional(),
|
||||
trends: userTrendsDtoSchema.optional(),
|
||||
goals: z.array(goalDtoSchema).optional(),
|
||||
is_federated: z.boolean().optional().default(false),
|
||||
handle: z.string().optional(),
|
||||
display_name: z.string().optional(),
|
||||
bio: z.string().optional(),
|
||||
actor_url: z.string().optional(),
|
||||
})
|
||||
export type UserProfileResponse = z.infer<typeof userProfileResponseSchema>
|
||||
|
||||
export const profileFieldDtoSchema = z.object({
|
||||
name: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
|
||||
export const profileResponseSchema = z.object({
|
||||
username: z.string(),
|
||||
display_name: z.string().optional(),
|
||||
bio: z.string().optional(),
|
||||
avatar_url: z.string().optional(),
|
||||
banner_url: z.string().optional(),
|
||||
also_known_as: z.string().optional(),
|
||||
fields: z.array(profileFieldDtoSchema),
|
||||
role: z.string(),
|
||||
})
|
||||
export type ProfileResponse = z.infer<typeof profileResponseSchema>
|
||||
|
||||
export const updateProfileFieldsRequestSchema = z.object({
|
||||
fields: z.array(profileFieldDtoSchema),
|
||||
})
|
||||
export type UpdateProfileFieldsRequest = z.infer<typeof updateProfileFieldsRequestSchema>
|
||||
|
||||
export function getUsers() {
|
||||
return get<UsersResponse>("/users")
|
||||
}
|
||||
|
||||
export function getUserProfile(id: string, params?: UserProfileQueryParams) {
|
||||
return get<UserProfileResponse>(`/users/${id}`, params)
|
||||
}
|
||||
|
||||
export function getProfile() {
|
||||
return get<ProfileResponse>("/profile")
|
||||
}
|
||||
|
||||
export type UpdateProfileData = {
|
||||
display_name?: string
|
||||
bio?: string
|
||||
also_known_as?: string
|
||||
avatar?: File
|
||||
banner?: File
|
||||
}
|
||||
|
||||
export function updateProfile(data: UpdateProfileData) {
|
||||
const form = new FormData()
|
||||
if (data.display_name != null) form.append("display_name", data.display_name)
|
||||
if (data.also_known_as != null) form.append("also_known_as", data.also_known_as)
|
||||
if (data.bio != null) form.append("bio", data.bio)
|
||||
if (data.avatar) form.append("avatar", data.avatar)
|
||||
if (data.banner) form.append("banner", data.banner)
|
||||
return putForm("/profile", form)
|
||||
}
|
||||
|
||||
export function updateProfileFields(data: UpdateProfileFieldsRequest) {
|
||||
return put("/profile/fields", data)
|
||||
}
|
||||
|
||||
export function reindexSearch() {
|
||||
return post("/admin/reindex-search")
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import type { Paginated } from "./common"
|
||||
import { movieDtoSchema, paginatedSchema } from "./common"
|
||||
import { del, get, post } from "./client"
|
||||
|
||||
export const watchlistEntryDtoSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
movie: movieDtoSchema,
|
||||
added_at: z.string(),
|
||||
})
|
||||
export type WatchlistEntryDto = z.infer<typeof watchlistEntryDtoSchema>
|
||||
|
||||
export const watchlistResponseSchema = paginatedSchema(watchlistEntryDtoSchema)
|
||||
export type WatchlistResponse = Paginated<WatchlistEntryDto>
|
||||
|
||||
export const addToWatchlistRequestSchema = z.object({
|
||||
movie_id: z.string().uuid().optional(),
|
||||
external_metadata_id: z.string().optional(),
|
||||
manual_title: z.string().optional(),
|
||||
manual_release_year: z.number().optional(),
|
||||
})
|
||||
export type AddToWatchlistRequest = z.infer<typeof addToWatchlistRequestSchema>
|
||||
|
||||
export const watchlistStatusResponseSchema = z.object({
|
||||
on_watchlist: z.boolean(),
|
||||
})
|
||||
export type WatchlistStatusResponse = z.infer<typeof watchlistStatusResponseSchema>
|
||||
|
||||
export function getWatchlist(params?: { limit?: number; offset?: number }) {
|
||||
return get<WatchlistResponse>("/watchlist", params)
|
||||
}
|
||||
|
||||
export function getWatchlistStatus(movieId: string) {
|
||||
return get<WatchlistStatusResponse>(`/watchlist/${movieId}`)
|
||||
}
|
||||
|
||||
export function addToWatchlist(data: AddToWatchlistRequest) {
|
||||
return post("/watchlist", data)
|
||||
}
|
||||
|
||||
export function removeFromWatchlist(movieId: string) {
|
||||
return del(`/watchlist/${movieId}`)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { del, get, post } from "./client"
|
||||
|
||||
export const webhookTokenDtoSchema = z.object({
|
||||
id: z.string(),
|
||||
provider: z.string(),
|
||||
label: z.string().optional(),
|
||||
created_at: z.string(),
|
||||
last_used_at: z.string().optional(),
|
||||
})
|
||||
export type WebhookTokenDto = z.infer<typeof webhookTokenDtoSchema>
|
||||
|
||||
export const generateTokenRequestSchema = z.object({
|
||||
provider: z.string(),
|
||||
label: z.string().optional(),
|
||||
})
|
||||
export type GenerateTokenRequest = z.infer<typeof generateTokenRequestSchema>
|
||||
|
||||
export const generateTokenResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
token: z.string(),
|
||||
webhook_url: z.string(),
|
||||
})
|
||||
export type GenerateTokenResponse = z.infer<typeof generateTokenResponseSchema>
|
||||
|
||||
export const watchQueueEntryDtoSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
year: z.number().optional(),
|
||||
movie_id: z.string().optional(),
|
||||
source: z.string(),
|
||||
watched_at: z.string(),
|
||||
})
|
||||
export type WatchQueueEntryDto = z.infer<typeof watchQueueEntryDtoSchema>
|
||||
|
||||
export const confirmWatchEntrySchema = z.object({
|
||||
watch_event_id: z.string().uuid(),
|
||||
rating: z.number(),
|
||||
comment: z.string().optional(),
|
||||
})
|
||||
|
||||
export const confirmWatchRequestSchema = z.object({
|
||||
confirmations: z.array(confirmWatchEntrySchema),
|
||||
})
|
||||
export type ConfirmWatchRequest = z.infer<typeof confirmWatchRequestSchema>
|
||||
|
||||
export const confirmWatchResponseSchema = z.object({
|
||||
confirmed: z.number(),
|
||||
})
|
||||
export type ConfirmWatchResponse = z.infer<typeof confirmWatchResponseSchema>
|
||||
|
||||
export const dismissWatchRequestSchema = z.object({
|
||||
event_ids: z.array(z.string().uuid()),
|
||||
})
|
||||
export type DismissWatchRequest = z.infer<typeof dismissWatchRequestSchema>
|
||||
|
||||
export const dismissWatchResponseSchema = z.object({
|
||||
dismissed: z.number(),
|
||||
})
|
||||
export type DismissWatchResponse = z.infer<typeof dismissWatchResponseSchema>
|
||||
|
||||
export function getWebhookTokens() {
|
||||
return get<WebhookTokenDto[]>("/settings/webhook-tokens")
|
||||
}
|
||||
|
||||
export function generateToken(data: GenerateTokenRequest) {
|
||||
return post<GenerateTokenResponse>("/settings/webhook-tokens", data)
|
||||
}
|
||||
|
||||
export function deleteToken(id: string) {
|
||||
return del(`/settings/webhook-tokens/${id}`)
|
||||
}
|
||||
|
||||
export function getWatchQueue() {
|
||||
return get<WatchQueueEntryDto[]>("/watch-queue")
|
||||
}
|
||||
|
||||
export function confirmWatch(data: ConfirmWatchRequest) {
|
||||
return post<ConfirmWatchResponse>("/watch-queue/confirm", data)
|
||||
}
|
||||
|
||||
export function dismissWatch(data: DismissWatchRequest) {
|
||||
return post<DismissWatchResponse>("/watch-queue/dismiss", data)
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import { z } from "zod"
|
||||
import { del, get, post } from "./client"
|
||||
|
||||
export const generateWrapUpRequestSchema = z.object({
|
||||
start_date: z.string(),
|
||||
end_date: z.string(),
|
||||
user_id: z.string().uuid().optional(),
|
||||
})
|
||||
export type GenerateWrapUpRequest = z.infer<typeof generateWrapUpRequestSchema>
|
||||
|
||||
export const wrapUpGeneratedResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
})
|
||||
export type WrapUpGeneratedResponse = z.infer<typeof wrapUpGeneratedResponseSchema>
|
||||
|
||||
export const wrapUpStatusResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
user_id: z.string().optional(),
|
||||
status: z.string(),
|
||||
start_date: z.string(),
|
||||
end_date: z.string(),
|
||||
created_at: z.string(),
|
||||
completed_at: z.string().optional(),
|
||||
error_message: z.string().optional(),
|
||||
})
|
||||
export type WrapUpStatusResponse = z.infer<typeof wrapUpStatusResponseSchema>
|
||||
|
||||
export const wrapUpListResponseSchema = z.object({
|
||||
items: z.array(wrapUpStatusResponseSchema),
|
||||
})
|
||||
export type WrapUpListResponse = z.infer<typeof wrapUpListResponseSchema>
|
||||
|
||||
export function generateWrapUp(data: GenerateWrapUpRequest) {
|
||||
return post<WrapUpGeneratedResponse>("/wrapups/generate", data)
|
||||
}
|
||||
|
||||
export function getWrapUps() {
|
||||
return get<WrapUpListResponse>("/wrapups")
|
||||
}
|
||||
|
||||
export function getWrapUp(id: string) {
|
||||
return get<WrapUpStatusResponse>(`/wrapups/${id}`)
|
||||
}
|
||||
|
||||
export function deleteWrapUp(id: string) {
|
||||
return del(`/wrapups/${id}`)
|
||||
}
|
||||
|
||||
export type MovieRef = {
|
||||
movie_id?: string
|
||||
title: string
|
||||
year: number
|
||||
runtime_minutes?: number
|
||||
poster_path?: string
|
||||
}
|
||||
|
||||
export type PersonStat = {
|
||||
person_id?: string
|
||||
name: string
|
||||
count: number
|
||||
avg_rating: number
|
||||
}
|
||||
|
||||
export type GenreStat = {
|
||||
genre: string
|
||||
count: number
|
||||
avg_rating: number
|
||||
}
|
||||
|
||||
export type MonthCount = {
|
||||
year_month: string
|
||||
label: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export type KeywordStat = {
|
||||
keyword: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export type LangStat = {
|
||||
language: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export type WrapUpReport = {
|
||||
date_range: { start: string; end: string }
|
||||
total_movies: number
|
||||
total_watch_time_minutes: number
|
||||
movies_per_month: MonthCount[]
|
||||
busiest_month?: string
|
||||
busiest_day_of_week?: string
|
||||
avg_rating?: number
|
||||
rating_distribution: number[]
|
||||
longest_movie?: MovieRef
|
||||
shortest_movie?: MovieRef
|
||||
top_directors: PersonStat[]
|
||||
top_actors: PersonStat[]
|
||||
director_diversity: number
|
||||
actor_diversity: number
|
||||
top_genres: GenreStat[]
|
||||
genre_diversity: number
|
||||
highest_rated_genre?: string
|
||||
lowest_rated_genre?: string
|
||||
top_keywords: KeywordStat[]
|
||||
total_budget_watched?: number
|
||||
avg_budget?: number
|
||||
language_distribution: LangStat[]
|
||||
oldest_movie?: MovieRef
|
||||
newest_movie?: MovieRef
|
||||
total_rewatches: number
|
||||
most_rewatched_movie?: MovieRef
|
||||
highest_rated_movie?: MovieRef
|
||||
lowest_rated_movie?: MovieRef
|
||||
first_movie_of_period?: MovieRef
|
||||
last_movie_of_period?: MovieRef
|
||||
poster_paths: string[]
|
||||
top_cast_profile_paths: string[]
|
||||
}
|
||||
|
||||
export function getWrapUpReport(id: string) {
|
||||
return get<WrapUpReport>(`/wrapups/${id}/report`)
|
||||
}
|
||||
@@ -17,3 +17,19 @@ export function shortDate(dateStr: string): string {
|
||||
return dateStr.slice(0, 10)
|
||||
}
|
||||
}
|
||||
|
||||
export function parseLocalDate(s: string): Date {
|
||||
const [datePart, timePart] = s.split("T")
|
||||
if (!datePart) return new Date()
|
||||
const [y, m, d] = datePart.split("-").map(Number)
|
||||
if (timePart) {
|
||||
const [h, min, sec] = timePart.split(":").map(Number)
|
||||
return new Date(y!, m! - 1, d!, h, min, sec)
|
||||
}
|
||||
return new Date(y!, m! - 1, d!)
|
||||
}
|
||||
|
||||
export function formatLocalDateTime(d: Date): string {
|
||||
const pad = (n: number) => n.toString().padStart(2, "0")
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user