Skip to content

Commit

Permalink
feat: add responsive admin dashboard with login
Browse files Browse the repository at this point in the history
- Add protected admin login page with password authentication
- Add secure admin dashboard with responsive stats cards
- Implement admin route protection in middleware
- Add admin link to navbar
- Fix Suspense boundary for useSearchParams
  • Loading branch information
while-basic committed Dec 3, 2024
1 parent ed58b4b commit 05a451c
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 2 deletions.
174 changes: 174 additions & 0 deletions app/admin/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"use client";

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useState } from "react";

interface DashboardStats {
totalUsers: number;
emailsCollected: number;
blogPosts: number;
totalViews: number;
}

export default function AdminDashboard() {
const [stats] = useState<DashboardStats>({
totalUsers: 1234,
emailsCollected: 567,
blogPosts: 15,
totalViews: 45678,
});

return (
<div className="p-8">
<h1 className="text-3xl font-bold mb-8">Portfolio Dashboard</h1>

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalUsers}</div>
<p className="text-xs text-muted-foreground">
+20.1% from last month
</p>
</CardContent>
</Card>

<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Emails Collected</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M22 17H2a3 3 0 0 0 3-3V9a7 7 0 0 1 14 0v5a3 3 0 0 0 3 3Z" />
<path d="M22 17v1a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-1" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.emailsCollected}</div>
<p className="text-xs text-muted-foreground">
+10.5% from last month
</p>
</CardContent>
</Card>

<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Blog Posts</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M21 15V6" />
<path d="M18.5 18a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z" />
<path d="M12 12H3" />
<path d="M16 6H3" />
<path d="M12 18H3" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.blogPosts}</div>
<p className="text-xs text-muted-foreground">
2 new posts this month
</p>
</CardContent>
</Card>

<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Views</CardTitle>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
className="h-4 w-4 text-muted-foreground"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats.totalViews}</div>
<p className="text-xs text-muted-foreground">
+35.2% from last month
</p>
</CardContent>
</Card>
</div>

<Card className="mb-8">
<CardHeader>
<CardTitle>Analytics Overview</CardTitle>
<p className="text-sm text-muted-foreground">
Your portfolio performance over the last 6 months
</p>
</CardHeader>
<CardContent>
<div className="h-[300px] w-full">
{/* Add your chart component here */}
<p className="text-muted-foreground text-center pt-20">
Chart visualization will be added here
</p>
</div>
</CardContent>
</Card>

<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Calendar</CardTitle>
<p className="text-sm text-muted-foreground">
Keep track of important dates
</p>
</CardHeader>
<CardContent>
{/* Add calendar component here */}
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle>Todo List</CardTitle>
<p className="text-sm text-muted-foreground">
Manage your tasks
</p>
</CardHeader>
<CardContent>
{/* Add todo list component here */}
</CardContent>
</Card>
</div>
</div>
);
}
62 changes: 62 additions & 0 deletions app/admin/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

export default function AdminLogin() {
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const router = useRouter();

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();

try {
const response = await fetch("/api/admin/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ password }),
});

if (response.ok) {
router.push("/admin/dashboard");
} else {
setError("Invalid password");
}
} catch {
setError("An error occurred");
}
};

return (
<div className="flex min-h-screen items-center justify-center bg-gray-100">
<Card className="w-[400px]">
<CardHeader>
<CardTitle className="text-2xl font-bold">Admin Login</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Input
type="password"
placeholder="Enter admin password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full"
/>
{error && <p className="text-sm text-red-500">{error}</p>}
</div>
<Button type="submit" className="w-full">
Login
</Button>
</form>
</CardContent>
</Card>
</div>
);
}
25 changes: 25 additions & 0 deletions app/api/admin/login/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function POST(request: Request) {
const body = await request.json();
const { password } = body;

// Compare with environment variable
if (password === process.env.ADMIN_PASSWORD) {
// Set an HTTP-only cookie for authentication
cookies().set("admin_token", "authenticated", {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 60 * 60 * 24, // 24 hours
});

return NextResponse.json({ success: true });
}

return NextResponse.json(
{ error: "Invalid password" },
{ status: 401 }
);
}
2 changes: 1 addition & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default function RootLayout({
disableTransitionOnChange
>
<AuthProvider>
<Suspense>
<Suspense fallback={<div>Loading...</div>}>
<RootLayoutClient>
{children}
</RootLayoutClient>
Expand Down
5 changes: 5 additions & 0 deletions components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ const Navbar = () => {
// label: "Chat",
// active: pathname === "/chat" || pathname === "/chat/image",
// },
{
href: "/admin/login",
label: "Admin",
active: pathname.startsWith("/admin"),
}
]

return (
Expand Down
7 changes: 7 additions & 0 deletions components/suspense-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client';

import { Suspense } from 'react';

export function SuspenseWrapper({ children }: { children: React.ReactNode }) {
return <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>;
}
10 changes: 9 additions & 1 deletion middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,17 @@ export async function middleware(req: NextRequest) {
return NextResponse.redirect(new URL('/auth/sign-in', req.url))
}

// Protect admin routes
if (req.nextUrl.pathname.startsWith('/admin') && req.nextUrl.pathname !== '/admin/login') {
const adminToken = req.cookies.get('admin_token')
if (!adminToken || adminToken.value !== 'authenticated') {
return NextResponse.redirect(new URL('/admin/login', req.url))
}
}

return res
}

export const config = {
matcher: ['/', '/auth/sign-in', '/auth/sign-up', '/dashboard', '/profile']
matcher: ['/', '/auth/sign-in', '/auth/sign-up', '/dashboard', '/profile', '/admin/:path*']
}

0 comments on commit 05a451c

Please sign in to comment.