Skip to content

Commit 6a4e07b

Browse files
feat: redesign guest list table and extract filter popover
1 parent f2c2774 commit 6a4e07b

File tree

6 files changed

+315
-124
lines changed

6 files changed

+315
-124
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { Filter } from "lucide-react";
2+
import { useState } from "react";
3+
import { Button } from "@/components/ui/Button";
4+
import { cn } from "@/lib/utils";
5+
6+
type GuestFilterPopoverProps = {
7+
availableFloors: Array<number>;
8+
availableGroupSizes: Array<number>;
9+
selectedFloor: string;
10+
selectedGroupSize: string;
11+
onApply: (floor: string, groupSize: string) => void;
12+
};
13+
14+
function FilterChip({
15+
label,
16+
selected,
17+
onClick,
18+
}: {
19+
label: string;
20+
selected: boolean;
21+
onClick: () => void;
22+
}) {
23+
return (
24+
<button
25+
type="button"
26+
onClick={onClick}
27+
className={cn(
28+
"rounded-lg border px-4 py-1.5 text-sm transition-colors",
29+
selected
30+
? "border-primary bg-primary/10 text-primary"
31+
: "border-stroke-subtle bg-white text-text-default hover:border-primary",
32+
)}
33+
>
34+
{label}
35+
</button>
36+
);
37+
}
38+
39+
export function GuestFilterPopover({
40+
availableFloors,
41+
availableGroupSizes,
42+
selectedFloor,
43+
selectedGroupSize,
44+
onApply,
45+
}: GuestFilterPopoverProps) {
46+
const [open, setOpen] = useState(false);
47+
const [pendingFloor, setPendingFloor] = useState(selectedFloor);
48+
const [pendingGroupSize, setPendingGroupSize] = useState(selectedGroupSize);
49+
50+
const handleOpen = () => {
51+
setPendingFloor(selectedFloor);
52+
setPendingGroupSize(selectedGroupSize);
53+
setOpen(true);
54+
};
55+
56+
const handleCancel = () => {
57+
setOpen(false);
58+
};
59+
60+
const handleApply = () => {
61+
onApply(pendingFloor, pendingGroupSize);
62+
setOpen(false);
63+
};
64+
65+
const handleReset = () => {
66+
setPendingFloor("all");
67+
setPendingGroupSize("all");
68+
};
69+
70+
const activeFilterCount =
71+
(selectedFloor !== "all" ? 1 : 0) + (selectedGroupSize !== "all" ? 1 : 0);
72+
73+
return (
74+
<div className="relative shrink-0">
75+
<button
76+
type="button"
77+
onClick={open ? handleCancel : handleOpen}
78+
className={cn(
79+
"flex h-11 items-center gap-2 rounded-lg border px-4 text-sm font-medium transition-colors",
80+
activeFilterCount > 0
81+
? "border-primary bg-primary/5 text-primary"
82+
: "border-stroke-subtle text-text-default hover:bg-primary/5",
83+
)}
84+
>
85+
<Filter className="h-4 w-4" />
86+
Filter
87+
{activeFilterCount > 0 && (
88+
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-primary text-xs text-white">
89+
{activeFilterCount}
90+
</span>
91+
)}
92+
</button>
93+
94+
{open && (
95+
<div className="absolute right-0 top-[calc(100%+0.5rem)] z-50 flex w-80 flex-col overflow-hidden rounded-2xl bg-white px-6 shadow-xl">
96+
<div className="flex items-center justify-between pt-5">
97+
<span className="text-base font-medium text-text-default">
98+
Filters
99+
</span>
100+
<button
101+
type="button"
102+
onClick={handleReset}
103+
className="text-sm text-text-subtle transition-colors hover:text-text-default"
104+
>
105+
Reset
106+
</button>
107+
</div>
108+
109+
<div className="flex flex-col gap-5 py-5">
110+
{availableFloors.length > 0 && (
111+
<section className="flex flex-col gap-3">
112+
<h3 className="text-sm font-medium text-text-default">Floor</h3>
113+
<div className="flex flex-wrap gap-2">
114+
{availableFloors.map((floor) => (
115+
<FilterChip
116+
key={floor}
117+
label={`Floor ${floor}`}
118+
selected={pendingFloor === String(floor)}
119+
onClick={() =>
120+
setPendingFloor(
121+
pendingFloor === String(floor)
122+
? "all"
123+
: String(floor),
124+
)
125+
}
126+
/>
127+
))}
128+
</div>
129+
</section>
130+
)}
131+
132+
{availableGroupSizes.length > 0 && (
133+
<section className="flex flex-col gap-3">
134+
<h3 className="text-sm font-medium text-text-default">
135+
Group Size
136+
</h3>
137+
<div className="flex flex-wrap gap-2">
138+
{availableGroupSizes.map((size) => (
139+
<FilterChip
140+
key={size}
141+
label={String(size)}
142+
selected={pendingGroupSize === String(size)}
143+
onClick={() =>
144+
setPendingGroupSize(
145+
pendingGroupSize === String(size)
146+
? "all"
147+
: String(size),
148+
)
149+
}
150+
/>
151+
))}
152+
</div>
153+
</section>
154+
)}
155+
</div>
156+
157+
<div className="border-t border-stroke-subtle" />
158+
<div className="flex justify-end gap-3 py-4">
159+
<Button variant="secondary" onClick={handleCancel}>
160+
Cancel
161+
</Button>
162+
<Button variant="primary" onClick={handleApply}>
163+
Apply
164+
</Button>
165+
</div>
166+
</div>
167+
)}
168+
</div>
169+
);
170+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { GuestFilterPopover } from "./GuestFilterPopover";
2+
import { GuestSearchBar } from "./GuestSearchBar";
3+
4+
type GuestListHeaderProps = {
5+
searchTerm: string;
6+
onSearchChange: (value: string) => void;
7+
availableFloors: Array<number>;
8+
availableGroupSizes: Array<number>;
9+
selectedFloor: string;
10+
selectedGroupSize: string;
11+
onApplyFilters: (floor: string, groupSize: string) => void;
12+
};
13+
14+
export function GuestListHeader({
15+
searchTerm,
16+
onSearchChange,
17+
availableFloors,
18+
availableGroupSizes,
19+
selectedFloor,
20+
selectedGroupSize,
21+
onApplyFilters,
22+
}: GuestListHeaderProps) {
23+
return (
24+
<div className="flex items-center gap-3">
25+
<div className="min-w-0 flex-1">
26+
<GuestSearchBar value={searchTerm} onChange={onSearchChange} />
27+
</div>
28+
<GuestFilterPopover
29+
availableFloors={availableFloors}
30+
availableGroupSizes={availableGroupSizes}
31+
selectedFloor={selectedFloor}
32+
selectedGroupSize={selectedGroupSize}
33+
onApply={onApplyFilters}
34+
/>
35+
</div>
36+
);
37+
}

clients/web/src/components/guests/GuestQuickListTable.tsx

Lines changed: 37 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,60 @@
1-
import { UserRound } from "lucide-react";
21
import type { GuestWithBooking } from "@shared";
32

43
type GuestQuickListTableProps = {
54
guests: Array<GuestWithBooking>;
6-
floorOptions: Array<number>;
7-
groupSizeOptions: Array<number>;
8-
groupFilter: string;
9-
floorFilter: string;
105
isLoading?: boolean;
11-
onGroupFilterChange: (value: string) => void;
12-
onFloorFilterChange: (value: string) => void;
136
onGuestClick: (guestId: string) => void;
147
};
158

16-
function avatarPill() {
17-
return (
18-
<div className="flex h-[2vw] w-[2vw] items-center justify-center rounded-full border border-black">
19-
<UserRound className="h-[2vh] w-[2vh] text-black" />
20-
</div>
21-
);
22-
}
9+
const COL_CLASSES =
10+
"grid-cols-[minmax(0,3fr)_minmax(0,2fr)_minmax(0,5fr)_minmax(5rem,1fr)]";
2311

2412
export function GuestQuickListTable({
2513
guests,
26-
floorOptions,
27-
groupSizeOptions,
28-
groupFilter,
29-
floorFilter,
3014
isLoading = false,
31-
onGroupFilterChange,
32-
onFloorFilterChange,
3315
onGuestClick,
3416
}: GuestQuickListTableProps) {
3517
return (
3618
<section className="w-full">
37-
<div className="mb-[1vh] grid grid-cols-[5fr_5fr_2fr_2fr_2fr] items-center gap-[1vw] px-[1vw] text-[1vw] text-black">
38-
<p>Government Name</p>
39-
<p>Preferred Name</p>
40-
<select
41-
value={groupFilter}
42-
onChange={(event) => onGroupFilterChange(event.target.value)}
43-
className="h-[3vh] min-h-[3vh] border border-black bg-white px-[1vw] text-[1vw]"
44-
aria-label="Group filter"
45-
>
46-
<option value="all">Group</option>
47-
{groupSizeOptions.map((size) => (
48-
<option key={size} value={String(size)}>
49-
{size}
50-
</option>
51-
))}
52-
</select>
53-
<select
54-
value={floorFilter}
55-
onChange={(event) => onFloorFilterChange(event.target.value)}
56-
className="h-[3vh] min-h-[3vh] border border-black bg-white px-[1vw] text-[1vw]"
57-
aria-label="Floor filter"
58-
>
59-
<option value="all">Floor</option>
60-
{floorOptions.map((floor) => (
61-
<option key={floor} value={String(floor)}>
62-
{floor}
63-
</option>
64-
))}
65-
</select>
66-
<p>Room</p>
19+
<div
20+
className={`mb-2 grid ${COL_CLASSES} items-center gap-4 px-4 py-2 text-sm font-medium text-primary`}
21+
>
22+
<p>Guest</p>
23+
<p>Specific Needs</p>
24+
<p>Active Bookings</p>
25+
<p>Requests</p>
6726
</div>
6827

69-
<div className="overflow-hidden border border-black bg-white">
70-
{guests.map((guest) => {
71-
const groupSize = guest.group_size as number | null | undefined;
28+
<div className="overflow-hidden rounded-xl border border-stroke-subtle bg-white">
29+
{guests.map((guest) => (
30+
<button
31+
key={guest.id}
32+
type="button"
33+
onClick={() => onGuestClick(guest.id)}
34+
className={`grid w-full ${COL_CLASSES} items-center gap-4 border-b border-stroke-subtle px-4 py-4 text-left last:border-b-0 hover:bg-bg-container`}
35+
>
36+
<p className="truncate text-sm font-medium text-primary">
37+
{guest.first_name} {guest.last_name}
38+
</p>
39+
40+
<p className="text-sm text-text-subtle"></p>
41+
42+
<div className="flex min-w-0 flex-wrap gap-1.5">
43+
{guest.room_number != null ? (
44+
<span className="inline-flex items-center rounded px-2 py-1 text-xs bg-bg-selected text-primary">
45+
Floor {guest.floor}, Suite {guest.room_number}
46+
</span>
47+
) : (
48+
<span className="text-base text-text-default">None</span>
49+
)}
50+
</div>
51+
52+
<p className="text-sm text-primary"></p>
53+
</button>
54+
))}
7255

73-
return (
74-
<button
75-
key={guest.id}
76-
type="button"
77-
onClick={() => onGuestClick(guest.id)}
78-
className="grid w-full grid-cols-[auto_5fr_5fr_2fr_2fr_2fr] items-center gap-[1vw] border-b border-black px-[1vw] py-[1vh] text-left last:border-b-0 hover:bg-neutral-50"
79-
>
80-
{avatarPill()}
81-
<p className="truncate text-[1vw] text-black">
82-
{guest.first_name} {guest.last_name}
83-
</p>
84-
<p className="truncate text-[1vw] text-black">
85-
{guest.preferred_name}
86-
</p>
87-
<p className="text-[1vw] text-black">
88-
{groupSize == null ? "—" : String(groupSize)}
89-
</p>
90-
<p className="text-[1vw] text-black">{guest.floor}</p>
91-
<p className="text-[1vw] text-black">{guest.room_number}</p>
92-
</button>
93-
);
94-
})}
9556
{!isLoading && guests.length === 0 && (
96-
<div className="px-[1vw] py-[2vh] text-[1vw] text-neutral-600">
57+
<div className="px-4 py-6 text-sm text-text-subtle">
9758
No guests match your current filters.
9859
</div>
9960
)}

clients/web/src/components/guests/GuestSearchBar.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ type GuestSearchBarProps = {
77

88
export function GuestSearchBar({ value, onChange }: GuestSearchBarProps) {
99
return (
10-
<SearchBar value={value} onChange={onChange} placeholder="Search Guests" />
10+
<SearchBar
11+
value={value}
12+
onChange={onChange}
13+
placeholder="Search Guests"
14+
className="rounded-lg px-4 py-2.5"
15+
/>
1116
);
1217
}

0 commit comments

Comments
 (0)