diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a186cd2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[compose.yaml] +indent_size = 4 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b8a428b --- /dev/null +++ b/.env.example @@ -0,0 +1,86 @@ +APP_NAME="PC Shop" +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=ru +APP_FALLBACK_LOCALE=ru +APP_FAKER_LOCALE=ru_RU + +SHOP_COMPANY_NAME="PC Shop" +SHOP_COMPANY_DESCRIPTION="Интернет-магазин компьютерных комплектующих, ноутбуков и периферии. Подбор, сравнение и заказ в одном месте." +SHOP_CONTACT_PHONE="+7 900 000 00 00" +SHOP_CONTACT_EMAIL="support@pcshop.test" +SHOP_CONTACT_TELEGRAM="@pcshop_support" +SHOP_TELEGRAM_BOT_TOKEN= +SHOP_TELEGRAM_CHAT_ID= +SHOP_CONTACT_ADDRESS="ул. Технопарк, 24, Техноград" +SHOP_CONTACT_HOURS="Пн-Вс: 10:00-20:00" +SHOP_CURRENCY_CODE="RUB" +SHOP_CURRENCY_SYMBOL="₽" +SHOP_PAYMENT_BANK="Сбербанк" +SHOP_PAYMENT_CARD_HOLDER="ИВАНОВА ИВАННА ИВАНОВНА" +SHOP_PAYMENT_CARD_NUMBER="0000 0000 0000 0000" + +SEO_SITE_NAME="${APP_NAME}" +SEO_DEFAULT_TITLE="Интернет-магазин компьютерных комплектующих" +SEO_DEFAULT_DESCRIPTION="Процессоры, видеокарты, материнские платы, ноутбуки и периферия с доставкой по стране." +SEO_DEFAULT_KEYWORDS="комплектующие для пк, процессоры, видеокарты, материнские платы, ноутбуки, интернет-магазин" +SEO_DEFAULT_IMAGE="/favicon.ico" + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=pc-shop +DB_USERNAME=workeruser +DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..d35ac89 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,42 @@ +name: Deploy + +on: + push: + branches: + - main + workflow_dispatch: + inputs: + branch: + description: Git branch to deploy + required: true + default: main + +concurrency: + group: production-deploy + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Prepare SSH key and known_hosts + env: + DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} + DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }} + run: | + install -m 700 -d ~/.ssh + printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 + printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts + chmod 600 ~/.ssh/id_ed25519 ~/.ssh/known_hosts + + - name: Run deployment on server + env: + DEPLOY_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || github.ref_name }} + DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} + DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }} + DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }} + DEPLOY_USER: ${{ secrets.DEPLOY_USER }} + run: | + ssh -i ~/.ssh/id_ed25519 -p "${DEPLOY_PORT:-22}" "${DEPLOY_USER}@${DEPLOY_HOST}" \ + "cd \"${DEPLOY_PATH}\" && bash scripts/update-from-github.sh \"${DEPLOY_BRANCH}\"" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..069e47a --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +*.log +.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.fleet +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/hot +/public/storage +/bootstrap/cache/*.php +!/bootstrap/cache/.gitignore +/storage/*.key +/storage/pail +/vendor +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..f625d2f --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,228 @@ +# Деплой PC Shop + +В репозитории подготовлены production-артефакты: + +- `scripts/deploy.sh` - полный деплой в текущем checkout. +- `scripts/update-from-github.sh` - `git pull` + production-деплой. +- `deploy/nginx/pc-shop.http.conf` - HTTP-конфиг для первого запуска и выпуска сертификата. +- `deploy/nginx/pc-shop.conf` - финальный HTTPS-конфиг для Nginx. +- `.github/workflows/deploy.yml` - автодеплой по SSH из GitHub Actions. + +## 1. Что должно быть на сервере + +- PHP 8.2+ с расширениями `bcmath`, `ctype`, `curl`, `fileinfo`, `intl`, `mbstring`, `openssl`, `pdo_pgsql`, `tokenizer`, `xml`, `zip` +- Composer 2.x +- Node.js 20+ и npm +- PostgreSQL 14+ +- Nginx + PHP-FPM +- Certbot +- Git + +Если деплой будет обновляться из GitHub, проект на сервере должен быть именно `git clone`, а не архивом без `.git`. + +## 2. Первый запуск на сервере + +```bash +cd /var/www +git clone git@github.com:YOUR_ACCOUNT/pc-shop.git +cd pc-shop + +cp .env.example .env +# заполните .env и переключите его на production значения + +php artisan key:generate --force +bash scripts/deploy.sh +``` + +Опционально добавить демо-данные: + +```bash +php artisan db:seed --class=ShopCatalogSeeder +``` + +## 3. Production `.env` + +Базой остаётся обычный `.env.example`, отдельный production-файл не нужен. После копирования в `.env` минимально проверьте: + +```dotenv +APP_ENV=production +APP_DEBUG=false +APP_URL=https://shop.example.com + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=pc-shop +DB_USERNAME=pc_shop +DB_PASSWORD=secret + +SESSION_DRIVER=database +CACHE_STORE=database +QUEUE_CONNECTION=database + +MAIL_MAILER=smtp +MAIL_HOST=127.0.0.1 +MAIL_PORT=587 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="sales@example.com" + +SHOP_CONTACT_PHONE="+7 900 000 00 00" +SHOP_CONTACT_EMAIL="support@example.com" +SHOP_TELEGRAM_BOT_TOKEN= +SHOP_TELEGRAM_CHAT_ID= +``` + +Отдельный queue worker и cron сейчас не обязательны: в коде нет фоновых jobs или scheduler-задач. Если они появятся позже, для воркера уже подготовлен безопасный `php artisan queue:restart` в deploy-скрипте. + +## 4. Что делает `scripts/deploy.sh` + +Скрипт: + +1. Переводит приложение в maintenance mode. +2. Выполняет `composer install --no-dev`. +3. Выполняет `npm ci` и `npm run build`. +4. Проверяет `storage:link`. +5. Выполняет `composer run deploy`. +6. Делает `php artisan queue:restart`, если воркеры запущены. +7. Возвращает приложение в online. + +Запуск: + +```bash +bash scripts/deploy.sh +``` + +## 5. Обновление с GitHub на сервере + +После обычного `git clone` обновление становится одной командой: + +```bash +bash scripts/update-from-github.sh main +``` + +Скрипт: + +- останавливается, если есть локальные изменения в tracked-файлах; +- делает `git fetch --prune`; +- переключается на нужную ветку; +- выполняет `git pull --ff-only`; +- запускает production-деплой. + +По умолчанию ветка `main`, но можно передать другую: + +```bash +bash scripts/update-from-github.sh release +``` + +## 6. Nginx + +В репозитории два шаблона: + +- `deploy/nginx/pc-shop.http.conf` - стартовый HTTP-конфиг для первого запуска и `/.well-known/acme-challenge/` +- `deploy/nginx/pc-shop.conf` - основной HTTPS-конфиг с редиректом `80 -> 443` + +До выпуска сертификата включите именно HTTP-шаблон: + +```bash +sudo cp deploy/nginx/pc-shop.http.conf /etc/nginx/sites-available/pc-shop.conf +sudo ln -s /etc/nginx/sites-available/pc-shop.conf /etc/nginx/sites-enabled/pc-shop.conf +sudo nginx -t +sudo systemctl reload nginx +``` + +Что нужно проверить в шаблонах: + +- `server_name` +- `root` +- путь в `fastcgi_pass` под ваш PHP-FPM socket + +Корень сайта должен смотреть в `public/`, не в корень проекта. + +## 7. HTTPS и сертификат + +1. Убедитесь, что домен уже указывает на сервер и порт `80` открыт. +2. В `.env` укажите `APP_URL=https://shop.example.com`. +3. Выпустите сертификат через `certbot`: + +```bash +sudo certbot certonly --webroot -w /var/www/pc-shop/public -d shop.example.com -d www.shop.example.com +``` + +4. После успешного выпуска переключите nginx на HTTPS-шаблон: + +```bash +sudo cp deploy/nginx/pc-shop.conf /etc/nginx/sites-available/pc-shop.conf +sudo nginx -t +sudo systemctl reload nginx +``` + +5. Обновите Laravel-кеши после смены `APP_URL`: + +```bash +composer run deploy +``` + +Что нужно проверить в `deploy/nginx/pc-shop.conf`: + +- `server_name` +- `fastcgi_pass` +- путь в `ssl_certificate` +- путь в `ssl_certificate_key` + +После этого сайт должен открываться по `https://`, а `http://` будет перекидываться на защищённую версию. + +Если вы предпочитаете, чтобы `certbot` сам правил nginx, можно вместо `certonly --webroot` использовать: + +```bash +sudo certbot --nginx -d shop.example.com -d www.shop.example.com +``` + +Но для предсказуемого деплоя удобнее оставить конфиг под контролем репозитория и использовать `certonly`. + +## 8. Автообновление из GitHub Actions + +В репозитории добавлен workflow `.github/workflows/deploy.yml`. + +Он: + +- запускается на push в `main`; +- умеет ручной `workflow_dispatch`; +- подключается по SSH на сервер; +- вызывает `bash scripts/update-from-github.sh `. + +Нужные GitHub Secrets: + +- `DEPLOY_HOST` +- `DEPLOY_PORT` +- `DEPLOY_USER` +- `DEPLOY_PATH` +- `DEPLOY_SSH_KEY` +- `DEPLOY_KNOWN_HOSTS` + +`DEPLOY_PATH` должен указывать на директорию проекта на сервере, например `/var/www/pc-shop`. + +`DEPLOY_KNOWN_HOSTS` можно получить так: + +```bash +ssh-keyscan -H your-server-hostname +``` + +## 9. Проверка после деплоя + +```bash +php artisan about +php artisan migrate:status +php artisan route:list +``` + +Проверьте страницы: + +- `https://shop.example.com` +- `/` +- `/catalog` +- `/cart` +- `/favorites` +- `/compare` +- `/login` +- `/admin/login` diff --git a/app/Http/Controllers/Admin/AdminAuthController.php b/app/Http/Controllers/Admin/AdminAuthController.php new file mode 100644 index 0000000..934fd5a --- /dev/null +++ b/app/Http/Controllers/Admin/AdminAuthController.php @@ -0,0 +1,66 @@ + $this->buildCaptchaQuestion($request, self::LOGIN_CAPTCHA_CONTEXT), + ]); + } + + public function login(Request $request) + { + $validated = $request->validate([ + 'email' => ['required', 'email'], + 'password' => ['required', 'string'], + 'captcha' => ['required', 'string', 'max:10'], + ]); + + if (!$this->captchaIsValid($request, self::LOGIN_CAPTCHA_CONTEXT)) { + return back()->withErrors(['captcha' => 'Неверный ответ на капчу.'])->withInput(); + } + + $credentials = [ + 'email' => $validated['email'], + 'password' => $validated['password'], + ]; + + if (!Auth::attempt($credentials, $request->boolean('remember'))) { + return back()->withErrors(['email' => 'Неверный email или пароль.'])->withInput(); + } + + $request->session()->regenerate(); + $this->clearCaptcha($request, self::LOGIN_CAPTCHA_CONTEXT); + + if (!$request->user()->is_admin) { + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return back()->withErrors(['email' => 'Доступ разрешен только администраторам.']); + } + + return redirect()->route('admin.dashboard'); + } + + public function logout(Request $request) + { + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('admin.login'); + } +} diff --git a/app/Http/Controllers/Admin/AdminDashboardController.php b/app/Http/Controllers/Admin/AdminDashboardController.php new file mode 100644 index 0000000..a5b0b4e --- /dev/null +++ b/app/Http/Controllers/Admin/AdminDashboardController.php @@ -0,0 +1,24 @@ + [ + 'categories' => Category::count(), + 'products' => Product::count(), + 'orders' => Order::count(), + 'revenue' => (float) Order::sum('total'), + ], + 'recentOrders' => Order::query()->latest('id')->take(8)->get(), + ]); + } +} diff --git a/app/Http/Controllers/Admin/CategoryController.php b/app/Http/Controllers/Admin/CategoryController.php new file mode 100644 index 0000000..1ac9088 --- /dev/null +++ b/app/Http/Controllers/Admin/CategoryController.php @@ -0,0 +1,94 @@ + Category::query()->latest('id')->paginate(20), + ]); + } + + public function create() + { + return view('admin.categories.create'); + } + + public function store(Request $request) + { + $data = $this->validateCategory($request); + $data['slug'] = $this->makeUniqueSlug($data['slug'] ?: $data['name']); + Category::create($data); + + return redirect()->route('admin.categories.index')->with('status', 'Категория создана.'); + } + + public function edit(Category $category) + { + return view('admin.categories.edit', [ + 'category' => $category, + ]); + } + + public function update(Request $request, Category $category) + { + $data = $this->validateCategory($request, $category); + $data['slug'] = $this->makeUniqueSlug($data['slug'] ?: $data['name'], $category->id); + $category->update($data); + + return redirect()->route('admin.categories.index')->with('status', 'Категория обновлена.'); + } + + public function destroy(Category $category) + { + $category->delete(); + + return redirect()->route('admin.categories.index')->with('status', 'Категория удалена.'); + } + + private function validateCategory(Request $request, ?Category $category = null): array + { + return $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'slug' => [ + 'nullable', + 'string', + 'max:255', + Rule::unique('categories', 'slug')->ignore($category?->id), + ], + 'description' => ['nullable', 'string'], + 'is_active' => ['nullable', 'boolean'], + ]) + ['is_active' => $request->boolean('is_active')]; + } + + private function makeUniqueSlug(string $value, ?int $ignoreId = null): string + { + $base = Str::slug($value); + if ($base === '') { + $base = 'category'; + } + + $slug = $base; + $index = 2; + + while ( + Category::query() + ->where('slug', $slug) + ->when($ignoreId, fn ($query) => $query->whereKeyNot($ignoreId)) + ->exists() + ) { + $slug = "{$base}-{$index}"; + $index++; + } + + return $slug; + } +} diff --git a/app/Http/Controllers/Admin/ChatController.php b/app/Http/Controllers/Admin/ChatController.php new file mode 100644 index 0000000..debb733 --- /dev/null +++ b/app/Http/Controllers/Admin/ChatController.php @@ -0,0 +1,178 @@ +with('user', 'latestMessage') + ->withCount([ + 'messages as unread_count' => fn ($query) => $query + ->where('sender', 'customer') + ->where('is_read', false), + ]) + ->orderByDesc('last_message_at') + ->orderByDesc('id') + ->paginate(30); + + $selectedConversation = $this->resolveSelectedConversation($request, $conversations); + $initialMessages = collect(); + + if ($selectedConversation) { + $this->markCustomerMessagesAsRead($selectedConversation); + $initialMessages = $this->conversationMessages($selectedConversation); + } + + return view('admin.chats.index', [ + 'conversations' => $conversations, + 'selectedConversation' => $selectedConversation, + 'initialMessages' => $initialMessages->map(fn (ChatMessage $message) => $this->messagePayload($message))->values(), + ]); + } + + public function messages(ChatConversation $conversation): JsonResponse + { + $this->markCustomerMessagesAsRead($conversation); + + return response()->json([ + 'conversation' => $this->conversationPayload($conversation->loadMissing('user')), + 'messages' => $this->conversationMessages($conversation) + ->map(fn (ChatMessage $message) => $this->messagePayload($message)) + ->values(), + ]); + } + + public function storeMessage(Request $request, ChatConversation $conversation): JsonResponse + { + $validated = $request->validate([ + 'message' => ['required', 'string', 'max:2000'], + ]); + + $messageText = $this->sanitizeMessage((string) $validated['message']); + if ($messageText === '') { + throw ValidationException::withMessages([ + 'message' => 'Сообщение содержит недопустимые символы.', + ]); + } + + $message = $conversation->messages()->create([ + 'sender' => 'admin', + 'body' => $messageText, + 'is_read' => false, + ]); + + $conversation->update([ + 'status' => 'open', + 'last_message_at' => now(), + ]); + + return response()->json([ + 'message' => $this->messagePayload($message), + ]); + } + + public function updateStatus(Request $request, ChatConversation $conversation): RedirectResponse + { + $validated = $request->validate([ + 'status' => ['required', 'string', Rule::in([ + ChatConversation::STATUS_OPEN, + ChatConversation::STATUS_CLOSED, + ])], + ]); + + $conversation->update([ + 'status' => $validated['status'], + ]); + + return redirect() + ->route('admin.chats.index', ['conversation' => $conversation->id]) + ->with('status', $conversation->isClosed() ? 'Чат закрыт.' : 'Чат открыт.'); + } + + public function destroy(ChatConversation $conversation): RedirectResponse + { + $conversation->delete(); + + return redirect() + ->route('admin.chats.index') + ->with('status', 'Чат удален.'); + } + + private function resolveSelectedConversation(Request $request, $conversations): ?ChatConversation + { + $selectedId = $request->integer('conversation'); + if ($selectedId > 0) { + return ChatConversation::query()->with('user')->find($selectedId); + } + + $firstConversation = $conversations->first(); + if (!$firstConversation) { + return null; + } + + return ChatConversation::query()->with('user')->find($firstConversation->id); + } + + private function markCustomerMessagesAsRead(ChatConversation $conversation): void + { + $conversation->messages() + ->where('sender', 'customer') + ->where('is_read', false) + ->update(['is_read' => true]); + } + + private function conversationMessages(ChatConversation $conversation) + { + return $conversation->messages() + ->orderBy('id') + ->limit(200) + ->get(); + } + + private function messagePayload(ChatMessage $message): array + { + return [ + 'id' => $message->id, + 'sender' => $message->sender, + 'body' => $message->body, + 'created_at' => $message->created_at?->toIso8601String(), + 'time' => $message->created_at?->format('H:i'), + ]; + } + + private function conversationPayload(ChatConversation $conversation): array + { + $preview = $conversation->latestMessage?->body; + + return [ + 'id' => $conversation->id, + 'title' => $conversation->display_name, + 'subtitle' => $conversation->user?->email ?: ('Токен: ' . Str::limit($conversation->visitor_token, 14, '...')), + 'preview' => $preview ? Str::limit($preview, 80) : 'Нет сообщений', + 'status' => $conversation->status, + 'is_closed' => $conversation->isClosed(), + ]; + } + + private function sanitizeMessage(string $value): string + { + $text = strip_tags($value); + $text = str_replace(["\r\n", "\r"], "\n", $text); + $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $text) ?? $text; + + return trim(Str::limit($text, 2000, '')); + } +} diff --git a/app/Http/Controllers/Admin/HomeSlideController.php b/app/Http/Controllers/Admin/HomeSlideController.php new file mode 100644 index 0000000..da2651c --- /dev/null +++ b/app/Http/Controllers/Admin/HomeSlideController.php @@ -0,0 +1,167 @@ +orderByRaw("CASE WHEN zone = 'left' THEN 0 ELSE 1 END") + ->ordered() + ->get() + ->groupBy('zone'); + + return view('admin.home-slides.index', [ + 'slides' => $slides, + 'zoneLabels' => $this->zoneLabels(), + ]); + } + + public function create(Request $request): View + { + $zone = in_array($request->query('zone'), self::ZONES, true) ? $request->query('zone') : 'left'; + + return view('admin.home-slides.create', [ + 'slide' => new HomeSlide([ + 'zone' => $zone, + 'show_title' => true, + 'show_subtitle' => true, + 'show_button' => true, + 'sort_order' => 100, + 'is_active' => true, + ]), + 'zoneLabels' => $this->zoneLabels(), + ]); + } + + public function store(Request $request): RedirectResponse + { + $data = $this->validateSlide($request, true); + $data['image_path'] = $this->storeImage($request); + + HomeSlide::create($data); + + return redirect()->route('admin.home-slides.index')->with('status', 'Слайд добавлен.'); + } + + public function edit(HomeSlide $homeSlide): View + { + return view('admin.home-slides.edit', [ + 'slide' => $homeSlide, + 'zoneLabels' => $this->zoneLabels(), + ]); + } + + public function update(Request $request, HomeSlide $homeSlide): RedirectResponse + { + $data = $this->validateSlide($request); + + if ($request->hasFile('image')) { + $newPath = $this->storeImage($request); + $oldPath = $homeSlide->image_path; + + $data['image_path'] = $newPath; + $homeSlide->update($data); + + if ($oldPath && $oldPath !== $newPath) { + $this->deleteImage($oldPath); + } + + return redirect()->route('admin.home-slides.index')->with('status', 'Слайд обновлен.'); + } + + $homeSlide->update($data); + + return redirect()->route('admin.home-slides.index')->with('status', 'Слайд обновлен.'); + } + + public function destroy(HomeSlide $homeSlide): RedirectResponse + { + $this->deleteImage($homeSlide->image_path); + $homeSlide->delete(); + + return redirect()->route('admin.home-slides.index')->with('status', 'Слайд удален.'); + } + + private function validateSlide(Request $request, bool $requireImage = false): array + { + return $request->validate([ + 'zone' => ['required', Rule::in(self::ZONES)], + 'title' => ['nullable', 'string', 'max:160'], + 'show_title' => ['nullable', 'boolean'], + 'subtitle' => ['nullable', 'string', 'max:1500'], + 'show_subtitle' => ['nullable', 'boolean'], + 'button_text' => ['nullable', 'string', 'max:60'], + 'button_url' => ['nullable', 'string', 'max:255', 'regex:/^(\\/|https?:\\/\\/).+/i'], + 'show_button' => ['nullable', 'boolean'], + 'sort_order' => ['required', 'integer', 'min:0', 'max:9999'], + 'is_active' => ['nullable', 'boolean'], + 'image' => [ + $requireImage ? 'required' : 'nullable', + 'image', + 'mimes:jpg,jpeg,png,webp', + 'max:5120', + ], + ]) + [ + 'show_title' => $request->boolean('show_title'), + 'show_subtitle' => $request->boolean('show_subtitle'), + 'show_button' => $request->boolean('show_button'), + 'is_active' => $request->boolean('is_active'), + ]; + } + + private function storeImage(Request $request): string + { + $file = $request->file('image'); + if (!$file) { + return ''; + } + + $extension = strtolower((string) $file->getClientOriginalExtension()); + if ($extension === '') { + $extension = 'jpg'; + } + + $name = now()->format('YmdHis') . '-' . Str::uuid() . '.' . $extension; + return (string) $file->storeAs('home-slides', $name, 'public'); + } + + private function deleteImage(?string $path): void + { + if (!$path) { + return; + } + + if (str_starts_with($path, 'uploads/')) { + $filePath = public_path(ltrim($path, '/')); + if (File::exists($filePath)) { + File::delete($filePath); + } + + return; + } + + Storage::disk('public')->delete($path); + } + + private function zoneLabels(): array + { + return [ + 'left' => 'Большой слайдер (2/3)', + 'right' => 'Малый слайдер (1/3)', + ]; + } +} diff --git a/app/Http/Controllers/Admin/OrderController.php b/app/Http/Controllers/Admin/OrderController.php new file mode 100644 index 0000000..6315da9 --- /dev/null +++ b/app/Http/Controllers/Admin/OrderController.php @@ -0,0 +1,38 @@ + Order::query()->latest('id')->paginate(20), + ]); + } + + public function show(Order $order) + { + $order->load('items.product', 'user'); + + return view('admin.orders.show', [ + 'order' => $order, + ]); + } + + public function update(Request $request, Order $order) + { + $data = $request->validate([ + 'status' => ['required', 'string', Rule::in(['new', 'processing', 'paid', 'shipped', 'completed', 'cancelled'])], + ]); + + $order->update($data); + + return back()->with('status', 'Статус заказа обновлен.'); + } +} diff --git a/app/Http/Controllers/Admin/ProductController.php b/app/Http/Controllers/Admin/ProductController.php new file mode 100644 index 0000000..5535778 --- /dev/null +++ b/app/Http/Controllers/Admin/ProductController.php @@ -0,0 +1,816 @@ + Product::query()->with('category')->latest('id')->paginate(20), + ]); + } + + public function create() + { + $categories = Category::query()->where('is_active', true)->orderBy('name')->get(); + + return view('admin.products.create', [ + 'categories' => $categories, + 'categorySpecsById' => $this->categorySpecDefinitionsById($categories), + ]); + } + + public function store(Request $request): RedirectResponse + { + $data = $this->validateProduct($request); + unset($data['image'], $data['gallery_images'], $data['remove_image'], $data['remove_gallery_paths']); + $data['slug'] = $this->makeUniqueSlug($data['slug'] ?: $data['name']); + $category = Category::query()->find($data['category_id']); + $data['specs'] = $this->parseSpecs( + (array) $request->input('specs', []), + $request->input('specs_text'), + $category?->slug + ); + + $imagePath = $request->hasFile('image') ? $this->storeImage($request) : null; + $galleryPaths = $request->hasFile('gallery_images') + ? $this->storeGalleryImages($request) + : []; + + [$imagePath, $galleryPaths] = $this->normalizeProductImageSet($imagePath, $galleryPaths); + + $data['image_path'] = $imagePath; + $data['gallery_paths'] = $galleryPaths; + + if ($data['image_path'] === null) { + unset($data['image_path']); + } + + if ($data['gallery_paths'] === []) { + unset($data['gallery_paths']); + } + + Product::create($data); + + return redirect()->route('admin.products.index')->with('status', 'Товар создан.'); + } + + public function edit(Product $product) + { + $categories = Category::query()->where('is_active', true)->orderBy('name')->get(); + + return view('admin.products.edit', [ + 'product' => $product, + 'categories' => $categories, + 'categorySpecsById' => $this->categorySpecDefinitionsById($categories), + ]); + } + + public function update(Request $request, Product $product): RedirectResponse + { + $data = $this->validateProduct($request, $product); + unset($data['image'], $data['gallery_images'], $data['remove_image'], $data['remove_gallery_paths']); + $data['slug'] = $this->makeUniqueSlug($data['slug'] ?: $data['name'], $product->id); + $category = Category::query()->find($data['category_id']); + $data['specs'] = $this->parseSpecs( + (array) $request->input('specs', []), + $request->input('specs_text'), + $category?->slug + ); + + $oldImagePath = $product->image_path; + $oldGalleryPaths = $this->normalizeImagePaths((array) ($product->gallery_paths ?? [])); + $removeImage = $request->boolean('remove_image'); + $removeGalleryPaths = $this->normalizeImagePaths((array) $request->input('remove_gallery_paths', [])); + + $nextImagePath = $oldImagePath; + $nextGalleryPaths = $oldGalleryPaths; + + if ($request->hasFile('image')) { + $nextImagePath = $this->storeImage($request); + } elseif ($removeImage) { + $nextImagePath = null; + if ($oldImagePath) { + $removeGalleryPaths[] = $oldImagePath; + } + } + + if ($request->hasFile('gallery_images')) { + $nextGalleryPaths = $this->storeGalleryImages($request); + } elseif ($removeGalleryPaths !== []) { + $removablePaths = array_values(array_intersect($oldGalleryPaths, $removeGalleryPaths)); + $nextGalleryPaths = array_values(array_filter( + $oldGalleryPaths, + fn (string $path) => !in_array($path, $removablePaths, true) + )); + } + + [$nextImagePath, $nextGalleryPaths] = $this->normalizeProductImageSet($nextImagePath, $nextGalleryPaths); + + $data['image_path'] = $nextImagePath; + $data['gallery_paths'] = $nextGalleryPaths; + + $product->update($data); + + $activePaths = $this->normalizeImagePaths( + array_merge( + [$product->image_path], + (array) ($product->gallery_paths ?? []) + ) + ); + + if ($oldImagePath && !in_array($oldImagePath, $activePaths, true)) { + $this->deleteImage($oldImagePath); + } + + foreach ($oldGalleryPaths as $oldPath) { + if (!in_array($oldPath, $activePaths, true)) { + $this->deleteImage($oldPath); + } + } + + return redirect()->route('admin.products.index')->with('status', 'Товар обновлен.'); + } + + public function destroy(Product $product): RedirectResponse + { + $this->deleteImages(array_merge([$product->image_path], (array) ($product->gallery_paths ?? []))); + $product->delete(); + + return redirect()->route('admin.products.index')->with('status', 'Товар удален.'); + } + + public function exportCsv(): StreamedResponse + { + $specKeys = $this->allSpecKeys(); + $headers = array_merge( + [ + 'id', + 'category_slug', + 'category_name', + 'name', + 'slug', + 'sku', + 'price', + 'old_price', + 'stock', + 'short_description', + 'description', + 'is_active', + ], + $specKeys, + ['specs_json'] + ); + + return response()->streamDownload(function () use ($headers, $specKeys) { + $output = fopen('php://output', 'wb'); + if (!$output) { + return; + } + + fwrite($output, "\xEF\xBB\xBF"); + fputcsv($output, $headers, ';'); + + Product::query() + ->with('category') + ->orderBy('id') + ->chunk(200, function (EloquentCollection $products) use ($output, $specKeys) { + foreach ($products as $product) { + $specs = (array) ($product->specs ?? []); + $row = [ + $product->id, + $product->category?->slug ?? '', + $product->category?->name ?? '', + $product->name, + $product->slug, + $product->sku, + $product->price, + $product->old_price, + $product->stock, + $product->short_description, + $product->description, + $product->is_active ? 1 : 0, + ]; + + foreach ($specKeys as $key) { + $row[] = $specs[$key] ?? ''; + } + + $row[] = $specs === [] ? '' : json_encode($specs, JSON_UNESCAPED_UNICODE); + + fputcsv($output, $row, ';'); + } + }); + + fclose($output); + }, 'products-' . now()->format('Ymd-His') . '.csv', [ + 'Content-Type' => 'text/csv; charset=UTF-8', + ]); + } + + public function importCsv(Request $request): RedirectResponse + { + $request->validate([ + 'csv_file' => ['required', 'file', 'mimes:csv,txt', 'max:5120'], + ]); + + $path = $request->file('csv_file')?->getRealPath(); + if (!$path) { + return back()->withErrors(['csv_file' => 'Не удалось прочитать файл.']); + } + + [$handle, $delimiter] = $this->openCsv($path); + if (!$handle) { + return back()->withErrors(['csv_file' => 'Не удалось открыть CSV файл.']); + } + + $headerRow = fgetcsv($handle, 0, $delimiter); + if ($headerRow === false) { + fclose($handle); + return back()->withErrors(['csv_file' => 'CSV файл пустой.']); + } + + $headers = array_map(fn ($header) => $this->normalizeCsvHeader((string) $header), $headerRow); + if (!in_array('name', $headers, true) || !in_array('price', $headers, true)) { + fclose($handle); + return back()->withErrors(['csv_file' => 'В CSV должны быть колонки name и price.']); + } + + if (!in_array('category_slug', $headers, true) && !in_array('category_name', $headers, true)) { + fclose($handle); + return back()->withErrors(['csv_file' => 'В CSV должна быть category_slug или category_name.']); + } + + $categories = Category::query()->get(); + $categoriesBySlug = $categories->keyBy(fn (Category $category) => mb_strtolower($category->slug)); + $categoriesByName = $categories->keyBy(fn (Category $category) => mb_strtolower($category->name)); + $allSpecKeys = $this->allSpecKeys(); + + $created = 0; + $updated = 0; + $skipped = 0; + + while (($row = fgetcsv($handle, 0, $delimiter)) !== false) { + if ($this->rowIsEmpty($row)) { + continue; + } + + $rowData = $this->mapCsvRow($headers, $row); + $category = $this->resolveCategory($rowData, $categoriesBySlug, $categoriesByName); + + $name = trim((string) ($rowData['name'] ?? '')); + $price = $this->parseDecimal($rowData['price'] ?? null); + $stock = $this->parseInteger($rowData['stock'] ?? 0); + + if (!$category || $name === '' || $price === null || $stock === null) { + $skipped++; + continue; + } + + $sku = $this->trimToNull((string) ($rowData['sku'] ?? '')); + $slugInput = trim((string) ($rowData['slug'] ?? '')); + + $existing = null; + if ($slugInput !== '') { + $existing = Product::query()->where('slug', Str::slug($slugInput))->first(); + } + + if (!$existing && $sku !== null) { + $existing = Product::query()->where('sku', $sku)->first(); + } + + $slug = $this->makeUniqueSlug($slugInput !== '' ? $slugInput : $name, $existing?->id); + $specs = $this->extractSpecsFromCsvRow($rowData, $allSpecKeys, $category->slug); + + $payload = [ + 'category_id' => $category->id, + 'name' => $name, + 'slug' => $slug, + 'sku' => $this->trimToNull((string) ($rowData['sku'] ?? '')), + 'price' => $price, + 'old_price' => $this->parseDecimal($rowData['old_price'] ?? null), + 'stock' => $stock, + 'short_description' => $this->trimToNull((string) ($rowData['short_description'] ?? '')), + 'description' => $this->trimToNull((string) ($rowData['description'] ?? '')), + 'is_active' => $this->parseBooleanValue($rowData['is_active'] ?? null, true), + 'specs' => $specs, + ]; + + if ($existing) { + $existing->update($payload); + $updated++; + } else { + Product::create($payload); + $created++; + } + } + + fclose($handle); + + return redirect() + ->route('admin.products.index') + ->with('status', "Импорт завершен: создано {$created}, обновлено {$updated}, пропущено {$skipped}."); + } + + private function validateProduct(Request $request, ?Product $product = null): array + { + return $request->validate([ + 'category_id' => ['required', 'integer', 'exists:categories,id'], + 'name' => ['required', 'string', 'max:255'], + 'slug' => [ + 'nullable', + 'string', + 'max:255', + Rule::unique('products', 'slug')->ignore($product?->id), + ], + 'sku' => ['nullable', 'string', 'max:255'], + 'price' => ['required', 'numeric', 'min:0'], + 'old_price' => ['nullable', 'numeric', 'min:0'], + 'stock' => ['required', 'integer', 'min:0'], + 'short_description' => ['nullable', 'string', 'max:255'], + 'description' => ['nullable', 'string'], + 'image' => ['nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'], + 'gallery_images' => ['nullable', 'array', 'max:12'], + 'gallery_images.*' => ['image', 'mimes:jpg,jpeg,png,webp', 'max:5120'], + 'remove_image' => ['nullable', 'boolean'], + 'remove_gallery_paths' => ['nullable', 'array'], + 'remove_gallery_paths.*' => ['string', 'max:255'], + 'is_active' => ['nullable', 'boolean'], + 'specs' => ['nullable', 'array'], + 'specs.*' => ['nullable', 'string', 'max:255'], + 'specs_text' => ['nullable', 'string'], + ]) + ['is_active' => $request->boolean('is_active')]; + } + + private function makeUniqueSlug(string $value, ?int $ignoreId = null): string + { + $base = Str::slug($value); + if ($base === '') { + $base = 'product'; + } + + $slug = $base; + $index = 2; + + while ( + Product::query() + ->where('slug', $slug) + ->when($ignoreId, fn ($query) => $query->whereKeyNot($ignoreId)) + ->exists() + ) { + $slug = "{$base}-{$index}"; + $index++; + } + + return $slug; + } + + private function parseSpecs(array $specsInput = [], ?string $specsText = null, ?string $categorySlug = null): array + { + $specs = []; + + foreach ($specsInput as $key => $value) { + $normalizedKey = $this->normalizeSpecKey((string) $key); + $normalizedValue = trim((string) $value); + + if ($normalizedKey === '' || $normalizedValue === '') { + continue; + } + + $specs[$normalizedKey] = $normalizedValue; + } + + if ($specsText !== null && trim($specsText) !== '') { + $specs = array_merge($this->parseSpecsFromText($specsText), $specs); + } + + return $this->filterSpecsByCategory($specs, $categorySlug); + } + + private function parseSpecsFromText(string $specsText): array + { + $rows = preg_split('/\r\n|\r|\n/', $specsText); + $specs = []; + + foreach ($rows as $row) { + $line = trim($row); + if ($line === '' || !str_contains($line, ':')) { + continue; + } + + [$key, $value] = array_map('trim', explode(':', $line, 2)); + $key = $this->normalizeSpecKey($key); + + if ($key === '' || $value === '') { + continue; + } + + $specs[$key] = $value; + } + + return $specs; + } + + private function normalizeSpecKey(string $key): string + { + return Str::of($key) + ->lower() + ->replace(' ', '_') + ->replace('-', '_') + ->trim() + ->toString(); + } + + private function categorySpecDefinitionsById(Collection $categories): array + { + $specDefinitions = $this->specDefinitions(); + $savedValuesByCategory = $this->savedSpecValuesByCategory($categories); + + return $categories + ->mapWithKeys(function (Category $category) use ($specDefinitions, $savedValuesByCategory) { + $definitions = $specDefinitions[$category->slug] ?? []; + $categoryValues = $savedValuesByCategory[$category->id] ?? []; + + $prepared = array_map(function (array $definition) use ($categoryValues) { + $key = (string) ($definition['key'] ?? ''); + if ($key === '') { + return $definition; + } + + $presetOptions = collect((array) ($definition['options'] ?? [])) + ->map(fn ($value) => trim((string) $value)) + ->filter() + ->values(); + + $savedOptions = collect($categoryValues[$key] ?? []) + ->map(fn ($value) => trim((string) $value)) + ->filter() + ->values(); + + $definition['options'] = $presetOptions + ->concat($savedOptions) + ->unique() + ->values() + ->all(); + + return $definition; + }, $definitions); + + return [$category->id => $prepared]; + }) + ->all(); + } + + private function specDefinitions(): array + { + return config('product_specs.categories', []); + } + + private function allSpecKeys(): array + { + return collect($this->specDefinitions()) + ->flatten(1) + ->pluck('key') + ->filter() + ->unique() + ->values() + ->all(); + } + + private function savedSpecValuesByCategory(Collection $categories): array + { + $categoryIds = $categories->pluck('id')->filter()->values(); + if ($categoryIds->isEmpty()) { + return []; + } + + $values = []; + + Product::query() + ->select(['id', 'category_id', 'specs']) + ->whereIn('category_id', $categoryIds->all()) + ->whereNotNull('specs') + ->chunkById(200, function (EloquentCollection $products) use (&$values) { + foreach ($products as $product) { + $specs = (array) ($product->specs ?? []); + if ($specs === []) { + continue; + } + + foreach ($specs as $key => $value) { + if (!is_scalar($value)) { + continue; + } + + $normalizedKey = $this->normalizeSpecKey((string) $key); + $normalizedValue = trim((string) $value); + + if ($normalizedKey === '' || $normalizedValue === '') { + continue; + } + + $values[$product->category_id][$normalizedKey][$normalizedValue] = true; + } + } + }); + + foreach ($values as $categoryId => $specValues) { + foreach ($specValues as $key => $valueSet) { + $options = array_keys($valueSet); + natcasesort($options); + $values[$categoryId][$key] = array_values($options); + } + } + + return $values; + } + + private function filterSpecsByCategory(array $specs, ?string $categorySlug): array + { + if (!$categorySlug) { + return $specs; + } + + $allowedKeys = collect($this->specDefinitions()[$categorySlug] ?? []) + ->pluck('key') + ->all(); + + if ($allowedKeys === []) { + return $specs; + } + + return collect($specs)->only($allowedKeys)->all(); + } + + private function openCsv(string $path): array + { + $handle = fopen($path, 'rb'); + if (!$handle) { + return [null, ';']; + } + + $sample = fgets($handle); + rewind($handle); + + $delimiter = substr_count((string) $sample, ';') >= substr_count((string) $sample, ',') ? ';' : ','; + + return [$handle, $delimiter]; + } + + private function normalizeCsvHeader(string $header): string + { + $value = str_replace("\xEF\xBB\xBF", '', $header); + + return Str::of($value) + ->trim() + ->lower() + ->replace(' ', '_') + ->replace('-', '_') + ->toString(); + } + + private function rowIsEmpty(array $row): bool + { + foreach ($row as $value) { + if (trim((string) $value) !== '') { + return false; + } + } + + return true; + } + + private function mapCsvRow(array $headers, array $row): array + { + $row = array_pad($row, count($headers), ''); + $mapped = []; + + foreach ($headers as $index => $header) { + if ($header === '') { + continue; + } + + $mapped[$header] = trim((string) ($row[$index] ?? '')); + } + + return $mapped; + } + + private function resolveCategory(array $rowData, Collection $bySlug, Collection $byName): ?Category + { + $slug = mb_strtolower(trim((string) ($rowData['category_slug'] ?? ''))); + if ($slug !== '' && $bySlug->has($slug)) { + return $bySlug->get($slug); + } + + $name = mb_strtolower(trim((string) ($rowData['category_name'] ?? ''))); + if ($name !== '' && $byName->has($name)) { + return $byName->get($name); + } + + return null; + } + + private function extractSpecsFromCsvRow(array $rowData, array $allSpecKeys, ?string $categorySlug): array + { + $specs = []; + + foreach ($allSpecKeys as $key) { + $value = trim((string) ($rowData[$key] ?? '')); + if ($value !== '') { + $specs[$key] = $value; + } + } + + $specsJson = trim((string) ($rowData['specs_json'] ?? '')); + if ($specsJson !== '') { + $decoded = json_decode($specsJson, true); + + if (is_array($decoded)) { + foreach ($decoded as $key => $value) { + if (!is_scalar($value)) { + continue; + } + + $normalizedKey = $this->normalizeSpecKey((string) $key); + $normalizedValue = trim((string) $value); + + if ($normalizedKey === '' || $normalizedValue === '') { + continue; + } + + $specs[$normalizedKey] = $normalizedValue; + } + } + } + + return $this->filterSpecsByCategory($specs, $categorySlug); + } + + private function parseDecimal(mixed $value): ?float + { + $string = trim((string) $value); + if ($string === '') { + return null; + } + + $normalized = str_replace([' ', ','], ['', '.'], $string); + if (!is_numeric($normalized)) { + return null; + } + + $decimal = (float) $normalized; + + return $decimal < 0 ? null : $decimal; + } + + private function parseInteger(mixed $value): ?int + { + $string = trim((string) $value); + if ($string === '') { + return 0; + } + + if (!preg_match('/^-?\d+$/', $string)) { + return null; + } + + $integer = (int) $string; + + return $integer < 0 ? null : $integer; + } + + private function parseBooleanValue(mixed $value, bool $default = true): bool + { + $string = mb_strtolower(trim((string) $value)); + if ($string === '') { + return $default; + } + + return match ($string) { + '1', 'true', 'yes', 'y', 'да', 'active', 'on' => true, + '0', 'false', 'no', 'n', 'нет', 'inactive', 'off' => false, + default => $default, + }; + } + + private function storeImage(Request $request): string + { + $file = $request->file('image'); + if (!$file instanceof UploadedFile) { + return ''; + } + + return $this->storeUploadedImage($file); + } + + private function storeGalleryImages(Request $request): array + { + $files = $request->file('gallery_images', []); + if (!is_array($files)) { + return []; + } + + $paths = []; + foreach ($files as $file) { + if (!$file instanceof UploadedFile) { + continue; + } + + $path = $this->storeUploadedImage($file); + if ($path !== '') { + $paths[] = $path; + } + } + + return $this->normalizeImagePaths($paths); + } + + private function storeUploadedImage(UploadedFile $file): string + { + $extension = strtolower((string) $file->getClientOriginalExtension()); + if ($extension === '') { + $extension = 'jpg'; + } + + $name = now()->format('YmdHis') . '-' . Str::uuid() . '.' . $extension; + + return (string) $file->storeAs('products', $name, 'public'); + } + + private function deleteImage(?string $path): void + { + if (!$path) { + return; + } + + if (str_starts_with($path, 'uploads/')) { + $filePath = public_path(ltrim($path, '/')); + if (File::exists($filePath)) { + File::delete($filePath); + } + + return; + } + + Storage::disk('public')->delete($path); + } + + private function deleteImages(array $paths): void + { + foreach ($this->normalizeImagePaths($paths) as $path) { + $this->deleteImage($path); + } + } + + private function normalizeImagePaths(array $paths): array + { + return collect($paths) + ->filter(fn ($path) => is_string($path) && trim($path) !== '') + ->map(fn (string $path) => trim($path)) + ->unique() + ->values() + ->all(); + } + + private function normalizeProductImageSet(?string $imagePath, array $galleryPaths): array + { + $normalizedImagePath = $this->trimToNull((string) $imagePath); + $normalizedGalleryPaths = $this->normalizeImagePaths($galleryPaths); + + if ($normalizedImagePath === null && $normalizedGalleryPaths !== []) { + $normalizedImagePath = array_shift($normalizedGalleryPaths); + } + + if ($normalizedImagePath !== null) { + $normalizedGalleryPaths = array_values(array_filter( + $normalizedGalleryPaths, + fn (string $path) => $path !== $normalizedImagePath + )); + } + + return [$normalizedImagePath, $normalizedGalleryPaths]; + } + + private function trimToNull(string $value): ?string + { + $trimmed = trim($value); + + return $trimmed === '' ? null : $trimmed; + } +} diff --git a/app/Http/Controllers/Concerns/ManagesCaptcha.php b/app/Http/Controllers/Concerns/ManagesCaptcha.php new file mode 100644 index 0000000..2a420ec --- /dev/null +++ b/app/Http/Controllers/Concerns/ManagesCaptcha.php @@ -0,0 +1,39 @@ + $first) { + [$first, $second] = [$second, $first]; + } + + $question = $isSubtraction ? "{$first} - {$second}" : "{$first} + {$second}"; + $answer = (string) ($isSubtraction ? $first - $second : $first + $second); + + $request->session()->put("captcha.{$context}.answer", $answer); + + return $question; + } + + protected function captchaIsValid(Request $request, string $context, string $field = 'captcha'): bool + { + $expected = (string) $request->session()->get("captcha.{$context}.answer", ''); + $provided = trim((string) $request->input($field, '')); + + return $expected !== '' && hash_equals($expected, $provided); + } + + protected function clearCaptcha(Request $request, string $context): void + { + $request->session()->forget("captcha.{$context}.answer"); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +user()->load(['orders' => function ($query) { + $query->latest('id'); + }]); + + return view('shop.account', [ + 'user' => $user, + 'orders' => $user->orders, + ]); + } + + public function update(Request $request) + { + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', 'unique:users,email,' . $request->user()->id], + ]); + + $request->user()->update($validated); + + return back()->with('status', 'Данные профиля обновлены.'); + } +} diff --git a/app/Http/Controllers/Shop/AuthController.php b/app/Http/Controllers/Shop/AuthController.php new file mode 100644 index 0000000..bacba8c --- /dev/null +++ b/app/Http/Controllers/Shop/AuthController.php @@ -0,0 +1,102 @@ + $this->buildCaptchaQuestion($request, self::LOGIN_CAPTCHA_CONTEXT), + ]); + } + + public function login(Request $request) + { + $validated = $request->validate([ + 'email' => ['required', 'email'], + 'password' => ['required', 'string'], + 'captcha' => ['required', 'string', 'max:10'], + ]); + + if (!$this->captchaIsValid($request, self::LOGIN_CAPTCHA_CONTEXT)) { + return back() + ->withInput($request->only('email', 'remember')) + ->withErrors(['captcha' => 'Неверный ответ на капчу.']); + } + + $credentials = [ + 'email' => $validated['email'], + 'password' => $validated['password'], + ]; + + $remember = $request->boolean('remember'); + + if (!Auth::attempt($credentials, $remember)) { + return back() + ->withInput($request->only('email', 'remember')) + ->withErrors(['email' => 'Неверный email или пароль.']); + } + + $request->session()->regenerate(); + $this->clearCaptcha($request, self::LOGIN_CAPTCHA_CONTEXT); + + return redirect()->intended(route('account')); + } + + public function showRegisterForm(Request $request) + { + return view('shop.auth.register', [ + 'captchaQuestion' => $this->buildCaptchaQuestion($request, self::REGISTER_CAPTCHA_CONTEXT), + ]); + } + + public function register(Request $request) + { + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', 'unique:users,email'], + 'password' => ['required', 'confirmed', Password::defaults()], + 'captcha' => ['required', 'string', 'max:10'], + ]); + + if (!$this->captchaIsValid($request, self::REGISTER_CAPTCHA_CONTEXT)) { + return back() + ->withInput($request->only('name', 'email')) + ->withErrors(['captcha' => 'Неверный ответ на капчу.']); + } + + $user = User::create([ + 'name' => $validated['name'], + 'email' => $validated['email'], + 'password' => $validated['password'], + ]); + + Auth::login($user); + $request->session()->regenerate(); + $this->clearCaptcha($request, self::REGISTER_CAPTCHA_CONTEXT); + + return redirect()->route('account'); + } + + public function logout(Request $request) + { + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('home'); + } +} diff --git a/app/Http/Controllers/Shop/CartController.php b/app/Http/Controllers/Shop/CartController.php new file mode 100644 index 0000000..75f2de8 --- /dev/null +++ b/app/Http/Controllers/Shop/CartController.php @@ -0,0 +1,106 @@ +get('cart', []); + $products = Product::query() + ->whereIn('id', array_keys($cart)) + ->where('is_active', true) + ->with('category') + ->get() + ->keyBy('id'); + + $items = collect($cart) + ->map(function ($quantity, $productId) use ($products) { + $product = $products->get((int) $productId); + if (!$product) { + return null; + } + + $qty = max(1, (int) $quantity); + + return [ + 'product' => $product, + 'quantity' => $qty, + 'subtotal' => (float) $product->price * $qty, + ]; + }) + ->filter() + ->values(); + + return view('shop.cart', [ + 'items' => $items, + 'itemsCount' => $items->sum('quantity'), + 'total' => $items->sum('subtotal'), + ]); + } + + public function add(Product $product) + { + if (!$product->is_active || $product->stock < 1) { + return back()->with('status', 'Товар сейчас недоступен для заказа.'); + } + + $cart = (array) session()->get('cart', []); + $current = (int) ($cart[$product->id] ?? 0); + + if ($current >= $product->stock) { + return back()->with('status', 'В корзине уже максимальное доступное количество.'); + } + + $cart[$product->id] = $current + 1; + session()->put('cart', $cart); + + return back()->with('status', "Товар \"{$product->name}\" добавлен в корзину."); + } + + public function update(Request $request, Product $product) + { + $validated = $request->validate([ + 'quantity' => ['required', 'integer', 'min:1', 'max:99'], + ]); + + $cart = (array) session()->get('cart', []); + if (!isset($cart[$product->id])) { + return back(); + } + + $available = max(0, (int) $product->stock); + $quantity = min((int) $validated['quantity'], $available); + + if ($quantity < 1) { + unset($cart[$product->id]); + session()->put('cart', $cart); + + return back()->with('status', "Товар \"{$product->name}\" удален из корзины."); + } + + $cart[$product->id] = $quantity; + session()->put('cart', $cart); + + $message = $quantity < (int) $validated['quantity'] + ? 'Количество ограничено текущим остатком.' + : 'Количество товара обновлено.'; + + return back()->with('status', $message); + } + + public function remove(Product $product) + { + $cart = (array) session()->get('cart', []); + if (isset($cart[$product->id])) { + unset($cart[$product->id]); + session()->put('cart', $cart); + } + + return back()->with('status', "Товар \"{$product->name}\" удален из корзины."); + } +} diff --git a/app/Http/Controllers/Shop/CatalogController.php b/app/Http/Controllers/Shop/CatalogController.php new file mode 100644 index 0000000..5acc1a4 --- /dev/null +++ b/app/Http/Controllers/Shop/CatalogController.php @@ -0,0 +1,325 @@ +input('q', '')); + $query = Product::query() + ->where('is_active', true) + ->with('category'); + + if ($searchQuery === '') { + $query->whereRaw('1 = 0'); + } else { + $this->applyNameSearch($query, $searchQuery); + } + + $sort = $request->string('sort')->toString(); + match ($sort) { + 'price_asc' => $query->orderBy('price'), + 'price_desc' => $query->orderByDesc('price'), + 'name_asc' => $query->orderBy('name'), + default => $query->orderByDesc('id'), + }; + + $products = $query->paginate(self::PER_PAGE)->withQueryString(); + + return view('shop.search', [ + 'products' => $products, + 'searchQuery' => $searchQuery, + 'sort' => $sort ?: 'newest', + ]); + } + + public function index(Request $request) + { + $query = Product::query()->where('is_active', true)->with('category'); + + if ($request->filled('q')) { + $this->applyNameSearch($query, (string) $request->input('q')); + } + + if ($request->filled('category')) { + $query->whereHas('category', function ($builder) use ($request) { + $builder->where('slug', $request->string('category')); + }); + } + + $products = $query->orderByDesc('id')->paginate(self::PER_PAGE)->withQueryString(); + $categories = Category::query() + ->where('is_active', true) + ->orderBy('name') + ->get(); + + return view('shop.catalog', [ + 'products' => $products, + 'categories' => $categories, + ]); + } + + public function category(Request $request, Category $category) + { + $baseQuery = Product::query() + ->where('category_id', $category->id) + ->where('is_active', true); + $query = (clone $baseQuery)->with('category'); + + $appliedFilters = collect((array) $request->input('filters', [])) + ->map(function ($value) { + if (!is_scalar($value)) { + return ''; + } + + return trim((string) $value); + }) + ->filter(fn (string $value) => $value !== '') + ->all(); + + if ($request->filled('q')) { + $searchTerm = (string) $request->input('q'); + $this->applyNameSearch($baseQuery, $searchTerm); + $this->applyNameSearch($query, $searchTerm); + } + + $filters = $this->filtersForCategory($category->slug); + $priceBounds = $this->priceBounds($baseQuery); + + $priceFrom = $this->parseFilterNumber($request->input('price_from')); + if ($priceFrom !== null) { + $query->where('price', '>=', $priceFrom); + } + + $priceTo = $this->parseFilterNumber($request->input('price_to')); + if ($priceTo !== null) { + $query->where('price', '<=', $priceTo); + } + + $rangeFilters = []; + foreach ($filters as $filter) { + if (!$this->isRangeFilter($filter)) { + continue; + } + + $key = (string) ($filter['key'] ?? ''); + if ($key === '') { + continue; + } + + $bounds = $this->numericSpecBounds($baseQuery, $key); + $fromParam = $key . '_from'; + $toParam = $key . '_to'; + + $rangeFrom = $this->parseFilterNumber($request->input($fromParam)); + if ($rangeFrom !== null) { + $query->whereRaw( + "NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric >= ?", + [$key, $rangeFrom] + ); + } + + $rangeTo = $this->parseFilterNumber($request->input($toParam)); + if ($rangeTo !== null) { + $query->whereRaw( + "NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric <= ?", + [$key, $rangeTo] + ); + } + + $rangeFilters[$key] = [ + 'from' => $request->filled($fromParam) + ? trim((string) $request->input($fromParam)) + : $this->formatFilterNumber($bounds['min']), + 'to' => $request->filled($toParam) + ? trim((string) $request->input($toParam)) + : $this->formatFilterNumber($bounds['max']), + 'min' => $this->formatFilterNumber($bounds['min']), + 'max' => $this->formatFilterNumber($bounds['max']), + ]; + } + + foreach ($filters as $filter) { + if ($this->isRangeFilter($filter)) { + continue; + } + + $key = (string) ($filter['key'] ?? ''); + if ($key === '') { + continue; + } + + $value = $appliedFilters[$key] ?? null; + if ($value !== null) { + $query->whereRaw("specs->>? = ?", [$key, $value]); + } + } + + $sort = $request->string('sort')->toString(); + match ($sort) { + 'price_asc' => $query->orderBy('price'), + 'price_desc' => $query->orderByDesc('price'), + 'name_asc' => $query->orderBy('name'), + default => $query->orderByDesc('id'), + }; + + $products = $query->paginate(self::PER_PAGE)->withQueryString(); + $filterOptions = $this->filterOptions($category, $filters); + $priceFilter = [ + 'from' => $request->filled('price_from') + ? trim((string) $request->input('price_from')) + : $this->formatFilterNumber($priceBounds['min']), + 'to' => $request->filled('price_to') + ? trim((string) $request->input('price_to')) + : $this->formatFilterNumber($priceBounds['max']), + 'min' => $this->formatFilterNumber($priceBounds['min']), + 'max' => $this->formatFilterNumber($priceBounds['max']), + ]; + + return view('shop.category', [ + 'category' => $category, + 'products' => $products, + 'filters' => $filters, + 'filterOptions' => $filterOptions, + 'appliedFilters' => $appliedFilters, + 'rangeFilters' => $rangeFilters, + 'priceFilter' => $priceFilter, + 'sort' => $sort ?: 'newest', + ]); + } + + private function filtersForCategory(string $slug): array + { + return config('product_specs.categories.' . $slug, []); + } + + private function filterOptions(Category $category, array $filters): array + { + if (empty($filters)) { + return []; + } + + $specs = $category->products() + ->where('is_active', true) + ->pluck('specs'); + + $options = []; + foreach ($filters as $filter) { + if ($this->isRangeFilter($filter)) { + continue; + } + + $key = (string) ($filter['key'] ?? ''); + if ($key === '') { + continue; + } + + $valuesFromProducts = $specs + ->pluck($key) + ->filter() + ->unique() + ->sort() + ->values() + ->all(); + + $presetValues = collect((array) ($filter['options'] ?? [])) + ->filter(fn ($value) => $value !== null && $value !== '') + ->map(fn ($value) => (string) $value) + ->values() + ->all(); + + $options[$key] = collect($presetValues) + ->concat($valuesFromProducts) + ->map(fn ($value) => (string) $value) + ->filter() + ->unique() + ->values() + ->all(); + } + + return $options; + } + + private function applyNameSearch(Builder $query, string $term): void + { + $normalizedTerm = mb_strtolower(trim($term)); + if ($normalizedTerm === '') { + return; + } + + $query->whereRaw('LOWER(name) LIKE ?', ['%' . $normalizedTerm . '%']); + } + + private function isRangeFilter(array $filter): bool + { + return (string) ($filter['filter'] ?? 'select') === 'range'; + } + + private function parseFilterNumber(mixed $value): ?float + { + $string = trim((string) $value); + if ($string === '') { + return null; + } + + $normalized = str_replace([' ', ','], ['', '.'], $string); + if (!is_numeric($normalized)) { + return null; + } + + return (float) $normalized; + } + + private function formatFilterNumber(?float $value): string + { + if ($value === null) { + return ''; + } + + if (abs($value - round($value)) < 0.000001) { + return (string) (int) round($value); + } + + return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.'); + } + + private function priceBounds(Builder $query): array + { + $bounds = (clone $query) + ->selectRaw('MIN(price) as min_value, MAX(price) as max_value') + ->first(); + + return [ + 'min' => $this->parseFilterNumber($bounds?->min_value), + 'max' => $this->parseFilterNumber($bounds?->max_value), + ]; + } + + private function numericSpecBounds(Builder $query, string $key): array + { + if ($key === '') { + return ['min' => null, 'max' => null]; + } + + $bounds = (clone $query) + ->selectRaw( + "MIN(NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric) as min_value, MAX(NULLIF(regexp_replace(specs->>?, '[^0-9.]', '', 'g'), '')::numeric) as max_value", + [$key, $key] + ) + ->first(); + + return [ + 'min' => $this->parseFilterNumber($bounds?->min_value), + 'max' => $this->parseFilterNumber($bounds?->max_value), + ]; + } +} diff --git a/app/Http/Controllers/Shop/ChatController.php b/app/Http/Controllers/Shop/ChatController.php new file mode 100644 index 0000000..706a5a6 --- /dev/null +++ b/app/Http/Controllers/Shop/ChatController.php @@ -0,0 +1,199 @@ +resolveConversation($request); + $messages = $this->conversationMessages($conversation); + + $conversation->messages() + ->where('sender', 'admin') + ->where('is_read', false) + ->update(['is_read' => true]); + + return response()->json([ + 'conversation' => $this->conversationPayload($conversation), + 'conversation_id' => $conversation->id, + 'messages' => $messages->map(fn (ChatMessage $message) => $this->messagePayload($message))->values(), + ]); + } + + public function store(Request $request): JsonResponse + { + $validated = $request->validate([ + 'message' => ['required', 'string', 'max:2000'], + ]); + + $messageText = $this->sanitizeMessage((string) $validated['message']); + if ($messageText === '') { + throw ValidationException::withMessages([ + 'message' => 'Сообщение содержит недопустимые символы.', + ]); + } + + $conversation = $this->resolveConversationForWriting($request); + $message = $conversation->messages()->create([ + 'sender' => 'customer', + 'body' => $messageText, + 'is_read' => false, + ]); + + $conversation->update([ + 'status' => 'open', + 'last_message_at' => now(), + ]); + + return response()->json([ + 'conversation' => $this->conversationPayload($conversation), + 'message' => $this->messagePayload($message), + ]); + } + + private function resolveConversation(Request $request): ChatConversation + { + $token = $this->sessionToken($request); + $user = $request->user(); + + if (!$user) { + $conversation = ChatConversation::query()->where('visitor_token', $token)->first(); + if ($conversation && $conversation->user_id !== null) { + $token = $this->rotateSessionToken($request); + $conversation = null; + } + + return $conversation ?: ChatConversation::query()->firstOrCreate( + ['visitor_token' => $token], + ['status' => 'open'] + ); + } + + $userConversation = ChatConversation::query() + ->where('user_id', $user->id) + ->latest('id') + ->first(); + + if ($userConversation) { + return $userConversation; + } + + $guestConversation = ChatConversation::query() + ->where('visitor_token', $token) + ->whereNull('user_id') + ->first(); + + if ($guestConversation) { + $guestConversation->update(['user_id' => $user->id]); + return $guestConversation; + } + + return ChatConversation::query()->create([ + 'user_id' => $user->id, + 'visitor_token' => $this->generateUniqueToken(), + 'status' => ChatConversation::STATUS_OPEN, + ]); + } + + private function resolveConversationForWriting(Request $request): ChatConversation + { + $conversation = $this->resolveConversation($request); + if (!$conversation->isClosed()) { + return $conversation; + } + + $user = $request->user(); + if (!$user) { + return ChatConversation::query()->create([ + 'visitor_token' => $this->rotateSessionToken($request), + 'status' => ChatConversation::STATUS_OPEN, + ]); + } + + return ChatConversation::query()->create([ + 'user_id' => $user->id, + 'visitor_token' => $this->generateUniqueToken(), + 'status' => ChatConversation::STATUS_OPEN, + ]); + } + + private function conversationMessages(ChatConversation $conversation) + { + return $conversation->messages() + ->latest('id') + ->limit(100) + ->get() + ->reverse() + ->values(); + } + + private function messagePayload(ChatMessage $message): array + { + return [ + 'id' => $message->id, + 'sender' => $message->sender, + 'body' => $message->body, + 'created_at' => $message->created_at?->toIso8601String(), + 'time' => $message->created_at?->format('H:i'), + ]; + } + + private function conversationPayload(ChatConversation $conversation): array + { + return [ + 'id' => $conversation->id, + 'status' => $conversation->status, + 'is_closed' => $conversation->isClosed(), + 'notice' => $conversation->isClosed() + ? 'Чат закрыт администратором. Отправьте новое сообщение, чтобы начать новый диалог.' + : null, + ]; + } + + private function sanitizeMessage(string $value): string + { + $text = strip_tags($value); + $text = str_replace(["\r\n", "\r"], "\n", $text); + $text = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $text) ?? $text; + + return trim(Str::limit($text, 2000, '')); + } + + private function sessionToken(Request $request): string + { + $token = (string) $request->session()->get(self::SESSION_TOKEN_KEY, ''); + if ($token !== '') { + return $token; + } + + return $this->rotateSessionToken($request); + } + + private function rotateSessionToken(Request $request): string + { + $token = $this->generateUniqueToken(); + $request->session()->put(self::SESSION_TOKEN_KEY, $token); + + return $token; + } + + private function generateUniqueToken(): string + { + do { + $token = (string) Str::uuid(); + } while (ChatConversation::query()->where('visitor_token', $token)->exists()); + + return $token; + } +} diff --git a/app/Http/Controllers/Shop/CheckoutController.php b/app/Http/Controllers/Shop/CheckoutController.php new file mode 100644 index 0000000..eb4e730 --- /dev/null +++ b/app/Http/Controllers/Shop/CheckoutController.php @@ -0,0 +1,174 @@ +cartItems($request); + + if ($items->isEmpty()) { + return redirect()->route('cart.index')->with('status', 'Корзина пустая. Добавьте товары перед оформлением.'); + } + + return view('shop.checkout', [ + 'items' => $items, + 'total' => $items->sum('subtotal'), + 'itemsCount' => $items->sum('quantity'), + ]); + } + + public function prepare(Request $request) + { + $items = $this->cartItems($request); + + if ($items->isEmpty()) { + return redirect()->route('cart.index')->with('status', 'Корзина пустая. Добавьте товары перед оформлением.'); + } + + $validated = $request->validate([ + 'customer_name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255'], + 'phone' => ['nullable', 'string', 'max:50'], + 'address' => ['required', 'string', 'max:500'], + 'comment' => ['nullable', 'string', 'max:1000'], + ]); + + $validated['payment_method'] = 'card_transfer'; + $request->session()->put(self::CHECKOUT_CUSTOMER_KEY, $validated); + + return redirect()->route('checkout.payment'); + } + + public function payment(Request $request) + { + $items = $this->cartItems($request); + + if ($items->isEmpty()) { + return redirect()->route('cart.index')->with('status', 'Корзина пустая. Добавьте товары перед оформлением.'); + } + + $customer = $request->session()->get(self::CHECKOUT_CUSTOMER_KEY); + if (!is_array($customer)) { + return redirect()->route('checkout.show')->with('status', 'Сначала заполните данные получателя.'); + } + + return view('shop.checkout-payment', [ + 'items' => $items, + 'total' => $items->sum('subtotal'), + 'itemsCount' => $items->sum('quantity'), + 'customer' => $customer, + ]); + } + + public function store(Request $request) + { + $items = $this->cartItems($request); + + if ($items->isEmpty()) { + return redirect()->route('cart.index')->with('status', 'Корзина пустая. Добавьте товары перед оформлением.'); + } + + $validated = $request->session()->get(self::CHECKOUT_CUSTOMER_KEY); + if (!is_array($validated)) { + return redirect()->route('checkout.show')->with('status', 'Сначала заполните данные получателя.'); + } + + $validator = validator($validated, [ + 'customer_name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255'], + 'phone' => ['nullable', 'string', 'max:50'], + 'address' => ['required', 'string', 'max:500'], + 'comment' => ['nullable', 'string', 'max:1000'], + 'payment_method' => ['required', 'string', Rule::in(['card_transfer'])], + ]); + $validated = $validator->validate(); + + $order = DB::transaction(function () use ($request, $items, $validated) { + $order = Order::create([ + 'user_id' => optional($request->user())->id, + 'status' => 'new', + 'payment_method' => $validated['payment_method'], + 'total' => $items->sum('subtotal'), + 'items_count' => $items->sum('quantity'), + 'customer_name' => $validated['customer_name'], + 'email' => $validated['email'], + 'phone' => $validated['phone'] ?? null, + 'address' => $validated['address'], + 'comment' => $validated['comment'] ?? null, + ]); + + $rows = $items->map(function (array $item) use ($order) { + return [ + 'order_id' => $order->id, + 'product_id' => $item['product']->id, + 'name' => $item['product']->name, + 'price' => $item['product']->price, + 'quantity' => $item['quantity'], + 'subtotal' => $item['subtotal'], + 'created_at' => now(), + 'updated_at' => now(), + ]; + })->all(); + + OrderItem::insert($rows); + + return $order; + }); + + $request->session()->forget('cart'); + $request->session()->forget(self::CHECKOUT_CUSTOMER_KEY); + + return redirect()->route('checkout.success', $order); + } + + public function success(Order $order) + { + return view('shop.checkout-success', [ + 'order' => $order, + ]); + } + + private function cartItems(Request $request) + { + $cart = (array) $request->session()->get('cart', []); + + $products = Product::query() + ->whereIn('id', array_keys($cart)) + ->where('is_active', true) + ->get() + ->keyBy('id'); + + return collect($cart) + ->map(function ($quantity, $productId) use ($products) { + $product = $products->get((int) $productId); + if (!$product) { + return null; + } + + $qty = max(1, min((int) $quantity, (int) $product->stock)); + if ($qty < 1) { + return null; + } + + return [ + 'product' => $product, + 'quantity' => $qty, + 'subtotal' => (float) $product->price * $qty, + ]; + }) + ->filter() + ->values(); + } +} diff --git a/app/Http/Controllers/Shop/CompareController.php b/app/Http/Controllers/Shop/CompareController.php new file mode 100644 index 0000000..206ce33 --- /dev/null +++ b/app/Http/Controllers/Shop/CompareController.php @@ -0,0 +1,119 @@ +compareIds(); + $position = array_flip($compareIds); + + $products = Product::query() + ->whereIn('id', $compareIds) + ->where('is_active', true) + ->with('category') + ->get() + ->sortBy(fn (Product $product) => $position[$product->id] ?? PHP_INT_MAX) + ->values(); + + $specKeys = $products + ->flatMap(fn (Product $product) => array_keys((array) $product->specs)) + ->unique() + ->values(); + + $specLabels = $specKeys->mapWithKeys(fn (string $key) => [$key => $this->specLabel($key)]); + + return view('shop.compare', [ + 'products' => $products, + 'specKeys' => $specKeys, + 'specLabels' => $specLabels, + ]); + } + + public function toggle(Product $product) + { + $compare = $this->compareIds(); + $exists = in_array($product->id, $compare, true); + + if ($exists) { + $compare = array_values(array_filter($compare, fn (int $id) => $id !== $product->id)); + session()->put('compare', $compare); + + return back()->with('status', "Товар \"{$product->name}\" удален из сравнения."); + } + + if (count($compare) >= 4) { + return back()->with('status', 'Можно сравнить не более 4 товаров одновременно.'); + } + + $compare[] = $product->id; + session()->put('compare', array_values(array_unique($compare))); + + return back()->with('status', "Товар \"{$product->name}\" добавлен в сравнение."); + } + + public function clear() + { + session()->forget('compare'); + + return back()->with('status', 'Список сравнения очищен.'); + } + + private function compareIds(): array + { + return array_values(array_map('intval', (array) session()->get('compare', []))); + } + + private function specLabel(string $key): string + { + return match ($key) { + 'manufacturer' => 'Производитель', + 'socket_type' => 'Тип сокета', + 'cpu_type' => 'Тип процессора', + 'form_factor' => 'Форм-фактор', + 'cpu_socket' => 'Сокет для процессора', + 'condition' => 'Состояние', + 'chipset' => 'Чипсет', + 'memory_type' => 'Тип памяти', + 'pcie_version' => 'Версия PCI Express', + 'wifi_standard' => 'Стандарт Wi-Fi', + 'max_memory' => 'Максимальный объем памяти', + 'cache' => 'Объем кэша', + 'capacity' => 'Объем', + 'gpu' => 'GPU', + 'vram' => 'Объем видеопамяти', + 'vram_type' => 'Тип видеопамяти', + 'kit' => 'Количество модулей', + 'frequency' => 'Частота', + 'power' => 'Мощность', + 'efficiency' => 'Сертификат 80 Plus', + 'size' => 'Типоразмер', + 'gpu_length' => 'Макс. длина видеокарты', + 'intel_socket' => 'Сокет Intel', + 'amd_socket' => 'Сокет AMD', + 'fan_speed' => 'Скорость вращения', + 'fans' => 'Количество вентиляторов', + 'type' => 'Тип', + 'model' => 'Модель', + 'color' => 'Цвет', + 'screen_size' => 'Диагональ экрана', + 'cpu_brand' => 'Производитель процессора', + 'cpu_model' => 'Модель процессора', + 'ram' => 'Оперативная память', + 'storage' => 'Накопитель', + 'panel' => 'Тип матрицы', + 'resolution' => 'Разрешение экрана', + 'refresh_rate' => 'Частота обновления', + 'smart_tv' => 'Smart TV', + 'cores' => 'Количество ядер', + 'gpu_brand' => 'Производитель видеокарты', + 'gpu_model' => 'Модель видеокарты', + default => Str::of($key)->replace('_', ' ')->title()->toString(), + }; + } +} diff --git a/app/Http/Controllers/Shop/ContactController.php b/app/Http/Controllers/Shop/ContactController.php new file mode 100644 index 0000000..36a6a63 --- /dev/null +++ b/app/Http/Controllers/Shop/ContactController.php @@ -0,0 +1,71 @@ +validate([ + 'name' => ['required', 'string', 'max:120'], + 'email' => ['required', 'email:rfc,dns', 'max:190'], + 'message' => ['required', 'string', 'max:2000'], + ]); + + $botToken = trim((string) config('shop.telegram_bot_token')); + $chatId = trim((string) config('shop.telegram_chat_id')); + + if ($botToken === '' || $chatId === '') { + return back() + ->withInput() + ->withErrors(['contact' => 'Не настроена отправка в Telegram. Заполните SHOP_TELEGRAM_BOT_TOKEN и SHOP_TELEGRAM_CHAT_ID.']); + } + + $message = $this->buildTelegramMessage($validated, $request); + $telegramUrl = sprintf('https://api.telegram.org/bot%s/sendMessage', $botToken); + + try { + $response = Http::asForm() + ->timeout(12) + ->post($telegramUrl, [ + 'chat_id' => $chatId, + 'text' => $message, + 'disable_web_page_preview' => true, + ]); + } catch (Throwable) { + return back() + ->withInput() + ->withErrors(['contact' => 'Не удалось отправить заявку в Telegram. Попробуйте еще раз.']); + } + + if (!$response->successful() || $response->json('ok') !== true) { + return back() + ->withInput() + ->withErrors(['contact' => 'Telegram не принял заявку. Проверьте токен бота и chat id.']); + } + + return back()->with('status', 'Заявка отправлена. Мы свяжемся с вами в ближайшее время.'); + } + + private function buildTelegramMessage(array $data, Request $request): string + { + $siteName = (string) config('shop.company_name', config('app.name', 'PC Shop')); + + return implode("\n", [ + 'Новая заявка с формы контактов', + 'Сайт: ' . $siteName, + 'Имя: ' . trim((string) ($data['name'] ?? '')), + 'Email: ' . trim((string) ($data['email'] ?? '')), + 'Сообщение:', + trim((string) ($data['message'] ?? '')), + 'IP: ' . $request->ip(), + 'Дата: ' . now()->format('d.m.Y H:i'), + ]); + } +} diff --git a/app/Http/Controllers/Shop/FavoriteController.php b/app/Http/Controllers/Shop/FavoriteController.php new file mode 100644 index 0000000..34f432c --- /dev/null +++ b/app/Http/Controllers/Shop/FavoriteController.php @@ -0,0 +1,50 @@ +favoriteIds(); + $position = array_flip($favoriteIds); + + $products = Product::query() + ->whereIn('id', $favoriteIds) + ->where('is_active', true) + ->with('category') + ->get() + ->sortBy(fn (Product $product) => $position[$product->id] ?? PHP_INT_MAX) + ->values(); + + return view('shop.favorites', [ + 'products' => $products, + ]); + } + + public function toggle(Product $product) + { + $favorites = $this->favoriteIds(); + $exists = in_array($product->id, $favorites, true); + + if ($exists) { + $favorites = array_values(array_filter($favorites, fn (int $id) => $id !== $product->id)); + session()->put('favorites', $favorites); + + return back()->with('status', "Товар \"{$product->name}\" удален из избранного."); + } + + $favorites[] = $product->id; + session()->put('favorites', array_values(array_unique($favorites))); + + return back()->with('status', "Товар \"{$product->name}\" добавлен в избранное."); + } + + private function favoriteIds(): array + { + return array_values(array_map('intval', (array) session()->get('favorites', []))); + } +} diff --git a/app/Http/Controllers/Shop/OrderController.php b/app/Http/Controllers/Shop/OrderController.php new file mode 100644 index 0000000..6e70030 --- /dev/null +++ b/app/Http/Controllers/Shop/OrderController.php @@ -0,0 +1,23 @@ +user_id !== (int) $request->user()->id) { + abort(403); + } + + $order->load('items.product'); + + return view('shop.order', [ + 'order' => $order, + ]); + } +} diff --git a/app/Http/Controllers/Shop/ProductController.php b/app/Http/Controllers/Shop/ProductController.php new file mode 100644 index 0000000..a09ba9e --- /dev/null +++ b/app/Http/Controllers/Shop/ProductController.php @@ -0,0 +1,101 @@ +load('category'); + $related = Product::query() + ->where('is_active', true) + ->where('stock', '>', 0) + ->where('category_id', $product->category_id) + ->whereKeyNot($product->id) + ->with('category') + ->latest('id') + ->take(4) + ->get(); + + return view('shop.product', [ + 'product' => $product, + 'specLabels' => $this->specLabelsForProduct($product), + 'related' => $related, + ]); + } + + private function specLabelsForCategory(?string $slug): array + { + if (!$slug) { + return []; + } + + return collect(config('product_specs.categories.' . $slug, [])) + ->mapWithKeys(fn (array $definition) => [$definition['key'] => $definition['label']]) + ->all(); + } + + private function specLabelsForProduct(Product $product): array + { + $labels = $this->specLabelsForCategory($product->category?->slug); + $fallback = $this->defaultSpecLabels(); + + foreach (array_keys((array) ($product->specs ?? [])) as $key) { + if (!isset($labels[$key])) { + $labels[$key] = $fallback[$key] ?? str_replace('_', ' ', $key); + } + } + + return $labels; + } + + private function defaultSpecLabels(): array + { + return [ + 'manufacturer' => 'Производитель', + 'socket_type' => 'Тип сокета', + 'cpu_type' => 'Тип процессора', + 'form_factor' => 'Форм-фактор', + 'cpu_socket' => 'Сокет для процессора', + 'condition' => 'Состояние', + 'chipset' => 'Чипсет', + 'memory_type' => 'Тип памяти', + 'pcie_version' => 'Версия PCI Express', + 'wifi_standard' => 'Стандарт Wi-Fi', + 'max_memory' => 'Максимальный объем памяти', + 'cache' => 'Объем кэша', + 'capacity' => 'Объем', + 'gpu' => 'GPU', + 'vram' => 'Объем видеопамяти', + 'vram_type' => 'Тип видеопамяти', + 'kit' => 'Количество модулей', + 'frequency' => 'Частота', + 'power' => 'Мощность', + 'efficiency' => 'Сертификат 80 Plus', + 'size' => 'Типоразмер', + 'gpu_length' => 'Макс. длина видеокарты', + 'intel_socket' => 'Сокет Intel', + 'amd_socket' => 'Сокет AMD', + 'fan_speed' => 'Скорость вращения', + 'fans' => 'Количество вентиляторов', + 'type' => 'Тип', + 'model' => 'Модель', + 'color' => 'Цвет', + 'screen_size' => 'Диагональ экрана', + 'cpu_brand' => 'Производитель процессора', + 'cpu_model' => 'Модель процессора', + 'ram' => 'Оперативная память', + 'storage' => 'Накопитель', + 'panel' => 'Тип матрицы', + 'resolution' => 'Разрешение экрана', + 'refresh_rate' => 'Частота обновления', + 'smart_tv' => 'Smart TV', + 'cores' => 'Количество ядер', + 'gpu_brand' => 'Производитель видеокарты', + 'gpu_model' => 'Модель видеокарты', + ]; + } +} diff --git a/app/Http/Controllers/Shop/ShopController.php b/app/Http/Controllers/Shop/ShopController.php new file mode 100644 index 0000000..2c5cddc --- /dev/null +++ b/app/Http/Controllers/Shop/ShopController.php @@ -0,0 +1,58 @@ +where('is_active', true) + ->orderBy('name') + ->take(12) + ->get(); + + $featured = Product::query() + ->where('is_active', true) + ->where('stock', '>', 0) + ->with('category') + ->orderByDesc('stock') + ->orderByDesc('id') + ->take(12) + ->get(); + + $newProducts = Product::query() + ->where('is_active', true) + ->where('stock', '>', 0) + ->with('category') + ->orderByDesc('created_at') + ->orderByDesc('id') + ->take(12) + ->get(); + + $leftSlides = HomeSlide::query() + ->active() + ->forZone('left') + ->ordered() + ->get(); + + $rightSlides = HomeSlide::query() + ->active() + ->forZone('right') + ->ordered() + ->get(); + + return view('shop.home', [ + 'categories' => $categories, + 'featured' => $featured, + 'newProducts' => $newProducts, + 'leftSlides' => $leftSlides, + 'rightSlides' => $rightSlides, + ]); + } +} diff --git a/app/Http/Controllers/SitemapController.php b/app/Http/Controllers/SitemapController.php new file mode 100644 index 0000000..95c9d34 --- /dev/null +++ b/app/Http/Controllers/SitemapController.php @@ -0,0 +1,91 @@ +where('is_active', true)->max('updated_at'); + $latestCategoryUpdate = Category::query()->where('is_active', true)->max('updated_at'); + $catalogLastmod = collect([$latestProductUpdate, $latestCategoryUpdate]) + ->filter() + ->map(fn ($value) => $value instanceof \Carbon\CarbonInterface ? $value : \Carbon\Carbon::parse((string) $value)) + ->max(); + $catalogLastmodIso = $catalogLastmod?->toAtomString() ?? now()->toAtomString(); + + $urls = collect([ + [ + 'loc' => route('home'), + 'lastmod' => $catalogLastmodIso, + 'changefreq' => 'daily', + 'priority' => '1.0', + ], + [ + 'loc' => route('catalog.index'), + 'lastmod' => $catalogLastmodIso, + 'changefreq' => 'daily', + 'priority' => '0.9', + ], + [ + 'loc' => route('pages.about'), + 'lastmod' => now()->toAtomString(), + 'changefreq' => 'monthly', + 'priority' => '0.5', + ], + [ + 'loc' => route('pages.contacts'), + 'lastmod' => now()->toAtomString(), + 'changefreq' => 'monthly', + 'priority' => '0.5', + ], + [ + 'loc' => route('pages.shipping-payment'), + 'lastmod' => now()->toAtomString(), + 'changefreq' => 'monthly', + 'priority' => '0.5', + ], + ]); + + $categoryUrls = Category::query() + ->where('is_active', true) + ->get() + ->map(function (Category $category) { + return [ + 'loc' => route('catalog.category', $category), + 'lastmod' => $category->updated_at?->toAtomString() ?? now()->toAtomString(), + 'changefreq' => 'weekly', + 'priority' => '0.8', + ]; + }); + + $productUrls = Product::query() + ->where('is_active', true) + ->get() + ->map(function (Product $product) { + return [ + 'loc' => route('products.show', $product), + 'lastmod' => $product->updated_at?->toAtomString() ?? now()->toAtomString(), + 'changefreq' => 'weekly', + 'priority' => '0.7', + ]; + }); + + $allUrls = $this->mergeUrls($urls, $categoryUrls, $productUrls); + + return response() + ->view('seo.sitemap', ['urls' => $allUrls]) + ->header('Content-Type', 'application/xml; charset=UTF-8'); + } + + private function mergeUrls(Collection ...$collections): Collection + { + return collect($collections) + ->flatten(1) + ->values(); + } +} diff --git a/app/Http/Middleware/AdminMiddleware.php b/app/Http/Middleware/AdminMiddleware.php new file mode 100644 index 0000000..84d847e --- /dev/null +++ b/app/Http/Middleware/AdminMiddleware.php @@ -0,0 +1,23 @@ +user()) { + return redirect()->route('admin.login'); + } + + if (!$request->user()->is_admin) { + abort(403); + } + + return $next($request); + } +} diff --git a/app/Models/Category.php b/app/Models/Category.php new file mode 100644 index 0000000..d0ee3de --- /dev/null +++ b/app/Models/Category.php @@ -0,0 +1,35 @@ + 'boolean', + ]; + } + + public function products() + { + return $this->hasMany(Product::class); + } + + public function getRouteKeyName(): string + { + return 'slug'; + } +} diff --git a/app/Models/ChatConversation.php b/app/Models/ChatConversation.php new file mode 100644 index 0000000..5595076 --- /dev/null +++ b/app/Models/ChatConversation.php @@ -0,0 +1,57 @@ + 'datetime', + ]; + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function messages() + { + return $this->hasMany(ChatMessage::class); + } + + public function latestMessage() + { + return $this->hasOne(ChatMessage::class)->latestOfMany(); + } + + public function getDisplayNameAttribute(): string + { + if ($this->user?->name) { + return $this->user->name; + } + + return 'Гость #' . $this->id; + } + + public function isClosed(): bool + { + return $this->status === self::STATUS_CLOSED; + } +} diff --git a/app/Models/ChatMessage.php b/app/Models/ChatMessage.php new file mode 100644 index 0000000..ebd50dd --- /dev/null +++ b/app/Models/ChatMessage.php @@ -0,0 +1,30 @@ + 'boolean', + ]; + } + + public function conversation() + { + return $this->belongsTo(ChatConversation::class, 'chat_conversation_id'); + } +} diff --git a/app/Models/HomeSlide.php b/app/Models/HomeSlide.php new file mode 100644 index 0000000..a4116c9 --- /dev/null +++ b/app/Models/HomeSlide.php @@ -0,0 +1,69 @@ + 'boolean', + 'show_subtitle' => 'boolean', + 'show_button' => 'boolean', + 'sort_order' => 'integer', + 'is_active' => 'boolean', + ]; + } + + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + public function scopeForZone($query, string $zone) + { + return $query->where('zone', $zone); + } + + public function scopeOrdered($query) + { + return $query->orderBy('sort_order')->orderBy('id'); + } + + public function getImageUrlAttribute(): ?string + { + if (!$this->image_path) { + return null; + } + + if (Str::startsWith($this->image_path, ['http://', 'https://', '/'])) { + return $this->image_path; + } + + if (Str::startsWith($this->image_path, 'uploads/')) { + return asset($this->image_path); + } + + return '/storage/' . ltrim($this->image_path, '/'); + } +} diff --git a/app/Models/Order.php b/app/Models/Order.php new file mode 100644 index 0000000..5032dca --- /dev/null +++ b/app/Models/Order.php @@ -0,0 +1,50 @@ + 'decimal:2', + 'items_count' => 'integer', + ]; + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function items() + { + return $this->hasMany(OrderItem::class); + } + + public function getPaymentMethodLabelAttribute(): string + { + return match ($this->payment_method) { + 'card_transfer' => 'Перевод по реквизитам (на карту)', + default => 'Не указан', + }; + } +} diff --git a/app/Models/OrderItem.php b/app/Models/OrderItem.php new file mode 100644 index 0000000..c216066 --- /dev/null +++ b/app/Models/OrderItem.php @@ -0,0 +1,39 @@ + 'decimal:2', + 'subtotal' => 'decimal:2', + 'quantity' => 'integer', + ]; + } + + public function order() + { + return $this->belongsTo(Order::class); + } + + public function product() + { + return $this->belongsTo(Product::class); + } +} diff --git a/app/Models/Product.php b/app/Models/Product.php new file mode 100644 index 0000000..3d3f6f6 --- /dev/null +++ b/app/Models/Product.php @@ -0,0 +1,89 @@ + 'decimal:2', + 'old_price' => 'decimal:2', + 'stock' => 'integer', + 'gallery_paths' => 'array', + 'specs' => 'array', + 'is_active' => 'boolean', + ]; + } + + public function category() + { + return $this->belongsTo(Category::class); + } + + public function getRouteKeyName(): string + { + return 'slug'; + } + + public function getImageUrlAttribute(): ?string + { + return $this->resolveImagePath($this->image_path); + } + + public function getGalleryUrlsAttribute(): array + { + $paths = collect((array) ($this->gallery_paths ?? [])) + ->prepend($this->image_path) + ->filter(fn ($path) => is_string($path) && trim($path) !== '') + ->map(fn (string $path) => trim($path)) + ->unique() + ->values(); + + return $paths + ->map(fn (string $path) => $this->resolveImagePath($path)) + ->filter() + ->values() + ->all(); + } + + private function resolveImagePath(?string $path): ?string + { + $path = trim((string) $path); + if ($path === '') { + return null; + } + + if (Str::startsWith($path, ['http://', 'https://', '/'])) { + return $path; + } + + if (Str::startsWith($path, 'uploads/')) { + return asset($path); + } + + return '/storage/' . ltrim($path, '/'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..ee12502 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,60 @@ + */ + use HasFactory, Notifiable; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'email', + 'password', + 'is_admin', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'is_admin' => 'boolean', + ]; + } + + public function orders() + { + return $this->hasMany(Order::class); + } + + public function chatConversations() + { + return $this->hasMany(ChatConversation::class); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..2d71ef6 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,68 @@ +input('email')); + + return Limit::perMinute(10)->by($email . '|' . $request->ip()); + }); + + RateLimiter::for('admin-login', function (Request $request) { + $email = Str::lower((string) $request->input('email')); + + return Limit::perMinute(5)->by($email . '|' . $request->ip()); + }); + + RateLimiter::for('chat-read', function (Request $request) { + $identity = $request->user()?->id + ? 'user:' . $request->user()->id + : 'session:' . $request->session()->getId(); + + return Limit::perMinute(180)->by($identity . '|' . $request->ip()); + }); + + RateLimiter::for('chat-send', function (Request $request) { + $identity = $request->user()?->id + ? 'user:' . $request->user()->id + : 'session:' . $request->session()->getId(); + + return Limit::perMinute(20)->by($identity . '|' . $request->ip()); + }); + + RateLimiter::for('contact-send', function (Request $request) { + $email = Str::lower(trim((string) $request->input('email', ''))); + + return Limit::perMinute(6)->by(($email !== '' ? $email : 'guest') . '|' . $request->ip()); + }); + + RateLimiter::for('admin-chat-read', function (Request $request) { + return Limit::perMinute(240)->by('admin:' . ($request->user()?->id ?? 'guest') . '|' . $request->ip()); + }); + + RateLimiter::for('admin-chat-send', function (Request $request) { + return Limit::perMinute(60)->by('admin:' . ($request->user()?->id ?? 'guest') . '|' . $request->ip()); + }); + } +} diff --git a/artisan b/artisan new file mode 100755 index 0000000..c35e31d --- /dev/null +++ b/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..c183276 --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,18 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + // + }) + ->withExceptions(function (Exceptions $exceptions): void { + // + })->create(); diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 0000000..38b258d --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,5 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.52.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "d5511fa74f4608dbb99864198b1954042aa8d5a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/d5511fa74f4608dbb99864198b1954042aa8d5a7", + "reference": "d5511fa74f4608dbb99864198b1954042aa8d5a7", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.7", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.9.0", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0|^1.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0|^1.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-02-17T17:07:04+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/ed8c466571b37e977532fb2fd3c272c784d7050d", + "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0|^8.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.13" + }, + "time": "2026-02-06T12:17:10+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.9", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "8f631589ab07b7b52fead814965f5a800459cb3e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/8f631589ab07b7b52fead814965f5a800459cb3e", + "reference": "8f631589ab07b7b52fead814965f5a800459cb3e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-02-03T06:55:34+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.11.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/c9f80cc835649b5c1842898fb043f8cc098dd741", + "reference": "c9f80cc835649b5c1842898fb043f8cc098dd741", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.11.1" + }, + "time": "2026-02-06T14:12:35+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/4efa10c1e56488e658d10adf7b7b7dcd19940bfb", + "reference": "4efa10c1e56488e658d10adf7b7b7dcd19940bfb", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2025-11-26T21:48:24+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/1717e0b3642b0df65ecb0cc89cdd99fa840672ff", + "reference": "1717e0b3642b0df65ecb0cc89cdd99fa840672ff", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.31.0" + }, + "time": "2026-01-23T15:38:47+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", + "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-01-14T17:24:56+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-01-15T06:54:53+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.11.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "f438fcc98f92babee98381d399c65336f3a3827f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f", + "reference": "f438fcc98f92babee98381d399c65336f3a3827f", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:26:29+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.4", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "086497a2f34b82fede9b5a41cc8e131d087cd8f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/086497a2f34b82fede9b5a41cc8e131d087cd8f7", + "reference": "086497a2f34b82fede9b5a41cc8e131d087cd8f7", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/tester": "^2.6", + "phpstan/phpstan": "^2.0@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.4" + }, + "time": "2026-02-08T02:54:00+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.3", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/bb3ea637e3d131d72acc033cfc2746ee893349fe", + "reference": "bb3ea637e3d131d72acc033cfc2746ee893349fe", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.3" + }, + "time": "2026-02-13T03:05:33+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.20", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/19678eb6b952a03b8a1d96ecee9edba518bb0373", + "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.20" + }, + "time": "2026-02-11T15:05:28+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "reference": "832119f9b8dbc6c8e6f65f30c5969eca1e88764f", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v8.0.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-12T15:46:48+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "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": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.4" + }, + "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-01-13T11:36:38+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/6225bd458c53ecdee056214cb4a2ffaf58bd592b", + "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v8.0.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-10-30T14:17:19+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "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 tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" + }, + "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-01-20T16:42:42+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "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 tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" + }, + "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-01-05T11:45:55+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "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": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.4.5" + }, + "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-01-26T15:07:59+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "446d0db2b1f21575f1284b74533e425096abdfb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/446d0db2b1f21575f1284b74533e425096abdfb6", + "reference": "446d0db2b1f21575f1284b74533e425096abdfb6", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "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": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.4.5" + }, + "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-01-27T16:16:02+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/229eda477017f92bd2ce7615d06222ec0c19e82a", + "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^7.3|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/flex": "<2.10", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^7.1|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/serializer": "^7.1|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "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 structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.4.5" + }, + "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-01-28T10:33:42+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "7b750074c40c694ceb34cb926d6dffee231c5cd6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/7b750074c40c694ceb34cb926d6dffee231c5cd6", + "reference": "7b750074c40c694ceb34cb926d6dffee231c5cd6", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/mime": "^7.2|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "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": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.4.4" + }, + "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-01-08T08:25:11+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/b18c7e6e9eee1e19958138df10412f3c4c316148", + "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "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": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.4.5" + }, + "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-01-27T08:59:58+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.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": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.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-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.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": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.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": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.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": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.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-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.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-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.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-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.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-06-23T16:12:55+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.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": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "608476f4604102976d687c483ac63a79ba18cc97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "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": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.5" + }, + "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-01-26T15:07:59+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "0798827fe2c79caeed41d70b680c2c3507d10147" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/0798827fe2c79caeed41d70b680c2c3507d10147", + "reference": "0798827fe2c79caeed41d70b680c2c3507d10147", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "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": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.4.4" + }, + "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-01-12T12:19:02+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "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-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "758b372d6882506821ed666032e43020c4f57194" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", + "reference": "758b372d6882506821ed666032e43020c4f57194", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.4" + }, + "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-01-12T12:37:40+00:00" + }, + { + "name": "symfony/translation", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "db70c8ce7db74fd2da7b1d268db46b2a8ce32c10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/db70c8ce7db74fd2da7b1d268db46b2a8ce32c10", + "reference": "db70c8ce7db74fd2da7b1d268db46b2a8ce32c10", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/http-client-contracts": "<2.5", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "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 tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v8.0.4" + }, + "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-01-13T13:06:50+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/65a8bc82080447fae78373aa10f8d13b38338977", + "reference": "65a8bc82080447fae78373aa10f8d13b38338977", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.1" + }, + "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-07-15T13:41:35+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.4.4" + }, + "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-01-03T23:30:35+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "0e4769b46a0c3c62390d124635ce59f66874b282" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e4769b46a0c3c62390d124635ce59f66874b282", + "reference": "0e4769b46a0c3c62390d124635ce59f66874b282", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.4.4" + }, + "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-01-01T22:13:48+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "reference": "aa71a01c309e7f66bc2ec4fb1a59291b82eb4abf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", + "pestphp/pest": "^2.20|^3.0|^4.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-02-09T13:44:54+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.27.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5", + "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.93.1", + "illuminate/view": "^12.51.0", + "larastan/larastan": "^3.9.2", + "laravel-zero/framework": "^12.0.5", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.3.3", + "pestphp/pest": "^3.8.5" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-02-10T20:00:20+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.53.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/e340eaa2bea9b99192570c48ed837155dbf24fbb", + "reference": "e340eaa2bea9b99192570c48ed837155dbf24fbb", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0|^13.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0|^8.0", + "symfony/yaml": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0|^11.0", + "phpstan/phpstan": "^2.0" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2026-02-06T12:16:02+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.1", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.2", + "laravel/framework": "^11.48.0 || ^12.52.0", + "laravel/pint": "^1.27.1", + "orchestra/testbench-core": "^9.12.0 || ^10.9.0", + "pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-02-17T17:33:08+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v8.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14", + "reference": "7a1a90ba1df6e821a6b53c4cabdc32a56cabfb14", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "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": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v8.0.1" + }, + "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-12-04T18:17:06+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..423eed5 --- /dev/null +++ b/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..7d1eb0d --- /dev/null +++ b/config/auth.php @@ -0,0 +1,115 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', App\Models\User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..b32aead --- /dev/null +++ b/config/cache.php @@ -0,0 +1,117 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", + | "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..df933e7 --- /dev/null +++ b/config/database.php @@ -0,0 +1,183 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..37d8fca --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..9e998a4 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..522b284 --- /dev/null +++ b/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + +]; diff --git a/config/product_specs.php b/config/product_specs.php new file mode 100644 index 0000000..1c91696 --- /dev/null +++ b/config/product_specs.php @@ -0,0 +1,90 @@ + [ + 'processors' => [ + ['key' => 'manufacturer', 'label' => 'Производитель'], + ['key' => 'socket_type', 'label' => 'Тип сокета'], + ['key' => 'cpu_type', 'label' => 'Тип процессора'], + ['key' => 'cores', 'label' => 'Количество ядер', 'options' => ['6', '8', '10', '12', '16']], + ['key' => 'cache', 'label' => 'Объем кэша', 'options' => ['16 МБ', '32 МБ', '36 МБ', '64 МБ']], + ], + 'motherboards' => [ + ['key' => 'manufacturer', 'label' => 'Производитель'], + ['key' => 'form_factor', 'label' => 'Форм-фактор'], + ['key' => 'cpu_socket', 'label' => 'Сокет для процессора'], + ['key' => 'chipset', 'label' => 'Чипсет'], + ['key' => 'memory_type', 'label' => 'Тип поддерживаемой памяти'], + ['key' => 'max_memory', 'label' => 'Максимальный объем памяти (ГБ)', 'filter' => 'range'], + ['key' => 'pcie_version', 'label' => 'Версия PCI Express'], + ['key' => 'wifi_standard', 'label' => 'Стандарт Wi-Fi'], + ], + 'hard-drives' => [ + ['key' => 'manufacturer', 'label' => 'Производитель'], + ['key' => 'capacity', 'label' => 'Объем накопителя'], + ['key' => 'form_factor', 'label' => 'Форм-фактор'], + ], + 'graphics-cards' => [ + ['key' => 'manufacturer', 'label' => 'Производитель'], + ['key' => 'gpu', 'label' => 'GPU', 'options' => ['GeForce RTX 4060', 'GeForce RTX 4070', 'Radeon RX 7800 XT']], + ['key' => 'vram', 'label' => 'Объем видеопамяти'], + ['key' => 'vram_type', 'label' => 'Тип видеопамяти'], + ], + 'memory' => [ + ['key' => 'manufacturer', 'label' => 'Производитель'], + ['key' => 'memory_type', 'label' => 'Тип памяти'], + ['key' => 'form_factor', 'label' => 'Форм-фактор'], + ['key' => 'kit', 'label' => 'Количество модулей'], + ['key' => 'capacity', 'label' => 'Объем памяти'], + ['key' => 'frequency', 'label' => 'Частота памяти'], + ], + 'psu' => [ + ['key' => 'manufacturer', 'label' => 'Производитель'], + ['key' => 'power', 'label' => 'Мощность'], + ['key' => 'efficiency', 'label' => 'Сертификат 80 Plus'], + ], + 'cases' => [ + ['key' => 'manufacturer', 'label' => 'Производитель'], + ['key' => 'size', 'label' => 'Типоразмер'], + ['key' => 'gpu_length', 'label' => 'Макс. длина видеокарты'], + ], + 'cooling' => [ + ['key' => 'manufacturer', 'label' => 'Производитель'], + ['key' => 'intel_socket', 'label' => 'Сокет Intel'], + ['key' => 'amd_socket', 'label' => 'Сокет AMD'], + ['key' => 'fan_speed', 'label' => 'Скорость вращения'], + ['key' => 'fans', 'label' => 'Количество вентиляторов'], + ], + 'laptops' => [ + ['key' => 'manufacturer', 'label' => 'Производитель'], + ['key' => 'type', 'label' => 'Тип'], + ['key' => 'screen_size', 'label' => 'Диагональ экрана'], + ['key' => 'cpu_brand', 'label' => 'Производитель процессора'], + ['key' => 'cpu_model', 'label' => 'Модель процессора'], + ['key' => 'ram', 'label' => 'Объем оперативной памяти'], + ['key' => 'storage', 'label' => 'Объем накопителя'], + ['key' => 'panel', 'label' => 'Тип матрицы'], + ['key' => 'resolution', 'label' => 'Разрешение экрана'], + ['key' => 'cores', 'label' => 'Количество ядер'], + ['key' => 'gpu_brand', 'label' => 'Производитель видеокарты'], + ['key' => 'gpu_model', 'label' => 'Модель видеокарты'], + ], + 'televizory' => [ + ['key' => 'manufacturer', 'label' => 'Производитель'], + ['key' => 'screen_size', 'label' => 'Диагональ экрана'], + ['key' => 'resolution', 'label' => 'Разрешение экрана'], + ['key' => 'panel', 'label' => 'Тип матрицы'], + ['key' => 'refresh_rate', 'label' => 'Частота обновления'], + ['key' => 'smart_tv', 'label' => 'Smart TV'], + ], + 'apple' => [ + ['key' => 'manufacturer', 'label' => 'Производитель'], + ['key' => 'type', 'label' => 'Тип устройства'], + ['key' => 'model', 'label' => 'Модель'], + ['key' => 'color', 'label' => 'Цвет', 'options' => ['Черный', 'Белый', 'Синий', 'Серебристый', 'Золотой', 'Фиолетовый']], + ['key' => 'screen_size', 'label' => 'Диагональ экрана'], + ['key' => 'storage', 'label' => 'Объем памяти'], + ['key' => 'condition', 'label' => 'Состояние', 'options' => ['Новое', 'Б/у']], + ], + ], +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/seo.php b/config/seo.php new file mode 100644 index 0000000..a4b9b25 --- /dev/null +++ b/config/seo.php @@ -0,0 +1,16 @@ + env('SEO_SITE_NAME', env('APP_NAME', 'PC Shop')), + 'default_title' => env('SEO_DEFAULT_TITLE', 'Интернет-магазин компьютерных комплектующих'), + 'default_description' => env( + 'SEO_DEFAULT_DESCRIPTION', + 'Процессоры, видеокарты, материнские платы, ноутбуки и периферия с доставкой по стране.' + ), + 'default_keywords' => env( + 'SEO_DEFAULT_KEYWORDS', + 'комплектующие для пк, процессоры, видеокарты, материнские платы, ноутбуки, интернет-магазин' + ), + 'default_image' => env('SEO_DEFAULT_IMAGE', '/favicon.ico'), +]; + diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..6a90eb8 --- /dev/null +++ b/config/services.php @@ -0,0 +1,38 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..5b541b7 --- /dev/null +++ b/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/config/shop.php b/config/shop.php new file mode 100644 index 0000000..f652b8e --- /dev/null +++ b/config/shop.php @@ -0,0 +1,21 @@ + env('SHOP_COMPANY_NAME', env('APP_NAME', 'PC Shop')), + 'company_description' => env( + 'SHOP_COMPANY_DESCRIPTION', + 'Интернет-магазин компьютерных комплектующих, ноутбуков и периферии. Подбор, сравнение и заказ в одном месте.' + ), + 'contact_phone' => env('SHOP_CONTACT_PHONE', '+7 900 000 00 00'), + 'contact_email' => env('SHOP_CONTACT_EMAIL', 'support@pcshop.test'), + 'contact_telegram' => env('SHOP_CONTACT_TELEGRAM', '@pcshop_support'), + 'telegram_bot_token' => env('SHOP_TELEGRAM_BOT_TOKEN', ''), + 'telegram_chat_id' => env('SHOP_TELEGRAM_CHAT_ID', ''), + 'contact_address' => env('SHOP_CONTACT_ADDRESS', 'ул. Технопарк, 24, Техноград'), + 'contact_hours' => env('SHOP_CONTACT_HOURS', 'Пн-Вс: 10:00-20:00'), + 'currency_code' => env('SHOP_CURRENCY_CODE', 'RUB'), + 'currency_symbol' => env('SHOP_CURRENCY_SYMBOL', '₽'), + 'payment_bank' => env('SHOP_PAYMENT_BANK', 'Сбербанк'), + 'payment_card_holder' => env('SHOP_PAYMENT_CARD_HOLDER', 'ИВАНОВА ИВАННА ИВАНОВНА'), + 'payment_card_number' => env('SHOP_PAYMENT_CARD_NUMBER', '0000 0000 0000 0000'), +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..584104c --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,44 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..05fb5d9 --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..ed758bd --- /dev/null +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..425e705 --- /dev/null +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/database/migrations/2026_02_19_000001_add_is_admin_to_users_table.php b/database/migrations/2026_02_19_000001_add_is_admin_to_users_table.php new file mode 100644 index 0000000..1d1addd --- /dev/null +++ b/database/migrations/2026_02_19_000001_add_is_admin_to_users_table.php @@ -0,0 +1,22 @@ +boolean('is_admin')->default(false)->after('password'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('is_admin'); + }); + } +}; diff --git a/database/migrations/2026_02_19_000002_create_categories_table.php b/database/migrations/2026_02_19_000002_create_categories_table.php new file mode 100644 index 0000000..f8f465a --- /dev/null +++ b/database/migrations/2026_02_19_000002_create_categories_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('categories'); + } +}; diff --git a/database/migrations/2026_02_19_000003_create_products_table.php b/database/migrations/2026_02_19_000003_create_products_table.php new file mode 100644 index 0000000..d20bbaa --- /dev/null +++ b/database/migrations/2026_02_19_000003_create_products_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('category_id')->constrained()->cascadeOnDelete(); + $table->string('name'); + $table->string('slug')->unique(); + $table->string('sku')->nullable(); + $table->decimal('price', 10, 2); + $table->decimal('old_price', 10, 2)->nullable(); + $table->unsignedInteger('stock')->default(0); + $table->text('short_description')->nullable(); + $table->longText('description')->nullable(); + $table->json('specs')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('products'); + } +}; diff --git a/database/migrations/2026_02_19_000004_create_orders_table.php b/database/migrations/2026_02_19_000004_create_orders_table.php new file mode 100644 index 0000000..da4f2eb --- /dev/null +++ b/database/migrations/2026_02_19_000004_create_orders_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->string('status')->default('new'); + $table->decimal('total', 10, 2)->default(0); + $table->unsignedInteger('items_count')->default(0); + $table->string('customer_name'); + $table->string('email'); + $table->string('phone')->nullable(); + $table->string('address')->nullable(); + $table->text('comment')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('orders'); + } +}; diff --git a/database/migrations/2026_02_19_000005_create_order_items_table.php b/database/migrations/2026_02_19_000005_create_order_items_table.php new file mode 100644 index 0000000..4f7bdcc --- /dev/null +++ b/database/migrations/2026_02_19_000005_create_order_items_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('order_id')->constrained()->cascadeOnDelete(); + $table->foreignId('product_id')->nullable()->constrained()->nullOnDelete(); + $table->string('name'); + $table->decimal('price', 10, 2); + $table->unsignedInteger('quantity'); + $table->decimal('subtotal', 10, 2); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('order_items'); + } +}; diff --git a/database/migrations/2026_02_21_000006_add_payment_method_to_orders_table.php b/database/migrations/2026_02_21_000006_add_payment_method_to_orders_table.php new file mode 100644 index 0000000..c547cc1 --- /dev/null +++ b/database/migrations/2026_02_21_000006_add_payment_method_to_orders_table.php @@ -0,0 +1,22 @@ +string('payment_method')->default('card_transfer')->after('status'); + }); + } + + public function down(): void + { + Schema::table('orders', function (Blueprint $table) { + $table->dropColumn('payment_method'); + }); + } +}; diff --git a/database/migrations/2026_02_21_000008_create_chat_tables.php b/database/migrations/2026_02_21_000008_create_chat_tables.php new file mode 100644 index 0000000..b1574c8 --- /dev/null +++ b/database/migrations/2026_02_21_000008_create_chat_tables.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->string('visitor_token')->unique(); + $table->string('status')->default('open'); + $table->timestamp('last_message_at')->nullable()->index(); + $table->timestamps(); + }); + + Schema::create('chat_messages', function (Blueprint $table) { + $table->id(); + $table->foreignId('chat_conversation_id')->constrained('chat_conversations')->cascadeOnDelete(); + $table->string('sender', 20); + $table->text('body'); + $table->boolean('is_read')->default(false); + $table->timestamps(); + + $table->index(['chat_conversation_id', 'created_at']); + $table->index(['chat_conversation_id', 'sender', 'is_read']); + }); + } + + public function down(): void + { + Schema::dropIfExists('chat_messages'); + Schema::dropIfExists('chat_conversations'); + } +}; diff --git a/database/migrations/2026_02_23_000009_create_home_slides_table.php b/database/migrations/2026_02_23_000009_create_home_slides_table.php new file mode 100644 index 0000000..6a3eb1c --- /dev/null +++ b/database/migrations/2026_02_23_000009_create_home_slides_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('zone', 10)->index(); + $table->string('title', 160)->nullable(); + $table->text('subtitle')->nullable(); + $table->string('button_text', 60)->nullable(); + $table->string('button_url')->nullable(); + $table->string('image_path'); + $table->unsignedInteger('sort_order')->default(100)->index(); + $table->boolean('is_active')->default(true)->index(); + $table->timestamps(); + + $table->index(['zone', 'is_active', 'sort_order']); + }); + } + + public function down(): void + { + Schema::dropIfExists('home_slides'); + } +}; diff --git a/database/migrations/2026_02_23_000010_create_sessions_table.php b/database/migrations/2026_02_23_000010_create_sessions_table.php new file mode 100644 index 0000000..faa998f --- /dev/null +++ b/database/migrations/2026_02_23_000010_create_sessions_table.php @@ -0,0 +1,31 @@ +string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + public function down(): void + { + if (Schema::hasTable('sessions')) { + Schema::drop('sessions'); + } + } +}; diff --git a/database/migrations/2026_02_23_000011_add_image_path_to_products_table.php b/database/migrations/2026_02_23_000011_add_image_path_to_products_table.php new file mode 100644 index 0000000..5a2e7ce --- /dev/null +++ b/database/migrations/2026_02_23_000011_add_image_path_to_products_table.php @@ -0,0 +1,26 @@ +string('image_path')->nullable()->after('description'); + } + }); + } + + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + if (Schema::hasColumn('products', 'image_path')) { + $table->dropColumn('image_path'); + } + }); + } +}; diff --git a/database/migrations/2026_02_23_000012_add_visibility_flags_to_home_slides_table.php b/database/migrations/2026_02_23_000012_add_visibility_flags_to_home_slides_table.php new file mode 100644 index 0000000..b69b723 --- /dev/null +++ b/database/migrations/2026_02_23_000012_add_visibility_flags_to_home_slides_table.php @@ -0,0 +1,42 @@ +boolean('show_title')->default(true)->after('title'); + } + + if (!Schema::hasColumn('home_slides', 'show_subtitle')) { + $table->boolean('show_subtitle')->default(true)->after('subtitle'); + } + + if (!Schema::hasColumn('home_slides', 'show_button')) { + $table->boolean('show_button')->default(true)->after('button_url'); + } + }); + } + + public function down(): void + { + Schema::table('home_slides', function (Blueprint $table) { + if (Schema::hasColumn('home_slides', 'show_title')) { + $table->dropColumn('show_title'); + } + + if (Schema::hasColumn('home_slides', 'show_subtitle')) { + $table->dropColumn('show_subtitle'); + } + + if (Schema::hasColumn('home_slides', 'show_button')) { + $table->dropColumn('show_button'); + } + }); + } +}; diff --git a/database/migrations/2026_02_25_000013_add_gallery_paths_to_products_table.php b/database/migrations/2026_02_25_000013_add_gallery_paths_to_products_table.php new file mode 100644 index 0000000..3e37383 --- /dev/null +++ b/database/migrations/2026_02_25_000013_add_gallery_paths_to_products_table.php @@ -0,0 +1,26 @@ +json('gallery_paths')->nullable()->after('image_path'); + } + }); + } + + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + if (Schema::hasColumn('products', 'gallery_paths')) { + $table->dropColumn('gallery_paths'); + } + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..f456ebf --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,27 @@ +create(); + + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + + $this->call(ShopCatalogSeeder::class); + } +} diff --git a/database/seeders/ShopCatalogSeeder.php b/database/seeders/ShopCatalogSeeder.php new file mode 100644 index 0000000..ea7470c --- /dev/null +++ b/database/seeders/ShopCatalogSeeder.php @@ -0,0 +1,545 @@ + 'Процессоры', + 'slug' => 'processors', + 'description' => 'Процессоры для игровых и рабочих сборок.', + 'products' => [ + [ + 'name' => 'AMD Ryzen 7 7700X', + 'price' => 33990, + 'short_description' => '8 ядер, 16 потоков, до 5.4 ГГц.', + 'specs' => [ + 'manufacturer' => 'AMD', + 'socket_type' => 'AM5', + 'cpu_type' => 'Ryzen 7', + 'cores' => '8', + 'cache' => '32 МБ', + ], + ], + [ + 'name' => 'Intel Core i7-13700K', + 'price' => 38990, + 'short_description' => '16 ядер, для мощных систем.', + 'specs' => [ + 'manufacturer' => 'Intel', + 'socket_type' => 'LGA1700', + 'cpu_type' => 'Core i7', + 'cores' => '16', + 'cache' => '30 МБ', + ], + ], + [ + 'name' => 'AMD Ryzen 5 7600', + 'price' => 22990, + 'short_description' => 'Оптимален для игровых сборок.', + 'specs' => [ + 'manufacturer' => 'AMD', + 'socket_type' => 'AM5', + 'cpu_type' => 'Ryzen 5', + 'cores' => '6', + 'cache' => '32 МБ', + ], + ], + ], + ], + [ + 'name' => 'Материнские платы', + 'slug' => 'motherboards', + 'description' => 'Надежные платы для любых конфигураций.', + 'products' => [ + [ + 'name' => 'ASUS TUF B650-PLUS', + 'price' => 21990, + 'short_description' => 'AM5, усиленное питание, PCIe 4.0.', + 'specs' => [ + 'manufacturer' => 'ASUS', + 'form_factor' => 'ATX', + 'cpu_socket' => 'AM5', + 'chipset' => 'B650', + 'memory_type' => 'DDR5', + 'max_memory' => '192 ГБ', + 'pcie_version' => 'PCIe 4.0', + 'wifi_standard' => 'Wi-Fi 6', + ], + ], + [ + 'name' => 'MSI MAG Z790 Tomahawk', + 'price' => 28990, + 'short_description' => 'LGA1700, мощное охлаждение VRM.', + 'specs' => [ + 'manufacturer' => 'MSI', + 'form_factor' => 'ATX', + 'cpu_socket' => 'LGA1700', + 'chipset' => 'Z790', + 'memory_type' => 'DDR5', + 'max_memory' => '256 ГБ', + 'pcie_version' => 'PCIe 5.0', + 'wifi_standard' => 'Wi-Fi 6E', + ], + ], + [ + 'name' => 'Gigabyte B760M DS3H', + 'price' => 13990, + 'short_description' => 'Компактная mATX для LGA1700.', + 'specs' => [ + 'manufacturer' => 'Gigabyte', + 'form_factor' => 'mATX', + 'cpu_socket' => 'LGA1700', + 'chipset' => 'B760', + 'memory_type' => 'DDR4', + 'max_memory' => '128 ГБ', + 'pcie_version' => 'PCIe 4.0', + 'wifi_standard' => 'Нет', + ], + ], + ], + ], + [ + 'name' => 'Жесткие диски', + 'slug' => 'hard-drives', + 'description' => 'SSD и HDD для хранения данных.', + 'products' => [ + [ + 'name' => 'Samsung 990 PRO 1TB', + 'price' => 10990, + 'short_description' => 'NVMe SSD с высокой скоростью.', + 'specs' => [ + 'manufacturer' => 'Samsung', + 'capacity' => '1 ТБ', + 'form_factor' => 'M.2 2280', + ], + ], + [ + 'name' => 'WD Blue 2TB', + 'price' => 6990, + 'short_description' => 'Надежный HDD для хранения.', + 'specs' => [ + 'manufacturer' => 'Western Digital', + 'capacity' => '2 ТБ', + 'form_factor' => '3.5\"', + ], + ], + [ + 'name' => 'Kingston NV2 500GB', + 'price' => 3990, + 'short_description' => 'Доступный NVMe накопитель.', + 'specs' => [ + 'manufacturer' => 'Kingston', + 'capacity' => '500 ГБ', + 'form_factor' => 'M.2 2280', + ], + ], + ], + ], + [ + 'name' => 'Видеокарты', + 'slug' => 'graphics-cards', + 'description' => 'GPU для игр и работы с графикой.', + 'products' => [ + [ + 'name' => 'NVIDIA GeForce RTX 4070', + 'price' => 69990, + 'short_description' => '12 ГБ GDDR6X, DLSS 3.', + 'specs' => [ + 'manufacturer' => 'NVIDIA', + 'gpu' => 'GeForce RTX 4070', + 'vram' => '12 ГБ', + 'vram_type' => 'GDDR6X', + ], + ], + [ + 'name' => 'AMD Radeon RX 7800 XT', + 'price' => 64990, + 'short_description' => '16 ГБ GDDR6, отличная производительность.', + 'specs' => [ + 'manufacturer' => 'AMD', + 'gpu' => 'Radeon RX 7800 XT', + 'vram' => '16 ГБ', + 'vram_type' => 'GDDR6', + ], + ], + [ + 'name' => 'NVIDIA GeForce RTX 4060', + 'price' => 39990, + 'short_description' => '8 ГБ GDDR6 для 1080p.', + 'specs' => [ + 'manufacturer' => 'NVIDIA', + 'gpu' => 'GeForce RTX 4060', + 'vram' => '8 ГБ', + 'vram_type' => 'GDDR6', + ], + ], + ], + ], + [ + 'name' => 'Оперативная память', + 'slug' => 'memory', + 'description' => 'DDR4/DDR5 комплекты для скорости.', + 'products' => [ + [ + 'name' => 'Corsair Vengeance 32GB', + 'price' => 10990, + 'short_description' => 'DDR5 6000 МГц, 2x16 ГБ.', + 'specs' => [ + 'manufacturer' => 'Corsair', + 'memory_type' => 'DDR5', + 'form_factor' => 'DIMM', + 'kit' => '2 модуля', + 'capacity' => '32 ГБ', + 'frequency' => '6000 МГц', + ], + ], + [ + 'name' => 'Kingston Fury Beast 16GB', + 'price' => 4990, + 'short_description' => 'DDR4 3200 МГц, 2x8 ГБ.', + 'specs' => [ + 'manufacturer' => 'Kingston', + 'memory_type' => 'DDR4', + 'form_factor' => 'DIMM', + 'kit' => '2 модуля', + 'capacity' => '16 ГБ', + 'frequency' => '3200 МГц', + ], + ], + [ + 'name' => 'G.Skill Ripjaws 32GB', + 'price' => 8990, + 'short_description' => 'DDR4 3600 МГц, 2x16 ГБ.', + 'specs' => [ + 'manufacturer' => 'G.Skill', + 'memory_type' => 'DDR4', + 'form_factor' => 'DIMM', + 'kit' => '2 модуля', + 'capacity' => '32 ГБ', + 'frequency' => '3600 МГц', + ], + ], + ], + ], + [ + 'name' => 'Блоки питания', + 'slug' => 'psu', + 'description' => 'Надежные блоки питания.', + 'products' => [ + [ + 'name' => 'Seasonic Focus GX-750', + 'price' => 11990, + 'short_description' => '750 Вт, 80 Plus Gold.', + 'specs' => [ + 'manufacturer' => 'Seasonic', + 'power' => '750 Вт', + 'efficiency' => '80 Plus Gold', + ], + ], + [ + 'name' => 'Corsair RM850e', + 'price' => 13990, + 'short_description' => '850 Вт, тихий режим работы.', + 'specs' => [ + 'manufacturer' => 'Corsair', + 'power' => '850 Вт', + 'efficiency' => '80 Plus Gold', + ], + ], + [ + 'name' => 'be quiet! Pure Power 12 M 650', + 'price' => 9990, + 'short_description' => '650 Вт, стабильная линия питания.', + 'specs' => [ + 'manufacturer' => 'be quiet!', + 'power' => '650 Вт', + 'efficiency' => '80 Plus Gold', + ], + ], + ], + ], + [ + 'name' => 'Корпуса', + 'slug' => 'cases', + 'description' => 'Корпуса с отличным airflow.', + 'products' => [ + [ + 'name' => 'Lian Li Lancool 216', + 'price' => 8990, + 'short_description' => 'ATX, сетчатый фронт.', + 'specs' => [ + 'manufacturer' => 'Lian Li', + 'size' => 'ATX Mid Tower', + 'gpu_length' => '392 мм', + ], + ], + [ + 'name' => 'NZXT H5 Flow', + 'price' => 9490, + 'short_description' => 'Компактный и продуваемый.', + 'specs' => [ + 'manufacturer' => 'NZXT', + 'size' => 'ATX Mid Tower', + 'gpu_length' => '365 мм', + ], + ], + [ + 'name' => 'Cooler Master NR200P', + 'price' => 10990, + 'short_description' => 'Мини‑корпус для ITX.', + 'specs' => [ + 'manufacturer' => 'Cooler Master', + 'size' => 'Mini ITX', + 'gpu_length' => '330 мм', + ], + ], + ], + ], + [ + 'name' => 'Системы охлаждения', + 'slug' => 'cooling', + 'description' => 'Охлаждение для процессора.', + 'products' => [ + [ + 'name' => 'Noctua NH-D15', + 'price' => 9990, + 'short_description' => 'Топовый воздушный кулер.', + 'specs' => [ + 'manufacturer' => 'Noctua', + 'intel_socket' => 'LGA1700', + 'amd_socket' => 'AM5', + 'fan_speed' => '1500 об/мин', + 'fans' => '2 вентилятора', + ], + ], + [ + 'name' => 'be quiet! Dark Rock 4', + 'price' => 7490, + 'short_description' => 'Тихая работа и охлаждение.', + 'specs' => [ + 'manufacturer' => 'be quiet!', + 'intel_socket' => 'LGA1700', + 'amd_socket' => 'AM4/AM5', + 'fan_speed' => '1400 об/мин', + 'fans' => '1 вентилятор', + ], + ], + [ + 'name' => 'DeepCool LS520', + 'price' => 10490, + 'short_description' => 'Жидкостное охлаждение 240 мм.', + 'specs' => [ + 'manufacturer' => 'DeepCool', + 'intel_socket' => 'LGA1700', + 'amd_socket' => 'AM5', + 'fan_speed' => '2250 об/мин', + 'fans' => '2 вентилятора', + ], + ], + ], + ], + [ + 'name' => 'Ноутбуки', + 'slug' => 'laptops', + 'description' => 'Ноутбуки для работы и игр.', + 'products' => [ + [ + 'name' => 'ASUS TUF Gaming A15', + 'price' => 99990, + 'short_description' => '15.6\", Ryzen 7, RTX 4060.', + 'specs' => [ + 'manufacturer' => 'ASUS', + 'type' => 'Игровой', + 'screen_size' => '15.6\"', + 'cpu_brand' => 'AMD', + 'cpu_model' => 'Ryzen 7 7840HS', + 'ram' => '16 ГБ', + 'storage' => '1 ТБ SSD', + 'panel' => 'IPS', + 'resolution' => '1920x1080', + 'cores' => '8', + 'gpu_brand' => 'NVIDIA', + 'gpu_model' => 'RTX 4060', + ], + ], + [ + 'name' => 'Lenovo IdeaPad 5', + 'price' => 64990, + 'short_description' => '14\", легкий для работы.', + 'specs' => [ + 'manufacturer' => 'Lenovo', + 'type' => 'Для работы', + 'screen_size' => '14\"', + 'cpu_brand' => 'Intel', + 'cpu_model' => 'Core i5-1340P', + 'ram' => '16 ГБ', + 'storage' => '512 ГБ SSD', + 'panel' => 'IPS', + 'resolution' => '1920x1200', + 'cores' => '12', + 'gpu_brand' => 'Intel', + 'gpu_model' => 'Iris Xe', + ], + ], + [ + 'name' => 'Acer Swift 3', + 'price' => 59990, + 'short_description' => '13.5\", компактный ультрабук.', + 'specs' => [ + 'manufacturer' => 'Acer', + 'type' => 'Ультрабук', + 'screen_size' => '13.5\"', + 'cpu_brand' => 'Intel', + 'cpu_model' => 'Core i5-1235U', + 'ram' => '8 ГБ', + 'storage' => '512 ГБ SSD', + 'panel' => 'IPS', + 'resolution' => '2256x1504', + 'cores' => '10', + 'gpu_brand' => 'Intel', + 'gpu_model' => 'Iris Xe', + ], + ], + ], + ], + [ + 'name' => 'Телевизоры', + 'slug' => 'televizory', + 'description' => 'Телевизоры для дома и офиса.', + 'products' => [ + [ + 'name' => 'Samsung UE55CU7100', + 'price' => 49990, + 'short_description' => '55\", 4K UHD, Smart TV.', + 'specs' => [ + 'manufacturer' => 'Samsung', + 'screen_size' => '55\"', + 'resolution' => '3840x2160', + 'panel' => 'LED', + 'refresh_rate' => '60 Гц', + 'smart_tv' => 'Да', + ], + ], + [ + 'name' => 'LG 50NANO766QA', + 'price' => 57990, + 'short_description' => '50\", NanoCell, webOS.', + 'specs' => [ + 'manufacturer' => 'LG', + 'screen_size' => '50\"', + 'resolution' => '3840x2160', + 'panel' => 'NanoCell', + 'refresh_rate' => '60 Гц', + 'smart_tv' => 'Да', + ], + ], + [ + 'name' => 'Xiaomi TV A Pro 43', + 'price' => 29990, + 'short_description' => '43\", 4K UHD, Android TV.', + 'specs' => [ + 'manufacturer' => 'Xiaomi', + 'screen_size' => '43\"', + 'resolution' => '3840x2160', + 'panel' => 'LED', + 'refresh_rate' => '60 Гц', + 'smart_tv' => 'Да', + ], + ], + ], + ], + [ + 'name' => 'Apple', + 'slug' => 'apple', + 'description' => 'Устройства Apple: ноутбуки, планшеты и смартфоны.', + 'products' => [ + [ + 'name' => 'Apple MacBook Air 13 M2', + 'price' => 124990, + 'short_description' => '13.6\", чип M2, 8/256 ГБ.', + 'specs' => [ + 'manufacturer' => 'Apple', + 'type' => 'Ноутбук', + 'model' => 'MacBook Air 13 M2', + 'color' => 'Серебристый', + 'screen_size' => '13.6\"', + 'storage' => '256 ГБ', + 'condition' => 'Новое', + ], + ], + [ + 'name' => 'Apple iPhone 14 128GB', + 'price' => 69990, + 'short_description' => '6.1\", 128 ГБ, отличное состояние.', + 'specs' => [ + 'manufacturer' => 'Apple', + 'type' => 'Смартфон', + 'model' => 'iPhone 14', + 'color' => 'Синий', + 'screen_size' => '6.1\"', + 'storage' => '128 ГБ', + 'condition' => 'Б/у', + ], + ], + [ + 'name' => 'Apple iPad Air 11 M2', + 'price' => 87990, + 'short_description' => '11\", чип M2, Wi‑Fi.', + 'specs' => [ + 'manufacturer' => 'Apple', + 'type' => 'Планшет', + 'model' => 'iPad Air 11 M2', + 'color' => 'Фиолетовый', + 'screen_size' => '11\"', + 'storage' => '256 ГБ', + 'condition' => 'Новое', + ], + ], + ], + ], + ]; + + foreach ($categories as $categoryData) { + $products = $categoryData['products']; + unset($categoryData['products']); + + $category = Category::query()->updateOrCreate( + ['slug' => $categoryData['slug']], + $categoryData + ); + + foreach ($products as $productData) { + $slug = Str::slug($productData['name']); + $skuPrefix = strtoupper(Str::replace('-', '', Str::substr($category->slug, 0, 3))); + $sku = $skuPrefix . '-' . strtoupper(Str::substr(sha1($slug), 0, 6)); + $specs = (array) ($productData['specs'] ?? []); + if (!isset($specs['condition']) || trim((string) $specs['condition']) === '') { + $specs['condition'] = 'Новое'; + } + + Product::query()->updateOrCreate( + ['slug' => $slug], + array_merge($productData, [ + 'category_id' => $category->id, + 'slug' => $slug, + 'sku' => $productData['sku'] ?? $sku, + 'stock' => $productData['stock'] ?? 12, + 'old_price' => $productData['old_price'] ?? round($productData['price'] * 1.12), + 'description' => $productData['description'] ?? ($productData['short_description'] ?? ''), + 'specs' => $specs, + 'is_active' => true, + ]) + ); + } + } + } +} diff --git a/deploy/nginx/pc-shop.conf b/deploy/nginx/pc-shop.conf new file mode 100644 index 0000000..e2abad2 --- /dev/null +++ b/deploy/nginx/pc-shop.conf @@ -0,0 +1,63 @@ +server { + listen 80; + listen [::]:80; + server_name shop.example.com www.shop.example.com; + root /var/www/pc-shop/public; + + location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; + try_files $uri =404; + } + + location / { + return 301 https://$host$request_uri; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name shop.example.com www.shop.example.com; + root /var/www/pc-shop/public; + index index.php; + + ssl_certificate /etc/letsencrypt/live/shop.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/shop.example.com/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + charset utf-8; + client_max_body_size 20m; + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { + access_log off; + log_not_found off; + } + + location = /robots.txt { + access_log off; + log_not_found off; + } + + error_page 404 /index.php; + + location ~ \.php$ { + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + fastcgi_pass unix:/run/php/php8.2-fpm.sock; + fastcgi_read_timeout 60s; + } + + location ~ /\.(?!well-known).* { + deny all; + } +} diff --git a/deploy/nginx/pc-shop.http.conf b/deploy/nginx/pc-shop.http.conf new file mode 100644 index 0000000..8d8f7ea --- /dev/null +++ b/deploy/nginx/pc-shop.http.conf @@ -0,0 +1,46 @@ +server { + listen 80; + listen [::]:80; + server_name shop.example.com www.shop.example.com; + root /var/www/pc-shop/public; + index index.php; + + charset utf-8; + client_max_body_size 20m; + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + + location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; + try_files $uri =404; + } + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { + access_log off; + log_not_found off; + } + + location = /robots.txt { + access_log off; + log_not_found off; + } + + error_page 404 /index.php; + + location ~ \.php$ { + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + fastcgi_pass unix:/run/php/php8.2-fpm.sock; + fastcgi_read_timeout 60s; + } + + location ~ /\.(?!well-known).* { + deny all; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..91bf986 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2423 @@ +{ + "name": "pc-shop", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "vite": "^7.0.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz", + "integrity": "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.0" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.0.tgz", + "integrity": "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.0", + "@tailwindcss/oxide-darwin-arm64": "4.2.0", + "@tailwindcss/oxide-darwin-x64": "4.2.0", + "@tailwindcss/oxide-freebsd-x64": "4.2.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.0", + "@tailwindcss/oxide-linux-x64-musl": "4.2.0", + "@tailwindcss/oxide-wasm32-wasi": "4.2.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.0" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.0.tgz", + "integrity": "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.0.tgz", + "integrity": "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.0.tgz", + "integrity": "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.0.tgz", + "integrity": "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.0.tgz", + "integrity": "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.0.tgz", + "integrity": "sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.0.tgz", + "integrity": "sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.0.tgz", + "integrity": "sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.0.tgz", + "integrity": "sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.0.tgz", + "integrity": "sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.0.tgz", + "integrity": "sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.0.tgz", + "integrity": "sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.0.tgz", + "integrity": "sha512-da9mFCaHpoOgtQiWtDGIikTrSpUFBtIZCG3jy/u2BGV+l/X1/pbxzmIUxNt6JWm19N3WtGi4KlJdSH/Si83WOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.0", + "@tailwindcss/oxide": "4.2.0", + "tailwindcss": "4.2.0" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.1.0.tgz", + "integrity": "sha512-z+ck2BSV6KWtYcoIzk9Y5+p4NEjqM+Y4i8/H+VZRLq0OgNjW2DqyADquwYu5j8qRvaXwzNmfCWl1KrMlV1zpsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^7.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.0.tgz", + "integrity": "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7686b29 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "vite": "^7.0.7" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..d703241 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,35 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/resources/css/app.css b/resources/css/app.css new file mode 100644 index 0000000..7fa1583 --- /dev/null +++ b/resources/css/app.css @@ -0,0 +1,2935 @@ +@import 'tailwindcss'; + +@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@source '../../storage/framework/views/*.php'; +@source '../**/*.blade.php'; +@source '../**/*.js'; + +@theme { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; +} + +:root { + color-scheme: light; + --bg: #fbfdff; + --bg-2: #ffffff; + --ink: #111827; + --muted: #64748b; + --brand: #e24a4a; + --brand-2: #c73a3a; + --accent: #0f172a; + --accent-2: #334155; + --danger: #ef4444; + --border: rgba(15, 23, 42, 0.12); + --card: #ffffff; + --card-soft: #f8fafc; + --shadow: 0 8px 24px rgba(15, 23, 42, 0.06); + --radius: 12px; + --radius-lg: 16px; + --font-display: "Space Grotesk", "Segoe UI", sans-serif; + --font-body: "Manrope", "Segoe UI", sans-serif; +} + +body.pc-body { + margin: 0; + min-height: 100vh; + font-family: var(--font-body); + font-size: 15px; + color: var(--ink); + background: var(--bg); + overflow-x: hidden; +} + +.pc-shell { + position: relative; + max-width: 1200px; + margin: 0 auto; + padding: 32px 24px 96px; + z-index: 1; +} + +.pc-glow { + display: none; +} + +.pc-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; + position: sticky; + top: 0; + z-index: 100; + background: rgba(255, 255, 255, 0.96); + backdrop-filter: blur(6px); + border: 1px solid var(--border); + border-radius: 12px; + padding: 10px 12px; + box-shadow: 0 4px 14px rgba(15, 23, 42, 0.05); +} + +.pc-header-nav { + flex-basis: 100%; + display: flex; + align-items: center; + gap: 8px; + margin-top: 4px; + padding-top: 10px; + border-top: 1px solid rgba(15, 23, 42, 0.08); + max-height: 56px; + opacity: 1; + transform: translateY(0); + overflow-x: auto; + scrollbar-width: thin; + transition: + max-height 0.24s ease, + opacity 0.2s ease, + padding 0.24s ease, + margin 0.24s ease, + border-color 0.24s ease; +} + +.pc-header.is-nav-hidden .pc-header-nav { + max-height: 0; + opacity: 0; + margin-top: 0; + padding-top: 0; + border-top-color: transparent; + overflow: hidden; + pointer-events: none; +} + +.pc-header-nav-link { + flex: 0 0 auto; + text-decoration: none; + color: var(--muted); + border: 1px solid transparent; + border-radius: 999px; + padding: 6px 12px; + font-size: 0.8rem; + font-weight: 600; + transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease; +} + +.pc-header-nav-link:hover { + color: var(--ink); + border-color: var(--border); + background: #f8fafc; +} + +.pc-header-nav-link.is-active { + color: #b23434; + border-color: rgba(199, 58, 58, 0.26); + background: rgba(199, 58, 58, 0.08); +} + +.pc-header-left { + display: flex; + align-items: center; + gap: 14px; + flex-wrap: wrap; +} + +.pc-header-center { + flex: 1; + display: flex; + justify-content: center; +} + +.pc-header-icons { + display: flex; + gap: 14px; + align-items: flex-start; +} + +.pc-mobile-menu-toggle { + display: none; +} + +.pc-hamburger { + display: none; + width: 40px; + height: 40px; + border-radius: 10px; + border: 1px solid var(--border); + background: #ffffff; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 4px; + cursor: pointer; +} + +.pc-hamburger span { + width: 16px; + height: 2px; + background: #334155; + border-radius: 2px; + transition: transform 0.2s ease, opacity 0.2s ease; +} + +.pc-mobile-menu-head { + display: none; +} + +.pc-mobile-menu-close { + width: 40px; + height: 40px; + border-radius: 10px; + border: 1px solid var(--border); + background: #ffffff; + color: var(--ink); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 1.3rem; + line-height: 1; + text-decoration: none; + cursor: pointer; +} + +.pc-logo { + display: inline-flex; + align-items: center; + gap: 12px; + font-family: var(--font-display); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + font-size: 0.95rem; + text-decoration: none; +} + +.pc-logo-mark { + width: 14px; + height: 14px; + border-radius: 999px; + background: var(--brand); + box-shadow: 0 0 0 4px rgba(226, 74, 74, 0.12); +} + +.pc-nav { + display: flex; + gap: 10px; + flex-wrap: wrap; + font-size: 0.85rem; + color: var(--muted); +} + +.pc-nav a { + text-decoration: none; + padding: 6px 12px; + border-radius: 999px; + border: 1px solid transparent; + transition: all 0.2s ease; +} + +.pc-nav a:hover { + color: var(--ink); + border-color: var(--border); + background: rgba(255, 255, 255, 0.05); +} + +.pc-actions { + display: flex; + gap: 12px; + align-items: center; + flex-wrap: wrap; +} + +.pc-search { + position: relative; +} + +.pc-search input { + width: 420px; + max-width: 100%; + border-radius: 999px; + padding: 10px 44px 10px 14px; + background: rgba(255, 255, 255, 0.9); + border: 1px solid var(--border); + color: var(--ink); +} + +.pc-search input::placeholder { + color: var(--muted); +} + +.pc-search-submit { + position: absolute; + top: 50%; + right: 8px; + transform: translateY(-50%); + width: 28px; + height: 28px; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--muted); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: 0; +} + +.pc-search-submit:hover { + color: var(--ink); + background: rgba(15, 23, 42, 0.06); +} + +.pc-search-submit:focus-visible { + outline: 2px solid rgba(226, 74, 74, 0.4); + outline-offset: 1px; +} + +.pc-search-submit svg { + width: 16px; + height: 16px; + fill: currentColor; +} + +.pc-icon-link { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + text-decoration: none; + color: var(--ink); + min-width: 64px; +} + +.pc-icon { + width: 38px; + height: 38px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 12px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.75); + position: relative; +} + +.pc-icon svg { + width: 18px; + height: 18px; + fill: currentColor; +} + +.pc-icon-count { + position: absolute; + top: -6px; + right: -6px; + min-width: 18px; + height: 18px; + padding: 0 4px; + border-radius: 999px; + background: #ef4444; + color: #ffffff; + font-size: 0.66rem; + font-weight: 700; + line-height: 18px; + text-align: center; +} + +.pc-icon-label { + font-size: 0.72rem; + color: var(--muted); +} + +.pc-catalog-btn { + padding: 10px 16px; +} + +.pc-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 18px; + border-radius: 10px; + border: 1px solid var(--border); + text-decoration: none; + color: var(--ink); + font-weight: 600; + font-size: 0.85rem; + background: rgba(255, 255, 255, 0.02); + transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease; +} + +.pc-btn:hover { + transform: none; + border-color: rgba(15, 23, 42, 0.25); +} + +.pc-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.pc-btn.primary { + background: var(--brand); + color: #ffffff; + border-color: var(--brand); +} + +.pc-btn.ghost { + background: rgba(255, 255, 255, 0.04); +} + +.pc-btn.is-active { + border-color: rgba(199, 58, 58, 0.36); + background: rgba(226, 74, 74, 0.1); + color: #b23434; +} + +.pc-badge { + padding: 2px 8px; + border-radius: 999px; + font-size: 0.7rem; + font-weight: 700; + background: var(--brand); + color: #ffffff; +} + +.pc-main { + display: flex; + flex-direction: column; + gap: 72px; + margin-top: 40px; +} + +.pc-alert { + padding: 12px 16px; + border-radius: 12px; + border: 1px solid rgba(16, 185, 129, 0.35); + background: rgba(16, 185, 129, 0.14); + color: #065f46; + font-size: 0.88rem; + transition: opacity 0.26s ease, transform 0.26s ease; +} + +.pc-alert.is-hiding { + opacity: 0; + transform: translateY(-6px); +} + +.pc-alert-error { + border-color: rgba(239, 68, 68, 0.32); + background: rgba(239, 68, 68, 0.12); + color: #991b1b; +} + +.pc-muted { + color: var(--muted); +} + +.pc-chat-widget { + position: fixed; + right: 24px; + bottom: 24px; + z-index: 240; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10px; +} + +.pc-chat-toggle { + border: 1px solid rgba(226, 74, 74, 0.38); + background: #e24a4a; + color: #ffffff; + border-radius: 999px; + padding: 10px 18px; + display: inline-flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 0.88rem; + cursor: pointer; +} + +.pc-chat-toggle svg { + width: 16px; + height: 16px; + fill: currentColor; +} + +.pc-chat-toggle:hover { + background: #c73a3a; +} + +.pc-chat-widget.is-open .pc-chat-toggle { + background: #0f172a; + border-color: rgba(15, 23, 42, 0.34); +} + +.pc-chat-panel { + width: min(360px, calc(100vw - 24px)); + border-radius: 14px; + border: 1px solid rgba(15, 23, 42, 0.12); + background: #ffffff; + box-shadow: 0 14px 34px rgba(15, 23, 42, 0.12); + padding: 14px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.pc-chat-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.pc-chat-header h3 { + margin: 0; + font-size: 1rem; +} + +.pc-chat-close { + width: 28px; + height: 28px; + border-radius: 8px; + border: 1px solid var(--border); + background: #ffffff; + color: var(--muted); + cursor: pointer; + font-size: 1rem; + line-height: 1; +} + +.pc-chat-close:hover { + color: var(--ink); +} + +.pc-chat-note { + margin: 0; + color: var(--muted); + font-size: 0.78rem; +} + +.pc-chat-messages { + min-height: 120px; + max-height: 280px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 9px; + padding: 10px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.1); + background: #f8fafc; +} + +.pc-chat-message { + display: flex; + flex-direction: column; + gap: 3px; + align-items: flex-start; +} + +.pc-chat-message.is-customer { + align-items: flex-end; +} + +.pc-chat-bubble { + max-width: 88%; + padding: 8px 10px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.09); + font-size: 0.82rem; + line-height: 1.45; + white-space: pre-wrap; +} + +.pc-chat-message.is-admin .pc-chat-bubble { + background: #ffffff; +} + +.pc-chat-message.is-customer .pc-chat-bubble { + background: rgba(226, 74, 74, 0.11); + border-color: rgba(226, 74, 74, 0.22); +} + +.pc-chat-meta { + font-size: 0.7rem; + color: var(--muted); +} + +.pc-chat-form { + display: grid; + gap: 8px; +} + +.pc-chat-form textarea { + border-radius: 10px; + border: 1px solid var(--border); + background: #ffffff; + padding: 9px 11px; + color: var(--ink); + resize: vertical; + min-height: 74px; +} + +.pc-admin-chat-layout { + display: grid; + grid-template-columns: 300px minmax(0, 1fr); + gap: 16px; + align-items: start; +} + +.pc-admin-chat-list { + padding: 10px; + max-height: 640px; + overflow-y: auto; + gap: 8px; +} + +.pc-admin-chat-item { + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px 12px; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.08); + color: inherit; + text-decoration: none; + background: #ffffff; +} + +.pc-admin-chat-item:hover { + border-color: rgba(226, 74, 74, 0.24); + background: #fff7f7; +} + +.pc-admin-chat-item.is-active { + border-color: rgba(226, 74, 74, 0.34); + background: rgba(226, 74, 74, 0.08); +} + +.pc-admin-chat-item-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.pc-admin-chat-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + padding: 0 6px; + border-radius: 999px; + background: #ef4444; + color: #ffffff; + font-size: 0.72rem; + font-weight: 700; +} + +.pc-admin-chat-panel { + min-height: 560px; +} + +#pc-admin-chat { + display: flex; + flex-direction: column; + gap: 12px; + height: 100%; +} + +.pc-admin-chat-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.pc-admin-chat-head h3 { + margin: 0 0 4px; +} + +.pc-admin-chat-head p { + margin: 0; +} + +.pc-admin-chat-actions { + display: flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; +} + +.pc-admin-chat-messages { + flex: 1; + min-height: 330px; + max-height: 560px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.08); + background: #f8fafc; +} + +.pc-admin-chat-message { + display: flex; + flex-direction: column; + gap: 4px; + align-items: flex-start; +} + +.pc-admin-chat-message.is-admin { + align-items: flex-end; +} + +.pc-admin-chat-bubble { + max-width: 88%; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.1); + padding: 9px 12px; + font-size: 0.85rem; + line-height: 1.45; + white-space: pre-wrap; + background: #ffffff; +} + +.pc-admin-chat-message.is-admin .pc-admin-chat-bubble { + background: rgba(226, 74, 74, 0.1); + border-color: rgba(226, 74, 74, 0.24); +} + +.pc-admin-chat-form { + display: grid; + gap: 10px; +} + +.pc-admin-chat-form textarea { + border-radius: 10px; + border: 1px solid var(--border); + background: #ffffff; + color: var(--ink); + padding: 10px 12px; + resize: vertical; +} + +.pc-home-sliders { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 16px; +} + +.pc-home-slider { + position: relative; + overflow: hidden; + border-radius: 16px; + border: 1px solid var(--border); + min-height: 340px; + background: #f2e8e8; +} + +.pc-home-slider.is-main { + min-height: 360px; +} + +.pc-home-slider-track { + position: relative; + min-height: inherit; + height: 100%; +} + +.pc-home-slide { + position: absolute; + inset: 0; + opacity: 0; + pointer-events: none; + transition: opacity 0.35s ease; +} + +.pc-home-slide.is-active { + opacity: 1; + pointer-events: auto; +} + +.pc-home-slide img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.pc-home-slide-overlay { + position: absolute; + inset: 0; + background: linear-gradient(115deg, rgba(15, 23, 42, 0.56), rgba(15, 23, 42, 0.22)); +} + +.pc-home-slide.is-fallback .pc-home-slide-overlay { + background: linear-gradient(120deg, rgba(15, 23, 42, 0.82), rgba(15, 23, 42, 0.48)); +} + +.pc-home-slide-content { + position: absolute; + left: 24px; + right: 24px; + bottom: 22px; + display: grid; + gap: 10px; + z-index: 2; + color: #f8fafc; + max-width: 640px; +} + +.pc-home-slider.is-side .pc-home-slide-content { + max-width: 360px; +} + +.pc-home-slide-content h2 { + margin: 0; + font-family: var(--font-display); + font-size: clamp(1.2rem, 2.2vw, 2rem); + line-height: 1.2; +} + +.pc-home-slide-content p { + margin: 0; + color: rgba(248, 250, 252, 0.92); + line-height: 1.45; +} + +.pc-home-slider-btn { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 34px; + height: 34px; + border-radius: 999px; + border: 1px solid rgba(248, 250, 252, 0.44); + background: rgba(15, 23, 42, 0.48); + color: #ffffff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + line-height: 1; + cursor: pointer; + z-index: 3; +} + +.pc-home-slider-btn:hover { + background: rgba(15, 23, 42, 0.65); +} + +.pc-home-slider-btn.is-prev { + left: 10px; +} + +.pc-home-slider-btn.is-next { + right: 10px; +} + +.pc-home-slider-btn:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.pc-admin-slide-item { + display: grid; + grid-template-columns: 112px minmax(0, 1fr); + gap: 12px; + padding: 12px; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.08); + background: #ffffff; +} + +.pc-admin-slide-main { + display: grid; + gap: 6px; +} + +.pc-admin-slide-preview { + width: 112px; + height: 76px; + border-radius: 10px; + background-color: #e2e8f0; + background-size: cover; + background-position: center; + border: 1px solid rgba(15, 23, 42, 0.1); +} + +.pc-admin-slide-current { + display: grid; + gap: 8px; +} + +.pc-admin-slide-current-preview { + width: 100%; + max-width: 360px; + aspect-ratio: 16 / 9; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.1); + background-color: #e2e8f0; + background-size: cover; + background-position: center; +} + +.pc-admin-product-image-current { + display: grid; + gap: 8px; +} + +.pc-admin-product-image-preview { + width: 100%; + max-width: 260px; + aspect-ratio: 4 / 3; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.1); + background-color: #e2e8f0; + object-fit: cover; +} + +.pc-admin-product-gallery-current { + display: grid; + gap: 8px; +} + +.pc-admin-product-gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); + gap: 8px; + max-width: 520px; +} + +.pc-admin-product-gallery-item { + display: grid; + gap: 6px; + align-content: start; +} + +.pc-admin-product-gallery-preview { + width: 100%; + aspect-ratio: 1 / 1; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.1); + background-color: #e2e8f0; + object-fit: cover; +} + +.pc-hero-home { + display: grid; + grid-template-columns: 1.4fr 1fr; + gap: 20px; + align-items: stretch; +} + +.pc-hero-home .pc-hero-copy { + background: #ffffff; + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 30px; + box-shadow: var(--shadow); +} + +.pc-hero-home .pc-hero-copy h1 { + margin: 16px 0; + font-size: clamp(2rem, 3.2vw, 3rem); +} + +.pc-hero-home .pc-hero-copy p { + color: var(--muted); + margin-bottom: 20px; +} + +.pc-hero-stats { + display: grid; + grid-template-columns: 1fr; + gap: 12px; +} + +.pc-hero { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 40px; + align-items: center; +} + +.pc-hero-copy h1 { + font-family: var(--font-display); + font-size: clamp(2.6rem, 4.6vw, 4.4rem); + line-height: 1.05; + margin: 16px 0; +} + +.pc-hero-copy p { + color: var(--muted); + font-size: 1.05rem; + margin-bottom: 22px; + max-width: 520px; +} + +.pc-pill { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 14px; + border-radius: 10px; + border: 1px solid var(--border); + background: #f1f5f9; + font-size: 0.7rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--muted); +} + +.pc-hero-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.pc-hero-copy .pc-hero-actions { + margin-bottom: 24px; +} + +.pc-stats { + display: flex; + gap: 16px; + flex-wrap: wrap; +} + +.pc-stat { + padding: 14px 18px; + border-radius: var(--radius); + border: 1px solid var(--border); + background: #ffffff; + min-width: 150px; +} + +.pc-stat-value { + font-family: var(--font-display); + font-size: 1.4rem; + font-weight: 700; +} + +.pc-stat-label { + color: var(--muted); + font-size: 0.85rem; + margin-top: 4px; +} + +.pc-hero-panel { + background: #ffffff; + border-radius: var(--radius-lg); + padding: 24px; + border: 1px solid var(--border); + box-shadow: var(--shadow); + position: relative; + overflow: hidden; +} + +.pc-hero-panel::after { + display: none; +} + +.pc-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.pc-panel-kicker { + color: var(--muted); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.2em; + margin-bottom: 6px; +} + +.pc-panel-header h3 { + font-family: var(--font-display); + font-size: 1.4rem; + margin: 0; +} + +.pc-chip { + padding: 6px 10px; + border-radius: 999px; + background: rgba(226, 74, 74, 0.16); + color: #b23434; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.pc-panel-list { + display: grid; + gap: 10px; + margin-bottom: 16px; +} + +.pc-panel-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + border-radius: 12px; + background: rgba(248, 250, 252, 0.9); + border: 1px solid rgba(15, 23, 42, 0.08); + font-size: 0.9rem; +} + +.pc-tag { + padding: 4px 8px; + border-radius: 999px; + background: rgba(45, 212, 191, 0.2); + color: #a7f3d0; + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.pc-progress { + width: 100%; + height: 6px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.12); + overflow: hidden; + margin-bottom: 12px; +} + +.pc-progress-bar { + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, var(--accent), var(--accent-2)); +} + +.pc-panel-footer { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.9rem; + color: var(--muted); +} + +.pc-panel-footer strong { + color: var(--ink); + font-family: var(--font-display); +} + +.pc-panel-note { + font-size: 0.75rem; +} + +.pc-section { + display: flex; + flex-direction: column; + gap: 10px; +} + +.pc-section-title h2 { + font-family: var(--font-display); + font-size: clamp(2rem, 3vw, 3rem); + margin: 5px 0; +} + +.pc-section-title p { + color: var(--muted); + max-width: 620px; +} + +.pc-about-content { + display: grid; + gap: 14px; + max-width: 900px; +} + +.pc-about-content h3 { + margin: 8px 0 2px; + font-family: var(--font-display); + font-size: 1.1rem; +} + +.pc-about-content p { + margin: 0; + color: var(--muted); + line-height: 1.65; +} + +.pc-grid { + display: grid; + gap: 18px; +} + +.pc-grid-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.pc-grid-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.pc-grid-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.pc-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; + box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05); + display: flex; + flex-direction: column; + gap: 12px; +} + +.pc-card h3 { + font-family: var(--font-display); + font-size: 1.2rem; + margin: 0; +} + +.pc-card p { + color: var(--muted); + margin: 0; +} + +.pc-card-meta { + font-size: 0.8rem; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.16em; +} + +.pc-category-card { + align-items: center; + text-align: center; + gap: 14px; + padding: 16px; +} + +.pc-category-image { + width: 100%; + aspect-ratio: 4 / 3; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.1); + background: #f1f5f9; + position: relative; + overflow: hidden; +} + +.pc-category-image::after { + display: none; +} + +.pc-category-title { + font-family: var(--font-display); + font-size: 1.05rem; + margin: 0; +} + +.pc-category-count { + font-size: 0.85rem; + color: var(--muted); +} + +.pc-list { + display: grid; + gap: 6px; + color: var(--muted); + font-size: 0.9rem; +} + +.pc-list li { + display: flex; + align-items: center; + gap: 8px; +} + +.pc-list li::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 999px; + background: var(--brand); + display: inline-block; +} + +.pc-anchor { + scroll-margin-top: 120px; +} + +.pc-compare { + display: grid; + gap: 8px; + font-size: 0.85rem; +} + +.pc-compare-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(248, 250, 252, 0.9); +} + +.pc-admin-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.pc-admin-stat { + padding: 12px; + border-radius: 14px; + background: rgba(248, 250, 252, 0.9); + border: 1px solid rgba(15, 23, 42, 0.08); +} + +.pc-admin-stat strong { + display: block; + font-family: var(--font-display); + font-size: 1.1rem; + margin-bottom: 4px; +} + +.pc-chart { + display: flex; + align-items: flex-end; + gap: 10px; + height: 120px; +} + +.pc-bar { + flex: 1; + border-radius: 12px; + background: linear-gradient(180deg, rgba(255, 122, 24, 0.9), rgba(255, 122, 24, 0.3)); +} + +.pc-bar.alt { + background: linear-gradient(180deg, rgba(45, 212, 191, 0.9), rgba(45, 212, 191, 0.3)); +} + +.pc-form { + gap: 16px; +} + +.pc-form label { + display: grid; + gap: 8px; + color: var(--muted); + font-size: 0.85rem; +} + +.pc-form input, +.pc-form textarea { + border-radius: 12px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.9); + padding: 12px 14px; + color: var(--ink); +} + +.pc-form textarea { + min-height: 120px; + resize: vertical; +} + +.pc-form select { + border-radius: 12px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.9); + padding: 12px 14px; + color: var(--ink); +} + +.pc-field-hint { + color: var(--muted); + font-size: 0.75rem; + line-height: 1.3; +} + +.pc-file-input { + border-radius: 10px; + border: 1px solid var(--border); + background: #ffffff; + padding: 8px 10px; + color: var(--ink); + max-width: 320px; +} + +.pc-spec-fields-wrap { + display: grid; + gap: 10px; + padding: 14px; + border: 1px dashed rgba(15, 23, 42, 0.2); + border-radius: 12px; + background: #f8fafc; +} + +.pc-spec-fields-title { + margin: 0; + font-size: 1rem; + font-family: var(--font-display); +} + +.pc-auth-section { + align-items: center; +} + +.pc-auth-section .pc-section-title { + width: min(100%, 560px); + text-align: center; +} + +.pc-auth-section .pc-section-title h2, +.pc-auth-section .pc-section-title p { + text-align: center; + margin-left: auto; + margin-right: auto; +} + +.pc-auth-form { + width: min(100%, 560px); + margin-inline: auto; +} + +.pc-auth-form label { + text-align: left; +} + +.pc-checkbox { + display: flex; + align-items: center; + flex-wrap: nowrap; + gap: 10px; + color: var(--ink); +} + +.pc-checkbox input { + width: 16px; + height: 16px; + margin: 0; +} + +.pc-checkbox span { + white-space: nowrap; +} + +.pc-account-orders { + display: grid; + gap: 10px; +} + +.pc-account-order { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(248, 250, 252, 0.9); + color: inherit; + text-decoration: none; +} + +.pc-account-order:hover { + border-color: rgba(15, 23, 42, 0.16); +} + +.pc-footer { + margin-top: 48px; + padding: 28px; + background: #0c0f14; + border: 1px solid rgba(148, 163, 184, 0.22); + border-radius: 14px; + display: flex; + flex-direction: column; + gap: 18px; + color: #cbd5e1; + font-size: 0.9rem; +} + +.pc-footer-top { + display: flex; + gap: 26px; + justify-content: space-between; + align-items: flex-start; + flex-wrap: wrap; +} + +.pc-footer-brand { + max-width: 360px; +} + +.pc-footer-brand h3 { + margin: 0 0 10px; + font-size: 1.2rem; + color: #f8fafc; +} + +.pc-footer-brand p { + margin: 0; + color: #94a3b8; + line-height: 1.55; +} + +.pc-footer-columns { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 24px; + flex: 1; + min-width: 520px; +} + +.pc-footer-col h4 { + margin: 0 0 10px; + color: #f8fafc; + font-size: 0.95rem; +} + +.pc-footer-list { + margin: 0; + padding: 0; + list-style: none; + display: grid; + gap: 7px; +} + +.pc-footer-list a, +.pc-footer-bottom a { + color: #cbd5e1; + text-decoration: none; +} + +.pc-footer-list a:hover, +.pc-footer-bottom a:hover { + color: #ffffff; +} + +.pc-footer-contact { + display: grid; + gap: 7px; + color: #94a3b8; + font-size: 0.85rem; +} + +.pc-footer-contact a { + color: #cbd5e1; + text-decoration: none; +} + +.pc-footer-contact a:hover { + color: #ffffff; +} + +.pc-footer-bottom { + border-top: 1px solid rgba(148, 163, 184, 0.22); + padding-top: 14px; + display: flex; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; + color: #94a3b8; + font-size: 0.82rem; +} + +.pc-breadcrumbs { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + font-size: 0.85rem; + color: var(--muted); +} + +.pc-breadcrumbs a { + color: var(--muted); + text-decoration: none; +} + +.pc-breadcrumbs a:hover { + color: var(--ink); +} + +.pc-breadcrumbs-current { + color: var(--ink); + font-weight: 600; +} + +.pc-breadcrumbs-sep { + opacity: 0.6; +} + +.pc-contact-link { + color: var(--brand-2); + text-decoration: none; + border-bottom: 1px dashed rgba(199, 58, 58, 0.35); +} + +.pc-contact-link:hover { + color: var(--brand); + border-bottom-color: rgba(226, 74, 74, 0.45); +} + +.pc-payment-details { + display: grid; + gap: 10px; + padding: 14px; + border-radius: 12px; + border: 1px dashed rgba(15, 23, 42, 0.18); + background: #f8fafc; +} + +.pc-payment-details h3 { + margin: 0; + font-size: 1rem; +} + +.pc-payment-total, +.pc-payment-purpose { + margin: 0; +} + +.pc-payment-grid { + display: grid; + gap: 8px; + width: 100%; + max-width: 480px; +} + +.pc-payment-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 8px 10px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.1); + background: #ffffff; +} + +.pc-payment-key { + color: var(--muted); + font-size: 0.82rem; +} + +.pc-payment-value { + font-size: 0.9rem; + letter-spacing: 0.04em; +} + +.pc-catalog-layout { + display: grid; + grid-template-columns: 260px 1fr; + gap: 24px; + align-items: start; +} + +.pc-filters { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px; + box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04); +} + +.pc-filter-details > summary { + display: none; +} + +.pc-filter-details > form { + display: block; +} + +.pc-filter-native-summary { + display: none; +} + +.pc-filter-inline-toggle { + display: none; +} + +.pc-filter-toggle { + list-style: none; + border-radius: 999px; + border: 1px solid var(--border); + background: #ffffff; + color: var(--ink); + padding: 6px 10px; + font-size: 0.78rem; + font-weight: 600; + line-height: 1; + gap: 6px; + align-items: center; + display: inline-flex; + cursor: pointer; +} + +.pc-filter-toggle::-webkit-details-marker { + display: none; +} + +.pc-filter-toggle svg { + width: 14px; + height: 14px; + fill: currentColor; +} + +.pc-filter-toggle-count { + min-width: 18px; + height: 18px; + border-radius: 999px; + border: 1px solid rgba(15, 23, 42, 0.14); + font-size: 0.7rem; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 4px; + background: rgba(15, 23, 42, 0.04); +} + +.pc-filter-title { + font-family: var(--font-display); + font-weight: 600; + margin-bottom: 12px; +} + +.pc-filter-block { + display: grid; + gap: 8px; + font-size: 0.85rem; + color: var(--muted); + margin-bottom: 12px; +} + +.pc-filter-block select { + border-radius: 10px; + border: 1px solid var(--border); + padding: 10px; + background: rgba(255, 255, 255, 0.9); + color: var(--ink); +} + +.pc-range-fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.pc-range-fields input { + border-radius: 10px; + border: 1px solid var(--border); + padding: 10px; + background: rgba(255, 255, 255, 0.9); + color: var(--ink); + min-width: 0; +} + +.pc-filter-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.pc-products-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; +} + +.pc-product-carousel { + display: grid; + grid-template-columns: 38px minmax(0, 1fr) 38px; + gap: 10px; + align-items: stretch; +} + +.pc-product-carousel-track { + display: grid; + grid-auto-flow: column; + grid-auto-columns: calc((100% - 54px) / 4); + gap: 18px; + overflow-x: auto; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + scrollbar-width: none; + -ms-overflow-style: none; + padding: 2px; +} + +.pc-product-carousel-track::-webkit-scrollbar { + display: none; +} + +.pc-product-carousel-item { + min-width: 0; + scroll-snap-align: start; +} + +.pc-product-carousel-btn { + border-radius: 12px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.9); + color: var(--ink); + font-size: 1.5rem; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: 0; +} + +.pc-product-carousel-btn:hover { + background: #ffffff; + border-color: rgba(15, 23, 42, 0.2); +} + +.pc-product-carousel-btn:disabled { + opacity: 0.45; + cursor: not-allowed; +} + +.pc-product-card { + display: flex; + flex-direction: column; + height: 100%; + gap: 14px; +} + +.pc-product-card h3 { + margin: 0; +} + +.pc-product-media { + position: relative; +} + +.pc-product-tools { + position: absolute; + top: 10px; + right: 10px; + display: flex; + gap: 8px; + z-index: 2; +} + +.pc-product-tool-form { + margin: 0; +} + +.pc-product-tool { + width: 32px; + height: 32px; + border-radius: 10px; + border: 1px solid rgba(31, 41, 55, 0.16); + background: rgba(255, 255, 255, 0.92); + color: #334155; + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + transition: background 0.2s ease, color 0.2s ease, transform 0.2s ease; +} + +.pc-product-tool:hover { + background: #ffffff; + color: #b23434; + transform: none; +} + +.pc-product-tool.is-active { + background: rgba(199, 58, 58, 0.12); + border-color: rgba(199, 58, 58, 0.36); + color: #b23434; +} + +.pc-product-tool svg { + width: 16px; + height: 16px; + fill: currentColor; +} + +.pc-product-image { + display: block; + width: 100%; + aspect-ratio: 4 / 3; + border-radius: 14px; + background: #f1f5f9; + border: 1px solid rgba(15, 23, 42, 0.08); + object-fit: cover; +} + +.pc-product-meta { + display: flex; + gap: 10px; + align-items: baseline; + margin-top: auto; +} + +.pc-product-old { + color: var(--muted); + text-decoration: line-through; + font-size: 0.9rem; +} + +.pc-product-link { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + min-height: 2.8em; + line-height: 1.4; + overflow: hidden; + color: inherit; + text-decoration: none; +} + +.pc-product-link:hover { + color: #b23434; +} + +.pc-product-card > p { + margin: 0; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + min-height: 2.8em; + overflow: hidden; +} + +.pc-stock { + font-size: 0.75rem; + border-radius: 999px; + padding: 4px 8px; + border: 1px solid transparent; + white-space: nowrap; +} + +.pc-stock-in { + color: #047857; + background: rgba(16, 185, 129, 0.14); + border-color: rgba(5, 150, 105, 0.22); +} + +.pc-stock-out { + color: #b91c1c; + background: rgba(239, 68, 68, 0.14); + border-color: rgba(220, 38, 38, 0.22); +} + +.pc-product-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-top: 8px; +} + +.pc-product-actions form { + margin: 0; +} + +.pc-category-link { + text-decoration: none; + color: inherit; +} + +.pc-product-page { + gap: 24px; +} + +.pc-product-hero { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 24px; + align-items: center; +} + +.pc-product-gallery { + display: grid; + gap: 10px; +} + +.pc-product-image-lg { + display: block; + width: 100%; + aspect-ratio: 4 / 3; + border-radius: 14px; + background: #f1f5f9; + border: 1px solid var(--border); + object-fit: cover; + touch-action: pan-y; + user-select: none; + -webkit-user-drag: none; + will-change: transform, opacity, filter; + backface-visibility: hidden; +} + +.pc-product-thumbs { + display: flex; + gap: 8px; + overflow-x: auto; + padding: 2px 1px 2px 0; + scrollbar-width: thin; +} + +.pc-product-thumb { + width: 64px; + height: 64px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.16); + background: #ffffff; + padding: 2px; + cursor: pointer; + flex: 0 0 auto; + transition: border-color 0.2s ease, background-color 0.2s ease; +} + +.pc-product-thumb img { + display: block; + width: 100%; + height: 100%; + border-radius: 8px; + object-fit: cover; +} + +.pc-product-thumb:hover { + border-color: rgba(199, 58, 58, 0.34); +} + +.pc-product-thumb.is-active { + border-color: rgba(199, 58, 58, 0.48); + background: rgba(199, 58, 58, 0.08); +} + +.pc-product-thumb:focus-visible { + outline: 2px solid rgba(226, 74, 74, 0.34); + outline-offset: 1px; +} + +.pc-product-info h1 { + font-family: var(--font-display); + font-size: clamp(1.8rem, 3vw, 2.6rem); + margin: 8px 0; +} + +.pc-product-badges { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +.pc-sku { + font-size: 0.8rem; + color: var(--muted); +} + +.pc-product-price { + display: flex; + align-items: baseline; + gap: 12px; + margin: 12px 0; + font-size: 1.2rem; +} + +.pc-tabs { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px; + box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04); +} + +.pc-tabs input[type="radio"] { + display: none; +} + +.pc-tab-labels { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 16px; +} + +.pc-tab-labels label { + cursor: pointer; + padding: 8px 14px; + border-radius: 999px; + border: 1px solid var(--border); + font-size: 0.85rem; + color: var(--muted); +} + +#tab-specs:checked ~ .pc-tab-labels label[for="tab-specs"], +#tab-desc:checked ~ .pc-tab-labels label[for="tab-desc"], +#tab-shipping:checked ~ .pc-tab-labels label[for="tab-shipping"], +#tab-payment:checked ~ .pc-tab-labels label[for="tab-payment"] { + background: rgba(249, 115, 22, 0.15); + color: var(--ink); + border-color: rgba(249, 115, 22, 0.3); +} + +.pc-tab-panel { + display: none; +} + +#tab-specs:checked ~ .pc-tab-content .pc-tab-specs, +#tab-desc:checked ~ .pc-tab-content .pc-tab-desc, +#tab-shipping:checked ~ .pc-tab-content .pc-tab-shipping, +#tab-payment:checked ~ .pc-tab-content .pc-tab-payment { + display: block; +} + +.pc-specs-grid { + display: grid; + gap: 10px; +} + +.pc-spec-row { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 10px 12px; + border-radius: 10px; + background: rgba(248, 250, 252, 0.9); + border: 1px solid rgba(15, 23, 42, 0.08); + font-size: 0.82rem; +} + +.pc-pagination { + margin-top: 18px; +} + +.pc-pager { + display: flex; + justify-content: center; +} + +.pc-pager-list { + list-style: none; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px; + margin: 0; + padding: 0; +} + +.pc-pager-link { + min-width: 34px; + height: 34px; + padding: 0 10px; + border: 1px solid var(--border); + border-radius: 9px; + background: #ffffff; + color: var(--ink); + text-decoration: none; + font-size: 0.8rem; + font-weight: 600; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.pc-pager-link:hover { + border-color: rgba(15, 23, 42, 0.22); + background: #f8fafc; +} + +.pc-pager-link.is-active { + border-color: rgba(226, 74, 74, 0.48); + background: rgba(226, 74, 74, 0.12); + color: #b23434; +} + +.pc-pager-link.is-disabled { + opacity: 0.5; + pointer-events: none; +} + +.pc-pager-link.is-gap { + border-color: transparent; + background: transparent; + min-width: 20px; + padding: 0 2px; +} + +.pc-category-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + flex-wrap: wrap; +} + +.pc-category-toolbar-controls { + display: flex; + align-items: center; + gap: 10px; +} + +.pc-sort-form { + display: flex; + align-items: center; + gap: 10px; + color: var(--muted); + font-size: 0.85rem; +} + +.pc-sort-form select { + border-radius: 10px; + border: 1px solid var(--border); + padding: 9px 12px; + background: rgba(255, 255, 255, 0.9); + color: var(--ink); +} + +.pc-search-page-form { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.pc-search-page-form .pc-search { + flex: 1 1 420px; +} + +.pc-filter-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.pc-filter-chip { + border-radius: 999px; + border: 1px solid rgba(226, 74, 74, 0.25); + background: rgba(226, 74, 74, 0.12); + color: #e24a4a; + font-size: 0.78rem; + padding: 5px 10px; +} + +.pc-cart-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + gap: 20px; + align-items: start; +} + +.pc-cart-list { + display: grid; + gap: 14px; +} + +.pc-cart-item { + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 16px; +} + +.pc-cart-item-main { + flex: 1; +} + +.pc-cart-item-main h3 { + margin-bottom: 8px; +} + +.pc-cart-item-side { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 10px; +} + +.pc-cart-qty-form { + display: flex; + align-items: center; + gap: 8px; +} + +.pc-cart-qty-form input { + width: 72px; + border-radius: 10px; + border: 1px solid var(--border); + padding: 9px 10px; + text-align: center; + background: rgba(255, 255, 255, 0.9); + color: var(--ink); +} + +.pc-cart-subtotal { + font-size: 1.05rem; +} + +.pc-cart-summary { + gap: 14px; +} + +.pc-cart-summary-row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + border-bottom: 1px dashed var(--border); + padding-bottom: 10px; +} + +.pc-compare-actions { + display: flex; + justify-content: flex-end; +} + +.pc-compare-actions form { + margin: 0; +} + +.pc-compare-table-wrap { + padding: 0; + overflow: hidden; +} + +.pc-table-scroll { + overflow-x: auto; +} + +.pc-compare-table { + width: 100%; + border-collapse: collapse; + min-width: 680px; +} + +.pc-compare-table th, +.pc-compare-table td { + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + padding: 12px 14px; + text-align: left; + vertical-align: top; + font-size: 0.85rem; +} + +.pc-compare-table thead th { + background: rgba(248, 250, 252, 0.9); + position: sticky; + top: 0; +} + +.pc-compare-table tbody th { + color: var(--muted); + font-weight: 600; + width: 220px; +} + +.pc-animate { + animation: none; +} + +@keyframes rise { + from { + opacity: 0; + transform: translateY(18px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@media (max-width: 1100px) { + .pc-hero { + grid-template-columns: 1fr; + } + + .pc-hero-home { + grid-template-columns: 1fr; + } + + .pc-header-center { + order: 3; + width: 100%; + } + + .pc-header-icons { + order: 2; + } + + .pc-search input { + width: 340px; + max-width: 100%; + } + + .pc-product-carousel-track { + grid-auto-columns: calc((100% - 36px) / 3); + } +} + +@media (max-width: 900px) { + .pc-grid-3 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .pc-grid-4 { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .pc-admin-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .pc-products-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .pc-home-sliders { + grid-template-columns: 1fr; + } + + .pc-catalog-layout { + grid-template-columns: 1fr; + } + + .pc-admin-chat-layout { + grid-template-columns: 1fr; + } + + .pc-admin-chat-list { + max-height: 300px; + } + + .pc-admin-chat-panel { + min-height: 0; + } + + .pc-cart-layout { + grid-template-columns: 1fr; + } + + .pc-footer-columns { + grid-template-columns: repeat(2, minmax(0, 1fr)); + min-width: 0; + } + + .pc-product-carousel-track { + grid-auto-columns: calc((100% - 18px) / 2); + } +} + +@media (max-width: 720px) { + body.pc-body { + font-size: 14px; + } + + .pc-shell { + padding: 14px 10px 72px; + } + + .pc-main { + gap: 40px; + margin-top: 20px; + } + + .pc-section { + gap: 8px; + } + + .pc-section-title h2 { + font-size: clamp(1.35rem, 5.6vw, 1.75rem); + margin: 4px 0; + } + + .pc-section-title p { + font-size: 0.84rem; + } + + .pc-card { + padding: 12px; + gap: 8px; + border-radius: 10px; + } + + .pc-card h3 { + font-size: 1rem; + } + + .pc-card p { + font-size: 0.84rem; + } + + .pc-btn { + padding: 8px 12px; + font-size: 0.78rem; + border-radius: 9px; + } + + .pc-icon { + width: 32px; + height: 32px; + border-radius: 10px; + } + + .pc-icon svg { + width: 15px; + height: 15px; + } + + .pc-icon-label { + font-size: 0.66rem; + } + + .pc-product-card { + gap: 10px; + } + + .pc-product-link { + display: block; + -webkit-line-clamp: unset; + -webkit-box-orient: initial; + min-height: 0; + overflow: visible; + } + + .pc-product-card > p { + display: block; + -webkit-line-clamp: unset; + -webkit-box-orient: initial; + min-height: 0; + overflow: visible; + } + + .pc-product-tool { + width: 28px; + height: 28px; + border-radius: 8px; + } + + .pc-product-tool svg { + width: 14px; + height: 14px; + } + + .pc-product-thumb { + width: 56px; + height: 56px; + border-radius: 9px; + } + + .pc-search input { + padding: 8px 40px 8px 12px; + } + + .pc-search-submit { + width: 24px; + height: 24px; + } + + .pc-search-submit svg { + width: 14px; + height: 14px; + } + + .pc-header { + align-items: center; + padding: 8px 10px; + row-gap: 10px; + } + + .pc-header-left { + flex: 1; + min-width: 0; + order: 1; + } + + .pc-logo { + font-size: 0.82rem; + letter-spacing: 0.08em; + } + + .pc-catalog-btn { + display: none; + } + + .pc-hamburger { + display: inline-flex; + order: 2; + margin-left: auto; + } + + .pc-mobile-menu-head { + display: none; + width: 100%; + } + + .pc-header-center { + display: none; + } + + .pc-header-icons { + display: none; + } + + .pc-header-nav { + display: none; + order: 5; + margin-top: 0; + padding-top: 0; + border-top: 0; + } + + .pc-mobile-menu-toggle:checked + .pc-hamburger { + display: none; + } + + .pc-mobile-menu-toggle:checked ~ .pc-mobile-menu-head { + display: flex; + justify-content: flex-end; + align-items: center; + order: 1; + padding-bottom: 8px; + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + } + + .pc-mobile-menu-toggle:checked ~ .pc-header-left { + order: 2; + width: 100%; + flex: 0 0 100%; + } + + .pc-mobile-menu-toggle:checked ~ .pc-header-center { + display: flex; + flex-direction: column; + gap: 10px; + order: 3; + width: 100%; + padding-top: 0; + border-top: 0; + justify-content: flex-start; + } + + .pc-mobile-menu-toggle:checked ~ .pc-header-icons { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + order: 4; + width: 100%; + } + + .pc-mobile-menu-toggle:checked ~ .pc-header-nav { + display: grid; + grid-template-columns: 1fr; + gap: 6px; + width: 100%; + max-height: none; + opacity: 1; + overflow: visible; + pointer-events: auto; + } + + .pc-mobile-menu-toggle:checked ~ .pc-header-nav .pc-header-nav-link { + width: 100%; + border: 1px solid var(--border); + border-radius: 10px; + padding: 8px 10px; + font-size: 0.8rem; + background: #ffffff; + } + + .pc-search { + width: 100%; + } + + .pc-icon-link { + min-width: 0; + } + + .pc-nav { + width: 100%; + } + + .pc-actions { + width: 100%; + } + + .pc-search input { + width: 100%; + } + + .pc-filters { + padding: 0; + border: 0; + box-shadow: none; + background: transparent; + } + + .pc-category-toolbar-controls { + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + align-items: center; + gap: 8px; + } + + .pc-category-toolbar-controls .pc-sort-form { + width: 100%; + min-width: 0; + } + + .pc-category-toolbar-controls .pc-sort-form label { + display: none; + } + + .pc-category-toolbar-controls .pc-sort-form select { + width: 100%; + min-width: 0; + height: 32px; + padding: 0 10px; + font-size: 0.76rem; + font-weight: 600; + border-radius: 999px; + border: 1px solid var(--border); + background: #ffffff; + color: var(--ink); + } + + .pc-filter-inline-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + border-radius: 999px; + border: 1px solid var(--border); + background: #ffffff; + color: var(--ink); + padding: 7px 9px; + font-size: 0.76rem; + font-weight: 600; + line-height: 1; + white-space: nowrap; + flex-shrink: 0; + width: 100%; + justify-content: center; + height: 32px; + padding: 0 10px; + } + + .pc-filter-inline-toggle svg { + width: 13px; + height: 13px; + fill: currentColor; + } + + .pc-filter-inline-toggle[aria-expanded="true"] { + border-color: rgba(226, 74, 74, 0.36); + background: rgba(226, 74, 74, 0.1); + color: #b23434; + } + + .pc-filter-details > summary { + display: none; + } + + .pc-filter-details > form { + display: none; + } + + .pc-filter-details.is-open > form, + .pc-filter-details[open] > form { + display: block; + margin-top: 8px; + padding: 14px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--card); + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.04); + } + + .pc-filter-details.is-open .pc-filter-title, + .pc-filter-details[open] .pc-filter-title { + display: none; + } + + .pc-product-carousel { + grid-template-columns: 32px minmax(0, 1fr) 32px; + gap: 8px; + } + + .pc-product-carousel-track { + grid-auto-columns: 100%; + gap: 12px; + } + + .pc-tab-labels { + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + margin-bottom: 12px; + } + + .pc-tab-labels::-webkit-scrollbar { + display: none; + } + + .pc-tab-labels label { + flex: 0 0 auto; + white-space: nowrap; + padding: 7px 11px; + font-size: 0.76rem; + } + + .pc-spec-row { + font-size: 0.76rem; + padding: 8px 10px; + gap: 8px; + } + + .pc-payment-row { + align-items: flex-start; + flex-direction: column; + } + + .pc-grid-2, + .pc-grid-3, + .pc-grid-4 { + grid-template-columns: 1fr; + } + + .pc-category-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + } + + .pc-category-grid .pc-category-card { + padding: 8px; + gap: 8px; + } + + .pc-category-grid .pc-category-title { + font-size: 0.75rem; + line-height: 1.25; + } + + .pc-category-grid .pc-category-count { + font-size: 0.66rem; + line-height: 1.2; + } + + .pc-panel-footer { + flex-direction: column; + align-items: flex-start; + gap: 6px; + } + + .pc-products-grid { + grid-template-columns: 1fr; + } + + .pc-product-hero { + grid-template-columns: 1fr; + } + + .pc-cart-item { + flex-direction: column; + align-items: flex-start; + } + + .pc-cart-item-side { + width: 100%; + align-items: flex-start; + } + + .pc-footer { + padding: 22px; + } + + .pc-footer-top { + flex-direction: column; + } + + .pc-footer-columns { + grid-template-columns: 1fr; + width: 100%; + } + + .pc-footer-bottom { + flex-direction: column; + align-items: flex-start; + } + + .pc-sort-form { + width: 100%; + } + + .pc-sort-form select { + flex: 1; + } + + .pc-pager-list { + gap: 4px; + } + + .pc-pager-link { + min-width: 30px; + height: 30px; + padding: 0 8px; + font-size: 0.74rem; + } + + .pc-home-slider, + .pc-home-slider.is-main { + min-height: 280px; + } + + .pc-home-slide-content { + left: 16px; + right: 16px; + bottom: 16px; + } + + .pc-home-slide-content h2 { + font-size: 1.1rem; + } + + .pc-admin-slide-item { + grid-template-columns: 1fr; + } + + .pc-admin-slide-preview { + width: 100%; + max-width: 220px; + } + + .pc-chat-widget { + right: 12px; + bottom: 12px; + } + + .pc-chat-panel { + width: min(100vw - 16px, 420px); + max-height: calc(100vh - 112px); + } + + .pc-chat-messages { + max-height: min(52vh, 320px); + } +} + +@media (prefers-reduced-motion: reduce) { + .pc-animate { + animation: none; + } +} diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..9964029 --- /dev/null +++ b/resources/js/app.js @@ -0,0 +1,622 @@ +import './bootstrap'; + +const SCROLL_KEY = 'pc-preserve-scroll-y'; +const ALERT_TIMEOUT = 5000; +const NAV_HIDE_OFFSET = 96; +const NAV_SCROLL_DELTA = 8; +const CHAT_OPEN_KEY = 'pc-chat-widget-open'; + +const header = document.querySelector('.pc-header'); +const mobileMenuToggle = document.getElementById('pc-mobile-menu-toggle'); +let lastScrollY = window.scrollY; +let isHeaderTicking = false; + +const updateHeaderNavVisibility = () => { + if (!header) { + return; + } + + const currentScrollY = window.scrollY; + const scrollDelta = currentScrollY - lastScrollY; + const isMobileMenuOpen = mobileMenuToggle instanceof HTMLInputElement && mobileMenuToggle.checked; + + if (currentScrollY <= NAV_HIDE_OFFSET || scrollDelta < -NAV_SCROLL_DELTA || isMobileMenuOpen) { + header.classList.remove('is-nav-hidden'); + } else if (scrollDelta > NAV_SCROLL_DELTA) { + header.classList.add('is-nav-hidden'); + } + + lastScrollY = currentScrollY; +}; + +const initChatWidget = () => { + const widget = document.querySelector('[data-chat-widget="true"]'); + if (!(widget instanceof HTMLElement)) { + return; + } + + const toggle = widget.querySelector('[data-chat-toggle]'); + const close = widget.querySelector('[data-chat-close]'); + const panel = widget.querySelector('[data-chat-panel]'); + const messages = widget.querySelector('[data-chat-messages]'); + const form = widget.querySelector('[data-chat-form]'); + const note = widget.querySelector('[data-chat-note]'); + + if ( + !(toggle instanceof HTMLButtonElement) || + !(panel instanceof HTMLElement) || + !(messages instanceof HTMLElement) || + !(form instanceof HTMLFormElement) + ) { + return; + } + + const fetchUrl = widget.dataset.fetchUrl; + const sendUrl = widget.dataset.sendUrl; + const csrf = widget.dataset.csrf; + const textarea = form.querySelector('textarea[name="message"]'); + const submitButton = form.querySelector('button[type="submit"]'); + + if ( + typeof fetchUrl !== 'string' || + fetchUrl === '' || + typeof sendUrl !== 'string' || + sendUrl === '' || + typeof csrf !== 'string' || + csrf === '' || + !(textarea instanceof HTMLTextAreaElement) || + !(submitButton instanceof HTMLButtonElement) + ) { + return; + } + + let pollTimer = null; + let isFetching = false; + let conversationClosed = false; + const defaultNoteText = note instanceof HTMLElement ? note.textContent ?? '' : ''; + + const applyConversationState = (conversation) => { + conversationClosed = Boolean(conversation && conversation.is_closed); + + if (note instanceof HTMLElement) { + const closedNotice = typeof conversation?.notice === 'string' ? conversation.notice : ''; + note.textContent = conversationClosed && closedNotice !== '' ? closedNotice : defaultNoteText; + } + + submitButton.textContent = conversationClosed ? 'Начать новый чат' : 'Отправить'; + + if (conversationClosed) { + stopPolling(); + } + }; + + const renderMessages = (items) => { + if (!Array.isArray(items)) { + return; + } + + messages.innerHTML = ''; + items.forEach((message) => { + const row = document.createElement('div'); + row.className = `pc-chat-message ${message.sender === 'admin' ? 'is-admin' : 'is-customer'}`; + + const bubble = document.createElement('div'); + bubble.className = 'pc-chat-bubble'; + bubble.textContent = typeof message.body === 'string' ? message.body : ''; + + const meta = document.createElement('span'); + meta.className = 'pc-chat-meta'; + meta.textContent = typeof message.time === 'string' ? message.time : ''; + + row.appendChild(bubble); + row.appendChild(meta); + messages.appendChild(row); + }); + + messages.scrollTop = messages.scrollHeight; + }; + + const fetchMessages = async () => { + if (isFetching) { + return; + } + + isFetching = true; + try { + const response = await fetch(fetchUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }); + + if (!response.ok) { + return; + } + + const payload = await response.json(); + renderMessages(payload.messages ?? []); + applyConversationState(payload.conversation ?? null); + } catch (error) { + // Network errors are expected during reconnects; do nothing. + } finally { + isFetching = false; + } + }; + + const startPolling = () => { + if (pollTimer !== null || conversationClosed) { + return; + } + + pollTimer = window.setInterval(fetchMessages, 4000); + }; + + const stopPolling = () => { + if (pollTimer === null) { + return; + } + + window.clearInterval(pollTimer); + pollTimer = null; + }; + + const openPanel = async () => { + panel.hidden = false; + widget.classList.add('is-open'); + window.localStorage.setItem(CHAT_OPEN_KEY, '1'); + await fetchMessages(); + startPolling(); + window.setTimeout(() => textarea.focus(), 80); + }; + + const closePanel = () => { + panel.hidden = true; + widget.classList.remove('is-open'); + window.localStorage.removeItem(CHAT_OPEN_KEY); + stopPolling(); + }; + + toggle.addEventListener('click', () => { + if (widget.classList.contains('is-open')) { + closePanel(); + } else { + void openPanel(); + } + }); + + if (close instanceof HTMLButtonElement) { + close.addEventListener('click', closePanel); + } + + form.addEventListener('submit', async (event) => { + event.preventDefault(); + const body = textarea.value.trim(); + if (body === '') { + return; + } + + submitButton.disabled = true; + textarea.disabled = true; + + try { + const response = await fetch(sendUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrf, + }, + body: JSON.stringify({ message: body }), + }); + + if (!response.ok) { + return; + } + + textarea.value = ''; + await fetchMessages(); + startPolling(); + } catch (error) { + // Skip noisy errors and keep the form usable. + } finally { + submitButton.disabled = false; + textarea.disabled = false; + textarea.focus(); + } + }); + + if (window.localStorage.getItem(CHAT_OPEN_KEY) === '1') { + void openPanel(); + } +}; + +const initHomeSliders = () => { + const sliders = document.querySelectorAll('[data-home-slider]'); + if (sliders.length === 0) { + return; + } + + sliders.forEach((slider) => { + const slides = Array.from(slider.querySelectorAll('[data-home-slide]')); + const prevButton = slider.querySelector('[data-home-slider-prev]'); + const nextButton = slider.querySelector('[data-home-slider-next]'); + + if ( + slides.length === 0 || + !(prevButton instanceof HTMLButtonElement) || + !(nextButton instanceof HTMLButtonElement) + ) { + return; + } + + let index = Math.max( + 0, + slides.findIndex((slide) => slide.classList.contains('is-active')), + ); + let timerId = null; + + const setSlide = (newIndex) => { + index = (newIndex + slides.length) % slides.length; + slides.forEach((slide, slideIndex) => { + slide.classList.toggle('is-active', slideIndex === index); + }); + }; + + const runNext = () => setSlide(index + 1); + const runPrev = () => setSlide(index - 1); + + const startAuto = () => { + if (slides.length <= 1 || timerId !== null) { + return; + } + + timerId = window.setInterval(runNext, 5000); + }; + + const stopAuto = () => { + if (timerId === null) { + return; + } + + window.clearInterval(timerId); + timerId = null; + }; + + prevButton.addEventListener('click', () => { + runPrev(); + stopAuto(); + startAuto(); + }); + + nextButton.addEventListener('click', () => { + runNext(); + stopAuto(); + startAuto(); + }); + + slider.addEventListener('mouseenter', stopAuto); + slider.addEventListener('mouseleave', startAuto); + + setSlide(index); + startAuto(); + }); +}; + +const initProductCarousels = () => { + const carousels = document.querySelectorAll('[data-product-carousel]'); + if (carousels.length === 0) { + return; + } + + carousels.forEach((carousel) => { + const track = carousel.querySelector('[data-product-carousel-track]'); + const prevButton = carousel.querySelector('[data-product-carousel-prev]'); + const nextButton = carousel.querySelector('[data-product-carousel-next]'); + + if ( + !(track instanceof HTMLElement) || + !(prevButton instanceof HTMLButtonElement) || + !(nextButton instanceof HTMLButtonElement) + ) { + return; + } + + const updateButtons = () => { + const maxScroll = Math.max(0, track.scrollWidth - track.clientWidth); + if (maxScroll <= 1) { + prevButton.disabled = true; + nextButton.disabled = true; + return; + } + + prevButton.disabled = track.scrollLeft <= 2; + nextButton.disabled = track.scrollLeft >= maxScroll - 2; + }; + + const scrollByItem = (direction) => { + const firstItem = track.querySelector('.pc-product-carousel-item'); + const styles = window.getComputedStyle(track); + const gapValue = styles.columnGap || styles.gap || '0px'; + const gap = Number.parseFloat(gapValue) || 0; + const itemWidth = + firstItem instanceof HTMLElement + ? firstItem.getBoundingClientRect().width + : Math.max(track.clientWidth * 0.9, 220); + const scrollAmount = itemWidth + gap; + + track.scrollBy({ + left: scrollAmount * direction, + behavior: 'smooth', + }); + }; + + prevButton.addEventListener('click', () => { + scrollByItem(-1); + }); + + nextButton.addEventListener('click', () => { + scrollByItem(1); + }); + + track.addEventListener( + 'scroll', + () => { + window.requestAnimationFrame(updateButtons); + }, + { passive: true }, + ); + + window.addEventListener('resize', updateButtons); + updateButtons(); + }); +}; + +const initProductGallery = () => { + const galleries = document.querySelectorAll('[data-product-gallery]'); + if (galleries.length === 0) { + return; + } + + galleries.forEach((gallery) => { + const mainImage = gallery.querySelector('[data-product-gallery-main]'); + if (!(mainImage instanceof HTMLImageElement)) { + return; + } + + const thumbs = Array.from(gallery.querySelectorAll('[data-product-gallery-thumb]')).filter( + (thumb) => thumb instanceof HTMLButtonElement, + ); + + if (thumbs.length === 0) { + return; + } + + let activeIndex = 0; + let touchStartX = null; + let touchStartY = null; + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + const setActiveThumb = (activeThumb) => { + thumbs.forEach((thumb) => { + const isActive = thumb === activeThumb; + thumb.classList.toggle('is-active', isActive); + thumb.setAttribute('aria-pressed', isActive ? 'true' : 'false'); + }); + }; + + const animateMainImage = (direction) => { + if (prefersReducedMotion || typeof mainImage.animate !== 'function') { + return; + } + + const offset = direction === 0 ? 0 : 28 * direction; + mainImage.getAnimations().forEach((animation) => animation.cancel()); + mainImage.animate([ + { + opacity: 0.38, + transform: `translate3d(${offset}px, 0, 0) scale(1.02)`, + filter: 'blur(10px)', + }, + { + opacity: 1, + transform: 'translate3d(0, 0, 0) scale(1)', + filter: 'blur(0)', + }, + ], { + duration: 320, + easing: 'cubic-bezier(0.22, 1, 0.36, 1)', + }); + }; + + const applyImage = (index) => { + const normalizedIndex = ((index % thumbs.length) + thumbs.length) % thumbs.length; + const thumb = thumbs[normalizedIndex]; + if (!(thumb instanceof HTMLButtonElement)) { + return; + } + + const src = (thumb.dataset.imageSrc || '').trim(); + if (src === '') { + return; + } + + const direction = normalizedIndex === activeIndex + ? 0 + : normalizedIndex > activeIndex + ? 1 + : -1; + + activeIndex = normalizedIndex; + + if (mainImage.getAttribute('src') !== src) { + mainImage.setAttribute('src', src); + } + + const alt = (thumb.dataset.imageAlt || '').trim(); + if (alt !== '') { + mainImage.setAttribute('alt', alt); + } + + animateMainImage(direction); + setActiveThumb(thumb); + thumb.scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'nearest', + }); + }; + + thumbs.forEach((thumb, index) => { + thumb.addEventListener('click', () => { + applyImage(index); + }); + }); + + mainImage.addEventListener('touchstart', (event) => { + const touch = event.changedTouches[0]; + if (!touch) { + return; + } + + touchStartX = touch.clientX; + touchStartY = touch.clientY; + }, { passive: true }); + + mainImage.addEventListener('touchend', (event) => { + const touch = event.changedTouches[0]; + if (!touch || touchStartX === null || touchStartY === null) { + return; + } + + const deltaX = touch.clientX - touchStartX; + const deltaY = touch.clientY - touchStartY; + + touchStartX = null; + touchStartY = null; + + if (Math.abs(deltaX) < 36 || Math.abs(deltaX) <= Math.abs(deltaY)) { + return; + } + + if (deltaX < 0) { + applyImage(activeIndex + 1); + } else { + applyImage(activeIndex - 1); + } + }, { passive: true }); + + const initialThumb = thumbs.find((thumb) => thumb.classList.contains('is-active')) ?? thumbs[0]; + if (initialThumb) { + applyImage(thumbs.indexOf(initialThumb)); + } + }); +}; + +const initCategoryFilterToggle = () => { + const toggles = document.querySelectorAll('[data-filter-toggle]'); + if (toggles.length === 0) { + return; + } + + toggles.forEach((toggle) => { + if (!(toggle instanceof HTMLButtonElement)) { + return; + } + + const targetId = toggle.getAttribute('aria-controls'); + if (!targetId) { + return; + } + + const container = document.getElementById(targetId); + if (!(container instanceof HTMLElement)) { + return; + } + + const syncState = () => { + const isOpen = container instanceof HTMLDetailsElement + ? container.open + : container.classList.contains('is-open'); + + toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); + }; + + toggle.addEventListener('click', () => { + if (container instanceof HTMLDetailsElement) { + container.open = !container.open; + } else { + container.classList.toggle('is-open'); + } + + syncState(); + }); + + if (container instanceof HTMLDetailsElement) { + container.addEventListener('toggle', syncState); + } + + syncState(); + }); +}; + +document.addEventListener('submit', (event) => { + const form = event.target; + if (!(form instanceof HTMLFormElement)) { + return; + } + + if (form.dataset.preserveScroll === 'true') { + sessionStorage.setItem(SCROLL_KEY, String(window.scrollY)); + } +}); + +window.addEventListener( + 'scroll', + () => { + if (!header || isHeaderTicking) { + return; + } + + isHeaderTicking = true; + window.requestAnimationFrame(() => { + updateHeaderNavVisibility(); + isHeaderTicking = false; + }); + }, + { passive: true }, +); + +if (mobileMenuToggle instanceof HTMLInputElement) { + mobileMenuToggle.addEventListener('change', () => { + if (mobileMenuToggle.checked) { + header?.classList.remove('is-nav-hidden'); + } + }); +} + +window.addEventListener('pageshow', () => { + const savedScrollY = sessionStorage.getItem(SCROLL_KEY); + if (savedScrollY !== null) { + sessionStorage.removeItem(SCROLL_KEY); + window.requestAnimationFrame(() => { + window.scrollTo(0, Number(savedScrollY) || 0); + updateHeaderNavVisibility(); + }); + } else { + updateHeaderNavVisibility(); + } + + document.querySelectorAll('.pc-alert').forEach((alert) => { + window.setTimeout(() => { + alert.classList.add('is-hiding'); + window.setTimeout(() => alert.remove(), 260); + }, ALERT_TIMEOUT); + }); +}); + +initChatWidget(); +initHomeSliders(); +initProductCarousels(); +initProductGallery(); +initCategoryFilterToggle(); diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js new file mode 100644 index 0000000..5f1390b --- /dev/null +++ b/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/resources/views/admin/auth/login.blade.php b/resources/views/admin/auth/login.blade.php new file mode 100644 index 0000000..fec0919 --- /dev/null +++ b/resources/views/admin/auth/login.blade.php @@ -0,0 +1,31 @@ +@extends('layouts.shop') + +@section('content') +
+
+

