// Garden sharing data layer: zod shapes for /gardens/:id/shares plus the // react-query hooks the share dialog and "leave garden" action use. import { queryOptions, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { z } from 'zod' import { api } from './api' export const shareRoleSchema = z.enum(['viewer', 'editor']) export type ShareRole = z.infer export const shareSchema = z.object({ id: z.number(), gardenId: z.number(), userId: z.number(), role: shareRoleSchema, createdBy: z.number(), version: z.number(), createdAt: z.string(), updatedAt: z.string(), email: z.string(), displayName: z.string(), }) export type Share = z.infer const sharesKey = (gardenId: number) => ['shares', gardenId] as const export function sharesQueryOptions(gardenId: number) { return queryOptions({ queryKey: sharesKey(gardenId), queryFn: async (): Promise => z.array(shareSchema).parse(await api.get(`/gardens/${gardenId}/shares`)), }) } /** The shares on a garden (owner-only endpoint). */ export function useShares(gardenId: number) { return useQuery(sharesQueryOptions(gardenId)) } // The add/update responses are a bare GardenShare (no email/displayName), so we // don't parse them into the list's Share shape; onSuccess refetches the list. export function useAddShare(gardenId: number) { const qc = useQueryClient() return useMutation({ mutationFn: (input: { email: string; role: ShareRole }) => api.post(`/gardens/${gardenId}/shares`, input), onSuccess: () => qc.invalidateQueries({ queryKey: sharesKey(gardenId) }), }) } export function useUpdateShareRole(gardenId: number) { const qc = useQueryClient() return useMutation({ mutationFn: ({ userId, role }: { userId: number; role: ShareRole }) => api.patch(`/gardens/${gardenId}/shares/${userId}`, { role }), onSuccess: () => qc.invalidateQueries({ queryKey: sharesKey(gardenId) }), }) } /** Remove a share. Used both by the owner (revoke) and by a recipient leaving a * garden (their own userId). Refreshes the garden list too, since a leave * changes what the current user can see. */ export function useRemoveShare(gardenId: number) { const qc = useQueryClient() return useMutation({ mutationFn: (userId: number) => api.delete(`/gardens/${gardenId}/shares/${userId}`), onSuccess: () => { qc.invalidateQueries({ queryKey: sharesKey(gardenId) }) qc.invalidateQueries({ queryKey: ['gardens'] }) }, }) } // --- public share link (owner-only management) ----------------------------- // The read-only public link that anyone (no account) can open. token is present // only when enabled; the page builds the URL as `${origin}/g/${token}`. export const shareLinkSchema = z.object({ enabled: z.boolean(), token: z.string().optional(), }) export type ShareLinkState = z.infer const shareLinkKey = (gardenId: number) => ['share-link', gardenId] as const export function shareLinkQueryOptions(gardenId: number) { return queryOptions({ queryKey: shareLinkKey(gardenId), queryFn: async (): Promise => shareLinkSchema.parse(await api.get(`/gardens/${gardenId}/share-link`)), }) } /** The garden's public-link state (owner-only endpoint). */ export function useShareLink(gardenId: number) { return useQuery(shareLinkQueryOptions(gardenId)) } /** Enable the public link, or with { rotate: true } issue a fresh token that * invalidates the old URL. Writes the returned state straight into the cache. */ export function useEnableShareLink(gardenId: number) { const qc = useQueryClient() return useMutation({ mutationFn: async ({ rotate = false }: { rotate?: boolean }): Promise => shareLinkSchema.parse(await api.post(`/gardens/${gardenId}/share-link`, { rotate })), onSuccess: (state) => qc.setQueryData(shareLinkKey(gardenId), state), }) } /** Disable the public link. */ export function useDisableShareLink(gardenId: number) { const qc = useQueryClient() return useMutation({ mutationFn: async (): Promise => shareLinkSchema.parse(await api.delete(`/gardens/${gardenId}/share-link`)), onSuccess: (state) => qc.setQueryData(shareLinkKey(gardenId), state), }) }