fix(ui): admin settings loading-loop, real user name, expanded admin nav
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:
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user