# Perauset Frontend UX Architecture Next.js (App Router) + Mantine v8. Backend: Pikku RPC over HTTP (port 6002). --- ## 1. Layout System ### Shell Structure ``` AppShell (Mantine AppShell) +--------------------------------------------------+ | Header | | [Logo/Name] [Search?] [Bell] [ThemeToggle] [Avatar] | +--------+-----------------------------------------+ | Sidebar| Main Content Area | | (nav) | | | | | | | | | | | | | | +--------+-----------------------------------------+ ``` Use Mantine's `AppShell` with `AppShell.Header`, `AppShell.Navbar`, `AppShell.Main`. **Header** (60px): Logo left, notification bell + user avatar right. Bell shows unread count badge. **Sidebar** (260px desktop, collapsed on mobile): Icon + label nav items. Collapsible groups per domain. Active item highlighted. On mobile, the sidebar becomes a hamburger-triggered drawer. ### Component Hierarchy ``` app/layout.tsx AuthProvider (session context) MantineProvider (theme) AppShell AppShell.Header Logo HeaderActions (NotificationBell, ThemeToggle, UserMenu) AppShell.Navbar SidebarNav (role-filtered) AppShell.Main children (page content) ``` ### Sidebar Navigation — Role-Based Visibility Each nav item declares the permission it requires. The sidebar component filters items against the session's resolved permissions. ```typescript type NavItem = { label: string icon: TablerIcon href: string permission?: string // Required permission, omit = visible to all authenticated children?: NavItem[] // Nested items (collapsible group) badge?: () => ReactNode // Dynamic badge (e.g., pending count) } const NAV_ITEMS: NavItem[] = [ { label: 'Dashboard', icon: IconDashboard, href: '/dashboard', // No permission — visible to everyone }, { label: 'My Requests', icon: IconSend, href: '/my-requests', // Visible to all authenticated users }, { label: 'Stays', icon: IconBed, href: '/stays', permission: 'stays.view_all', children: [ { label: 'Requests', href: '/stays/requests', permission: 'stays.review' }, { label: 'Active Stays', href: '/stays/active', permission: 'stays.view_all' }, { label: 'Check-in/out', href: '/stays/checkin', permission: 'stays.manage' }, ], }, { label: 'Rooms', icon: IconDoor, href: '/rooms', permission: 'rooms.view_all', children: [ { label: 'Occupancy', href: '/rooms/occupancy', permission: 'rooms.view_all' }, { label: 'Manage', href: '/rooms/manage', permission: 'rooms.manage' }, ], }, { label: 'Boats', icon: IconSailboat, href: '/boats', permission: 'boats.view_all', children: [ { label: 'Schedule', href: '/boats/schedule', permission: 'boats.view_all' }, { label: 'Requests', href: '/boats/requests', permission: 'boats.manage' }, ], }, { label: 'Tasks', icon: IconChecklist, href: '/tasks', permission: 'tasks.view_all', children: [ { label: 'Board', href: '/tasks/board', permission: 'tasks.view_all' }, { label: 'Templates', href: '/tasks/templates', permission: 'tasks.manage' }, ], }, { label: 'Retreats', icon: IconMountain, href: '/retreats', permission: 'retreats.view', }, { label: 'Kitchen', icon: IconToolsKitchen2, href: '/kitchen', permission: 'kitchen.view', children: [ { label: 'Meal Services', href: '/kitchen/meals', permission: 'kitchen.view' }, { label: 'Dietary Profiles', href: '/kitchen/dietary', permission: 'kitchen.manage' }, { label: 'My Dietary Info', href: '/kitchen/my-dietary', permission: 'kitchen.update_dietary' }, ], }, { label: 'Inventory', icon: IconPackage, href: '/inventory', permission: 'inventory.request', children: [ { label: 'Items', href: '/inventory/items', permission: 'inventory.manage' }, { label: 'Requests', href: '/inventory/requests', permission: 'inventory.manage' }, ], }, { label: 'People', icon: IconUsers, href: '/people', permission: 'users.view', children: [ { label: 'Users', href: '/people/users', permission: 'users.view' }, { label: 'Roles', href: '/people/roles', permission: 'roles.assign' }, ], }, { label: 'Audit Log', icon: IconHistory, href: '/audit', permission: 'audit_log.view', }, ] ``` **Filtering logic:** ```typescript function filterNavItems(items: NavItem[], userPermissions: Set): NavItem[] { return items .filter(item => !item.permission || userPermissions.has(item.permission) || userPermissions.has('*')) .map(item => ({ ...item, children: item.children ? filterNavItems(item.children, userPermissions) : undefined, })) .filter(item => !item.children || item.children.length > 0) } ``` ### Mobile Navigation On viewports below 768px: - Sidebar collapses entirely. A hamburger icon in the header toggles a Mantine `Drawer` from the left. - The drawer contains the same nav items, rendered as a flat accordion. - Header shrinks: logo becomes icon-only, actions collapse into a single menu. - Bottom tab bar for the 4-5 most-used items: Dashboard, My Requests, Tasks (if permitted), Boats (if permitted), More (opens drawer). ``` Mobile layout: +--------------------------------------------+ | [=] Logo [Bell] [Avatar] | +--------------------------------------------+ | Main Content (full width, scrollable) | | | +--------------------------------------------+ | [Dashboard] [Requests] [Tasks] [More] | <-- BottomNav +--------------------------------------------+ ``` --- ## 2. Component Patterns ### 2.1 Request/Approval Cards The platform has several request-then-review flows: stay requests, boat seat requests, inventory requests. All follow the same structural pattern. **RequestCard component:** ``` +-------------------------------------------------------+ | [StatusBadge] [TimeAgo] | | | | Requester Name | | Request type or summary line | | | | Key details (dates, notes, etc.) in a compact grid | | Start: Mar 20 End: Mar 27 Type: Volunteer | | | | --- coordinator-only section (if has review perm) --- | | [Approve] [Reject] [Review Notes input] | +-------------------------------------------------------+ ``` Props interface: ```typescript type RequestCardProps = { id: string status: string entityType: 'stay_request' | 'boat_reservation_request' | 'inventory_request' requester: { name: string; avatarUrl?: string } title: string subtitle?: string details: { label: string; value: string }[] notes?: string createdAt: Date reviewActions?: { onApprove: () => void onReject: () => void onReviewNotes?: (notes: string) => void } } ``` Use Mantine `Card` with `Card.Section` dividers. The review actions section renders only when `reviewActions` is provided (which the page component controls based on permission). **Request lifecycle view (community user):** ``` My Requests page (list of own requests across all types) - Filterable by type (stay, boat, inventory) - Filterable by status - Each card shows status + basic info - Clicking opens detail drawer/modal ``` **Request review view (coordinator):** ``` Stays > Requests page (all pending stay requests) - Default filter: status = submitted | under_review - Cards show review action buttons inline - Approve opens a confirmation dialog - Reject opens a dialog with required rejection reason - Optimistic update: card moves to approved/rejected state immediately ``` ### 2.2 Status Badges with Transitions Statuses are central to this platform. Every entity has a state machine. Badges must be visually distinct per status family. **Color mapping (Mantine color names):** ```typescript const STATUS_COLORS: Record = { // Pending / needs attention submitted: 'yellow', under_review: 'orange', planned: 'gray', draft: 'gray', waitlisted: 'orange', // Active / in progress open: 'blue', active: 'blue', confirmed: 'teal', assigned: 'indigo', in_progress: 'violet', checked_in: 'teal', published: 'blue', approved: 'teal', // Completed / success completed: 'green', checked_out: 'green', fulfilled: 'green', // Negative / terminal cancelled: 'red', rejected: 'red', declined: 'red', blocked: 'red', } ``` **StatusBadge component:** ```typescript function StatusBadge({ status, entityType }: { status: string; entityType?: string }) { const color = STATUS_COLORS[status] ?? 'gray' const label = status.replace(/_/g, ' ') return {label} } ``` **Transition indicators:** When a coordinator takes an action (approve, reject, check-in), briefly show the old status with a strikethrough fading into the new status. Accomplish this with Mantine's `Transition` component and a simple CSS animation, not a library. ```typescript // StatusTransition: shows old -> new with animation function StatusTransition({ from, to }: { from: string; to: string }) { return ( ) } ``` ### 2.3 Operational Boards #### Task Kanban Board For coordinators/staff with `tasks.view_all`. Uses columns based on task status. ``` +----------+----------+----------+----------+----------+ | Open | Assigned |In Progress| Blocked | Completed| +----------+----------+----------+----------+----------+ | [Card] | [Card] | [Card] | [Card] | [Card] | | [Card] | [Card] | | | | | [Card] | | | | | +----------+----------+----------+----------+----------+ ``` Implementation approach: - Use CSS Grid with horizontal scroll on mobile (snap scrolling). - Each column is a vertical stack of `TaskCard` components. - Drag-and-drop is a nice-to-have, NOT a requirement for v1. Use explicit action buttons on each card instead (Assign, Start, Complete, Block). - Filter bar above: category (housekeeping, kitchen, maintenance...), assigned user, date range. - On mobile: switch to a list view grouped by status, with collapsible status sections. ```typescript type TaskCardProps = { taskId: string title: string category: TaskCategory status: TaskStatus assignee?: { name: string } scheduledAt?: Date location?: string actions: TaskAction[] // Derived from state machine transitions } ``` #### Boat Trip Timeline For boat coordinators. A day-view timeline showing trips along a time axis. ``` Today: March 17, 2026 [< Prev Day] [Next Day >] +-----+------+------+------+------+------+------+ | 6am | 8am | 10am | 12pm | 2pm | 4pm | 6pm | +-----+------+------+------+------+------+------+ | Route A: Island -> Mainland | | [08:00 =========> 09:30] MV Seabird (12/15) | | [14:00 =========> 15:30] MV Seabird (8/15) | +--------------------------------------------------+ | Route B: Mainland -> Island | | [10:00 =========> 11:30] MV Coral (10/20) | +--------------------------------------------------+ ``` Implementation: - Custom component using CSS Grid where columns represent time slots. - Each trip is a positioned bar spanning its departure-to-arrival time. - Bar color indicates status (planned=gray, open=blue, full=orange, completed=green, cancelled=red with strikethrough). - Clicking a trip bar opens a detail panel/drawer showing manifest, seat requests, actions. - On mobile: switch to a simple chronological list sorted by departure time. ### 2.4 Date Range Pickers for Stays/Rooms Use Mantine `DatePickerInput` with `type="range"` for selecting stay date ranges. **Stay request form:** ```typescript ``` **Room allocation form (coordinator):** Same date range picker, but also displays a mini occupancy preview below showing room availability for the selected range. This is a custom component that queries `listRoomAllocations` for the date range and overlays occupied/available indicators. ### 2.5 Occupancy Grid for Rooms A calendar-style grid showing room occupancy over time. This is the core scheduling view for coordinators. ``` Mar 17 Mar 18 Mar 19 Mar 20 Mar 21 Mar 22 +------------+ +-------+-------+-------+-------+-------+-------+ | Room A (2) | |[=====John Doe=====] | |[===Jane===] | | Room B (4) | |[==============Retreat Group==============] | | Room C (1) | | MAINT | | |[===Bob Smith===] | | Room D (2) | | | |[====Alice====]| | | +------------+ +-------+-------+-------+-------+-------+-------+ ``` Implementation approach: - Rows = rooms (sorted by type, then name). Columns = days. - Each cell can be: empty, occupied (colored bar spanning multiple days), blocked (hatched pattern for maintenance/admin holds). - Occupied bars show guest name, colored by stay type (volunteer=blue, guest=green, staff=purple, facilitator=teal). - Clicking an occupied bar opens the stay detail. - Clicking an empty cell opens the room allocation form pre-filled with that room and date. - Horizontal scroll with sticky room name column on the left. - Time range selector above: week view (default), 2-week, month. - Room type filter: show all, private only, dorm only, etc. ```typescript type OccupancyGridProps = { rooms: Room[] allocations: RoomAllocation[] blocks: RoomBlock[] dateRange: [Date, Date] onCellClick: (roomId: string, date: Date) => void onAllocationClick: (allocationId: string) => void } ``` On mobile: the grid is too wide. Switch to a single-room view (select room from dropdown, see its calendar). Or a day-view showing all rooms for one day as a vertical list. ### 2.6 List/Detail Pattern for CRUD Entities Every domain entity (users, rooms, boats, retreats, inventory items, task templates) follows the same list/detail pattern. **List page structure:** ``` +-----------------------------------------------------+ | Page Title [+ Create] btn | +-----------------------------------------------------+ | Filters bar: | | [Status v] [Type v] [Search________] [Date range] | +-----------------------------------------------------+ | Table or Card list | | +--------------------------------------------------+| | | Name/Title | Status | Key Field | Actions || | +--------------------------------------------------+| | | Row 1 | Active | ... | [View] [Edit] || | | Row 2 | Draft | ... | [View] [Edit] || | +--------------------------------------------------+| | Pagination: [< 1 2 3 ... 10 >] | +-----------------------------------------------------+ ``` - On desktop: use Mantine `Table` with sortable columns. - On mobile: switch to card-based layout (each row becomes a card). - The `[View]` action navigates to a detail page or opens a detail drawer (depending on entity complexity). **Detail page structure:** ``` +-----------------------------------------------------+ | [< Back to list] | | Entity Title [StatusBadge] | | Created by X on Date | +-----------------------------------------------------+ | Tab bar: [Overview] [History] [Related] | +-----------------------------------------------------+ | Tab content (scrollable) | | | | Overview: Key-value detail grid | | History: Timeline of status changes (from audit log) | | Related: Linked entities (e.g., stay -> room alloc) | +-----------------------------------------------------+ | Action bar (sticky bottom on mobile): | | [Primary Action] [Secondary Action] [Cancel/Delete] | +-----------------------------------------------------+ ``` **Reusable components for this pattern:** ```typescript // Generic list page wrapper type EntityListPageProps = { title: string createHref?: string createPermission?: string filters: FilterConfig[] columns: ColumnConfig[] // For table view cardRenderer?: (item: T) => ReactNode // For card view fetchFn: (params: FilterParams) => Promise<{ items: T[]; total: number }> } // Generic detail page wrapper type EntityDetailPageProps = { title: string backHref: string status?: string entityType?: string tabs: TabConfig[] actions?: ActionConfig[] } ``` --- ## 3. State Management ### Auth Session ```typescript // contexts/auth-context.tsx type AuthState = { user: { userId: string email: string displayName: string roles: string[] } | null permissions: Set // Resolved from roles via ROLE_PERMISSIONS isLoading: boolean } const AuthContext = createContext(...) ``` Session is loaded on app mount via a `getSession` Pikku call. Stored in React context (not a global store — Mantine and Next.js App Router work well with context). **Permission checking hook:** ```typescript function usePermission(permission: string): boolean { const { permissions } = useAuth() return permissions.has(permission) || permissions.has('*') } function useAnyPermission(...perms: string[]): boolean { const { permissions } = useAuth() return permissions.has('*') || perms.some(p => permissions.has(p)) } ``` **Role-based rendering:** ```typescript // Wrapper component for permission gating function RequirePermission({ permission, children, fallback }: { permission: string children: ReactNode fallback?: ReactNode }) { const allowed = usePermission(permission) if (!allowed) return fallback ?? null return <>{children} } ``` Usage in pages: ```typescript // In a page component ``` Server-side: use Pikku session from cookies in server components to pre-check permissions and avoid rendering pages the user cannot access. Redirect at the layout/page level, not just hide UI. ### Optimistic Updates For state transitions (approve, reject, check-in, complete, etc.), use optimistic updates: ```typescript function useOptimisticTransition( items: T[], mutationFn: (id: string) => Promise ) { const [optimisticItems, setOptimisticItems] = useState(items) async function transition(id: string, newStatus: string) { // Immediately update local state setOptimisticItems(prev => prev.map(item => item.id === id ? { ...item, status: newStatus } : item) ) try { await mutationFn(id) // On success, refetch or keep optimistic state } catch { // Rollback on failure setOptimisticItems(items) // Show error notification } } return { items: optimisticItems, transition } } ``` Alternatively, use `@tanstack/react-query` (TanStack Query) for cache management and optimistic updates. This is the recommended approach since it handles cache invalidation, background refetching, and stale-while-revalidate automatically. ```typescript // Recommended: TanStack Query pattern const queryClient = useQueryClient() const approveStay = useMutation({ mutationFn: (requestId: string) => api.approveStayRequest({ requestId }), onMutate: async (requestId) => { await queryClient.cancelQueries({ queryKey: ['stayRequests'] }) const previous = queryClient.getQueryData(['stayRequests']) queryClient.setQueryData(['stayRequests'], (old) => old.map(r => r.requestId === requestId ? { ...r, status: 'approved' } : r) ) return { previous } }, onError: (_err, _id, context) => { queryClient.setQueryData(['stayRequests'], context.previous) }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['stayRequests'] }) }, }) ``` --- ## 4. Data Fetching Pattern ### When to Use Server vs Client **Server-side (RSC / SSR) — use for:** - Initial page loads: list pages, detail pages, dashboard summaries - Any data that does not change in response to user interaction on the same page - SEO-irrelevant here (internal app), but SSR still helps with perceived performance - Auth session resolution (read cookie, call Pikku `getSession` server-side) **Client-side (SWR/TanStack Query) — use for:** - Data that changes after user actions (approve a request, the list updates) - Polling / real-time data (notification count, task board) - Infinite scroll / pagination interactions - Form submissions and their responses - Any data dependent on client-side filter/search state ### Pikku RPC Integration Pikku functions are called via HTTP. Create a thin client wrapper: ```typescript // lib/api.ts import { createPikkuClient } from '@pikku/client' // Or a thin fetch wrapper const api = createPikkuClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:6002/api', }) // Server-side variant (includes cookie forwarding) export function createServerApi(cookies: ReadonlyHeaders) { return createPikkuClient({ baseUrl: process.env.API_URL ?? 'http://localhost:6002/api', headers: { cookie: cookies.get('cookie')?.value ?? '' }, }) } ``` **Server component example:** ```typescript // app/stays/requests/page.tsx (Server Component) import { createServerApi } from '@/lib/api' import { cookies } from 'next/headers' export default async function StayRequestsPage() { const api = createServerApi(await cookies()) const { items } = await api.listStayRequests({ limit: 50, offset: 0 }) return } ``` **Client component with TanStack Query:** ```typescript // components/stays/StayRequestsList.tsx 'use client' function StayRequestsList({ initialData }: { initialData: StayRequest[] }) { const { data } = useQuery({ queryKey: ['stayRequests', filters], queryFn: () => api.listStayRequests(filters), initialData: { items: initialData }, staleTime: 30_000, // 30s before refetch }) return /* render list with data.items */ } ``` ### Query Key Convention Consistent query keys for cache management: ``` ['stayRequests', { status, limit, offset }] ['stays', { status, limit, offset }] ['stay', stayId] ['rooms'] ['roomAllocations', { startDate, endDate }] ['boatTrips', { date }] ['boatRequests', { tripId }] ['tasks', { status, category }] ['retreats', { status }] ['retreat', retreatId] ['notifications', { limit, offset }] ['notificationCount'] // Unread count, polled frequently ['inventory'] ['inventoryRequests', { status }] ['users', { search }] ['mealServices', { date }] ``` --- ## 5. Mobile and Offline Considerations The island has limited and intermittent connectivity. This shapes the architecture significantly. ### Connectivity Tiers 1. **Online**: Full functionality, real-time updates. 2. **Slow/intermittent**: App works, but requests may be slow. Show loading states, avoid blocking the UI. 3. **Offline**: Read cached data, queue write actions for sync when back online. ### What Should Work Offline / Cached **Must cache (service worker + TanStack Query persistence):** - Current user session and permissions (avoid auth failures offline) - Today's boat schedule (critical for island operations) - Active task list for the current user - Room occupancy for the current week - Own stay details and dates - Notification history (already fetched) - Dietary profiles (kitchen needs this regardless of connectivity) **Queue for sync when online:** - Task status updates (claim, start, complete) — most common offline action - Meal attendance marking (kitchen staff marking who showed up) - Check-in/check-out (coordinator at the dock with no signal) ### Implementation ```typescript // Offline queue using IndexedDB (via idb-keyval or Dexie) type QueuedAction = { id: string endpoint: string method: string payload: unknown createdAt: Date retries: number } // On mutation when offline: if (!navigator.onLine) { await offlineQueue.add({ id: crypto.randomUUID(), endpoint: '/stays/check-in', method: 'POST', payload: { stayId }, createdAt: new Date(), retries: 0, }) // Update local cache optimistically // Show "queued" indicator on the item return } ``` **Sync indicator in the header:** ``` [Online] — green dot, hidden after 3s [Syncing (3)] — orange dot + count of queued actions [Offline] — red dot, persistent ``` ### TanStack Query Persistence Use `@tanstack/query-persist-client` with an IndexedDB persister to survive page reloads and brief offline periods: ```typescript const persister = createIDBPersister('perauset-cache') const queryClient = new QueryClient({ defaultOptions: { queries: { gcTime: 1000 * 60 * 60 * 24, // 24h garbage collection staleTime: 1000 * 30, // 30s stale time retry: 3, retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), }, }, }) ``` ### Mobile-Specific Adaptations | Desktop pattern | Mobile adaptation | |---|---| | Sidebar nav | Bottom tab bar + hamburger drawer | | Occupancy grid | Single-room calendar or day-view list | | Task kanban board | Status-grouped list with collapsible sections | | Boat timeline | Chronological trip list | | Table views | Card-based layouts | | Detail in page | Full-screen detail with back gesture | | Multi-column forms | Single-column stacked form | | Hover tooltips | Long-press or inline text | --- ## 6. Notification UX ### Bell Icon with Unread Count ``` Header: ... [Bell(3)] [Avatar] ... ``` The bell icon lives in the header, always visible. The unread count badge uses Mantine `Indicator`. ```typescript function NotificationBell() { const { data: count } = useQuery({ queryKey: ['notificationCount'], queryFn: () => api.listNotifications({ limit: 1, offset: 0 }) .then(res => res.items.filter(n => !n.readAt).length), refetchInterval: 30_000, // Poll every 30s when online refetchIntervalInBackground: false, }) // Better: dedicated unread-count endpoint to avoid fetching full list // For now, derive from list query cached data return ( 0 ? count : undefined} size={16} disabled={!count || count === 0} color="red" > ) } ``` ### Notification Panel Clicking the bell opens a dropdown panel (Mantine `Popover` on desktop, full-screen drawer on mobile). ``` +------------------------------------------+ | Notifications [Mark all read]| +------------------------------------------+ | * Stay request approved | <- unread (bold, blue dot) | Your stay Mar 20-27 was approved | | 2 hours ago | +------------------------------------------+ | Boat trip tomorrow | <- read (normal weight) | MV Seabird departs 08:00 | | 1 day ago | +------------------------------------------+ | Task assigned to you | | Kitchen cleanup - Building A | | 2 days ago | +------------------------------------------+ | [View all notifications] | +------------------------------------------+ ``` **Behavior:** - Clicking a notification marks it as read (call `markNotificationRead`) and navigates to the related entity if `entityType` and `entityId` are present. - Navigation mapping from notification: ```typescript function getNotificationHref(entityType: string | null, entityId: string | null): string | null { if (!entityType || !entityId) return null const routes: Record = { stay_request: `/stays/requests/${entityId}`, stay: `/stays/${entityId}`, boat_trip: `/boats/schedule/${entityId}`, boat_reservation_request: `/boats/requests/${entityId}`, task_instance: `/tasks/${entityId}`, retreat: `/retreats/${entityId}`, inventory_request: `/inventory/requests/${entityId}`, room_allocation: `/rooms/occupancy`, } return routes[entityType] ?? null } ``` - "Mark all read" calls `markNotificationRead` for each unread notification (batch endpoint would be a good backend addition). - The panel shows the 10 most recent. "View all" navigates to `/notifications` which is a full paginated list. ### Notification Types and Display Map notification `type` field to icons and colors: ```typescript const NOTIFICATION_CONFIG: Record = { stay_request_submitted: { icon: IconBed, color: 'blue' }, stay_request_approved: { icon: IconCheck, color: 'green' }, stay_request_rejected: { icon: IconX, color: 'red' }, stay_checked_in: { icon: IconLogin, color: 'teal' }, boat_trip_reminder: { icon: IconSailboat, color: 'blue' }, boat_request_confirmed: { icon: IconCheck, color: 'green' }, boat_request_declined: { icon: IconX, color: 'red' }, task_assigned: { icon: IconChecklist, color: 'violet' }, task_completed: { icon: IconCheck, color: 'green' }, retreat_published: { icon: IconMountain, color: 'blue' }, inventory_request_approved: { icon: IconPackage, color: 'green' }, inventory_request_fulfilled: { icon: IconCheck, color: 'green' }, generic: { icon: IconBell, color: 'gray' }, } ``` --- ## 7. Page Route Map ``` / -> Redirect to /dashboard /login -> Login page (public) /dashboard -> Role-adaptive dashboard /my-requests -> User's own requests (stays, boats, inventory) /notifications -> Full notification list /stays /stays/requests -> Stay request list (coordinator: review queue) /stays/requests/new -> New stay request form /stays/requests/[id] -> Stay request detail /stays/active -> Active stays list /stays/[id] -> Stay detail (check-in/out actions) /stays/checkin -> Quick check-in/out interface /rooms /rooms/occupancy -> Occupancy grid (calendar view) /rooms/manage -> Room list (CRUD) /rooms/[id] -> Room detail /boats /boats/schedule -> Boat trip timeline (day view) /boats/schedule/[id] -> Trip detail (manifest, requests) /boats/requests -> Seat request review queue /boats/request/new -> Request a seat form /boats/manage -> Boats + routes CRUD /tasks /tasks/board -> Kanban board /tasks/[id] -> Task detail /tasks/templates -> Task template management /tasks/claim -> Claimable tasks (community view) /retreats /retreats -> Retreat list /retreats/new -> Create retreat /retreats/[id] -> Retreat detail (people, schedule tabs) /kitchen /kitchen/meals -> Meal service calendar /kitchen/meals/[id] -> Meal detail (attendance, dietary summary) /kitchen/dietary -> All dietary profiles (kitchen lead view) /kitchen/my-dietary -> Own dietary profile (self-service) /inventory /inventory/items -> Item list with quantities /inventory/requests -> Request review queue /inventory/request/new -> Submit inventory request /people /people/users -> User directory /people/users/[id] -> User detail + role management /people/roles -> Role assignment interface /audit -> Audit log viewer ``` --- ## 8. Dashboard Design The dashboard is role-adaptive. Each widget checks permissions before rendering. **Coordinator/Admin dashboard:** ``` +---------------------------+---------------------------+ | Today's Overview | Pending Actions | | - 12 active stays | - 3 stay requests | | - 8 rooms occupied (60%) | - 5 boat seat requests | | - 2 boat trips scheduled | - 2 inventory requests | | - 4 tasks in progress | | +---------------------------+---------------------------+ | Today's Boat Schedule | Task Board Summary | | 08:00 -> Mainland (12/15) | Open: 3 Assigned: 5 | | 14:00 <- Mainland (8/20) | In Progress: 4 Blocked: 1| +---------------------------+---------------------------+ | This Week's Arrivals/Departures | | Mar 17: 2 arrivals, 1 departure | | Mar 18: 0 arrivals, 3 departures | | ... | +-------------------------------------------------------+ ``` **Community member dashboard:** ``` +---------------------------+---------------------------+ | My Stay | Upcoming Boats | | Mar 20-27 (Volunteer) | Mar 20 08:00 -> Island | | Status: Confirmed | Seat: Confirmed | | Room: Building A, Room 3 | | +---------------------------+---------------------------+ | My Tasks | Notifications | | Kitchen cleanup (today) | Stay approved (2h ago) | | Garden maintenance (tmrw) | Task assigned (1d ago) | +---------------------------+---------------------------+ | Upcoming Retreats | | Spring Meditation - Mar 25-30 (Published) | +-------------------------------------------------------+ ``` --- ## 9. Mantine v8 Component Mapping Key Mantine components used per pattern: | Pattern | Mantine Components | |---|---| | Shell layout | `AppShell`, `AppShell.Header`, `AppShell.Navbar`, `AppShell.Main`, `Burger` | | Navigation | `NavLink`, `ScrollArea`, `Drawer` (mobile) | | Request cards | `Card`, `Card.Section`, `Badge`, `Group`, `Stack`, `Text`, `Button` | | Status badges | `Badge` with `variant="light"` | | Data tables | `Table`, `Table.Thead`, `Table.Tr`, etc. (native Mantine, or `mantine-datatable`) | | Filters | `Select`, `MultiSelect`, `TextInput`, `DatePickerInput` | | Forms | `TextInput`, `Textarea`, `Select`, `NumberInput`, `DatePickerInput`, `Checkbox` | | Task board | `SimpleGrid`, `Card`, `ScrollArea` | | Detail pages | `Tabs`, `Stack`, `Group`, `Paper`, `Timeline` | | Modals/drawers | `Modal`, `Drawer` | | Notifications | `Popover`, `Indicator`, `ActionIcon`, `ScrollArea` | | Occupancy grid | Custom CSS Grid + `Tooltip`, `Paper` | | Theme toggle | `SegmentedControl` or `ActionIcon` with system/light/dark | | Loading | `Skeleton`, `LoadingOverlay`, `Progress` | | Empty states | `Center`, `Stack`, `Text`, `ThemeIcon` | --- ## 10. Theme System Use Mantine's built-in theme system with `useComputedColorScheme` and `useMantineColorScheme`. ```typescript // components/ThemeToggle.tsx function ThemeToggle() { const { setColorScheme, colorScheme } = useMantineColorScheme() return ( setColorScheme(value as 'light' | 'dark' | 'auto')} data={[ { label: 'Light', value: 'light' }, { label: 'Dark', value: 'dark' }, { label: 'Auto', value: 'auto' }, ]} /> ) } ``` Mantine v8 handles light/dark natively via CSS variables. No custom CSS variable system needed beyond Mantine's theme configuration in `MantineProvider`. Custom theme tokens go in the Mantine `createTheme` call: ```typescript const theme = createTheme({ primaryColor: 'teal', fontFamily: 'Inter, system-ui, sans-serif', headings: { fontFamily: 'Inter, system-ui, sans-serif' }, defaultRadius: 'md', other: { // App-specific tokens statusColors: STATUS_COLORS, }, }) ```