Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion clients/shared/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,16 @@ export const createRequest = (
return async <T>(config: RequestConfig): Promise<T> => {
let fullUrl = `${baseUrl}${config.url}`;
if (config.params && Object.keys(config.params).length > 0) {
const searchParams = new URLSearchParams(config.params);
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(config.params)) {
if (Array.isArray(value)) {
for (const item of value) {
searchParams.append(key, String(item));
}
} else if (value !== undefined && value !== null) {
searchParams.append(key, String(value));
}
}
fullUrl += '?' + searchParams.toString();
}

Expand Down
7 changes: 7 additions & 0 deletions clients/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,10 @@ export {
useGetApiV1GuestsId,
usePutApiV1GuestsId,
} from "./api/generated/endpoints/guests/guests";

export { useGetRooms, useGetRoomsFloors } from "./api/generated/endpoints/rooms/rooms";

export type {
RoomWithOptionalGuestBooking,
GetRoomsParams,
} from "./api/generated/models";
85 changes: 85 additions & 0 deletions clients/web/src/components/rooms/FloorDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { ChevronDown } from "lucide-react";
import { useGetRoomsFloors } from "@shared";
import { Button } from "../ui/Button";
import { useDropdown } from "../../hooks/use-dropdown";
import { FloorDropdownOptions } from "./FloorDropdownOptions";
import { FloorDropdownSearch } from "./FloorDropdownSearch";

type FloorDropdownProps = {
selected?: Array<number>;
onChangeSelectedFloors?: (floors: Array<number>) => void;
};

function getFloorLabel(selected: Array<number>) {
switch (selected.length) {
case 0:
return "All Floors";
case 1:
return `Floor ${selected[0]}`;
default:
return `${selected.length} floors selected`;
}
}

export function FloorDropdown({
selected = [],
onChangeSelectedFloors,
}: FloorDropdownProps) {
const { data: floors = [] } = useGetRoomsFloors();

const {
open,
search,
pending,
searchProps,
triggerProps,
toggle,
selectProps,
cancelProps,
} = useDropdown<number>({
selected,
onChangeSelectedItems: onChangeSelectedFloors,
});

const filtered = floors.filter((f) =>
`floor ${f}`.includes(search.trim().toLowerCase()),
);

return (
<div className="relative min-w-0 w-full max-w-75 bg-bg-primary rounded-md">
<button
type="button"
{...triggerProps}
className={`flex w-full min-w-0 items-center justify-between text-sm text-text-default px-4 py-3 ${open ? "rounded-t-md" : "rounded-md"}`}
>
<span className="truncate">{getFloorLabel(selected)}</span>
<ChevronDown
className={`h-5 w-5 shrink-0 transition-transform duration-200 ${open ? "rotate-180" : ""}`}
strokeWidth={2.5}
/>
</button>

{open && (
<div className="absolute left-0 top-full z-10 w-full bg-bg-primary rounded-b-md shadow-md">
<FloorDropdownSearch {...searchProps} />

<FloorDropdownOptions
floors={filtered}
pending={pending}
search={search}
onToggle={toggle}
/>

<div className="flex justify-end gap-3 border-t border-stroke-subtle p-3">
<Button variant="secondary" {...cancelProps}>
Cancel
</Button>
<Button variant="primary" {...selectProps}>
Select
</Button>
</div>
</div>
)}
</div>
);
}
38 changes: 38 additions & 0 deletions clients/web/src/components/rooms/FloorDropdownOptions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
type FloorDropdownOptionsProps = {
floors: Array<number>;
pending: Array<number>;
search: string;
onToggle: (floor: number) => void;
};

export function FloorDropdownOptions({
floors,
pending,
search,
onToggle,
}: FloorDropdownOptionsProps) {
return (
<div className="flex flex-col max-h-28 overflow-y-auto">
{floors.length === 0 ? (
<p className="px-4 py-2 text-sm text-text-subtle">
No floors match "{search}"
</p>
) : (
floors.map((floor) => (
<label
key={floor}
className="flex items-center gap-3 px-4 py-2 text-sm cursor-pointer hover:bg-bg-selected"
>
<input
type="checkbox"
checked={pending.includes(floor)}
onChange={() => onToggle(floor)}
className="accent-black"
/>
Floor {floor}
</label>
))
)}
</div>
);
}
29 changes: 29 additions & 0 deletions clients/web/src/components/rooms/FloorDropdownSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Search, X } from "lucide-react";

type FloorDropdownSearchProps = {
value: string;
onChange: (value: string) => void;
};

export const FloorDropdownSearch = ({
value,
onChange,
}: FloorDropdownSearchProps) => {
return (
<div className="flex items-center gap-3 border-y border-stroke-subtle px-4 py-3">
<Search className="h-5 w-5 shrink-0 text-text-subtle" strokeWidth={2.5} />
<input
type="text"
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder="Search..."
className="flex-1 bg-transparent text-sm outline-none placeholder:text-text-subtle"
/>
{value && (
<button type="button" onClick={() => onChange("")}>
<X className="h-5 w-5 text-text-subtle hover:text-text-default" />
</button>
)}
</div>
);
};
74 changes: 0 additions & 74 deletions clients/web/src/components/rooms/FloorFilterDropdown.tsx

