completed the frontend dashboard component and bound it to the tenant admin pwa for the optimal onboarding experience.. Added a profile page.

This commit is contained in:
Codex Agent
2025-11-04 22:28:37 +01:00
parent fe380689fb
commit b32413b108
29 changed files with 1416 additions and 425 deletions

View File

@@ -1,14 +1,19 @@
import AppLogoIcon from './app-logo-icon';
import { usePage } from '@inertiajs/react';
import { type SharedData } from '@/types';
export default function AppLogo() {
const { translations } = usePage<SharedData>().props;
const areaLabel =
(translations?.dashboard?.navigation?.group_label as string | undefined) ?? 'Kundenbereich';
return (
<>
<div className="flex aspect-square size-8 items-center justify-center rounded-md bg-sidebar-primary text-sidebar-primary-foreground">
<AppLogoIcon className="size-5 fill-current text-white dark:text-black" />
<div className="flex items-center gap-3">
<img src="/logo-transparent-md.png" alt="Fotospiel" className="h-10 w-auto" />
<div className="grid text-left leading-tight">
<span className="text-sm font-semibold text-sidebar-foreground">Fotospiel</span>
<span className="text-xs text-sidebar-foreground/70">{areaLabel}</span>
</div>
<div className="ml-1 grid flex-1 text-left text-sm">
<span className="mb-0.5 truncate leading-tight font-semibold">Laravel Starter Kit</span>
</div>
</>
</div>
);
}

View File

@@ -8,11 +8,11 @@ interface AppShellProps {
}
export function AppShell({ children, variant = 'header' }: AppShellProps) {
const isOpen = usePage<SharedData>().props.sidebarOpen;
const { sidebarOpen = false } = usePage<SharedData>().props;
if (variant === 'header') {
return <div className="flex min-h-screen w-full flex-col">{children}</div>;
}
return <SidebarProvider defaultOpen={isOpen}>{children}</SidebarProvider>;
return <SidebarProvider defaultOpen={sidebarOpen}>{children}</SidebarProvider>;
}

View File

@@ -1,11 +1,10 @@
import { NavFooter } from '@/components/nav-footer';
import { NavMain } from '@/components/nav-main';
import { NavUser } from '@/components/nav-user';
import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { dashboard } from '@/routes';
import { type NavItem } from '@/types';
import { Link } from '@inertiajs/react';
import { BookOpen, Folder, LayoutGrid, UserRound } from 'lucide-react';
import { LayoutGrid, UserRound } from 'lucide-react';
import AppLogo from './app-logo';
const mainNavItems: NavItem[] = [
@@ -21,19 +20,6 @@ const mainNavItems: NavItem[] = [
},
];
const footerNavItems: NavItem[] = [
{
title: 'Repository',
href: 'https://github.com/laravel/react-starter-kit',
icon: Folder,
},
{
title: 'Documentation',
href: 'https://laravel.com/docs/starter-kits#react',
icon: BookOpen,
},
];
export function AppSidebar() {
return (
<Sidebar collapsible="icon" variant="inset">
@@ -54,7 +40,6 @@ export function AppSidebar() {
</SidebarContent>
<SidebarFooter>
<NavFooter items={footerNavItems} className="mt-auto" />
<NavUser />
</SidebarFooter>
</Sidebar>

View File

@@ -0,0 +1,86 @@
import { useState } from 'react';
import { router, usePage } from '@inertiajs/react';
import { Check, Languages } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { type SharedData } from '@/types';
import { setLocale } from '@/routes';
export function DashboardLanguageSwitcher() {
const page = usePage<SharedData>();
const { locale, supportedLocales, translations } = page.props;
const locales = supportedLocales && supportedLocales.length > 0 ? supportedLocales : ['de', 'en'];
const activeLocale = locales.includes(locale as string) && typeof locale === 'string' ? locale : locales[0];
const languageCopy = (translations?.dashboard?.language_switcher as Record<string, string | undefined>) ?? {};
const label = languageCopy.label ?? 'Sprache';
const changeLabel = languageCopy.change ?? 'Sprache wechseln';
const [pendingLocale, setPendingLocale] = useState<string | null>(null);
const handleChange = (nextLocale: string) => {
if (nextLocale === activeLocale || pendingLocale === nextLocale || !locales.includes(nextLocale)) {
return;
}
setPendingLocale(nextLocale);
router.post(
setLocale().url,
{ locale: nextLocale },
{
preserveScroll: true,
preserveState: true,
onFinish: () => setPendingLocale(null),
},
);
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="sm"
className="flex items-center gap-2 rounded-full border-white/30 bg-white/80 px-3 text-xs font-semibold uppercase tracking-wide text-slate-900 backdrop-blur transition hover:bg-white dark:border-white/20 dark:bg-white/10 dark:text-white"
aria-label={label}
>
<Languages className="size-4" />
<span>{label}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[9rem]">
<DropdownMenuLabel className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
{changeLabel}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{locales.map((code) => {
const isActive = code === activeLocale;
const isPending = code === pendingLocale;
return (
<DropdownMenuItem
key={code}
onSelect={(event) => {
event.preventDefault();
handleChange(code);
}}
disabled={isPending}
className="flex items-center justify-between gap-3"
>
<span className="text-sm font-medium uppercase tracking-wide">{code}</span>
{(isActive || isPending) && <Check className="size-4 text-pink-500" />}
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -1,12 +1,16 @@
import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar';
import { type NavItem } from '@/types';
import { type NavItem, type SharedData } from '@/types';
import { Link, usePage } from '@inertiajs/react';
export function NavMain({ items = [] }: { items: NavItem[] }) {
const page = usePage();
const page = usePage<SharedData>();
const { translations } = page.props;
const groupLabel =
(translations?.dashboard?.navigation?.group_label as string | undefined) ?? 'Kundenbereich';
return (
<SidebarGroup className="px-2 py-0">
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarGroupLabel>{groupLabel}</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>

View File

@@ -2,10 +2,10 @@ import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSep
import { UserInfo } from '@/components/user-info';
import { useMobileNavigation } from '@/hooks/use-mobile-navigation';
import { logout } from '@/routes';
import { edit } from '@/routes/settings/profile';
import profileRoutes from '@/routes/profile';
import { type User } from '@/types';
import { Link, router } from '@inertiajs/react';
import { LogOut, Settings } from 'lucide-react';
import { LogOut, UserRound } from 'lucide-react';
interface UserMenuContentProps {
user: User;
@@ -29,9 +29,9 @@ export function UserMenuContent({ user }: UserMenuContentProps) {
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem asChild>
<Link className="block w-full" href={edit()} as="button" prefetch onClick={cleanup}>
<Settings className="mr-2" />
Settings
<Link className="block w-full" href={profileRoutes.index().url} as="button" prefetch onClick={cleanup}>
<UserRound className="mr-2" />
Profil
</Link>
</DropdownMenuItem>
</DropdownMenuGroup>
@@ -39,7 +39,7 @@ export function UserMenuContent({ user }: UserMenuContentProps) {
<DropdownMenuItem asChild>
<Link className="block w-full" href={logout()} as="button" onClick={handleLogout}>
<LogOut className="mr-2" />
Log out
Abmelden
</Link>
</DropdownMenuItem>
</>