enhancements of the homepage in the guest pwa
This commit is contained in:
92
package-lock.json
generated
92
package-lock.json
generated
@@ -26,6 +26,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.2",
|
"@radix-ui/react-toggle": "^1.1.2",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@react-spring/web": "^10.0.3",
|
||||||
"@stripe/stripe-js": "^8.5.3",
|
"@stripe/stripe-js": "^8.5.3",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@tamagui/button": "~1.139.2",
|
"@tamagui/button": "~1.139.2",
|
||||||
@@ -42,6 +43,7 @@
|
|||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@use-gesture/react": "^10.3.1",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -4810,6 +4812,78 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-spring/animated": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-7MrxADV3vaUADn2V9iYhaIL6iOWRx9nCJjYrsk2AHD2kwPr6fg7Pt0v+deX5RnCDmCKNnD6W5fasiyM8D+wzJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/shared": "~10.0.3",
|
||||||
|
"@react-spring/types": "~10.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/core": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-D4DwNO68oohDf/0HG2G0Uragzb9IA1oXblxrd6MZAcBcUQG2EHUWXewjdECMPLNmQvlYVyyBRH6gPxXM5DX7DQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/animated": "~10.0.3",
|
||||||
|
"@react-spring/shared": "~10.0.3",
|
||||||
|
"@react-spring/types": "~10.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-spring/donate"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/rafz": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-Ri2/xqt8OnQ2iFKkxKMSF4Nqv0LSWnxXT4jXFzBDsHgeeH/cHxTLupAWUwmV9hAGgmEhBmh5aONtj3J6R/18wg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/shared": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-geCal66nrkaQzUVhPkGomylo+Jpd5VPK8tPMEDevQEfNSWAQP15swHm+MCRG4wVQrQlTi9lOzKzpRoTL3CA84Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/rafz": "~10.0.3",
|
||||||
|
"@react-spring/types": "~10.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/types": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-H5Ixkd2OuSIgHtxuHLTt7aJYfhMXKXT/rK32HPD/kSrOB6q6ooeiWAXkBy7L8F3ZxdkBb9ini9zP9UwnEFzWgQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/web": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-ndU+kWY81rHsT7gTFtCJ6mrVhaJ6grFmgTnENipzmKqot4HGf5smPNK+cZZJqoGeDsj9ZsiWPW4geT/NyD484A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/animated": "~10.0.3",
|
||||||
|
"@react-spring/core": "~10.0.3",
|
||||||
|
"@react-spring/shared": "~10.0.3",
|
||||||
|
"@react-spring/types": "~10.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-stately/flags": {
|
"node_modules/@react-stately/flags": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
|
||||||
@@ -7667,6 +7741,24 @@
|
|||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@use-gesture/core": {
|
||||||
|
"version": "10.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
|
||||||
|
"integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@use-gesture/react": {
|
||||||
|
"version": "10.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
|
||||||
|
"integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@use-gesture/core": "10.3.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.2",
|
"@radix-ui/react-toggle": "^1.1.2",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@react-spring/web": "^10.0.3",
|
||||||
"@stripe/stripe-js": "^8.5.3",
|
"@stripe/stripe-js": "^8.5.3",
|
||||||
"@tailwindcss/vite": "^4.1.17",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"@tamagui/button": "~1.139.2",
|
"@tamagui/button": "~1.139.2",
|
||||||
@@ -79,6 +80,7 @@
|
|||||||
"@tanstack/react-query": "^5.90.12",
|
"@tanstack/react-query": "^5.90.12",
|
||||||
"@types/react": "^19.2.7",
|
"@types/react": "^19.2.7",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@use-gesture/react": "^10.3.1",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@vitejs/plugin-react": "^4.7.0",
|
||||||
"canvas-confetti": "^1.9.4",
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -95,9 +97,9 @@
|
|||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"laravel-vite-plugin": "^2.0.1",
|
"laravel-vite-plugin": "^2.0.1",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"react-colorful": "^5.6.1",
|
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-hot-toast": "^2.6.0",
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-i18next": "^16.4.1",
|
"react-i18next": "^16.4.1",
|
||||||
|
|||||||
1
public/patterns/pattern-hearts-dark.svg
Normal file
1
public/patterns/pattern-hearts-dark.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="58" height="87"><rect width="100%" height="100%" fill="#2b2b31"/><path fill="none" stroke="#ecc94b" stroke-linecap="square" d="M43.5 5.829a2.125 2.125 0 1 0 0 4.25 2.125 2.125 0 0 0 0-4.25zm-25.057 5.67a2.125 2.125 0 1 0 0 4.251 2.125 2.125 0 0 0 0-4.25zM14.5 49.33a2.125 2.125 0 1 0 0 4.25 2.125 2.125 0 0 0 0-4.25zM47.443 55a2.125 2.125 0 1 0 0 4.25 2.125 2.125 0 0 0 0-4.25z"/><path fill="none" stroke="#f44034" stroke-linecap="square" d="M9.75 36.005h-4.5m2.25-2.25v4.5m35.572-12.63h-4.5m2.25-2.25v4.5m-26.75 41.25h-4.5m2.25-2.25v4.5m26.928 8.13h-4.5m2.25-2.25v4.5"/><path fill="none" stroke="#00bdd6" stroke-linecap="square" d="m9.123-3.597-.005.002A5.26 5.26 0 0 0 7.581-7.1a5.283 5.283 0 0 0-7.47 0L0-6.99l-.11-.11a5.283 5.283 0 0 0-7.472 0 5.26 5.26 0 0 0-1.536 3.505l-.005-.002s-.899 5.66 9.112 11.543l.008.008q.002 0 .002-.002l.002.002.008-.008C10.022 2.063 9.123-3.597 9.123-3.597zm58 0-.005.002A5.26 5.26 0 0 0 65.582-7.1a5.283 5.283 0 0 0-7.471 0L58-6.99l-.11-.11a5.283 5.283 0 0 0-7.471 0 5.26 5.26 0 0 0-1.537 3.505l-.006-.002s-.899 5.66 9.113 11.543l.008.008s.002 0 .002-.002l.002.002.008-.008C68.02 2.063 67.122-3.597 67.122-3.597zm-29 43.5-.005.002a5.26 5.26 0 0 0-1.536-3.505 5.283 5.283 0 0 0-7.471 0l-.111.11-.11-.11a5.283 5.283 0 0 0-7.472 0 5.26 5.26 0 0 0-1.536 3.505l-.005-.002s-.899 5.66 9.113 11.543l.008.008s.002 0 .002-.002l.002.002.008-.008c10.012-5.883 9.113-11.543 9.113-11.543zm-29 43.5h-.005A5.26 5.26 0 0 0 7.581 79.9a5.283 5.283 0 0 0-7.47 0L0 80.01l-.11-.11a5.283 5.283 0 0 0-7.472 0 5.26 5.26 0 0 0-1.536 3.505l-.005-.001s-.899 5.66 9.112 11.543l.008.008q.002 0 .002-.002l.002.002.008-.008c10.013-5.883 9.114-11.543 9.114-11.543zm58 0h-.005a5.26 5.26 0 0 0-1.536-3.504 5.283 5.283 0 0 0-7.471 0l-.111.11-.11-.11a5.283 5.283 0 0 0-7.471 0 5.26 5.26 0 0 0-1.537 3.505l-.006-.001s-.899 5.66 9.113 11.543l.008.008s.002 0 .002-.002l.002.002.008-.008c10.012-5.883 9.113-11.543 9.113-11.543z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
1
public/patterns/pattern-hearts-red.svg
Normal file
1
public/patterns/pattern-hearts-red.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="53.101" height="79.65"><rect width="100%" height="100%" fill="#e4446e"/><path fill="none" stroke="#ecc94b" d="M57.589 69.148 45.16 81.466M55.4 65.24 41.735 78.784M50.49 64.03 38.873 75.544m5.424-11.453L37 71.324m20.642-1.662-.006.002a5.98 5.98 0 0 0-1.747-3.985 6.006 6.006 0 0 0-8.495 0l-.125.125-.125-.125a6.006 6.006 0 0 0-8.495 0 5.98 5.98 0 0 0-1.747 3.985l-.006-.002s-1.022 6.436 10.362 13.126l.009.009.003-.002.003.002.009-.01c11.382-6.689 10.36-13.125 10.36-13.125Zm-.053-80.164L45.16 1.816M55.4-14.408 41.735-.866M50.49-15.62 38.873-4.106m5.424-11.453L37-8.326m20.642-1.662-.006.002a5.98 5.98 0 0 0-1.747-3.985 6.006 6.006 0 0 0-8.495 0l-.125.125-.125-.125a6.006 6.006 0 0 0-8.495 0 5.98 5.98 0 0 0-1.747 3.985l-.006-.002S35.874-3.552 47.258 3.138l.009.009.003-.002.003.002.009-.01c11.382-6.689 10.36-13.125 10.36-13.125ZM4.488 69.148-7.94 81.466M2.299 65.24l-13.663 13.543m8.752-14.753-11.615 11.513m5.423-11.453-7.297 7.233m20.642-1.662-.006.002a5.98 5.98 0 0 0-1.746-3.985 6.006 6.006 0 0 0-8.495 0l-.125.125-.126-.125a6.006 6.006 0 0 0-8.495 0 5.98 5.98 0 0 0-1.746 3.985l-.006-.002s-1.023 6.436 10.361 13.126l.01.009.002-.002.003.002.01-.01c11.382-6.689 10.36-13.125 10.36-13.125ZM33.283 29.32 20.854 41.639m10.24-16.224L17.43 38.957m8.753-14.753L14.567 35.717m5.424-11.452-7.297 7.232m20.642-1.662-.006.002a5.98 5.98 0 0 0-1.747-3.985 6.006 6.006 0 0 0-8.494 0l-.126.125-.125-.125a6.006 6.006 0 0 0-8.495 0 5.98 5.98 0 0 0-1.747 3.985l-.006-.002s-1.022 6.437 10.362 13.126l.009.009.003-.002.003.002.009-.01c11.382-6.688 10.36-13.125 10.36-13.125ZM4.488-10.502-7.94 1.816M2.299-14.408-11.364-.866m8.752-14.753L-14.227-4.106m5.423-11.453-7.297 7.233M4.541-9.988l-.006.002a5.98 5.98 0 0 0-1.746-3.985 6.006 6.006 0 0 0-8.495 0l-.125.125-.126-.125a6.006 6.006 0 0 0-8.495 0 5.98 5.98 0 0 0-1.746 3.985l-.006-.002S-17.227-3.552-5.843 3.138l.01.009.002-.002.003.002.01-.01C5.563-3.551 4.541-9.987 4.541-9.987Z"/><path fill="none" stroke="#4051b5" d="M31.106 13.62h4m-2-2v4m9.462 11.313h4m-2-2v4M6.806 53.445h4m-2-2v4m4.962 11.313h4m-2-2v4"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
1
public/patterns/pattern-triangles-dark.svg
Normal file
1
public/patterns/pattern-triangles-dark.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="35.783" height="83.523"><rect width="100%" height="100%"/><path fill="none" stroke="#ecc94b" stroke-linecap="square" stroke-width=".5" d="m14.13 80.123 3.694-8.621-3.694-8.621-3.694 8.62zM7.597 78.13l6.533 15.25 6.533-15.25-2.84-6.628-3.693 8.621-3.694-8.621zm0 0-2.84 6.628 9.373 21.878 9.372-21.878-2.84-6.628-6.532 15.25zm24.425-39.768 3.693-8.622-3.693-8.62-3.694 8.62zm-6.534-1.993 6.534 15.249 6.532-15.25-2.839-6.628-3.693 8.622-3.694-8.622zm0 0-2.84 6.628 9.374 21.877 9.372-21.877-2.84-6.628-6.532 15.249zM19.81 49.625 32.022 78.13l12.212-28.505-2.84-6.628-9.372 21.877-9.373-21.877zm0 0-2.84 6.628 15.053 35.133 15.051-35.133-2.839-6.628L32.022 78.13zM14.13 62.88l17.892 41.761 17.89-41.761-2.839-6.628-15.051 35.133L16.97 56.253zM-.067 29.74l-3.694-8.622-3.694 8.621 3.694 8.622zm-7.388 0-2.84 6.628 6.534 15.249 6.533-15.25-2.84-6.628-3.693 8.622zm3.694 21.877-6.533-15.25-2.84 6.629 9.373 21.877L5.61 42.997l-2.84-6.628zm-9.373-8.621-2.84 6.628L-3.76 78.13 8.45 49.625l-2.84-6.628-9.372 21.877zm9.373 35.133-12.213-28.505-2.84 6.628L-3.76 91.386l15.05-35.133-2.839-6.628zm-15.052-21.877-2.84 6.628 17.892 41.761 17.891-41.76-2.84-6.628-15.05 35.133zm38.622-90.151-2.84 6.628L32.023 7.863l15.05-35.133-2.839-6.628L32.022-5.392zM14.13-20.642l17.892 41.761 17.89-41.761-2.839-6.628L32.022 7.863 16.97-27.27zM-3.761-5.392l-12.213-28.506-2.84 6.628L-3.76 7.863 11.29-27.27l-2.839-6.628zM-18.813-27.27l-2.84 6.628L-3.76 21.119l17.89-41.761-2.84-6.628L-3.76 7.863zm29.25 15.249-2.84 6.628 6.533 15.25 6.533-15.25-2.84-6.628L14.13-3.4zM14.13 9.856 7.597-5.393l-2.84 6.628 9.373 21.878 9.372-21.878-2.84-6.628zm-9.373-8.62-2.84 6.627L14.13 36.37 26.342 7.863l-2.84-6.628-9.372 21.878zm9.373 35.133L1.918 7.863l-2.84 6.629L14.13 49.625l15.052-35.133-2.84-6.629zM-.922 14.492l-2.84 6.627L14.13 62.881l17.891-41.762-2.84-6.627L14.13 49.625zM40.54 1.235 37.7 7.863 49.914 36.37 62.126 7.863l-2.84-6.628-9.373 21.878zm9.373 35.134L37.701 7.863l-2.84 6.629 15.052 35.133 15.052-35.133-2.84-6.629zM34.861 14.492l-2.84 6.627 17.892 41.762 17.891-41.762-2.84-6.627-15.05 35.133z"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
1
public/patterns/rays-sunburst.svg
Normal file
1
public/patterns/rays-sunburst.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%' viewBox='0 0 800 800'><rect fill='#ffffff' width='800' height='800'/><defs><radialGradient id='a' cx='400' cy='400' r='50%' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#ffffff'/><stop offset='1' stop-color='#FF540B'/></radialGradient><radialGradient id='b' cx='400' cy='400' r='70%' gradientUnits='userSpaceOnUse'><stop offset='0' stop-color='#ffffff'/><stop offset='1' stop-color='#FFBD19'/></radialGradient></defs><rect fill='url(#a)' width='800' height='800'/><g fill-opacity='.8'><path fill='url(#b)' d='M998.7 439.2c1.7-26.5 1.7-52.7 0.1-78.5L401 399.9c0 0 0-0.1 0-0.1l587.6-116.9c-5.1-25.9-11.9-51.2-20.3-75.8L400.9 399.7c0 0 0-0.1 0-0.1l537.3-265c-11.6-23.5-24.8-46.2-39.3-67.9L400.8 399.5c0 0 0-0.1-0.1-0.1l450.4-395c-17.3-19.7-35.8-38.2-55.5-55.5l-395 450.4c0 0-0.1 0-0.1-0.1L733.4-99c-21.7-14.5-44.4-27.6-68-39.3l-265 537.4c0 0-0.1 0-0.1 0l192.6-567.4c-24.6-8.3-49.9-15.1-75.8-20.2L400.2 399c0 0-0.1 0-0.1 0l39.2-597.7c-26.5-1.7-52.7-1.7-78.5-0.1L399.9 399c0 0-0.1 0-0.1 0L282.9-188.6c-25.9 5.1-51.2 11.9-75.8 20.3l192.6 567.4c0 0-0.1 0-0.1 0l-265-537.3c-23.5 11.6-46.2 24.8-67.9 39.3l332.8 498.1c0 0-0.1 0-0.1 0.1L4.4-51.1C-15.3-33.9-33.8-15.3-51.1 4.4l450.4 395c0 0 0 0.1-0.1 0.1L-99 66.6c-14.5 21.7-27.6 44.4-39.3 68l537.4 265c0 0 0 0.1 0 0.1l-567.4-192.6c-8.3 24.6-15.1 49.9-20.2 75.8L399 399.8c0 0 0 0.1 0 0.1l-597.7-39.2c-1.7 26.5-1.7 52.7-0.1 78.5L399 400.1c0 0 0 0.1 0 0.1l-587.6 116.9c5.1 25.9 11.9 51.2 20.3 75.8l567.4-192.6c0 0 0 0.1 0 0.1l-537.3 265c11.6 23.5 24.8 46.2 39.3 67.9l498.1-332.8c0 0 0 0.1 0.1 0.1l-450.4 395c17.3 19.7 35.8 38.2 55.5 55.5l395-450.4c0 0 0.1 0 0.1 0.1L66.6 899c21.7 14.5 44.4 27.6 68 39.3l265-537.4c0 0 0.1 0 0.1 0L207.1 968.3c24.6 8.3 49.9 15.1 75.8 20.2L399.8 401c0 0 0.1 0 0.1 0l-39.2 597.7c26.5 1.7 52.7 1.7 78.5 0.1L400.1 401c0 0 0.1 0 0.1 0l116.9 587.6c25.9-5.1 51.2-11.9 75.8-20.3L400.3 400.9c0 0 0.1 0 0.1 0l265 537.3c23.5-11.6 46.2-24.8 67.9-39.3L400.5 400.8c0 0 0.1 0 0.1-0.1l395 450.4c19.7-17.3 38.2-35.8 55.5-55.5l-450.4-395c0 0 0-0.1 0.1-0.1L899 733.4c14.5-21.7 27.6-44.4 39.3-68l-537.4-265c0 0 0-0.1 0-0.1l567.4 192.6c8.3-24.6 15.1-49.9 20.2-75.8L401 400.2c0 0 0-0.1 0-0.1L998.7 439.2z'/></g></svg>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans:
|
--font-sans:
|
||||||
'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
'Montserrat', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||||
--font-display: 'Playfair Display', serif;
|
--font-display: 'Playfair Display', serif;
|
||||||
--font-serif: 'Lora', serif;
|
--font-serif: 'Lora', serif;
|
||||||
--font-sans-marketing: 'Montserrat', sans-serif;
|
--font-sans-marketing: 'Montserrat', sans-serif;
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
--guest-link: #007aff;
|
--guest-link: #007aff;
|
||||||
--guest-font-family: 'Montserrat', 'Inter', 'Helvetica Neue', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
--guest-font-family: 'Montserrat', 'Inter', 'Helvetica Neue', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
--guest-body-font: 'Montserrat', 'Inter', 'Helvetica Neue', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
--guest-body-font: 'Montserrat', 'Inter', 'Helvetica Neue', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
--guest-heading-font: 'Montserrat', 'Inter', 'Helvetica Neue', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
--guest-heading-font: 'Playfair Display', 'Montserrat', 'Inter', 'Helvetica Neue', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
--guest-serif-font: 'Lora', serif;
|
--guest-serif-font: 'Lora', serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,51 +110,54 @@ export default function EmotionPicker({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div className="relative">
|
||||||
className={cn(
|
<div
|
||||||
'grid gap-3 pb-2',
|
className={cn(
|
||||||
variant === 'standalone' ? 'grid-cols-2 sm:grid-cols-3' : 'grid-cols-2 sm:grid-cols-3'
|
'grid grid-rows-2 grid-flow-col auto-cols-[170px] sm:auto-cols-[190px] gap-3 overflow-x-auto pb-2 pr-12',
|
||||||
)}
|
'scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent'
|
||||||
aria-label="Emotions"
|
)}
|
||||||
>
|
aria-label="Emotions"
|
||||||
{emotions.map((emotion) => {
|
>
|
||||||
// Localize name and description if they are JSON
|
{emotions.map((emotion) => {
|
||||||
const localize = (value: string | object, defaultValue: string = ''): string => {
|
// Localize name and description if they are JSON
|
||||||
if (typeof value === 'string' && value.startsWith('{')) {
|
const localize = (value: string | object, defaultValue: string = ''): string => {
|
||||||
try {
|
if (typeof value === 'string' && value.startsWith('{')) {
|
||||||
const data = JSON.parse(value as string);
|
try {
|
||||||
return data.de || data.en || defaultValue || '';
|
const data = JSON.parse(value as string);
|
||||||
} catch {
|
return data.de || data.en || defaultValue || '';
|
||||||
return value as string;
|
} catch {
|
||||||
|
return value as string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return value as string;
|
||||||
return value as string;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const localizedName = localize(emotion.name, emotion.name);
|
const localizedName = localize(emotion.name, emotion.name);
|
||||||
const localizedDescription = localize(emotion.description || '', '');
|
const localizedDescription = localize(emotion.description || '', '');
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={emotion.id}
|
key={emotion.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleEmotionSelect(emotion)}
|
onClick={() => handleEmotionSelect(emotion)}
|
||||||
className="group flex min-w-[180px] flex-col gap-2 rounded-2xl border border-white/40 bg-white/80 px-4 py-3 text-left shadow-sm transition hover:-translate-y-0.5 hover:border-pink-200 hover:shadow-md dark:border-gray-700/60 dark:bg-gray-900/70"
|
className="group flex flex-col gap-2 rounded-2xl border border-muted/40 bg-white/80 px-4 py-3 text-left shadow-sm transition hover:-translate-y-0.5 hover:border-foreground/10 hover:shadow-md dark:border-gray-700/60 dark:bg-gray-900/70"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-2xl" aria-hidden>
|
<span className="text-2xl" aria-hidden>
|
||||||
{emotion.emoji}
|
{emotion.emoji}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="font-medium text-sm text-foreground line-clamp-1">{localizedName}</div>
|
<div className="font-medium text-sm text-foreground">{localizedName}</div>
|
||||||
{localizedDescription && (
|
{localizedDescription && (
|
||||||
<div className="text-xs text-muted-foreground line-clamp-1">{localizedDescription}</div>
|
<div className="text-xs text-muted-foreground line-clamp-2">{localizedDescription}</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="h-4 w-4 text-muted-foreground opacity-0 transition group-hover:opacity-100" />
|
||||||
</div>
|
</div>
|
||||||
<ChevronRight className="h-4 w-4 text-muted-foreground opacity-0 transition group-hover:opacity-100" />
|
</button>
|
||||||
</div>
|
);
|
||||||
</button>
|
})}
|
||||||
);
|
</div>
|
||||||
})}
|
<div className="pointer-events-none absolute inset-y-0 right-0 w-10 bg-gradient-to-l from-[var(--guest-background)] via-[var(--guest-background)]/90 to-transparent dark:from-black dark:via-black/80" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Skip option */}
|
{/* Skip option */}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getDeviceId } from '../lib/device';
|
|||||||
import { usePollGalleryDelta } from '../polling/usePollGalleryDelta';
|
import { usePollGalleryDelta } from '../polling/usePollGalleryDelta';
|
||||||
import { Heart } from 'lucide-react';
|
import { Heart } from 'lucide-react';
|
||||||
import { useTranslation } from '../i18n/useTranslation';
|
import { useTranslation } from '../i18n/useTranslation';
|
||||||
|
import { useEventBranding } from '../context/EventBrandingContext';
|
||||||
|
|
||||||
type Props = { token: string };
|
type Props = { token: string };
|
||||||
|
|
||||||
@@ -27,10 +28,15 @@ type PreviewPhoto = {
|
|||||||
|
|
||||||
export default function GalleryPreview({ token }: Props) {
|
export default function GalleryPreview({ token }: Props) {
|
||||||
const { locale } = useTranslation();
|
const { locale } = useTranslation();
|
||||||
|
const { branding } = useEventBranding();
|
||||||
const { photos, loading } = usePollGalleryDelta(token, locale);
|
const { photos, loading } = usePollGalleryDelta(token, locale);
|
||||||
const [mode, setMode] = React.useState<PreviewFilter>('latest');
|
const [mode, setMode] = React.useState<PreviewFilter>('latest');
|
||||||
const typedPhotos = React.useMemo(() => photos as PreviewPhoto[], [photos]);
|
const typedPhotos = React.useMemo(() => photos as PreviewPhoto[], [photos]);
|
||||||
const hasPhotobooth = React.useMemo(() => typedPhotos.some((p) => p.ingest_source === 'photobooth'), [typedPhotos]);
|
const hasPhotobooth = React.useMemo(() => typedPhotos.some((p) => p.ingest_source === 'photobooth'), [typedPhotos]);
|
||||||
|
const radius = branding.buttons?.radius ?? 12;
|
||||||
|
const linkColor = branding.buttons?.linkColor ?? branding.secondaryColor;
|
||||||
|
const headingFont = branding.typography?.heading ?? branding.fontFamily ?? undefined;
|
||||||
|
const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined;
|
||||||
|
|
||||||
const items = React.useMemo(() => {
|
const items = React.useMemo(() => {
|
||||||
let arr = typedPhotos.slice();
|
let arr = typedPhotos.slice();
|
||||||
@@ -84,64 +90,82 @@ export default function GalleryPreview({ token }: Props) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="space-y-4">
|
<Card className="border border-muted/30 shadow-sm" style={{ borderRadius: radius, background: 'var(--guest-surface)', fontFamily: bodyFont }}>
|
||||||
<div className="flex items-center justify-between">
|
<CardContent className="space-y-3 p-3">
|
||||||
<div>
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-xs uppercase tracking-wide text-muted-foreground">Live-Galerie</p>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-foreground">Alle Uploads auf einen Blick</h3>
|
<p className="text-xs uppercase tracking-wide text-muted-foreground" style={headingFont ? { fontFamily: headingFont } : undefined}>Live-Galerie</p>
|
||||||
|
<h3 className="text-lg font-semibold text-foreground" style={headingFont ? { fontFamily: headingFont } : undefined}>Alle Uploads auf einen Blick</h3>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to={`/e/${encodeURIComponent(token)}/gallery?mode=${mode}`}
|
||||||
|
className="text-sm font-semibold transition"
|
||||||
|
style={{ color: linkColor }}
|
||||||
|
>
|
||||||
|
Alle ansehen →
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
|
||||||
to={`/e/${encodeURIComponent(token)}/gallery?mode=${mode}`}
|
|
||||||
className="text-sm font-semibold text-pink-600 hover:text-pink-700"
|
|
||||||
>
|
|
||||||
Alle ansehen →
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2 overflow-x-auto pb-1 text-sm font-medium [-ms-overflow-style:none] [scrollbar-width:none]">
|
<div className="flex gap-2 overflow-x-auto pb-1 text-sm font-medium [-ms-overflow-style:none] [scrollbar-width:none]">
|
||||||
{filters.map((filter) => (
|
{filters.map((filter) => (
|
||||||
<button
|
<button
|
||||||
key={filter.value}
|
key={filter.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMode(filter.value)}
|
onClick={() => setMode(filter.value)}
|
||||||
className={`rounded-full border px-4 py-1 transition ${
|
style={{
|
||||||
mode === filter.value
|
borderRadius: radius,
|
||||||
? 'border-pink-500 bg-pink-500 text-white shadow'
|
border: mode === filter.value ? `1px solid ${branding.primaryColor}` : `1px solid ${branding.primaryColor}22`,
|
||||||
: 'border-transparent bg-white/70 text-muted-foreground hover:border-pink-200'
|
background: mode === filter.value ? branding.primaryColor : 'var(--guest-surface)',
|
||||||
}`}
|
color: mode === filter.value ? '#ffffff' : 'var(--foreground)',
|
||||||
|
boxShadow: mode === filter.value ? `0 8px 18px ${branding.primaryColor}33` : 'none',
|
||||||
|
}}
|
||||||
|
className="px-4 py-1 transition"
|
||||||
>
|
>
|
||||||
{filter.label}
|
{filter.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading && <p className="text-sm text-muted-foreground">Lädt…</p>}
|
{loading && <p className="text-sm text-muted-foreground">Lädt…</p>}
|
||||||
{!loading && items.length === 0 && (
|
{!loading && items.length === 0 && (
|
||||||
<Card>
|
<div className="flex items-center gap-3 rounded-xl border border-muted/30 bg-[var(--guest-surface)] p-3 text-sm text-muted-foreground">
|
||||||
<CardContent className="p-3 text-sm text-muted-foreground">
|
<Heart className="h-4 w-4" style={{ color: branding.secondaryColor }} aria-hidden />
|
||||||
Noch keine Fotos. Starte mit deinem ersten Upload!
|
Noch keine Fotos. Starte mit deinem ersten Upload!
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
<div className="grid gap-2 sm:grid-cols-2">
|
||||||
{items.map((p: PreviewPhoto) => (
|
{items.map((p: PreviewPhoto) => (
|
||||||
<Link
|
<Link
|
||||||
key={p.id}
|
key={p.id}
|
||||||
to={`/e/${encodeURIComponent(token)}/gallery?photoId=${p.id}`}
|
to={`/e/${encodeURIComponent(token)}/gallery?photoId=${p.id}`}
|
||||||
className="group relative block overflow-hidden rounded-3xl border border-white/30 bg-gray-900 text-white shadow-lg"
|
className="group relative block overflow-hidden text-foreground"
|
||||||
|
style={{
|
||||||
|
borderRadius: radius,
|
||||||
|
border: `1px solid ${branding.primaryColor}22`,
|
||||||
|
background: 'var(--guest-surface)',
|
||||||
|
boxShadow: `0 12px 26px ${branding.primaryColor}22`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={p.thumbnail_path || p.file_path}
|
src={p.thumbnail_path || p.file_path}
|
||||||
alt={p.title || 'Foto'}
|
alt={p.title || 'Foto'}
|
||||||
className="h-48 w-full object-cover transition duration-300 group-hover:scale-105"
|
className="h-40 w-full object-cover transition duration-300 group-hover:scale-105"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/10 to-transparent" aria-hidden />
|
<div
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-4">
|
className="absolute inset-0"
|
||||||
<p className="text-sm font-semibold leading-tight line-clamp-2">{p.title || getPhotoTitle(p)}</p>
|
style={{
|
||||||
<div className="mt-2 flex items-center gap-1 text-xs text-white/80">
|
background: `linear-gradient(180deg, transparent 50%, ${branding.primaryColor}33 100%)`,
|
||||||
<Heart className="h-4 w-4 fill-current" aria-hidden />
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 space-y-1 p-3">
|
||||||
|
<p className="text-sm font-semibold leading-tight line-clamp-2" style={headingFont ? { fontFamily: headingFont } : undefined}>
|
||||||
|
{p.title || getPhotoTitle(p)}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-1 text-xs text-foreground/80">
|
||||||
|
<Heart className="h-4 w-4" style={{ color: branding.primaryColor }} aria-hidden />
|
||||||
{p.likes_count ?? 0}
|
{p.likes_count ?? 0}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,12 +173,13 @@ export default function GalleryPreview({ token }: Props) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-center text-sm text-muted-foreground">
|
<p className="text-center text-sm text-muted-foreground">
|
||||||
Lust auf mehr?{' '}
|
Lust auf mehr?{' '}
|
||||||
<Link to={`/e/${encodeURIComponent(token)}/gallery`} className="font-semibold text-pink-600 hover:text-pink-700">
|
<Link to={`/e/${encodeURIComponent(token)}/gallery`} className="font-semibold transition" style={{ color: linkColor }}>
|
||||||
Zur Galerie →
|
Zur Galerie →
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,19 +172,13 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
|||||||
}, [notificationsOpen]);
|
}, [notificationsOpen]);
|
||||||
|
|
||||||
if (!eventToken) {
|
if (!eventToken) {
|
||||||
const guestName = identity?.name && identity?.hydrated ? identity.name : null;
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="guest-header sticky top-0 z-20 flex items-center justify-between border-b bg-white/70 px-4 py-2 backdrop-blur dark:bg-black/40"
|
className="guest-header z-20 flex items-center justify-between border-b bg-white/70 px-4 py-2 dark:bg-black/40"
|
||||||
style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}
|
style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="font-semibold">{title}</div>
|
<div className="font-semibold">{title}</div>
|
||||||
{guestName && (
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{`${t('common.hi')} ${guestName}`}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AppearanceToggleDropdown />
|
<AppearanceToggleDropdown />
|
||||||
@@ -194,20 +188,20 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const guestName =
|
const headerFont = branding.typography?.heading ?? branding.fontFamily ?? undefined;
|
||||||
identity && identity.eventKey === eventToken && identity.hydrated && identity.name ? identity.name : null;
|
const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined;
|
||||||
|
|
||||||
const headerStyle: React.CSSProperties = {
|
const headerStyle: React.CSSProperties = {
|
||||||
background: `linear-gradient(135deg, ${branding.primaryColor}, ${branding.secondaryColor})`,
|
background: `linear-gradient(135deg, ${branding.primaryColor}, ${branding.secondaryColor})`,
|
||||||
color: headerTextColor,
|
color: headerTextColor,
|
||||||
fontFamily: branding.fontFamily ?? undefined,
|
fontFamily: headerFont,
|
||||||
};
|
};
|
||||||
|
|
||||||
const accentColor = branding.secondaryColor;
|
const accentColor = branding.secondaryColor;
|
||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'loading') {
|
||||||
return (
|
return (
|
||||||
<div className="guest-header sticky top-0 z-20 flex items-center justify-between border-b border-white/10 px-4 py-2 text-white shadow-sm backdrop-blur" style={headerStyle}>
|
<div className="guest-header z-20 flex items-center justify-between border-b border-white/10 px-4 py-2 text-white shadow-sm backdrop-blur" style={headerStyle}>
|
||||||
<div className="font-semibold" style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>{t('header.loading')}</div>
|
<div className="font-semibold" style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>{t('header.loading')}</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AppearanceToggleDropdown />
|
<AppearanceToggleDropdown />
|
||||||
@@ -225,19 +219,14 @@ export default function Header({ eventToken, title = '' }: { eventToken?: string
|
|||||||
statsContext && statsContext.eventKey === eventToken ? statsContext : undefined;
|
statsContext && statsContext.eventKey === eventToken ? statsContext : undefined;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="guest-header sticky top-0 z-20 flex items-center justify-between border-b border-white/10 px-4 py-3 text-white shadow-sm backdrop-blur"
|
className="guest-header z-20 flex items-center justify-between border-b border-white/10 px-4 py-3 text-white shadow-sm backdrop-blur"
|
||||||
style={headerStyle}
|
style={headerStyle}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{renderEventAvatar(event.name, event.type?.icon, accentColor, headerTextColor, branding.logo)}
|
{renderEventAvatar(event.name, event.type?.icon, accentColor, headerTextColor, branding.logo)}
|
||||||
<div className="flex flex-col" style={branding.fontFamily ? { fontFamily: branding.fontFamily } : undefined}>
|
<div className="flex flex-col" style={headerFont ? { fontFamily: headerFont } : undefined}>
|
||||||
<div className="font-semibold text-base">{event.name}</div>
|
<div className="font-semibold text-lg">{event.name}</div>
|
||||||
{guestName && (
|
<div className="flex items-center gap-2 text-xs text-white/70" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||||
<span className="text-xs text-white/80">
|
|
||||||
{`${t('common.hi')} ${guestName}`}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center gap-2 text-xs text-white/70">
|
|
||||||
{stats && (
|
{stats && (
|
||||||
<>
|
<>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const DEFAULT_EVENT_BRANDING: EventBranding = {
|
|||||||
primaryColor: '#f43f5e',
|
primaryColor: '#f43f5e',
|
||||||
secondaryColor: '#fb7185',
|
secondaryColor: '#fb7185',
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
fontFamily: null,
|
fontFamily: 'Montserrat, Inter, "Helvetica Neue", system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
|
||||||
logoUrl: null,
|
logoUrl: null,
|
||||||
palette: {
|
palette: {
|
||||||
primary: '#f43f5e',
|
primary: '#f43f5e',
|
||||||
@@ -19,8 +19,8 @@ export const DEFAULT_EVENT_BRANDING: EventBranding = {
|
|||||||
surface: '#ffffff',
|
surface: '#ffffff',
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
heading: null,
|
heading: 'Playfair Display, "Times New Roman", serif',
|
||||||
body: null,
|
body: 'Montserrat, Inter, "Helvetica Neue", system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
|
||||||
sizePreset: 'm',
|
sizePreset: 'm',
|
||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
@@ -71,7 +71,7 @@ function resolveBranding(input?: EventBranding | null): EventBranding {
|
|||||||
primaryColor: normaliseHexColor(palettePrimary, DEFAULT_EVENT_BRANDING.primaryColor),
|
primaryColor: normaliseHexColor(palettePrimary, DEFAULT_EVENT_BRANDING.primaryColor),
|
||||||
secondaryColor: normaliseHexColor(paletteSecondary, DEFAULT_EVENT_BRANDING.secondaryColor),
|
secondaryColor: normaliseHexColor(paletteSecondary, DEFAULT_EVENT_BRANDING.secondaryColor),
|
||||||
backgroundColor: normaliseHexColor(paletteBackground, DEFAULT_EVENT_BRANDING.backgroundColor),
|
backgroundColor: normaliseHexColor(paletteBackground, DEFAULT_EVENT_BRANDING.backgroundColor),
|
||||||
fontFamily: bodyFont?.trim() || null,
|
fontFamily: bodyFont?.trim() || DEFAULT_EVENT_BRANDING.fontFamily,
|
||||||
logoUrl: logoMode === 'upload' ? (logoValue?.trim() || null) : null,
|
logoUrl: logoMode === 'upload' ? (logoValue?.trim() || null) : null,
|
||||||
palette: {
|
palette: {
|
||||||
primary: normaliseHexColor(palettePrimary, DEFAULT_EVENT_BRANDING.primaryColor),
|
primary: normaliseHexColor(palettePrimary, DEFAULT_EVENT_BRANDING.primaryColor),
|
||||||
@@ -80,8 +80,8 @@ function resolveBranding(input?: EventBranding | null): EventBranding {
|
|||||||
surface: normaliseHexColor(paletteSurface, paletteBackground ?? DEFAULT_EVENT_BRANDING.backgroundColor),
|
surface: normaliseHexColor(paletteSurface, paletteBackground ?? DEFAULT_EVENT_BRANDING.backgroundColor),
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
heading: headingFont?.trim() || null,
|
heading: headingFont?.trim() || DEFAULT_EVENT_BRANDING.typography?.heading || null,
|
||||||
body: bodyFont?.trim() || null,
|
body: bodyFont?.trim() || DEFAULT_EVENT_BRANDING.typography?.body || DEFAULT_EVENT_BRANDING.fontFamily,
|
||||||
sizePreset,
|
sizePreset,
|
||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
|
|||||||
@@ -132,6 +132,19 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
|||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
fallbackGuestName: 'Gast',
|
fallbackGuestName: 'Gast',
|
||||||
|
welcomeLine: 'Willkommen {name}!',
|
||||||
|
introRotating: {
|
||||||
|
0: 'Hilf uns, diesen besonderen Tag mit deinen schönsten Momenten festzuhalten.',
|
||||||
|
1: 'Fang die Stimmung des Events ein und teile sie mit allen Gästen.',
|
||||||
|
2: 'Deine Sicht zählt: Halte Augenblicke fest, die sonst niemand bemerkt.',
|
||||||
|
3: 'Erfülle kleine Fotoaufgaben und fülle die gemeinsame Galerie mit Leben.',
|
||||||
|
4: 'Zeig uns, was du siehst – deine Fotos erzählen die Geschichte dieses Tages.',
|
||||||
|
5: 'Mach aus Schnappschüssen gemeinsame Erinnerungen in einer großen Event-Galerie.',
|
||||||
|
6: 'Diese App ist eure Fotozentrale – fotografiere, lade hoch und begeistere die anderen.',
|
||||||
|
7: 'Sorge dafür, dass kein wichtiger Moment verloren geht – mit deinen Bildern.',
|
||||||
|
8: 'Lass dich von Fotoaufgaben inspirieren und halte die besonderen Szenen fest.',
|
||||||
|
9: 'Mach mit beim Fotospiel: Deine Fotos machen dieses Event unvergesslich.',
|
||||||
|
},
|
||||||
hero: {
|
hero: {
|
||||||
subtitle: 'Willkommen zur Party',
|
subtitle: 'Willkommen zur Party',
|
||||||
title: 'Hey {name}!',
|
title: 'Hey {name}!',
|
||||||
@@ -776,6 +789,19 @@ export const messages: Record<LocaleCode, NestedMessages> = {
|
|||||||
},
|
},
|
||||||
home: {
|
home: {
|
||||||
fallbackGuestName: 'Guest',
|
fallbackGuestName: 'Guest',
|
||||||
|
welcomeLine: 'Welcome {name}!',
|
||||||
|
introRotating: {
|
||||||
|
0: 'Help us capture this special day with your favourite moments.',
|
||||||
|
1: 'Capture the mood of the event and share it with everyone.',
|
||||||
|
2: 'Your view matters: save the moments that others might miss.',
|
||||||
|
3: 'Complete playful photo missions and fill the shared gallery with life.',
|
||||||
|
4: 'Show us what you see – your photos tell the story of this day.',
|
||||||
|
5: 'Turn quick snapshots into shared memories in one big event gallery.',
|
||||||
|
6: 'This app is your photo hub – shoot, upload, and delight the other guests.',
|
||||||
|
7: 'Make sure no important moment gets lost – with your pictures.',
|
||||||
|
8: 'Let photo missions inspire you and capture the scenes that really matter.',
|
||||||
|
9: 'Join the photo game: your pictures make this event unforgettable.',
|
||||||
|
},
|
||||||
hero: {
|
hero: {
|
||||||
subtitle: 'Welcome to the party',
|
subtitle: 'Welcome to the party',
|
||||||
title: 'Hey {name}!',
|
title: 'Hey {name}!',
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ export default function GalleryPage() {
|
|||||||
styleOverride={{ borderRadius: radius, fontFamily: headingFont }}
|
styleOverride={{ borderRadius: radius, fontFamily: headingFont }}
|
||||||
/>
|
/>
|
||||||
{loading && <p className="px-4" style={bodyFont ? { fontFamily: bodyFont } : undefined}>{t('galleryPage.loading', 'Lade…')}</p>}
|
{loading && <p className="px-4" style={bodyFont ? { fontFamily: bodyFont } : undefined}>{t('galleryPage.loading', 'Lade…')}</p>}
|
||||||
<div className="grid gap-3 px-4 pb-16 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-3 gap-2 px-4 pb-16 sm:grid-cols-3 md:grid-cols-4">
|
||||||
{list.map((p: GalleryPhoto) => {
|
{list.map((p: GalleryPhoto) => {
|
||||||
const imageUrl = normalizeImageUrl(p.thumbnail_path || p.file_path);
|
const imageUrl = normalizeImageUrl(p.thumbnail_path || p.file_path);
|
||||||
const createdLabel = p.created_at
|
const createdLabel = p.created_at
|
||||||
@@ -357,7 +357,7 @@ export default function GalleryPage() {
|
|||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={altText}
|
alt={altText}
|
||||||
className="h-64 w-full object-cover transition duration-500 group-hover:scale-105"
|
className="aspect-[3/4] w-full object-cover transition duration-500 group-hover:scale-105"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
(e.target as HTMLImageElement).src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjRjNGNEY2Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPk5vIEltYWdlPC90ZXh0Pjwvc3ZnPg==';
|
(e.target as HTMLImageElement).src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjRjNGNEY2Ii8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPk5vIEltYWdlPC90ZXh0Pjwvc3ZnPg==';
|
||||||
}}
|
}}
|
||||||
@@ -425,6 +425,20 @@ export default function GalleryPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{list.length === 0 && Array.from({ length: 6 }).map((_, idx) => (
|
||||||
|
<div
|
||||||
|
key={`placeholder-${idx}`}
|
||||||
|
className="relative overflow-hidden border border-muted/40 bg-[var(--guest-surface,#f7f7f7)] shadow-sm"
|
||||||
|
style={{ borderRadius: radius }}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-white/60 via-white/30 to-transparent dark:from-white/5 dark:via-white/0" aria-hidden />
|
||||||
|
<div className="flex aspect-[3/4] items-center justify-center gap-2 p-4 text-muted-foreground/70">
|
||||||
|
<ImageIcon className="h-6 w-6" aria-hidden />
|
||||||
|
<div className="h-2 w-10 rounded-full bg-muted/40" />
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 animate-pulse bg-white/30 dark:bg-white/5" aria-hidden />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{currentPhotoIndex !== null && list.length > 0 && (
|
{currentPhotoIndex !== null && list.length > 0 && (
|
||||||
<PhotoLightbox
|
<PhotoLightbox
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import { Sparkles, UploadCloud, X, RefreshCw } from 'lucide-react';
|
|||||||
import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
|
import { useTranslation, type TranslateFn } from '../i18n/useTranslation';
|
||||||
import { useEventBranding } from '../context/EventBrandingContext';
|
import { useEventBranding } from '../context/EventBrandingContext';
|
||||||
import type { EventBranding } from '../types/event-branding';
|
import type { EventBranding } from '../types/event-branding';
|
||||||
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
|
import { useGesture } from '@use-gesture/react';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const { token } = useParams<{ token: string }>();
|
const { token } = useParams<{ token: string }>();
|
||||||
@@ -25,17 +27,7 @@ export default function HomePage() {
|
|||||||
const radius = branding.buttons?.radius ?? 12;
|
const radius = branding.buttons?.radius ?? 12;
|
||||||
|
|
||||||
const heroStorageKey = token ? `guestHeroDismissed_${token}` : 'guestHeroDismissed';
|
const heroStorageKey = token ? `guestHeroDismissed_${token}` : 'guestHeroDismissed';
|
||||||
const [heroVisible, setHeroVisible] = React.useState(() => {
|
const [heroVisible, setHeroVisible] = React.useState(false);
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return window.sessionStorage.getItem(heroStorageKey) !== '1';
|
|
||||||
} catch {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@@ -43,9 +35,11 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setHeroVisible(window.sessionStorage.getItem(heroStorageKey) !== '1');
|
const stored = window.sessionStorage.getItem(heroStorageKey);
|
||||||
|
// standardmäßig versteckt, nur sichtbar falls explizit gesetzt (kann später wieder aktiviert werden)
|
||||||
|
setHeroVisible(stored === 'show');
|
||||||
} catch {
|
} catch {
|
||||||
setHeroVisible(true);
|
setHeroVisible(false);
|
||||||
}
|
}
|
||||||
}, [heroStorageKey]);
|
}, [heroStorageKey]);
|
||||||
|
|
||||||
@@ -67,20 +61,37 @@ export default function HomePage() {
|
|||||||
const accentColor = branding.primaryColor;
|
const accentColor = branding.primaryColor;
|
||||||
const secondaryAccent = branding.secondaryColor;
|
const secondaryAccent = branding.secondaryColor;
|
||||||
|
|
||||||
const [missionPreview, setMissionPreview] = React.useState<MissionPreview | null>(null);
|
const [missionDeck, setMissionDeck] = React.useState<MissionPreview[]>([]);
|
||||||
const [missionLoading, setMissionLoading] = React.useState(false);
|
const [missionLoading, setMissionLoading] = React.useState(false);
|
||||||
const missionPoolRef = React.useRef<MissionPreview[]>([]);
|
const missionPoolRef = React.useRef<MissionPreview[]>([]);
|
||||||
|
|
||||||
const shuffleMissionPreview = React.useCallback(() => {
|
const drawRandom = React.useCallback((excludeIds: Set<number>) => {
|
||||||
|
const pool = missionPoolRef.current.filter((item) => !excludeIds.has(item.id));
|
||||||
|
if (!pool.length) return null;
|
||||||
|
return pool[Math.floor(Math.random() * pool.length)];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const resetDeck = React.useCallback(() => {
|
||||||
const pool = missionPoolRef.current;
|
const pool = missionPoolRef.current;
|
||||||
if (!pool.length) {
|
if (!pool.length) {
|
||||||
setMissionPreview(null);
|
setMissionDeck([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const choice = pool[Math.floor(Math.random() * pool.length)];
|
const shuffled = [...pool].sort(() => Math.random() - 0.5);
|
||||||
setMissionPreview(choice);
|
setMissionDeck(shuffled.slice(0, 4));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const advanceDeck = React.useCallback(() => {
|
||||||
|
setMissionDeck((prev) => {
|
||||||
|
if (!prev.length) return prev;
|
||||||
|
const [, ...rest] = prev;
|
||||||
|
const exclude = new Set(rest.map((r) => r.id));
|
||||||
|
const nextCandidate = drawRandom(exclude);
|
||||||
|
const replenished = nextCandidate ? [...rest, nextCandidate] : rest;
|
||||||
|
return replenished;
|
||||||
|
});
|
||||||
|
}, [drawRandom]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -108,16 +119,16 @@ export default function HomePage() {
|
|||||||
duration: typeof task.duration === 'number' ? task.duration : 3,
|
duration: typeof task.duration === 'number' ? task.duration : 3,
|
||||||
emotion: task.emotion ?? null,
|
emotion: task.emotion ?? null,
|
||||||
}));
|
}));
|
||||||
shuffleMissionPreview();
|
resetDeck();
|
||||||
} else {
|
} else {
|
||||||
missionPoolRef.current = [];
|
missionPoolRef.current = [];
|
||||||
setMissionPreview(null);
|
setMissionDeck([]);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
console.warn('Mission preview failed', err);
|
console.warn('Mission preview failed', err);
|
||||||
missionPoolRef.current = [];
|
missionPoolRef.current = [];
|
||||||
setMissionPreview(null);
|
setMissionDeck([]);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
@@ -129,14 +140,35 @@ export default function HomePage() {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [shuffleMissionPreview, token, locale]);
|
}, [resetDeck, token, locale]);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const introArray: string[] = [];
|
||||||
|
for (let i = 0; i < 12; i += 1) {
|
||||||
|
const candidate = t(`home.introRotating.${i}`, '');
|
||||||
|
if (candidate) {
|
||||||
|
introArray.push(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const introMessage =
|
||||||
|
introArray.length > 0 ? introArray[Math.floor(Math.random() * introArray.length)] : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 pb-32" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
<div className="space-y-0.5 pb-24" style={bodyFont ? { fontFamily: bodyFont } : undefined}>
|
||||||
|
<section className="space-y-1 px-4">
|
||||||
|
<p className="text-sm font-semibold text-foreground">
|
||||||
|
{t('home.welcomeLine').replace('{name}', displayName)}
|
||||||
|
</p>
|
||||||
|
{introMessage && (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{introMessage}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
{heroVisible && (
|
{heroVisible && (
|
||||||
<HeroCard
|
<HeroCard
|
||||||
name={displayName}
|
name={displayName}
|
||||||
@@ -150,23 +182,17 @@ export default function HomePage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<section className="space-y-4" style={headingFont ? { fontFamily: headingFont } : undefined}>
|
<section className="space-y-0.5" style={headingFont ? { fontFamily: headingFont } : undefined}>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="space-y-0.5">
|
||||||
<div>
|
|
||||||
<h2 className="text-base font-semibold text-foreground">Starte dein Fotospiel</h2>
|
|
||||||
<p className="text-xs text-muted-foreground">Wähle, wie du den nächsten Moment einfängst.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
<MissionActionCard
|
<MissionActionCard
|
||||||
token={token}
|
token={token}
|
||||||
mission={missionPreview}
|
mission={missionDeck[0] ?? null}
|
||||||
loading={missionLoading}
|
loading={missionLoading}
|
||||||
onShuffle={shuffleMissionPreview}
|
onAdvance={advanceDeck}
|
||||||
|
stack={missionDeck.slice(0, 3)}
|
||||||
/>
|
/>
|
||||||
<EmotionActionCard />
|
|
||||||
<UploadActionCard token={token} accentColor={accentColor} secondaryAccent={secondaryAccent} radius={radius} bodyFont={bodyFont} />
|
<UploadActionCard token={token} accentColor={accentColor} secondaryAccent={secondaryAccent} radius={radius} bodyFont={bodyFont} />
|
||||||
|
<EmotionActionCard />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -254,58 +280,233 @@ function MissionActionCard({
|
|||||||
token,
|
token,
|
||||||
mission,
|
mission,
|
||||||
loading,
|
loading,
|
||||||
onShuffle,
|
onAdvance,
|
||||||
|
stack,
|
||||||
}: {
|
}: {
|
||||||
token: string;
|
token: string;
|
||||||
mission: MissionPreview | null;
|
mission: MissionPreview | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onShuffle: () => void;
|
onAdvance: () => void;
|
||||||
|
stack: MissionPreview[];
|
||||||
}) {
|
}) {
|
||||||
|
const { branding } = useEventBranding();
|
||||||
|
const radius = branding.buttons?.radius ?? 12;
|
||||||
|
const primary = branding.buttons?.primary ?? branding.primaryColor;
|
||||||
|
const secondary = branding.buttons?.secondary ?? branding.secondaryColor;
|
||||||
|
const buttonStyle = branding.buttons?.style ?? 'filled';
|
||||||
|
const headingFont = branding.typography?.heading ?? branding.fontFamily ?? undefined;
|
||||||
|
const bodyFont = branding.typography?.body ?? branding.fontFamily ?? undefined;
|
||||||
|
const textColor = '#1f2937';
|
||||||
|
const subTextColor = '#334155';
|
||||||
|
const swipeThreshold = 120;
|
||||||
|
const stackLayers = stack.slice(1, 4);
|
||||||
|
|
||||||
|
const cardStyle: React.CSSProperties = {
|
||||||
|
borderRadius: `${radius + 8}px`,
|
||||||
|
backgroundColor: '#fcf7ef',
|
||||||
|
backgroundImage: `linear-gradient(0deg, ${primary}33, ${primary}22), url(/patterns/rays-sunburst.svg)`,
|
||||||
|
backgroundBlendMode: 'multiply, normal',
|
||||||
|
backgroundSize: '330% 330%',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
filter: 'contrast(1.12) saturate(1.1)',
|
||||||
|
border: `1px solid ${primary}26`,
|
||||||
|
boxShadow: `0 12px 28px ${primary}22, 0 2px 6px ${primary}1f`,
|
||||||
|
fontFamily: bodyFont,
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
};
|
||||||
|
|
||||||
|
const [{ x, y, rotateZ, rotateY, rotateX, scale, opacity }, api] = useSpring(() => ({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
rotateZ: 0,
|
||||||
|
rotateY: 0,
|
||||||
|
rotateX: 0,
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
config: { tension: 320, friction: 26 },
|
||||||
|
}));
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
api.start({ x: 0, y: 0, rotateZ: 0, rotateY: 0, rotateX: 0, scale: 1, opacity: 1, immediate: false });
|
||||||
|
}, [mission?.id, api]);
|
||||||
|
|
||||||
|
const bind = useGesture(
|
||||||
|
{
|
||||||
|
onDrag: ({ active, movement: [mx, my], velocity: [vx], direction: [dx], cancel }) => {
|
||||||
|
if (active && Math.abs(mx) > swipeThreshold) {
|
||||||
|
cancel?.();
|
||||||
|
api.start({
|
||||||
|
x: dx > 0 ? 520 : -520,
|
||||||
|
y: my,
|
||||||
|
rotateZ: dx > 0 ? 12 : -12,
|
||||||
|
rotateY: dx > 0 ? 18 : -18,
|
||||||
|
rotateX: -my / 10,
|
||||||
|
opacity: 0,
|
||||||
|
scale: 1,
|
||||||
|
immediate: false,
|
||||||
|
onRest: () => {
|
||||||
|
onAdvance();
|
||||||
|
api.start({ x: 0, y: 0, rotateZ: 0, rotateY: 0, rotateX: 0, opacity: 1, scale: 1, immediate: false });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.start({
|
||||||
|
x: mx,
|
||||||
|
y: my,
|
||||||
|
rotateZ: mx / 18,
|
||||||
|
rotateY: mx / 28,
|
||||||
|
rotateX: -my / 36,
|
||||||
|
scale: active ? 1.02 : 1,
|
||||||
|
opacity: 1,
|
||||||
|
immediate: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDragEnd: () => {
|
||||||
|
api.start({ x: 0, y: 0, rotateZ: 0, rotateY: 0, rotateX: 0, scale: 1, opacity: 1, immediate: false });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
drag: {
|
||||||
|
filterTaps: true,
|
||||||
|
bounds: { left: -200, right: 200, top: -120, bottom: 120 },
|
||||||
|
rubberband: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border-0 shadow-sm">
|
<Card className="border-0 shadow-none bg-transparent" style={{ fontFamily: bodyFont }}>
|
||||||
<CardHeader className="flex flex-row items-start gap-3">
|
<CardContent className="px-0 py-0">
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-pink-100 text-pink-600">
|
<div className="relative min-h-[280px]">
|
||||||
<Sparkles className="h-5 w-5" aria-hidden />
|
{stackLayers.map((item, index) => {
|
||||||
</div>
|
const depth = index + 1;
|
||||||
<div>
|
const scaleDown = 1 - depth * 0.03;
|
||||||
<CardTitle className="text-base font-semibold text-foreground">Mission starten</CardTitle>
|
const translateY = depth * 12;
|
||||||
<CardDescription className="text-xs text-muted-foreground">
|
const fade = Math.max(0.25, 0.55 - depth * 0.08);
|
||||||
Wir haben bereits eine Aufgabe für dich vorbereitet. Tippe, um direkt loszulegen.
|
return (
|
||||||
</CardDescription>
|
<div
|
||||||
</div>
|
key={item.id ?? index}
|
||||||
</CardHeader>
|
className="absolute inset-0 pointer-events-none"
|
||||||
<CardContent className="space-y-3">
|
style={{
|
||||||
{mission ? (
|
...cardStyle,
|
||||||
<>
|
transform: `translateY(${translateY}px) scale(${scaleDown})`,
|
||||||
<p className="text-lg font-semibold text-foreground">{mission.title}</p>
|
opacity: fade,
|
||||||
{mission.description && (
|
filter: 'brightness(0.96) contrast(0.98)',
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2">{mission.description}</p>
|
}}
|
||||||
)}
|
aria-hidden
|
||||||
<div className="flex gap-3 text-xs text-muted-foreground">
|
/>
|
||||||
<span>{mission.duration ?? 3} Min</span>
|
);
|
||||||
{mission.emotion?.name && <span>{mission.emotion.name}</span>}
|
})}
|
||||||
</div>
|
<animated.div
|
||||||
</>
|
className="relative overflow-hidden touch-pan-y"
|
||||||
) : (
|
style={{
|
||||||
<p className="text-sm text-muted-foreground">
|
...cardStyle,
|
||||||
Ziehe deine erste Mission im Aufgaben-Tab oder lade deine Stimmung hoch.
|
x,
|
||||||
</p>
|
y,
|
||||||
)}
|
rotateZ,
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
rotateY,
|
||||||
<Button asChild className="flex-1">
|
rotateX,
|
||||||
<Link to={`/e/${encodeURIComponent(token)}/tasks`}>Mission starten</Link>
|
scale,
|
||||||
</Button>
|
opacity,
|
||||||
<Button
|
transformOrigin: 'center center',
|
||||||
type="button"
|
willChange: 'transform',
|
||||||
variant="ghost"
|
}}
|
||||||
className="flex-1"
|
{...bind()}
|
||||||
onClick={onShuffle}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
>
|
||||||
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} aria-hidden />
|
<div
|
||||||
Andere Mission
|
className="absolute inset-x-0 top-0 h-12"
|
||||||
</Button>
|
style={{
|
||||||
</div>
|
background: `linear-gradient(90deg, ${secondary}80, ${primary}cc)`,
|
||||||
|
filter: 'saturate(1.05)',
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div className="relative z-10 flex flex-col gap-3 px-5 pb-4 pt-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div
|
||||||
|
className="flex h-11 w-11 items-center justify-center rounded-2xl bg-white/90 text-foreground shadow-sm"
|
||||||
|
style={{ borderRadius: `${radius - 2}px` }}
|
||||||
|
>
|
||||||
|
<Sparkles className="h-5 w-5" aria-hidden />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p
|
||||||
|
className="text-xs font-semibold uppercase tracking-wide"
|
||||||
|
style={{ ...(headingFont ? { fontFamily: headingFont } : {}), color: subTextColor }}
|
||||||
|
>
|
||||||
|
Fotoaufgabe
|
||||||
|
</p>
|
||||||
|
<p className="text-sm" style={{ color: subTextColor }}>
|
||||||
|
Wir haben schon etwas für dich vorbereitet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mission ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div
|
||||||
|
className="rounded-[14px] py-3 text-center"
|
||||||
|
style={{
|
||||||
|
background: 'rgba(255,255,255,0.6)',
|
||||||
|
paddingLeft: '30px',
|
||||||
|
paddingRight: '30px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className="text-lg font-semibold leading-snug"
|
||||||
|
style={{ ...(headingFont ? { fontFamily: headingFont } : {}), color: textColor }}
|
||||||
|
>
|
||||||
|
{mission.title}
|
||||||
|
</p>
|
||||||
|
{mission.description && (
|
||||||
|
<p className="text-sm leading-relaxed" style={{ color: subTextColor }}>
|
||||||
|
{mission.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-center" style={{ color: subTextColor }}>
|
||||||
|
Ziehe deine erste Mission im Aufgaben-Tab oder wähle eine Stimmung.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-[2fr_1fr] gap-2 pt-1">
|
||||||
|
<Button asChild className="w-full" style={{ borderRadius: `${radius}px` }}>
|
||||||
|
<Link
|
||||||
|
to={
|
||||||
|
mission
|
||||||
|
? `/e/${encodeURIComponent(token)}/upload?task=${mission.id}`
|
||||||
|
: `/e/${encodeURIComponent(token)}/tasks`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Aufgabe starten
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full"
|
||||||
|
onClick={onShuffle}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
borderRadius: `${radius}px`,
|
||||||
|
backgroundColor: secondary,
|
||||||
|
color: '#ffffff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`mr-2 h-4 w-4 ${loading ? 'animate-spin' : ''}`} aria-hidden />
|
||||||
|
Andere Aufgabe
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</animated.div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -314,13 +515,13 @@ function MissionActionCard({
|
|||||||
function EmotionActionCard() {
|
function EmotionActionCard() {
|
||||||
return (
|
return (
|
||||||
<Card className="border border-muted/40 shadow-sm">
|
<Card className="border border-muted/40 shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader className="py-[4px]">
|
||||||
<CardTitle>Foto nach Gefühlslage</CardTitle>
|
<CardTitle>Wähle eine Stimmung und erhalte eine passende Aufgabe</CardTitle>
|
||||||
<CardDescription className="text-sm text-muted-foreground">
|
<CardDescription className="text-sm text-muted-foreground">
|
||||||
Wähle deine Stimmung, wir schlagen dir passende Missionen vor.
|
Tippe deinen Mood, wir picken die nächste Mission für dich.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="py-[4px]">
|
||||||
<EmotionPicker variant="embedded" showSkip={false} />
|
<EmotionPicker variant="embedded" showSkip={false} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -342,30 +543,35 @@ function UploadActionCard({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className="overflow-hidden border-0 text-white shadow-sm"
|
className="overflow-hidden border border-muted/30 shadow-sm"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(120deg, ${accentColor}, ${secondaryAccent})`,
|
background: 'var(--guest-surface)',
|
||||||
borderRadius: `${radius}px`,
|
borderRadius: `${radius}px`,
|
||||||
fontFamily: bodyFont,
|
fontFamily: bodyFont,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent className="flex flex-col gap-3 py-5">
|
<CardContent className="flex flex-col gap-1.5 py-[4px]">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="rounded-2xl bg-white/15 p-3">
|
<div className="rounded-2xl p-3" style={{ background: `${accentColor}15` }}>
|
||||||
<UploadCloud className="h-5 w-5" aria-hidden />
|
<UploadCloud className="h-5 w-5" aria-hidden style={{ color: accentColor }} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-lg font-semibold">Direkt hochladen</p>
|
<p className="text-lg font-semibold text-foreground">Direkt hochladen</p>
|
||||||
<p className="text-sm text-white/80">Kamera öffnen oder ein Foto aus deiner Galerie wählen.</p>
|
<p className="text-sm text-muted-foreground">Kamera öffnen oder ein Foto aus deiner Galerie wählen.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
className="bg-white/90 text-slate-900 hover:bg-white"
|
className="text-white"
|
||||||
style={{ borderRadius: `${radius}px` }}
|
style={{
|
||||||
|
borderRadius: `${radius}px`,
|
||||||
|
background: `linear-gradient(120deg, ${accentColor}, ${secondaryAccent})`,
|
||||||
|
boxShadow: `0 12px 28px ${accentColor}25`,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Link to={`/e/${encodeURIComponent(token)}/upload`}>Foto hochladen</Link>
|
<Link to={`/e/${encodeURIComponent(token)}/upload`}>Foto hochladen</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
<p className="text-xs text-muted-foreground">Offline möglich – wir laden später hoch.</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user