diff --git a/app/(app)/equipment/equipment-browse-client.tsx b/app/(app)/equipment/equipment-browse-client.tsx new file mode 100644 index 0000000..46c7e2b --- /dev/null +++ b/app/(app)/equipment/equipment-browse-client.tsx @@ -0,0 +1,134 @@ +'use client' + +import { useState } from 'react' +import { toast } from 'sonner' +import { Hammer } from 'lucide-react' +import { PageHeader, PageTitle } from '@/components/ui/page-title' +import { Badge } from '@/components/ui/badge' +import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty' +import { reserveEquipment } from '@/lib/actions' +import { EQUIPMENT_STATUS_LABEL } from '@/lib/equipment-logic' +import { useRouter } from 'next/navigation' + +type Item = { + id: string + name: string + description: string | null + location: string | null + status: string + required_certification_id: string | null + certifications: { name: string } | { name: string }[] | null + member_certified: boolean +} + +function certName(i: Item): string | null { + const c = Array.isArray(i.certifications) ? i.certifications[0] : i.certifications + return c?.name ?? null +} + +export function EquipmentBrowseClient({ equipment }: { equipment: unknown[] }) { + const router = useRouter() + const items = equipment as Item[] + const [openId, setOpenId] = useState(null) + const [form, setForm] = useState({ starts_at: '', ends_at: '', notes: '' }) + const [busy, setBusy] = useState(false) + + async function onReserve(e: React.FormEvent, item: Item) { + e.preventDefault() + if (!form.starts_at || !form.ends_at) return toast.error('Pick a start and end time') + setBusy(true) + const res = await reserveEquipment({ + equipmentId: item.id, + starts_at: form.starts_at, + ends_at: form.ends_at, + notes: form.notes.trim() || null, + }) + setBusy(false) + if ('error' in res && res.error) return toast.error(res.error) + toast.success('Reserved') + setOpenId(null) + setForm({ starts_at: '', ends_at: '', notes: '' }) + router.refresh() + } + + return ( + <> + + Equipment + + +
+ {items.length === 0 ? ( + + + + No equipment listed + Nothing is available to reserve yet. + + + ) : ( + + )} +
+ + ) +} diff --git a/app/(app)/equipment/page.tsx b/app/(app)/equipment/page.tsx new file mode 100644 index 0000000..43e5ab3 --- /dev/null +++ b/app/(app)/equipment/page.tsx @@ -0,0 +1,27 @@ +import { createClient } from '@/lib/supabase/server' +import { redirect } from 'next/navigation' +import { listEquipmentForMembers } from '@/lib/actions' +import { EquipmentBrowseClient } from './equipment-browse-client' + +export const dynamic = 'force-dynamic' + +export default async function EquipmentPage() { + const supabase = await createClient() + const { + data: { user }, + } = await supabase.auth.getUser() + if (!user) redirect('/login') + + const { data: member } = await supabase + .from('space_members') + .select('space_id') + .eq('user_id', user.id) + .in('status', ['current', 'unverified', 'late']) + .maybeSingle() + if (!member) redirect('/signup') + + const res = await listEquipmentForMembers() + const equipment = 'data' in res ? res.data : [] + + return +} diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 0004645..457e6fd 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -136,6 +136,7 @@ export function AppSidebar({ member, roleName, taskBadge = 0, commsBadge = 0, pa + {isAdmin && (