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:
@@ -46,3 +46,10 @@
|
||||
|
||||
# Composer
|
||||
/composer.phar
|
||||
|
||||
###> symfony/webpack-encore-bundle ###
|
||||
/node_modules/
|
||||
/public/build/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
###< symfony/webpack-encore-bundle ###
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { useAuthStore } from './stores/authStore';
|
||||
import AdminLayout from './components/layout/AdminLayout';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import DashboardPage from './pages/DashboardPage';
|
||||
|
||||
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
return isAuthenticated ? <>{children}</> : <Navigate to="/admin/login" replace />;
|
||||
}
|
||||
|
||||
function PublicRoute({ children }: { children: React.ReactNode }) {
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
return isAuthenticated ? <Navigate to="/admin/dashboard" replace /> : <>{children}</>;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path="/admin/login"
|
||||
element={
|
||||
<PublicRoute>
|
||||
<LoginPage />
|
||||
</PublicRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/*"
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<AdminLayout />
|
||||
</PrivateRoute>
|
||||
}
|
||||
>
|
||||
<Route index element={<Navigate to="/admin/dashboard" replace />} />
|
||||
<Route path="dashboard" element={<DashboardPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/admin/dashboard" replace />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Toaster } from 'sonner';
|
||||
import App from './App';
|
||||
import './styles.css';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: 1, staleTime: 30_000 },
|
||||
},
|
||||
});
|
||||
|
||||
const root = document.getElementById('admin-root')!;
|
||||
createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
<Toaster position="top-left" richColors />
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
interface AuthState {
|
||||
token: string | null;
|
||||
refreshToken: string | null;
|
||||
isAuthenticated: boolean;
|
||||
login: (token: string, refreshToken: string) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
login: (token, refreshToken) =>
|
||||
set({ token, refreshToken, isAuthenticated: true }),
|
||||
logout: () =>
|
||||
set({ token: null, refreshToken: null, isAuthenticated: false }),
|
||||
}),
|
||||
{ name: 'clinicpro-auth' }
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface UiState {
|
||||
sidebarOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
setSidebarOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const useUiStore = create<UiState>((set) => ({
|
||||
sidebarOpen: true,
|
||||
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
|
||||
setSidebarOpen: (open) => set({ sidebarOpen: open }),
|
||||
}));
|
||||
@@ -0,0 +1,30 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-primary-50: #f5f3ff;
|
||||
--color-primary-100: #ede9fe;
|
||||
--color-primary-200: #ddd6fe;
|
||||
--color-primary-300: #c4b5fd;
|
||||
--color-primary-400: #a78bfa;
|
||||
--color-primary-500: #8b5cf6;
|
||||
--color-primary-600: #7c3aed;
|
||||
--color-primary-700: #6d28d9;
|
||||
--color-primary-800: #5b21b6;
|
||||
--color-primary-900: #4c1d95;
|
||||
|
||||
--color-bg-body: #f1f5f9;
|
||||
--color-bg-card: #ffffff;
|
||||
--color-bg-sidebar: #0f172a;
|
||||
--color-bg-sidebar-active: rgba(139, 92, 246, 0.15);
|
||||
|
||||
--font-sans: "Vazirmatn", "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-bg-body);
|
||||
direction: rtl;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { registerReactControllerComponents } from '@symfony/ux-react';
|
||||
import './stimulus_bootstrap.js';
|
||||
/*
|
||||
* Welcome to your app's main JavaScript file!
|
||||
*
|
||||
* We recommend including the built version of this JavaScript file
|
||||
* (and its CSS file) in your base layout (base.html.twig).
|
||||
*/
|
||||
|
||||
// any CSS you import will output into a single css file (app.css in this case)
|
||||
import './styles/app.css';
|
||||
|
||||
registerReactControllerComponents(require.context('./react/controllers', true, /\.(j|t)sx?$/));
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"controllers": {
|
||||
"@symfony/ux-react": {
|
||||
"react": {
|
||||
"enabled": true,
|
||||
"fetch": "eager"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoints": []
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
const nameCheck = /^[-_a-zA-Z0-9]{4,22}$/;
|
||||
const tokenCheck = /^[-_/+a-zA-Z0-9]{24,}$/;
|
||||
|
||||
// Generate and double-submit a CSRF token in a form field and a cookie, as defined by Symfony's SameOriginCsrfTokenManager
|
||||
// Use `form.requestSubmit()` to ensure that the submit event is triggered. Using `form.submit()` will not trigger the event
|
||||
// and thus this event-listener will not be executed.
|
||||
document.addEventListener('submit', function (event) {
|
||||
generateCsrfToken(event.target);
|
||||
}, true);
|
||||
|
||||
// When @hotwired/turbo handles form submissions, send the CSRF token in a header in addition to a cookie
|
||||
// The `framework.csrf_protection.check_header` config option needs to be enabled for the header to be checked
|
||||
document.addEventListener('turbo:submit-start', function (event) {
|
||||
const h = generateCsrfHeaders(event.detail.formSubmission.formElement);
|
||||
Object.keys(h).map(function (k) {
|
||||
event.detail.formSubmission.fetchRequest.headers[k] = h[k];
|
||||
});
|
||||
});
|
||||
|
||||
// When @hotwired/turbo handles form submissions, remove the CSRF cookie once a form has been submitted
|
||||
document.addEventListener('turbo:submit-end', function (event) {
|
||||
removeCsrfToken(event.detail.formSubmission.formElement);
|
||||
});
|
||||
|
||||
export function generateCsrfToken (formElement) {
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
let csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
let csrfToken = csrfField.value;
|
||||
|
||||
if (!csrfCookie && nameCheck.test(csrfToken)) {
|
||||
csrfField.setAttribute('data-csrf-protection-cookie-value', csrfCookie = csrfToken);
|
||||
csrfField.defaultValue = csrfToken = btoa(String.fromCharCode.apply(null, (window.crypto || window.msCrypto).getRandomValues(new Uint8Array(18))));
|
||||
}
|
||||
csrfField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
if (csrfCookie && tokenCheck.test(csrfToken)) {
|
||||
const cookie = csrfCookie + '_' + csrfToken + '=' + csrfCookie + '; path=/; samesite=strict';
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
}
|
||||
|
||||
export function generateCsrfHeaders (formElement) {
|
||||
const headers = {};
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
headers[csrfCookie] = csrfField.value;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function removeCsrfToken (formElement) {
|
||||
const csrfField = formElement.querySelector('input[data-controller="csrf-protection"], input[name="_csrf_token"]');
|
||||
|
||||
if (!csrfField) {
|
||||
return;
|
||||
}
|
||||
|
||||
const csrfCookie = csrfField.getAttribute('data-csrf-protection-cookie-value');
|
||||
|
||||
if (tokenCheck.test(csrfField.value) && nameCheck.test(csrfCookie)) {
|
||||
const cookie = csrfCookie + '_' + csrfField.value + '=0; path=/; samesite=strict; max-age=0';
|
||||
|
||||
document.cookie = window.location.protocol === 'https:' ? '__Host-' + cookie + '; secure' : cookie;
|
||||
}
|
||||
}
|
||||
|
||||
/* stimulusFetch: 'lazy' */
|
||||
export default 'csrf-protection-controller';
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Controller } from '@hotwired/stimulus';
|
||||
|
||||
/*
|
||||
* This is an example Stimulus controller!
|
||||
*
|
||||
* Any element with a data-controller="hello" attribute will cause
|
||||
* this controller to be executed. The name "hello" comes from the filename:
|
||||
* hello_controller.js -> "hello"
|
||||
*
|
||||
* Delete this file or adapt it for your use!
|
||||
*/
|
||||
export default class extends Controller {
|
||||
connect() {
|
||||
this.element.textContent = 'Hello Stimulus! Edit me in assets/controllers/hello_controller.js';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function (props) {
|
||||
return <div>Hello {props.fullName}</div>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { startStimulusApp } from '@symfony/stimulus-bridge';
|
||||
|
||||
// Registers Stimulus controllers from controllers.json and in the controllers/ directory
|
||||
export const app = startStimulusApp(require.context(
|
||||
'@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
|
||||
true,
|
||||
/\.[jt]sx?$/
|
||||
));
|
||||
// register any custom, 3rd party controllers here
|
||||
// app.register('some_controller_name', SomeImportedController);
|
||||
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
background-color: lightgray;
|
||||
}
|
||||
@@ -29,8 +29,11 @@
|
||||
"symfony/runtime": "7.4.*",
|
||||
"symfony/security-bundle": "7.4.*",
|
||||
"symfony/serializer": "7.4.*",
|
||||
"symfony/twig-bundle": "7.4.*",
|
||||
"symfony/uid": "7.4.*",
|
||||
"symfony/ux-react": "^2.36",
|
||||
"symfony/validator": "7.4.*",
|
||||
"symfony/webpack-encore-bundle": "^2.4",
|
||||
"symfony/yaml": "7.4.*",
|
||||
"twig/twig": "*",
|
||||
"zircote/swagger-php": "*"
|
||||
|
||||
Generated
+435
-116
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "4fb67bd5aaa2425722fa73ea10b6a1b2",
|
||||
"content-hash": "245ea537c0b4605205b72071da16ee4b",
|
||||
"packages": [
|
||||
{
|
||||
"name": "doctrine/collections",
|
||||
@@ -5738,6 +5738,79 @@
|
||||
],
|
||||
"time": "2026-03-28T09:44:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/stimulus-bundle",
|
||||
"version": "v2.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/stimulus-bundle.git",
|
||||
"reference": "377a3d1ec5834631a7db53bd275276ff3c5b49df"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/stimulus-bundle/zipball/377a3d1ec5834631a7db53bd275276ff3c5b49df",
|
||||
"reference": "377a3d1ec5834631a7db53bd275276ff3c5b49df",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"symfony/config": "^5.4|^6.0|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^5.4|^6.0|^7.0|^8.0",
|
||||
"symfony/deprecation-contracts": "^2.0|^3.0",
|
||||
"symfony/finder": "^5.4|^6.0|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^5.4|^6.0|^7.0|^8.0",
|
||||
"twig/twig": "^2.15.3|^3.8"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/asset-mapper": "^6.3|^7.0|^8.0",
|
||||
"symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0",
|
||||
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0|^8.0",
|
||||
"symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
|
||||
"zenstruck/browser": "^1.4"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\UX\\StimulusBundle\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Integration with your Symfony app & Stimulus!",
|
||||
"keywords": [
|
||||
"symfony-ux"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/stimulus-bundle/tree/v2.36.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-06T04:31:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/stopwatch",
|
||||
"version": "v7.4.8",
|
||||
@@ -5977,6 +6050,211 @@
|
||||
],
|
||||
"time": "2026-01-05T13:30:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/twig-bridge",
|
||||
"version": "v7.4.12",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/twig-bridge.git",
|
||||
"reference": "81663873d946531129c76c65e80b681ce99c0e89"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/twig-bridge/zipball/81663873d946531129c76c65e80b681ce99c0e89",
|
||||
"reference": "81663873d946531129c76c65e80b681ce99c0e89",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/translation-contracts": "^2.5|^3",
|
||||
"twig/twig": "^3.21"
|
||||
},
|
||||
"conflict": {
|
||||
"phpdocumentor/reflection-docblock": "<5.2|>=7",
|
||||
"phpdocumentor/type-resolver": "<1.5.1",
|
||||
"symfony/console": "<6.4",
|
||||
"symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4",
|
||||
"symfony/http-foundation": "<6.4",
|
||||
"symfony/http-kernel": "<6.4",
|
||||
"symfony/mime": "<6.4.37|>7,<7.4.9|>8.0,<8.0.9",
|
||||
"symfony/serializer": "<6.4",
|
||||
"symfony/translation": "<6.4",
|
||||
"symfony/workflow": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"egulias/email-validator": "^2.1.10|^3|^4",
|
||||
"league/html-to-markdown": "^5.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.2|^6.0",
|
||||
"symfony/asset": "^6.4|^7.0|^8.0",
|
||||
"symfony/asset-mapper": "^6.4|^7.0|^8.0",
|
||||
"symfony/console": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/emoji": "^7.1|^8.0",
|
||||
"symfony/expression-language": "^6.4|^7.0|^8.0",
|
||||
"symfony/finder": "^6.4|^7.0|^8.0",
|
||||
"symfony/form": "^6.4.32|~7.3.10|^7.4.4|^8.0.4",
|
||||
"symfony/html-sanitizer": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-foundation": "^7.3|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/intl": "^6.4|^7.0|^8.0",
|
||||
"symfony/mime": "^6.4.37|^7.4.9|^8.0.9",
|
||||
"symfony/polyfill-intl-icu": "~1.0",
|
||||
"symfony/property-info": "^6.4|^7.0|^8.0",
|
||||
"symfony/routing": "^6.4|^7.0|^8.0",
|
||||
"symfony/security-acl": "^2.8|^3.0",
|
||||
"symfony/security-core": "^6.4|^7.0|^8.0",
|
||||
"symfony/security-csrf": "^6.4|^7.0|^8.0",
|
||||
"symfony/security-http": "^6.4|^7.0|^8.0",
|
||||
"symfony/serializer": "^6.4.3|^7.0.3|^8.0",
|
||||
"symfony/stopwatch": "^6.4|^7.0|^8.0",
|
||||
"symfony/translation": "^6.4|^7.0|^8.0",
|
||||
"symfony/validator": "^6.4|^7.0|^8.0",
|
||||
"symfony/web-link": "^6.4|^7.0|^8.0",
|
||||
"symfony/workflow": "^6.4|^7.0|^8.0",
|
||||
"symfony/yaml": "^6.4|^7.0|^8.0",
|
||||
"twig/cssinliner-extra": "^3",
|
||||
"twig/inky-extra": "^3",
|
||||
"twig/markdown-extra": "^3"
|
||||
},
|
||||
"type": "symfony-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Bridge\\Twig\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides integration for Twig with various Symfony components",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/twig-bridge/tree/v7.4.12"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-29T17:13:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/twig-bundle",
|
||||
"version": "v7.4.8",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/twig-bundle.git",
|
||||
"reference": "ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/twig-bundle/zipball/ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95",
|
||||
"reference": "ba1e06d7ff1ebb1d1799b6608d925f4eaba88d95",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-runtime-api": ">=2.1",
|
||||
"php": ">=8.2",
|
||||
"symfony/config": "^7.4|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/http-foundation": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-kernel": "^6.4.13|^7.1.6|^8.0",
|
||||
"symfony/twig-bridge": "^7.3|^8.0",
|
||||
"twig/twig": "^3.12"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/framework-bundle": "<6.4",
|
||||
"symfony/translation": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/asset": "^6.4|^7.0|^8.0",
|
||||
"symfony/expression-language": "^6.4|^7.0|^8.0",
|
||||
"symfony/finder": "^6.4|^7.0|^8.0",
|
||||
"symfony/form": "^6.4|^7.0|^8.0",
|
||||
"symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0",
|
||||
"symfony/routing": "^6.4|^7.0|^8.0",
|
||||
"symfony/runtime": "^6.4.13|^7.1.6",
|
||||
"symfony/stopwatch": "^6.4|^7.0|^8.0",
|
||||
"symfony/translation": "^6.4|^7.0|^8.0",
|
||||
"symfony/web-link": "^6.4|^7.0|^8.0",
|
||||
"symfony/yaml": "^6.4|^7.0|^8.0"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Bundle\\TwigBundle\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides a tight integration of Twig into the Symfony full-stack framework",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/twig-bundle/tree/v7.4.8"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-03-24T13:12:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/type-info",
|
||||
"version": "v7.4.9",
|
||||
@@ -6138,6 +6416,86 @@
|
||||
],
|
||||
"time": "2026-04-30T15:19:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/ux-react",
|
||||
"version": "v2.36.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/ux-react.git",
|
||||
"reference": "d7436063f39be1af8b0f9f1a2b746aedd9fffdda"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/ux-react/zipball/d7436063f39be1af8b0f9f1a2b746aedd9fffdda",
|
||||
"reference": "d7436063f39be1af8b0f9f1a2b746aedd9fffdda",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"symfony/stimulus-bundle": "^2.9.1|^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/asset-mapper": "^6.3|^7.0|^8.0",
|
||||
"symfony/finder": "^5.4|^6.0|^7.0|^8.0",
|
||||
"symfony/framework-bundle": "^5.4|^6.0|^7.0|^8.0",
|
||||
"symfony/phpunit-bridge": "^5.4|^6.0|^7.0|^8.0",
|
||||
"symfony/twig-bundle": "^5.4|^6.0|^7.0|^8.0",
|
||||
"symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/ux",
|
||||
"name": "symfony/ux"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\UX\\React\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Titouan Galopin",
|
||||
"email": "galopintitouan@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Integration of React in Symfony",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"symfony-ux"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/ux-react/tree/v2.36.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-05-06T04:31:36+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/validator",
|
||||
"version": "v7.4.10",
|
||||
@@ -6410,6 +6768,82 @@
|
||||
],
|
||||
"time": "2026-04-18T13:18:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/webpack-encore-bundle",
|
||||
"version": "v2.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/webpack-encore-bundle.git",
|
||||
"reference": "5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/webpack-encore-bundle/zipball/5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e",
|
||||
"reference": "5b932e0feddd81aaf0ecd7d5fcd2e450e5a7817e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1.0",
|
||||
"symfony/asset": "^5.4 || ^6.2 || ^7.0 || ^8.0",
|
||||
"symfony/config": "^5.4 || ^6.2 || ^7.0 || ^8.0",
|
||||
"symfony/dependency-injection": "^5.4 || ^6.2 || ^7.0 || ^8.0",
|
||||
"symfony/http-kernel": "^5.4 || ^6.2 || ^7.0 || ^8.0",
|
||||
"symfony/service-contracts": "^1.1.9 || ^2.1.3 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/framework-bundle": "^5.4 || ^6.2 || ^7.0 || ^8.0",
|
||||
"symfony/http-client": "^5.4 || ^6.2 || ^7.0 || ^8.0",
|
||||
"symfony/phpunit-bridge": "^5.4 || ^6.2 || ^7.0 || ^8.0",
|
||||
"symfony/twig-bundle": "^5.4 || ^6.2 || ^7.0 || ^8.0",
|
||||
"symfony/web-link": "^5.4 || ^6.2 || ^7.0 || ^8.0"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/webpack-encore",
|
||||
"name": "symfony/webpack-encore"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\WebpackEncoreBundle\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Integration of your Symfony app with Webpack Encore",
|
||||
"support": {
|
||||
"issues": "https://github.com/symfony/webpack-encore-bundle/issues",
|
||||
"source": "https://github.com/symfony/webpack-encore-bundle/tree/v2.4.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-27T13:41:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v7.4.13",
|
||||
@@ -9033,121 +9467,6 @@
|
||||
],
|
||||
"time": "2026-05-23T16:05:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/twig-bridge",
|
||||
"version": "v7.4.12",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/twig-bridge.git",
|
||||
"reference": "81663873d946531129c76c65e80b681ce99c0e89"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/twig-bridge/zipball/81663873d946531129c76c65e80b681ce99c0e89",
|
||||
"reference": "81663873d946531129c76c65e80b681ce99c0e89",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"symfony/deprecation-contracts": "^2.5|^3",
|
||||
"symfony/translation-contracts": "^2.5|^3",
|
||||
"twig/twig": "^3.21"
|
||||
},
|
||||
"conflict": {
|
||||
"phpdocumentor/reflection-docblock": "<5.2|>=7",
|
||||
"phpdocumentor/type-resolver": "<1.5.1",
|
||||
"symfony/console": "<6.4",
|
||||
"symfony/form": "<6.4.32|>7,<7.3.10|>7.4,<7.4.4|>8.0,<8.0.4",
|
||||
"symfony/http-foundation": "<6.4",
|
||||
"symfony/http-kernel": "<6.4",
|
||||
"symfony/mime": "<6.4.37|>7,<7.4.9|>8.0,<8.0.9",
|
||||
"symfony/serializer": "<6.4",
|
||||
"symfony/translation": "<6.4",
|
||||
"symfony/workflow": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"egulias/email-validator": "^2.1.10|^3|^4",
|
||||
"league/html-to-markdown": "^5.0",
|
||||
"phpdocumentor/reflection-docblock": "^5.2|^6.0",
|
||||
"symfony/asset": "^6.4|^7.0|^8.0",
|
||||
"symfony/asset-mapper": "^6.4|^7.0|^8.0",
|
||||
"symfony/console": "^6.4|^7.0|^8.0",
|
||||
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||
"symfony/emoji": "^7.1|^8.0",
|
||||
"symfony/expression-language": "^6.4|^7.0|^8.0",
|
||||
"symfony/finder": "^6.4|^7.0|^8.0",
|
||||
"symfony/form": "^6.4.32|~7.3.10|^7.4.4|^8.0.4",
|
||||
"symfony/html-sanitizer": "^6.4|^7.0|^8.0",
|
||||
"symfony/http-foundation": "^7.3|^8.0",
|
||||
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||
"symfony/intl": "^6.4|^7.0|^8.0",
|
||||
"symfony/mime": "^6.4.37|^7.4.9|^8.0.9",
|
||||
"symfony/polyfill-intl-icu": "~1.0",
|
||||
"symfony/property-info": "^6.4|^7.0|^8.0",
|
||||
"symfony/routing": "^6.4|^7.0|^8.0",
|
||||
"symfony/security-acl": "^2.8|^3.0",
|
||||
"symfony/security-core": "^6.4|^7.0|^8.0",
|
||||
"symfony/security-csrf": "^6.4|^7.0|^8.0",
|
||||
"symfony/security-http": "^6.4|^7.0|^8.0",
|
||||
"symfony/serializer": "^6.4.3|^7.0.3|^8.0",
|
||||
"symfony/stopwatch": "^6.4|^7.0|^8.0",
|
||||
"symfony/translation": "^6.4|^7.0|^8.0",
|
||||
"symfony/validator": "^6.4|^7.0|^8.0",
|
||||
"symfony/web-link": "^6.4|^7.0|^8.0",
|
||||
"symfony/workflow": "^6.4|^7.0|^8.0",
|
||||
"symfony/yaml": "^6.4|^7.0|^8.0",
|
||||
"twig/cssinliner-extra": "^3",
|
||||
"twig/inky-extra": "^3",
|
||||
"twig/markdown-extra": "^3"
|
||||
},
|
||||
"type": "symfony-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Bridge\\Twig\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Provides integration for Twig with various Symfony components",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/twig-bridge/tree/v7.4.12"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-29T17:13:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "theseer/tokenizer",
|
||||
"version": "2.0.1",
|
||||
|
||||
@@ -10,4 +10,8 @@ return [
|
||||
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
|
||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||
Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],
|
||||
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
|
||||
Symfony\UX\React\ReactBundle::class => ['all' => true],
|
||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
twig:
|
||||
file_name_pattern: '*.twig'
|
||||
|
||||
when@test:
|
||||
twig:
|
||||
strict_variables: true
|
||||
@@ -0,0 +1,45 @@
|
||||
webpack_encore:
|
||||
# The path where Encore is building the assets - i.e. Encore.setOutputPath()
|
||||
output_path: '%kernel.project_dir%/public/build'
|
||||
# If multiple builds are defined (as shown below), you can disable the default build:
|
||||
# output_path: false
|
||||
|
||||
# Set attributes that will be rendered on all script and link tags
|
||||
script_attributes:
|
||||
defer: true
|
||||
# Uncomment (also under link_attributes) if using Turbo Drive
|
||||
# https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
|
||||
# 'data-turbo-track': reload
|
||||
# link_attributes:
|
||||
# Uncomment if using Turbo Drive
|
||||
# 'data-turbo-track': reload
|
||||
|
||||
# If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
|
||||
# crossorigin: 'anonymous'
|
||||
|
||||
# Preload all rendered script and link tags automatically via the HTTP/2 Link header
|
||||
# preload: true
|
||||
|
||||
# Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
|
||||
# strict_mode: false
|
||||
|
||||
# If you have multiple builds:
|
||||
# builds:
|
||||
# frontend: '%kernel.project_dir%/public/frontend/build'
|
||||
|
||||
# pass the build name as the 3rd argument to the Twig functions
|
||||
# {{ encore_entry_script_tags('entry1', null, 'frontend') }}
|
||||
|
||||
framework:
|
||||
assets:
|
||||
json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
|
||||
|
||||
#when@prod:
|
||||
# webpack_encore:
|
||||
# # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
|
||||
# # Available in version 1.2
|
||||
# cache: true
|
||||
|
||||
#when@test:
|
||||
# webpack_encore:
|
||||
# strict_mode: false
|
||||
@@ -1436,6 +1436,57 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* }>,
|
||||
* },
|
||||
* }
|
||||
* @psalm-type StimulusConfig = array{
|
||||
* controller_paths?: list<scalar|Param|null>,
|
||||
* controllers_json?: scalar|Param|null, // Default: "%kernel.project_dir%/assets/controllers.json"
|
||||
* }
|
||||
* @psalm-type ReactConfig = array{
|
||||
* controllers_path?: scalar|Param|null, // The path to the directory where React controller components are stored - relevant only when using symfony/asset-mapper. // Default: "%kernel.project_dir%/assets/react/controllers"
|
||||
* name_glob?: list<scalar|Param|null>,
|
||||
* }
|
||||
* @psalm-type WebpackEncoreConfig = array{
|
||||
* output_path?: scalar|Param|null, // The path where Encore is building the assets - i.e. Encore.setOutputPath()
|
||||
* crossorigin?: false|"anonymous"|"use-credentials"|Param, // crossorigin value when Encore.enableIntegrityHashes() is used, can be false (default), anonymous or use-credentials // Default: false
|
||||
* preload?: bool|Param, // preload all rendered script and link tags automatically via the http2 Link header. // Default: false
|
||||
* cache?: bool|Param, // Enable caching of the entry point file(s) // Default: false
|
||||
* strict_mode?: bool|Param, // Throw an exception if the entrypoints.json file is missing or an entry is missing from the data // Default: true
|
||||
* builds?: array<string, scalar|Param|null>,
|
||||
* script_attributes?: array<string, scalar|Param|null>,
|
||||
* link_attributes?: array<string, scalar|Param|null>,
|
||||
* }
|
||||
* @psalm-type TwigConfig = array{
|
||||
* form_themes?: list<scalar|Param|null>,
|
||||
* globals?: array<string, array{ // Default: []
|
||||
* id?: scalar|Param|null,
|
||||
* type?: scalar|Param|null,
|
||||
* value?: mixed,
|
||||
* }>,
|
||||
* autoescape_service?: scalar|Param|null, // Default: null
|
||||
* autoescape_service_method?: scalar|Param|null, // Default: null
|
||||
* base_template_class?: scalar|Param|null, // Deprecated: The child node "base_template_class" at path "twig.base_template_class" is deprecated.
|
||||
* cache?: scalar|Param|null, // Default: true
|
||||
* charset?: scalar|Param|null, // Default: "%kernel.charset%"
|
||||
* debug?: bool|Param, // Default: "%kernel.debug%"
|
||||
* strict_variables?: bool|Param, // Default: "%kernel.debug%"
|
||||
* auto_reload?: scalar|Param|null,
|
||||
* optimizations?: int|Param,
|
||||
* default_path?: scalar|Param|null, // The default path used to load templates. // Default: "%kernel.project_dir%/templates"
|
||||
* file_name_pattern?: string|list<scalar|Param|null>,
|
||||
* paths?: array<string, mixed>,
|
||||
* date?: array{ // The default format options used by the date filter.
|
||||
* format?: scalar|Param|null, // Default: "F j, Y H:i"
|
||||
* interval_format?: scalar|Param|null, // Default: "%d days"
|
||||
* timezone?: scalar|Param|null, // The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used. // Default: null
|
||||
* },
|
||||
* number_format?: array{ // The default format options for the number_format filter.
|
||||
* decimals?: int|Param, // Default: 0
|
||||
* decimal_point?: scalar|Param|null, // Default: "."
|
||||
* thousands_separator?: scalar|Param|null, // Default: ","
|
||||
* },
|
||||
* mailer?: array{
|
||||
* html_to_text_converter?: scalar|Param|null, // A service implementing the "Symfony\Component\Mime\HtmlToTextConverter\HtmlToTextConverterInterface". // Default: null
|
||||
* },
|
||||
* }
|
||||
* @psalm-type ConfigType = array{
|
||||
* imports?: ImportsConfig,
|
||||
* parameters?: ParametersConfig,
|
||||
@@ -1447,6 +1498,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* nelmio_api_doc?: NelmioApiDocConfig,
|
||||
* stimulus?: StimulusConfig,
|
||||
* react?: ReactConfig,
|
||||
* webpack_encore?: WebpackEncoreConfig,
|
||||
* twig?: TwigConfig,
|
||||
* "when@dev"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
* parameters?: ParametersConfig,
|
||||
@@ -1460,6 +1515,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* debug?: DebugConfig,
|
||||
* maker?: MakerConfig,
|
||||
* nelmio_api_doc?: NelmioApiDocConfig,
|
||||
* stimulus?: StimulusConfig,
|
||||
* react?: ReactConfig,
|
||||
* webpack_encore?: WebpackEncoreConfig,
|
||||
* twig?: TwigConfig,
|
||||
* },
|
||||
* "when@prod"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
@@ -1472,6 +1531,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* nelmio_api_doc?: NelmioApiDocConfig,
|
||||
* stimulus?: StimulusConfig,
|
||||
* react?: ReactConfig,
|
||||
* webpack_encore?: WebpackEncoreConfig,
|
||||
* twig?: TwigConfig,
|
||||
* },
|
||||
* "when@test"?: array{
|
||||
* imports?: ImportsConfig,
|
||||
@@ -1484,6 +1547,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* lexik_jwt_authentication?: LexikJwtAuthenticationConfig,
|
||||
* nelmio_cors?: NelmioCorsConfig,
|
||||
* nelmio_api_doc?: NelmioApiDocConfig,
|
||||
* stimulus?: StimulusConfig,
|
||||
* react?: ReactConfig,
|
||||
* webpack_encore?: WebpackEncoreConfig,
|
||||
* twig?: TwigConfig,
|
||||
* },
|
||||
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
||||
* imports?: ImportsConfig,
|
||||
|
||||
Generated
+8880
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/preset-typescript": "^7.0.0",
|
||||
"@hotwired/stimulus": "^3.0.0",
|
||||
"@symfony/stimulus-bridge": "^3.2.0 || ^4.0.0",
|
||||
"@symfony/ux-react": "file:vendor/symfony/ux-react/assets",
|
||||
"@symfony/webpack-encore": "^6.0.0",
|
||||
"@tailwindcss/postcss": "^4.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"core-js": "^3.38.0",
|
||||
"postcss": "^8.0.0",
|
||||
"postcss-loader": "^8.0.0",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"ts-loader": "^9.6.0",
|
||||
"typescript": "^5.0.0",
|
||||
"webpack": "^5.72",
|
||||
"webpack-cli": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.0",
|
||||
"@hookform/resolvers": "^5.4.0",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@tanstack/react-table": "^8.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.0.0",
|
||||
"react-hot-toast": "^2.0.0",
|
||||
"react-router-dom": "^7.0.0",
|
||||
"react-select": "^5.0.0",
|
||||
"sonner": "^1.0.0",
|
||||
"zod": "^3.0.0",
|
||||
"zustand": "^5.0.0"
|
||||
},
|
||||
"license": "UNLICENSED",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev-server": "encore dev-server",
|
||||
"dev": "encore dev",
|
||||
"watch": "encore dev --watch",
|
||||
"build": "encore production --progress"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Admin\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class AdminController extends AbstractController
|
||||
{
|
||||
#[Route('/admin/{reactRouting}', name: 'admin_app', requirements: ['reactRouting' => '.*'], defaults: ['reactRouting' => ''])]
|
||||
public function index(): Response
|
||||
{
|
||||
return $this->render('admin/index.html.twig');
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,34 @@
|
||||
"config/routes/security.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/stimulus-bundle": {
|
||||
"version": "2.36",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.24",
|
||||
"ref": "d21494ed2ddbde38942e8278299a23ce5cf4a9a1"
|
||||
},
|
||||
"files": [
|
||||
"assets/controllers.json",
|
||||
"assets/controllers/csrf_protection_controller.js",
|
||||
"assets/controllers/hello_controller.js",
|
||||
"assets/stimulus_bootstrap.js"
|
||||
]
|
||||
},
|
||||
"symfony/twig-bundle": {
|
||||
"version": "7.4",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.4",
|
||||
"ref": "f250159ebe99153d0c640a3e7742876fc7453f2c"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/twig.yaml",
|
||||
"templates/base.html.twig"
|
||||
]
|
||||
},
|
||||
"symfony/uid": {
|
||||
"version": "7.4",
|
||||
"recipe": {
|
||||
@@ -205,6 +233,18 @@
|
||||
"ref": "0df5844274d871b37fc3816c57a768ffc60a43a5"
|
||||
}
|
||||
},
|
||||
"symfony/ux-react": {
|
||||
"version": "2.36",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.9",
|
||||
"ref": "adec905b8d643c7e15a3a8d070c58ff3021288c6"
|
||||
},
|
||||
"files": [
|
||||
"assets/react/controllers/Hello.jsx"
|
||||
]
|
||||
},
|
||||
"symfony/validator": {
|
||||
"version": "7.4",
|
||||
"recipe": {
|
||||
@@ -216,5 +256,21 @@
|
||||
"files": [
|
||||
"config/packages/validator.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/webpack-encore-bundle": {
|
||||
"version": "2.4",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "2.1",
|
||||
"ref": "b346dae458e64a1921ded2125993d94bd719a8dd"
|
||||
},
|
||||
"files": [
|
||||
"assets/app.js",
|
||||
"assets/styles/app.css",
|
||||
"config/packages/webpack_encore.yaml",
|
||||
"package.json",
|
||||
"webpack.config.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fa" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ClinicPro Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100..900&display=swap" rel="stylesheet">
|
||||
{{ encore_entry_link_tags('admin') }}
|
||||
</head>
|
||||
<body>
|
||||
<div id="admin-root"></div>
|
||||
{{ encore_entry_script_tags('admin') }}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Welcome!{% endblock %}</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>">
|
||||
{% block stylesheets %}
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{% endblock %}
|
||||
|
||||
{% set frankenphpHotReload = app.request.server.get('FRANKENPHP_HOT_RELOAD') %}
|
||||
{% if frankenphpHotReload %}
|
||||
<meta name="frankenphp-hot-reload:url" content="{{ frankenphpHotReload }}">
|
||||
<script src="https://cdn.jsdelivr.net/npm/idiomorph"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/frankenphp-hot-reload/+esm" type="module"></script>
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["assets/admin/*"]
|
||||
}
|
||||
},
|
||||
"include": ["assets/admin/**/*"],
|
||||
"exclude": ["node_modules", "public"]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
const Encore = require('@symfony/webpack-encore');
|
||||
|
||||
if (!Encore.isRuntimeEnvironmentConfigured()) {
|
||||
Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
|
||||
}
|
||||
|
||||
Encore
|
||||
.setOutputPath('public/build/')
|
||||
.setPublicPath('/build')
|
||||
|
||||
.addEntry('app', './assets/app.js')
|
||||
.addEntry('admin', './assets/admin/index.tsx')
|
||||
|
||||
.splitEntryChunks()
|
||||
.enableSingleRuntimeChunk()
|
||||
|
||||
.cleanupOutputBeforeBuild()
|
||||
.enableSourceMaps(!Encore.isProduction())
|
||||
.enableVersioning(Encore.isProduction())
|
||||
|
||||
.enableReactPreset()
|
||||
.enableTypeScriptLoader()
|
||||
.enablePostCssLoader()
|
||||
|
||||
.enableStimulusBridge('./assets/controllers.json')
|
||||
|
||||
.configureBabelPresetEnv((config) => {
|
||||
config.useBuiltIns = 'usage';
|
||||
config.corejs = '3.38';
|
||||
})
|
||||
;
|
||||
|
||||
module.exports = Encore.getWebpackConfig();
|
||||
Reference in New Issue
Block a user