Вход в админку

+

Доступ только для администраторов.

+
+ +
+ @csrf + + + + + +
+
+@endsection diff --git a/resources/views/admin/categories/_form.blade.php b/resources/views/admin/categories/_form.blade.php new file mode 100644 index 0000000..9e4da2b --- /dev/null +++ b/resources/views/admin/categories/_form.blade.php @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/resources/views/admin/categories/create.blade.php b/resources/views/admin/categories/create.blade.php new file mode 100644 index 0000000..b3e7653 --- /dev/null +++ b/resources/views/admin/categories/create.blade.php @@ -0,0 +1,22 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Категории', 'url' => route('admin.categories.index')], + ['label' => 'Новая категория', 'url' => null], + ], + ]) + +
+
+

Новая категория

+
+ +
+ @csrf + @include('admin.categories._form', ['submitLabel' => 'Создать']) +
+
+@endsection diff --git a/resources/views/admin/categories/edit.blade.php b/resources/views/admin/categories/edit.blade.php new file mode 100644 index 0000000..9a2b4de --- /dev/null +++ b/resources/views/admin/categories/edit.blade.php @@ -0,0 +1,23 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Категории', 'url' => route('admin.categories.index')], + ['label' => 'Редактирование', 'url' => null], + ], + ]) + +
+
+

