From 98f27b7476858d9b97221f3b519c3454f51827ca Mon Sep 17 00:00:00 2001 From: macnelson9 Date: Thu, 22 Jan 2026 14:12:01 +0100 Subject: [PATCH 1/4] feat(web): implement invitations page with search and filters (closes #191) Add search and status filtering with created/property sorting. Add pagination for mobile/desktop and a responsive mobile menu drawer. Resolve useUserRole merge conflicts and add loading state. --- apps/web/src/app/invitations/page.tsx | 458 ++++++++++++++++++++++++-- apps/web/src/hooks/useUserRole.tsx | 47 --- 2 files changed, 426 insertions(+), 79 deletions(-) diff --git a/apps/web/src/app/invitations/page.tsx b/apps/web/src/app/invitations/page.tsx index 24f6b1cb..2767ce68 100644 --- a/apps/web/src/app/invitations/page.tsx +++ b/apps/web/src/app/invitations/page.tsx @@ -1,19 +1,157 @@ 'use client'; import { RightSidebar } from '@/components/layout/RightSidebar'; -import { Search } from 'lucide-react'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { IconContainer } from '@/components/ui/icon-container'; +import { + DUAL_MENU_ITEMS, + GUEST_MENU_ITEMS, + HOST_MENU_ITEMS, + type MenuItem, + TENANT_MENU_ITEMS, +} from '@/constants/menu-items'; +import { useAuth } from '@/hooks/auth/use-auth'; +import { useUserRole } from '@/hooks/useUserRole'; +import { ChevronDown, Menu, Search, X } from 'lucide-react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useEffect, useMemo, useState } from 'react'; + +type Invitation = { + id: string; + property: string; + owner: string; + checkIn: string; + checkOut: string; + created: string; + status: string; + invitation: string; + paymentMethod?: string; +}; + +const MOBILE_INVITATIONS: Invitation[] = []; +const DESKTOP_INVITATIONS: Invitation[] = []; const InvitationsPage = () => { + const { isAuthenticated } = useAuth(); + const { role } = useUserRole(); + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [desktopPage, setDesktopPage] = useState(1); + const [searchQuery, setSearchQuery] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); + const [createdSort, setCreatedSort] = useState('newest'); + const [propertySort, setPropertySort] = useState('a-z'); + const mobileRowsPerPage = 5; + const desktopRowsPerPage = 10; + + const menuItems: MenuItem[] = useMemo(() => { + if (!isAuthenticated) return GUEST_MENU_ITEMS; + switch (role) { + case 'host': + return HOST_MENU_ITEMS; + case 'dual': + return DUAL_MENU_ITEMS; + default: + return TENANT_MENU_ITEMS; + } + }, [role, isAuthenticated]); + + const drawerItems = useMemo(() => menuItems.filter((item) => item.id !== 'menu'), [menuItems]); + const filteredDesktopInvitations = useMemo(() => { + const normalizedQuery = searchQuery.trim().toLowerCase(); + let filtered = DESKTOP_INVITATIONS.filter((invite) => { + if (statusFilter !== 'all' && invite.status.toLowerCase() !== statusFilter) { + return false; + } + if (!normalizedQuery) return true; + return ( + invite.property.toLowerCase().includes(normalizedQuery) || + invite.owner.toLowerCase().includes(normalizedQuery) || + invite.status.toLowerCase().includes(normalizedQuery) || + invite.invitation.toLowerCase().includes(normalizedQuery) + ); + }); + + filtered = [...filtered].sort((left, right) => { + const propertyCompare = left.property.localeCompare(right.property); + if (propertyCompare !== 0) { + return propertySort === 'a-z' ? propertyCompare : -propertyCompare; + } + + const leftDate = new Date(left.created).getTime(); + const rightDate = new Date(right.created).getTime(); + return createdSort === 'newest' ? rightDate - leftDate : leftDate - rightDate; + }); + + return filtered; + }, [createdSort, propertySort, searchQuery, statusFilter]); + const filteredMobileInvitations = useMemo(() => { + const normalizedQuery = searchQuery.trim().toLowerCase(); + let filtered = MOBILE_INVITATIONS.filter((invite) => { + if (statusFilter !== 'all' && invite.status.toLowerCase() !== statusFilter) { + return false; + } + if (!normalizedQuery) return true; + return ( + invite.property.toLowerCase().includes(normalizedQuery) || + invite.owner.toLowerCase().includes(normalizedQuery) || + invite.status.toLowerCase().includes(normalizedQuery) || + invite.invitation.toLowerCase().includes(normalizedQuery) + ); + }); + + filtered = [...filtered].sort((left, right) => { + const propertyCompare = left.property.localeCompare(right.property); + if (propertyCompare !== 0) { + return propertySort === 'a-z' ? propertyCompare : -propertyCompare; + } + + const leftDate = new Date(left.created).getTime(); + const rightDate = new Date(right.created).getTime(); + return createdSort === 'newest' ? rightDate - leftDate : leftDate - rightDate; + }); + + return filtered; + }, [createdSort, propertySort, searchQuery, statusFilter]); + const totalPages = Math.max(1, Math.ceil(filteredMobileInvitations.length / mobileRowsPerPage)); + const desktopTotalPages = Math.max( + 1, + Math.ceil(filteredDesktopInvitations.length / desktopRowsPerPage) + ); + const currentInvitations = filteredMobileInvitations.slice( + (currentPage - 1) * mobileRowsPerPage, + currentPage * mobileRowsPerPage + ); + const currentDesktopInvitations = filteredDesktopInvitations.slice( + (desktopPage - 1) * desktopRowsPerPage, + desktopPage * desktopRowsPerPage + ); + return (
-
-
+
+
+

My invitations

-
+
@@ -21,57 +159,313 @@ const InvitationsPage = () => { setSearchQuery(event.target.value)} />
-
-
Rows per page
-
10
+
+
+ Total: {filteredMobileInvitations.length} invitations +
+
+ Total: {filteredDesktopInvitations.length} invitations +
+ +
+ + + + + + + All + Pending + Accepted + Declined + Expired + + + + + + + + + + Newest + Oldest + + + + + + + + + + A-Z + Z-A + + + +
+ Rows per page + + +
+
-
+
Property
Owner
Check-in
Check-out
-
Created
+
+ Created + +
Status
Invitation
-
- No invitations sent yet -
+ {filteredDesktopInvitations.length === 0 ? ( +
+ No invitations sent yet +
+ ) : ( +
+ {currentDesktopInvitations.map((invite) => ( +
+
{invite.property}
+
{invite.owner}
+
{invite.checkIn}
+
{invite.checkOut}
+
{invite.created}
+
{invite.status}
+
{invite.invitation}
+
+ ))} +
+ )} +
+ +
+ {mobileInvitations.length === 0 ? ( +
+ No invitations sent yet +
+ ) : ( + currentInvitations.map((invite) => ( +
+
+
+
{invite.property}
+
{invite.owner}
+
+ + {invite.status} + +
+ +
+
+
Check-in
+
{invite.checkIn}
+
+
+
Check-out
+
{invite.checkOut}
+
+
+
Created
+
{invite.created}
+
+
+
Payment
+
{invite.paymentMethod}
+
+
+ +
+ {invite.invitation} + View +
+
+ )) + )}
-
-
Total: 0 invitations
-
- - +
+
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
+
+
+ Page {desktopPage} of {desktopTotalPages} +
+
+ + +
- +
+ +
+ +
+ +
+
+ {drawerItems.map((drawerItem) => ( + setIsMenuOpen(false)} + > + {drawerItem.withContainer ? ( + + } + /> + ) : ( + {drawerItem.alt} + )} + {drawerItem.label} + + ))} +
+ +
); }; diff --git a/apps/web/src/hooks/useUserRole.tsx b/apps/web/src/hooks/useUserRole.tsx index 0205a095..753421d8 100644 --- a/apps/web/src/hooks/useUserRole.tsx +++ b/apps/web/src/hooks/useUserRole.tsx @@ -1,7 +1,6 @@ 'use client'; import { useEffect, useState } from 'react'; -<<<<<<< HEAD import { profileAPI } from '~/services/api'; import type { RoleInfo, UserRole } from '~/types/roles'; import { useAuth } from './auth/use-auth'; @@ -11,19 +10,12 @@ interface UseUserRoleReturn extends RoleInfo { } export function useUserRole(): UseUserRoleReturn { -======= -import type { RoleInfo, UserRole } from '~/types/roles'; -import { useAuth } from './auth/use-auth'; - -export function useUserRole(): RoleInfo { ->>>>>>> 60310ea (feat: add stellar contract dependencies and integration setup) const { user, isAuthenticated } = useAuth(); const [roleInfo, setRoleInfo] = useState({ role: 'guest', canAccessHostDashboard: false, hasProperties: false, }); -<<<<<<< HEAD const [isLoading, setIsLoading] = useState(true); useEffect(() => { @@ -120,43 +112,4 @@ export function useUserRole(): RoleInfo { }, [user, isAuthenticated]); return { ...roleInfo, isLoading }; -======= - - useEffect(() => { - if (!isAuthenticated || !user) { - setRoleInfo({ - role: 'guest', - canAccessHostDashboard: false, - hasProperties: false, - }); - return; - } - - // Check if user has host status in localStorage or from API - const storedHostStatus = localStorage.getItem('hostStatus'); - const storedHasProperties = localStorage.getItem('hasProperties') === 'true'; - - let role: UserRole = 'guest'; - let canAccessHostDashboard = false; - - // User is a host if they have verified host status and properties - if (storedHostStatus === 'verified' && storedHasProperties) { - role = 'dual'; // Can be both guest and host - canAccessHostDashboard = true; - } else if (storedHostStatus === 'verified') { - // Verified but no properties yet - role = 'host'; - canAccessHostDashboard = false; // No dashboard access without properties - } - - setRoleInfo({ - role, - hostStatus: storedHostStatus as 'pending' | 'verified' | 'rejected' | 'suspended' | undefined, - canAccessHostDashboard, - hasProperties: storedHasProperties, - }); - }, [user, isAuthenticated]); - - return roleInfo; ->>>>>>> 60310ea (feat: add stellar contract dependencies and integration setup) } From 47a2097c7e20346fa8a077a1864f5a9e5d1f8c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?DomRift=20=E2=9A=A1=EF=B8=8F=F0=9F=92=BB?= <119934253+Macnelson9@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:26:05 +0100 Subject: [PATCH 2/4] Update apps/web/src/app/invitations/page.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- apps/web/src/app/invitations/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/invitations/page.tsx b/apps/web/src/app/invitations/page.tsx index 2767ce68..606a3441 100644 --- a/apps/web/src/app/invitations/page.tsx +++ b/apps/web/src/app/invitations/page.tsx @@ -298,8 +298,8 @@ const InvitationsPage = () => {
- {mobileInvitations.length === 0 ? ( -
+ {filteredMobileInvitations.length === 0 ? ( +
No invitations sent yet
) : ( From beeb64dcebccde32de22111b428b33e66d2d66ba Mon Sep 17 00:00:00 2001 From: macnelson9 Date: Thu, 22 Jan 2026 22:34:26 +0100 Subject: [PATCH 3/4] feat(ui): implement roleguard auth system --- apps/web/src/app/invitations/page.tsx | 1 + apps/web/src/components/guards/RoleGuard.tsx | 85 ++++++++------------ 2 files changed, 35 insertions(+), 51 deletions(-) diff --git a/apps/web/src/app/invitations/page.tsx b/apps/web/src/app/invitations/page.tsx index 606a3441..55e02238 100644 --- a/apps/web/src/app/invitations/page.tsx +++ b/apps/web/src/app/invitations/page.tsx @@ -63,6 +63,7 @@ const InvitationsPage = () => { } }, [role, isAuthenticated]); + const mobileInvitations = MOBILE_INVITATIONS; const drawerItems = useMemo(() => menuItems.filter((item) => item.id !== 'menu'), [menuItems]); const filteredDesktopInvitations = useMemo(() => { const normalizedQuery = searchQuery.trim().toLowerCase(); diff --git a/apps/web/src/components/guards/RoleGuard.tsx b/apps/web/src/components/guards/RoleGuard.tsx index ce34d7f4..fdf498e9 100644 --- a/apps/web/src/components/guards/RoleGuard.tsx +++ b/apps/web/src/components/guards/RoleGuard.tsx @@ -1,16 +1,14 @@ 'use client'; import { useRouter } from 'next/navigation'; -<<<<<<< HEAD -import { useEffect, useState } from 'react'; -======= -import { useEffect } from 'react'; ->>>>>>> 60310ea (feat: add stellar contract dependencies and integration setup) +import type { ReactNode } from 'react'; +import { useEffect, useMemo } from 'react'; +import { useAuth } from '~/hooks/auth/use-auth'; import { useUserRole } from '~/hooks/useUserRole'; import type { UserRole } from '~/types/roles'; interface RoleGuardProps { - children: React.ReactNode; + children: ReactNode; requiredRole: UserRole; fallbackPath?: string; } @@ -20,67 +18,52 @@ export function RoleGuard({ requiredRole, fallbackPath = '/become-host', }: RoleGuardProps) { -<<<<<<< HEAD - const roleInfo = useUserRole(); - const { canAccessHostDashboard, isLoading } = roleInfo; const router = useRouter(); - const [isChecking, setIsChecking] = useState(true); + const { isAuthenticated, isLoading: authLoading } = useAuth(); + const { role, canAccessHostDashboard, isLoading: roleLoading } = useUserRole(); - useEffect(() => { - // Wait for role data to load - if (!isLoading) { - setIsChecking(false); - - // Redirect if user doesn't have required access - if (requiredRole === 'host' && !canAccessHostDashboard) { - router.push(fallbackPath); - } - } - }, [requiredRole, canAccessHostDashboard, isLoading, router, fallbackPath]); + const isLoading = authLoading || roleLoading; - // Show loading state while checking authentication - if (isLoading || isChecking) { - return ( -
-
-
-

Verifying access...

-======= - const { canAccessHostDashboard } = useUserRole(); - const router = useRouter(); + const hasAccess = useMemo(() => { + if (requiredRole === 'guest') return true; + if (!isAuthenticated) return false; + if (requiredRole === 'host') return canAccessHostDashboard; + return role === 'dual'; + }, [canAccessHostDashboard, isAuthenticated, requiredRole, role]); useEffect(() => { - if (requiredRole === 'host' && !canAccessHostDashboard) { - router.push(fallbackPath); + if (!isLoading && !hasAccess) { + router.replace(fallbackPath); } - }, [requiredRole, canAccessHostDashboard, router, fallbackPath]); + }, [fallbackPath, hasAccess, isLoading, router]); + + if (isLoading) { + return ( +
+ Checking access... +
+ ); + } - if (requiredRole === 'host' && !canAccessHostDashboard) { + if (!hasAccess) { return ( -
-
-

Host Access Required

-

You need to become a host to access this page.

+
+
+

Host Access Required

+

+ You need a verified host profile with properties to access this page. +

->>>>>>> 60310ea (feat: add stellar contract dependencies and integration setup)
); } -<<<<<<< HEAD - // Return null during redirect to prevent flash of unauthorized content - if (requiredRole === 'host' && !canAccessHostDashboard) { - return null; - } - -======= ->>>>>>> 60310ea (feat: add stellar contract dependencies and integration setup) return <>{children}; } From f85700749e54e22870c0ba6ce071cf5f56650e77 Mon Sep 17 00:00:00 2001 From: macnelson9 Date: Fri, 23 Jan 2026 00:00:44 +0100 Subject: [PATCH 4/4] refactor: refactor invitations and roleguard code files --- .../src/app/dashboard/host-dashboard/page.tsx | 2 +- .../app/dashboard/tenant-dashboard/page.tsx | 2 +- apps/web/src/app/invitations/page.tsx | 679 ++++++++++-------- apps/web/src/hooks/auth/RoleGuard.tsx | 52 -- apps/web/src/hooks/useUserRole.ts | 102 --- 5 files changed, 366 insertions(+), 471 deletions(-) delete mode 100644 apps/web/src/hooks/auth/RoleGuard.tsx delete mode 100644 apps/web/src/hooks/useUserRole.ts diff --git a/apps/web/src/app/dashboard/host-dashboard/page.tsx b/apps/web/src/app/dashboard/host-dashboard/page.tsx index 84926551..d01fef93 100644 --- a/apps/web/src/app/dashboard/host-dashboard/page.tsx +++ b/apps/web/src/app/dashboard/host-dashboard/page.tsx @@ -4,7 +4,7 @@ import BookingHistory from '@/components/dashboard/BookingHistory'; import NotificationSystem from '@/components/dashboard/NotificationSystem'; import ProfileManagement from '@/components/dashboard/ProfileManagement'; import PropertyManagement from '@/components/dashboard/PropertyManagement'; -import RoleGuard from '@/hooks/auth/RoleGuard'; +import { RoleGuard } from '@/components/guards/RoleGuard'; import { useRealTimeNotifications } from '@/hooks/useRealTimeUpdates'; import { Calendar, DollarSign, Settings, User, Wallet } from 'lucide-react'; import Image from 'next/image'; diff --git a/apps/web/src/app/dashboard/tenant-dashboard/page.tsx b/apps/web/src/app/dashboard/tenant-dashboard/page.tsx index 5c4af42a..00690dbb 100644 --- a/apps/web/src/app/dashboard/tenant-dashboard/page.tsx +++ b/apps/web/src/app/dashboard/tenant-dashboard/page.tsx @@ -3,7 +3,7 @@ import BookingHistory from '@/components/dashboard/BookingHistory'; import NotificationSystem from '@/components/dashboard/NotificationSystem'; import ProfileManagement from '@/components/dashboard/ProfileManagement'; -import RoleGuard from '@/hooks/auth/RoleGuard'; +import { RoleGuard } from '@/components/guards/RoleGuard'; import { Activity, AlertCircle, diff --git a/apps/web/src/app/invitations/page.tsx b/apps/web/src/app/invitations/page.tsx index 55e02238..724e46fb 100644 --- a/apps/web/src/app/invitations/page.tsx +++ b/apps/web/src/app/invitations/page.tsx @@ -21,7 +21,7 @@ import { useUserRole } from '@/hooks/useUserRole'; import { ChevronDown, Menu, Search, X } from 'lucide-react'; import Image from 'next/image'; import Link from 'next/link'; -import { useEffect, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; type Invitation = { id: string; @@ -35,21 +35,23 @@ type Invitation = { paymentMethod?: string; }; -const MOBILE_INVITATIONS: Invitation[] = []; -const DESKTOP_INVITATIONS: Invitation[] = []; +type SortOrder = 'newest' | 'oldest'; +type PropertySort = 'a-z' | 'z-a'; +type StatusFilter = 'all' | 'pending' | 'accepted' | 'declined' | 'expired'; + +const MOBILE_ROWS_PER_PAGE = 5; +const DESKTOP_ROWS_PER_PAGE = 10; const InvitationsPage = () => { const { isAuthenticated } = useAuth(); const { role } = useUserRole(); const [isMenuOpen, setIsMenuOpen] = useState(false); - const [currentPage, setCurrentPage] = useState(1); + const [mobilePage, setMobilePage] = useState(1); const [desktopPage, setDesktopPage] = useState(1); const [searchQuery, setSearchQuery] = useState(''); - const [statusFilter, setStatusFilter] = useState('all'); - const [createdSort, setCreatedSort] = useState('newest'); - const [propertySort, setPropertySort] = useState('a-z'); - const mobileRowsPerPage = 5; - const desktopRowsPerPage = 10; + const [statusFilter, setStatusFilter] = useState('all'); + const [createdSort, setCreatedSort] = useState('newest'); + const [propertySort, setPropertySort] = useState('a-z'); const menuItems: MenuItem[] = useMemo(() => { if (!isAuthenticated) return GUEST_MENU_ITEMS; @@ -63,39 +65,13 @@ const InvitationsPage = () => { } }, [role, isAuthenticated]); - const mobileInvitations = MOBILE_INVITATIONS; const drawerItems = useMemo(() => menuItems.filter((item) => item.id !== 'menu'), [menuItems]); - const filteredDesktopInvitations = useMemo(() => { - const normalizedQuery = searchQuery.trim().toLowerCase(); - let filtered = DESKTOP_INVITATIONS.filter((invite) => { - if (statusFilter !== 'all' && invite.status.toLowerCase() !== statusFilter) { - return false; - } - if (!normalizedQuery) return true; - return ( - invite.property.toLowerCase().includes(normalizedQuery) || - invite.owner.toLowerCase().includes(normalizedQuery) || - invite.status.toLowerCase().includes(normalizedQuery) || - invite.invitation.toLowerCase().includes(normalizedQuery) - ); - }); - filtered = [...filtered].sort((left, right) => { - const propertyCompare = left.property.localeCompare(right.property); - if (propertyCompare !== 0) { - return propertySort === 'a-z' ? propertyCompare : -propertyCompare; - } - - const leftDate = new Date(left.created).getTime(); - const rightDate = new Date(right.created).getTime(); - return createdSort === 'newest' ? rightDate - leftDate : leftDate - rightDate; - }); + const invitations = useMemo(() => [], []); - return filtered; - }, [createdSort, propertySort, searchQuery, statusFilter]); - const filteredMobileInvitations = useMemo(() => { + const filteredInvitations = useMemo(() => { const normalizedQuery = searchQuery.trim().toLowerCase(); - let filtered = MOBILE_INVITATIONS.filter((invite) => { + let filtered = invitations.filter((invite) => { if (statusFilter !== 'all' && invite.status.toLowerCase() !== statusFilter) { return false; } @@ -120,25 +96,53 @@ const InvitationsPage = () => { }); return filtered; - }, [createdSort, propertySort, searchQuery, statusFilter]); - const totalPages = Math.max(1, Math.ceil(filteredMobileInvitations.length / mobileRowsPerPage)); + }, [createdSort, invitations, propertySort, searchQuery, statusFilter]); + + const mobileTotalPages = Math.max( + 1, + Math.ceil(filteredInvitations.length / MOBILE_ROWS_PER_PAGE) + ); const desktopTotalPages = Math.max( 1, - Math.ceil(filteredDesktopInvitations.length / desktopRowsPerPage) + Math.ceil(filteredInvitations.length / DESKTOP_ROWS_PER_PAGE) ); - const currentInvitations = filteredMobileInvitations.slice( - (currentPage - 1) * mobileRowsPerPage, - currentPage * mobileRowsPerPage + const currentMobileInvitations = filteredInvitations.slice( + (mobilePage - 1) * MOBILE_ROWS_PER_PAGE, + mobilePage * MOBILE_ROWS_PER_PAGE ); - const currentDesktopInvitations = filteredDesktopInvitations.slice( - (desktopPage - 1) * desktopRowsPerPage, - desktopPage * desktopRowsPerPage + const currentDesktopInvitations = filteredInvitations.slice( + (desktopPage - 1) * DESKTOP_ROWS_PER_PAGE, + desktopPage * DESKTOP_ROWS_PER_PAGE ); + const handleSearchChange = (value: string) => { + setSearchQuery(value); + setMobilePage(1); + setDesktopPage(1); + }; + + const handleStatusChange = (value: StatusFilter) => { + setStatusFilter(value); + setMobilePage(1); + setDesktopPage(1); + }; + + const handleCreatedSortChange = (value: SortOrder) => { + setCreatedSort(value); + setMobilePage(1); + setDesktopPage(1); + }; + + const handlePropertySortChange = (value: PropertySort) => { + setPropertySort(value); + setMobilePage(1); + setDesktopPage(1); + }; + return (
-
-
+
+
-
+
-
- - - - setSearchQuery(event.target.value)} - /> -
+
- Total: {filteredMobileInvitations.length} invitations + Total: {filteredInvitations.length} invitations
- Total: {filteredDesktopInvitations.length} invitations + Total: {filteredInvitations.length} invitations
- - - - - - - All - Pending - Accepted - Declined - Expired - - - - - - - - - - Newest - Oldest - - - - - - - - - - A-Z - Z-A - - - + + handleStatusChange(value as StatusFilter)} + > + All + Pending + Accepted + Declined + Expired + + + + + handleCreatedSortChange(value as SortOrder)} + > + Newest + Oldest + + + + + handlePropertySortChange(value as PropertySort)} + > + A-Z + Z-A + + + + +
Rows per page
@@ -263,137 +246,25 @@ const InvitationsPage = () => {
-
-
-
Property
-
Owner
-
Check-in
-
Check-out
-
- Created - -
-
Status
-
Invitation
-
- - {filteredDesktopInvitations.length === 0 ? ( -
- No invitations sent yet -
- ) : ( -
- {currentDesktopInvitations.map((invite) => ( -
-
{invite.property}
-
{invite.owner}
-
{invite.checkIn}
-
{invite.checkOut}
-
{invite.created}
-
{invite.status}
-
{invite.invitation}
-
- ))} -
- )} -
+ -
- {filteredMobileInvitations.length === 0 ? ( -
- No invitations sent yet -
- ) : ( - currentInvitations.map((invite) => ( -
-
-
-
{invite.property}
-
{invite.owner}
-
- - {invite.status} - -
- -
-
-
Check-in
-
{invite.checkIn}
-
-
-
Check-out
-
{invite.checkOut}
-
-
-
Created
-
{invite.created}
-
-
-
Payment
-
{invite.paymentMethod}
-
-
- -
- {invite.invitation} - View -
-
- )) - )} -
+
-
-
- Page {currentPage} of {totalPages} -
-
- - -
-
-
-
- Page {desktopPage} of {desktopTotalPages} -
-
- - -
-
+ setMobilePage((prev) => Math.max(1, prev - 1))} + onNext={() => setMobilePage((prev) => Math.min(mobileTotalPages, prev + 1))} + /> + setDesktopPage((prev) => Math.max(1, prev - 1))} + onNext={() => setDesktopPage((prev) => Math.min(desktopTotalPages, prev + 1))} + />
@@ -403,56 +274,226 @@ const InvitationsPage = () => {
-
setIsMenuOpen(false)} + menuItems={drawerItems} + /> +
+ ); +}; + +const SearchBar = ({ value, onChange }: { value: string; onChange: (value: string) => void }) => ( +
+ + + + onChange(event.target.value)} + /> +
+); + +const FilterDropdown = ({ + label, + valueLabel, + children, +}: { + label: string; + valueLabel: string; + children: React.ReactNode; +}) => ( + + + + + + {children} + + +); + +const DesktopInvitationsTable = ({ invitations }: { invitations: Invitation[] }) => ( +
+
+
Property
+
Owner
+
Check-in
+
Check-out
+
+ Created + +
+
Status
+
Invitation
+
+ + {invitations.length === 0 ? ( +
+ No invitations sent yet +
+ ) : ( +
+ {invitations.map((invite) => ( +
+
{invite.property}
+
{invite.owner}
+
{invite.checkIn}
+
{invite.checkOut}
+
{invite.created}
+
{invite.status}
+
{invite.invitation}
+
+ ))} +
+ )} +
+); + +const MobileInvitationsList = ({ invitations }: { invitations: Invitation[] }) => ( +
+ {invitations.length === 0 ? ( +
+ No invitations sent yet +
+ ) : ( + invitations.map((invite) => ( +
-
-

Menu

- +
+
+
{invite.property}
+
{invite.owner}
+
+ + {invite.status} +
-
- {drawerItems.map((drawerItem) => ( - setIsMenuOpen(false)} - > - {drawerItem.withContainer ? ( - - } - /> - ) : ( + +
+
+
Check-in
+
{invite.checkIn}
+
+
+
Check-out
+
{invite.checkOut}
+
+
+
Created
+
{invite.created}
+
+
+
Payment
+
{invite.paymentMethod ?? '-'}
+
+
+ +
+ {invite.invitation} + View +
+
+ )) + )} +
+); + +const PaginationControls = ({ + className = '', + currentPage, + totalPages, + onPrevious, + onNext, +}: { + className?: string; + currentPage: number; + totalPages: number; + onPrevious: () => void; + onNext: () => void; +}) => ( +
+
+ Page {currentPage} of {totalPages} +
+
+ + +
+
+); + +const MobileMenuDrawer = ({ + isOpen, + onClose, + menuItems, +}: { + isOpen: boolean; + onClose: () => void; + menuItems: MenuItem[]; +}) => ( +
+ +
+
+ {menuItems.map((drawerItem) => ( + + {drawerItem.withContainer ? ( + { height={20} className="p-0.5" /> - )} - {drawerItem.label} - - ))} -
- + } + /> + ) : ( + {drawerItem.alt} + )} + {drawerItem.label} + + ))}
-
- ); -}; + +
+); export default InvitationsPage; diff --git a/apps/web/src/hooks/auth/RoleGuard.tsx b/apps/web/src/hooks/auth/RoleGuard.tsx deleted file mode 100644 index 1d3b6221..00000000 --- a/apps/web/src/hooks/auth/RoleGuard.tsx +++ /dev/null @@ -1,52 +0,0 @@ -// apps/web/src/hooks/auth/RoleGuard.tsx -'use client'; - -import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; -import { useUserRole } from '../useUserRole'; - -interface RoleGuardProps { - children: React.ReactNode; - requiredRole: 'guest' | 'host'; -} - -export default function RoleGuard({ children, requiredRole }: RoleGuardProps) { - const { role, canAccessHostDashboard, isLoading } = useUserRole(); - const router = useRouter(); - - useEffect(() => { - if (isLoading) { - return; - } - - // Redirect unauthenticated users to main dashboard - if (!role) { - router.replace('/dashboard'); - return; - } - - if (requiredRole === 'host') { - if (!(role === 'host' || role === 'dual') || !canAccessHostDashboard) { - router.replace('/dashboard'); - } - } else if (requiredRole === 'guest') { - // Allow: guest, dual, or host without host-dashboard access - if (role !== 'guest' && role !== 'dual' && !(role === 'host' && !canAccessHostDashboard)) { - router.replace('/dashboard'); - } - } - }, [role, canAccessHostDashboard, isLoading, router, requiredRole]); - - if (isLoading) { - return ( -
-
-
-

