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,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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user