feat(frontend): add react spa with wishlist flows and public profile
This commit is contained in:
41
apps/frontend/src/features/wishes/wishes.api.ts
Normal file
41
apps/frontend/src/features/wishes/wishes.api.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
CreateWishInput,
|
||||
UpdateWishInput,
|
||||
Wish,
|
||||
WishStatus,
|
||||
} from '@family-wishlist/shared';
|
||||
import { api } from '@/lib/api';
|
||||
|
||||
export type WishWithOwnerBadge = Wish & { isNewForOwner?: boolean };
|
||||
|
||||
export type OwnerStatus = 'active' | 'archived' | 'completed' | 'deleted';
|
||||
|
||||
export const wishesApi = {
|
||||
list: (status: OwnerStatus) =>
|
||||
api.get<WishWithOwnerBadge[]>(`/api/wishes?status=${status}`),
|
||||
|
||||
get: (id: string) => api.get<Wish>(`/api/wishes/${id}`),
|
||||
|
||||
create: (input: CreateWishInput) => api.post<Wish, CreateWishInput>('/api/wishes', input),
|
||||
|
||||
update: (id: string, input: UpdateWishInput) =>
|
||||
api.patch<Wish, UpdateWishInput>(`/api/wishes/${id}`, input),
|
||||
|
||||
remove: (id: string) => api.delete<Wish>(`/api/wishes/${id}`),
|
||||
archive: (id: string) => api.post<Wish>(`/api/wishes/${id}/archive`),
|
||||
complete: (id: string) => api.post<Wish>(`/api/wishes/${id}/complete`),
|
||||
restore: (id: string) => api.post<Wish>(`/api/wishes/${id}/restore`),
|
||||
duplicate: (id: string) => api.post<Wish>(`/api/wishes/${id}/duplicate`),
|
||||
|
||||
uploadImage: (id: string, file: File) => {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file, file.name);
|
||||
return api.upload<Wish>(`/api/wishes/${id}/image`, fd);
|
||||
},
|
||||
refreshOg: (id: string) => api.post<Wish>(`/api/wishes/${id}/image/refresh-og`),
|
||||
deleteImage: (id: string) => api.delete<Wish>(`/api/wishes/${id}/image`),
|
||||
};
|
||||
|
||||
export function statusToQuery(s: WishStatus): OwnerStatus {
|
||||
return s.toLowerCase() as OwnerStatus;
|
||||
}
|
||||
145
apps/frontend/src/features/wishes/wishes.hooks.ts
Normal file
145
apps/frontend/src/features/wishes/wishes.hooks.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { CreateWishInput, UpdateWishInput } from '@family-wishlist/shared';
|
||||
import { toast } from 'sonner';
|
||||
import { wishesApi, type OwnerStatus } from './wishes.api';
|
||||
import { ApiError } from '@/lib/api';
|
||||
|
||||
const LIST_KEY = (status: OwnerStatus) => ['wishes', status] as const;
|
||||
|
||||
function invalidateAll(client: ReturnType<typeof useQueryClient>): void {
|
||||
void client.invalidateQueries({ queryKey: ['wishes'] });
|
||||
}
|
||||
|
||||
function toastError(err: unknown, fallback = 'Something went wrong'): void {
|
||||
if (err instanceof ApiError) toast.error(err.message);
|
||||
else toast.error(fallback);
|
||||
}
|
||||
|
||||
export function useWishes(status: OwnerStatus) {
|
||||
return useQuery({
|
||||
queryKey: LIST_KEY(status),
|
||||
queryFn: () => wishesApi.list(status),
|
||||
staleTime: 10_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateWish() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (input: CreateWishInput) => wishesApi.create(input),
|
||||
onSuccess: () => {
|
||||
invalidateAll(client);
|
||||
toast.success('Wish added');
|
||||
},
|
||||
onError: (err) => toastError(err),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateWish() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (vars: { id: string; input: UpdateWishInput }) =>
|
||||
wishesApi.update(vars.id, vars.input),
|
||||
onSuccess: () => {
|
||||
invalidateAll(client);
|
||||
toast.success('Saved');
|
||||
},
|
||||
onError: (err) => toastError(err),
|
||||
});
|
||||
}
|
||||
|
||||
export function useArchiveWish() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => wishesApi.archive(id),
|
||||
onSuccess: () => {
|
||||
invalidateAll(client);
|
||||
toast.success('Moved to archive');
|
||||
},
|
||||
onError: (err) => toastError(err),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCompleteWish() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => wishesApi.complete(id),
|
||||
onSuccess: () => {
|
||||
invalidateAll(client);
|
||||
toast.success('Marked as fulfilled');
|
||||
},
|
||||
onError: (err) => toastError(err),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteWish() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => wishesApi.remove(id),
|
||||
onSuccess: () => {
|
||||
invalidateAll(client);
|
||||
toast.success('Moved to trash (30 days to restore)');
|
||||
},
|
||||
onError: (err) => toastError(err),
|
||||
});
|
||||
}
|
||||
|
||||
export function useRestoreWish() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => wishesApi.restore(id),
|
||||
onSuccess: () => {
|
||||
invalidateAll(client);
|
||||
toast.success('Restored');
|
||||
},
|
||||
onError: (err) => toastError(err),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDuplicateWish() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => wishesApi.duplicate(id),
|
||||
onSuccess: () => {
|
||||
invalidateAll(client);
|
||||
toast.success('New wish created from the fulfilled one');
|
||||
},
|
||||
onError: (err) => toastError(err),
|
||||
});
|
||||
}
|
||||
|
||||
export function useUploadWishImage() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (vars: { id: string; file: File }) => wishesApi.uploadImage(vars.id, vars.file),
|
||||
onSuccess: () => {
|
||||
invalidateAll(client);
|
||||
toast.success('Image updated');
|
||||
},
|
||||
onError: (err) => toastError(err),
|
||||
});
|
||||
}
|
||||
|
||||
export function useRefreshOg() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => wishesApi.refreshOg(id),
|
||||
onSuccess: () => {
|
||||
invalidateAll(client);
|
||||
toast.success('Image refreshed from link');
|
||||
},
|
||||
onError: (err) => toastError(err, 'Could not fetch image from link'),
|
||||
});
|
||||
}
|
||||
|
||||
export function useResetWishImage() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => wishesApi.deleteImage(id),
|
||||
onSuccess: () => {
|
||||
invalidateAll(client);
|
||||
toast.success('Reset to default image');
|
||||
},
|
||||
onError: (err) => toastError(err),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user