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:
39
spa/src/hooks/use-auth.ts
Normal file
39
spa/src/hooks/use-auth.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { useAuth } from "@/components/auth-provider"
|
||||
import { login, register } from "@/lib/api/auth"
|
||||
import type { LoginRequest, RegisterRequest } from "@/lib/api/auth"
|
||||
|
||||
export function useLogin() {
|
||||
const { login: setAuth } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: LoginRequest) => login(data),
|
||||
onSuccess: (res) => {
|
||||
setAuth({
|
||||
token: res.token,
|
||||
user_id: res.user_id,
|
||||
email: res.email,
|
||||
role: res.role,
|
||||
expires_at: res.expires_at,
|
||||
})
|
||||
qc.clear()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRegister() {
|
||||
return useMutation({
|
||||
mutationFn: (data: RegisterRequest) => register(data),
|
||||
})
|
||||
}
|
||||
|
||||
export function useLogout() {
|
||||
const { logout } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
logout()
|
||||
qc.clear()
|
||||
},
|
||||
})
|
||||
}
|
||||
12
spa/src/hooks/use-debounce.ts
Normal file
12
spa/src/hooks/use-debounce.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export function useDebounce<T>(value: T, delay = 300): T {
|
||||
const [debounced, setDebounced] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebounced(value), delay)
|
||||
return () => clearTimeout(timer)
|
||||
}, [value, delay])
|
||||
|
||||
return debounced
|
||||
}
|
||||
89
spa/src/hooks/use-diary.ts
Normal file
89
spa/src/hooks/use-diary.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query"
|
||||
import {
|
||||
deleteReview,
|
||||
getActivityFeed,
|
||||
getDiary,
|
||||
logReview,
|
||||
} from "@/lib/api/diary"
|
||||
import type {
|
||||
ActivityFeedQueryParams,
|
||||
DiaryQueryParams,
|
||||
LogReviewRequest,
|
||||
} from "@/lib/api/diary"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const diaryKeys = {
|
||||
all: ["diary"] as const,
|
||||
list: (params?: Partial<DiaryQueryParams>) => [...diaryKeys.all, "list", params] as const,
|
||||
infinite: (params?: Partial<DiaryQueryParams>) => [...diaryKeys.all, "infinite", params] as const,
|
||||
feed: (params?: ActivityFeedQueryParams) =>
|
||||
["activity-feed", params] as const,
|
||||
}
|
||||
|
||||
export function useDiary(params?: DiaryQueryParams) {
|
||||
return useQuery({
|
||||
queryKey: diaryKeys.list(params),
|
||||
queryFn: () => getDiary(params),
|
||||
})
|
||||
}
|
||||
|
||||
export function useInfiniteDiary(params?: Omit<DiaryQueryParams, "limit" | "offset">) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: diaryKeys.infinite(params),
|
||||
queryFn: ({ pageParam = 0 }) =>
|
||||
getDiary({ ...params, limit: PAGE_SIZE, offset: pageParam }),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (last) => {
|
||||
const next = last.offset + last.limit
|
||||
return next < last.total_count ? next : undefined
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useActivityFeed(params?: ActivityFeedQueryParams) {
|
||||
return useQuery({
|
||||
queryKey: diaryKeys.feed(params),
|
||||
queryFn: () => getActivityFeed(params),
|
||||
})
|
||||
}
|
||||
|
||||
export function useInfiniteActivityFeed(
|
||||
params?: Omit<ActivityFeedQueryParams, "limit" | "offset">,
|
||||
) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: diaryKeys.feed(params),
|
||||
queryFn: ({ pageParam = 0 }) =>
|
||||
getActivityFeed({ ...params, limit: PAGE_SIZE, offset: pageParam }),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (last) => {
|
||||
const next = last.offset + last.limit
|
||||
return next < last.total_count ? next : undefined
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useLogReview() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: LogReviewRequest) => logReview(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: diaryKeys.all })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteReview() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => deleteReview(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: diaryKeys.all })
|
||||
},
|
||||
})
|
||||
}
|
||||
95
spa/src/hooks/use-imports.ts
Normal file
95
spa/src/hooks/use-imports.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
applyMapping,
|
||||
confirmImport,
|
||||
createImportSession,
|
||||
deleteImportProfile,
|
||||
getImportPreview,
|
||||
getImportProfiles,
|
||||
getImportSession,
|
||||
saveImportProfile,
|
||||
} from "@/lib/api/imports"
|
||||
import type {
|
||||
ApplyMappingRequest,
|
||||
ConfirmRequest,
|
||||
SaveProfileRequest,
|
||||
} from "@/lib/api/imports"
|
||||
|
||||
export const importKeys = {
|
||||
session: (id: string) => ["import-session", id] as const,
|
||||
preview: (id: string) => ["import-preview", id] as const,
|
||||
profiles: ["import-profiles"] as const,
|
||||
}
|
||||
|
||||
export function useImportPreview(id: string) {
|
||||
return useQuery({
|
||||
queryKey: importKeys.preview(id),
|
||||
queryFn: () => getImportPreview(id),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export function useCreateImportSession() {
|
||||
return useMutation({
|
||||
mutationFn: (file: File) => createImportSession(file),
|
||||
})
|
||||
}
|
||||
|
||||
export function useImportSession(id: string) {
|
||||
return useQuery({
|
||||
queryKey: importKeys.session(id),
|
||||
queryFn: () => getImportSession(id),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export function useApplyMapping() {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
sessionId,
|
||||
data,
|
||||
}: {
|
||||
sessionId: string
|
||||
data: ApplyMappingRequest
|
||||
}) => applyMapping(sessionId, data),
|
||||
})
|
||||
}
|
||||
|
||||
export function useConfirmImport() {
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
sessionId,
|
||||
data,
|
||||
}: {
|
||||
sessionId: string
|
||||
data: ConfirmRequest
|
||||
}) => confirmImport(sessionId, data),
|
||||
})
|
||||
}
|
||||
|
||||
export function useImportProfiles() {
|
||||
return useQuery({
|
||||
queryKey: importKeys.profiles,
|
||||
queryFn: getImportProfiles,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSaveImportProfile() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: SaveProfileRequest) => saveImportProfile(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: importKeys.profiles })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteImportProfile() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => deleteImportProfile(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: importKeys.profiles })
|
||||
},
|
||||
})
|
||||
}
|
||||
19
spa/src/hooks/use-mobile.ts
Normal file
19
spa/src/hooks/use-mobile.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
58
spa/src/hooks/use-movies.ts
Normal file
58
spa/src/hooks/use-movies.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
getMovie,
|
||||
getMovieHistory,
|
||||
getMovieProfile,
|
||||
getMovies,
|
||||
syncPoster,
|
||||
} from "@/lib/api/movies"
|
||||
import type { MoviesQueryParams } from "@/lib/api/movies"
|
||||
|
||||
export const movieKeys = {
|
||||
all: ["movies"] as const,
|
||||
list: (params?: MoviesQueryParams) => [...movieKeys.all, params] as const,
|
||||
detail: (id: string) => [...movieKeys.all, id] as const,
|
||||
history: (id: string) => [...movieKeys.all, id, "history"] as const,
|
||||
profile: (id: string) => [...movieKeys.all, id, "profile"] as const,
|
||||
}
|
||||
|
||||
export function useMovies(params?: MoviesQueryParams) {
|
||||
return useQuery({
|
||||
queryKey: movieKeys.list(params),
|
||||
queryFn: () => getMovies(params),
|
||||
})
|
||||
}
|
||||
|
||||
export function useMovie(id: string) {
|
||||
return useQuery({
|
||||
queryKey: movieKeys.detail(id),
|
||||
queryFn: () => getMovie(id),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export function useMovieHistory(id: string) {
|
||||
return useQuery({
|
||||
queryKey: movieKeys.history(id),
|
||||
queryFn: () => getMovieHistory(id),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export function useMovieProfile(id: string) {
|
||||
return useQuery({
|
||||
queryKey: movieKeys.profile(id),
|
||||
queryFn: () => getMovieProfile(id),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export function useSyncPoster() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => syncPoster(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: movieKeys.all })
|
||||
},
|
||||
})
|
||||
}
|
||||
52
spa/src/hooks/use-search.ts
Normal file
52
spa/src/hooks/use-search.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"
|
||||
import { getPerson, getPersonCredits, search } from "@/lib/api/search"
|
||||
import type { SearchQueryParams } from "@/lib/api/search"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
176
spa/src/hooks/use-social.ts
Normal file
176
spa/src/hooks/use-social.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
acceptFollower,
|
||||
addBlockedDomain,
|
||||
blockActor,
|
||||
follow,
|
||||
getBlockedActors,
|
||||
getBlockedDomains,
|
||||
getFollowers,
|
||||
getFollowing,
|
||||
getPendingFollowers,
|
||||
getUserFollowers,
|
||||
getUserFollowing,
|
||||
rejectFollower,
|
||||
removeBlockedDomain,
|
||||
removeFollower,
|
||||
unblockActor,
|
||||
unfollow,
|
||||
} from "@/lib/api/social"
|
||||
import type {
|
||||
ActorUrlRequest,
|
||||
AddBlockedDomainRequest,
|
||||
FollowRequest,
|
||||
} from "@/lib/api/social"
|
||||
|
||||
export const socialKeys = {
|
||||
following: ["following"] as const,
|
||||
followers: ["followers"] as const,
|
||||
pending: ["followers-pending"] as const,
|
||||
userFollowing: (id: string) => ["following", id] as const,
|
||||
userFollowers: (id: string) => ["followers", id] as const,
|
||||
blockedDomains: ["blocked-domains"] as const,
|
||||
blockedActors: ["blocked-actors"] as const,
|
||||
}
|
||||
|
||||
export function useUserFollowing(userId: string) {
|
||||
return useQuery({
|
||||
queryKey: socialKeys.userFollowing(userId),
|
||||
queryFn: () => getUserFollowing(userId),
|
||||
enabled: !!userId,
|
||||
})
|
||||
}
|
||||
|
||||
export function useUserFollowers(userId: string) {
|
||||
return useQuery({
|
||||
queryKey: socialKeys.userFollowers(userId),
|
||||
queryFn: () => getUserFollowers(userId),
|
||||
enabled: !!userId,
|
||||
})
|
||||
}
|
||||
|
||||
export function useFollowing() {
|
||||
return useQuery({
|
||||
queryKey: socialKeys.following,
|
||||
queryFn: getFollowing,
|
||||
})
|
||||
}
|
||||
|
||||
export function useFollowers() {
|
||||
return useQuery({
|
||||
queryKey: socialKeys.followers,
|
||||
queryFn: getFollowers,
|
||||
})
|
||||
}
|
||||
|
||||
export function usePendingFollowers() {
|
||||
return useQuery({
|
||||
queryKey: socialKeys.pending,
|
||||
queryFn: getPendingFollowers,
|
||||
})
|
||||
}
|
||||
|
||||
export function useFollow() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: FollowRequest) => follow(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.following })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useUnfollow() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: FollowRequest) => unfollow(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.following })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useAcceptFollower() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: ActorUrlRequest) => acceptFollower(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.pending })
|
||||
qc.invalidateQueries({ queryKey: socialKeys.followers })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRejectFollower() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: ActorUrlRequest) => rejectFollower(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.pending })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRemoveFollower() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: ActorUrlRequest) => removeFollower(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.followers })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useBlockedDomains() {
|
||||
return useQuery({
|
||||
queryKey: socialKeys.blockedDomains,
|
||||
queryFn: getBlockedDomains,
|
||||
})
|
||||
}
|
||||
|
||||
export function useAddBlockedDomain() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: AddBlockedDomainRequest) => addBlockedDomain(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.blockedDomains })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRemoveBlockedDomain() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (domain: string) => removeBlockedDomain(domain),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.blockedDomains })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useBlockActor() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: ActorUrlRequest) => blockActor(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.blockedActors })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useUnblockActor() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: ActorUrlRequest) => unblockActor(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: socialKeys.blockedActors })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useBlockedActors() {
|
||||
return useQuery({
|
||||
queryKey: socialKeys.blockedActors,
|
||||
queryFn: getBlockedActors,
|
||||
})
|
||||
}
|
||||
63
spa/src/hooks/use-users.ts
Normal file
63
spa/src/hooks/use-users.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
getProfile,
|
||||
getUserProfile,
|
||||
getUsers,
|
||||
updateProfile,
|
||||
updateProfileFields,
|
||||
} from "@/lib/api/users"
|
||||
import type {
|
||||
UpdateProfileData,
|
||||
UpdateProfileFieldsRequest,
|
||||
UserProfileQueryParams,
|
||||
} from "@/lib/api/users"
|
||||
|
||||
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 })
|
||||
},
|
||||
})
|
||||
}
|
||||
69
spa/src/hooks/use-watchlist.ts
Normal file
69
spa/src/hooks/use-watchlist.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query"
|
||||
import {
|
||||
addToWatchlist,
|
||||
getWatchlist,
|
||||
getWatchlistStatus,
|
||||
removeFromWatchlist,
|
||||
} from "@/lib/api/watchlist"
|
||||
import type { AddToWatchlistRequest } from "@/lib/api/watchlist"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
export const watchlistKeys = {
|
||||
all: ["watchlist"] as const,
|
||||
list: () => [...watchlistKeys.all, "list"] as const,
|
||||
status: (movieId: string) => [...watchlistKeys.all, movieId] as const,
|
||||
}
|
||||
|
||||
export function useWatchlist() {
|
||||
return useQuery({
|
||||
queryKey: watchlistKeys.list(),
|
||||
queryFn: () => getWatchlist(),
|
||||
})
|
||||
}
|
||||
|
||||
export function useInfiniteWatchlist() {
|
||||
return useInfiniteQuery({
|
||||
queryKey: watchlistKeys.list(),
|
||||
queryFn: ({ pageParam = 0 }) =>
|
||||
getWatchlist({ limit: PAGE_SIZE, offset: pageParam }),
|
||||
initialPageParam: 0,
|
||||
getNextPageParam: (last) => {
|
||||
const next = last.offset + last.limit
|
||||
return next < last.total_count ? next : undefined
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useWatchlistStatus(movieId: string) {
|
||||
return useQuery({
|
||||
queryKey: watchlistKeys.status(movieId),
|
||||
queryFn: () => getWatchlistStatus(movieId),
|
||||
enabled: !!movieId,
|
||||
})
|
||||
}
|
||||
|
||||
export function useAddToWatchlist() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: AddToWatchlistRequest) => addToWatchlist(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: watchlistKeys.all })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useRemoveFromWatchlist() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (movieId: string) => removeFromWatchlist(movieId),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: watchlistKeys.all })
|
||||
},
|
||||
})
|
||||
}
|
||||
73
spa/src/hooks/use-webhooks.ts
Normal file
73
spa/src/hooks/use-webhooks.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
confirmWatch,
|
||||
deleteToken,
|
||||
dismissWatch,
|
||||
generateToken,
|
||||
getWatchQueue,
|
||||
getWebhookTokens,
|
||||
} from "@/lib/api/webhooks"
|
||||
import type {
|
||||
ConfirmWatchRequest,
|
||||
DismissWatchRequest,
|
||||
GenerateTokenRequest,
|
||||
} from "@/lib/api/webhooks"
|
||||
|
||||
export const webhookKeys = {
|
||||
tokens: ["webhook-tokens"] as const,
|
||||
queue: ["watch-queue"] as const,
|
||||
}
|
||||
|
||||
export function useWebhookTokens() {
|
||||
return useQuery({
|
||||
queryKey: webhookKeys.tokens,
|
||||
queryFn: getWebhookTokens,
|
||||
})
|
||||
}
|
||||
|
||||
export function useGenerateToken() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: GenerateTokenRequest) => generateToken(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: webhookKeys.tokens })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteToken() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => deleteToken(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: webhookKeys.tokens })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useWatchQueue() {
|
||||
return useQuery({
|
||||
queryKey: webhookKeys.queue,
|
||||
queryFn: getWatchQueue,
|
||||
})
|
||||
}
|
||||
|
||||
export function useConfirmWatch() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: ConfirmWatchRequest) => confirmWatch(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: webhookKeys.queue })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDismissWatch() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: DismissWatchRequest) => dismissWatch(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: webhookKeys.queue })
|
||||
},
|
||||
})
|
||||
}
|
||||
59
spa/src/hooks/use-wrapup.ts
Normal file
59
spa/src/hooks/use-wrapup.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import {
|
||||
deleteWrapUp,
|
||||
generateWrapUp,
|
||||
getWrapUp,
|
||||
getWrapUpReport,
|
||||
getWrapUps,
|
||||
} from "@/lib/api/wrapup"
|
||||
import type { GenerateWrapUpRequest } from "@/lib/api/wrapup"
|
||||
|
||||
export const wrapupKeys = {
|
||||
all: ["wrapups"] as const,
|
||||
list: () => [...wrapupKeys.all, "list"] as const,
|
||||
detail: (id: string) => [...wrapupKeys.all, id] as const,
|
||||
report: (id: string) => [...wrapupKeys.all, id, "report"] as const,
|
||||
}
|
||||
|
||||
export function useWrapUpReport(id: string) {
|
||||
return useQuery({
|
||||
queryKey: wrapupKeys.report(id),
|
||||
queryFn: () => getWrapUpReport(id),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export function useWrapUps() {
|
||||
return useQuery({
|
||||
queryKey: wrapupKeys.list(),
|
||||
queryFn: getWrapUps,
|
||||
})
|
||||
}
|
||||
|
||||
export function useWrapUp(id: string) {
|
||||
return useQuery({
|
||||
queryKey: wrapupKeys.detail(id),
|
||||
queryFn: () => getWrapUp(id),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export function useGenerateWrapUp() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (data: GenerateWrapUpRequest) => generateWrapUp(data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: wrapupKeys.all })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useDeleteWrapUp() {
|
||||
const qc = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => deleteWrapUp(id),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: wrapupKeys.all })
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user