fix(ui): mobile + dashboard polish + dev CSRF relaxation
- filter-bar: hide select / multi-select fields when the options list is
empty (was rendering bare "Tags" / "Status" labels above empty inputs)
- berth-detail-header: show "Berth A1" title on mobile (was hidden via
`hidden sm:block`)
- dashboard-shell: time-aware greeting (Good morning/afternoon/evening,
firstName) using the existing ['me'] cache; falls back to
"Welcome back" when firstName isn't set yet
- mobile-topbar: hide UUID-segment fallback title flash on detail-page
navigation — when the URL last segment is a UUID, walk up to the
parent collection name ("Clients", "Yachts") until the page sets the
real entity title via useMobileChrome
- mobile-bottom-tabs: subtle bg-primary/10 pill behind icon on active
tab for a clear "you are here" cue
- branded-auth-shell: lock to viewport via fixed/inset-0 so the iOS
Safari rubber-band bounce doesn't scroll the centered login card
- middleware: skip CSRF origin check in development. LAN testing
(real iPhone on 192.168.x.x hitting the Mac dev server while a Mac
browser tab is on localhost) trips the cross-origin defense; prod
keeps it as-is.
- package.json dev script: -H 0.0.0.0 so the dev server is reachable
from devices on the LAN
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
|
||||
import { usePortContext } from '@/providers/port-provider';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
import { PageHeader } from '@/components/shared/page-header';
|
||||
import { ActivityFeed } from './activity-feed';
|
||||
import { DateRangePicker } from './date-range-picker';
|
||||
@@ -39,11 +41,42 @@ function rangeLabel(range: DateRange): string {
|
||||
return PRESET_LABELS[range];
|
||||
}
|
||||
|
||||
interface MeData {
|
||||
data?: {
|
||||
firstName?: string | null;
|
||||
displayName?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
function timeOfDayGreeting(): string {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 5) return 'Up late';
|
||||
if (hour < 12) return 'Good morning';
|
||||
if (hour < 18) return 'Good afternoon';
|
||||
return 'Good evening';
|
||||
}
|
||||
|
||||
export function DashboardShell() {
|
||||
const [range, setRange] = useState<DateRange>('30d');
|
||||
const { currentPort } = usePortContext();
|
||||
const portName = currentPort?.name ?? 'this port';
|
||||
|
||||
// Reuses the existing ['me'] cache (5-minute staleTime) populated by
|
||||
// useTablePreferences elsewhere — usually a cache hit, so no extra
|
||||
// request. Falls back to a generic greeting if the profile isn't
|
||||
// available yet so we never block the dashboard render.
|
||||
const me = useQuery<MeData>({
|
||||
queryKey: ['me'],
|
||||
queryFn: ({ signal }) => apiFetch<MeData>('/api/v1/me', { signal }),
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
const firstName = me.data?.data?.firstName?.trim();
|
||||
// Time-aware greeting line, falls back to a generic "Welcome back" when
|
||||
// we don't know the user's first name yet (e.g. profile not filled out).
|
||||
const greeting = firstName
|
||||
? `${timeOfDayGreeting()}, ${firstName}`
|
||||
: 'Welcome back';
|
||||
|
||||
// Use a partial query-key prefix (no range segment) for invalidations.
|
||||
// Reading: "any cached analytics result, regardless of range, please
|
||||
// refetch on this event." This avoids any chance that a custom-range
|
||||
@@ -66,8 +99,8 @@ export function DashboardShell() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Dashboard"
|
||||
eyebrow="Overview"
|
||||
title={greeting}
|
||||
eyebrow="Dashboard"
|
||||
description={`Live snapshot of ${portName} activity`}
|
||||
kpiLine={<span>{rangeLabel(range)}</span>}
|
||||
variant="gradient"
|
||||
|
||||
Reference in New Issue
Block a user