feat(admin): implement live global search, fixed position sidebar, click-to-navigate dashboard cards, and new orders count badge on sidebar
Some checks failed
Deploy Canina / deploy (push) Failing after 2m37s
Some checks failed
Deploy Canina / deploy (push) Failing after 2m37s
This commit is contained in:
parent
72650dba36
commit
fd45910387
@ -9,7 +9,7 @@ export default function Layout() {
|
||||
return (
|
||||
<div className="flex min-h-screen bg-gray-50 text-gray-900 font-vazir relative overflow-hidden" dir="rtl">
|
||||
<Sidebar isOpen={isMobileMenuOpen} setIsOpen={setIsMobileMenuOpen} />
|
||||
<div className="flex-1 flex flex-col min-h-screen w-full lg:w-auto">
|
||||
<div className="flex-1 flex flex-col min-h-screen w-full lg:w-auto lg:pr-64">
|
||||
<Topbar toggleMenu={() => setIsMobileMenuOpen(!isMobileMenuOpen)} />
|
||||
<main className="flex-1 p-6 overflow-y-auto">
|
||||
<Outlet />
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Users, ShoppingCart, Tag, Settings, LogOut, TrendingUp, FolderTree, FileText, BookOpen, Heart, Package, LayoutDashboard, Languages, Image } from 'lucide-react';
|
||||
import api from '../services/api';
|
||||
|
||||
const menuGroups = [
|
||||
{
|
||||
@ -50,6 +52,24 @@ interface SidebarProps {
|
||||
export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [newOrdersCount, setNewOrdersCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchOrdersCount = async () => {
|
||||
try {
|
||||
const response = await api.get('/admin/dashboard/stats');
|
||||
if (response.data?.success) {
|
||||
setNewOrdersCount(response.data.data.newOrders);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch stats in sidebar', err);
|
||||
}
|
||||
};
|
||||
fetchOrdersCount();
|
||||
// Poll stats every 60 seconds
|
||||
const interval = setInterval(fetchOrdersCount, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('adminToken');
|
||||
@ -66,7 +86,7 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
<aside className={`w-64 bg-white border-l border-gray-200 h-screen fixed lg:sticky top-0 flex flex-col font-vazir shadow-sm z-50 transform transition-transform duration-300 ease-in-out ${isOpen ? 'translate-x-0' : 'translate-x-full lg:translate-x-0'
|
||||
<aside className={`w-64 bg-white border-l border-gray-200 h-screen fixed lg:fixed lg:right-0 lg:top-0 lg:bottom-0 flex flex-col font-vazir shadow-sm z-50 transform transition-transform duration-300 ease-in-out ${isOpen ? 'translate-x-0' : 'translate-x-full lg:translate-x-0'
|
||||
}`}>
|
||||
<div className="h-16 flex items-center justify-center border-b border-gray-200 shrink-0">
|
||||
<h1 className="text-xl font-black text-purple-600">کانینا | ادمینپنل</h1>
|
||||
@ -92,6 +112,11 @@ export default function Sidebar({ isOpen, setIsOpen }: SidebarProps) {
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span>{item.label}</span>
|
||||
{item.label === 'سفارشات' && newOrdersCount > 0 && (
|
||||
<span className="mr-auto bg-purple-600 text-white text-[11px] font-black px-2 py-0.5 rounded-full min-w-[20px] text-center">
|
||||
{newOrdersCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
|
||||
@ -1,13 +1,57 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Bell, Search, UserCircle, Menu } from 'lucide-react';
|
||||
|
||||
interface TopbarProps {
|
||||
toggleMenu: () => void;
|
||||
}
|
||||
|
||||
const SEARCHABLE_PAGES = [
|
||||
{ label: 'داشبورد (خلاصه وضعیت فروشگاه)', path: '/' },
|
||||
{ label: 'گزارشات کامل فروش و بازدیدها', path: '/reports' },
|
||||
{ label: 'مدیریت سفارشات مشتریان', path: '/orders' },
|
||||
{ label: 'مدیریت محصولات فروشگاه', path: '/products' },
|
||||
{ label: 'دستهبندیهای محصولات (Categories)', path: '/categories' },
|
||||
{ label: 'کدهای تخفیف و کوپنهای خرید', path: '/coupons' },
|
||||
{ label: 'مدیریت کاربران و مشتریان سایت', path: '/users' },
|
||||
{ label: 'مدیریت سگها و گربهها (Pets)', path: '/pets' },
|
||||
{ label: 'مدیریت مقالات وبلاگ', path: '/blogs' },
|
||||
{ label: 'مدیریت دانشنامه و مقالات علمی (Wiki)', path: '/wiki' },
|
||||
{ label: 'تنظیمات کلی سیستم و درگاه پرداخت', path: '/settings' },
|
||||
{ label: 'متون رابط کاربری و ترجمهها', path: '/ui-texts' },
|
||||
{ label: 'مدیریت رسانه، عکسها و گالری', path: '/media' },
|
||||
];
|
||||
|
||||
export default function Topbar({ toggleMenu }: TopbarProps) {
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
const searchContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close search results dropdown on clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (searchContainerRef.current && !searchContainerRef.current.contains(event.target as Node)) {
|
||||
setShowResults(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const filteredResults = SEARCHABLE_PAGES.filter(page =>
|
||||
page.label.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const handleResultClick = (path: string) => {
|
||||
navigate(path);
|
||||
setSearchQuery('');
|
||||
setShowResults(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 sm:px-6 sticky top-0 z-10 shadow-sm font-vazir">
|
||||
<div className="flex-1 flex items-center gap-3 max-w-xl">
|
||||
<header className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-4 sm:px-6 sticky top-0 z-40 shadow-sm font-vazir">
|
||||
<div className="flex-1 flex items-center gap-3 max-w-xl" ref={searchContainerRef}>
|
||||
<button
|
||||
onClick={toggleMenu}
|
||||
className="lg:hidden p-2 text-gray-500 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
@ -20,9 +64,37 @@ export default function Topbar({ toggleMenu }: TopbarProps) {
|
||||
</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"
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setShowResults(true);
|
||||
}}
|
||||
onFocus={() => setShowResults(true)}
|
||||
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 transition-all"
|
||||
/>
|
||||
|
||||
{/* Live Search Results Dropdown */}
|
||||
{showResults && searchQuery.trim() !== '' && (
|
||||
<div className="absolute top-full right-0 left-0 mt-2 bg-white border border-gray-100 rounded-xl shadow-2xl z-50 overflow-hidden max-h-60 overflow-y-auto">
|
||||
{filteredResults.length > 0 ? (
|
||||
<div className="py-1">
|
||||
{filteredResults.map((result, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => handleResultClick(result.path)}
|
||||
className="w-full text-right px-4 py-3 text-sm font-bold text-gray-700 hover:bg-purple-50 hover:text-purple-600 transition-colors flex items-center justify-between"
|
||||
>
|
||||
<span>{result.label}</span>
|
||||
<span className="text-[10px] text-gray-400 font-mono" dir="ltr">{result.path}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 text-center text-sm text-gray-400 font-bold">نتیجهای یافت نشد.</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { DollarSign, ShoppingCart, Users, Activity } from 'lucide-react';
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
|
||||
import api from '../services/api';
|
||||
@ -43,10 +44,10 @@ export default function Dashboard() {
|
||||
const COLORS = ['#0088FE', '#00C49F', '#FFBB28'];
|
||||
|
||||
const stats = [
|
||||
{ title: 'درآمد کل', value: `${data.revenue.toLocaleString()} تومان`, icon: DollarSign, color: 'text-green-600', bg: 'bg-green-100' },
|
||||
{ title: 'سفارشات جدید', value: data.newOrders.toString(), icon: ShoppingCart, color: 'text-blue-600', bg: 'bg-blue-100' },
|
||||
{ title: 'کاربران فعال', value: data.users.toString(), icon: Users, color: 'text-purple-600', bg: 'bg-purple-100' },
|
||||
{ title: 'بازدید امروز', value: data.todayVisits.toString(), icon: Activity, color: 'text-orange-600', bg: 'bg-orange-100' },
|
||||
{ title: 'درآمد کل', value: `${data.revenue.toLocaleString()} تومان`, icon: DollarSign, color: 'text-green-600', bg: 'bg-green-100', link: '/reports' },
|
||||
{ title: 'سفارشات جدید', value: data.newOrders.toString(), icon: ShoppingCart, color: 'text-blue-600', bg: 'bg-blue-100', link: '/orders' },
|
||||
{ title: 'کاربران فعال', value: data.users.toString(), icon: Users, color: 'text-purple-600', bg: 'bg-purple-100', link: '/users' },
|
||||
{ title: 'بازدید امروز', value: data.todayVisits.toString(), icon: Activity, color: 'text-orange-600', bg: 'bg-orange-100', link: '/reports' },
|
||||
];
|
||||
|
||||
return (
|
||||
@ -60,7 +61,7 @@ export default function Dashboard() {
|
||||
{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">
|
||||
<Link key={i} to={stat.link} className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100 flex items-center gap-4 hover:shadow-md hover:border-purple-200 transition-all cursor-pointer">
|
||||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center ${stat.bg}`}>
|
||||
<Icon className={`w-6 h-6 ${stat.color}`} />
|
||||
</div>
|
||||
@ -68,7 +69,7 @@ export default function Dashboard() {
|
||||
<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>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user