handle dashboard access by role

This commit is contained in:
haniyeroozmand
2026-03-30 12:59:12 +03:30
parent d9902b7d99
commit be8212deed
4 changed files with 312 additions and 111 deletions

View File

@@ -5,8 +5,14 @@ import {
User, Crown, Home, ShoppingBag, Wallet, MapPin,
Headphones, Heart, IdCard, LogOut, TrendingUp,
Plus, Hash, Clock, AlertCircle, Eye, Reply,
CreditCard, Gift, Edit, Trash2, CircleCheck, Info, Box, Camera, Check, Shield, ShieldAlert, Mail
CreditCard, Gift, Edit, Trash2, CircleCheck, Info, Box, Camera, Check, Shield, ShieldAlert, Mail, CheckCircle, ShieldX
} from 'lucide-react';
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
// === رابط‌ها و کامپوننت‌های کمکی ===
interface PriceDisplayProps {
@@ -89,7 +95,22 @@ const sampleAddresses = [
];
export default function Dashboard() {
const [activeTab, setActiveTab] = useState('dash');
const searchParams = useSearchParams();
const success = searchParams.get("success");
const [authorized, setAuthorized] = useState<boolean | null>(null);
React.useEffect(() => {
const token = localStorage.getItem("accessToken");
const role = localStorage.getItem("role");
if (token && (role === "user" || role === "admin")) {
setAuthorized(true);
} else {
setAuthorized(false);
}
}, []);
const menuItems = [
{ id: 'dash', label: 'پیشخوان', icon: Home },
@@ -100,9 +121,73 @@ export default function Dashboard() {
{ id: 'profile', label: 'مشخصات حساب', icon: IdCard },
];
// ✅ حالت در حال بررسی
if (authorized === null) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50" dir="rtl">
<p className="text-gray-600">در حال بررسی دسترسی...</p>
</div>
);
}
// ❌ دسترسی ندارد
if (!authorized) {
const router = useRouter();
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 px-4" dir="rtl">
<div className="bg-white shadow-xl rounded-2xl p-10 max-w-md w-full text-center border border-gray-100">
<div className="flex justify-center mb-6">
<div className="bg-red-100 p-4 rounded-full">
<ShieldX className="w-10 h-10 text-red-600" />
</div>
</div>
<h1 className="text-2xl font-bold text-gray-800 mb-2">
دسترسی غیرمجاز
</h1>
<p className="text-gray-500 text-sm leading-relaxed mb-6">
شما اجازه دسترسی به این صفحه را ندارید.
برای مشاهده داشبورد باید ابتدا وارد حساب کاربری خود شوید.
</p>
<div className="flex gap-3 justify-center">
<button
onClick={() => router.push("/")}
className="flex cursor-pointer items-center gap-2 bg-gray-900 text-white px-4 py-2 rounded-lg text-sm hover:bg-black transition"
>
<Home className="w-4 h-4" />
بازگشت به صفحه اصلی
</button>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8" dir="rtl">
<div className="container mx-auto px-4 max-w-6xl">
{success === "login" && (
<div className="mb-4 flex items-center gap-2 p-3 bg-[#d2f6dca3] text-green-700 rounded-xl text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>ورود با موفقیت انجام شد</span>
</div>
)}
{success === "register" && (
<div className="mb-4 flex items-center gap-2 p-3 bg-[#d2f6dca3] text-green-700 rounded-xl text-sm">
<CheckCircle className="w-4 h-4 text-green-600" />
<span>ثبت نام با موفقیت انجام شد</span>
</div>
)}
<div className="flex flex-col lg:flex-row gap-8">
<aside className="w-full lg:w-[280px] bg-white rounded-2xl shadow-sm flex-shrink-0 h-fit">
@@ -129,7 +214,7 @@ export default function Dashboard() {
<li key={item.id}>
<button
onClick={() => setActiveTab(item.id)}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all duration-200 ${activeTab === item.id
className={`w-full cursor-pointer flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold transition-all duration-200 ${activeTab === item.id
? 'bg-[#1a2332] text-white shadow-md'
: 'text-gray-600 hover:bg-gray-50 hover:text-[#ffb900]'
}`}
@@ -153,38 +238,38 @@ export default function Dashboard() {
{activeTab === 'dash' && (
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500">
<h2 className="text-2xl font-bold text-gray-800 mb-6">خوش آمدید، علی عزیز</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-white p-5 rounded-2xl border border-gray-100 shadow-sm flex items-center gap-4 hover:shadow-md transition-shadow">
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 mb-6">
<div className="bg-white p-5 rounded-2xl border border-gray-100 flex-col shadow-sm flex items-center gap-4 hover:shadow-md transition-shadow">
<div className="w-12 h-12 rounded-xl bg-blue-50 text-blue-600 flex items-center justify-center flex-shrink-0"><Wallet size={24} /></div>
<div>
<h4 className="text-xs text-gray-500 mb-1">موجودی کیف پول</h4>
<strong className="text-lg text-gray-800"><PriceDisplay amount={1500000} /></strong>
<strong className="text-[1em] text-gray-800"><PriceDisplay amount={1500000} /></strong>
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-100 shadow-sm flex items-center gap-4 hover:shadow-md transition-shadow">
<div className="bg-white flex-col p-5 rounded-2xl border border-gray-100 shadow-sm flex items-center gap-4 hover:shadow-md transition-shadow">
<div className="w-12 h-12 rounded-xl bg-green-50 text-green-600 flex items-center justify-center flex-shrink-0"><ShoppingBag size={24} /></div>
<div>
<div className='flex flex-col items-center'>
<h4 className="text-xs text-gray-500 mb-1">سفارشات جاری</h4>
<strong className="text-lg text-gray-800">۲ <span className="text-xs font-normal text-gray-500">مورد</span></strong>
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-100 shadow-sm flex items-center gap-4 hover:shadow-md transition-shadow">
<div className="bg-white flex-col p-5 rounded-2xl border border-gray-100 shadow-sm flex items-center gap-4 hover:shadow-md transition-shadow">
<div className="w-12 h-12 rounded-xl bg-orange-50 text-orange-600 flex items-center justify-center flex-shrink-0"><Headphones size={24} /></div>
<div>
<div className='flex flex-col items-center'>
<h4 className="text-xs text-gray-500 mb-1">تیکتهای باز</h4>
<strong className="text-lg text-gray-800">۱ <span className="text-xs font-normal text-gray-500">مورد</span></strong>
</div>
</div>
<div className="bg-white p-5 rounded-2xl border border-gray-100 shadow-sm flex items-center gap-4 hover:shadow-md transition-shadow">
<div className="bg-white flex-col p-5 rounded-2xl border border-gray-100 shadow-sm flex items-center gap-4 hover:shadow-md transition-shadow">
<div className="w-12 h-12 rounded-xl bg-purple-50 text-purple-600 flex items-center justify-center flex-shrink-0"><TrendingUp size={24} /></div>
<div>
<div className='flex flex-col items-center'>
<h4 className="text-xs text-gray-500 mb-1">امتیاز وفاداری</h4>
<strong className="text-lg text-gray-800">۲,۳۴۰ <span className="text-xs font-normal text-gray-500">امتیاز</span></strong>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="grid lg:grid-cols-2 gap-6">
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm p-6">
<div className="flex justify-between items-center pb-4 border-b border-gray-100 mb-4">
<h3 className="font-bold text-gray-800">آخرین سفارشات</h3>
@@ -647,3 +732,5 @@ export default function Dashboard() {
</div>
);
}

View File

@@ -17,50 +17,6 @@ import Link from "next/link";
import { useCategories } from "@/components/context/categoryprovider";
const features = [
{
icon: Headphones,
title: "مشاوره فنی رایگان",
desc: "انتخاب بهترین قطعه با توجه به نیاز شما",
},
{
icon: FileText,
title: "فاکتور رسمی",
desc: "صدور فاکتور برای شرکت‌ها",
},
{
icon: Truck,
title: "ارسال فوری",
desc: "ارسال در سریع‌ترین زمان ممکن",
},
{
icon: ShieldCheck,
title: "ضمانت اصالت کالا",
desc: "تضمین اورجینال بودن محصولات",
},
];
const tabs = ["پرفروش‌ترین‌ها", "تخفیف‌دار", "جدیدترین‌ها"];
const faqs = [
{
question: "آیا تمامی قطعات دارای ضمانت اصالت هستند؟",
answer:
"بله، تمامی محصولات ارائه شده دارای ضمانت اصالت کالا بوده و از برندهای معتبر جهانی تأمین می‌شوند.",
},
{
question: "امکان صدور فاکتور رسمی برای شرکت‌ها وجود دارد؟",
answer:
"بله، برای تمامی سفارشات امکان صدور فاکتور رسمی وجود دارد.",
},
{
question: "سفارش‌ها چه زمانی ارسال می‌شوند؟",
answer:
"سفارش‌ها در سریع‌ترین زمان ممکن پردازش شده و از طریق روش‌های ارسال معتبر ارسال می‌شوند.",
},
];
export default function Home() {
@@ -68,26 +24,52 @@ export default function Home() {
const brands = ["NTN", "KOYO", "NACHI", "TIMKEN", "FAG", "SKF"];
const latestArticles = articles.slice(-4);
const { rootCategories } = useCategories();
const features = [
{
icon: Headphones,
title: "مشاوره فنی رایگان",
desc: "انتخاب بهترین قطعه با توجه به نیاز شما",
},
{
icon: FileText,
title: "فاکتور رسمی",
desc: "صدور فاکتور برای شرکت‌ها",
},
{
icon: Truck,
title: "ارسال فوری",
desc: "ارسال در سریع‌ترین زمان ممکن",
},
{
icon: ShieldCheck,
title: "ضمانت اصالت کالا",
desc: "تضمین اورجینال بودن محصولات",
},
];
const tabs = ["پرفروش‌ترین‌ها", "تخفیف‌دار", "جدیدترین‌ها"];
const faqs = [
{
question: "آیا تمامی قطعات دارای ضمانت اصالت هستند؟",
answer:
"بله، تمامی محصولات ارائه شده دارای ضمانت اصالت کالا بوده و از برندهای معتبر جهانی تأمین می‌شوند.",
},
{
question: "امکان صدور فاکتور رسمی برای شرکت‌ها وجود دارد؟",
answer:
"بله، برای تمامی سفارشات امکان صدور فاکتور رسمی وجود دارد.",
},
{
question: "سفارش‌ها چه زمانی ارسال می‌شوند؟",
answer:
"سفارش‌ها در سریع‌ترین زمان ممکن پردازش شده و از طریق روش‌های ارسال معتبر ارسال می‌شوند.",
},
];
// const uniqueCategories = Array.from(new Set(products.map((p) => p.category)));
// const getCategoryIcon = (categoryName: string) => {
// switch (categoryName) {
// case "شیار عمیق":
// return CircleDashed;
// case "مخروطی":
// return Disc;
// case "شبکه ای":
// return Hexagon;
// case "سوزنی":
// return Settings;
// case "یاتاقان":
// return Wrench;
// case "گریس و روانکار":
// return Droplets;
// default:
// return Settings; // آیکون پیش‌فرض
// }
// };
const categoryIcons = [
CircleDashed,
@@ -241,7 +223,7 @@ export default function Home() {
{/* cards */}
<div className="grid justify-center grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-6">
{rootCategories.map((cat, index) => {
const Icon = categoryIcons[index % categoryIcons.length];
@@ -254,7 +236,7 @@ export default function Home() {
>
<Icon size={34} strokeWidth={1} className="text-gray-400 mb-3 group-hover:text-blue-500 transition-colors" />
<span className="text-sm font-medium text-gray-700 group-hover:text-blue-600">{cat.name}</span>
</Link>
);
})}

View File

@@ -40,6 +40,13 @@ export function Header() {
const [registerError, setRegisterError] = useState("");
const [loginError, setLoginError] = useState("");
const { rootCategories } = useCategories();
const [user, setUser] = useState<{ username: string } | null>(null);
const [userMenuOpen, setUserMenuOpen] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showLoginPassword, setShowLoginPassword] = useState(false);
const [loginForm, setLoginForm] = useState({
username: "",
password: "",
@@ -59,6 +66,17 @@ export function Header() {
});
};
useEffect(() => {
const token = localStorage.getItem("accessToken");
const username = localStorage.getItem("username");
if (token && username) {
setUser({ username });
}
}, []);
const handleRegister = async () => {
setRegisterError("");
@@ -84,12 +102,19 @@ export function Header() {
};
const res = await registerUser(payload);
const role = res.data?.user?.role;
localStorage.setItem("accessToken", res.data.accessToken);
localStorage.setItem("refreshToken", res.data.refreshToken);
localStorage.setItem("username", username);
localStorage.setItem("role", role.toLowerCase());
console.log(res);
setUser({ username });
setIsOpen(false);
router.push("/dashboard");
router.push("/dashboard?success=register");
} catch (error: any) {
@@ -126,11 +151,16 @@ export function Header() {
try {
const res = await loginUser({ username, password });
const role = res.data?.user?.role;
localStorage.setItem("role", role.toLowerCase());
localStorage.setItem("accessToken", res.data.accessToken);
localStorage.setItem("refreshToken", res.data.refreshToken);
localStorage.setItem("username", username);
console.log(res);
setUser({ username });
setIsOpen(false);
router.push("/dashboard");
router.push("/dashboard?success=login");
} catch (error: any) {
const msg = error?.message?.toLowerCase() || "";
@@ -162,6 +192,16 @@ export function Header() {
}
};
const handleLogout = () => {
localStorage.removeItem("accessToken");
localStorage.removeItem("refreshToken");
localStorage.removeItem("username");
localStorage.removeItem("role");
setUser(null);
router.push("/");
};
@@ -467,15 +507,52 @@ export function Header() {
</div>
</div>
<button
onClick={() => setIsOpen(true)}
className="flex cursor-pointer items-center gap-2 px-3 py-2.5 bg-white border border-gray-300/60 rounded-xl text-xs text-gray-700 hover:bg-gray-50 transition"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-4 h-4 text-gray-500">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.5 20.25a7.5 7.5 0 0115 0" />
</svg>
<span>ورود / عضویت</span>
</button>
{!user ? (
<button
onClick={() => setIsOpen(true)}
className="flex cursor-pointer items-center gap-2 px-3 py-2.5 bg-white border border-gray-300/60 rounded-xl text-xs text-gray-700 hover:bg-gray-50 transition"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-4 h-4 text-gray-500">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.5 20.25a7.5 7.5 0 0115 0" />
</svg>
<span>ورود / عضویت</span>
</button>
) : (
<div className="relative" onClick={() => setUserMenuOpen(!userMenuOpen)}
>
<button
className="flex cursor-pointer items-center gap-2 px-3 py-2.5 bg-white border border-gray-300/60 rounded-xl text-xs text-gray-700 hover:bg-gray-50 transition"
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.8} d="M5.121 17.804A9 9 0 1118.879 17.804M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{user.username}</span>
</button>
{userMenuOpen && (
<div className="absolute left-0 mt-2 w-40 bg-white border border-gray-200 rounded-xl shadow-lg p-2 text-xs">
<button
onClick={() => router.push("/dashboard")}
className="w-full cursor-pointer text-right px-3 py-2 rounded-lg hover:bg-gray-100"
>
داشبورد
</button>
<button
onClick={handleLogout}
className="w-full cursor-pointer text-right px-3 py-2 rounded-lg text-red-500 hover:bg-red-50"
>
خروج
</button>
</div>
)}
</div>
)}
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[#1A2332]/40 backdrop-blur-sm transition-opacity" dir="rtl">
@@ -542,7 +619,7 @@ export function Header() {
<button
onClick={() => setFormType("password")}
className="w-full text-xs text-gray-500 hover:text-[#1A2332]"
className="w-full cursor-pointer text-xs text-gray-500 hover:text-[#1A2332]"
>
ورود با نام کاربری و رمز عبور
</button>
@@ -567,18 +644,45 @@ export function Header() {
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/>
<input
type="password"
value={loginForm.password}
onChange={(e) =>
setLoginForm((prev) => ({
...prev,
password: e.target.value,
}))
}
placeholder="رمز عبور"
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/>
<div className="relative w-full">
<input
type={showLoginPassword ? "text" : "password"} // تغییر نوع بر اساس state
value={loginForm.password}
onChange={(e) =>
setLoginForm((prev) => ({
...prev,
password: e.target.value,
}))
}
placeholder="رمز عبور"
className="w-full px-4 pl-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/>
<button
type="button"
onClick={() => setShowLoginPassword(!showLoginPassword)}
className="absolute cursor-pointer left-4 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 p-1" // پدینگ اضافه شد تا کلیک راحت‌تر باشد
>
{showLoginPassword ? (
// آیکون چشم خط خورده (eye-off)
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none"
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
<path strokeLinecap="round" strokeLinejoin="round"
d="M3 3l18 18M10.477 10.477A3 3 0 0113.5 13.5m-7.09-2.664A9.956 9.956 0 003 12s2.91-6 9-6a9.953 9.953 0 016.328 2.318M15.54 15.54A9.953 9.953 0 0112 18c-6.09 0-9-6-9-6a9.956 9.956 0 012.41-3.868" />
</svg>
) : (
// آیکون چشم باز (eye)
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none"
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
<path strokeLinecap="round" strokeLinejoin="round"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
<path strokeLinecap="round" strokeLinejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
)}
</button>
</div>
<button onClick={handleLogin} className="w-full cursor-pointer py-3.5 bg-[#ffb900] hover:bg-[#e5a600] text-[#1A2332] font-semibold rounded-2xl text-sm">
ورود
@@ -591,7 +695,7 @@ export function Header() {
<button
onClick={() => setFormType("mobile")}
className="w-full text-xs text-gray-500 hover:text-[#1A2332]"
className="w-full cursor-pointer text-xs text-gray-500 hover:text-[#1A2332]"
>
ورود با شماره موبایل
</button>
@@ -631,13 +735,41 @@ export function Header() {
placeholder="نام کاربری"
className='w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]'
/>
<input
name="password"
value={registerForm.password}
onChange={handleChange} type="password"
placeholder="رمز عبور"
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/>
<div className="relative w-full">
<input
name="password"
value={registerForm.password}
onChange={handleChange}
type={showPassword ? "text" : "password"}
placeholder="رمز عبور"
className="w-full px-4 py-3.5 bg-gray-50 border border-gray-200 rounded-2xl text-sm text-right focus:outline-none focus:border-[#ffb900]"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute cursor-pointer left-4 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700"
>
{showPassword ? (
// eye-off icon
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none"
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
<path strokeLinecap="round" strokeLinejoin="round"
d="M3 3l18 18M10.477 10.477A3 3 0 0113.5 13.5m-7.09-2.664A9.956 9.956 0 003 12s2.91-6 9-6a9.953 9.953 0 016.328 2.318M15.54 15.54A9.953 9.953 0 0112 18c-6.09 0-9-6-9-6a9.956 9.956 0 012.41-3.868" />
</svg>
) : (
// eye icon
<svg xmlns="http://www.w3.org/2000/svg" className="w-5 h-5" fill="none"
viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
<path strokeLinecap="round" strokeLinejoin="round"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.477 0 8.268 2.943 9.542 7-1.274 4.057-5.065 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
<path strokeLinecap="round" strokeLinejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
)}
</button>
</div>
{/* <input
name="confirmPassword"

View File

@@ -7,4 +7,4 @@ export interface ApiResponse<T> {
timestamp: string;
data: T; // دیتای اصلی که در هر API فرق می‌کند اینجا قرار می‌گیرد
}