Редактирование категории

+
+ +
+ @csrf + @method('put') + @include('admin.categories._form', ['category' => $category, 'submitLabel' => 'Сохранить']) +
+
+@endsection diff --git a/resources/views/admin/categories/index.blade.php b/resources/views/admin/categories/index.blade.php new file mode 100644 index 0000000..3417db5 --- /dev/null +++ b/resources/views/admin/categories/index.blade.php @@ -0,0 +1,45 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Категории', 'url' => null], + ], + ]) + +
+
+
+

Категории

+
+ Добавить категорию +
+ +
+ @if ($categories->isEmpty()) +

Категории пока не созданы.

+ @else + + @endif +
+ +
+ {{ $categories->links() }} +
+
+@endsection diff --git a/resources/views/admin/chats/index.blade.php b/resources/views/admin/chats/index.blade.php new file mode 100644 index 0000000..27b18bc --- /dev/null +++ b/resources/views/admin/chats/index.blade.php @@ -0,0 +1,205 @@ +@extends('layouts.shop') + +@section('meta_title', 'Чаты с клиентами') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Чаты', 'url' => null], + ], + ]) + +
+
+

Чаты с клиентами

+

Отвечайте клиентам прямо из админки.

+
+ + @if ($conversations->isEmpty()) +
+

