feat: setup admin layout and base routing

This commit is contained in:
پارسا آقایی 2026-06-12 13:34:09 +03:30
parent 7fcfbda8cd
commit 222581c6a3
5 changed files with 160 additions and 117 deletions

View File

@ -1,122 +1,22 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
import heroImg from './assets/hero.png'
import './App.css'
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Dashboard from './pages/Dashboard';
function App() {
const [count, setCount] = useState(0)
return (
<>
<section id="center">
<div className="hero">
<img src={heroImg} className="base" width="170" height="179" alt="" />
<img src={reactLogo} className="framework" alt="React logo" />
<img src={viteLogo} className="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
</p>
</div>
<button
type="button"
className="counter"
onClick={() => setCount((count) => count + 1)}
>
Count is {count}
</button>
</section>
<div className="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img className="logo" src={viteLogo} alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://react.dev/" target="_blank">
<img className="button-icon" src={reactLogo} alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div className="ticks"></div>
<section id="spacer"></section>
</>
)
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Dashboard />} />
<Route path="users" element={<div className="p-4">مدیریت کاربران</div>} />
<Route path="products" element={<div className="p-4">مدیریت محصولات</div>} />
<Route path="orders" element={<div className="p-4">مدیریت سفارشات</div>} />
<Route path="coupons" element={<div className="p-4">کدهای تخفیف</div>} />
<Route path="settings" element={<div className="p-4">تنظیمات سیستم</div>} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default App
export default App;

View File

@ -0,0 +1,17 @@
import { Outlet } from 'react-router-dom';
import Sidebar from './Sidebar';
import Topbar from './Topbar';
export default function Layout() {
return (
<div className="flex min-h-screen bg-gray-50 text-gray-900 font-vazir" dir="rtl">
<Sidebar />
<div className="flex-1 flex flex-col min-h-screen">
<Topbar />
<main className="flex-1 p-6 overflow-y-auto">
<Outlet />
</main>
</div>
</div>
);
}

View File

@ -0,0 +1,52 @@
import { Link, useLocation } from 'react-router-dom';
import { Home, Users, ShoppingBag, ShoppingCart, Tag, Settings, LogOut } from 'lucide-react';
const menuItems = [
{ icon: Home, label: 'داشبورد', path: '/' },
{ icon: Users, label: 'کاربران', path: '/users' },
{ icon: ShoppingBag, label: 'محصولات', path: '/products' },
{ icon: ShoppingCart, label: 'سفارشات', path: '/orders' },
{ icon: Tag, label: 'تخفیف‌ها', path: '/coupons' },
{ icon: Settings, label: 'تنظیمات', path: '/settings' },
];
export default function Sidebar() {
const location = useLocation();
return (
<aside className="w-64 bg-white border-l border-gray-200 h-screen sticky top-0 flex flex-col font-vazir shadow-sm">
<div className="h-16 flex items-center justify-center border-b border-gray-200">
<h1 className="text-xl font-black text-purple-600">کانینا | ادمینپنل</h1>
</div>
<nav className="flex-1 overflow-y-auto py-4 px-3 space-y-1">
{menuItems.map((item) => {
const isActive = location.pathname === item.path || (item.path !== '/' && location.pathname.startsWith(item.path));
const Icon = item.icon;
return (
<Link
key={item.path}
to={item.path}
className={`flex items-center gap-3 px-3 py-3 rounded-xl transition-all font-bold ${
isActive
? 'bg-purple-50 text-purple-600'
: 'text-gray-500 hover:bg-gray-50 hover:text-gray-900'
}`}
>
<Icon className="w-5 h-5" />
<span>{item.label}</span>
</Link>
);
})}
</nav>
<div className="p-4 border-t border-gray-200">
<button className="flex items-center gap-3 px-3 py-3 w-full text-red-500 hover:bg-red-50 rounded-xl transition-all font-bold">
<LogOut className="w-5 h-5" />
<span>خروج از حساب</span>
</button>
</div>
</aside>
);
}

View File

@ -0,0 +1,34 @@
import { Bell, Search, UserCircle } from 'lucide-react';
export default function Topbar() {
return (
<header className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 sticky top-0 z-10 shadow-sm font-vazir">
<div className="flex-1 flex items-center max-w-xl">
<div className="relative w-full">
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<Search className="w-5 h-5 text-gray-400" />
</div>
<input
type="text"
placeholder="جستجو در پنل..."
className="w-full bg-gray-50 border border-gray-200 text-gray-900 text-sm rounded-xl focus:ring-purple-500 focus:border-purple-500 block pr-10 p-2.5 outline-none font-bold"
/>
</div>
</div>
<div className="flex items-center gap-4">
<button className="w-10 h-10 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 transition-all relative">
<Bell className="w-5 h-5" />
<span className="absolute top-2 right-2 w-2 h-2 bg-red-500 rounded-full"></span>
</button>
<div className="flex items-center gap-3 pl-2 border-l border-gray-200 ml-2">
<div className="text-left hidden md:block">
<p className="text-sm font-bold text-gray-900">مدیر سیستم</p>
<p className="text-xs font-medium text-gray-500">ادمین ارشد</p>
</div>
<UserCircle className="w-10 h-10 text-gray-300" />
</div>
</div>
</header>
);
}

View File

@ -0,0 +1,40 @@
import { DollarSign, ShoppingCart, Users, Activity } from 'lucide-react';
export default function Dashboard() {
const stats = [
{ title: 'درآمد کل', value: '۴۵,۰۰۰,۰۰۰ تومان', icon: DollarSign, color: 'text-green-600', bg: 'bg-green-100' },
{ title: 'سفارشات جدید', value: '۱۲', icon: ShoppingCart, color: 'text-blue-600', bg: 'bg-blue-100' },
{ title: 'کاربران فعال', value: '۱,۰۲۴', icon: Users, color: 'text-purple-600', bg: 'bg-purple-100' },
{ title: 'بازدید امروز', value: '۳۴۲', icon: Activity, color: 'text-orange-600', bg: 'bg-orange-100' },
];
return (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-black text-gray-900">داشبورد</h2>
<p className="text-gray-500 font-medium mt-1">خلاصهای از وضعیت فروشگاه کانینا</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{stats.map((stat, i) => {
const Icon = stat.icon;
return (
<div key={i} className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 flex items-center gap-4">
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center ${stat.bg}`}>
<Icon className={`w-6 h-6 ${stat.color}`} />
</div>
<div>
<p className="text-sm font-bold text-gray-500">{stat.title}</p>
<p className="text-xl font-black text-gray-900 mt-1">{stat.value}</p>
</div>
</div>
);
})}
</div>
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 min-h-[400px] flex items-center justify-center">
<p className="text-gray-400 font-bold">بخش نمودارها (در فاز بعدی اضافه میشود)</p>
</div>
</div>
);
}