From d91108c8839962e592a571017ccbde0368b026bc Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Wed, 12 Nov 2025 13:19:28 +0100 Subject: [PATCH] weitere verbesserungen der Guest PWA (vor allem TaskPicker) --- app/Http/Middleware/ContentSecurityPolicy.php | 4 +- config/app.php | 2 +- config/sanctum.php | 2 +- package-lock.json | 961 +++++++++++++++++- package.json | 13 +- resources/css/app.css | 4 + .../js/guest/components/EmotionPicker.tsx | 77 +- .../js/guest/components/GalleryPreview.tsx | 7 + resources/js/guest/components/Header.tsx | 141 ++- .../context/NotificationCenterContext.tsx | 59 ++ .../js/guest/hooks/useGuestTaskProgress.ts | 2 + resources/js/guest/i18n/messages.ts | 154 +++ .../lib/__tests__/limitSummaries.test.ts | 20 +- resources/js/guest/lib/limitSummaries.ts | 20 +- resources/js/guest/pages/AchievementsPage.tsx | 87 +- resources/js/guest/pages/HomePage.tsx | 354 +++---- resources/js/guest/pages/TaskPickerPage.tsx | 898 ++++++++++------ resources/js/guest/pages/UploadPage.tsx | 123 ++- resources/js/guest/router.tsx | 25 +- vite.config.ts | 6 +- 20 files changed, 2306 insertions(+), 653 deletions(-) create mode 100644 resources/js/guest/context/NotificationCenterContext.tsx diff --git a/app/Http/Middleware/ContentSecurityPolicy.php b/app/Http/Middleware/ContentSecurityPolicy.php index ec1f425..498ca3b 100644 --- a/app/Http/Middleware/ContentSecurityPolicy.php +++ b/app/Http/Middleware/ContentSecurityPolicy.php @@ -99,13 +99,13 @@ class ContentSecurityPolicy if (app()->environment(['local', 'development']) || config('app.debug')) { $devHosts = [ - 'http://localhost:5173', + 'http://fotospiel-app.test:5173', 'http://127.0.0.1:5173', 'https://localhost:5173', 'https://127.0.0.1:5173', ]; $wsHosts = [ - 'ws://localhost:5173', + 'ws://fotospiel-app.test:5173', 'ws://127.0.0.1:5173', 'wss://localhost:5173', 'wss://127.0.0.1:5173', diff --git a/config/app.php b/config/app.php index 21061de..1ece482 100644 --- a/config/app.php +++ b/config/app.php @@ -52,7 +52,7 @@ return [ | */ - 'url' => env('APP_URL', 'http://localhost'), + 'url' => env('APP_URL', 'http://fotospiel-app.test'), /* |-------------------------------------------------------------------------- diff --git a/config/sanctum.php b/config/sanctum.php index 44527d6..5ca95b9 100644 --- a/config/sanctum.php +++ b/config/sanctum.php @@ -17,7 +17,7 @@ return [ 'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( '%s%s', - 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', + 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1,fotospiel-app.test,fotospiel.app', Sanctum::currentApplicationUrlWithPort(), // Sanctum::currentRequestHost(), ))), diff --git a/package-lock.json b/package-lock.json index 20ed197..d8eb1bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,10 +7,11 @@ "dependencies": { "@headlessui/react": "^2.2.0", "@inertiajs/react": "^2.1.0", + "@jpisnice/shadcn-ui-mcp-server": "^1.1.4", "@modelcontextprotocol/server-puppeteer": "^2025.5.12", "@modelcontextprotocol/server-sequential-thinking": "^2025.7.1", "@paypal/react-paypal-js": "^8.9.2", - "@playwright/mcp": "^0.0.37", + "@playwright/mcp": "^0.0.46", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.1.4", @@ -34,9 +35,9 @@ "@tanstack/react-query": "^5.90.2", "@types/react": "^19.0.3", "@types/react-dom": "^19.0.2", - "@upstash/context7-mcp": "^1.0.21", + "@upstash/context7-mcp": "^1.0.26", "@vitejs/plugin-react": "^4.6.0", - "chrome-devtools-mcp": "^0.9.0", + "chrome-devtools-mcp": "^0.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "concurrently": "^9.0.1", @@ -642,6 +643,15 @@ "statuses": "^2.0.1" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -757,6 +767,17 @@ "node": ">=18" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@dotenvx/dotenvx": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.0.tgz", @@ -1577,6 +1598,21 @@ "node": ">=10.13.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@headlessui/react": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz", @@ -1867,6 +1903,326 @@ "node": ">=18.0.0" } }, + "node_modules/@jpisnice/shadcn-ui-mcp-server": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@jpisnice/shadcn-ui-mcp-server/-/shadcn-ui-mcp-server-1.1.4.tgz", + "integrity": "sha512-T4ZZfGHmA/uuB2eoHzsEsmbmFawfa+pqrQtTe9in8Ew9n53Y556q78xYfhjs0tXK3305oXr4DG++al66N6/ykw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.16.0", + "axios": "^1.8.4", + "cheerio": "^1.0.0", + "cors": "^2.8.5", + "express": "^4.21.2", + "joi": "^17.13.3", + "uuid": "^10.0.0", + "winston": "^3.15.0", + "zod": "^3.24.2" + }, + "bin": { + "shadcn-mcp": "build/index.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@jpisnice/shadcn-ui-mcp-server/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -2330,13 +2686,13 @@ } }, "node_modules/@playwright/mcp": { - "version": "0.0.37", - "resolved": "https://registry.npmjs.org/@playwright/mcp/-/mcp-0.0.37.tgz", - "integrity": "sha512-BnI2Ijim1rhIGhoFKJRCa+MaWtNr7M2lnLeDldDsR0n+ZB2G7zjt+MAMqy5eRD/mMiWsTaQsXlzZmXeixqBdsA==", + "version": "0.0.46", + "resolved": "https://registry.npmjs.org/@playwright/mcp/-/mcp-0.0.46.tgz", + "integrity": "sha512-AXV2GxA6GKfKm/elCd898XTsP6OutKoKs52lATrF1oIOw2nX32vNLst79mjWNYpwaxP5I1kd083M2shggsy8eQ==", "license": "Apache-2.0", "dependencies": { - "playwright": "1.56.0-alpha-2025-09-06", - "playwright-core": "1.56.0-alpha-2025-09-06" + "playwright": "1.57.0-alpha-2025-11-07", + "playwright-core": "1.57.0-alpha-2025-11-07" }, "bin": { "mcp-server-playwright": "cli.js" @@ -2346,12 +2702,12 @@ } }, "node_modules/@playwright/mcp/node_modules/playwright": { - "version": "1.56.0-alpha-2025-09-06", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0-alpha-2025-09-06.tgz", - "integrity": "sha512-suVjiF5eeUtIqFq5E/5LGgkV0/bRSik87N+M7uLsjPQrKln9QHbZt3cy7Zybicj3ZqTBWWHvpN9b4cnpg6hS0g==", + "version": "1.57.0-alpha-2025-11-07", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0-alpha-2025-11-07.tgz", + "integrity": "sha512-E5fgekU+NuIfE16bjL9xIffhmag2cInC/KDfXwvVGkCka5TpZfiWhHvCYIRW6/hEGr+eJS3jPHR91cyPO3gQgA==", "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.56.0-alpha-2025-09-06" + "playwright-core": "1.57.0-alpha-2025-11-07" }, "bin": { "playwright": "cli.js" @@ -3854,6 +4210,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause" + }, "node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", @@ -3867,6 +4244,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, "node_modules/@stripe/react-stripe-js": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.0.0.tgz", @@ -4486,6 +4873,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -4775,9 +5168,9 @@ } }, "node_modules/@upstash/context7-mcp": { - "version": "1.0.21", - "resolved": "https://registry.npmjs.org/@upstash/context7-mcp/-/context7-mcp-1.0.21.tgz", - "integrity": "sha512-kO9kGxt/ZgbSi7rarysNasc92l1b6RoaHUSDOJ8/SzDkLvE9VL0K2F8lC4w6eFDc+B4rfW2uSPeBCNC65nYnxw==", + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/@upstash/context7-mcp/-/context7-mcp-1.0.26.tgz", + "integrity": "sha512-Iz6nJIXpMzkKd7kcli00M61G76+74X6C/x3Jg4icxnlUQb+w0TqQAziXNTFZZ5gBKmgPKovPoZ6gvtvxmiD+7Q==", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.5", @@ -5245,6 +5638,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -5389,6 +5788,12 @@ "node": ">=4" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -5627,6 +6032,12 @@ "node": ">=18" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -5913,6 +6324,69 @@ "node": ">= 16" } }, + "node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5959,9 +6433,9 @@ } }, "node_modules/chrome-devtools-mcp": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/chrome-devtools-mcp/-/chrome-devtools-mcp-0.9.0.tgz", - "integrity": "sha512-7MzI/fdnwbKHzgnGWUmCyEYdKnSpfSIelDV9XNTz8wrjycoMB6cENryKLyZkLHXkZLlDdOLfYa9YtF+3lQoM2g==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/chrome-devtools-mcp/-/chrome-devtools-mcp-0.10.1.tgz", + "integrity": "sha512-jyDAbaf2zyl4HVa8vfRqwkCCqT+QpuiRgrEQ6iodqOb5BaqATQb9zZARqYGIDNlwEpmAhKvhkQ0w+0R9dvCc2A==", "license": "Apache-2.0", "bin": { "chrome-devtools-mcp": "build/src/index.js" @@ -6156,6 +6630,19 @@ "dev": true, "license": "MIT" }, + "node_modules/color": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz", + "integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.0.1", + "color-string": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6174,6 +6661,27 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz", + "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", + "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -6184,6 +6692,27 @@ "color-support": "bin.js" } }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz", + "integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", + "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6380,6 +6909,34 @@ "utrie": "^1.0.2" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -6734,6 +7291,16 @@ "node": ">=6" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -6798,6 +7365,44 @@ "license": "MIT", "peer": true }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, "node_modules/domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -6822,6 +7427,21 @@ "node": ">=12" } }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, "node_modules/dompurify": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", @@ -6832,6 +7452,20 @@ "@types/trusted-types": "^2.0.7" } }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -6939,6 +7573,12 @@ "dev": true, "license": "MIT" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -6948,6 +7588,19 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -6981,7 +7634,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "devOptional": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -8196,6 +8848,12 @@ } } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/fetch-blob": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", @@ -8322,6 +8980,12 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -9130,6 +9794,25 @@ "integrity": "sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==", "license": "Apache-2.0" }, + "node_modules/htmlparser2": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -10020,6 +10703,19 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10288,6 +10984,12 @@ "node": ">=6" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/laravel-vite-plugin": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz", @@ -10676,6 +11378,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10795,6 +11514,15 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -10820,6 +11548,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -11202,6 +11942,18 @@ "set-blocking": "^2.0.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/nwsapi": { "version": "2.2.22", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", @@ -11355,6 +12107,15 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -11584,6 +12345,55 @@ "dev": true, "license": "MIT" }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -11783,9 +12593,9 @@ } }, "node_modules/playwright-core": { - "version": "1.56.0-alpha-2025-09-06", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0-alpha-2025-09-06.tgz", - "integrity": "sha512-B2s/cuqYuu+mT4hIHG8gIOXjCSKh0Np1gJNCp0CrDk/UTLB74gThwXiyPAJU0fADIQH6Dv1glv8ZvKTDVT8Fng==", + "version": "1.57.0-alpha-2025-11-07", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0-alpha-2025-11-07.tgz", + "integrity": "sha512-p61pk1XLuFBSia+37jfeuw62HbAi/KaXOGvjUoAiPdnDgO6AOj/DfodbWkZ1fqLZbW+q6Mja30YnzhD0CaePEQ==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -12726,7 +13536,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "devOptional": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -13171,6 +13980,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -13649,6 +14467,15 @@ "node": ">=0.10.0" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -13741,7 +14568,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "devOptional": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -14241,6 +15067,12 @@ "b4a": "^1.6.4" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", @@ -14437,6 +15269,15 @@ "tree-kill": "cli.js" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -14889,6 +15730,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", @@ -14899,6 +15749,19 @@ "base64-arraybuffer": "^1.0.2" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/value-or-function": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", @@ -16277,7 +17140,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" @@ -16290,7 +17152,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -16482,6 +17343,54 @@ "node": ">=8" } }, + "node_modules/winston": { + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", + "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index ae17232..45179d5 100644 --- a/package.json +++ b/package.json @@ -39,10 +39,11 @@ "dependencies": { "@headlessui/react": "^2.2.0", "@inertiajs/react": "^2.1.0", + "@jpisnice/shadcn-ui-mcp-server": "^1.1.4", "@modelcontextprotocol/server-puppeteer": "^2025.5.12", "@modelcontextprotocol/server-sequential-thinking": "^2025.7.1", "@paypal/react-paypal-js": "^8.9.2", - "@playwright/mcp": "^0.0.37", + "@playwright/mcp": "^0.0.46", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.1.4", @@ -66,9 +67,9 @@ "@tanstack/react-query": "^5.90.2", "@types/react": "^19.0.3", "@types/react-dom": "^19.0.2", - "@upstash/context7-mcp": "^1.0.21", + "@upstash/context7-mcp": "^1.0.26", "@vitejs/plugin-react": "^4.6.0", - "chrome-devtools-mcp": "^0.9.0", + "chrome-devtools-mcp": "^0.10.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "concurrently": "^9.0.1", @@ -76,6 +77,8 @@ "embla-carousel": "^8.6.0", "embla-carousel-autoplay": "^8.6.0", "embla-carousel-react": "^8.6.0", + "fabric": "^6.0.1", + "fabricjs-design-tool": "github:rifrocket/fabricjs-design-tool#main", "globals": "^15.14.0", "html5-qrcode": "^2.3.8", "i18next": "^25.5.3", @@ -83,14 +86,12 @@ "i18next-http-backend": "^3.0.2", "laravel-vite-plugin": "^2.0", "lucide-react": "^0.475.0", - "fabric": "^6.0.1", - "fabricjs-design-tool": "github:rifrocket/fabricjs-design-tool#main", "pdf-lib": "^1.17.1", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-rnd": "^10.4.12", "react-hot-toast": "^2.6.0", "react-i18next": "^16.0.0", + "react-rnd": "^10.4.12", "react-router-dom": "^7.8.2", "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.0", diff --git a/resources/css/app.css b/resources/css/app.css index 7d4582b..d6c19db 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -134,6 +134,10 @@ html.dark { background-color: oklch(0.145 0 0); } +body { + font-family: var(--guest-font-family), 'Inter', 'Helvetica Neue', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; +} + @keyframes aurora { 0%, 100% { diff --git a/resources/js/guest/components/EmotionPicker.tsx b/resources/js/guest/components/EmotionPicker.tsx index f768491..fa4ddaa 100644 --- a/resources/js/guest/components/EmotionPicker.tsx +++ b/resources/js/guest/components/EmotionPicker.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { ChevronRight } from 'lucide-react'; +import { cn } from '@/lib/utils'; interface Emotion { id: number; @@ -13,9 +14,19 @@ interface Emotion { interface EmotionPickerProps { onSelect?: (emotion: Emotion) => void; + variant?: 'standalone' | 'embedded'; + title?: string; + subtitle?: string; + showSkip?: boolean; } -export default function EmotionPicker({ onSelect }: EmotionPickerProps) { +export default function EmotionPicker({ + onSelect, + variant = 'standalone', + title, + subtitle, + showSkip, +}: EmotionPickerProps) { const { token } = useParams<{ token: string }>(); const eventKey = token ?? ''; const navigate = useNavigate(); @@ -73,17 +84,29 @@ export default function EmotionPicker({ onSelect }: EmotionPickerProps) { } }; + const headingTitle = title ?? 'Wie fühlst du dich?'; + const headingSubtitle = subtitle ?? '(optional)'; + const shouldShowSkip = showSkip ?? variant === 'standalone'; + const content = (
-
-

- Wie fühlst du dich? - (optional) -

- {loading && Lade Emotionen…} -
+ {(variant === 'standalone' || title) && ( +
+

+ {headingTitle} + {headingSubtitle && {headingSubtitle}} +

+ {loading && Lade Emotionen…} +
+ )} -
+
{emotions.map((emotion) => { // Localize name and description if they are JSON const localize = (value: string | object, defaultValue: string = ''): string => { @@ -125,18 +148,20 @@ export default function EmotionPicker({ onSelect }: EmotionPickerProps) {
{/* Skip option */} -
- -
+ {shouldShowSkip && ( +
+ +
+ )}
); @@ -148,9 +173,9 @@ export default function EmotionPicker({ onSelect }: EmotionPickerProps) { ); } - return ( -
- {content} -
- ); + if (variant === 'embedded') { + return content; + } + + return
{content}
; } diff --git a/resources/js/guest/components/GalleryPreview.tsx b/resources/js/guest/components/GalleryPreview.tsx index ccaf3a4..536257a 100644 --- a/resources/js/guest/components/GalleryPreview.tsx +++ b/resources/js/guest/components/GalleryPreview.tsx @@ -123,6 +123,13 @@ export default function GalleryPreview({ token }: Props) { ))}
+ +