Пока нет сообщений от клиентов.

+
+ @else +
+ + +
+ @if ($selectedConversation) + @php + $isClosed = $selectedConversation->status === \App\Models\ChatConversation::STATUS_CLOSED; + @endphp +
+
+
+

{{ $selectedConversation->display_name }}

+

{{ $selectedConversation->user?->email ?: 'Гость' }}

+

{{ $isClosed ? 'Чат закрыт' : 'Чат открыт' }}

+
+
+
+ @csrf + @method('patch') + + +
+
+ @csrf + @method('delete') + +
+
+
+ +
+ +
+ + +
+
+ @else +

Выберите чат слева.

+ @endif +
+
+ +
+ {{ $conversations->links() }} +
+ @endif +
+@endsection + +@if ($selectedConversation) + @push('scripts') + + @endpush +@endif diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php new file mode 100644 index 0000000..439f22a --- /dev/null +++ b/resources/views/admin/dashboard.blade.php @@ -0,0 +1,63 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => null], + ], + ]) + +
+
+

Панель администратора

+

Управление товарами, категориями, заказами, чатами и слайдерами.

+
+ +
+
+

Категории

+

{{ $stats['categories'] }}

+
+
+

Товары

+

{{ $stats['products'] }}

+
+
+

Заказы

+