Loading...

-
-
- ); - } - - return <>{children}; -} diff --git a/apps/web/src/hooks/useUserRole.ts b/apps/web/src/hooks/useUserRole.ts deleted file mode 100644 index 349ee711..00000000 --- a/apps/web/src/hooks/useUserRole.ts +++ /dev/null @@ -1,102 +0,0 @@ -// apps/web/src/hooks/useUserRole.ts -'use client'; - -import { useEffect, useState } from 'react'; -import { useAuth } from './auth/use-auth'; - -// NOTE: The `profileAPI` is not yet implemented, so we're using a mock. -// import { profileAPI } from '~/services/api'; - -// Mock user profiles for development, keyed by user ID -const mockUserProfiles = { - 'user-1': { - role: 'host' as const, - hostStatus: 'verified' as const, - properties: [{ id: 1, name: 'Host Property' }], - }, - 'user-2': { - role: 'guest' as const, - hostStatus: 'unverified' as const, - properties: [], - }, - 'user-3': { - role: 'dual' as const, - hostStatus: 'verified' as const, - properties: [{ id: 2, name: 'Dual User Property' }], - }, -}; - -// Mock implementation of profileAPI for development -const profileAPI = { - getProfile: async (userId: string) => { - // In a real app, this would be a network request. - // For development, we simulate a delay and return a mock profile. - await new Promise((resolve) => setTimeout(resolve, 500)); - - if (process.env.NODE_ENV === 'development') { - // @ts-ignore - const profile = mockUserProfiles[userId]; - if (profile) { - return profile; - } - } - - // Default profile for production or if user not in mock data - return { - role: 'guest' as const, - hostStatus: 'unverified' as const, - properties: [], - }; - }, -}; - -interface Property { - id: number; - name: string; -} - -export function useUserRole() { - const { user, isAuthenticated } = useAuth(); - const [role, setRole] = useState<'guest' | 'host' | 'dual' | null>(null); - const [hostStatus, setHostStatus] = useState<'verified' | 'unverified' | 'pending' | null>(null); - const [properties, setProperties] = useState([]); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - async function fetchUserRole() { - if (isAuthenticated && user) { - setIsLoading(true); - try { - // The user object from useAuth needs to have an `id` property. - // For now, we'll assume it does. If not, this will need adjustment. - // @ts-ignore - const userProfile = await profileAPI.getProfile(user.id || 'user-2'); // Default to user-2 for demo - - setRole(userProfile.role); - setHostStatus(userProfile.hostStatus); - setProperties(userProfile.properties); - } catch (error) { - console.error('Failed to fetch user role:', error); - setRole('guest'); // Default to 'guest' on error - setHostStatus('unverified'); - setProperties([]); - } finally { - setIsLoading(false); - } - } else if (!isAuthenticated) { - setRole(null); - setHostStatus(null); - setProperties([]); - setIsLoading(false); - } - } - - fetchUserRole(); - }, [isAuthenticated, user]); - - const hasProperties = properties.length > 0; - const canAccessHostDashboard = - (role === 'host' || role === 'dual') && hostStatus === 'verified' && hasProperties; - - return { role, canAccessHostDashboard, isLoading, hasProperties }; -}