fix(ui): admin settings loading-loop, real user name, expanded admin nav
All checks were successful
Build & Push Docker Images / lint (pull_request) Successful in 1m0s
Build & Push Docker Images / build-and-push (pull_request) Has been skipped

SettingsFormCard
- Parent components pass `FIELDS.slice(...)` inline, so the prop reference
  changes on every render. The fetch callback's useCallback re-created
  itself, useEffect re-fired, and loading flicker meant the form never
  rendered. Capture fields in a ref so the callback is stable.

Sidebar
- Show real user name + avatar initial from session/profile, replacing
  the hardcoded "User Name" / "U" placeholder.
- Default the admin-section to expanded so its items are reachable on
  first page load (was collapsed behind a chevron).

Dashboard layout
- Pass {name, email} from the session/profile through to <Sidebar />.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-04-27 23:44:04 +02:00
parent 4877b97f27
commit 0ccc66833d
3 changed files with 26 additions and 8 deletions

View File

@@ -38,7 +38,14 @@ export default async function DashboardLayout({ children }: { children: React.Re
<PermissionsProvider> <PermissionsProvider>
<SocketProvider> <SocketProvider>
<div className="flex h-screen overflow-hidden bg-background"> <div className="flex h-screen overflow-hidden bg-background">
<Sidebar portRoles={portRoles} isSuperAdmin={profile?.isSuperAdmin ?? false} /> <Sidebar
portRoles={portRoles}
isSuperAdmin={profile?.isSuperAdmin ?? false}
user={{
name: profile?.displayName ?? session.user.name ?? session.user.email,
email: session.user.email,
}}
/>
<div className="flex-1 flex flex-col overflow-hidden min-w-0"> <div className="flex-1 flex flex-col overflow-hidden min-w-0">
<Topbar ports={ports} /> <Topbar ports={ports} />
<main className="flex-1 overflow-y-auto bg-background p-6">{children}</main> <main className="flex-1 overflow-y-auto bg-background p-6">{children}</main>

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect, useState, type ReactNode } from 'react'; import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
@@ -67,12 +67,19 @@ export function SettingsFormCard({ title, description, fields, extra }: Settings
const [values, setValues] = useState<Record<string, unknown>>({}); const [values, setValues] = useState<Record<string, unknown>>({});
const [originals, setOriginals] = useState<Record<string, unknown>>({}); const [originals, setOriginals] = useState<Record<string, unknown>>({});
// Parent components often pass `FIELDS.slice(0, 5)` directly, so the prop
// reference changes on every render. Capture it in a ref so the fetch
// callback can read the current list without being re-created and looping
// through useEffect forever.
const fieldsRef = useRef(fields);
fieldsRef.current = fields;
const fetchValues = useCallback(async () => { const fetchValues = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
const res = await apiFetch<ListResponse>('/api/v1/admin/settings'); const res = await apiFetch<ListResponse>('/api/v1/admin/settings');
const next: Record<string, unknown> = {}; const next: Record<string, unknown> = {};
for (const field of fields) { for (const field of fieldsRef.current) {
const port = res.data.portSettings.find((s) => s.key === field.key); const port = res.data.portSettings.find((s) => s.key === field.key);
const global = res.data.globalSettings.find((s) => s.key === field.key); const global = res.data.globalSettings.find((s) => s.key === field.key);
next[field.key] = port?.value ?? global?.value ?? field.defaultValue; next[field.key] = port?.value ?? global?.value ?? field.defaultValue;
@@ -82,7 +89,7 @@ export function SettingsFormCard({ title, description, fields, extra }: Settings
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [fields]); }, []);
useEffect(() => { useEffect(() => {
void fetchValues(); void fetchValues();

View File

@@ -40,6 +40,7 @@ import type { Role } from '@/lib/db/schema/users';
interface SidebarProps { interface SidebarProps {
portRoles: (UserPortRole & { port: { id: string; slug: string; name: string }; role: Role })[]; portRoles: (UserPortRole & { port: { id: string; slug: string; name: string }; role: Role })[];
isSuperAdmin?: boolean; isSuperAdmin?: boolean;
user?: { name: string; email: string };
} }
interface NavItem { interface NavItem {
@@ -178,6 +179,7 @@ function SidebarContent({
hasAdminAccess, hasAdminAccess,
hasMarinaAccess, hasMarinaAccess,
hasResidentialAccess, hasResidentialAccess,
user,
}: { }: {
collapsed: boolean; collapsed: boolean;
portSlug: string | undefined; portSlug: string | undefined;
@@ -185,9 +187,10 @@ function SidebarContent({
hasAdminAccess: boolean; hasAdminAccess: boolean;
hasMarinaAccess: boolean; hasMarinaAccess: boolean;
hasResidentialAccess: boolean; hasResidentialAccess: boolean;
user?: SidebarProps['user'];
}) { }) {
const pathname = usePathname(); const pathname = usePathname();
const [adminExpanded, setAdminExpanded] = useState(false); const [adminExpanded, setAdminExpanded] = useState(true);
const sections = buildNavSections(portSlug); const sections = buildNavSections(portSlug);
function isActive(href: string, exact?: boolean): boolean { function isActive(href: string, exact?: boolean): boolean {
@@ -285,11 +288,11 @@ function SidebarContent({
<Avatar className="w-8 h-8 shrink-0"> <Avatar className="w-8 h-8 shrink-0">
<AvatarImage src={undefined} /> <AvatarImage src={undefined} />
<AvatarFallback className="bg-[#3a7bc8] text-white text-xs font-semibold"> <AvatarFallback className="bg-[#3a7bc8] text-white text-xs font-semibold">
U {(user?.name ?? 'U').slice(0, 1).toUpperCase()}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-white text-sm font-medium truncate">User Name</p> <p className="text-white text-sm font-medium truncate">{user?.name ?? 'User'}</p>
<Badge <Badge
variant="outline" variant="outline"
className="text-[10px] px-1.5 py-0 text-[#83aab1] border-[#474e66] mt-0.5" className="text-[10px] px-1.5 py-0 text-[#83aab1] border-[#474e66] mt-0.5"
@@ -304,7 +307,7 @@ function SidebarContent({
); );
} }
export function Sidebar({ portRoles, isSuperAdmin = false }: SidebarProps) { export function Sidebar({ portRoles, isSuperAdmin = false, user }: SidebarProps) {
const sidebarCollapsed = useUIStore((s) => s.sidebarCollapsed); const sidebarCollapsed = useUIStore((s) => s.sidebarCollapsed);
const toggleSidebar = useUIStore((s) => s.toggleSidebar); const toggleSidebar = useUIStore((s) => s.toggleSidebar);
const currentPortSlug = useUIStore((s) => s.currentPortSlug); const currentPortSlug = useUIStore((s) => s.currentPortSlug);
@@ -341,6 +344,7 @@ export function Sidebar({ portRoles, isSuperAdmin = false }: SidebarProps) {
hasAdminAccess={hasAdminAccess} hasAdminAccess={hasAdminAccess}
hasMarinaAccess={hasMarinaAccess} hasMarinaAccess={hasMarinaAccess}
hasResidentialAccess={hasResidentialAccess} hasResidentialAccess={hasResidentialAccess}
user={user}
/> />
{/* Collapse toggle */} {/* Collapse toggle */}