🐛 fix: align dashboard with secured APIs

This commit is contained in:
2026-08-13 03:29:33 +07:00
parent e90dc8382b
commit 3f09e330be
47 changed files with 1280 additions and 397 deletions
+96 -16
View File
@@ -1,6 +1,11 @@
import { Loader2 } from "lucide-react";
"use client";
import { useRef } from "react";
import type * as React from "react";
import { FadeIn } from "@/components/motion";
import { BrandMark } from "@/components/brand";
import { cn } from "@/lib/cn";
import { gsap, prefersReducedMotion, useGSAP } from "@/lib/motion";
type PageHeaderProps = {
eyebrow: string;
@@ -24,17 +29,19 @@ export function PageHeader({
className,
}: PageHeaderProps) {
return (
<header className={cn("page-heading", className)}>
<div className={cn(leading && "flex items-center gap-4")}>
{leading}
<div>
<span className="page-eyebrow">{eyebrow}</span>
<h1 className="page-title">{title}</h1>
{description && <p className="page-description">{description}</p>}
<FadeIn y={12} duration={0.45}>
<header className={cn("page-heading", className)}>
<div className={cn(leading && "flex items-center gap-4")}>
{leading}
<div>
<span className="page-eyebrow">{eyebrow}</span>
<h1 className="page-title">{title}</h1>
{description && <p className="page-description">{description}</p>}
</div>
</div>
</div>
{actions}
</header>
{actions}
</header>
</FadeIn>
);
}
@@ -58,7 +65,9 @@ export function StatePanel({
...props
}: StatePanelProps) {
return (
<div
<FadeIn
y={10}
duration={0.4}
className={cn(
"flex min-h-48 items-center justify-center border border-dashed bg-card/60 p-6 text-center",
className
@@ -67,7 +76,7 @@ export function StatePanel({
>
<div className="flex max-w-lg flex-col items-center gap-2">
{loading ? (
<Loader2 className="mb-2 size-7 animate-spin text-muted-foreground" />
<LoaderMark className="mb-2" />
) : (
icon && <div className="mb-2 text-muted-foreground">{icon}</div>
)}
@@ -75,16 +84,87 @@ export function StatePanel({
{description && <div className="text-sm text-muted-foreground">{description}</div>}
{action && <div className="mt-3">{action}</div>}
</div>
</FadeIn>
);
}
function LoaderMark({ className }: { className?: string }) {
const ref = useRef<HTMLDivElement>(null);
useGSAP(
() => {
const el = ref.current;
if (!el || prefersReducedMotion()) return;
gsap.to(el, {
rotate: 180,
duration: 1.1,
repeat: -1,
yoyo: true,
ease: "power1.inOut",
});
},
{ scope: ref }
);
return (
<div ref={ref} className={cn("text-muted-foreground", className)}>
<BrandMark className="size-8 text-sm" />
</div>
);
}
export function FullScreenLoader({ label }: { label?: string }) {
const rootRef = useRef<HTMLDivElement>(null);
useGSAP(
() => {
const root = rootRef.current;
if (!root || prefersReducedMotion()) return;
const mark = root.querySelector("[data-loader-mark]");
const ring = root.querySelector("[data-loader-ring]");
const copy = root.querySelector("[data-loader-copy]");
gsap.fromTo(
[mark, copy],
{ autoAlpha: 0, y: 8 },
{ autoAlpha: 1, y: 0, duration: 0.45, stagger: 0.08, ease: "power3.out" }
);
gsap.to(mark, {
scale: 1.06,
duration: 0.9,
repeat: -1,
yoyo: true,
ease: "sine.inOut",
});
gsap.to(ring, {
rotate: 360,
duration: 2.4,
repeat: -1,
ease: "none",
});
},
{ scope: rootRef }
);
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-3 bg-sidebar text-sidebar-foreground">
<Loader2 className="size-8 animate-spin text-sidebar-primary" />
<div
ref={rootRef}
className="flex min-h-screen flex-col items-center justify-center gap-4 bg-sidebar text-sidebar-foreground"
>
<div className="relative grid size-16 place-items-center">
<span
data-loader-ring=""
className="absolute inset-0 border border-sidebar-primary/35 border-t-sidebar-primary"
/>
<div data-loader-mark="">
<BrandMark className="size-11" />
</div>
</div>
{label && (
<span className="font-mono text-[10px] font-bold uppercase tracking-widest text-sidebar-foreground/50">
<span
data-loader-copy=""
className="font-mono text-[10px] font-bold uppercase tracking-widest text-sidebar-foreground/50"
>
{label}
</span>
)}