{{ $stats['orders'] }}

+
+
+

Выручка

+

{{ number_format($stats['revenue'], 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}

+
+
+ + + +
+

Последние заказы

+ @if ($recentOrders->isEmpty()) +

Пока нет заказов.

+ @else + + @endif +
+
+@endsection diff --git a/resources/views/admin/home-slides/_form.blade.php b/resources/views/admin/home-slides/_form.blade.php new file mode 100644 index 0000000..c65368d --- /dev/null +++ b/resources/views/admin/home-slides/_form.blade.php @@ -0,0 +1,71 @@ + + + + + + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +@if (!empty($slide?->image_url)) +
+
+ Текущее изображение. Загрузите новое, если хотите заменить. +
+@endif + + + + diff --git a/resources/views/admin/home-slides/create.blade.php b/resources/views/admin/home-slides/create.blade.php new file mode 100644 index 0000000..fe57337 --- /dev/null +++ b/resources/views/admin/home-slides/create.blade.php @@ -0,0 +1,22 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Слайдеры главной', 'url' => route('admin.home-slides.index')], + ['label' => 'Новый слайд', 'url' => null], + ], + ]) + +
+
+

Новый слайд

+
+ +
+ @csrf + @include('admin.home-slides._form', ['submitLabel' => 'Создать']) +
+
+@endsection diff --git a/resources/views/admin/home-slides/edit.blade.php b/resources/views/admin/home-slides/edit.blade.php new file mode 100644 index 0000000..0647d0c --- /dev/null +++ b/resources/views/admin/home-slides/edit.blade.php @@ -0,0 +1,23 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Слайдеры главной', 'url' => route('admin.home-slides.index')], + ['label' => 'Редактирование', 'url' => null], + ], + ]) + +
+
+

Редактирование слайда

+
+ +
+ @csrf + @method('put') + @include('admin.home-slides._form', ['slide' => $slide, 'submitLabel' => 'Сохранить']) +
+
+@endsection diff --git a/resources/views/admin/home-slides/index.blade.php b/resources/views/admin/home-slides/index.blade.php new file mode 100644 index 0000000..82f16fa --- /dev/null +++ b/resources/views/admin/home-slides/index.blade.php @@ -0,0 +1,57 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Слайдеры главной', 'url' => null], + ], + ]) + +
+
+
+

Слайдеры главной страницы

+

Загружайте баннеры для блока 2/3 и 1/3 на первом экране.

+
+ +
+ +
+ @foreach ($zoneLabels as $zone => $zoneLabel) + @php + $zoneSlides = $slides->get($zone, collect()); + @endphp +
+

{{ $zoneLabel }}

+ @if ($zoneSlides->isEmpty()) +

Слайды пока не добавлены.

+ @else + + @endif +
+ @endforeach +
+
+@endsection diff --git a/resources/views/admin/orders/index.blade.php b/resources/views/admin/orders/index.blade.php new file mode 100644 index 0000000..29d193b --- /dev/null +++ b/resources/views/admin/orders/index.blade.php @@ -0,0 +1,35 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Заказы', 'url' => null], + ], + ]) + +
+
+

Заказы

+
+ +
+ @if ($orders->isEmpty()) +

Заказов пока нет.

+ @else + + @endif +
+ +
+ {{ $orders->links() }} +
+
+@endsection diff --git a/resources/views/admin/orders/show.blade.php b/resources/views/admin/orders/show.blade.php new file mode 100644 index 0000000..92131ed --- /dev/null +++ b/resources/views/admin/orders/show.blade.php @@ -0,0 +1,68 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Заказы', 'url' => route('admin.orders.index')], + ['label' => 'Заказ #' . $order->id, 'url' => null], + ], + ]) + +
+
+

Заказ #{{ $order->id }}

+
+ +
+
+

Статус

+
+ @csrf + @method('put') + + +
+
+ +
+

Покупатель

+

{{ $order->customer_name }}

+

{{ $order->email }}

+

Оплата: {{ $order->payment_method_label }}

+ @if ($order->phone) +

{{ $order->phone }}

+ @endif + @if ($order->address) +

{{ $order->address }}

+ @endif + @if ($order->comment) +

Комментарий: {{ $order->comment }}

+ @endif +
+
+ +
+

Состав заказа

+ +
+ Итого + {{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }} +
+
+
+@endsection diff --git a/resources/views/admin/products/_form.blade.php b/resources/views/admin/products/_form.blade.php new file mode 100644 index 0000000..a17139d --- /dev/null +++ b/resources/views/admin/products/_form.blade.php @@ -0,0 +1,219 @@ +@php + $currentProduct = $product ?? null; + $selectedCategoryId = (int) old('category_id', $currentProduct->category_id ?? 0); + $specValues = old('specs', $currentProduct->specs ?? []); + $selectedGalleryPathsForRemoval = collect(old('remove_gallery_paths', [])) + ->filter(fn ($path) => is_string($path) && trim($path) !== '') + ->map(fn (string $path) => trim($path)) + ->unique() + ->values() + ->all(); + $galleryItems = collect((array) ($currentProduct->gallery_paths ?? [])) + ->filter(fn ($path) => is_string($path) && trim($path) !== '') + ->map(fn (string $path) => trim($path)) + ->unique() + ->filter(fn (string $path) => $path !== ($currentProduct->image_path ?? null)) + ->map(function (string $path) { + $url = $path; + + if (!\Illuminate\Support\Str::startsWith($path, ['http://', 'https://', '/'])) { + $url = \Illuminate\Support\Str::startsWith($path, 'uploads/') + ? asset($path) + : '/storage/' . ltrim($path, '/'); + } + + return [ + 'path' => $path, + 'url' => $url, + ]; + }) + ->values() + ->all(); +@endphp + + + + + + + + + + + + + +
+ + +
+ + + + + + + +@if (!empty($currentProduct?->image_url)) +
+ {{ $currentProduct->name }} + Текущее изображение. Загрузите новое, чтобы заменить. + +
+@endif + +@if ($galleryItems !== []) + +@endif + +
+

Характеристики категории

+
+
+ + + + + + diff --git a/resources/views/admin/products/create.blade.php b/resources/views/admin/products/create.blade.php new file mode 100644 index 0000000..589c4c4 --- /dev/null +++ b/resources/views/admin/products/create.blade.php @@ -0,0 +1,22 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Товары', 'url' => route('admin.products.index')], + ['label' => 'Новый товар', 'url' => null], + ], + ]) + +
+
+

Новый товар

+
+ +
+ @csrf + @include('admin.products._form', ['submitLabel' => 'Создать']) +
+
+@endsection diff --git a/resources/views/admin/products/edit.blade.php b/resources/views/admin/products/edit.blade.php new file mode 100644 index 0000000..05cc8bc --- /dev/null +++ b/resources/views/admin/products/edit.blade.php @@ -0,0 +1,23 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Товары', 'url' => route('admin.products.index')], + ['label' => 'Редактирование', 'url' => null], + ], + ]) + +
+
+

Редактирование товара

+
+ +
+ @csrf + @method('put') + @include('admin.products._form', ['product' => $product, 'submitLabel' => 'Сохранить']) +
+
+@endsection diff --git a/resources/views/admin/products/index.blade.php b/resources/views/admin/products/index.blade.php new file mode 100644 index 0000000..586982e --- /dev/null +++ b/resources/views/admin/products/index.blade.php @@ -0,0 +1,58 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Админка', 'url' => route('admin.dashboard')], + ['label' => 'Товары', 'url' => null], + ], + ]) + +
+
+
+

Товары

+
+ +
+ +
+

Импорт из CSV

+

Загрузите CSV файл (UTF-8). Поддерживаются колонки category_slug/category_name, name, price, stock и дополнительные характеристики.

+
+ @csrf + + +
+
+ +
+ @if ($products->isEmpty()) +

Товары пока не созданы.

+ @else + + @endif +
+ +
+ {{ $products->links() }} +
+
+@endsection diff --git a/resources/views/components/chat-widget.blade.php b/resources/views/components/chat-widget.blade.php new file mode 100644 index 0000000..0e2ed9d --- /dev/null +++ b/resources/views/components/chat-widget.blade.php @@ -0,0 +1,35 @@ +
+ + + +
diff --git a/resources/views/components/footer.blade.php b/resources/views/components/footer.blade.php new file mode 100644 index 0000000..f23d948 --- /dev/null +++ b/resources/views/components/footer.blade.php @@ -0,0 +1,75 @@ +@php + $companyName = config('shop.company_name', config('app.name')); + $companyDescription = config('shop.company_description'); + $contactPhone = trim((string) config('shop.contact_phone')); + $contactEmail = trim((string) config('shop.contact_email')); + $contactTelegram = trim((string) config('shop.contact_telegram')); + $contactHours = trim((string) config('shop.contact_hours')); + $telegramUrl = ''; + $phoneUrl = ''; + $emailUrl = ''; + + if ($contactPhone !== '') { + $phoneUrl = 'tel:' . preg_replace('/[^\d+]/', '', $contactPhone); + } + + if ($contactEmail !== '') { + $emailUrl = 'mailto:' . $contactEmail; + } + + if ($contactTelegram !== '') { + $telegramUrl = str_starts_with($contactTelegram, 'http://') || str_starts_with($contactTelegram, 'https://') + ? $contactTelegram + : 'https://t.me/' . ltrim($contactTelegram, '@/'); + } +@endphp + + diff --git a/resources/views/components/header.blade.php b/resources/views/components/header.blade.php new file mode 100644 index 0000000..a844540 --- /dev/null +++ b/resources/views/components/header.blade.php @@ -0,0 +1,100 @@ +@php + $favoritesCount = count((array) session('favorites', [])); + $compareCount = count((array) session('compare', [])); + $cartCount = collect((array) session('cart', []))->sum(fn ($quantity) => (int) $quantity); + $companyName = config('shop.company_name', config('app.name')); + $navItems = [ + ['label' => 'Главная', 'route' => route('home'), 'active' => request()->routeIs('home')], + [ + 'label' => 'Каталог', + 'route' => route('catalog.index'), + 'active' => request()->routeIs('catalog.*') || request()->routeIs('products.show') || request()->routeIs('search.index'), + ], + ['label' => 'О нас', 'route' => route('pages.about'), 'active' => request()->routeIs('pages.about')], + [ + 'label' => 'Доставка и оплата', + 'route' => route('pages.shipping-payment'), + 'active' => request()->routeIs('pages.shipping-payment'), + ], + ['label' => 'Контакты', 'route' => route('pages.contacts'), 'active' => request()->routeIs('pages.contacts')], + ]; +@endphp + +
+ + +
+ +
+ +
+ +
+ + +
diff --git a/resources/views/layouts/shop.blade.php b/resources/views/layouts/shop.blade.php new file mode 100644 index 0000000..0251d89 --- /dev/null +++ b/resources/views/layouts/shop.blade.php @@ -0,0 +1,146 @@ + + + + @php + $siteName = config('seo.site_name', config('app.name', 'PC Shop')); + $metaTitleRaw = trim($__env->yieldContent('meta_title')); + $metaTitle = $metaTitleRaw !== '' ? "{$metaTitleRaw} - {$siteName}" : config('seo.default_title', $siteName); + $metaDescription = trim($__env->yieldContent('meta_description')) ?: config('seo.default_description'); + $metaKeywords = trim($__env->yieldContent('meta_keywords')) ?: config('seo.default_keywords'); + $metaCanonical = trim($__env->yieldContent('meta_canonical')) ?: url()->current(); + $metaOgType = trim($__env->yieldContent('meta_og_type')) ?: 'website'; + $request = request(); + $defaultNoIndex = $request->is('admin*') + || $request->routeIs( + 'favorites.*', + 'compare.*', + 'cart.*', + 'checkout.*', + 'account*', + 'login', + 'login.attempt', + 'register', + 'register.store' + ); + $metaRobots = trim($__env->yieldContent('meta_robots')) ?: ($defaultNoIndex ? 'noindex,nofollow' : 'index,follow'); + $metaImagePath = trim($__env->yieldContent('meta_image')) ?: config('seo.default_image'); + $metaImage = str_starts_with($metaImagePath, 'http://') || str_starts_with($metaImagePath, 'https://') + ? $metaImagePath + : url($metaImagePath); + $metaImageAlt = trim($__env->yieldContent('meta_image_alt')) ?: $metaTitle; + $siteUrl = url('/'); + $companyName = config('shop.company_name', $siteName); + $companyDescription = config('shop.company_description', $metaDescription); + $companyPhone = trim((string) config('shop.contact_phone', '')); + $companyEmail = trim((string) config('shop.contact_email', '')); + $companyAddress = trim((string) config('shop.contact_address', '')); + $companyTelegram = trim((string) config('shop.contact_telegram', '')); + if ($companyTelegram !== '' && str_starts_with($companyTelegram, '@')) { + $companyTelegram = 'https://t.me/' . ltrim($companyTelegram, '@'); + } + + $organizationSchema = [ + '@context' => 'https://schema.org', + '@type' => 'Organization', + 'name' => $companyName, + 'url' => $siteUrl, + 'description' => $companyDescription, + ]; + + if ($companyPhone !== '') { + $organizationSchema['telephone'] = $companyPhone; + } + + if ($companyEmail !== '') { + $organizationSchema['email'] = $companyEmail; + } + + if ($companyAddress !== '') { + $organizationSchema['address'] = [ + '@type' => 'PostalAddress', + 'streetAddress' => $companyAddress, + ]; + } + + if ($companyTelegram !== '' && (str_starts_with($companyTelegram, 'http://') || str_starts_with($companyTelegram, 'https://'))) { + $organizationSchema['sameAs'] = [$companyTelegram]; + } + + $websiteSchema = [ + '@context' => 'https://schema.org', + '@type' => 'WebSite', + 'name' => $siteName, + 'url' => $siteUrl, + 'potentialAction' => [ + '@type' => 'SearchAction', + 'target' => route('search.index') . '?q={search_term_string}', + 'query-input' => 'required name=search_term_string', + ], + ]; + @endphp + + + + {{ $metaTitle }} + + + + + + + + + + + + + + + + + + + + + + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @endif + + @unless($request->is('admin*')) + + + @endunless + + @stack('structured_data') + + +
+ + +
+ @if (session('status')) +
{{ session('status') }}
+ @endif + @if ($errors->any()) +
{{ $errors->first() }}
+ @endif + @yield('content') +
+ + @if (!request()->is('admin*')) + + @endif + + +
+ + @stack('scripts') + + diff --git a/resources/views/pages/about.blade.php b/resources/views/pages/about.blade.php new file mode 100644 index 0000000..19ee24b --- /dev/null +++ b/resources/views/pages/about.blade.php @@ -0,0 +1,44 @@ +@extends('layouts.shop') + +@section('meta_title', 'О компании') +@section('meta_description', 'О компании: помощь в подборе комплектующих, консультации и поддержка при сборке ПК.') +@section('meta_keywords', 'о компании, магазин комплектующих, поддержка') +@section('meta_canonical', route('pages.about')) + +@section('content') + @php + $companyName = config('shop.company_name', config('app.name')); + @endphp + + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'О нас', 'url' => null], + ], + ]) + +
+
+

