feat: initialize project with Symfony, React, and Tailwind CSS setup
- Add package.json with development and production dependencies - Create postcss.config.js for Tailwind CSS integration - Implement AdminController for handling admin routes - Add admin index template with React root element - Create base template for consistent layout - Configure TypeScript with tsconfig.json - Set up Webpack configuration for asset management
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import Sidebar from './Sidebar';
|
||||
import Topbar from './Topbar';
|
||||
|
||||
export default function AdminLayout() {
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden bg-[#f1f5f9]">
|
||||
<Sidebar />
|
||||
<div className="flex flex-col flex-1 overflow-hidden">
|
||||
<Topbar />
|
||||
<main className="flex-1 overflow-y-auto p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import React from 'react';
|
||||
import { NavLink, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ChartBarIcon,
|
||||
UserGroupIcon,
|
||||
HeartIcon,
|
||||
BuildingOffice2Icon,
|
||||
CalendarDaysIcon,
|
||||
CreditCardIcon,
|
||||
BanknotesIcon,
|
||||
UsersIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
StarIcon,
|
||||
DevicePhoneMobileIcon,
|
||||
TagIcon,
|
||||
DocumentTextIcon,
|
||||
KeyIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronLeftIcon,
|
||||
MagnifyingGlassIcon,
|
||||
ArrowLeftOnRectangleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { useUiStore } from '../../stores/uiStore';
|
||||
import { useAuthStore } from '../../stores/authStore';
|
||||
|
||||
const sections = [
|
||||
{
|
||||
label: 'عمومی',
|
||||
items: [
|
||||
{ to: '/admin/dashboard', icon: ChartBarIcon, label: 'داشبورد' },
|
||||
{ to: '/admin/users', icon: UserGroupIcon, label: 'کاربران' },
|
||||
{ to: '/admin/doctors', icon: HeartIcon, label: 'پزشکان' },
|
||||
{ to: '/admin/clinics', icon: BuildingOffice2Icon, label: 'کلینیکها' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'مدیریت',
|
||||
items: [
|
||||
{ to: '/admin/appointments', icon: CalendarDaysIcon, label: 'نوبتها' },
|
||||
{ to: '/admin/payments', icon: CreditCardIcon, label: 'پرداختها' },
|
||||
{ to: '/admin/settlements', icon: BanknotesIcon, label: 'تسویهحساب' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'محتوا',
|
||||
items: [
|
||||
{ to: '/admin/comments', icon: ChatBubbleLeftEllipsisIcon, label: 'نظرات' },
|
||||
{ to: '/admin/ratings', icon: StarIcon, label: 'امتیازها' },
|
||||
{ to: '/admin/blogs', icon: DocumentTextIcon, label: 'بلاگ' },
|
||||
{ to: '/admin/sms', icon: DevicePhoneMobileIcon, label: 'پیامک' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'سیستم',
|
||||
items: [
|
||||
{ to: '/admin/categories', icon: TagIcon, label: 'دستهبندیها' },
|
||||
{ to: '/admin/representations', icon: UsersIcon, label: 'نمایندگان' },
|
||||
{ to: '/admin/secretaries', icon: KeyIcon, label: 'منشیها' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
const open = useUiStore((s) => s.sidebarOpen);
|
||||
const toggle = useUiStore((s) => s.toggleSidebar);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/admin/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<aside
|
||||
style={{ transition: 'width 300ms cubic-bezier(0.4,0,0.2,1)' }}
|
||||
className={`${open ? 'w-[260px]' : 'w-[72px]'} shrink-0 bg-[#0f172a] flex flex-col h-screen overflow-hidden`}
|
||||
>
|
||||
{/* Logo */}
|
||||
<div className="flex items-center justify-between px-4 py-5 border-b border-slate-700/50">
|
||||
{open && (
|
||||
<span className="text-white font-bold text-lg tracking-tight">ClinicPro</span>
|
||||
)}
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="text-slate-400 hover:text-white p-1 rounded-md hover:bg-slate-700/50 transition-colors"
|
||||
>
|
||||
{open ? <ChevronRightIcon className="w-5 h-5" /> : <ChevronLeftIcon className="w-5 h-5" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
{open && (
|
||||
<div className="px-3 py-3 border-b border-slate-700/50">
|
||||
<div className="flex items-center gap-2 bg-slate-800 rounded-lg px-3 py-2">
|
||||
<MagnifyingGlassIcon className="w-4 h-4 text-slate-400 shrink-0" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="جستجوی سریع..."
|
||||
className="bg-transparent text-sm text-slate-300 placeholder-slate-500 outline-none w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 overflow-y-auto py-3 space-y-1 scrollbar-thin">
|
||||
{sections.map((section) => (
|
||||
<div key={section.label} className="mb-2">
|
||||
{open && (
|
||||
<p className="text-[11px] font-semibold text-slate-500 uppercase tracking-wider px-4 py-1">
|
||||
{section.label}
|
||||
</p>
|
||||
)}
|
||||
{section.items.map(({ to, icon: Icon, label }) => (
|
||||
<NavLink
|
||||
key={to}
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 mx-2 px-3 py-2.5 rounded-lg text-sm transition-colors group ${
|
||||
isActive
|
||||
? 'bg-primary-500/15 text-primary-400 font-semibold border-r-4 border-primary-500'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-700/40'
|
||||
}`
|
||||
}
|
||||
title={!open ? label : undefined}
|
||||
>
|
||||
<Icon className="w-5 h-5 shrink-0" />
|
||||
{open && <span>{label}</span>}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* User card */}
|
||||
<div className="border-t border-slate-700/50 p-3">
|
||||
{open ? (
|
||||
<div className="flex items-center gap-3 bg-slate-800 rounded-xl p-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center text-white text-sm font-bold shrink-0">
|
||||
A
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white text-sm font-medium truncate">Admin</p>
|
||||
<p className="text-slate-400 text-xs truncate">مدیر سیستم</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-slate-400 hover:text-red-400 transition-colors"
|
||||
title="خروج"
|
||||
>
|
||||
<ArrowLeftOnRectangleIcon className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex justify-center py-2 text-slate-400 hover:text-red-400 transition-colors"
|
||||
title="خروج"
|
||||
>
|
||||
<ArrowLeftOnRectangleIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { Bars3Icon, BellIcon } from '@heroicons/react/24/outline';
|
||||
import { useUiStore } from '../../stores/uiStore';
|
||||
|
||||
interface TopbarProps {
|
||||
pageTitle?: string;
|
||||
breadcrumbs?: { label: string; to?: string }[];
|
||||
}
|
||||
|
||||
export default function Topbar({ pageTitle, breadcrumbs }: TopbarProps) {
|
||||
const toggle = useUiStore((s) => s.toggleSidebar);
|
||||
|
||||
return (
|
||||
<header className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 shrink-0">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="text-gray-500 hover:text-gray-700 p-1 rounded-md hover:bg-gray-100 transition-colors md:hidden"
|
||||
>
|
||||
<Bars3Icon className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div>
|
||||
{pageTitle && <h2 className="text-base font-semibold text-gray-800">{pageTitle}</h2>}
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
<nav className="flex items-center gap-1 text-xs text-gray-500">
|
||||
{breadcrumbs.map((crumb, i) => (
|
||||
<React.Fragment key={i}>
|
||||
{i > 0 && <span>/</span>}
|
||||
<span className={i === breadcrumbs.length - 1 ? 'text-gray-700 font-medium' : ''}>
|
||||
{crumb.label}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button className="relative text-gray-500 hover:text-gray-700 p-2 rounded-lg hover:bg-gray-100 transition-colors">
|
||||
<BellIcon className="w-5 h-5" />
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<div className="w-8 h-8 rounded-full bg-primary-600 flex items-center justify-center text-white text-sm font-bold">
|
||||
A
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-700 hidden sm:block">Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user