UX: Skeleton-Ladebildschirm (Seitenstruktur statt Text), shadcn Skeleton
Build & Deploy / deploy (push) Successful in 24s

This commit is contained in:
2026-10-04 22:22:59 +02:00
parent 717aef0d98
commit c4e402d6d2
3 changed files with 71 additions and 9 deletions
+2 -8
View File
@@ -2,6 +2,7 @@ import { Routes, Route, Navigate } from 'react-router'
import { Component, Suspense, lazy, type ReactNode } from 'react' import { Component, Suspense, lazy, type ReactNode } from 'react'
import { StoreProvider, useStore } from './store/store' import { StoreProvider, useStore } from './store/store'
import AppLayout from './components/AppLayout' import AppLayout from './components/AppLayout'
import LadeSkeleton from './components/LadeSkeleton'
import Login from './pages/Login' import Login from './pages/Login'
// Seiten werden beim ersten Aufruf nachgeladen (Code-Splitting) – // Seiten werden beim ersten Aufruf nachgeladen (Code-Splitting) –
@@ -51,14 +52,7 @@ class FehlerGrenze extends Component<{ children: ReactNode }, { fehler: boolean
} }
function Ladebildschirm() { function Ladebildschirm() {
return ( return <LadeSkeleton />
<div className="flex min-h-screen items-center justify-center bg-[#22333b]">
<div className="text-center text-slate-300">
<div className="text-lg font-semibold text-white">Hinterstein Immobilien</div>
<div className="mt-1 text-sm">Mieterportal wird geladen …</div>
</div>
</div>
)
} }
function RequireAuth({ children }: { children: React.ReactElement }) { function RequireAuth({ children }: { children: React.ReactElement }) {
+68
View File
@@ -0,0 +1,68 @@
import { Skeleton } from './ui/skeleton'
import logo from '../assets/logo.png'
/** Skeleton-Ladebildschirm: zeigt statt Text die ungefähre Seitenstruktur
* (Kopf, Suche, Listenzeilen) mit Puls-Animation – wirkt schneller und
* orientiert den Nutzer, während die Daten geladen werden. */
export default function LadeSkeleton() {
return (
<div className="flex min-h-screen bg-slate-50 dark:bg-slate-950" aria-busy="true" aria-label="Mieterportal wird geladen">
{/* Desktop-Sidebar */}
<aside className="hidden w-64 flex-col gap-4 border-r bg-[#22333b] p-4 lg:flex">
<div className="flex items-center gap-3 px-2 py-2">
<img src={logo} alt="" className="h-9 w-9 rounded-full bg-white object-contain p-0.5" />
<Skeleton className="h-4 w-32 bg-white/20" />
</div>
<div className="mt-4 space-y-2.5 px-2">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} className="h-8 w-full bg-white/10" />
))}
</div>
</aside>
<div className="flex min-w-0 flex-1 flex-col">
{/* Mobile top bar */}
<header className="flex items-center gap-3 border-b bg-white px-4 py-3 dark:bg-slate-900 lg:hidden">
<img src={logo} alt="" className="h-9 w-9 rounded-full bg-white object-contain p-0.5 ring-1 ring-slate-200" />
<Skeleton className="h-4 w-40" />
</header>
<main className="min-w-0 flex-1 space-y-6 p-4 lg:p-8">
{/* Seitenkopf */}
<div className="flex flex-wrap items-end justify-between gap-3">
<div className="space-y-2">
<Skeleton className="h-7 w-56" />
<Skeleton className="h-4 w-72 max-w-full" />
</div>
<Skeleton className="h-9 w-32" />
</div>
{/* Filterzeile */}
<div className="flex flex-wrap gap-2">
<Skeleton className="h-9 w-56" />
<Skeleton className="h-9 w-36" />
<Skeleton className="h-9 w-32" />
</div>
{/* Listenzeilen */}
<div className="overflow-hidden rounded-2xl border bg-white dark:border-slate-700 dark:bg-slate-900">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="flex items-center gap-4 border-b px-4 py-3.5 last:border-0 dark:border-slate-800"
>
<Skeleton className="h-8 w-8 shrink-0 rounded-xl" />
<div className="min-w-0 flex-1 space-y-2">
<Skeleton className="h-4 w-1/3" />
<Skeleton className="h-3 w-1/2" />
</div>
<Skeleton className="hidden h-4 w-24 sm:block" />
<Skeleton className="h-8 w-8" />
</div>
))}
</div>
</main>
</div>
</div>
)
}
+1 -1
View File
@@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
return ( return (
<div <div
data-slot="skeleton" data-slot="skeleton"
className={cn("bg-accent animate-pulse rounded-md", className)} className={cn("bg-slate-200/70 dark:bg-slate-800/60 animate-pulse rounded-md", className)}
{...props} {...props}
/> />
) )