{{ $companyName }} — интернет-магазин компьютерных комплектующих.

+

Мы помогаем подобрать совместимую сборку, оформить заказ и получить технику с понятной поддержкой после покупки.

+
+ +
+

Кто мы

+

{{ $companyName }} работает для тех, кому важно собрать быстрый и надежный ПК без ошибок по совместимости. В каталоге есть комплектующие для домашних, рабочих и игровых систем: процессоры, материнские платы, видеокарты, память, накопители, блоки питания, корпуса, системы охлаждения, ноутбуки и периферия.

+

Мы делаем акцент на понятном выборе: категории с фильтрами, сравнение товаров, избранное, корзина и личный кабинет с историей заказов. Это помогает быстрее принять решение и не потерять важные позиции при подборе сборки.

+ +

Как мы помогаем клиентам

+
    +
  • Проверяем ключевые характеристики и совместимость комплектующих.
  • +
  • Подсказываем оптимальные варианты под бюджет и задачи.
  • +
  • Сопровождаем заказ от оформления до получения.
  • +
  • Объясняем условия доставки, оплаты, возврата и гарантии простым языком.
  • +
+ +

Наш подход

+

Для нас важны прозрачность и сервис: актуальные цены, понятные характеристики и честная обратная связь. Мы стремимся, чтобы покупка техники была удобной как для новичков, так и для опытных пользователей, которые собирают ПК самостоятельно.

