Files
movies-diary/spa/src/features/users.ts

251 lines
7.2 KiB
TypeScript

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<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 actorStatDtoSchema = z.object({
name: z.string(),
profile_path: z.string().optional(),
movie_count: z.number(),
score: z.number(),
})
export type ActorStatDto = z.infer<typeof actorStatDtoSchema>
export const genreStatDtoSchema = z.object({
genre: z.string(),
count: z.number(),
})
export type GenreStatDto = z.infer<typeof genreStatDtoSchema>
export const watchMediumStatDtoSchema = z.object({
medium: z.string(),
count: z.number(),
})
export type WatchMediumStatDto = z.infer<typeof watchMediumStatDtoSchema>
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<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>
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>
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<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 type UpdateProfileData = {
display_name?: string
bio?: string
also_known_as?: string
avatar?: File
banner?: File
}
function getUsers() {
return get<UsersResponse>("/users")
}
function getUserProfile(id: string, params?: UserProfileQueryParams) {
return get<UserProfileResponse>(`/users/${id}`, params)
}
function getProfile() {
return get<ProfileResponse>("/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"] })
},
})
}