From 935921f6989d806c1d6c49298f32f8f5d922aae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Fri, 27 Mar 2026 16:23:06 +0100 Subject: [PATCH] Added multi-lingua, english-spanish. Timer conditions and Linting bugfixes. --- .github/copilot-instructions.md | 25 ++++ .github/hooks/rtk-rewrite.json | 12 ++ bun.lock | 16 +-- package-lock.json | 113 ++++++++++++++++-- package.json | 4 + src/components/AboutSection.tsx | 107 ++++++++--------- src/components/BookingSection.tsx | 165 +++++++++++++++++++------- src/components/FloatingButton.tsx | 6 +- src/components/FooterSection.tsx | 145 +++++++++++++---------- src/components/GallerySection.tsx | 95 +++++++-------- src/components/HeroSection.tsx | 45 +++++--- src/components/HotelSection.tsx | 125 +++++++++++--------- src/components/Navbar.tsx | 38 +++++- src/components/OrgSection.tsx | 54 +++++---- src/components/PracticalSection.tsx | 164 +++++++++++++------------- src/components/ScheduleSection.tsx | 172 +++++++++++++++++++--------- src/components/StaffSection.tsx | 48 +++++--- src/data/event-data.ts | 59 ++-------- src/i18n.ts | 23 ++++ src/locales/en.json | 156 +++++++++++++++++++++++++ src/locales/es.json | 156 +++++++++++++++++++++++++ src/main.tsx | 1 + 22 files changed, 1220 insertions(+), 509 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/hooks/rtk-rewrite.json create mode 100644 src/i18n.ts create mode 100644 src/locales/en.json create mode 100644 src/locales/es.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..87388b6 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,25 @@ +# RTK — Token-Optimized CLI + +**rtk** is a CLI proxy that filters and compresses command outputs, saving 60-90% tokens. + +## Rule + +Always prefix shell commands with `rtk`: + +```bash +# Instead of: Use: +git status rtk git status +git log -10 rtk git log -10 +cargo test rtk cargo test +docker ps rtk docker ps +kubectl get pods rtk kubectl pods +``` + +## Meta commands (use directly) + +```bash +rtk gain # Token savings dashboard +rtk gain --history # Per-command savings history +rtk discover # Find missed rtk opportunities +rtk proxy # Run raw (no filtering) but track usage +``` diff --git a/.github/hooks/rtk-rewrite.json b/.github/hooks/rtk-rewrite.json new file mode 100644 index 0000000..eb2a5a7 --- /dev/null +++ b/.github/hooks/rtk-rewrite.json @@ -0,0 +1,12 @@ +{ + "hooks": { + "PreToolUse": [ + { + "type": "command", + "command": "rtk hook copilot", + "cwd": ".", + "timeout": 5 + } + ] + } +} diff --git a/bun.lock b/bun.lock index 4f80baa..ad2e2f6 100644 --- a/bun.lock +++ b/bun.lock @@ -34,6 +34,8 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-query": "^5.83.0", + "baseline-browser-mapping": "^2.10.11", + "caniuse-lite": "^1.0.30001781", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -494,6 +496,8 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], @@ -510,7 +514,7 @@ "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="], + "caniuse-lite": ["caniuse-lite@1.0.30001781", "", {}, "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw=="], "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], @@ -1122,6 +1126,10 @@ "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="], + + "browserslist/caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="], + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "cssstyle/cssom": ["cssom@0.3.8", "", {}, "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="], @@ -1148,8 +1156,6 @@ "strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], "tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], @@ -1166,8 +1172,6 @@ "glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], @@ -1215,7 +1219,5 @@ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/package-lock.json b/package-lock.json index 192873b..2c5d9cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,12 +37,15 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-query": "^5.83.0", + "baseline-browser-mapping": "^2.10.11", + "caniuse-lite": "^1.0.30001781", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^3.6.0", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.35.0", + "i18next": "^25.10.10", "input-otp": "^1.4.2", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", @@ -50,6 +53,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.61.1", + "react-i18next": "^16.6.6", "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", "recharts": "^2.15.4", @@ -130,9 +134,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", - "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3680,6 +3684,18 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", + "integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3792,10 +3808,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", - "dev": true, + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", "funding": [ { "type": "opencollective", @@ -5183,6 +5198,15 @@ "node": ">=12" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -5212,6 +5236,37 @@ "node": ">= 6" } }, + "node_modules/i18next": { + "version": "25.10.10", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.10.tgz", + "integrity": "sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -6782,6 +6837,33 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, + "node_modules/react-i18next": { + "version": "16.6.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.6.tgz", + "integrity": "sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.10.9", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -7661,7 +7743,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7808,9 +7890,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -8013,6 +8095,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", diff --git a/package.json b/package.json index 17c95c0..073cb8d 100644 --- a/package.json +++ b/package.json @@ -42,12 +42,15 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-query": "^5.83.0", + "baseline-browser-mapping": "^2.10.11", + "caniuse-lite": "^1.0.30001781", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^3.6.0", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.35.0", + "i18next": "^25.10.10", "input-otp": "^1.4.2", "lucide-react": "^0.462.0", "next-themes": "^0.3.0", @@ -55,6 +58,7 @@ "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.61.1", + "react-i18next": "^16.6.6", "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", "recharts": "^2.15.4", diff --git a/src/components/AboutSection.tsx b/src/components/AboutSection.tsx index e413c79..74acadc 100644 --- a/src/components/AboutSection.tsx +++ b/src/components/AboutSection.tsx @@ -1,63 +1,68 @@ import { motion } from "framer-motion"; -import { ABOUT_EVENT } from "@/data/event-data"; import aboutImg from "@/assets/about-event.jpg"; import { Music, Users, Sparkles, PartyPopper } from "lucide-react"; +import { useTranslation } from "react-i18next"; const iconMap = [Music, Users, Sparkles, PartyPopper]; -const AboutSection = () => ( -
-
-
- {/* Imagen */} - - Evento de Lambada - +const AboutSection = () => { + const { t } = useTranslation(); + const highlightKeys = ["workshops", "socialDancing", "liveShows", "djSets"] as const; - {/* Texto */} - -

- {ABOUT_EVENT.title} -

-

- {ABOUT_EVENT.description} -

-

- {ABOUT_EVENT.lambadaInfo} -

+ return ( +
+
+
+ {/* Imagen */} + + {t("about.title")} + - {/* Highlights */} -
- {ABOUT_EVENT.highlights.map((item, i) => { - const Icon = iconMap[i % iconMap.length]; - return ( -
-
- + {/* Texto */} + +

+ {t("about.title")} +

+

+ {t("about.description")} +

+

+ {t("about.lambadaInfo")} +

+ + {/* Highlights */} +
+ {highlightKeys.map((key, i) => { + const Icon = iconMap[i % iconMap.length]; + return ( +
+
+ +
+ {t(`about.highlights.${key}`)}
- {item} -
- ); - })} -
- + ); + })} +
+ +
-
-
-); +
+ ); +}; export default AboutSection; diff --git a/src/components/BookingSection.tsx b/src/components/BookingSection.tsx index f1e9c8a..e4afdfa 100644 --- a/src/components/BookingSection.tsx +++ b/src/components/BookingSection.tsx @@ -4,6 +4,9 @@ import { z } from "zod"; import { Button } from "@/components/ui/button"; import { WEBHOOK_URL, WEBHOOK_SECRET } from "@/data/event-data"; import { CheckCircle, Loader2, AlertCircle, Search } from "lucide-react"; +import { EVENT_INFO } from "@/data/event-data"; +import { getTimeLeft } from "@/components/HeroSection"; +import { useTranslation } from "react-i18next"; /** * FORMULARIO DE RESERVA @@ -17,25 +20,25 @@ import { CheckCircle, Loader2, AlertCircle, Search } from "lucide-react"; // Define pass types with prices const PASS_TYPES = [ - { id: "full", name: "Full Pass", price: 150 }, - { id: "party", name: "Party Pass", price: 80 }, - { id: "single", name: "Single Day Pass", price: 40 }, + { id: "full", price: 150 }, + { id: "party", price: 80 }, + { id: "single", price: 40 }, ]; -const bookingSchema = z.object({ - requestId: z.string().optional(), - name: z.string().trim().min(2, "El nombre debe tener al menos 2 caracteres").max(100), - surname: z.string().trim().min(2, "El apellido debe tener al menos 2 caracteres").max(100), - email: z.string().trim().email("Email no válido").max(255), - passType: z.string().min(1, "Selecciona un tipo de pass"), - amount: z.string().min(1, "Selecciona la cantidad"), - price: z.number().optional(), - country: z.string().trim().min(2, "Indica tu país").max(100), -}); - -type BookingData = z.infer; +type BookingData = { + requestId?: string; + name: string; + surname: string; + email: string; + passType: string; + amount: string; + price: number; + country: string; +}; const BookingSection = () => { + const { t, i18n } = useTranslation(); + const [form, setForm] = useState({ requestId: "", name: "", surname: "", email: "", passType: "", amount: "1", price: 0, country: "", }); @@ -46,6 +49,31 @@ const BookingSection = () => { const countryDropdownRef = useRef(null); const countryInputRef = useRef(null); const sectionRef = useRef(null); + + const bookingSchema = z.object({ + requestId: z.string().optional(), + name: z.string().trim().min(2, i18n.t("booking.validation.nameMin")).max(100), + surname: z.string().trim().min(2, i18n.t("booking.validation.surnameMin")).max(100), + email: z.string().trim().email(i18n.t("booking.validation.emailInvalid")).max(255), + passType: z.string().min(1, i18n.t("booking.validation.passTypeRequired")), + amount: z.string().min(1, i18n.t("booking.validation.amountRequired")), + price: z.number().optional(), + country: z.string().trim().min(2, i18n.t("booking.validation.countryMin")).max(100), + }); + + const updatePassTypeOrAmount = (field: "passType" | "amount", value: string) => { + setForm((prev) => { + const updatedForm = { ...prev, [field]: value } as BookingData; + const selectedPass = PASS_TYPES.find((pass) => pass.id === (field === "passType" ? value : updatedForm.passType)); + const amountValue = parseInt(updatedForm.amount); + const newPrice = selectedPass ? selectedPass.price * amountValue : updatedForm.price; + + return { ...updatedForm, price: newPrice }; + }); + + setErrors((prev) => ({ ...prev, [field]: undefined })); + }; + // Generate unique requestId on component mount useEffect(() => { const uniqueId = `REQ-${Date.now()}-${Math.floor(Math.random() * 10000)}`; @@ -121,7 +149,7 @@ const BookingSection = () => { const result = bookingSchema.safeParse(form); if (!result.success) { - const fieldErrors: typeof errors = {}; + const fieldErrors: Partial> = {}; result.error.errors.forEach((err) => { const field = err.path[0] as keyof BookingData; fieldErrors[field] = err.message; @@ -151,6 +179,16 @@ const BookingSection = () => { } }; + const [timeLeft, setTimeLeft] = useState(getTimeLeft(EVENT_INFO.date)); + const isEventOngoing = timeLeft.days === 0 && timeLeft.hours === 0 && timeLeft.minutes === 0 && timeLeft.seconds === 0; + + useEffect(() => { + const timer = setInterval(() => { + setTimeLeft(getTimeLeft(EVENT_INFO.date)); + }, 1000); + return () => clearInterval(timer); + }, []); + if (status === "success") { return (
{ >

- ¡Reserva recibida! + {t("booking.status.successTitle")}

- Recibirás confirmación por email. ¡Nos vemos en la pista! + {t("booking.status.success")} +

+ +
+ ); + } + + if (isEventOngoing) { + return ( +
+ + +

+ {t("booking.status.eventInProgressTitle")} +

+

+ {t("booking.status.eventInProgressDescription")}

@@ -191,11 +254,9 @@ const BookingSection = () => { className="text-center mb-10" >

- Reserva tu pase + {t("booking.subtitle")}

-

- Selecciona tu pase y cantidad. Sin pago online, paga en el evento. -

+

{t("booking.description")}

{
{/* Nombre */}
- + {errors.name &&

{errors.name}

}
{/* Apellido */}
- + {errors.surname &&

{errors.surname}

}
@@ -238,21 +303,25 @@ const BookingSection = () => { {/* Email */}
- + {errors.email &&

{errors.email}

}
{/* País - Searchable Selector */}
- +
{ setShowCountryDropdown(true); }} onFocus={() => setShowCountryDropdown(true)} - placeholder="Buscar país..." + placeholder={t("booking.formFields.countryPlaceholder")} className="w-full rounded-lg border border-input bg-background px-4 py-3 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring pr-10" />
@@ -290,7 +359,7 @@ const BookingSection = () => { )) ) : (
- No se encontraron países + {t("booking.formFields.countryNoResults")}
)} @@ -302,7 +371,9 @@ const BookingSection = () => { {/* Tipo de Pass - Cards */}
- +
{PASS_TYPES.map((pass) => ( { ? "border-primary bg-primary/5 shadow-lg" : "border-border hover:border-primary/50 hover:shadow-md" }`} - onClick={() => handleChange({ target: { name: "passType", value: pass.id } } as any)} + onClick={() => updatePassTypeOrAmount("passType", pass.id)} >
-

{pass.name}

+

+ {pass.id === "full" + ? t("booking.fullPass") + : pass.id === "party" + ? t("booking.partyPass") + : t("booking.singleDayPass")} +

{pass.price}€

@@ -328,7 +405,9 @@ const BookingSection = () => { {/* Cantidad - Add/Subtract Buttons */}
- +
{ if (!form.passType) return; const currentAmount = parseInt(form.amount); if (currentAmount > 1) { - handleChange({ target: { name: "amount", value: String(currentAmount - 1) } } as any); + updatePassTypeOrAmount("amount", String(currentAmount - 1)); } }} className={`w-12 h-12 rounded-full flex items-center justify-center text-xl font-bold transition-colors ${ @@ -367,7 +446,7 @@ const BookingSection = () => { if (!form.passType) return; const currentAmount = parseInt(form.amount); if (currentAmount < 10) { - handleChange({ target: { name: "amount", value: String(currentAmount + 1) } } as any); + updatePassTypeOrAmount("amount", String(currentAmount + 1)); } }} className={`w-12 h-12 rounded-full flex items-center justify-center text-xl font-bold transition-colors ${ @@ -391,9 +470,9 @@ const BookingSection = () => { className="bg-gradient-to-r from-primary/10 to-primary/5 rounded-xl p-6 border border-primary/20" >
-

Precio Total

+

{t("booking.priceSummaryLabel")}

{form.price.toFixed(2)}€

-

Sin pago online • Paga en el evento

+

{t("booking.priceSummaryNote")}

)} @@ -401,7 +480,7 @@ const BookingSection = () => { {status === "error" && (
- Error al enviar. Inténtalo de nuevo. + {t("booking.status.error")}
)} @@ -413,9 +492,11 @@ const BookingSection = () => { disabled={status === "loading"} > {status === "loading" ? ( - <> Enviando... + <> + {t("booking.status.loading")} + ) : ( - "Reservar Pass" + t("booking.buyButton") )} diff --git a/src/components/FloatingButton.tsx b/src/components/FloatingButton.tsx index 2ea98ba..82f3631 100644 --- a/src/components/FloatingButton.tsx +++ b/src/components/FloatingButton.tsx @@ -2,10 +2,12 @@ import { Button } from "@/components/ui/button"; import { motion, AnimatePresence } from "framer-motion"; import { useState, useEffect } from "react"; import { ChevronUp } from "lucide-react"; +import { useTranslation } from "react-i18next"; /** Botón flotante de reserva + scroll to top */ const FloatingButton = () => { const [visible, setVisible] = useState(false); + const { t } = useTranslation(); useEffect(() => { const onScroll = () => setVisible(window.scrollY > 400); @@ -42,13 +44,13 @@ const FloatingButton = () => { - Reservar + {t("nav.booking")} diff --git a/src/components/FooterSection.tsx b/src/components/FooterSection.tsx index adc218b..fdd0bf0 100644 --- a/src/components/FooterSection.tsx +++ b/src/components/FooterSection.tsx @@ -1,72 +1,99 @@ -import { ABOUT_ORG, FOOTER } from "@/data/event-data"; +import { ABOUT_ORG } from "@/data/event-data"; import { Instagram, Facebook, Youtube, Mail } from "lucide-react"; import hacecalor from "@/assets/hacecalor.png"; import activat from "@/assets/activat.png"; import { motion } from "framer-motion"; +import { useTranslation } from "react-i18next"; -const FooterSection = () => ( - -
-
- {/* Brand */} -
-

ZoukLambadaBCN

-

- Hacecalor - Activat -

-
+const FooterSection = () => { + const { t } = useTranslation(); + const year = new Date().getFullYear(); + const email = t("footer.email"); + const copyright = t("footer.copyright", { year }); - {/* Contacto */} -
-

Contacto

- - - {FOOTER.email} - -
+ return ( + +
+
+ {/* Brand */} +
+

ZoukLambadaBCN

+

+ Hacecalor + Activat +

+
- {/* Redes */} -
-

Síguenos

-
- {ABOUT_ORG.socials.instagram && ( - - - - )} - {ABOUT_ORG.socials.facebook && ( - - - - )} - {ABOUT_ORG.socials.youtube && ( - - - - )} + {/* Contacto */} +
+

+ {t("footer.contact")} +

+ + + {email} + +
+ + {/* Redes */} +
+

+ {t("footer.followUs")} +

+
+ {ABOUT_ORG.socials.instagram && ( + + + + )} + {ABOUT_ORG.socials.facebook && ( + + + + )} + {ABOUT_ORG.socials.youtube && ( + + + + )} +
-
-
-

{FOOTER.copyright}

+
+

{copyright}

+
-
- -); + + ); +}; export default FooterSection; diff --git a/src/components/GallerySection.tsx b/src/components/GallerySection.tsx index a616abf..d6da036 100644 --- a/src/components/GallerySection.tsx +++ b/src/components/GallerySection.tsx @@ -1,53 +1,58 @@ import { motion } from "framer-motion"; import { GALLERY_IMAGES } from "@/data/event-data"; import { ImageIcon } from "lucide-react"; +import { useTranslation } from "react-i18next"; -const GallerySection = () => ( - -); + + ); +}; export default GallerySection; diff --git a/src/components/HeroSection.tsx b/src/components/HeroSection.tsx index 9f64423..da79c34 100644 --- a/src/components/HeroSection.tsx +++ b/src/components/HeroSection.tsx @@ -1,11 +1,12 @@ import { useState, useEffect } from "react"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; +import { useTranslation } from "react-i18next"; import { EVENT_INFO } from "@/data/event-data"; import heroBg from "@/assets/hero-bg.jpg"; /** Calcula diferencia entre ahora y la fecha del evento */ -const getTimeLeft = (target: string) => { +export const getTimeLeft = (target: string) => { const diff = new Date(target).getTime() - Date.now(); if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0 }; return { @@ -17,6 +18,7 @@ const getTimeLeft = (target: string) => { }; const HeroSection = () => { + const { t } = useTranslation(); const [timeLeft, setTimeLeft] = useState(getTimeLeft(EVENT_INFO.date)); useEffect(() => { @@ -27,10 +29,10 @@ const HeroSection = () => { }, []); const countdownItems = [ - { value: timeLeft.days, label: "Días" }, - { value: timeLeft.hours, label: "Horas" }, - { value: timeLeft.minutes, label: "Min" }, - { value: timeLeft.seconds, label: "Seg" }, + { value: timeLeft.days, label: t('hero.days') }, + { value: timeLeft.hours, label: t('hero.hours') }, + { value: timeLeft.minutes, label: t('hero.minutes') }, + { value: timeLeft.seconds, label: t('hero.seconds') }, ]; return ( @@ -53,7 +55,7 @@ const HeroSection = () => { transition={{ delay: 0.2 }} className="text-primary font-body text-sm uppercase tracking-[0.3em] mb-4" > - {EVENT_INFO.dateDisplay} · {EVENT_INFO.city} + {t('hero.dates')} · {t('hero.location')} { transition={{ delay: 0.4 }} className="font-hero text-4xl md:text-7xl lg:text-8xl font-bold text-primary-foreground mb-4 leading-tight" > - {EVENT_INFO.name} + {t('hero.title')} { transition={{ delay: 0.6 }} className="text-primary-foreground/80 font-body text-lg md:text-xl mb-10" > - {EVENT_INFO.subtitle} + {t('hero.subtitle')} {/* Countdown */} @@ -81,18 +83,31 @@ const HeroSection = () => { transition={{ delay: 0.8 }} className="flex justify-center gap-4 md:gap-8 mb-10" > - {countdownItems.map((item) => ( -
-
+ {timeLeft.days === 0 && timeLeft.hours === 0 && timeLeft.minutes === 0 && timeLeft.seconds === 0 ? ( +
+
- {String(item.value).padStart(2, "0")} + {t('hero.eventInProgress')}

- {item.label} + {t('hero.dontMiss')}

- ))} + ) : ( + countdownItems.map((item) => ( +
+
+ + {String(item.value).padStart(2, "0")} + +
+

+ {item.label} +

+
+ )) + )} { transition={{ delay: 1 }} >
diff --git a/src/components/HotelSection.tsx b/src/components/HotelSection.tsx index 05d546b..eedb34b 100644 --- a/src/components/HotelSection.tsx +++ b/src/components/HotelSection.tsx @@ -2,64 +2,79 @@ import { motion } from "framer-motion"; import { HOTEL_ROOMS } from "@/data/event-data"; import { Button } from "@/components/ui/button"; import { BedDouble, ExternalLink } from "lucide-react"; +import { useTranslation } from "react-i18next"; -const HotelSection = () => ( -
-
- -

- Alojamiento -

-

- Habitaciones recomendadas cerca del venue. -

-
+const HotelSection = () => { + const { t } = useTranslation(); + const roomTypeKeyById: Record = { + "1": "individual", + "2": "double", + "3": "suite", + }; -
- {HOTEL_ROOMS.map((room, i) => ( - - {/* Imagen */} -
- {room.image ? ( - {room.name} - ) : ( - - )} -
+ return ( +
+
+ +

+ {t("hotel.title")} +

+

{t("hotel.subtitle")}

+
-
-

- {room.name} -

-

{room.price}

-

{room.description}

- -
- - ))} +
+ {HOTEL_ROOMS.map((room, i) => { + const typeKey = roomTypeKeyById[room.id]; + const translatedName = typeKey ? t(`hotel.${typeKey}`) : room.name; + const translatedPrice = room.price?.includes("[") ? t("hotel.pricePerNight") : room.price; + const translatedDescription = room.description?.includes("[") ? t("hotel.description") : room.description; + + return ( + + {/* Imagen */} +
+ {room.image ? ( + {translatedName} + ) : ( + + )} +
+ +
+

+ {translatedName} +

+

{translatedPrice}

+

{translatedDescription}

+ +
+
+ ); + })} +
-
-
-); + + ); +}; export default HotelSection; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 4a7d1d8..ef7ffc7 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import { Menu, X } from "lucide-react"; +import { Menu, X, Globe } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { NAV_LINKS } from "@/data/event-data"; import Logo from "@/assets/logo.png"; @@ -11,6 +12,12 @@ import Logo from "@/assets/logo.png"; const Navbar = () => { const [scrolled, setScrolled] = useState(false); const [menuOpen, setMenuOpen] = useState(false); + const { t, i18n } = useTranslation(); + + const toggleLanguage = () => { + const newLang = i18n.language === 'es' ? 'en' : 'es'; + i18n.changeLanguage(newLang); + }; useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 50); @@ -49,9 +56,21 @@ const Navbar = () => { href={link.href} className="text-sm font-medium text-black/80 hover:text-primary transition-colors" > - {link.label} + {t(`nav.${link.href.substring(1)}`)} ))} + + {/* Language Switcher */} +
{/* Mobile toggle */} @@ -85,9 +104,22 @@ const Navbar = () => { onClick={() => setMenuOpen(false)} className="text-3xl md:text-4xl lg:text-5xl pt-3 pb-6 break-words leading-[1.6] font-display font-medium text-foreground hover:text-primary transition-colors" > - {link.label} + {t(`nav.${link.href.substring(1)}`)} ))} + + {/* Language Switcher */} + + + {i18n.language === 'es' ? 'EN' : 'ES'} + )} diff --git a/src/components/OrgSection.tsx b/src/components/OrgSection.tsx index 4a624e6..9de7b71 100644 --- a/src/components/OrgSection.tsx +++ b/src/components/OrgSection.tsx @@ -2,30 +2,33 @@ import { motion } from "framer-motion"; import { ABOUT_ORG } from "@/data/event-data"; import communityImg from "@/assets/community.jpg"; import { Instagram, Facebook, Youtube } from "lucide-react"; +import { useTranslation } from "react-i18next"; const maskStyle = "radial-gradient(196.7px at 50% 264px, rgb(0, 0, 0) 99%, rgba(0, 0, 0, 0) 101%) calc(50% - 176px) 0px / 352px 100%, radial-gradient(196.7px at 50% -176px, rgba(0, 0, 0, 0) 99%, rgb(0, 0, 0) 101%) 50% 78px / 352px 100% repeat-x"; -const OrgSection = () => ( -
-
-
- {/* Texto */} - -

- {ABOUT_ORG.title} -

-
-

{ABOUT_ORG.history}

-

{ABOUT_ORG.philosophy}

-
+const OrgSection = () => { + const { t } = useTranslation(); + + return ( +
+
+
+ {/* Texto */} + +

+ {t("about.orgTitle")} +

+
+

{t("about.orgDescription")}

+
{/* Redes sociales */}
@@ -63,7 +66,7 @@ const OrgSection = () => ( )}
-
+ {/* Imagen */} ( className="rounded-2xl shadow-elevated w-full h-auto object-contain bg-muted" /> +
-
-
-); + + ); +}; export default OrgSection; diff --git a/src/components/PracticalSection.tsx b/src/components/PracticalSection.tsx index 450d873..a5f3f61 100644 --- a/src/components/PracticalSection.tsx +++ b/src/components/PracticalSection.tsx @@ -1,93 +1,103 @@ import { motion } from "framer-motion"; import { EVENT_INFO, PRACTICAL_INFO } from "@/data/event-data"; import { MapPin, Plane, Train } from "lucide-react"; +import { useTranslation } from "react-i18next"; -const PracticalSection = () => ( -
-
- -

- Información Práctica -

-
+const PracticalSection = () => { + const { t } = useTranslation(); -
- {/* Mapa */} + return ( +
+
-
-