+

Если вам нужна консультация перед покупкой, команда {{ $companyName }} поможет подобрать комплектующие и предложит сбалансированные варианты под ваши задачи.

+
+
+@endsection diff --git a/resources/views/pages/contacts.blade.php b/resources/views/pages/contacts.blade.php new file mode 100644 index 0000000..3170d44 --- /dev/null +++ b/resources/views/pages/contacts.blade.php @@ -0,0 +1,81 @@ +@extends('layouts.shop') + +@section('meta_title', 'Контакты') +@section('meta_description', 'Контакты магазина: телефон, email, адрес и часы работы поддержки.') +@section('meta_keywords', 'контакты магазина, телефон, email, адрес') +@section('meta_canonical', route('pages.contacts')) + +@section('content') + @php + $companyName = config('shop.company_name', config('app.name')); + $contactPhone = trim((string) config('shop.contact_phone')); + $contactEmail = trim((string) config('shop.contact_email')); + $contactTelegram = trim((string) config('shop.contact_telegram')); + $contactHours = trim((string) config('shop.contact_hours')); + $telegramUrl = ''; + $phoneUrl = ''; + $emailUrl = ''; + + if ($contactPhone !== '') { + $phoneUrl = 'tel:' . preg_replace('/[^\d+]/', '', $contactPhone); + } + + if ($contactEmail !== '') { + $emailUrl = 'mailto:' . $contactEmail; + } + + if ($contactTelegram !== '') { + $telegramUrl = str_starts_with($contactTelegram, 'http://') || str_starts_with($contactTelegram, 'https://') + ? $contactTelegram + : 'https://t.me/' . ltrim($contactTelegram, '@/'); + } + @endphp + + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Контакты', 'url' => null], + ], + ]) + +
+
+

Поможем с вашей сборкой.

+

Оставьте заявку — поможем подобрать комплектующие и ответим по доставке.

+
+
+
+
Поддержка
+

{{ $companyName }} — поддержка клиентов

+ @if ($phoneUrl !== '') +

Телефон: {{ $contactPhone }}

+ @endif + @if ($emailUrl !== '') +

Почта: {{ $contactEmail }}

+ @endif + @if ($telegramUrl !== '') +

Telegram: {{ $contactTelegram }}

+ @endif + @if ($contactHours !== '') +

Часы: {{ $contactHours }}

+ @endif +
+
+ @csrf + + + + +
+
+
+@endsection diff --git a/resources/views/pages/shipping-payment.blade.php b/resources/views/pages/shipping-payment.blade.php new file mode 100644 index 0000000..186ad93 --- /dev/null +++ b/resources/views/pages/shipping-payment.blade.php @@ -0,0 +1,42 @@ +@extends('layouts.shop') + +@section('meta_title', 'Доставка и оплата') +@section('meta_description', 'Условия доставки и способы оплаты заказов в интернет-магазине комплектующих.') +@section('meta_keywords', 'доставка, оплата, условия заказа') +@section('meta_canonical', route('pages.shipping-payment')) + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Доставка и оплата', 'url' => null], + ], + ]) + +
+
+

Быстрая доставка и удобная оплата.

+

Выбирайте курьера или доставку по времени с безопасной оплатой.

+
+
+
+
Доставка
+

Варианты доставки

+
    +
  • День в день в пределах города
  • +
  • 1-3 дня по стране
  • +
  • Онлайн‑треккинг
  • +
+
+
+
Оплата
+

Способы оплаты

+
    +
  • Оплата банковской картой
  • +
  • Безналичный расчет для юрлиц
  • +
  • Подтверждение оплаты в личном кабинете
  • +
+
+
+
+@endsection diff --git a/resources/views/partials/breadcrumbs.blade.php b/resources/views/partials/breadcrumbs.blade.php new file mode 100644 index 0000000..805b053 --- /dev/null +++ b/resources/views/partials/breadcrumbs.blade.php @@ -0,0 +1,50 @@ +@if (!empty($items)) + @php + $breadcrumbSchemaItems = collect($items) + ->values() + ->map(function ($item, $index) { + if (!is_array($item)) { + return null; + } + + $label = trim((string) ($item['label'] ?? '')); + if ($label === '') { + return null; + } + + $url = !empty($item['url']) ? (string) $item['url'] : url()->current(); + + return [ + '@type' => 'ListItem', + 'position' => $index + 1, + 'name' => $label, + 'item' => $url, + ]; + }) + ->filter() + ->values() + ->all(); + $breadcrumbSchema = [ + '@context' => 'https://schema.org', + '@type' => 'BreadcrumbList', + 'itemListElement' => $breadcrumbSchemaItems, + ]; + @endphp + + + + @if (!empty($breadcrumbSchemaItems)) + + @endif +@endif diff --git a/resources/views/partials/home-slider.blade.php b/resources/views/partials/home-slider.blade.php new file mode 100644 index 0000000..b2f080f --- /dev/null +++ b/resources/views/partials/home-slider.blade.php @@ -0,0 +1,49 @@ +@php + $slides = $slides ?? collect(); + $isSingle = $slides->count() <= 1; +@endphp + +
+
+ @forelse ($slides as $slide) + @php + $hasTitle = $slide->show_title && !empty($slide->title); + $hasSubtitle = $slide->show_subtitle && !empty($slide->subtitle); + $hasButton = $slide->show_button && !empty($slide->button_text) && !empty($slide->button_url); + $hasContent = $hasTitle || $hasSubtitle || $hasButton; + $altText = $hasTitle ? $slide->title : 'Слайд на главной странице'; + @endphp +
+ {{ $altText }} +
+ @if ($hasContent) +
+ @if ($hasTitle) +

{{ $slide->title }}

+ @endif + @if ($hasSubtitle) +

{{ $slide->subtitle }}

+ @endif + @if ($hasButton) + {{ $slide->button_text }} + @endif +
+ @endif +
+ @empty +
+
+
+

{{ $fallbackTitle ?? 'Собирайте ПК быстрее' }}

+

{{ $fallbackText ?? 'Загрузите баннеры в админке, чтобы вывести акции и подборки товаров на главной странице.' }}

+ @if (!empty($fallbackUrl)) + {{ $fallbackButton ?? 'Открыть каталог' }} + @endif +
+
+ @endforelse +
+ + + +
diff --git a/resources/views/partials/pagination.blade.php b/resources/views/partials/pagination.blade.php new file mode 100644 index 0000000..66f562f --- /dev/null +++ b/resources/views/partials/pagination.blade.php @@ -0,0 +1,41 @@ +@if ($paginator->hasPages()) + +@endif diff --git a/resources/views/partials/payment-requisites.blade.php b/resources/views/partials/payment-requisites.blade.php new file mode 100644 index 0000000..33354dc --- /dev/null +++ b/resources/views/partials/payment-requisites.blade.php @@ -0,0 +1,47 @@ +@php + $paymentBank = config('shop.payment_bank'); + $paymentCardHolder = config('shop.payment_card_holder'); + $paymentCardNumber = config('shop.payment_card_number'); + $telegram = trim((string) config('shop.contact_telegram')); + $telegramUrl = ''; + + if ($telegram !== '') { + $telegramUrl = str_starts_with($telegram, 'http://') || str_starts_with($telegram, 'https://') + ? $telegram + : 'https://t.me/' . ltrim($telegram, '@/'); + } +@endphp + +
+

{{ $title ?? 'Оплата по реквизитам' }}

+ + @isset($amount) +