+ Lust auf mehr?{' '} + + Zur Galerie → + +

); } diff --git a/resources/js/guest/components/Header.tsx b/resources/js/guest/components/Header.tsx index cd6f38f..bd50aa1 100644 --- a/resources/js/guest/components/Header.tsx +++ b/resources/js/guest/components/Header.tsx @@ -1,12 +1,15 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import AppearanceToggleDropdown from '@/components/appearance-dropdown'; -import { User, Heart, Users, PartyPopper, Camera } from 'lucide-react'; +import { User, Heart, Users, PartyPopper, Camera, Bell, ArrowUpRight } from 'lucide-react'; import { useEventData } from '../hooks/useEventData'; import { useOptionalEventStats } from '../context/EventStatsContext'; import { useOptionalGuestIdentity } from '../context/GuestIdentityContext'; import { SettingsSheet } from './settings-sheet'; import { useTranslation } from '../i18n/useTranslation'; import { DEFAULT_EVENT_BRANDING, useOptionalEventBranding } from '../context/EventBrandingContext'; +import { useOptionalNotificationCenter } from '../context/NotificationCenterContext'; +import { useGuestTaskProgress, TASK_BADGE_TARGET } from '../hooks/useGuestTaskProgress'; const EVENT_ICON_COMPONENTS: Record> = { heart: Heart, @@ -86,6 +89,31 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string const branding = brandingContext?.branding ?? DEFAULT_EVENT_BRANDING; const primaryForeground = '#ffffff'; const { event, status } = useEventData(); + const notificationCenter = useOptionalNotificationCenter(); + const [notificationsOpen, setNotificationsOpen] = React.useState(false); + const taskProgress = useGuestTaskProgress(eventToken); + const panelRef = React.useRef(null); + const checklistItems = React.useMemo( + () => [ + t('home.checklist.steps.first'), + t('home.checklist.steps.second'), + t('home.checklist.steps.third'), + ], + [t], + ); + + React.useEffect(() => { + if (!notificationsOpen) { + return; + } + const handler = (event: MouseEvent) => { + if (!panelRef.current) return; + if (panelRef.current.contains(event.target as Node)) return; + setNotificationsOpen(false); + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, [notificationsOpen]); if (!eventToken) { const guestName = identity?.name && identity?.hydrated ? identity.name : null; @@ -139,7 +167,6 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string const stats = statsContext && statsContext.eventKey === eventToken ? statsContext : undefined; - return (
+ {notificationCenter && ( + setNotificationsOpen((prev) => !prev)} + panelRef={panelRef} + checklistItems={checklistItems} + taskProgress={taskProgress?.hydrated ? taskProgress : undefined} + /> + )}
@@ -179,4 +217,101 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string ); } -export {} +function NotificationButton({ + center, + eventToken, + open, + onToggle, + panelRef, + checklistItems, + taskProgress, +}: { + center: { + queueCount: number; + inviteCount: number; + totalCount: number; + }; + eventToken: string; + open: boolean; + onToggle: () => void; + panelRef: React.RefObject; + checklistItems: string[]; + taskProgress?: ReturnType; +}) { + if (!center) { + return null; + } + + const totalCount = center.totalCount; + const progressRatio = taskProgress + ? Math.min(1, taskProgress.completedCount / TASK_BADGE_TARGET) + : 0; + + return ( +
+ + {open && ( +
+

Benachrichtigungen

+

Uploads in Warteschlange: {center.queueCount}

+ + Zur Warteschlange + + + {taskProgress && ( +
+
+
+

Badge-Fortschritt

+

+ {taskProgress.completedCount}/{TASK_BADGE_TARGET} +

+
+ + Weiter + +
+
+
+
+
+ )} +
+

So funktioniert’s

+
    + {checklistItems.map((item) => ( +
  • + + {item} +
  • + ))} +
+
+ )} +
+ ); +} diff --git a/resources/js/guest/context/NotificationCenterContext.tsx b/resources/js/guest/context/NotificationCenterContext.tsx new file mode 100644 index 0000000..2bde8b1 --- /dev/null +++ b/resources/js/guest/context/NotificationCenterContext.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { useUploadQueue } from '../queue/hooks'; +import type { QueueItem } from '../queue/queue'; + +type NotificationCenterValue = { + queueItems: QueueItem[]; + queueCount: number; + inviteCount: number; + totalCount: number; + loading: boolean; + refreshQueue: () => Promise; + eventToken: string; +}; + +const NotificationCenterContext = React.createContext(null); + +export function NotificationCenterProvider({ + eventToken, + children, +}: { + eventToken: string; + children: React.ReactNode; +}) { + const { items, loading, refresh } = useUploadQueue(); + + const queueCount = React.useMemo( + () => items.filter((item) => item.status !== 'done').length, + [items], + ); + + const value = React.useMemo( + () => ({ + queueItems: items, + queueCount, + inviteCount: 0, + totalCount: queueCount, + loading, + refreshQueue: refresh, + eventToken, + }), + [items, queueCount, loading, refresh, eventToken], + ); + + return ( + {children} + ); +} + +export function useNotificationCenter() { + const ctx = React.useContext(NotificationCenterContext); + if (!ctx) { + throw new Error('useNotificationCenter must be used within NotificationCenterProvider'); + } + return ctx; +} + +export function useOptionalNotificationCenter() { + return React.useContext(NotificationCenterContext); +} diff --git a/resources/js/guest/hooks/useGuestTaskProgress.ts b/resources/js/guest/hooks/useGuestTaskProgress.ts index 0631a34..15a68bb 100644 --- a/resources/js/guest/hooks/useGuestTaskProgress.ts +++ b/resources/js/guest/hooks/useGuestTaskProgress.ts @@ -1,5 +1,7 @@ import React from 'react'; +export const TASK_BADGE_TARGET = 5; + function storageKey(eventKey: string) { return `guestTasks_${eventKey}`; } diff --git a/resources/js/guest/i18n/messages.ts b/resources/js/guest/i18n/messages.ts index 9cbc35d..d2ca50c 100644 --- a/resources/js/guest/i18n/messages.ts +++ b/resources/js/guest/i18n/messages.ts @@ -184,6 +184,40 @@ export const messages: Record = { days: 'vor {count} Tagen', }, }, + tasks: { + page: { + eyebrow: 'Aufgaben-Zentrale', + title: 'Deine nächste Aufgabe', + subtitle: 'Wähle eine Stimmung aus oder lass dich überraschen.', + swipeHint: 'Tipp: Links wischen = neue Aufgabe · Rechts wischen = Inspiration', + completedLabel: 'Schon erledigt', + ctaStart: "Los geht's!", + shuffleCta: 'Was Neues!', + shuffleButton: 'Shuffle', + inspirationTitle: 'Foto-Inspiration', + inspirationLoading: 'lädt…', + inspirationEmptyTitle: 'Noch kein Foto zu dieser Aufgabe', + inspirationEmptyDescription: 'Sei die/der Erste und lade eins hoch', + inspirationMore: 'Mehr', + inspirationError: 'Fotos konnten nicht geladen werden', + suggestionsEyebrow: 'Mehr Inspiration', + suggestionsTitle: 'Spring direkt zur nächsten Aufgabe', + noTasksAlert: 'Für dieses Event sind derzeit keine Aufgaben hinterlegt.', + emptyTitle: 'Keine passende Aufgabe gefunden', + emptyDescriptionWithTasks: 'Für deine aktuelle Stimmung gibt es gerade keine Aufgabe. Wähle eine andere Stimmung oder lade neue Aufgaben.', + emptyDescriptionNoTasks: 'Hier sind noch keine Aufgaben hinterlegt. Bitte versuche es später erneut.', + reloadButton: 'Aufgaben neu laden', + filters: { + none: 'Kein Filter', + recentFallback: 'Stimmung wählen', + showAll: 'Alle zeigen', + dialogTitle: 'Alle verfügbaren Stimmungen', + empty: 'Für dieses Event stehen noch keine Stimmungen bereit.', + countOne: '{count} Aufgabe', + countMany: '{count} Aufgaben', + }, + }, + }, notFound: { title: 'Nicht gefunden', description: 'Die Seite konnte nicht gefunden werden.', @@ -255,6 +289,49 @@ export const messages: Record = { }, dismiss: 'Verstanden', }, + hud: { + title: 'Live-Missionen', + subtitle: 'Bleib im Flow – Kamera bereithalten.', + moodLabel: 'Stimmung: {mood}', + moodFallback: 'Freestyle', + ctaLabel: 'Inspiration öffnen', + cards: { + online: 'Gäste online', + completed: 'Aufgaben gelöst', + lastUpload: 'Letzter Upload', + }, + progressLabel: 'Story {count}/{target} aktiv', + liveGuests: '{count} Gäste live', + relative: { + now: 'Gerade eben', + minutes: 'vor {count} Min', + hours: 'vor {count} Std', + days: 'vor {count} Tagen', + }, + }, + limitSummary: { + title: 'Uploads & Slots', + subtitle: 'Dein Event-Paket im Überblick', + badgeLabel: 'Aktuell', + cards: { + photos: { + title: 'Fotos insgesamt', + remaining: '{remaining} von {limit} frei', + unlimited: 'Unlimitierte Foto-Uploads aktiv', + }, + guests: { + title: 'Geräte im Einsatz', + remaining: '{remaining} Slots verfügbar', + unlimited: 'Unlimitierte Geräte freigeschaltet', + }, + }, + badges: { + ok: 'OK', + warning: 'Bald voll', + limit_reached: 'Limit erreicht', + unlimited: 'Unlimitiert', + }, + }, cameraUnsupported: { title: 'Kamera nicht verfügbar', message: 'Dein Gerät unterstützt keine Kameravorschau im Browser. Du kannst stattdessen Fotos aus deiner Galerie hochladen.', @@ -652,6 +729,40 @@ export const messages: Record = { days: '{count} days ago', }, }, + tasks: { + page: { + eyebrow: 'Mission hub', + title: 'Your next task', + subtitle: 'Pick a mood or stay spontaneous.', + swipeHint: 'Tip: Swipe left for a new task · right for inspiration', + completedLabel: 'Already done', + ctaStart: "Let's go!", + shuffleCta: 'Something new!', + shuffleButton: 'Shuffle', + inspirationTitle: 'Photo inspiration', + inspirationLoading: 'loading…', + inspirationEmptyTitle: 'No photo for this task yet', + inspirationEmptyDescription: 'Be the first one to upload!', + inspirationMore: 'More', + inspirationError: 'Photos could not be loaded', + suggestionsEyebrow: 'More inspiration', + suggestionsTitle: 'Jump straight to the next task', + noTasksAlert: 'No tasks available for this event yet.', + emptyTitle: 'No matching task found', + emptyDescriptionWithTasks: 'No task matches this mood right now. Pick another mood or load new tasks.', + emptyDescriptionNoTasks: 'No tasks are available yet. Please try again later.', + reloadButton: 'Reload tasks', + filters: { + none: 'No filter', + recentFallback: 'Select mood', + showAll: 'Show all', + dialogTitle: 'All available moods', + empty: 'No moods are available for this event yet.', + countOne: '{count} task', + countMany: '{count} tasks', + }, + }, + }, notFound: { title: 'Not found', description: 'We could not find the page you requested.', @@ -723,6 +834,49 @@ export const messages: Record = { }, dismiss: 'Got it', }, + hud: { + title: 'Live missions', + subtitle: 'Stay in the flow – keep the camera ready.', + moodLabel: 'Mood: {mood}', + moodFallback: 'Freestyle', + ctaLabel: 'Open inspiration', + cards: { + online: 'Guests online', + completed: 'Tasks completed', + lastUpload: 'Latest upload', + }, + progressLabel: 'Story {count}/{target} active', + liveGuests: '{count} guests live', + relative: { + now: 'Just now', + minutes: '{count} min ago', + hours: '{count} h ago', + days: '{count} days ago', + }, + }, + limitSummary: { + title: 'Uploads & slots', + subtitle: 'Your event package overview', + badgeLabel: 'Current', + cards: { + photos: { + title: 'Photos total', + remaining: '{remaining} of {limit} free', + unlimited: 'Unlimited photo uploads', + }, + guests: { + title: 'Devices in use', + remaining: '{remaining} slots available', + unlimited: 'Unlimited devices enabled', + }, + }, + badges: { + ok: 'OK', + warning: 'Almost full', + limit_reached: 'Limit reached', + unlimited: 'Unlimited', + }, + }, cameraUnsupported: { title: 'Camera not available', message: 'Your device does not support live camera preview in this browser. You can upload photos from your gallery instead.', diff --git a/resources/js/guest/lib/__tests__/limitSummaries.test.ts b/resources/js/guest/lib/__tests__/limitSummaries.test.ts index f230d4d..13dea16 100644 --- a/resources/js/guest/lib/__tests__/limitSummaries.test.ts +++ b/resources/js/guest/lib/__tests__/limitSummaries.test.ts @@ -4,16 +4,16 @@ import type { EventPackageLimits } from '../../services/eventApi'; import { buildLimitSummaries } from '../limitSummaries'; const translations = new Map([ - ['upload.status.cards.photos.title', 'Fotos'], - ['upload.status.cards.photos.remaining', 'Noch {remaining} von {limit}'], - ['upload.status.cards.photos.unlimited', 'Unbegrenzte Uploads'], - ['upload.status.cards.guests.title', 'Gäste'], - ['upload.status.cards.guests.remaining', '{remaining} Gäste frei (max. {limit})'], - ['upload.status.cards.guests.unlimited', 'Unbegrenzte Gäste'], - ['upload.status.badges.ok', 'OK'], - ['upload.status.badges.warning', 'Warnung'], - ['upload.status.badges.limit_reached', 'Limit erreicht'], - ['upload.status.badges.unlimited', 'Unbegrenzt'], + ['upload.limitSummary.cards.photos.title', 'Fotos'], + ['upload.limitSummary.cards.photos.remaining', 'Noch {remaining} von {limit}'], + ['upload.limitSummary.cards.photos.unlimited', 'Unbegrenzte Uploads'], + ['upload.limitSummary.cards.guests.title', 'Gäste'], + ['upload.limitSummary.cards.guests.remaining', '{remaining} Gäste frei (max. {limit})'], + ['upload.limitSummary.cards.guests.unlimited', 'Unbegrenzte Gäste'], + ['upload.limitSummary.badges.ok', 'OK'], + ['upload.limitSummary.badges.warning', 'Warnung'], + ['upload.limitSummary.badges.limit_reached', 'Limit erreicht'], + ['upload.limitSummary.badges.unlimited', 'Unbegrenzt'], ]); const t = (key: string) => translations.get(key) ?? key; diff --git a/resources/js/guest/lib/limitSummaries.ts b/resources/js/guest/lib/limitSummaries.ts index 5ca3d9f..ba24593 100644 --- a/resources/js/guest/lib/limitSummaries.ts +++ b/resources/js/guest/lib/limitSummaries.ts @@ -35,13 +35,13 @@ function buildCard( summary: LimitUsageSummary, t: TranslateFn ): LimitSummaryCard { - const labelKey = id === 'photos' ? 'upload.status.cards.photos.title' : 'upload.status.cards.guests.title'; + const labelKey = id === 'photos' ? 'upload.limitSummary.cards.photos.title' : 'upload.limitSummary.cards.guests.title'; const remainingKey = id === 'photos' - ? 'upload.status.cards.photos.remaining' - : 'upload.status.cards.guests.remaining'; + ? 'upload.limitSummary.cards.photos.remaining' + : 'upload.limitSummary.cards.guests.remaining'; const unlimitedKey = id === 'photos' - ? 'upload.status.cards.photos.unlimited' - : 'upload.status.cards.guests.unlimited'; + ? 'upload.limitSummary.cards.photos.unlimited' + : 'upload.limitSummary.cards.guests.unlimited'; const tone = resolveTone(summary.state); const progress = typeof summary.limit === 'number' && summary.limit > 0 @@ -50,7 +50,7 @@ function buildCard( const valueLabel = typeof summary.limit === 'number' && summary.limit > 0 ? `${summary.used.toLocaleString()} / ${summary.limit.toLocaleString()}` - : t('upload.status.badges.unlimited'); + : t('upload.limitSummary.badges.unlimited'); const description = summary.state === 'unlimited' ? t(unlimitedKey) @@ -63,13 +63,13 @@ function buildCard( const badgeKey = (() => { switch (summary.state) { case 'limit_reached': - return 'upload.status.badges.limit_reached'; + return 'upload.limitSummary.badges.limit_reached'; case 'warning': - return 'upload.status.badges.warning'; + return 'upload.limitSummary.badges.warning'; case 'unlimited': - return 'upload.status.badges.unlimited'; + return 'upload.limitSummary.badges.unlimited'; default: - return 'upload.status.badges.ok'; + return 'upload.limitSummary.badges.ok'; } })(); diff --git a/resources/js/guest/pages/AchievementsPage.tsx b/resources/js/guest/pages/AchievementsPage.tsx index 36c71fe..3362d17 100644 --- a/resources/js/guest/pages/AchievementsPage.tsx +++ b/resources/js/guest/pages/AchievementsPage.tsx @@ -269,33 +269,66 @@ function Highlights({ topPhoto, trendingEmotion }: { topPhoto: TopPhotoHighlight ); } -function SummaryCards({ data }: { data: AchievementsPayload }) { +function FlowSummary({ data, token }: { data: AchievementsPayload; token: string }) { + const personal = data.personal; + const tasksDone = personal?.tasks ?? data.summary.tasksSolved; + const photos = personal?.photos ?? data.summary.totalPhotos; + const likes = personal?.likes ?? data.summary.likesTotal; + const guests = data.summary.uniqueGuests; + const earnedBadges = personal?.badges.filter((badge) => badge.earned).length ?? 0; + const nextBadge = personal?.badges.find((badge) => !badge.earned); + return ( -
- - - Fotos gesamt - {formatNumber(data.summary.totalPhotos)} - - - - - Aktive Gäste - {formatNumber(data.summary.uniqueGuests)} - - - - - Erfüllte Aufgaben - {formatNumber(data.summary.tasksSolved)} - - - - - Likes insgesamt - {formatNumber(data.summary.likesTotal)} - - +
+
+
+

Dein Flow

+

+ {tasksDone === 0 ? 'Starte deine erste Mission.' : 'Weiter so, dein Event lebt!'} +

+

+ {nextBadge ? `Noch ${Math.max(0, nextBadge.target - nextBadge.progress)} Schritte bis „${nextBadge.title}“.` : 'Alle aktuellen Badges freigeschaltet.'} +

+
+
+ + +
+
+
+ + + + +
+
+ + +
+
+ ); +} + +function FlowStat({ label, value, muted = false }: { label: string; value: string; muted?: boolean }) { + return ( +
+

{label}

+

{value}

); } @@ -393,7 +426,7 @@ export default function AchievementsPage() { {!loading && !error && data && ( <> - +
- - - {t('home.checklist.title')} - {t('home.checklist.description')} - - - {checklistItems.map((item) => ( -
- - {item} -
- ))} -
-
- - -
); @@ -265,112 +232,119 @@ function HeroCard({ ); } -function StatsRibbon({ - items, - accentColor, - fontFamily, +type MissionPreview = { + id: number; + title: string; + description?: string; + duration?: number; + emotion?: { name?: string; slug?: string } | null; +}; + +function MissionActionCard({ + token, + mission, + loading, + onShuffle, }: { - items: { icon: React.ReactNode; label: string; value: string }[]; - accentColor: string; - fontFamily?: string | null; + token: string; + mission: MissionPreview | null; + loading: boolean; + onShuffle: () => void; }) { return ( -
-
- {items.map((item) => ( -
+ +
+ +
+
+ Mission starten + + Wir haben bereits eine Aufgabe für dich vorbereitet. Tippe, um direkt loszulegen. + +
+
+ + {mission ? ( + <> +

{mission.title}

+ {mission.description && ( +

{mission.description}

+ )} +
+ {mission.duration ?? 3} Min + {mission.emotion?.name && {mission.emotion.name}} +
+ + ) : ( +

+ Ziehe deine erste Mission im Aufgaben-Tab oder lade deine Stimmung hoch. +

+ )} +
+ +
- ))} -
-
+ + Andere Mission + +
+ + ); } -function QuickActionCard({ - action, +function EmotionActionCard() { + return ( + + + Foto nach Gefühlslage + + Wähle deine Stimmung, wir schlagen dir passende Missionen vor. + + + + + + + ); +} + +function UploadActionCard({ + token, accentColor, secondaryAccent, }: { - action: { to: string; label: string; description: string; icon: React.ReactNode; highlight?: boolean }; + token: string; accentColor: string; secondaryAccent: string; }) { - const highlightStyle = action.highlight - ? { - background: `linear-gradient(120deg, ${accentColor}, ${secondaryAccent})`, - color: '#fff', - } - : undefined; - return ( - - - -
- {action.icon} + + +
+
+
-
- - {action.label} - - - {action.description} - +
+

Direkt hochladen

+

Kamera öffnen oder ein Foto aus deiner Galerie wählen.

- - - - +
+ + + ); } - -function formatLatestUpload(isoDate: string | null, t: TranslateFn) { - if (!isoDate) { - return t('home.latestUpload.none'); - } - const date = new Date(isoDate); - if (Number.isNaN(date.getTime())) { - return t('home.latestUpload.invalid'); - } - const diffMs = Date.now() - date.getTime(); - const diffMinutes = Math.round(diffMs / 60000); - if (diffMinutes < 1) { - return t('home.latestUpload.justNow'); - } - if (diffMinutes < 60) { - return t('home.latestUpload.minutes').replace('{count}', `${diffMinutes}`); - } - const diffHours = Math.round(diffMinutes / 60); - if (diffHours < 24) { - return t('home.latestUpload.hours').replace('{count}', `${diffHours}`); - } - const diffDays = Math.round(diffHours / 24); - return t('home.latestUpload.days').replace('{count}', `${diffDays}`); -} diff --git a/resources/js/guest/pages/TaskPickerPage.tsx b/resources/js/guest/pages/TaskPickerPage.tsx index 3c4f819..48bfbcb 100644 --- a/resources/js/guest/pages/TaskPickerPage.tsx +++ b/resources/js/guest/pages/TaskPickerPage.tsx @@ -1,10 +1,15 @@ import React from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { Button } from '@/components/ui/button'; -import { Badge } from '@/components/ui/badge'; import { Alert, AlertDescription } from '@/components/ui/alert'; -import { Sparkles, RefreshCw, Smile, Timer as TimerIcon, CheckCircle2, AlertTriangle } from 'lucide-react'; +import { Sparkles, RefreshCw, Smile, Camera, Timer as TimerIcon, Heart, ChevronRight } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; import { useGuestTaskProgress } from '../hooks/useGuestTaskProgress'; +import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'; +import { cn } from '@/lib/utils'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { useEventBranding } from '../context/EventBrandingContext'; +import { useTranslation, type TranslateFn } from '../i18n/useTranslation'; interface Task { id: number; @@ -24,26 +29,169 @@ type EmotionOption = { name: string; }; -const TASK_PROGRESS_TARGET = 5; -const TIMER_VIBRATION = [0, 60, 120, 60]; +type EventPhoto = { + id: number; + thumbnail_path?: string | null; + file_path?: string | null; + likes_count?: number | null; + task_id?: number | null; +}; + +const SWIPE_THRESHOLD_PX = 40; +const SIMILAR_PHOTO_LIMIT = 6; +type EmotionTheme = { + gradientClass: string; + gradientBackground: string; + suggestionGradient: string; + suggestionBorder: string; +}; + +type EmotionIdentity = { + slug?: string | null; + name?: string | null; +}; + +const themeFreude: EmotionTheme = { + gradientClass: 'from-amber-300 via-orange-400 to-rose-500', + gradientBackground: 'linear-gradient(135deg, #fde68a, #fb923c, #fb7185)', + suggestionGradient: 'from-amber-50 via-white to-orange-50 dark:from-amber-500/20 dark:via-gray-900 dark:to-orange-500/10', + suggestionBorder: 'border-amber-200/60 dark:border-amber-400/30', +}; +const themeLiebe: EmotionTheme = { + gradientClass: 'from-rose-400 via-fuchsia-500 to-purple-600', + gradientBackground: 'linear-gradient(135deg, #fb7185, #ec4899, #9333ea)', + suggestionGradient: 'from-rose-50 via-white to-fuchsia-50 dark:from-rose-500/20 dark:via-gray-900 dark:to-fuchsia-500/10', + suggestionBorder: 'border-rose-200/60 dark:border-rose-400/30', +}; +const themeEkstase: EmotionTheme = { + gradientClass: 'from-fuchsia-400 via-purple-500 to-indigo-500', + gradientBackground: 'linear-gradient(135deg, #f472b6, #a855f7, #6366f1)', + suggestionGradient: 'from-fuchsia-50 via-white to-indigo-50 dark:from-fuchsia-500/20 dark:via-gray-900 dark:to-indigo-500/10', + suggestionBorder: 'border-fuchsia-200/60 dark:border-fuchsia-400/30', +}; +const themeEntspannt: EmotionTheme = { + gradientClass: 'from-teal-300 via-emerald-400 to-cyan-500', + gradientBackground: 'linear-gradient(135deg, #5eead4, #34d399, #22d3ee)', + suggestionGradient: 'from-teal-50 via-white to-emerald-50 dark:from-teal-500/20 dark:via-gray-900 dark:to-emerald-500/10', + suggestionBorder: 'border-emerald-200/60 dark:border-emerald-400/30', +}; +const themeBesinnlich: EmotionTheme = { + gradientClass: 'from-slate-500 via-blue-500 to-indigo-600', + gradientBackground: 'linear-gradient(135deg, #64748b, #3b82f6, #4f46e5)', + suggestionGradient: 'from-slate-50 via-white to-blue-50 dark:from-slate-600/20 dark:via-gray-900 dark:to-blue-500/10', + suggestionBorder: 'border-slate-200/60 dark:border-slate-500/30', +}; +const themeUeberraschung: EmotionTheme = { + gradientClass: 'from-indigo-300 via-violet-500 to-rose-500', + gradientBackground: 'linear-gradient(135deg, #a5b4fc, #a855f7, #fb7185)', + suggestionGradient: 'from-indigo-50 via-white to-violet-50 dark:from-indigo-500/20 dark:via-gray-900 dark:to-violet-500/10', + suggestionBorder: 'border-indigo-200/60 dark:border-indigo-400/30', +}; +const themeDefault: EmotionTheme = { + gradientClass: 'from-pink-500 via-purple-500 to-indigo-600', + gradientBackground: 'linear-gradient(135deg, #ec4899, #a855f7, #4f46e5)', + suggestionGradient: 'from-pink-50 via-white to-indigo-50 dark:from-pink-500/20 dark:via-gray-900 dark:to-indigo-500/10', + suggestionBorder: 'border-pink-200/60 dark:border-pink-400/30', +}; + +const EMOTION_THEMES: Record = { + freude: themeFreude, + happy: themeFreude, + liebe: themeLiebe, + romance: themeLiebe, + romantik: themeLiebe, + nostalgie: themeEntspannt, + relaxed: themeEntspannt, + ruehrung: themeBesinnlich, + traurigkeit: themeBesinnlich, + teamgeist: themeFreude, + gemeinschaft: themeFreude, + ueberraschung: themeUeberraschung, + surprise: themeUeberraschung, + ekstase: themeEkstase, + excited: themeEkstase, + besinnlichkeit: themeBesinnlich, + sad: themeBesinnlich, + default: themeDefault, +}; + +const EMOTION_ICONS: Record = { + freude: '😊', + happy: '😊', + liebe: '❤️', + romantik: '💞', + nostalgie: '📼', + ruehrung: '🥲', + teamgeist: '🤝', + ueberraschung: '😲', + surprise: '😲', + ekstase: '🤩', + besinnlichkeit: '🕯️', +}; + +function sluggify(value?: string | null): string { + return (value ?? '') + .toString() + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-z0-9]+/g, '') + .trim(); +} + +function resolveEmotionKey(identity?: EmotionIdentity | null): string { + if (!identity) return 'default'; + const nameKey = sluggify(identity.name); + if (nameKey && EMOTION_THEMES[nameKey]) { + return nameKey; + } + const slugKey = sluggify(identity.slug); + if (slugKey && EMOTION_THEMES[slugKey]) { + return slugKey; + } + return 'default'; +} + +function getEmotionTheme(identity?: EmotionIdentity | null): EmotionTheme { + const key = resolveEmotionKey(identity); + return EMOTION_THEMES[key] ?? themeDefault; +} + +function getEmotionIcon(identity?: EmotionIdentity | null): string { + const key = resolveEmotionKey(identity); + return EMOTION_ICONS[key] ?? '✨'; +} export default function TaskPickerPage() { const { token } = useParams<{ token: string }>(); const eventKey = token ?? ''; const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); + const { branding } = useEventBranding(); + const { t } = useTranslation(); - const { completedCount, markCompleted, isCompleted } = useGuestTaskProgress(eventKey); + const { completedCount, isCompleted } = useGuestTaskProgress(eventKey); const [tasks, setTasks] = React.useState([]); const [currentTask, setCurrentTask] = React.useState(null); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [selectedEmotion, setSelectedEmotion] = React.useState('all'); - const [timeLeft, setTimeLeft] = React.useState(0); - const [timerRunning, setTimerRunning] = React.useState(false); - const [timeUp, setTimeUp] = React.useState(false); const [isFetching, setIsFetching] = React.useState(false); + const [photoPool, setPhotoPool] = React.useState([]); + const [photoPoolLoading, setPhotoPoolLoading] = React.useState(false); + const [photoPoolError, setPhotoPoolError] = React.useState(null); + const [hasSwiped, setHasSwiped] = React.useState(false); + const [emotionPickerOpen, setEmotionPickerOpen] = React.useState(false); + const [recentEmotionSlug, setRecentEmotionSlug] = React.useState(null); + + const heroCardRef = React.useRef(null); + + const cameraButtonStyle = React.useMemo(() => ({ + background: `radial-gradient(circle at 20% 20%, ${branding.secondaryColor}, ${branding.primaryColor})`, + boxShadow: `0 18px 30px ${branding.primaryColor}44`, + color: '#ffffff', + }), [branding.primaryColor, branding.secondaryColor]); const recentTaskIdsRef = React.useRef([]); const initialEmotionRef = React.useRef(false); @@ -95,11 +243,25 @@ export default function TaskPickerPage() { return Array.from(map.entries()).map(([slugValue, name]) => ({ slug: slugValue, name })); }, [tasks]); + const emotionCounts = React.useMemo(() => { + const map = new Map(); + tasks.forEach((task) => { + const slugValue = task.emotion?.slug; + if (!slugValue) return; + map.set(slugValue, (map.get(slugValue) ?? 0) + 1); + }); + return map; + }, [tasks]); + const filteredTasks = React.useMemo(() => { if (selectedEmotion === 'all') return tasks; return tasks.filter((task) => task.emotion?.slug === selectedEmotion); }, [tasks, selectedEmotion]); + const alternativeTasks = React.useMemo(() => { + return filteredTasks.filter((task) => task.id !== currentTask?.id).slice(0, 6); + }, [filteredTasks, currentTask]); + const selectRandomTask = React.useCallback( (list: Task[]) => { if (!list.length) { @@ -120,6 +282,51 @@ export default function TaskPickerPage() { [isCompleted] ); + const handleSelectEmotion = React.useCallback( + (slugValue: string) => { + setSelectedEmotion(slugValue); + const next = new URLSearchParams(searchParams.toString()); + if (slugValue === 'all') { + next.delete('emotion'); + } else { + next.set('emotion', slugValue); + setRecentEmotionSlug(slugValue); + } + setSearchParams(next, { replace: true }); + }, + [searchParams, setSearchParams] + ); + + const handleNewTask = React.useCallback(() => { + selectRandomTask(filteredTasks); + }, [filteredTasks, selectRandomTask]); + + const handleStartUpload = () => { + if (!currentTask || !eventKey) return; + navigate(`/e/${encodeURIComponent(eventKey)}/upload?task=${currentTask.id}&emotion=${currentTask.emotion?.slug || ''}`); + }; + + const handleViewSimilar = React.useCallback(() => { + if (!currentTask || !eventKey) return; + navigate(`/e/${encodeURIComponent(eventKey)}/gallery?task=${currentTask.id}`); + }, [currentTask, eventKey, navigate]); + + const handleSelectTask = React.useCallback((task: Task) => { + setCurrentTask(task); + }, []); + + const handleRetryFetch = () => { + fetchTasks(); + }; + + const handlePhotoPreview = React.useCallback( + (photoId: number) => { + if (!eventKey) return; + navigate(`/e/${encodeURIComponent(eventKey)}/gallery?photoId=${photoId}&task=${currentTask?.id ?? ''}`); + }, + [eventKey, navigate, currentTask?.id] + ); + React.useEffect(() => { if (!filteredTasks.length) { setCurrentTask(null); @@ -127,158 +334,164 @@ export default function TaskPickerPage() { } if (!currentTask || !filteredTasks.some((task) => task.id === currentTask.id)) { selectRandomTask(filteredTasks); - return; } - const matchingTask = filteredTasks.find((task) => task.id === currentTask.id); - const durationMinutes = matchingTask?.duration ?? currentTask.duration; - setTimeLeft(durationMinutes * 60); - setTimerRunning(false); - setTimeUp(false); }, [filteredTasks, currentTask, selectRandomTask]); React.useEffect(() => { - if (!currentTask) { - setTimeLeft(0); - setTimerRunning(false); - setTimeUp(false); - return; + if (currentTask?.emotion?.slug) { + setRecentEmotionSlug(currentTask.emotion.slug); } - setTimeLeft(currentTask.duration * 60); - setTimerRunning(false); - setTimeUp(false); - }, [currentTask]); + }, [currentTask?.emotion?.slug]); React.useEffect(() => { - if (!timerRunning) return; - if (timeLeft <= 0) { - setTimerRunning(false); - triggerTimeUp(); - return; - } - const tick = window.setInterval(() => { - setTimeLeft((prev) => { - if (prev <= 1) { - window.clearInterval(tick); - triggerTimeUp(); - return 0; + if (!eventKey || photoPool.length) return; + const controller = new AbortController(); + setPhotoPoolLoading(true); + setPhotoPoolError(null); + fetch(`/api/v1/events/${encodeURIComponent(eventKey)}/photos?locale=de`, { + signal: controller.signal, + }) + .then((res) => { + if (!res.ok) { + throw new Error(t('tasks.page.inspirationError')); + } + return res.json(); + }) + .then((payload) => { + const data = Array.isArray(payload?.data) ? (payload.data as EventPhoto[]) : []; + setPhotoPool(data); + }) + .catch((err) => { + if (controller.signal.aborted) return; + console.error('Failed to load photos', err); + setPhotoPoolError(t('tasks.page.inspirationError')); + }) + .finally(() => { + if (!controller.signal.aborted) { + setPhotoPoolLoading(false); } - return prev - 1; }); - }, 1000); - return () => window.clearInterval(tick); - }, [timerRunning, timeLeft]); - function triggerTimeUp() { - const supportsVibration = typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function'; - setTimerRunning(false); - setTimeUp(true); - if (supportsVibration) { - try { - navigator.vibrate(TIMER_VIBRATION); - } catch (error) { - console.warn('Vibration not permitted', error); - } - } - window.setTimeout(() => setTimeUp(false), 4000); - return; -} + return () => controller.abort(); + }, [eventKey, photoPool.length, t]); - const formatTime = React.useCallback((seconds: number) => { - const mins = Math.floor(seconds / 60); - const secs = Math.max(0, seconds % 60); - return `${mins}:${secs.toString().padStart(2, '0')}`; - }, []); + const similarPhotos = React.useMemo(() => { + if (!currentTask) return []; + const matches = photoPool.filter((photo) => photo.task_id === currentTask.id); + return matches.slice(0, SIMILAR_PHOTO_LIMIT); + }, [photoPool, currentTask]); - const progressRatio = currentTask ? Math.min(1, completedCount / TASK_PROGRESS_TARGET) : 0; + React.useEffect(() => { + const card = heroCardRef.current; + if (!card) return; + let startX: number | null = null; + let startY: number | null = null; - const handleSelectEmotion = (slugValue: string) => { - setSelectedEmotion(slugValue); - const next = new URLSearchParams(searchParams.toString()); - if (slugValue === 'all') { - next.delete('emotion'); - } else { - next.set('emotion', slugValue); - } - setSearchParams(next, { replace: true }); - }; + const onTouchStart = (event: TouchEvent) => { + const touch = event.touches[0]; + startX = touch.clientX; + startY = touch.clientY; + }; - const handleNewTask = () => { - selectRandomTask(filteredTasks); - }; - - const handleStartUpload = () => { - if (!currentTask || !eventKey) return; - if (!eventKey) return; - navigate(`/e/${encodeURIComponent(eventKey)}/upload?task=${currentTask.id}&emotion=${currentTask.emotion?.slug || ''}`); - }; - - const handleMarkCompleted = () => { - if (!currentTask) return; - markCompleted(currentTask.id); - selectRandomTask(filteredTasks); - }; - - const handleRetryFetch = () => { - fetchTasks(); - }; - - const handleTimerToggle = () => { - if (!currentTask) return; - if (timerRunning) { - setTimerRunning(false); - setTimeLeft(currentTask.duration * 60); - setTimeUp(false); - } else { - if (timeLeft <= 0) { - setTimeLeft(currentTask.duration * 60); + const onTouchEnd = (event: TouchEvent) => { + if (startX === null || startY === null) return; + const touch = event.changedTouches[0]; + const deltaX = touch.clientX - startX; + const deltaY = touch.clientY - startY; + if (Math.abs(deltaX) > SWIPE_THRESHOLD_PX && Math.abs(deltaY) < 60) { + if (deltaX < 0) { + handleNewTask(); + } else { + handleViewSimilar(); + } + setHasSwiped(true); } - setTimerRunning(true); - setTimeUp(false); - } - }; + startX = null; + startY = null; + }; + + card.addEventListener('touchstart', onTouchStart, { passive: true }); + card.addEventListener('touchend', onTouchEnd); + return () => { + card.removeEventListener('touchstart', onTouchStart); + card.removeEventListener('touchend', onTouchEnd); + }; + }, [handleNewTask, handleViewSimilar]); const emptyState = !loading && (!filteredTasks.length || !currentTask); + const heroTheme = React.useMemo(() => getEmotionTheme(currentTask?.emotion ?? null), [currentTask?.emotion]); + const heroEmotionIcon = getEmotionIcon(currentTask?.emotion ?? null); + const recentEmotionOption = React.useMemo( + () => emotionOptions.find((option) => option.slug === recentEmotionSlug) ?? null, + [emotionOptions, recentEmotionSlug] + ); + const toggleValue = selectedEmotion === 'all' ? 'none' : 'recent'; + + const handleToggleChange = React.useCallback( + (value: string) => { + if (!value) return; + if (value === 'picker') { + setEmotionPickerOpen(true); + return; + } + if (value === 'none') { + handleSelectEmotion('all'); + return; + } + if (value === 'recent') { + if (recentEmotionSlug) { + handleSelectEmotion(recentEmotionSlug); + } else { + setEmotionPickerOpen(true); + } + } + }, + [handleSelectEmotion, recentEmotionSlug] + ); return ( -
-
-
-

Aufgabe auswählen

- - Schon {completedCount} Aufgaben erledigt - -
-
-
- Auf dem Weg zum nächsten Erfolg - - {completedCount >= TASK_PROGRESS_TARGET - ? 'Stark!' - : `${Math.max(0, TASK_PROGRESS_TARGET - completedCount)} bis zum Badge`} - -
-
-
-
+ <> +
+
+
+

{t('tasks.page.eyebrow')}

+

{t('tasks.page.title')}

+

{t('tasks.page.subtitle')}

{emotionOptions.length > 0 && ( -
- handleSelectEmotion('all')} - /> - {emotionOptions.map((emotion) => ( - handleSelectEmotion(emotion.slug)} - /> - ))} +
+ + + 🎲 + {t('tasks.page.filters.none')} + + + {getEmotionIcon(recentEmotionOption)} + {recentEmotionOption?.name ?? t('tasks.page.filters.recentFallback')} + + + 🗂️ + {t('tasks.page.filters.showAll')} + +
)}
@@ -307,178 +520,182 @@ export default function TaskPickerPage() { onRetry={handleRetryFetch} emotionOptions={emotionOptions} onEmotionSelect={handleSelectEmotion} + t={t} /> )} {!emptyState && currentTask && ( -
-
-
-
-
- -

Deine Mission

-

{currentTask.title}

-
-
-
- - {timeUp && ( - - - Zeit abgelaufen! - - )} -
-
- -
-
- +
+
+
+
+ + + {heroEmotionIcon} {currentTask.emotion?.name ?? 'Neue Mission'} + + {currentTask.duration} Min - - {currentTask.emotion?.name && ( - - - {currentTask.emotion.name} - - )} - {isCompleted(currentTask.id) && ( - - - Bereits erledigt - - )} +
-

{currentTask.description}

+
+

{currentTask.title}

+

{currentTask.description}

+
+ + {!hasSwiped && ( +

+ {t('tasks.page.swipeHint')} +

+ )} {currentTask.instructions && ( -
+
{currentTask.instructions}
)} -
    - - - -
+
+ {isCompleted(currentTask.id) && ( + + + {t('tasks.page.completedLabel')} + + )} +
- {timerRunning && currentTask.duration > 0 && ( -
-
- Countdown - Restzeit: {formatTime(timeLeft)} -
-
-
+
+ + +
+ + {(photoPoolLoading || photoPoolError || similarPhotos.length > 0) && ( +
+
+ {t('tasks.page.inspirationTitle')} + {photoPoolLoading && {t('tasks.page.inspirationLoading')}}
+ {photoPoolError && similarPhotos.length === 0 ? ( +

{photoPoolError}

+ ) : similarPhotos.length > 0 ? ( +
+ {similarPhotos.map((photo) => ( + + ))} + +
+ ) : ( + + )}
)}
-
+ -
- - -
- -
- - -
+ {alternativeTasks.length > 0 && ( +
+
+
+

{t('tasks.page.suggestionsEyebrow')}

+

{t('tasks.page.suggestionsTitle')}

+
+ +
+
+ {alternativeTasks.map((task) => ( + + ))} +
+
+ )}
)} {!loading && !tasks.length && !error && ( - Für dieses Event sind derzeit keine Aufgaben hinterlegt. + {t('tasks.page.noTasksAlert')} )} -
- ); -} - -function timerTone(timeLeft: number, durationMinutes: number) { - const totalSeconds = Math.max(1, durationMinutes * 60); - const ratio = timeLeft / totalSeconds; - if (ratio > 0.5) return 'okay'; - if (ratio > 0.25) return 'warm'; - return 'hot'; -} - -function EmotionChip({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) { - return ( - - ); -} - -function ChecklistItem({ text }: { text: string }) { - return ( -
  • - - {text} -
  • - ); -} - -function BadgeTimer({ label, value, tone }: { label: string; value: string; tone: 'okay' | 'warm' | 'hot' }) { - const toneClasses = { - okay: 'bg-emerald-500/15 text-emerald-500 border-emerald-500/30', - warm: 'bg-amber-500/15 text-amber-500 border-amber-500/30', - hot: 'bg-rose-500/15 text-rose-500 border-rose-500/30', - }[tone]; - return ( -
    - - {label} - {value} -
    +
    + + + + {t('tasks.page.filters.dialogTitle')} + + {emotionOptions.length ? ( +
    + {emotionOptions.map((emotion) => { + const count = emotionCounts.get(emotion.slug) ?? 0; + return ( + + ); + })} +
    + ) : ( +

    {t('tasks.page.filters.empty')}

    + )} +
    +
    + ); } @@ -491,38 +708,129 @@ function EmptyState({ onRetry, emotionOptions, onEmotionSelect, + t, }: { hasTasks: boolean; onRetry: () => void; emotionOptions: EmotionOption[]; onEmotionSelect: (slug: string) => void; + t: TranslateFn; }) { return (
    - +
    -

    Keine passende Aufgabe gefunden

    +

    {t('tasks.page.emptyTitle')}

    - {hasTasks - ? 'Für deine aktuelle Stimmung gibt es gerade keine Aufgabe. Wähle eine andere Stimmung oder lade neue Aufgaben.' - : 'Hier sind noch keine Aufgaben hinterlegt. Bitte versuche es später erneut.'} + {hasTasks ? t('tasks.page.emptyDescriptionWithTasks') : t('tasks.page.emptyDescriptionNoTasks')}

    {hasTasks && emotionOptions.length > 0 && ( -
    +
    {emotionOptions.map((emotion) => ( - onEmotionSelect(emotion.slug)} - /> + className="rounded-full border border-border px-4 py-1 text-sm text-muted-foreground transition hover:border-pink-400 hover:text-foreground" + > + {emotion.name} + ))}
    )}
    ); } + +function HeroActionButton({ + icon: Icon, + label, + detail, + onClick, + className, +}: { + icon: LucideIcon; + label: string; + detail?: string; + onClick: () => void; + className?: string; +}) { + return ( + + ); +} + +function SimilarPhotoChip({ photo, onOpen }: { photo: EventPhoto; onOpen: (photoId: number) => void }) { + const cover = photo.thumbnail_path || photo.file_path || ''; + return ( + + ); +} + +function TaskSuggestionCard({ task, onSelect }: { task: Task; onSelect: (task: Task) => void }) { + const theme = getEmotionTheme(task.emotion ?? null); + const emotionIcon = getEmotionIcon(task.emotion ?? null); + return ( + + ); +} diff --git a/resources/js/guest/pages/UploadPage.tsx b/resources/js/guest/pages/UploadPage.tsx index 2cbe983..2aa202d 100644 --- a/resources/js/guest/pages/UploadPage.tsx +++ b/resources/js/guest/pages/UploadPage.tsx @@ -29,9 +29,10 @@ import { ZapOff, } from 'lucide-react'; import { getEventPackage, type EventPackage } from '../services/eventApi'; -import { useTranslation } from '../i18n/useTranslation'; +import { useTranslation, type TranslateFn } from '../i18n/useTranslation'; import { buildLimitSummaries, type LimitSummaryCard } from '../lib/limitSummaries'; import { resolveUploadErrorDialog, type UploadErrorDialog } from '../lib/uploadErrorDialog'; +import { useEventStats } from '../context/EventStatsContext'; interface Task { id: number; @@ -102,13 +103,15 @@ const LIMIT_CARD_STYLES: Record(); const eventKey = token ?? ''; const navigate = useNavigate(); const [searchParams] = useSearchParams(); - const { markCompleted } = useGuestTaskProgress(token); + const { markCompleted, completedCount } = useGuestTaskProgress(token); const { t } = useTranslation(); + const stats = useEventStats(); const taskIdParam = searchParams.get('task'); const emotionSlug = searchParams.get('emotion') || ''; @@ -605,6 +608,11 @@ const [canUpload, setCanUpload] = useState(true); reader.readAsDataURL(file); }, [canUpload, t]); + const handleOpenInspiration = useCallback(() => { + if (!eventKey) return; + navigate(`/e/${encodeURIComponent(eventKey)}/gallery`); + }, [eventKey, navigate]); + const difficultyBadgeClass = useMemo(() => { if (!task) return 'text-white'; switch (task.difficulty) { @@ -620,6 +628,10 @@ const [canUpload, setCanUpload] = useState(true); const isCameraActive = permissionState === 'granted' && mode !== 'uploading'; const showTaskOverlay = task && mode !== 'uploading'; + const relativeLastUpload = useMemo( + () => formatRelativeTimeLabel(stats.latestPhotoAt, t), + [stats.latestPhotoAt, t], + ); useEffect(() => () => { resetCountdownTimer(); @@ -628,24 +640,31 @@ const [canUpload, setCanUpload] = useState(true); } }, [resetCountdownTimer]); + const heroEmotion = task?.emotion?.name ?? t('upload.hud.moodFallback'); + const limitStatusSection = limitCards.length > 0 ? ( -
    -
    -

    - {t('upload.status.title')} -

    -

    - {t('upload.status.subtitle')} -

    +
    +
    +
    +

    + {t('upload.limitSummary.title')} +

    +

    + {t('upload.limitSummary.subtitle')} +

    +
    + + {t('upload.limitSummary.badgeLabel')} +
    -
    +
    {limitCards.map((card) => { const styles = LIMIT_CARD_STYLES[card.tone]; return (
    @@ -676,14 +695,6 @@ const [canUpload, setCanUpload] = useState(true);
    ) : null; - const renderPage = (content: ReactNode, mainClassName = 'px-4 py-6') => ( -
    -
    -
    {content}
    - -
    - ); - const dialogToneIconClass: Record, string> = { danger: 'text-rose-500', warning: 'text-amber-500', @@ -714,12 +725,18 @@ const [canUpload, setCanUpload] = useState(true); ); - const renderWithDialog = (content: ReactNode, mainClassName = 'px-4 py-6') => ( - <> - {renderPage(content, mainClassName)} - {errorDialogNode} - - ); +const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[120px]') => ( + <> +
    +
    +
    +
    {content}
    +
    + +
    + {errorDialogNode} + +); if (!supportsCamera && !task) { return renderWithDialog( @@ -759,7 +776,7 @@ const [canUpload, setCanUpload] = useState(true); const renderPrimer = () => ( showPrimer && ( -
    +
    @@ -781,14 +798,14 @@ const [canUpload, setCanUpload] = useState(true); if (permissionState === 'granted') return null; if (permissionState === 'unsupported') { return ( - - {t('upload.cameraUnsupported.message')} + + {t('upload.cameraUnsupported.message')} ); } if (permissionState === 'denied' || permissionState === 'error') { return ( - +
    {permissionMessage}