import { z } from "zod" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { diaryEntryDtoSchema, paginatedSchema } from "@/lib/api/common" import { delWithBody, get, post, put, putForm } from "@/lib/api/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 export const usersResponseSchema = z.object({ users: z.array(userSummaryDtoSchema), }) export type UsersResponse = z.infer 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 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 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 export const directorStatDtoSchema = z.object({ director: z.string(), count: z.number(), }) export type DirectorStatDto = z.infer export const actorStatDtoSchema = z.object({ name: z.string(), profile_path: z.string().optional(), movie_count: z.number(), score: z.number(), }) export type ActorStatDto = z.infer export const genreStatDtoSchema = z.object({ genre: z.string(), count: z.number(), }) export type GenreStatDto = z.infer export const watchMediumStatDtoSchema = z.object({ medium: z.string(), count: z.number(), }) export type WatchMediumStatDto = z.infer export const userTrendsDtoSchema = z.object({ monthly_ratings: z.array(monthlyRatingDtoSchema), top_directors: z.array(directorStatDtoSchema), max_director_count: z.number(), top_actors: z.array(actorStatDtoSchema).default([]), top_genres: z.array(genreStatDtoSchema).default([]), rating_distribution: z.array(z.number()).default([]), watch_medium_distribution: z.array(watchMediumStatDtoSchema).default([]), }) export type UserTrendsDto = z.infer 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 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 const userDiaryResponseSchema = paginatedSchema(diaryEntryDtoSchema) 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 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 export const updateProfileFieldsRequestSchema = z.object({ fields: z.array(profileFieldDtoSchema), }) export type UpdateProfileFieldsRequest = z.infer export type UpdateProfileData = { display_name?: string bio?: string also_known_as?: string avatar?: File banner?: File } function getUsers() { return get("/users") } function getUserProfile(id: string, params?: UserProfileQueryParams) { return get(`/users/${id}`, params) } function getProfile() { return get("/profile") } 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) } function updateProfileFields(data: UpdateProfileFieldsRequest) { return put("/profile/fields", data) } export function reindexSearch() { return post("/admin/reindex-search") } export const userKeys = { all: ["users"] as const, list: () => [...userKeys.all, "list"] as const, profile: (id: string, params?: UserProfileQueryParams) => [...userKeys.all, id, params] as const, me: ["profile"] as const, } export function useUsers() { return useQuery({ queryKey: userKeys.list(), queryFn: getUsers, }) } export function useUserProfile(id: string, params?: UserProfileQueryParams) { return useQuery({ queryKey: userKeys.profile(id, params), queryFn: () => getUserProfile(id, params), enabled: !!id, }) } export function useProfile() { return useQuery({ queryKey: userKeys.me, queryFn: getProfile, }) } export function useUpdateProfile() { const qc = useQueryClient() return useMutation({ mutationFn: (data: UpdateProfileData) => updateProfile(data), onSuccess: () => { qc.invalidateQueries({ queryKey: userKeys.me }) }, }) } export function useUpdateProfileFields() { const qc = useQueryClient() return useMutation({ mutationFn: (data: UpdateProfileFieldsRequest) => updateProfileFields(data), onSuccess: () => { qc.invalidateQueries({ queryKey: userKeys.me }) }, }) } export type DeleteAccountRequest = { password: string } export function useDeleteOwnAccount() { return useMutation({ mutationFn: (data: DeleteAccountRequest) => delWithBody("/users/me", data), }) } export function useAdminDeleteUser() { const qc = useQueryClient() return useMutation({ mutationFn: ({ id, password }: DeleteAccountRequest & { id: string }) => delWithBody(`/users/${id}`, { password }), onSuccess: () => { qc.invalidateQueries({ queryKey: userKeys.all }) qc.invalidateQueries({ queryKey: ["activity-feed"] }) }, }) }