Сумма к оплате: {{ number_format((float) $amount, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}

+ @endisset + +
+
+ Банк + {{ $paymentBank }} +
+
+ Получатель + {{ $paymentCardHolder }} +
+
+ Номер карты + {{ $paymentCardNumber }} +
+
+ + @isset($purpose) +

Назначение платежа: {{ $purpose }}

+ @endisset + + @if (!empty($showHelp)) +

После оплаты отправьте чек в поддержку для подтверждения заказа.

+ @if ($telegramUrl !== '') +

Telegram: {{ $telegram }}

+ @endif + @endif +
diff --git a/resources/views/partials/product-card.blade.php b/resources/views/partials/product-card.blade.php new file mode 100644 index 0000000..b9b46fa --- /dev/null +++ b/resources/views/partials/product-card.blade.php @@ -0,0 +1,60 @@ +@php + $favoriteIds = array_map('intval', (array) session('favorites', [])); + $compareIds = array_map('intval', (array) session('compare', [])); + $isFavorite = in_array($product->id, $favoriteIds, true); + $isCompared = in_array($product->id, $compareIds, true); + $cartItems = (array) session('cart', []); + $isInCart = isset($cartItems[$product->id]); +@endphp + +
+
+
+
+ @csrf + +
+
+ @csrf + +
+
+ @if (!empty($product->image_url)) + {{ $product->name }} + @else + + @endif +
+

+ {{ $product->name }} +

+ @if (!empty($product->short_description)) +

{{ $product->short_description }}

+ @endif +
+ {{ number_format($product->price, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }} + @if (!empty($product->old_price)) + {{ number_format($product->old_price, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }} + @endif +
+
+ @if ($product->stock > 0) +
+ @csrf + +
+ @else + + @endif +
+
diff --git a/resources/views/partials/product-carousel.blade.php b/resources/views/partials/product-carousel.blade.php new file mode 100644 index 0000000..909df3c --- /dev/null +++ b/resources/views/partials/product-carousel.blade.php @@ -0,0 +1,28 @@ +@php + $items = $products ?? collect(); +@endphp + +
+
+

{{ $title ?? 'Товары' }}

+ @if (!empty($description)) +

{{ $description }}

+ @endif +
+ + @if ($items->isEmpty()) +
{{ $emptyText ?? 'Пока нет товаров.' }}
+ @else + + @endif +
diff --git a/resources/views/seo/sitemap.blade.php b/resources/views/seo/sitemap.blade.php new file mode 100644 index 0000000..4c47ad1 --- /dev/null +++ b/resources/views/seo/sitemap.blade.php @@ -0,0 +1,12 @@ +{!! '' !!} + +@foreach ($urls as $url) + + {{ $url['loc'] }} + {{ $url['lastmod'] }} + {{ $url['changefreq'] }} + {{ $url['priority'] }} + +@endforeach + + diff --git a/resources/views/shop/account.blade.php b/resources/views/shop/account.blade.php new file mode 100644 index 0000000..591f0d6 --- /dev/null +++ b/resources/views/shop/account.blade.php @@ -0,0 +1,58 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Личный кабинет', 'url' => null], + ], + ]) + +
+
+

Личный кабинет

+

Управляйте данными профиля и просматривайте историю заказов.

+
+ +
+
+
+ @csrf +

Данные аккаунта

+ + +
+ +
+
+
+ @csrf + +
+
+ +
+

Мои заказы

+ @if ($orders->isEmpty()) +

Пока нет заказов.

+ Перейти в каталог + @else + + @endif +
+
+
+@endsection diff --git a/resources/views/shop/auth/login.blade.php b/resources/views/shop/auth/login.blade.php new file mode 100644 index 0000000..d08ab99 --- /dev/null +++ b/resources/views/shop/auth/login.blade.php @@ -0,0 +1,41 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Вход', 'url' => null], + ], + ]) + +
+
+

Вход

+

Введите email и пароль для доступа к заказам и профилю.

+
+ +
+ @csrf + + + + + +
+
+@endsection diff --git a/resources/views/shop/auth/register.blade.php b/resources/views/shop/auth/register.blade.php new file mode 100644 index 0000000..f45c376 --- /dev/null +++ b/resources/views/shop/auth/register.blade.php @@ -0,0 +1,45 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Регистрация', 'url' => null], + ], + ]) + +
+
+

Регистрация

+

Создайте аккаунт, чтобы отслеживать заказы и сохранять избранное.

+
+ +
+ @csrf + + + + + +
+ + Уже есть аккаунт +
+
+
+@endsection diff --git a/resources/views/shop/cart.blade.php b/resources/views/shop/cart.blade.php new file mode 100644 index 0000000..7bada11 --- /dev/null +++ b/resources/views/shop/cart.blade.php @@ -0,0 +1,69 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Корзина', 'url' => null], + ], + ]) + +
+
+

Товары в корзине

+
+ + @if ($items->isEmpty()) +
+

Корзина пустая

+

Добавьте товары из каталога, чтобы оформить заказ.

+ Перейти в каталог +
+ @else +
+
+ @foreach ($items as $item) + @php($product = $item['product']) +
+
+

+ {{ $product->name }} +

+ @if ($product->short_description) +

{{ $product->short_description }}

+ @endif +
+
+
+ @csrf + @method('patch') + + +
+ {{ number_format($item['subtotal'], 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }} +
+ @csrf + @method('delete') + +
+
+
+ @endforeach +
+ + +
+ @endif +
+@endsection diff --git a/resources/views/shop/catalog.blade.php b/resources/views/shop/catalog.blade.php new file mode 100644 index 0000000..c0b1bf7 --- /dev/null +++ b/resources/views/shop/catalog.blade.php @@ -0,0 +1,92 @@ +@extends('layouts.shop') + +@php + $searchQuery = trim((string) request('q', '')); + $hasCatalogQuery = $searchQuery !== '' || request()->filled('category') || request()->filled('page'); + $catalogCategoryList = collect($categories ?? []) + ->values() + ->map(fn ($category, $index) => [ + '@type' => 'ListItem', + 'position' => $index + 1, + 'url' => route('catalog.category', $category), + 'name' => $category->name, + ]) + ->all(); + $catalogSchema = [ + '@context' => 'https://schema.org', + '@type' => 'CollectionPage', + 'name' => 'Каталог товаров', + 'url' => route('catalog.index'), + 'description' => 'Каталог компьютерных комплектующих и техники.', + 'mainEntity' => [ + '@type' => 'ItemList', + 'numberOfItems' => count($catalogCategoryList), + 'itemListElement' => $catalogCategoryList, + ], + ]; +@endphp +@section('meta_title', $searchQuery !== '' ? "Поиск: {$searchQuery}" : 'Каталог товаров') +@section( + 'meta_description', + $searchQuery !== '' + ? "Результаты поиска по запросу «{$searchQuery}». Подберите нужные комплектующие по наименованию." + : 'Каталог компьютерных комплектующих: процессоры, материнские платы, видеокарты, память, накопители и ноутбуки.' +) +@section('meta_keywords', 'каталог комплектующих, поиск товаров, процессоры, материнские платы, видеокарты') +@section('meta_canonical', route('catalog.index')) +@section('meta_robots', $hasCatalogQuery ? 'noindex,follow' : 'index,follow') + +@push('structured_data') + +@endpush + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Каталог', 'url' => null], + ], + ]) + +
+
+

Категории товаров

+
+ +
+ @forelse ($categories as $category) + + +

{{ $category->name }}

+
+ @empty +
Категории пока не добавлены.
+ @endforelse +
+
+ + @if (request()->filled('q')) +
+
+
+

Результаты по запросу: "{{ request('q') }}"

+
+

Найдено: {{ $products->total() }}

+
+ +
+ @forelse ($products as $product) + @include('partials.product-card', ['product' => $product]) + @empty +
По вашему запросу ничего не найдено.
+ @endforelse +
+ +
+ {{ $products->links('partials.pagination') }} +
+
+ @endif +@endsection diff --git a/resources/views/shop/category.blade.php b/resources/views/shop/category.blade.php new file mode 100644 index 0000000..742ac03 --- /dev/null +++ b/resources/views/shop/category.blade.php @@ -0,0 +1,266 @@ +@extends('layouts.shop') + +@php + $hasSeoFilters = request()->filled('q') + || request()->filled('sort') + || request()->filled('page') + || request()->filled('price_from') + || request()->filled('price_to') + || collect((array) request('filters', [])) + ->contains(fn ($value) => is_scalar($value) && trim((string) $value) !== '') + || collect(request()->query()) + ->keys() + ->contains(fn ($key) => is_string($key) && (str_ends_with($key, '_from') || str_ends_with($key, '_to'))); + + $categoryItemList = collect($products->items()) + ->values() + ->map(fn ($product, $index) => [ + '@type' => 'ListItem', + 'position' => $index + 1, + 'url' => route('products.show', $product), + 'name' => $product->name, + ]) + ->all(); + $categorySchema = [ + '@context' => 'https://schema.org', + '@type' => 'CollectionPage', + 'name' => $category->name, + 'url' => route('catalog.category', $category), + 'description' => $category->description ?: 'Категория товаров ' . $category->name, + 'mainEntity' => [ + '@type' => 'ItemList', + 'numberOfItems' => $products->total(), + 'itemListElement' => $categoryItemList, + ], + ]; +@endphp + +@section('meta_title', $category->name) +@section('meta_description', ($category->description ?: 'Товары категории ' . $category->name . '.') . ' Фильтры и сортировка для быстрого подбора.') +@section('meta_keywords', $category->name . ', комплектующие, купить, фильтры товаров') +@section('meta_canonical', route('catalog.category', $category)) +@section('meta_robots', $hasSeoFilters ? 'noindex,follow' : 'index,follow') + +@push('structured_data') + +@endpush + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Каталог', 'url' => route('catalog.index')], + ['label' => $category->name, 'url' => null], + ], + ]) + +
+
+

{{ $category->name }}

+
+ + @php + $activeFilters = collect((array) ($appliedFilters ?? [])) + ->filter(fn ($value) => is_scalar($value) && trim((string) $value) !== '') + ->values(); + + if (request()->filled('price_from') || request()->filled('price_to')) { + $priceFromLabel = trim((string) request('price_from', '')); + $priceToLabel = trim((string) request('price_to', '')); + $activeFilters->push("Цена: {$priceFromLabel} - {$priceToLabel}"); + } + + foreach ((array) ($filters ?? []) as $filter) { + if ((string) ($filter['filter'] ?? 'select') !== 'range') { + continue; + } + + $rangeKey = (string) ($filter['key'] ?? ''); + if ($rangeKey === '') { + continue; + } + + $fromParam = $rangeKey . '_from'; + $toParam = $rangeKey . '_to'; + if (!request()->filled($fromParam) && !request()->filled($toParam)) { + continue; + } + + $fromLabel = trim((string) request($fromParam, '')); + $toLabel = trim((string) request($toParam, '')); + $activeFilters->push(($filter['label'] ?? $rangeKey) . ": {$fromLabel} - {$toLabel}"); + } + @endphp + +
+

Найдено товаров: {{ $products->total() }}

+
+
+ @foreach ((array) ($appliedFilters ?? []) as $key => $value) + @if (is_scalar($value) && trim((string) $value) !== '') + + @endif + @endforeach + @if (request()->filled('price_from')) + + @endif + @if (request()->filled('price_to')) + + @endif + @foreach ((array) ($filters ?? []) as $filter) + @php + $rangeKey = (string) ($filter['key'] ?? ''); + @endphp + @continue($rangeKey === '' || (string) ($filter['filter'] ?? 'select') !== 'range') + @if (request()->filled($rangeKey . '_from')) + + @endif + @if (request()->filled($rangeKey . '_to')) + + @endif + @endforeach + @if (request()->filled('q')) + + @endif + + +
+ +
+
+ @if ($activeFilters->isNotEmpty()) +
+ @foreach ($activeFilters as $key => $value) + {{ $value }} + @endforeach +
+ @endif + +
+ + +
+ @forelse ($products as $product) + @include('partials.product-card', ['product' => $product]) + @empty +
Пока нет товаров в этой категории.
+ @endforelse +
+
+ +
+ {{ $products->links('partials.pagination') }} +
+
+@endsection diff --git a/resources/views/shop/checkout-payment.blade.php b/resources/views/shop/checkout-payment.blade.php new file mode 100644 index 0000000..4552feb --- /dev/null +++ b/resources/views/shop/checkout-payment.blade.php @@ -0,0 +1,69 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Корзина', 'url' => route('cart.index')], + ['label' => 'Данные получателя', 'url' => route('checkout.show')], + ['label' => 'Реквизиты для оплаты', 'url' => null], + ], + ]) + +
+
+

Реквизиты для оплаты

+

Переведите сумму заказа по реквизитам ниже и подтвердите оформление.

+
+ +
+
+

Данные получателя

+

{{ $customer['customer_name'] }}

+

{{ $customer['email'] }}

+ @if (!empty($customer['phone'])) +

{{ $customer['phone'] }}

+ @endif +

{{ $customer['address'] }}

+ @if (!empty($customer['comment'])) +

Комментарий: {{ $customer['comment'] }}

+ @endif + + +
+ + +
+ + @include('partials.payment-requisites', [ + 'amount' => $total, + 'purpose' => 'Номер заказа будет присвоен после подтверждения', + 'showHelp' => true, + ]) + +
+ @csrf + +
+
+@endsection diff --git a/resources/views/shop/checkout-success.blade.php b/resources/views/shop/checkout-success.blade.php new file mode 100644 index 0000000..3880e6b --- /dev/null +++ b/resources/views/shop/checkout-success.blade.php @@ -0,0 +1,32 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Заказ оформлен', 'url' => null], + ], + ]) + +
+
+

Заказ №{{ $order->id }} успешно оформлен

+

Мы приняли заказ в обработку. Статус заказа: {{ $order->status }}.

+

Способ оплаты: {{ $order->payment_method_label }}.

+

Сумма заказа: {{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}.

+ + @include('partials.payment-requisites', [ + 'amount' => $order->total, + 'purpose' => 'Заказ #' . $order->id, + 'showHelp' => true, + ]) + + +
+
+@endsection diff --git a/resources/views/shop/checkout.blade.php b/resources/views/shop/checkout.blade.php new file mode 100644 index 0000000..d39b713 --- /dev/null +++ b/resources/views/shop/checkout.blade.php @@ -0,0 +1,65 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Корзина', 'url' => route('cart.index')], + ['label' => 'Данные получателя', 'url' => null], + ], + ]) + +
+
+

Данные получателя

+

Заполните контакты и перейдите на страницу с реквизитами для оплаты.

+
+ +
+
+ @csrf + + + + + + +
+ + +
+
+@endsection diff --git a/resources/views/shop/compare.blade.php b/resources/views/shop/compare.blade.php new file mode 100644 index 0000000..29a1667 --- /dev/null +++ b/resources/views/shop/compare.blade.php @@ -0,0 +1,65 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Сравнение', 'url' => null], + ], + ]) + +
+
+

Сравнение товаров

+
+ + @if ($products->isEmpty()) +
+

Список сравнения пуст

+

Добавьте товары в сравнение из карточек каталога.

+ Перейти в каталог +
+ @else +
+
+ @csrf + @method('delete') + +
+
+ +
+ @foreach ($products as $product) + @include('partials.product-card', ['product' => $product]) + @endforeach +
+ + @if ($specKeys->isNotEmpty()) +
+
+ + + + + @foreach ($products as $product) + + @endforeach + + + + @foreach ($specKeys as $key) + + + @foreach ($products as $product) + + @endforeach + + @endforeach + +
Характеристика{{ $product->name }}
{{ $specLabels[$key] ?? $key }}{{ data_get($product->specs, $key, '—') }}
+
+
+ @endif + @endif +
+@endsection diff --git a/resources/views/shop/favorites.blade.php b/resources/views/shop/favorites.blade.php new file mode 100644 index 0000000..156b0bc --- /dev/null +++ b/resources/views/shop/favorites.blade.php @@ -0,0 +1,30 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Избранное', 'url' => null], + ], + ]) + +
+
+

Избранные товары

+
+ + @if ($products->isEmpty()) +
+

Список пуст

+

Добавьте товары в избранное из каталога или со страницы товара.

+ Перейти в каталог +
+ @else +
+ @foreach ($products as $product) + @include('partials.product-card', ['product' => $product]) + @endforeach +
+ @endif +
+@endsection diff --git a/resources/views/shop/home.blade.php b/resources/views/shop/home.blade.php new file mode 100644 index 0000000..539b174 --- /dev/null +++ b/resources/views/shop/home.blade.php @@ -0,0 +1,71 @@ +@extends('layouts.shop') + +@section('meta_title', 'Главная') +@section('meta_description', 'Интернет-магазин комплектующих для ПК: процессоры, материнские платы, видеокарты, ноутбуки и периферия.') +@section('meta_keywords', 'интернет-магазин пк, комплектующие, процессоры, видеокарты, ноутбуки') +@section('meta_canonical', route('home')) + +@section('content') +
+ @include('partials.home-slider', [ + 'slides' => $leftSlides, + 'sliderClass' => 'is-main', + 'fallbackTitle' => 'Собирайте ПК быстрее', + 'fallbackText' => 'Процессоры, материнские платы, видеокарты, ноутбуки и периферия в одном каталоге.', + 'fallbackUrl' => route('catalog.index'), + 'fallbackButton' => 'Перейти в каталог', + ]) + @include('partials.home-slider', [ + 'slides' => $rightSlides, + 'sliderClass' => 'is-side', + 'fallbackTitle' => 'Доставка и оплата', + 'fallbackText' => 'Узнайте сроки доставки и способы оплаты заказа.', + 'fallbackUrl' => route('pages.shipping-payment'), + 'fallbackButton' => 'Подробнее', + ]) +
+ +
+
+

Категории

+
+
+ @foreach ($categories as $category) + + +

{{ $category->name }}

+
+ @endforeach +
+
+ + @include('partials.product-carousel', [ + 'title' => 'Популярные товары', + 'products' => $featured, + 'emptyText' => 'Пока нет популярных товаров.', + ]) + + @include('partials.product-carousel', [ + 'title' => 'Новые товары', + 'products' => $newProducts, + 'emptyText' => 'Пока нет новых товаров.', + ]) + +
+ +
Сервис
+

Доставка и оплата

+

Условия доставки, способы оплаты и сроки отправки.

+
+ +
Компания
+

О нас

+

Чем занимаемся и как помогаем выбрать комплектующие.

+
+ +
Поддержка
+

Контакты

+

Свяжитесь с нами для консультации по вашей сборке.

+
+
+@endsection diff --git a/resources/views/shop/order.blade.php b/resources/views/shop/order.blade.php new file mode 100644 index 0000000..d8c9c2f --- /dev/null +++ b/resources/views/shop/order.blade.php @@ -0,0 +1,60 @@ +@extends('layouts.shop') + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Личный кабинет', 'url' => route('account')], + ['label' => 'Заказ #' . $order->id, 'url' => null], + ], + ]) + +
+
+

Заказ #{{ $order->id }}

+

Статус: {{ $order->status }}

+

Способ оплаты: {{ $order->payment_method_label }}

+
+ +
+
+

Состав заказа

+ +
+ Итого + {{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }} +
+
+ +
+

Данные получателя

+

{{ $order->customer_name }}

+

{{ $order->email }}

+ @if ($order->phone) +

{{ $order->phone }}

+ @endif + @if ($order->address) +

{{ $order->address }}

+ @endif + @if ($order->comment) +

Комментарий: {{ $order->comment }}

+ @endif +
+
+ + @if (!in_array($order->status, ['paid', 'shipped', 'completed'], true)) + @include('partials.payment-requisites', [ + 'amount' => $order->total, + 'purpose' => 'Заказ #' . $order->id, + 'showHelp' => true, + ]) + @endif +
+@endsection diff --git a/resources/views/shop/product.blade.php b/resources/views/shop/product.blade.php new file mode 100644 index 0000000..b2097d3 --- /dev/null +++ b/resources/views/shop/product.blade.php @@ -0,0 +1,221 @@ +@extends('layouts.shop') + +@php + $productGallery = $product->gallery_urls; + $productImage = $productGallery[0] ?? ($product->image_url ?: config('seo.default_image')); + $productImageUrl = str_starts_with($productImage, 'http://') || str_starts_with($productImage, 'https://') + ? $productImage + : url($productImage); + $productSchemaImages = array_map( + fn (string $image) => str_starts_with($image, 'http://') || str_starts_with($image, 'https://') + ? $image + : url($image), + $productGallery !== [] ? array_values($productGallery) : [$productImageUrl] + ); + + $manufacturer = trim((string) ($product->specs['manufacturer'] ?? '')); + $conditionRaw = mb_strtolower(trim((string) ($product->specs['condition'] ?? ''))); + $itemCondition = str_contains($conditionRaw, 'б/у') || str_contains($conditionRaw, 'used') + ? 'https://schema.org/UsedCondition' + : 'https://schema.org/NewCondition'; + + $productSchema = [ + '@context' => 'https://schema.org', + '@type' => 'Product', + 'name' => $product->name, + 'description' => $product->short_description ?: ($product->description ?: "Купить {$product->name} по выгодной цене."), + 'sku' => $product->sku ?: null, + 'category' => $product->category?->name, + 'image' => $productSchemaImages, + 'url' => route('products.show', $product), + 'offers' => [ + '@type' => 'Offer', + 'priceCurrency' => config('shop.currency_code', 'RUB'), + 'price' => (string) $product->price, + 'availability' => $product->stock > 0 ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock', + 'itemCondition' => $itemCondition, + 'url' => route('products.show', $product), + ], + ]; + + if ($manufacturer !== '') { + $productSchema['brand'] = [ + '@type' => 'Brand', + 'name' => $manufacturer, + ]; + } +@endphp + +@section('meta_title', $product->name) +@section('meta_description', \Illuminate\Support\Str::limit(strip_tags($product->short_description ?: ($product->description ?: "Купить {$product->name} по выгодной цене.")), 160)) +@section('meta_keywords', $product->name . ', ' . ($product->category?->name ?? 'товар') . ', купить') +@section('meta_canonical', route('products.show', $product)) +@section('meta_image', $productImageUrl) +@section('meta_image_alt', $product->name) +@section('meta_og_type', 'product') + +@push('structured_data') + +@endpush + +@section('content') + @php + $favoriteIds = array_map('intval', (array) session('favorites', [])); + $compareIds = array_map('intval', (array) session('compare', [])); + $isFavorite = in_array($product->id, $favoriteIds, true); + $isCompared = in_array($product->id, $compareIds, true); + $cartItems = (array) session('cart', []); + $isInCart = isset($cartItems[$product->id]); + @endphp + + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Каталог', 'url' => route('catalog.index')], + ['label' => $product->category?->name ?? 'Категория', 'url' => $product->category ? route('catalog.category', $product->category) : null], + ['label' => $product->name, 'url' => null], + ], + ]) + +
+
+ +
+

{{ $product->name }}

+

{{ $product->short_description }}

+
+ @if ($product->sku) + Артикул: {{ $product->sku }} + @endif +
+
+ {{ number_format($product->price, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }} + @if (!empty($product->old_price)) + {{ number_format($product->old_price, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }} + @endif +
+
+ @if ($product->stock > 0) +
+ @csrf + +
+ @else + + @endif +
+ @csrf + +
+
+ @csrf + +
+
+
+
+ +
+ + + + + +
+ + + + +
+ +
+
+
+ @forelse (($product->specs ?? []) as $key => $value) +
+ {{ $specLabels[$key] ?? str_replace('_', ' ', $key) }} + {{ $value }} +
+ @empty +

Характеристики еще не добавлены.

+ @endforelse +
+
+
+

{{ $product->description ?? 'Описание товара будет добавлено позже.' }}

+
+
+
    +
  • Доставка курьером по городу
  • +
  • Самовывоз из пункта выдачи
  • +
  • Отправка по стране 1-3 дня
  • +
+
+
+
    +
  • Оплата картой онлайн
  • +
  • Банковский перевод
  • +
  • Рассрочка на крупные заказы
  • +
+
+
+
+
+ + @if ($related->isNotEmpty()) +
+
+

Что еще посмотреть в этой категории

+
+
+ @foreach ($related as $item) + @include('partials.product-card', ['product' => $item]) + @endforeach +
+
+ @endif +@endsection diff --git a/resources/views/shop/search.blade.php b/resources/views/shop/search.blade.php new file mode 100644 index 0000000..090c56a --- /dev/null +++ b/resources/views/shop/search.blade.php @@ -0,0 +1,112 @@ +@extends('layouts.shop') + +@php + $searchItemList = collect($products->items()) + ->values() + ->map(fn ($product, $index) => [ + '@type' => 'ListItem', + 'position' => $index + 1, + 'url' => route('products.show', $product), + 'name' => $product->name, + ]) + ->all(); + $searchSchema = $searchQuery !== '' + ? [ + '@context' => 'https://schema.org', + '@type' => 'SearchResultsPage', + 'name' => "Результаты поиска: {$searchQuery}", + 'url' => route('search.index', ['q' => $searchQuery]), + 'mainEntity' => [ + '@type' => 'ItemList', + 'numberOfItems' => $products->total(), + 'itemListElement' => $searchItemList, + ], + ] + : null; +@endphp + +@section('meta_title', $searchQuery !== '' ? "Поиск: {$searchQuery}" : 'Поиск товаров') +@section( + 'meta_description', + $searchQuery !== '' + ? "Найденные товары по запросу «{$searchQuery}». Выберите подходящий товар и откройте подробную карточку." + : 'Поиск товаров по наименованию: процессоры, видеокарты, материнские платы, ноутбуки и периферия.' +) +@section('meta_keywords', 'поиск товаров, результаты поиска, комплектующие пк, ноутбуки') +@section('meta_canonical', route('search.index')) +@section('meta_robots', 'noindex,follow') + +@push('structured_data') + @if ($searchSchema !== null) + + @endif +@endpush + +@section('content') + @include('partials.breadcrumbs', [ + 'items' => [ + ['label' => 'Главная', 'url' => route('home')], + ['label' => 'Поиск', 'url' => null], + ], + ]) + +
+
+
+

{{ $searchQuery !== '' ? 'Результаты поиска' : 'Поиск товаров' }}

+

+ @if ($searchQuery !== '') + Запрос: "{{ $searchQuery }}" + @else + Введите название товара, чтобы увидеть найденные позиции. + @endif +

+
+ + @if ($searchQuery !== '') +
+ + + +
+ @endif +
+ +
+ + + @if ($searchQuery !== '') + Очистить + @endif +
+ + @if ($searchQuery === '') +
+ Введите запрос в строку поиска, чтобы открыть список найденных товаров. +
+ @else +

Найдено товаров: {{ $products->total() }}

+ +
+ @forelse ($products as $product) + @include('partials.product-card', ['product' => $product]) + @empty +
По вашему запросу ничего не найдено.
+ @endforelse +
+ +
+ {{ $products->links('partials.pagination') }} +
+ @endif +
+@endsection diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..e6e2096 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,131 @@ +name('sitemap'); +Route::get('/robots.txt', function () { + $content = implode(PHP_EOL, [ + 'User-agent: *', + 'Allow: /', + 'Disallow: /admin', + 'Disallow: /search', + 'Disallow: /favorites', + 'Disallow: /compare', + 'Disallow: /cart', + 'Disallow: /checkout', + 'Disallow: /account', + 'Disallow: /login', + 'Disallow: /register', + 'Sitemap: ' . route('sitemap'), + ]); + + return response($content, 200, ['Content-Type' => 'text/plain; charset=UTF-8']); +})->name('robots'); + +Route::get('/', [ShopController::class, 'home'])->name('home'); + +Route::get('/search', [CatalogController::class, 'search'])->name('search.index'); +Route::get('/catalog', [CatalogController::class, 'index'])->name('catalog.index'); +Route::get('/category/{category:slug}', [CatalogController::class, 'category'])->name('catalog.category'); +Route::get('/products/{product:slug}', [ProductController::class, 'show'])->name('products.show'); +Route::get('/chat/messages', [ShopChatController::class, 'messages']) + ->middleware('throttle:chat-read') + ->name('chat.messages'); +Route::post('/chat/messages', [ShopChatController::class, 'store']) + ->middleware('throttle:chat-send') + ->name('chat.send'); + +Route::get('/favorites', [FavoriteController::class, 'index'])->name('favorites.index'); +Route::post('/favorites/{product}', [FavoriteController::class, 'toggle'])->name('favorites.toggle'); + +Route::get('/compare', [CompareController::class, 'index'])->name('compare.index'); +Route::post('/compare/{product}', [CompareController::class, 'toggle'])->name('compare.toggle'); +Route::delete('/compare', [CompareController::class, 'clear'])->name('compare.clear'); + +Route::get('/cart', [CartController::class, 'index'])->name('cart.index'); +Route::post('/cart/{product}', [CartController::class, 'add'])->name('cart.add'); +Route::patch('/cart/{product}', [CartController::class, 'update'])->name('cart.update'); +Route::delete('/cart/{product}', [CartController::class, 'remove'])->name('cart.remove'); + +Route::get('/checkout', [CheckoutController::class, 'show'])->name('checkout.show'); +Route::post('/checkout/requisites', [CheckoutController::class, 'prepare'])->name('checkout.prepare'); +Route::get('/checkout/requisites', [CheckoutController::class, 'payment'])->name('checkout.payment'); +Route::post('/checkout', [CheckoutController::class, 'store'])->name('checkout.store'); +Route::get('/checkout/success/{order}', [CheckoutController::class, 'success'])->name('checkout.success'); + +Route::view('/about', 'pages.about')->name('pages.about'); +Route::view('/contacts', 'pages.contacts')->name('pages.contacts'); +Route::post('/contacts', [ContactController::class, 'submit']) + ->middleware('throttle:contact-send') + ->name('pages.contacts.submit'); +Route::view('/shipping-payment', 'pages.shipping-payment')->name('pages.shipping-payment'); + +Route::middleware('guest')->group(function () { + Route::get('/login', [AuthController::class, 'showLoginForm'])->name('login'); + Route::post('/login', [AuthController::class, 'login']) + ->middleware('throttle:auth') + ->name('login.attempt'); + Route::get('/register', [AuthController::class, 'showRegisterForm'])->name('register'); + Route::post('/register', [AuthController::class, 'register']) + ->middleware('throttle:auth') + ->name('register.store'); +}); + +Route::post('/logout', [AuthController::class, 'logout'])->name('logout')->middleware('auth'); + +Route::middleware('auth')->group(function () { + Route::get('/account', [AccountController::class, 'show'])->name('account'); + Route::post('/account', [AccountController::class, 'update'])->name('account.update'); + Route::get('/account/orders/{order}', [OrderController::class, 'show'])->name('account.orders.show'); +}); + +Route::prefix('admin')->name('admin.')->group(function () { + Route::get('login', [AdminAuthController::class, 'showLoginForm'])->name('login'); + Route::post('login', [AdminAuthController::class, 'login']) + ->middleware('throttle:admin-login') + ->name('login.attempt'); + + Route::middleware([\App\Http\Middleware\AdminMiddleware::class])->group(function () { + Route::post('logout', [AdminAuthController::class, 'logout'])->name('logout'); + Route::get('/', [AdminDashboardController::class, 'index'])->name('dashboard'); + Route::get('chats', [AdminChatController::class, 'index'])->name('chats.index'); + Route::get('chats/{conversation}/messages', [AdminChatController::class, 'messages']) + ->middleware('throttle:admin-chat-read') + ->name('chats.messages'); + Route::post('chats/{conversation}/messages', [AdminChatController::class, 'storeMessage']) + ->middleware('throttle:admin-chat-send') + ->name('chats.messages.store'); + Route::patch('chats/{conversation}/status', [AdminChatController::class, 'updateStatus']) + ->name('chats.status'); + Route::delete('chats/{conversation}', [AdminChatController::class, 'destroy']) + ->name('chats.destroy'); + Route::get('products/export/csv', [AdminProductController::class, 'exportCsv'])->name('products.export'); + Route::post('products/import/csv', [AdminProductController::class, 'importCsv'])->name('products.import'); + Route::resource('home-slides', AdminHomeSlideController::class) + ->parameters(['home-slides' => 'homeSlide']) + ->except('show'); + Route::resource('products', AdminProductController::class)->except('show'); + Route::resource('categories', AdminCategoryController::class)->except('show'); + Route::resource('orders', AdminOrderController::class)->only(['index', 'show', 'update']); + }); +}); diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..fc91d10 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +log() { + printf '\n[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" +} + +require_command() { + if ! command -v "$1" >/dev/null 2>&1; then + printf 'Missing required command: %s\n' "$1" >&2 + exit 1 + fi +} + +require_command php +require_command composer +require_command npm + +if [[ ! -f artisan ]]; then + printf 'artisan not found in %s\n' "$ROOT_DIR" >&2 + exit 1 +fi + +if [[ ! -f .env ]]; then + printf '.env not found. Create it from .env.example and set production values before deploying.\n' >&2 + exit 1 +fi + +maintenance_enabled=0 +deploy_succeeded=0 + +cleanup() { + if [[ "$maintenance_enabled" -eq 1 && "$deploy_succeeded" -eq 0 ]]; then + printf '\nDeployment failed. The application remains in maintenance mode.\n' >&2 + printf 'Fix the issue and run: php artisan up\n' >&2 + fi +} + +trap cleanup EXIT + +log "Putting the application into maintenance mode" +php artisan down --retry=60 +maintenance_enabled=1 + +log "Installing PHP dependencies" +composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction + +log "Installing Node.js dependencies" +npm ci + +log "Building frontend assets" +npm run build + +log "Ensuring the public storage link exists" +php artisan storage:link >/dev/null 2>&1 || true + +log "Running Laravel deployment tasks" +composer run deploy --no-interaction + +log "Restarting queue workers if they are running" +php artisan queue:restart >/dev/null 2>&1 || true + +log "Bringing the application back online" +php artisan up +maintenance_enabled=0 +deploy_succeeded=1 + +log "Deployment completed successfully" diff --git a/scripts/update-from-github.sh b/scripts/update-from-github.sh new file mode 100755 index 0000000..60fc19c --- /dev/null +++ b/scripts/update-from-github.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +branch="${1:-${DEPLOY_BRANCH:-main}}" +remote="${2:-${DEPLOY_REMOTE:-origin}}" + +log() { + printf '\n[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" +} + +if ! command -v git >/dev/null 2>&1; then + printf 'Missing required command: git\n' >&2 + exit 1 +fi + +if [[ ! -d .git ]]; then + printf 'No .git directory found in %s\n' "$ROOT_DIR" >&2 + printf 'Clone the repository on the server with git before using this script.\n' >&2 + exit 1 +fi + +if ! git diff --quiet || ! git diff --cached --quiet; then + printf 'Tracked local changes detected. Commit or stash them before updating from GitHub.\n' >&2 + exit 1 +fi + +log "Fetching updates from ${remote}" +git fetch --prune "$remote" + +if git show-ref --verify --quiet "refs/heads/$branch"; then + log "Switching to branch ${branch}" + git checkout "$branch" +else + log "Creating local branch ${branch} tracking ${remote}/${branch}" + git checkout -b "$branch" --track "${remote}/${branch}" +fi + +log "Pulling the latest changes for ${branch}" +git pull --ff-only "$remote" "$branch" + +log "Running deployment script" +bash "$ROOT_DIR/scripts/deploy.sh" diff --git a/storage/app/.gitignore b/storage/app/.gitignore new file mode 100644 index 0000000..fedb287 --- /dev/null +++ b/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/storage/app/private/.gitignore b/storage/app/private/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/Feature/AdminProductImageUploadTest.php b/tests/Feature/AdminProductImageUploadTest.php new file mode 100644 index 0000000..5ca8376 --- /dev/null +++ b/tests/Feature/AdminProductImageUploadTest.php @@ -0,0 +1,134 @@ +create([ + 'is_admin' => true, + ]); + + $category = Category::query()->create([ + 'name' => 'Видеокарты', + 'slug' => 'video-cards', + 'is_active' => true, + ]); + + $response = $this + ->actingAs($admin) + ->post(route('admin.products.store'), [ + 'category_id' => $category->id, + 'name' => 'RTX 5090', + 'slug' => '', + 'sku' => 'RTX-5090', + 'price' => '199999.99', + 'old_price' => '209999.99', + 'stock' => 3, + 'short_description' => 'Флагманская видеокарта', + 'description' => 'Описание товара', + 'image' => UploadedFile::fake()->create('main.jpg', 120, 'image/jpeg'), + 'gallery_images' => [ + UploadedFile::fake()->create('gallery-1.jpg', 120, 'image/jpeg'), + UploadedFile::fake()->create('gallery-2.webp', 120, 'image/webp'), + ], + 'is_active' => '1', + ]); + + $response + ->assertRedirect(route('admin.products.index')) + ->assertSessionDoesntHaveErrors(); + + $product = Product::query()->where('name', 'RTX 5090')->firstOrFail(); + + $this->assertSame('video-cards', $product->category->slug); + $this->assertNotNull($product->image_path); + $this->assertIsArray($product->gallery_paths); + $this->assertCount(2, $product->gallery_paths); + $this->assertStringStartsWith('products/', $product->image_path); + + Storage::disk('public')->assertExists($product->image_path); + + foreach ($product->gallery_paths as $path) { + $this->assertStringStartsWith('products/', $path); + Storage::disk('public')->assertExists($path); + } + } + + public function test_admin_can_remove_product_images_from_edit_form(): void + { + Storage::fake('public'); + + $admin = User::factory()->create([ + 'is_admin' => true, + ]); + + $category = Category::query()->create([ + 'name' => 'Ноутбуки', + 'slug' => 'laptops', + 'is_active' => true, + ]); + + Storage::disk('public')->put('products/main.jpg', 'main-image'); + Storage::disk('public')->put('products/gallery-1.jpg', 'gallery-one'); + Storage::disk('public')->put('products/gallery-2.webp', 'gallery-two'); + + $product = Product::query()->create([ + 'category_id' => $category->id, + 'name' => 'Lenovo Legion', + 'slug' => 'lenovo-legion', + 'sku' => 'LEGION-01', + 'price' => '159999.99', + 'old_price' => '169999.99', + 'stock' => 5, + 'short_description' => 'Игровой ноутбук', + 'description' => 'Описание ноутбука', + 'image_path' => 'products/main.jpg', + 'gallery_paths' => ['products/gallery-1.jpg', 'products/gallery-2.webp'], + 'is_active' => true, + ]); + + $response = $this + ->actingAs($admin) + ->put(route('admin.products.update', $product), [ + 'category_id' => $category->id, + 'name' => 'Lenovo Legion', + 'slug' => 'lenovo-legion', + 'sku' => 'LEGION-01', + 'price' => '159999.99', + 'old_price' => '169999.99', + 'stock' => 5, + 'short_description' => 'Игровой ноутбук', + 'description' => 'Описание ноутбука', + 'remove_image' => '1', + 'remove_gallery_paths' => ['products/gallery-1.jpg'], + 'is_active' => '1', + ]); + + $response + ->assertRedirect(route('admin.products.index')) + ->assertSessionDoesntHaveErrors(); + + $product->refresh(); + + $this->assertSame('products/gallery-2.webp', $product->image_path); + $this->assertSame([], $product->gallery_paths ?? []); + + Storage::disk('public')->assertMissing('products/main.jpg'); + Storage::disk('public')->assertMissing('products/gallery-1.jpg'); + Storage::disk('public')->assertExists('products/gallery-2.webp'); + } +} diff --git a/tests/Feature/ChatConversationClosingTest.php b/tests/Feature/ChatConversationClosingTest.php new file mode 100644 index 0000000..05b9f2d --- /dev/null +++ b/tests/Feature/ChatConversationClosingTest.php @@ -0,0 +1,115 @@ +create([ + 'is_admin' => true, + ]); + + $customer = User::factory()->create(); + $conversation = ChatConversation::query()->create([ + 'user_id' => $customer->id, + 'visitor_token' => (string) Str::uuid(), + 'status' => ChatConversation::STATUS_OPEN, + 'last_message_at' => now(), + ]); + + $response = $this + ->actingAs($admin) + ->patch(route('admin.chats.status', $conversation), [ + 'status' => ChatConversation::STATUS_CLOSED, + ]); + + $response->assertRedirect(route('admin.chats.index', ['conversation' => $conversation->id])); + $this->assertSame(ChatConversation::STATUS_CLOSED, $conversation->fresh()->status); + } + + public function test_customer_message_after_chat_closure_starts_new_conversation(): void + { + $customer = User::factory()->create(); + + $closedConversation = ChatConversation::query()->create([ + 'user_id' => $customer->id, + 'visitor_token' => (string) Str::uuid(), + 'status' => ChatConversation::STATUS_CLOSED, + 'last_message_at' => now(), + ]); + + $closedConversation->messages()->create([ + 'sender' => 'customer', + 'body' => 'Старый вопрос', + 'is_read' => true, + ]); + + $this + ->actingAs($customer) + ->getJson(route('chat.messages')) + ->assertOk() + ->assertJsonPath('conversation.id', $closedConversation->id) + ->assertJsonPath('conversation.is_closed', true); + + $this + ->actingAs($customer) + ->postJson(route('chat.send'), [ + 'message' => 'Новый вопрос', + ]) + ->assertOk() + ->assertJsonPath('conversation.is_closed', false); + + $this->assertSame(ChatConversation::STATUS_CLOSED, $closedConversation->fresh()->status); + $this->assertSame(2, ChatConversation::query()->where('user_id', $customer->id)->count()); + + $newConversation = ChatConversation::query() + ->where('user_id', $customer->id) + ->latest('id') + ->firstOrFail(); + + $this->assertNotSame($closedConversation->id, $newConversation->id); + $this->assertSame(ChatConversation::STATUS_OPEN, $newConversation->status); + $this->assertSame('Новый вопрос', $newConversation->messages()->firstOrFail()->body); + } + + public function test_admin_can_delete_chat_conversation_with_messages(): void + { + $admin = User::factory()->create([ + 'is_admin' => true, + ]); + + $customer = User::factory()->create(); + $conversation = ChatConversation::query()->create([ + 'user_id' => $customer->id, + 'visitor_token' => (string) Str::uuid(), + 'status' => ChatConversation::STATUS_OPEN, + 'last_message_at' => now(), + ]); + + $message = $conversation->messages()->create([ + 'sender' => 'customer', + 'body' => 'Нужно удалить этот чат', + 'is_read' => false, + ]); + + $response = $this + ->actingAs($admin) + ->delete(route('admin.chats.destroy', $conversation)); + + $response->assertRedirect(route('admin.chats.index')); + $this->assertDatabaseMissing('chat_conversations', ['id' => $conversation->id]); + $this->assertDatabaseMissing('chat_messages', ['id' => $message->id]); + $this->assertSame(0, ChatConversation::query()->count()); + $this->assertSame(0, ChatMessage::query()->count()); + } +} diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..22ef0c5 --- /dev/null +++ b/tests/Feature/ExampleTest.php @@ -0,0 +1,21 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/tests/Unit/ImageUrlResolutionTest.php b/tests/Unit/ImageUrlResolutionTest.php new file mode 100644 index 0000000..ac1bc19 --- /dev/null +++ b/tests/Unit/ImageUrlResolutionTest.php @@ -0,0 +1,34 @@ + 'products/example.jpg', + 'gallery_paths' => ['products/gallery-1.jpg', 'products/gallery-2.webp'], + ]); + + $this->assertSame('/storage/products/example.jpg', $product->image_url); + $this->assertSame([ + '/storage/products/example.jpg', + '/storage/products/gallery-1.jpg', + '/storage/products/gallery-2.webp', + ], $product->gallery_urls); + } + + public function test_home_slide_uses_relative_storage_url_for_uploaded_images(): void + { + $slide = new HomeSlide([ + 'image_path' => 'home-slides/banner.webp', + ]); + + $this->assertSame('/storage/home-slides/banner.webp', $slide->image_url); + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..f35b4e7 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + tailwindcss(), + ], + server: { + watch: { + ignored: ['**/storage/framework/views/**'], + }, + }, +});