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:
hamed
2026-06-09 22:27:16 +03:30
parent de1a78a235
commit c98d7b7968
33 changed files with 10399 additions and 116 deletions
+129
View File
@@ -0,0 +1,129 @@
import React from 'react';
import {
UserGroupIcon,
HeartIcon,
BuildingOffice2Icon,
CalendarDaysIcon,
CreditCardIcon,
ChatBubbleLeftEllipsisIcon,
BanknotesIcon,
ArrowTrendingUpIcon,
} from '@heroicons/react/24/outline';
interface StatCardProps {
label: string;
value: string | number;
trend?: string;
trendUp?: boolean;
icon: React.ElementType;
iconBg: string;
iconColor: string;
}
function StatCard({ label, value, trend, trendUp, icon: Icon, iconBg, iconColor }: StatCardProps) {
return (
<div className="bg-white rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,.08)] p-6">
<div className="flex items-start justify-between">
<div>
<p className="text-sm text-gray-500 mb-1">{label}</p>
<p className="text-3xl font-bold text-gray-900">{value}</p>
{trend && (
<div className={`flex items-center gap-1 mt-2 text-xs font-medium ${trendUp ? 'text-emerald-600' : 'text-red-500'}`}>
<ArrowTrendingUpIcon className={`w-3.5 h-3.5 ${!trendUp && 'rotate-180'}`} />
<span>{trend}</span>
</div>
)}
</div>
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${iconBg}`}>
<Icon className={`w-6 h-6 ${iconColor}`} />
</div>
</div>
</div>
);
}
const stats: StatCardProps[] = [
{
label: 'کل کاربران',
value: '۱۲,۴۸۴',
trend: '۸٪ نسبت به ماه قبل',
trendUp: true,
icon: UserGroupIcon,
iconBg: 'bg-violet-100',
iconColor: 'text-violet-600',
},
{
label: 'پزشکان فعال',
value: '۱,۲۸۴',
trend: '۱۲٪ نسبت به ماه قبل',
trendUp: true,
icon: HeartIcon,
iconBg: 'bg-emerald-100',
iconColor: 'text-emerald-600',
},
{
label: 'کلینیک‌ها',
value: '۳۲۱',
trend: '۴٪ نسبت به ماه قبل',
trendUp: false,
icon: BuildingOffice2Icon,
iconBg: 'bg-blue-100',
iconColor: 'text-blue-600',
},
{
label: 'نوبت‌های امروز',
value: '۱۴۸',
trend: '۲۳٪ نسبت به دیروز',
trendUp: true,
icon: CalendarDaysIcon,
iconBg: 'bg-orange-100',
iconColor: 'text-orange-600',
},
{
label: 'پرداخت‌های امروز',
value: '۴۸,۲۰۰,۰۰۰',
trend: '۱۵٪ نسبت به دیروز',
trendUp: true,
icon: CreditCardIcon,
iconBg: 'bg-pink-100',
iconColor: 'text-pink-600',
},
{
label: 'نظرات در انتظار',
value: '۱۲',
icon: ChatBubbleLeftEllipsisIcon,
iconBg: 'bg-yellow-100',
iconColor: 'text-yellow-600',
},
{
label: 'درخواست تسویه',
value: '۵',
icon: BanknotesIcon,
iconBg: 'bg-teal-100',
iconColor: 'text-teal-600',
},
];
export default function DashboardPage() {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900">داشبورد</h1>
<p className="text-sm text-gray-500 mt-1">خلاصه وضعیت سیستم</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
{stats.map((stat) => (
<StatCard key={stat.label} {...stat} />
))}
</div>
<div className="mt-6 bg-white rounded-2xl shadow-[0_1px_3px_rgba(0,0,0,.08)] p-6">
<h3 className="text-base font-semibold text-gray-800 mb-4">فعالیتهای اخیر</h3>
<div className="text-center py-10 text-gray-400 text-sm">
در حال توسعه...
</div>
</div>
</div>
);
}
+94
View File
@@ -0,0 +1,94 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { toast } from 'sonner';
import { useAuthStore } from '../stores/authStore';
const schema = z.object({
mobile: z.string().min(10, 'شماره موبایل معتبر نیست'),
password: z.string().min(6, 'رمز عبور باید حداقل ۶ کاراکتر باشد'),
});
type FormData = z.infer<typeof schema>;
export default function LoginPage() {
const login = useAuthStore((s) => s.login);
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = async (data: FormData) => {
try {
const res = await fetch('/api/v1/user/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mobile_number: data.mobile, password: data.password }),
});
if (!res.ok) {
const err = await res.json();
toast.error(err.message || 'خطا در ورود');
return;
}
const json = await res.json();
login(json.access_token, json.refresh_token ?? '');
toast.success('ورود موفق');
} catch {
toast.error('خطا در اتصال به سرور');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-[#f1f5f9]">
<div className="bg-white rounded-2xl shadow-lg p-8 w-full max-w-sm">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-gray-900">ClinicPro</h1>
<p className="text-gray-500 text-sm mt-1">پنل مدیریت</p>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
شماره موبایل
</label>
<input
{...register('mobile')}
type="tel"
dir="ltr"
placeholder="09xxxxxxxxx"
className="w-full h-11 border border-gray-300 rounded-[10px] px-3 text-right focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
{errors.mobile && (
<p className="text-red-500 text-xs mt-1">{errors.mobile.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
رمز عبور
</label>
<input
{...register('password')}
type="password"
placeholder="••••••••"
className="w-full h-11 border border-gray-300 rounded-[10px] px-3 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
{errors.password && (
<p className="text-red-500 text-xs mt-1">{errors.password.message}</p>
)}
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full h-11 bg-primary-600 hover:bg-primary-700 disabled:opacity-60 text-white font-medium rounded-[10px] transition-colors"
>
{isSubmitting ? 'در حال ورود...' : 'ورود'}
</button>
</form>
</div>
</div>
);
}