import { useState, type FormEvent } from 'react' import { Modal } from '@/components/ui/Modal' import { Alert } from '@/components/ui/Alert' import { Button } from '@/components/ui/Button' import { Select } from '@/components/ui/Select' import { TextField } from '@/components/ui/TextField' import { cn } from '@/lib/cn' import { fieldControlClass } from '@/components/ui/field' import { errorMessage } from '@/lib/api' import type { Garden } from '@/lib/gardens' import { useAddShare, useDisableShareLink, useEnableShareLink, useRemoveShare, useShareLink, useShares, useUpdateShareRole, type ShareRole, } from '@/lib/shares' const roleOptions = [ { value: 'viewer', label: 'Viewer (read-only)' }, { value: 'editor', label: 'Editor (can edit)' }, ] /** * Owner-only dialog to manage a garden's shares: invite an existing user by * email as viewer/editor, change a share's role, or remove it. Targets existing * accounts only (v1 has no invitation emails) — an unknown email surfaces a * friendly "no account with that email". */ export function ShareGardenModal({ garden, onClose }: { garden: Garden; onClose: () => void }) { const shares = useShares(garden.id) const add = useAddShare(garden.id) const updateRole = useUpdateShareRole(garden.id) const remove = useRemoveShare(garden.id) const [email, setEmail] = useState('') const [role, setRole] = useState('viewer') const [error, setError] = useState(null) async function onInvite(e: FormEvent) { e.preventDefault() setError(null) if (!email.trim()) { setError('Enter an email address.') return } try { await add.mutateAsync({ email: email.trim(), role }) setEmail('') } catch (err) { setError(errorMessage(err, 'Could not share the garden.')) } } const onMutationError = (fallback: string) => (err: unknown) => setError(errorMessage(err, fallback)) return (
setEmail(e.target.value)} />
{ setError(null) updateRole.mutate( { userId: sh.userId, role: e.target.value as ShareRole }, { onError: onMutationError('Could not change that role.') }, ) }} aria-label={`Role for ${sh.displayName}`} className={cn(fieldControlClass, 'w-auto px-2 py-1 text-sm')} > ))}
) } /** The public read-only link controls: create, copy, regenerate, turn off. */ function PublicLinkSection({ gardenId }: { gardenId: number }) { const link = useShareLink(gardenId) const enable = useEnableShareLink(gardenId) const disable = useDisableShareLink(gardenId) const [copied, setCopied] = useState(false) const [error, setError] = useState(null) const token = link.data?.enabled ? link.data.token : undefined const url = token ? `${window.location.origin}/g/${token}` : '' const busy = link.isPending || enable.isPending || disable.isPending const run = (p: Promise, fallback: string) => { setError(null) p.catch((err) => setError(errorMessage(err, fallback))) } async function copy() { if (!url) return try { await navigator.clipboard.writeText(url) setCopied(true) window.setTimeout(() => setCopied(false), 1500) } catch { // Clipboard API may be unavailable (e.g. non-secure context); the field is // selectable so the user can still copy manually. } } return (

Public link

Anyone with the link can view this garden read-only — no account needed.

{link.isError && Could not load the public link.} {error && {error}} {link.isSuccess && !link.data.enabled && ( )} {link.isSuccess && link.data.enabled && (
e.currentTarget.select()} aria-label="Public link URL" className={cn(fieldControlClass, 'min-w-0 flex-1 text-sm')} />
)}
) }