|
1 | 1 | import Link from "next/link"; |
2 | 2 | import { createClient } from "@/lib/supabase/server"; |
3 | | -import { CalendarDays, Clock, ArrowRight } from "lucide-react"; |
| 3 | +import { CalendarDays, Clock, ArrowRight, Users } from "lucide-react"; |
4 | 4 |
|
5 | 5 | export default async function BookPage() { |
6 | 6 | const supabase = await createClient(); |
@@ -28,59 +28,155 @@ export default async function BookPage() { |
28 | 28 |
|
29 | 29 | const deskRow = deskAvail?.[0]; |
30 | 30 |
|
| 31 | + // Fetch non-desk bookable resources for inline display |
| 32 | + const { data: rooms } = spaceId |
| 33 | + ? await supabase |
| 34 | + .from("resources") |
| 35 | + .select( |
| 36 | + "id, name, capacity, floor, image_url, resource_type_id, resource_type:resource_types!inner(id, slug, name, bookable)", |
| 37 | + ) |
| 38 | + .eq("space_id", spaceId) |
| 39 | + .eq("resource_types.bookable", true) |
| 40 | + .neq("resource_types.slug", "desk") |
| 41 | + .eq("status", "available") |
| 42 | + .order("sort_order", { ascending: true }) |
| 43 | + : { data: null }; |
| 44 | + |
| 45 | + // Fetch rates for display |
| 46 | + const { data: rates } = spaceId |
| 47 | + ? await supabase |
| 48 | + .from("rate_config") |
| 49 | + .select("resource_type_id, rate_cents, currency") |
| 50 | + .eq("space_id", spaceId) |
| 51 | + : { data: null }; |
| 52 | + |
| 53 | + const rateMap = new Map( |
| 54 | + (rates ?? []).map((r) => [r.resource_type_id, r]), |
| 55 | + ); |
| 56 | + |
| 57 | + // Group rooms by resource type |
| 58 | + const roomsByType = new Map<string, typeof rooms>(); |
| 59 | + for (const room of rooms ?? []) { |
| 60 | + const rtId = room.resource_type_id; |
| 61 | + if (!roomsByType.has(rtId)) roomsByType.set(rtId, []); |
| 62 | + roomsByType.get(rtId)!.push(room); |
| 63 | + } |
| 64 | + |
| 65 | + const deskType = (resourceTypes ?? []).find((rt) => rt.slug === "desk"); |
| 66 | + const nonDeskTypes = (resourceTypes ?? []).filter((rt) => rt.slug !== "desk"); |
| 67 | + |
31 | 68 | return ( |
32 | 69 | <div> |
33 | 70 | <h1 className="text-2xl font-semibold tracking-tight">Book a Resource</h1> |
34 | 71 | <p className="mt-1 text-sm text-muted-foreground"> |
35 | | - Select a resource type to make a booking. |
| 72 | + Select a resource to make a booking. |
36 | 73 | </p> |
37 | 74 |
|
38 | | - <div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> |
39 | | - {(resourceTypes ?? []).map((rt) => { |
40 | | - const isDesk = rt.slug === "desk"; |
41 | | - const href = isDesk ? "/book/desk" : "/book/room"; |
42 | | - const count = (rt.resources as unknown as { count: number }[])?.[0]?.count ?? 0; |
43 | | - |
44 | | - return ( |
45 | | - <Link |
46 | | - key={rt.id} |
47 | | - href={href} |
48 | | - className="group relative flex flex-col rounded-xl border border-border bg-card p-6 transition-colors hover:border-primary/30 hover:bg-accent/50" |
49 | | - > |
50 | | - <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10"> |
51 | | - {isDesk ? ( |
52 | | - <CalendarDays className="h-5 w-5 text-primary" /> |
53 | | - ) : ( |
54 | | - <Clock className="h-5 w-5 text-primary" /> |
55 | | - )} |
56 | | - </div> |
| 75 | + {/* Desk card */} |
| 76 | + {deskType && ( |
| 77 | + <div className="mt-8"> |
| 78 | + <Link |
| 79 | + href="/book/desk" |
| 80 | + className="group flex flex-col rounded-xl border border-border bg-card p-6 transition-colors hover:border-primary/30 hover:bg-accent/50 sm:max-w-sm" |
| 81 | + > |
| 82 | + <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10"> |
| 83 | + <CalendarDays className="h-5 w-5 text-primary" /> |
| 84 | + </div> |
| 85 | + <h3 className="mt-4 text-lg font-semibold">{deskType.name}</h3> |
| 86 | + <p className="mt-1 text-sm text-muted-foreground"> |
| 87 | + {deskRow ? ( |
| 88 | + <> |
| 89 | + {deskRow.available_desks}/{deskRow.total_desks} available today |
| 90 | + </> |
| 91 | + ) : ( |
| 92 | + <> |
| 93 | + {(deskType.resources as unknown as { count: number }[])?.[0]?.count ?? 0} desks |
| 94 | + </> |
| 95 | + )} |
| 96 | + </p> |
| 97 | + <div className="mt-4 flex items-center text-sm font-medium text-primary"> |
| 98 | + Book a Desk |
| 99 | + <ArrowRight className="ml-1.5 h-4 w-4 transition-transform group-hover:translate-x-0.5" /> |
| 100 | + </div> |
| 101 | + </Link> |
| 102 | + </div> |
| 103 | + )} |
57 | 104 |
|
58 | | - <h3 className="mt-4 text-lg font-semibold">{rt.name}</h3> |
59 | | - |
60 | | - <p className="mt-1 text-sm text-muted-foreground"> |
61 | | - {isDesk ? ( |
62 | | - deskRow ? ( |
63 | | - <> |
64 | | - {deskRow.available_desks}/{deskRow.total_desks} available today |
65 | | - </> |
66 | | - ) : ( |
67 | | - <>{count} desks</> |
68 | | - ) |
69 | | - ) : ( |
70 | | - <> |
71 | | - {count} {count === 1 ? "room" : "rooms"} |
72 | | - </> |
73 | | - )} |
74 | | - </p> |
| 105 | + {/* Non-desk types with individual rooms */} |
| 106 | + {nonDeskTypes.map((rt) => { |
| 107 | + const typeRooms = roomsByType.get(rt.id) ?? []; |
| 108 | + const rate = rateMap.get(rt.id); |
| 109 | + const pricePerHour = rate |
| 110 | + ? new Intl.NumberFormat("en", { |
| 111 | + style: "currency", |
| 112 | + currency: (rate.currency ?? "eur").toUpperCase(), |
| 113 | + minimumFractionDigits: 0, |
| 114 | + maximumFractionDigits: 2, |
| 115 | + }).format(rate.rate_cents / 100) |
| 116 | + : null; |
| 117 | + |
| 118 | + return ( |
| 119 | + <div key={rt.id} className="mt-8"> |
| 120 | + <h2 className="text-lg font-semibold">{rt.name}</h2> |
| 121 | + |
| 122 | + {typeRooms.length > 0 ? ( |
| 123 | + <div className="mt-3 grid gap-4 sm:grid-cols-2"> |
| 124 | + {typeRooms.map((room) => ( |
| 125 | + <Link |
| 126 | + key={room.id} |
| 127 | + href={`/book/room/${room.id}`} |
| 128 | + className="group flex flex-col overflow-hidden rounded-xl border border-border bg-card transition-colors hover:border-primary/30 hover:bg-accent/50" |
| 129 | + > |
| 130 | + {room.image_url ? ( |
| 131 | + /* eslint-disable-next-line @next/next/no-img-element */ |
| 132 | + <img |
| 133 | + src={room.image_url} |
| 134 | + alt={room.name} |
| 135 | + className="aspect-[16/9] w-full object-cover" |
| 136 | + /> |
| 137 | + ) : ( |
| 138 | + <div className="flex aspect-[16/9] w-full items-center justify-center bg-muted"> |
| 139 | + <Clock className="h-8 w-8 text-muted-foreground/50" /> |
| 140 | + </div> |
| 141 | + )} |
75 | 142 |
|
76 | | - <div className="mt-4 flex items-center text-sm font-medium text-primary"> |
77 | | - {isDesk ? "Book a Desk" : "Book a Room"} |
78 | | - <ArrowRight className="ml-1.5 h-4 w-4 transition-transform group-hover:translate-x-0.5" /> |
| 143 | + <div className="p-6"> |
| 144 | + <h3 className="text-lg font-semibold">{room.name}</h3> |
| 145 | + |
| 146 | + <div className="mt-1.5 flex flex-wrap items-center gap-2 text-sm text-muted-foreground"> |
| 147 | + {room.capacity && ( |
| 148 | + <span className="inline-flex items-center gap-1"> |
| 149 | + <Users className="h-3.5 w-3.5" /> |
| 150 | + Capacity: {room.capacity} |
| 151 | + </span> |
| 152 | + )} |
| 153 | + {room.floor !== null && room.floor !== undefined && ( |
| 154 | + <span>· Floor {room.floor}</span> |
| 155 | + )} |
| 156 | + </div> |
| 157 | + |
| 158 | + {pricePerHour && ( |
| 159 | + <p className="mt-1 text-sm text-muted-foreground"> |
| 160 | + {pricePerHour}/hr or use credits |
| 161 | + </p> |
| 162 | + )} |
| 163 | + |
| 164 | + <div className="mt-4 flex items-center text-sm font-medium text-primary"> |
| 165 | + View Availability |
| 166 | + <ArrowRight className="ml-1.5 h-4 w-4 transition-transform group-hover:translate-x-0.5" /> |
| 167 | + </div> |
| 168 | + </div> |
| 169 | + </Link> |
| 170 | + ))} |
79 | 171 | </div> |
80 | | - </Link> |
81 | | - ); |
82 | | - })} |
83 | | - </div> |
| 172 | + ) : ( |
| 173 | + <p className="mt-3 text-sm text-muted-foreground"> |
| 174 | + No rooms available |
| 175 | + </p> |
| 176 | + )} |
| 177 | + </div> |
| 178 | + ); |
| 179 | + })} |
84 | 180 |
|
85 | 181 | {(resourceTypes ?? []).length === 0 && ( |
86 | 182 | <div className="mt-8 flex flex-col items-center rounded-xl border border-dashed border-border bg-card px-6 py-14 text-center"> |
|
0 commit comments