Improve frontend layout and auth routing

This commit is contained in:
2026-05-12 09:38:51 +08:00
parent 2138e1cc0d
commit f401a5a104
8 changed files with 347 additions and 125 deletions
+94 -18
View File
@@ -3,20 +3,41 @@
import { useEffect, Suspense } from 'react'; import { useEffect, Suspense } from 'react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store'; import { useAuthStore } from '@/lib/auth-store';
import { getUserInfo } from '@/lib/api';
import Layout from '@/components/layout'; import Layout from '@/components/layout';
import { Activity, BadgeCheck, Boxes, ShieldCheck } from 'lucide-react';
const cards = [
{
title: '欢迎回来',
value: 'IMTIM Portal',
description: '您已成功登录企业门户',
icon: ShieldCheck,
},
{
title: '账户状态',
value: '已激活',
description: '当前账号可正常访问系统',
icon: BadgeCheck,
},
{
title: '系统版本',
value: '1.0.0',
description: '基础登录门户服务',
icon: Boxes,
},
];
function DashboardContent() { function DashboardContent() {
const { login, initialize } = useAuthStore(); const { user, isAuthenticated, isInitialized, login, initialize } = useAuthStore();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const token = searchParams.get('token');
useEffect(() => { useEffect(() => {
initialize(); initialize();
}, [initialize]); }, [initialize]);
useEffect(() => { useEffect(() => {
const token = searchParams.get('token');
if (token) { if (token) {
const fetchUser = async () => { const fetchUser = async () => {
try { try {
@@ -39,25 +60,74 @@ function DashboardContent() {
}; };
fetchUser(); fetchUser();
} }
}, [searchParams, login, router]); }, [token, login, router]);
useEffect(() => {
if (isInitialized && !isAuthenticated && !token) {
router.replace('/login');
}
}, [isInitialized, isAuthenticated, token, router]);
if (token || !isInitialized || !isAuthenticated) {
return (
<div className="mx-auto flex min-h-[calc(100svh-9.6rem)] w-full max-w-6xl items-center justify-center px-4 py-10 sm:px-6">
<div className="rounded-xl border bg-white px-5 py-4 text-sm text-muted-foreground shadow-sm">
...
</div>
</div>
);
}
return ( return (
<div className="container mx-auto px-4 py-8"> <div className="mx-auto w-full max-w-6xl px-4 py-8 sm:px-6 sm:py-10">
<h1 className="text-3xl font-bold mb-6"></h1> <div className="mb-8 flex flex-col justify-between gap-4 sm:flex-row sm:items-end">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <div>
<div className="rounded-lg border p-6 shadow-sm"> <p className="text-sm font-medium text-primary"></p>
<h2 className="text-xl font-semibold mb-2"></h2> <h1 className="mt-2 text-3xl font-bold tracking-tight sm:text-4xl"></h1>
<p className="text-muted-foreground"> <p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
IMTIM Portal {user?.username ? `${user.username},查看登录状态、账户可用性和基础系统信息。` : '查看登录状态、账户可用性和基础系统信息。'}
</p> </p>
</div> </div>
<div className="rounded-lg border p-6 shadow-sm"> <div className="inline-flex w-fit items-center gap-2 rounded-full border bg-white px-3 py-1.5 text-sm font-medium text-emerald-700 shadow-sm">
<h2 className="text-xl font-semibold mb-2"></h2> <Activity className="h-4 w-4" />
<p className="text-green-600 font-medium"></p> 线
</div> </div>
<div className="rounded-lg border p-6 shadow-sm"> </div>
<h2 className="text-xl font-semibold mb-2"></h2>
<p className="text-muted-foreground"> 1.0.0</p> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{cards.map((card) => {
const Icon = card.icon;
return (
<div key={card.title} className="rounded-xl border bg-white p-5 shadow-sm sm:p-6">
<div className="mb-5 flex items-center justify-between gap-4">
<span className="flex h-11 w-11 items-center justify-center rounded-lg bg-primary/10 text-primary">
<Icon className="h-5 w-5" />
</span>
{card.title === '账户状态' ? (
<span className="rounded-full bg-emerald-100 px-2.5 py-1 text-xs font-medium text-emerald-700">
Active
</span>
) : null}
</div>
<h2 className="text-sm font-medium text-muted-foreground">{card.title}</h2>
<p className="mt-2 text-2xl font-semibold tracking-tight">{card.value}</p>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{card.description}</p>
</div>
);
})}
</div>
<div className="mt-4 rounded-xl border bg-white p-5 shadow-sm sm:mt-6 sm:p-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-lg font-semibold"></h2>
<p className="mt-1 text-sm text-muted-foreground">
OAuth
</p>
</div>
<span className="w-fit rounded-full bg-secondary px-3 py-1 text-sm font-medium text-secondary-foreground">
Ready
</span>
</div> </div>
</div> </div>
</div> </div>
@@ -67,7 +137,13 @@ function DashboardContent() {
export default function DashboardPage() { export default function DashboardPage() {
return ( return (
<Layout> <Layout>
<Suspense fallback={<div className="container mx-auto px-4 py-8">...</div>}> <Suspense
fallback={
<div className="mx-auto w-full max-w-6xl px-4 py-8 text-sm text-muted-foreground sm:px-6">
...
</div>
}
>
<DashboardContent /> <DashboardContent />
</Suspense> </Suspense>
</Layout> </Layout>
+46 -14
View File
@@ -1,34 +1,66 @@
@import "tailwindcss"; @import "tailwindcss";
:root { :root {
--background: oklch(1 0 0); --background: oklch(0.985 0.006 247.858);
--foreground: oklch(0.145 0 0); --foreground: oklch(0.21 0.034 264.665);
--card: oklch(1 0 0); --card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.21 0.034 264.665);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.21 0.034 264.665);
--primary: oklch(0.205 0 0); --primary: oklch(0.52 0.19 253.3);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0); --secondary: oklch(0.96 0.013 247.858);
--secondary-foreground: oklch(0.205 0 0); --secondary-foreground: oklch(0.279 0.041 260.031);
--muted: oklch(0.97 0 0); --muted: oklch(0.96 0.013 247.858);
--muted-foreground: oklch(0.556 0 0); --muted-foreground: oklch(0.446 0.043 257.281);
--accent: oklch(0.97 0 0); --accent: oklch(0.94 0.025 255.585);
--accent-foreground: oklch(0.205 0 0); --accent-foreground: oklch(0.279 0.041 260.031);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325); --destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0); --border: oklch(0.9 0.022 252.894);
--input: oklch(0.922 0 0); --input: oklch(0.9 0.022 252.894);
--ring: oklch(0.708 0 0); --ring: oklch(0.52 0.19 253.3);
--radius: 0.5rem; --radius: 0.5rem;
} }
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
}
* { * {
border-color: var(--border); border-color: var(--border);
} }
html {
min-width: 320px;
scroll-behavior: smooth;
}
body { body {
background-color: var(--background); background-color: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: system-ui, -apple-system, sans-serif; font-family: system-ui, -apple-system, sans-serif;
} }
button,
a {
-webkit-tap-highlight-color: transparent;
}
+58 -13
View File
@@ -4,18 +4,22 @@ import { useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store'; import { useAuthStore } from '@/lib/auth-store';
import { getLoginUrl } from '@/lib/api'; import { getLoginUrl } from '@/lib/api';
import { LogIn } from 'lucide-react'; import { BadgeCheck, KeyRound, LogIn } from 'lucide-react';
import Layout from '@/components/layout'; import Layout from '@/components/layout';
export default function LoginPage() { export default function LoginPage() {
const { isAuthenticated } = useAuthStore(); const { isAuthenticated, isInitialized, initialize } = useAuthStore();
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
if (isAuthenticated) { initialize();
router.push('/dashboard'); }, [initialize]);
useEffect(() => {
if (isInitialized && isAuthenticated) {
router.replace('/dashboard');
} }
}, [isAuthenticated, router]); }, [isInitialized, isAuthenticated, router]);
const handleLogin = async () => { const handleLogin = async () => {
try { try {
@@ -28,25 +32,66 @@ export default function LoginPage() {
return ( return (
<Layout> <Layout>
<div className="flex items-center justify-center min-h-[calc(100vh-12rem)]"> <div className="mx-auto grid min-h-[calc(100svh-9.6rem)] w-full max-w-6xl items-center gap-10 px-4 py-10 sm:px-6 lg:grid-cols-[1fr_0.88fr]">
<div className="w-full max-w-md"> <section className="hidden lg:block">
<div className="rounded-lg border p-8 shadow-sm"> <p className="text-sm font-medium text-primary"></p>
<div className="text-center mb-8"> <h1 className="mt-3 max-w-xl text-4xl font-bold tracking-tight">
<h1 className="text-2xl font-bold mb-2"></h1> Casdoor IMTIM Portal
<p className="text-muted-foreground"> </h1>
<div className="mt-8 grid max-w-xl gap-4">
<div className="rounded-xl border bg-white p-5 shadow-sm">
<div className="flex items-start gap-4">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<KeyRound className="h-5 w-5" />
</span>
<div>
<h2 className="font-semibold"></h2>
<p className="mt-1 text-sm leading-6 text-muted-foreground">
使
</p>
</div>
</div>
</div>
<div className="rounded-xl border bg-white p-5 shadow-sm">
<div className="flex items-start gap-4">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-emerald-100 text-emerald-700">
<BadgeCheck className="h-5 w-5" />
</span>
<div>
<h2 className="font-semibold"></h2>
<p className="mt-1 text-sm leading-6 text-muted-foreground">
</p>
</div>
</div>
</div>
</div>
</section>
<section className="mx-auto w-full max-w-md lg:mx-0 lg:justify-self-end">
<div className="rounded-xl border bg-white p-6 shadow-sm sm:p-8">
<div className="mb-8">
<div className="mb-5 flex h-12 w-12 items-center justify-center rounded-xl bg-primary text-primary-foreground">
<LogIn className="h-6 w-6" />
</div>
<h1 className="text-2xl font-semibold tracking-tight"></h1>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
使 Casdoor IMTIM Portal 使 Casdoor IMTIM Portal
</p> </p>
</div> </div>
<button <button
onClick={handleLogin} onClick={handleLogin}
className="w-full flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors" className="flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
> >
<LogIn className="h-5 w-5" /> <LogIn className="h-5 w-5" />
<span>使 Casdoor </span> <span>使 Casdoor </span>
</button> </button>
<p className="mt-5 text-center text-xs leading-5 text-muted-foreground">
Casdoor
</p>
</div> </div>
</div> </section>
</div> </div>
</Layout> </Layout>
); );
+100 -50
View File
@@ -3,71 +3,121 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store'; import { useAuthStore } from '@/lib/auth-store';
import { ArrowRight, Shield, Zap, Users } from 'lucide-react'; import { ArrowRight, CheckCircle2, Shield, Users, Zap } from 'lucide-react';
import Layout from '@/components/layout'; import Layout from '@/components/layout';
const features = [
{
title: '安全可靠',
description: '基于 OAuth 2.0 的企业级认证,支持统一身份入口和多因素认证。',
icon: Shield,
},
{
title: '快速部署',
description: '面向容器化交付,适合快速接入现有服务并降低运维成本。',
icon: Zap,
},
{
title: '易于扩展',
description: '模块化架构设计,便于扩展自定义业务逻辑和第三方集成。',
icon: Users,
},
];
const metrics = [
['OAuth 2.0', '认证协议'],
['Docker', '部署方式'],
['SSO', '统一登录'],
];
export default function Home() { export default function Home() {
const { isAuthenticated, initialize } = useAuthStore(); const { initialize } = useAuthStore();
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
initialize(); initialize();
}, [initialize]); }, [initialize]);
useEffect(() => {
if (isAuthenticated) {
router.push('/dashboard');
}
}, [isAuthenticated, router]);
return ( return (
<Layout> <Layout>
<div className="flex flex-col items-center"> <div className="overflow-hidden">
{/* Hero Section */} <section className="border-b bg-[linear-gradient(180deg,white_0%,oklch(0.96_0.013_247.858)_100%)]">
<section className="w-full py-24 bg-gradient-to-b from-background to-secondary/20"> <div className="mx-auto grid min-h-[calc(100svh-9.6rem)] w-full max-w-6xl items-center gap-10 px-4 py-12 sm:px-6 sm:py-16 lg:grid-cols-[1fr_0.84fr] lg:py-20">
<div className="container px-4 text-center"> <div className="max-w-2xl">
<h1 className="text-4xl md:text-6xl font-bold tracking-tight mb-4"> <div className="mb-5 inline-flex items-center gap-2 rounded-full border bg-white px-3 py-1 text-sm font-medium text-muted-foreground shadow-sm">
IMTIM Portal <CheckCircle2 className="h-4 w-4 text-emerald-600" />
</h1>
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto"> </div>
<h1 className="text-4xl font-bold tracking-tight text-foreground sm:text-5xl lg:text-6xl">
IMTIM Portal
</h1>
<p className="mt-5 max-w-xl text-base leading-7 text-muted-foreground sm:text-lg">
</p> </p>
<button <div className="mt-8 flex flex-col gap-3 sm:flex-row">
onClick={() => router.push('/login')} <button
className="inline-flex items-center gap-2 px-6 py-3 text-base font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90" onClick={() => router.push('/login')}
> className="inline-flex h-12 items-center justify-center gap-2 rounded-lg bg-primary px-6 text-base font-medium text-primary-foreground shadow-sm transition hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
使 >
<ArrowRight className="h-5 w-5" /> 使
</button> <ArrowRight className="h-5 w-5" />
</button>
<button
onClick={() => router.push('/dashboard')}
className="inline-flex h-12 items-center justify-center rounded-lg border bg-white px-6 text-base font-medium shadow-sm transition hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
</button>
</div>
</div>
<div className="rounded-xl border bg-white p-4 shadow-sm sm:p-5">
<div className="rounded-lg bg-secondary p-4">
<div className="flex items-center justify-between gap-4 border-b pb-4">
<div>
<p className="text-sm text-muted-foreground"></p>
<p className="mt-1 text-2xl font-semibold"></p>
</div>
<span className="rounded-full bg-emerald-100 px-3 py-1 text-sm font-medium text-emerald-700">
Active
</span>
</div>
<div className="mt-4 grid gap-3 sm:grid-cols-3 lg:grid-cols-1 xl:grid-cols-3">
{metrics.map(([value, label]) => (
<div key={label} className="rounded-lg bg-white p-4">
<p className="text-lg font-semibold">{value}</p>
<p className="mt-1 text-sm text-muted-foreground">{label}</p>
</div>
))}
</div>
</div>
</div>
</div> </div>
</section> </section>
{/* Features Section */} <section className="bg-white py-12 sm:py-16">
<section className="w-full py-16"> <div className="mx-auto w-full max-w-6xl px-4 sm:px-6">
<div className="container px-4"> <div className="max-w-2xl">
<h2 className="text-3xl font-bold text-center mb-12"></h2> <h2 className="text-2xl font-semibold tracking-tight sm:text-3xl"></h2>
<div className="grid gap-8 md:grid-cols-3 max-w-5xl mx-auto"> <p className="mt-3 text-muted-foreground">
<div className="rounded-lg border p-6 text-center">
<Shield className="h-12 w-12 mx-auto mb-4 text-primary" /> </p>
<h3 className="text-xl font-semibold mb-2"></h3> </div>
<p className="text-muted-foreground"> <div className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
OAuth 2.0 {features.map((feature) => {
</p> const Icon = feature.icon;
</div> return (
<div className="rounded-lg border p-6 text-center"> <div key={feature.title} className="rounded-xl border bg-card p-6 shadow-sm">
<Zap className="h-12 w-12 mx-auto mb-4 text-primary" /> <div className="mb-5 flex h-11 w-11 items-center justify-center rounded-lg bg-primary/10 text-primary">
<h3 className="text-xl font-semibold mb-2"></h3> <Icon className="h-5 w-5" />
<p className="text-muted-foreground"> </div>
Docker <h3 className="text-lg font-semibold">{feature.title}</h3>
</p> <p className="mt-2 text-sm leading-6 text-muted-foreground">
</div> {feature.description}
<div className="rounded-lg border p-6 text-center"> </p>
<Users className="h-12 w-12 mx-auto mb-4 text-primary" /> </div>
<h3 className="text-xl font-semibold mb-2"></h3> );
<p className="text-muted-foreground"> })}
</p>
</div>
</div> </div>
</div> </div>
</section> </section>
+21 -14
View File
@@ -1,7 +1,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { useAuthStore } from '@/lib/auth-store'; import { useAuthStore } from '@/lib/auth-store';
import { getLoginUrl } from '@/lib/api'; import { getLoginUrl } from '@/lib/api';
import { LogOut, User } from 'lucide-react'; import { LogIn, LogOut, User } from 'lucide-react';
import * as Avatar from '@radix-ui/react-avatar'; import * as Avatar from '@radix-ui/react-avatar';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
@@ -23,18 +23,23 @@ export default function Header() {
}; };
return ( return (
<header className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <header className="sticky top-0 z-40 border-b bg-white/85 backdrop-blur-xl supports-[backdrop-filter]:bg-white/70">
<div className="container flex h-16 items-center justify-between px-4"> <div className="mx-auto flex h-16 w-full max-w-6xl items-center justify-between gap-3 px-4 sm:px-6">
<Link href="/" className="text-xl font-bold"> <Link href="/" className="flex min-w-0 items-center gap-3">
IMTIM Portal <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary text-sm font-bold text-primary-foreground shadow-sm">
IM
</span>
<span className="truncate text-base font-semibold tracking-tight sm:text-lg">
IMTIM Portal
</span>
</Link> </Link>
<nav className="flex items-center gap-4"> <nav className="flex shrink-0 items-center gap-2">
{isAuthenticated && user ? ( {isAuthenticated && user ? (
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger asChild> <DropdownMenu.Trigger asChild>
<button className="flex items-center gap-2 rounded-full hover:bg-accent p-1"> <button className="flex max-w-[44vw] items-center gap-2 rounded-full border bg-white p-1 pr-2 text-left shadow-sm transition hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:max-w-none sm:pr-3">
<Avatar.Root className="h-8 w-8 rounded-full bg-secondary flex items-center justify-center overflow-hidden"> <Avatar.Root className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-secondary">
{user.avatar ? ( {user.avatar ? (
<Avatar.Image <Avatar.Image
src={user.avatar} src={user.avatar}
@@ -47,16 +52,17 @@ export default function Header() {
</Avatar.Fallback> </Avatar.Fallback>
)} )}
</Avatar.Root> </Avatar.Root>
<span className="text-sm font-medium">{user.username}</span> <span className="truncate text-sm font-medium">{user.username}</span>
</button> </button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content <DropdownMenu.Content
className="min-w-[220px] bg-popover rounded-md p-1 shadow-md border" className="z-50 min-w-56 rounded-lg border bg-popover p-1.5 shadow-xl"
align="end"
sideOffset={5} sideOffset={5}
> >
<DropdownMenu.Item className="flex items-center gap-2 px-3 py-2 text-sm rounded cursor-pointer hover:bg-accent outline-none"> <DropdownMenu.Item className="flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-sm outline-none hover:bg-accent focus:bg-accent">
<User className="h-4 w-4" /> <User className="h-4 w-4" />
<span></span> <span></span>
</DropdownMenu.Item> </DropdownMenu.Item>
@@ -64,7 +70,7 @@ export default function Header() {
<DropdownMenu.Separator className="h-px bg-border my-1" /> <DropdownMenu.Separator className="h-px bg-border my-1" />
<DropdownMenu.Item <DropdownMenu.Item
className="flex items-center gap-2 px-3 py-2 text-sm rounded cursor-pointer hover:bg-accent text-red-600 outline-none" className="flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-sm text-red-600 outline-none hover:bg-red-50 focus:bg-red-50"
onClick={handleLogout} onClick={handleLogout}
> >
<LogOut className="h-4 w-4" /> <LogOut className="h-4 w-4" />
@@ -76,9 +82,10 @@ export default function Header() {
) : ( ) : (
<button <button
onClick={handleLogin} onClick={handleLogin}
className="px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90" className="inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground shadow-sm transition hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:px-4"
> >
<LogIn className="h-4 w-4" />
<span></span>
</button> </button>
)} )}
</nav> </nav>
+3 -3
View File
@@ -6,11 +6,11 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<div className="min-h-screen flex flex-col"> <div className="flex min-h-screen flex-col bg-background text-foreground">
<Header /> <Header />
<main className="flex-1">{children}</main> <main className="flex-1">{children}</main>
<footer className="border-t py-6"> <footer className="border-t bg-white/70 py-5">
<div className="container px-4 text-center text-sm text-muted-foreground"> <div className="mx-auto w-full max-w-6xl px-4 text-center text-xs text-muted-foreground sm:px-6">
© 2026 IMTIM Portal. All rights reserved. © 2026 IMTIM Portal. All rights reserved.
</div> </div>
</footer> </footer>
+9 -7
View File
@@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/lib/auth-store'; import { useAuthStore } from '@/lib/auth-store';
export default function ProtectedRoute({ children }: { children: React.ReactNode }) { export default function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, initialize } = useAuthStore(); const { isAuthenticated, isInitialized, initialize } = useAuthStore();
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
@@ -13,15 +13,17 @@ export default function ProtectedRoute({ children }: { children: React.ReactNode
}, [initialize]); }, [initialize]);
useEffect(() => { useEffect(() => {
if (!isAuthenticated) { if (isInitialized && !isAuthenticated) {
router.push('/login'); router.replace('/login');
} }
}, [isAuthenticated, router]); }, [isInitialized, isAuthenticated, router]);
if (!isAuthenticated) { if (!isInitialized || !isAuthenticated) {
return ( return (
<div className="flex items-center justify-center min-h-[200px]"> <div className="mx-auto flex min-h-[calc(100svh-9.6rem)] w-full max-w-6xl items-center justify-center px-4 py-10 sm:px-6">
<div className="text-muted-foreground">...</div> <div className="rounded-xl border bg-white px-5 py-4 text-sm text-muted-foreground shadow-sm">
...
</div>
</div> </div>
); );
} }
+16 -6
View File
@@ -13,6 +13,7 @@ interface AuthState {
user: User | null; user: User | null;
token: string | null; token: string | null;
isAuthenticated: boolean; isAuthenticated: boolean;
isInitialized: boolean;
login: (token: string, user: User) => void; login: (token: string, user: User) => void;
logout: () => void; logout: () => void;
initialize: () => void; initialize: () => void;
@@ -22,6 +23,7 @@ export const useAuthStore = create<AuthState>((set) => ({
user: null, user: null,
token: null, token: null,
isAuthenticated: false, isAuthenticated: false,
isInitialized: false,
login: (token, user) => { login: (token, user) => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -36,7 +38,7 @@ export const useAuthStore = create<AuthState>((set) => ({
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('user'); localStorage.removeItem('user');
} }
set({ user: null, token: null, isAuthenticated: false }); set({ user: null, token: null, isAuthenticated: false, isInitialized: true });
}, },
initialize: () => { initialize: () => {
@@ -44,12 +46,20 @@ export const useAuthStore = create<AuthState>((set) => ({
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
const userStr = localStorage.getItem('user'); const userStr = localStorage.getItem('user');
if (token && userStr) { if (token && userStr) {
set({ try {
token, set({
user: JSON.parse(userStr), token,
isAuthenticated: true, user: JSON.parse(userStr),
}); isAuthenticated: true,
isInitialized: true,
});
return;
} catch {
localStorage.removeItem('token');
localStorage.removeItem('user');
}
} }
} }
set({ user: null, token: null, isAuthenticated: false, isInitialized: true });
}, },
})); }));