This file was deleted.

60 changes: 60 additions & 0 deletions clients/web/src/components/rooms/OrderByDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ChevronDown } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";

type OrderByDropdownProps = {
ascending: boolean;
setAscending: (ascending: boolean) => void;
};

export function OrderByDropdown({
ascending,
setAscending,
}: OrderByDropdownProps) {
const [open, setOpen] = useState(false);
const label = ascending ? "Ascending" : "Descending";

return (
<div
className="relative min-w-0 w-full max-w-31.5"
onBlur={(e) => {
if (!e.currentTarget.contains(e.relatedTarget)) setOpen(false);
}}
>
<button
type="button"
className={cn(
"flex w-full items-center justify-between gap-2 py-1 pl-2 pr-1 border border-stroke-subtle bg-bg-primary text-left",
open ? "rounded-t-md" : "rounded-md",
)}
onClick={() => setOpen((o) => !o)}
aria-expanded={open}
>
<span className="min-w-0 truncate text-md text-text-default">
{label}
</span>
<ChevronDown
className={cn(
"h-4 w-4 shrink-0 transition-transform",
open && "rotate-180",
)}
/>
</button>

{open && (
<div className="absolute left-0 top-full z-10 w-full rounded-b-md border border-t-0 border-stroke-subtle bg-bg-primary shadow-md">
<button
type="button"
className="block w-full pl-2 pr-1 py-1 text-left text-md text-text-default hover:bg-bg-selected"
onClick={() => {
setAscending(!ascending);
setOpen(false);
}}
>
{ascending ? "Descending" : "Ascending"}
</button>
</div>
)}
</div>
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good but def shadcn for future cases, you're the goat for cooking up from scratch though

55 changes: 35 additions & 20 deletions clients/web/src/components/rooms/RoomCard.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,58 @@
import type { Room } from "@/components/rooms/RoomsList";
import {
BrushCleaning,
CircleAlert,
CircleUserRound,
UserRoundCheck,
} from "lucide-react";
import { Tag } from "../ui/Tag";
import type { RoomWithOptionalGuestBooking } from "@shared";
import { cn } from "@/lib/utils";

type RoomCardProps = {
room: Room;
room: RoomWithOptionalGuestBooking;
isSelected?: boolean;
onClick: () => void;
};

function getGuestDisplay(room: RoomWithOptionalGuestBooking) {
const hasGuests = (room.guests?.length ?? 0) > 0;
const guestLabel = room.guests
?.map((guest) => `${guest.first_name} ${guest.last_name}`)
.join(", ");

return { hasGuests, guestLabel };
}

export function RoomCard({ room, isSelected = false, onClick }: RoomCardProps) {
const { hasGuests, guestLabel } = getGuestDisplay(room);

return (
<button
type="button"
onClick={onClick}
className={cn(
"flex flex-col items-start gap-[1.5vh] flex-1 min-w-0 w-full min-h-[11vh] text-left rounded-md border px-[0.6vw] py-[0.9vh] transition-colors",
"flex flex-col items-start flex-1 min-w-0 w-full min-h-36.25 text-left rounded-md border gap-2 px-5 py-4 transition-colors",
isSelected
? "border-stroke-subtle bg-white shadow-sm"
: "border-zinc-200 hover:bg-selected",
? "border-stroke-subtle bg-bg-selected shadow-sm"
: "border-stroke-subtle hover:bg-bg-selected cursor-pointer",
)}
>
<span className="text-xl font-bold text-zinc-900 ">
<span className="text-xl font-bold text-text-default ">
Room {room.room_number}
</span>
<span className="text-sm font-light text-zinc-500">{room.room_type}</span>
<span className="text-xs font-medium text-zinc-900">
Jane Doe, John Doe
</span>
{room.tags && room.tags.length > 0 && (
<div className="flex flex-wrap gap-[0.4vh]">
{room.tags.map((tag) => (
<span
key={tag}
className="inline-flex items-center px-[2vw] py-[0.5vh] rounded text-xs font-medium bg-primary text-white"
>
{tag}
</span>
))}
<span className="text-sm text-text-subtle">{room.suite_type}</span>
{hasGuests && (
<div className="flex items-center gap-1 text-sm text-text-subtle pb-3">
<CircleUserRound className="h-4.5 w-4.5 shrink-0" />
<span className="truncate">{guestLabel}</span>
</div>
)}
{/* Hardcoded tag for now */}
<div className="flex flex-row gap-3 pt-1">
<Tag icon={CircleAlert} label="Urgent Task" variant="danger" />
<Tag icon={UserRoundCheck} label="Occupied" variant="default" />
<Tag icon={BrushCleaning} label="Needs Cleaning" variant="default" />
</div>
</button>
);
}
Loading
Loading