diff --git a/.gitignore b/.gitignore index a8f86bc..0f5b2ba 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +.vscode diff --git a/package-lock.json b/package-lock.json index efef1d8..d814ba3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "@contentstack/live-preview-utils": "^4.2.1", "@contentstack/personalize-edge-sdk": "^1.0.16", "@contentstack/utils": "^1.4.4", + "@headlessui/react": "^2.2.9", + "@heroicons/react": "^2.2.0", "contentstack": "^3.26.2", "next": "15.5.4", "next-intl": "^4.3.9", @@ -285,28 +287,56 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", - "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", - "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.4", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, "node_modules/@formatjs/ecma402-abstract": { @@ -370,6 +400,35 @@ "tslib": "2" } }, + "node_modules/@headlessui/react": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz", + "integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1464,6 +1523,7 @@ "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-2.6.1.tgz", "integrity": "sha512-Gp3DI1T/0YyirwJnImR8l9xyVJgKiVzJXmEhic1/7SPw3zStrsvuBpwKnD609CzsIdzxprWa6yTNXN+VLLZPGQ==", "license": "MIT", + "peer": true, "dependencies": { "@preact/signals-core": "^1.12.2" }, @@ -1485,6 +1545,103 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/@react-aria/focus": { + "version": "3.21.5", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.5.tgz", + "integrity": "sha512-V18fwCyf8zqgJdpLQeDU5ZRNd9TeOfBbhLgmX77Zr5ae9XwaoJ1R3SFJG1wCJX60t34AW+aLZSEEK+saQElf3Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.1", + "@react-aria/utils": "^3.33.1", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.27.1", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.27.1.tgz", + "integrity": "sha512-M3wLpTTmDflI0QGNK0PJNUaBXXfeBXue8ZxLMngfc1piHNiH4G5lUvWd9W14XVbqrSCVY8i8DfGrNYpyyZu0tw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.1", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.33.1.tgz", + "integrity": "sha512-kIx1Sj6bbAT0pdqCegHuPanR9zrLn5zMRiM7LN12rgRf55S19ptd9g3ncahArifYTRkfEU9VIn+q0HjfMqS9/w==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.1", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz", + "integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.33.1.tgz", + "integrity": "sha512-oJHtjvLG43VjwemQDadlR5g/8VepK56B/xKO2XORPHt9zlW6IZs3tZrYlvH29BMvoqC7RtE7E5UjgbnbFtDGag==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz", @@ -1973,6 +2130,33 @@ "tailwindcss": "4.1.18" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz", + "integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.23" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "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/@tanstack/virtual-core": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", + "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -2021,6 +2205,7 @@ "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2087,6 +2272,7 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -2573,6 +2759,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2854,6 +3041,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -3021,6 +3209,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3103,7 +3300,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -3538,6 +3736,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3711,6 +3910,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5698,17 +5898,6 @@ } } }, - "node_modules/next-intl/node_modules/@swc/helpers": { - "version": "0.5.18", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", - "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.8.0" - } - }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -6062,6 +6251,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz", "integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -6146,6 +6336,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6155,6 +6346,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6767,6 +6959,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, "node_modules/tailwindcss": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", @@ -6829,6 +7027,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6984,6 +7183,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7084,6 +7284,15 @@ "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" } }, + "node_modules/use-sync-external-store": { + "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" + } + }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/package.json b/package.json index d14ecc3..57e3fd4 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "@contentstack/live-preview-utils": "^4.2.1", "@contentstack/personalize-edge-sdk": "^1.0.16", "@contentstack/utils": "^1.4.4", + "@headlessui/react": "^2.2.9", + "@heroicons/react": "^2.2.0", "contentstack": "^3.26.2", "next": "15.5.4", "next-intl": "^4.3.9", diff --git a/public/fonts/250e8e4b496e4dc5220091e3deb9695d.woff2 b/public/fonts/250e8e4b496e4dc5220091e3deb9695d.woff2 new file mode 100644 index 0000000..e6d1844 Binary files /dev/null and b/public/fonts/250e8e4b496e4dc5220091e3deb9695d.woff2 differ diff --git a/public/fonts/68f02d7f0c95808a4553b17278e213cd.woff b/public/fonts/68f02d7f0c95808a4553b17278e213cd.woff new file mode 100644 index 0000000..da0a9ab Binary files /dev/null and b/public/fonts/68f02d7f0c95808a4553b17278e213cd.woff differ diff --git a/public/fonts/7893d414982a0e78c26139ee5b97b2de.woff b/public/fonts/7893d414982a0e78c26139ee5b97b2de.woff new file mode 100644 index 0000000..a76e7c6 Binary files /dev/null and b/public/fonts/7893d414982a0e78c26139ee5b97b2de.woff differ diff --git a/public/fonts/acf09532871d2823b12362210faf5b46.woff b/public/fonts/acf09532871d2823b12362210faf5b46.woff new file mode 100644 index 0000000..90a634c Binary files /dev/null and b/public/fonts/acf09532871d2823b12362210faf5b46.woff differ diff --git a/public/fonts/cec2d493e9b85b4b4e19838cd9579fd8.woff2 b/public/fonts/cec2d493e9b85b4b4e19838cd9579fd8.woff2 new file mode 100644 index 0000000..929daa4 Binary files /dev/null and b/public/fonts/cec2d493e9b85b4b4e19838cd9579fd8.woff2 differ diff --git a/public/fonts/f4a729dac7ca5c2ee60fcccc3547954b.woff2 b/public/fonts/f4a729dac7ca5c2ee60fcccc3547954b.woff2 new file mode 100644 index 0000000..30eb5fc Binary files /dev/null and b/public/fonts/f4a729dac7ca5c2ee60fcccc3547954b.woff2 differ diff --git a/public/fonts/rift/524071a6787baf1dcac8a09b88a39672.woff b/public/fonts/rift/524071a6787baf1dcac8a09b88a39672.woff new file mode 100644 index 0000000..7f85179 Binary files /dev/null and b/public/fonts/rift/524071a6787baf1dcac8a09b88a39672.woff differ diff --git a/public/fonts/rift/a b/public/fonts/rift/a new file mode 100644 index 0000000..2ebadf1 Binary files /dev/null and b/public/fonts/rift/a differ diff --git a/public/fonts/rift/cf8df83caa33a220e6abb7190465ebc3.woff2 b/public/fonts/rift/cf8df83caa33a220e6abb7190465ebc3.woff2 new file mode 100644 index 0000000..8f20eaf Binary files /dev/null and b/public/fonts/rift/cf8df83caa33a220e6abb7190465ebc3.woff2 differ diff --git a/public/fonts/rift/d.woff b/public/fonts/rift/d.woff new file mode 100644 index 0000000..53148e2 Binary files /dev/null and b/public/fonts/rift/d.woff differ diff --git a/public/fonts/rift/l.woff2 b/public/fonts/rift/l.woff2 new file mode 100644 index 0000000..632f207 Binary files /dev/null and b/public/fonts/rift/l.woff2 differ diff --git a/src/app/[locale]/[...slug]/layout.jsx b/src/app/[locale]/[...slug]/layout.jsx new file mode 100644 index 0000000..79667bd --- /dev/null +++ b/src/app/[locale]/[...slug]/layout.jsx @@ -0,0 +1,47 @@ + +import { cache } from "react"; +import { headers } from "next/headers"; +import ContentstackServer from "@/lib/cstack"; +import DataContextProvider from "@/context/data.context"; + +const fetchData = cache(async (locale) => { + const headersList = await headers(); + const variantParam = headersList.get('x-personalize-variants'); + // example of how to fetch seo metadata from contentstack, replace "homepage" with the content type which contains the seo metadata + const data = await ContentstackServer.getElementByType("landing_pages", locale, {}, variantParam); + return data; +}); + +export const generateMetadata = async ({ params }) => { + const { locale } = await params; + const data = await fetchData(locale); + const entry = data?.[0]; + + return { + title: entry?.seo?.title, + description: entry?.seo?.description, + robots: { + index: entry?.seo?.no_index || false, + follow: entry?.seo?.no_follow || false, + }, + openGraph: { + title: entry?.seo?.og_meta_tags?.title, + description: entry?.seo?.og_meta_tags?.description, + images: entry?.seo?.og_meta_tags?.image, + }, + } +}; + +export default async function RootLayout({ + children, + params, +}) { + const { locale } = await params; + const data = await fetchData(locale); + + return ( + + {children} + + ); +} diff --git a/src/app/[locale]/[...slug]/page.jsx b/src/app/[locale]/[...slug]/page.jsx new file mode 100644 index 0000000..7cf1845 --- /dev/null +++ b/src/app/[locale]/[...slug]/page.jsx @@ -0,0 +1,96 @@ +"use client"; +import { useDataContext } from "@/context/data.context"; +import { ContentstackClient } from "@/lib/contentstack-client"; +import UnlockAdventureSection from "@/components/UnlockAdventureSection"; +import PreviewVehicle from "@/components/PreviewVehicle"; +import HeroBanner from "@/components/HeroBanner"; +import { useState, useEffect, use } from "react"; +import TextAndImage from "@/components/TextAndImage"; +import CardsCollection from "@/components/CardsCollection"; +import BuyingTools from "@/components/BuyingTools"; +import Navbar from "@/components/Navbar"; + +export default function Home({ params }) { + const { locale } = use(params); + const initialData = useDataContext(); + const pageUrl = use(params).slug?.length > 0 ? "/"+use(params).slug.join('/') : null; + + const [entry, setEntry] = useState(null); + + const getContent = async () => { + const data = await ContentstackClient.getElementByUrlWithRefs( + "landing_pages", + pageUrl, locale, + [ + 'hero_banner', + 'hero_banner.cta.internal_link', + 'modular_blocks.unlock_adventure_section.reference', + 'modular_blocks.unlock_adventure_section.reference.vehicles.internal_url', + 'modular_blocks.preview_vehicle.vehicle_preview_reference', + 'modular_blocks.preview_vehicle.vehicle_preview_reference.vehicle_models', + 'modular_blocks.preview_vehicle.vehicle_preview_reference.link.internal_link', + 'modular_blocks.buying_tools.reference', + 'modular_blocks.buying_tools.reference.icon_list.internal_url', + ], + // initialData + ) + + setEntry(data[0]); + }; + + useEffect(() => { + ContentstackClient.onEntryChange(() => { + getContent(); + }); + }, []); + + return ( +
+
+ + +
+ {entry?.modular_blocks.map((block, index) => ( +
+ {block.hasOwnProperty("unlock_adventure_section") && ( + + )} + {block.hasOwnProperty("preview_vehicle") && ( + + )} + {block.hasOwnProperty("text_and_image") && ( + + )} + {block.hasOwnProperty("card_collection") && ( + + )} + {block.hasOwnProperty("buying_tools") && ( + + )} +
+ ))} +
+
+
+ ); +} + diff --git a/src/app/[locale]/page.jsx b/src/app/[locale]/page.jsx index 7c6cc3b..0f6f3df 100644 --- a/src/app/[locale]/page.jsx +++ b/src/app/[locale]/page.jsx @@ -1,7 +1,13 @@ "use client"; import { useDataContext } from "@/context/data.context"; import { ContentstackClient } from "@/lib/contentstack-client"; +import HeroBanner from "@/components/HeroBanner"; +import UnlockAdventureSection from "@/components/UnlockAdventureSection"; import { useState, useEffect, use } from "react"; +import TextAndImage from "@/components/TextAndImage"; +import CardsCollection from "@/components/CardsCollection"; +import Navbar from "@/components/Navbar"; +import BuyingTools from "@/components/BuyingTools"; export default function Home({ params }) { const { locale } = use(params); @@ -9,23 +15,74 @@ export default function Home({ params }) { const [entry, setEntry] = useState(null); - useEffect(() => { - const fetchData = async () => { - // example of how to fetch data from contentstack, replace "homepage" with the content type you want to fetch - const data = await ContentstackClient.getElementByType("homepage", locale, initialData); - if(data) { - setEntry(data[0]); - } else { - setEntry(null); - } - } + const getContent = async () => { + const data = await ContentstackClient.getElementByTypeWithRefs( + "homepage", + locale, + [ + 'hero_carousel', + 'hero_carousel.cta.internal_link', + 'modular_blocks.unlock_adventure_section.reference', + 'modular_blocks.unlock_adventure_section.reference.vehicles.internal_url', + 'modular_blocks.buying_tools.reference', + 'modular_blocks.buying_tools.reference.icon_list.internal_url', + ], + // initialData + ); + // console.log(data); + + setEntry(data[0]); + }; - ContentstackClient.onEntryChange(fetchData); - }, [locale, initialData]); + useEffect(() => { + ContentstackClient.onEntryChange(() => { + getContent(); + }); + }, []); return (
-

{entry?.title}

+
+ + +
+ {entry?.modular_blocks.map((block, index) => ( +
+ {block.hasOwnProperty("unlock_adventure_section") && ( + + )} + {block.hasOwnProperty("text_and_image") && ( + + )} + {block.hasOwnProperty("card_collection") && ( + + )} + {block.hasOwnProperty("buying_tools") && ( + + )} +
+ ))} +
+
); } diff --git a/src/app/api/contentstack/getElementByReference/route.js b/src/app/api/contentstack/getElementByReference/route.js new file mode 100644 index 0000000..bb105a0 --- /dev/null +++ b/src/app/api/contentstack/getElementByReference/route.js @@ -0,0 +1,13 @@ +import ContentstackServer from "@/lib/cstack"; + +export async function POST(request) { + try { + const variantParam = request.headers.get('x-personalize-variants'); + const { type, locale, referenceUids, live_preview } = await request.json(); + const res = await ContentstackServer.getElementByReference(type, locale, referenceUids, live_preview, variantParam); + return Response.json(res || {}); + } catch (error) { + console.error(error); + return Response.json({ error: "Failed to fetch data" }, { status: 500 }); + } +} diff --git a/src/app/globals.css b/src/app/globals.css index a461c50..7399cb1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1 +1,139 @@ -@import "tailwindcss"; \ No newline at end of file +@import "tailwindcss"; + +@font-face { + font-family: Montserrat; + src: url(/fonts/68f02d7f0c95808a4553b17278e213cd.woff) format("woff2"), url(/fonts/68f02d7f0c95808a4553b17278e213cd.woff) format("woff"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: riftdemi; + src: url(/fonts/f4a729dac7ca5c2ee60fcccc3547954b.woff2) format("woff2"), url(/fonts/7893d414982a0e78c26139ee5b97b2de.woff) format("woff"); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: aktiv_grotesk; + src: url(/fonts/250e8e4b496e4dc5220091e3deb9695d.woff) format("woff2"), url(/fonts/acf09532871d2823b12362210faf5b46.woff) format("woff"); + font-weight: 400; + font-style: normal; +} +/* @font-face { + font-family: rift; + src: url(/fonts/rift/l.woff2) format("woff2"), url(fonts/rift/d.woff) format("woff"), url(fonts/rift/a) format("opentype"); + font-display: auto; + font-style: normal; + font-weight: 600; + font-stretch: normal; +} */ +@font-face { + font-family: riftbold; + src: url(/fonts/524071a6787baf1dcac8a09b88a39672.woff2) format("woff2"), url(/fonts/cf8df83caa33a220e6abb7190465ebc3.woff) format("woff"); + font-weight: 400; + font-style: normal; +} + +@theme { + --color-isuzu-red: #e30613; + --font-montserrat: Montserrat; + --font-riftdemi: riftdemi; + --font-aktiv_grotesk: aktiv_grotesk; + --font-rift: rift; + --font-riftbold: riftbold; +} + +@keyframes hero-banner-content-in { + from { + opacity: 0; + transform: translate3d(0, 10%, 0); + } + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + +.hero-banner-content { + animation: hero-banner-content-in 0.50s ease-out forwards; +} + +@keyframes slideinvehicle { + 0% { + opacity: 0; + margin-left: -20px; + } + 100% { + opacity: 1; + margin-left: 0; + } +} + +.sub-nav-mega-item__image { + animation: slideinvehicle 0.5s ease-out; +} + +@keyframes slidetexticoncontainer{ + 0% { + opacity: 0; + margin-top: 60px; + } + 100% { + opacity: 1; + margin-top: 0; + } +} + +.sub-nav-mega-item__text-icon-container { + animation: slidetexticoncontainer 0.3s ease-out; +} + +/* @keyframes cards-collection-slide-next { + from { + transform: translate3d(80px, 0, 0); + } + to { + transform: translate3d(0, 0, 0); + } +} + +@keyframes cards-collection-slide-prev { + from { + transform: translate3d(-80px, 0, 0); + } + to { + transform: translate3d(0, 0, 0); + } +} + +.cards-collection-slide-next { + animation: cards-collection-slide-next 500ms ease-in-out forwards; +} + +.cards-collection-slide-prev { + animation: cards-collection-slide-prev 500ms ease-in-out forwards; +} */ + +@media (prefers-reduced-motion: reduce) { + .hero-banner-content { + animation: none; + opacity: 1; + transform: translate3d(0, 0, 0); + } + + .sub-nav-mega-item__image { + animation: none; + opacity: 1; + margin-left: 0; + } + + .sub-nav-mega-item__text-icon-container { + animation: none; + opacity: 1; + margin-top: 0; + } + + /* .cards-collection-slide-next, + .cards-collection-slide-prev { + animation: none; + } */ +} \ No newline at end of file diff --git a/src/components/BuyingTools.jsx b/src/components/BuyingTools.jsx new file mode 100644 index 0000000..85ed61c --- /dev/null +++ b/src/components/BuyingTools.jsx @@ -0,0 +1,141 @@ +"use client"; + +function getToolHref(item) { + if (!item) return ""; + const url = item?.internal_url?.[0]?.url; + return url || null; +} + +function isFullWidthFlag(value) { + if (value === true) return true; + if (typeof value === "string") { + const v = value.trim().toLowerCase(); + return v === "true" || v === "yes" || v === "1"; + } + return false; +} + +function BuyingToolItem({ item, index, count, barLayout, content, groupUid, fullWidth }) { + const href = getToolHref(item); + const iconUrl = item?.icon?.url || ""; + const IconWrapper = href ? "a" : "div"; + const label = item?.text || ""; + + + const borders = barLayout + ? [ + index % 2 === 0 ? "max-md:border-r max-md:border-neutral-300/80" : "", + index < 2 ? "max-md:border-b max-md:border-neutral-300/80 md:border-b-0" : "", + index < count - 1 ? "md:border-r md:border-neutral-300/80" : "", + ] + .filter(Boolean) + .join(" ") + : ""; + + return ( + +
+ {iconUrl ? ( + + ) : ( + + Icon + + )} +
+ {label ? ( +
+ + {label} + + +
+ ) : null} +
+ ); +} + +export default function BuyingTools({ content }) { + const items = Array.isArray(content?.icon_list) + ? content.icon_list + : Array.isArray(content?.icons) + ? content.icons + : []; + const list = items.filter(Boolean); + const groupUid = Array.isArray(content?.icon_list) + ? "icon_list" + : "icons"; + const fullWidth = isFullWidthFlag( + content?.full_width ?? content?.fullWidth, + ); + const bgColor = content?.backgound_color?.hex + + if (list.length === 0) return null; + + return ( +
+
+ {fullWidth && content?.title ? ( +
+

+ {content.title} +

+
+ ) : null} + +
+ {list.map((item, index) => ( + + ))} +
+
+
+ ); +} diff --git a/src/components/CardsCollection.jsx b/src/components/CardsCollection.jsx new file mode 100644 index 0000000..1e4e67d --- /dev/null +++ b/src/components/CardsCollection.jsx @@ -0,0 +1,251 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; + +function getStartIndexes(totalCards, visibleCount, step) { + if (totalCards <= visibleCount) return [0]; + + const indexes = []; + const maxStart = totalCards - visibleCount; + + for (let index = 0; index <= maxStart; index += step) { + indexes.push(index); + } + + if (indexes[indexes.length - 1] !== maxStart) { + indexes.push(maxStart); + } + + return indexes; +} + +function getCtaData(cta) { + const item = Array.isArray(cta) ? cta[0] : cta; + if (!item) return { label: "", href: "#" }; + + return { + label: item.link_text, + href: item.internal_link?.[0]?.url || item.external_link || "#", + }; +} + +function CarouselArrow({ direction, onClick }) { + const isPrev = direction === "prev"; + + return ( + + ); +} + +function CollectionCta({ cta, editableAttrs, compact = false }) { + const { label, href } = getCtaData(cta); + + if (!label) return null; + + return ( + + + {label} + + + + + + + ); +} + +function CollectionCard({ card, gridType }) { + const imageUrl = card?.image?.url || ""; + const ctaAttrs = card?.link?.$?.link_text || {}; + const isTwoByTwo = gridType === "2x2"; + return ( +
+
+ {imageUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : ( +
+ Image +
+ )} +
+ +
+
+ {card?.heading ? ( +

+ {card.heading} +

+ ) : null} + + {card?.description ? ( +
+ ) : null} +
+ + +
+
+ ); +} + +export default function CardsCollection({ content }) { + const gridValue = String(content?.grid || "4x1").trim().toLowerCase(); + const gridType = gridValue === "2x2" ? "2x2" : "4x1"; + const cards = Array.isArray(content?.cards) ? content.cards.filter(Boolean) : []; + const cardsPerPage = 4; + const stepSize = 2; + const startIndexes = useMemo( + () => getStartIndexes(cards.length, cardsPerPage, stepSize), + [cards.length], + ); + const [stepIndex, setStepIndex] = useState(0); + // const [slideDirection, setSlideDirection] = useState("next"); + + useEffect(() => { + setStepIndex((current) => Math.min(current, startIndexes.length - 1)); + }, [startIndexes]); + + const visibleCards = useMemo(() => { + const start = startIndexes[stepIndex] ?? 0; + return cards.slice(start, start + cardsPerPage); + }, [cards, startIndexes, stepIndex]); + + const showArrows = cards.length > cardsPerPage; + + const gridClass = + gridType === "2x2" + ? "grid-cols-1 gap-6 sm:grid-cols-2" + : "grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-4 px-[40px]"; + const sectionMaxWidth = + gridType === "2x2" ? "max-w-[1630px]" : "max-w-[1550px]"; + + return ( +
+ {content?.title ? ( +
+

+ {content.title} +

+
+ ) : null} + +
+ {showArrows ? ( + <> + { + // setSlideDirection("prev"); + setStepIndex((current) => + current === 0 ? startIndexes.length - 1 : current - 1, + ); + }} + /> + { + // setSlideDirection("next"); + setStepIndex((current) => + current === startIndexes.length - 1 ? 0 : current + 1, + ); + }} + /> + + ) : null} + +
+ {visibleCards.map((card, index) => ( + + ))} +
+
+
+ ); +} diff --git a/src/components/HeroBanner.jsx b/src/components/HeroBanner.jsx new file mode 100644 index 0000000..76ca656 --- /dev/null +++ b/src/components/HeroBanner.jsx @@ -0,0 +1,182 @@ +"use client"; + +import { useState, useEffect } from "react"; + +export default function HeroBanner({ content = [] }) { + const [currentIndex, setCurrentIndex] = useState(0); + const totalSlides = content?.length; + const currentSlide = content?.[currentIndex] || {}; + const hasHeading = Boolean(currentSlide?.heading); + const hasSubheading = Boolean(currentSlide?.sub_heading); + const isButtonOnly = !hasHeading && !hasSubheading && currentSlide?.cta?.link_text; + + // Auto slide (optional - can remove if not needed) + useEffect(() => { + if (totalSlides <= 1) return; + + const interval = setInterval(() => { + setCurrentIndex((prev) => (prev + 1) % totalSlides); + }, 8000); + + return () => clearInterval(interval); + }, [totalSlides]); + + const getTextAlignment = () => { + switch (currentSlide?.text_position) { + case "Left": + return "items-start text-left"; + case "Center": + return "items-center text-center"; + case "Right": + default: + return "items-end text-right"; + } + }; + + const goToPrev = () => { + if (totalSlides <= 1) return; + setCurrentIndex((prev) => (prev - 1 + totalSlides) % totalSlides); + }; + + const goToNext = () => { + if (totalSlides <= 1) return; + setCurrentIndex((prev) => (prev + 1) % totalSlides); + }; + + return ( +
+ {/* Background media: video first, image fallback */} + {currentSlide?.video?.url ? ( + + ) : currentSlide?.background_image?.url ? ( + hero + ) : null} + + {/* Overlay (for readability like your image) */} + {/*
*/} + + {/* Content */} +
+ {/* Heading */} + {currentSlide?.heading && ( +

+ {currentSlide?.heading} +

+ )} + + {/* Subheading */} + {currentSlide?.sub_heading && ( +

+ {currentSlide.sub_heading} +

+ )} + + {/* CTA */} + {currentSlide?.cta?.link_text && ( + + {currentSlide.cta.link_text} + + + + + )} +
+ + {/* Carousel arrows (only if more than 1 slide) */} + {totalSlides > 1 && ( + <> + + + + )} + + {/* Dots Navigation (ONLY if more than 1 slide) */} + {totalSlides > 1 && ( +
+ {content.map((_, index) => ( +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/components/Navbar/SecondaryNavBar.jsx b/src/components/Navbar/SecondaryNavBar.jsx new file mode 100644 index 0000000..e63b267 --- /dev/null +++ b/src/components/Navbar/SecondaryNavBar.jsx @@ -0,0 +1,263 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useId, useMemo, useState } from "react"; + +// Renders either next/link or a plain so menu URLs work for internal paths and absolute external URLs. +function NavAnchor({ link, className, children, ...rest }) { + const href = link; + const external = /^https?:\/\//i.test(String(href).trim()); + + if (external) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +} + +// Collects all column links from every menu_item so mega + secondary bar can render one combined list. +export function flattenSubNavMenuLinks(sub) { + const items = sub?.menu_items || sub?.menu_item || []; + return items.flatMap((mi) => mi?.links || mi?.link || []); +} + +// Returns the URL path stored on a sub-nav link’s internal_link entry for route matching and active states. +function getSubNavLinkInternalPath(link) { + const url = link?.internal_link?.[0]?.url; + if (url == null ) return null; + const t = url.trim(); + const clean = t.startsWith("/") ? t : `/${t}`; + return clean; +} + +// Compares slugParam to a sub-nav link’s internal path so the secondary bar can track the current page. +function slugParamMatchesSubNavLink(slugParam, link) { + const internalPath = getSubNavLinkInternalPath(link); + if (internalPath == null || slugParam == null) return false; + const slugStr = Array.isArray(slugParam) + ? slugParam.filter(Boolean).join("/") + : String(slugParam).trim(); + if (!slugStr) return false; + const pathNorm = internalPath.replace(/^\/+|\/+$/g, ""); + const slugNorm = slugStr.replace(/^\/+|\/+$/g, ""); + return slugNorm === pathNorm; +} + +// True when the active route matches this sub-nav link (used for secondary bar visibility and link highlighting). +export function subNavLinkMatchesCurrentRoute(link, slugParam) { + return ( + slugParamMatchesSubNavLink(slugParam, link) + ); +} + +function getSecondaryNavLogoUrl(sub) { + if (!sub) return null; + const img = sub?.secondary_nav_logo; + if (img?.url) return img.url; + if (Array.isArray(img) && img[0]?.url) return img[0].url; + return null; +} + +function getSecondaryNavCta(sub) { + if (!sub) return { text: null, link: null }; + return { + text: sub?.secondary_nav_cta?.text, + link: { + internal_link: sub?.secondary_nav_cta?.internal_link + }, + }; +} + +function secondaryNavLinkClass(active) { + return `font-riftdemi pb-0.5 text-[15px] uppercase tracking-wide transition-colors ${ + active + ? "border-b-1 border-isuzu-red text-white/40" + : "text-white/85 hover:text-white hover:border-b-1 hover:border-isuzu-red" + }`; +} + +export default function SecondaryNavBar({ sub, slugParam, pathname }) { + const [mobileExpanded, setMobileExpanded] = useState(false); + const mobilePanelId = useId(); + const flatLinks = useMemo(() => flattenSubNavMenuLinks(sub), [sub]); + const logoUrl = getSecondaryNavLogoUrl(sub); + const cta = getSecondaryNavCta(sub); + const logoLive = + sub?.$?.secondary_nav_logo || + sub?.$?.secondary_navigation_logo || + sub?.$?.secondary_nav_logo_image || + {}; + + const activeLink = useMemo( + () => flatLinks.find((link) => subNavLinkMatchesCurrentRoute(link, slugParam)), + [flatLinks, slugParam], + ); + + const collapsedLabel = + activeLink?.link_text || + activeLink?.title || + activeLink?.text || + flatLinks[0]?.link_text || + ""; + + useEffect(() => { + setMobileExpanded(false); + }, [pathname, slugParam]); + + return ( +
+ {/* Below lg: collapsed row (active label + chevron only); tap expands links + CTA. */} +
+ + {mobileExpanded ? ( +
+
    + {flatLinks.map((link, idx) => { + const href = + link?.internal_link?.[0]?.url || link?.external_link || "#"; + const active = subNavLinkMatchesCurrentRoute(link, slugParam); + return ( +
  • + setMobileExpanded(false)} + > + {link?.link_text} + +
  • + ); + })} +
+ {cta.text ? ( +
+ setMobileExpanded(false)} + > + {cta.text} + + + + +
+ ) : null} +
+ ) : null} +
+ + {/* lg+: logo, full link row, CTA */} +
+ + {logoUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : null} + +
    + {flatLinks.map((link, idx) => { + const href = + link?.internal_link?.[0]?.url || link?.external_link || "#"; + const active = subNavLinkMatchesCurrentRoute(link, slugParam); + return ( +
  • + + {link?.link_text} + +
  • + ); + })} +
+
+ {cta.text ? ( + + {cta.text} + + + + + ) : null} +
+
+
+ ); +} diff --git a/src/components/Navbar/index.jsx b/src/components/Navbar/index.jsx new file mode 100644 index 0000000..9e1f396 --- /dev/null +++ b/src/components/Navbar/index.jsx @@ -0,0 +1,671 @@ +"use client"; + +import Link from "next/link"; +import { useParams, usePathname } from "next/navigation"; +import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; +import { ContentstackClient } from "@/lib/contentstack-client"; +import SecondaryNavBar, { flattenSubNavMenuLinks, subNavLinkMatchesCurrentRoute} from "./SecondaryNavBar"; + +function getSubNavEntry(item) { + const ref = item?.sub_nav; + if (!ref) return null; + return Array.isArray(ref) ? ref[0] : ref; +} + +function showSecondaryNavBar(sub) { + if (!sub) return false; + const v = sub?.show_secondary_navigation_bar; + if (v === true || v === "true") return true; + return Boolean(v); +} + +// Decides if a top-level item should show a chevron and mega/accordion (any columns, images, or tiles in sub_nav). +function hasSubNavigation(item) { + const sub = getSubNavEntry(item); + if (!sub) return false; + const items = sub.menu_items || sub.menu_item || []; + const hasMenuItems = + Array.isArray(items) && + items.length > 0 && + Boolean( + items[0] && + (items[0].title || + items[0].links?.length || + items[0].image?.url || + items[0].tiles?.length > 0), + ); + const anyItemImage = Array.isArray(items) && items.some((mi) => mi?.image?.url); + const anyItemTiles = + Array.isArray(items) && + items.some((mi) => mi?.tiles?.length > 0); + return hasMenuItems || anyItemImage || anyItemTiles; +} + +// Finds which sub_nav (if any) should drive the secondary bar when the current slug matches one of its links. +function findSecondaryNavContext(menu, slugParam) { + if (!Array.isArray(menu)) return null; + for (let i = 0; i < menu.length; i++) { + const item = menu[i]; + if (!hasSubNavigation(item)) continue; + const sub = getSubNavEntry(item); + if (!sub || !showSecondaryNavBar(sub)) continue; + const links = flattenSubNavMenuLinks(sub); + for (const link of links) { + if (subNavLinkMatchesCurrentRoute(link, slugParam)) { + return { sub }; + } + } + } + return null; +} + +// Renders either next/link or a plain so menu URLs work for internal paths and absolute external URLs. +function NavAnchor({ link, className, children, ...rest }) { + const href = link; + const external = /^https?:\/\//i.test(String(href).trim()); + + if (external) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); +} + +function Chevron({ open, active }) { + return ( + + + + ); +} + +function ChevronRightMobile({ expanded }) { + return ( + + + + ); +} + +function subNavMegaFlexClass(count, plainMobile) { + if (plainMobile) { + return "flex flex-col gap-8"; + } + if (count <= 1) { + return "flex flex-col items-center gap-10 md:gap-12 lg:gap-16"; + } + if (count === 2 || count === 3) { + return "flex flex-col gap-10 md:flex-row md:items-start md:gap-12 lg:gap-16"; + } + if (count === 4 || count === 5) { + return "flex flex-col gap-10 sm:flex-row sm:flex-wrap sm:items-start sm:gap-x-10 sm:gap-y-10 lg:flex-nowrap lg:gap-12"; + } + return "flex flex-col gap-10 sm:flex-row sm:flex-wrap sm:items-start sm:gap-x-10 sm:gap-y-10 xl:flex-nowrap xl:gap-12 xl:gap-16"; +} + +function subNavMegaItemFlexClass(count, plainMobile) { + if (plainMobile) return "w-full min-w-0"; + if (count <= 1) return "w-full max-w-3xl"; + if (count === 2 || count === 3) { + return "w-full min-w-0 md:flex-1 md:basis-0"; + } + if (count === 4 || count === 5) { + return "w-full min-w-0 sm:flex-1 sm:basis-1/2 lg:basis-0 lg:flex-1"; + } + return "w-full min-w-0 sm:flex-1 sm:basis-1/2 md:basis-1/3 xl:basis-0 xl:flex-1"; +} + +function SubNavMegaItem({ item, locale, count, plainMobile, onMegaLinkClick }) { + const iconUrl = plainMobile ? null : item?.icon?.url; + const title = item?.title; + const links = item?.links || item?.link || []; + const imageUrl = plainMobile ? null : item?.image?.url; + const tiles = item?.tiles || item?.tile || []; + const centerTextOnlyColumn = !imageUrl; + + const titleClass = plainMobile + ? "font-riftdemi text-sm uppercase tracking-wide text-neutral-900" + : count === 1 + ? "font-riftdemi font-bold text-[18px] md:text-5xl uppercase tracking-wide text-neutral-900" + : "font-riftdemi font-bold text-[18px] md:text-5xl uppercase tracking-wide text-neutral-900"; + + const linkClass = plainMobile + ? "font-riftdemi text-[16px] uppercase tracking-wide text-neutral-900 transition-colors group-hover:text-isuzu-red" + : "font-riftdemi text-[18px] uppercase tracking-wide text-neutral-900 transition-colors group-hover:text-isuzu-red"; + + const textColumn = ( +
+ {iconUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : null} +
+ {title ?

{title}

: null} +
    + {links.map((link, idx) => ( +
  • + onMegaLinkClick?.()} + > + + {link?.link_text} + + + +
  • + ))} +
+
+
+ ); + + const imageBlock = imageUrl ? ( +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + +
+
+ ) : null; + + return ( +
+ {imageUrl ? ( +
+ {textColumn} + {imageBlock} +
+ ) : ( + textColumn + )} + {tiles.length > 0 ? ( +
+ {tiles.map((tile, idx) => { + const tIcon = tile?.icon?.url; + const label = tile?.text; + const href = tile?.internal_url?.[0]?.url || '#'; + return ( + + {tIcon ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : ( + + )} + + {label} + + + + ); + })} +
+ ) : null} +
+ ); +} + +function SubNavMega({sub, locale, panelId, plainMobile = false, onSubNavLinkActivate}) { + const menuItems = sub?.menu_items || sub?.menu_item || []; + const count = menuItems.length; + const flexClass = subNavMegaFlexClass(count, plainMobile); + const megaLinkClick = + typeof onSubNavLinkActivate === "function" + ? () => onSubNavLinkActivate() + : undefined; + + return ( +
+
+
+ {menuItems.map((item, idx) => ( + + ))} +
+
+
+ ); +} + +export default function Navbar({ navigation }) { + const params = useParams(); + const pathname = usePathname(); + const locale = params?.locale || "en"; + const slugParam = params?.slug; + const baseId = useId(); + const headerRef = useRef(null); + const [openIndex, setOpenIndex] = useState(null); + const [mobileNavOpen, setMobileNavOpen] = useState(false); + const [mobileSubIndex, setMobileSubIndex] = useState(null); + + // Runs when a user follows a link inside the mega / mobile sub-nav so overlays close after navigation. + const handleSubNavLinkActivate = useCallback(() => { + setOpenIndex(null); + setMobileNavOpen(false); + setMobileSubIndex(null); + }, []); + + // Collapses primary mega, mobile drawer, and accordions (route change, Escape, click outside header). + const closeMenus = useCallback(() => { + setOpenIndex(null); + setMobileNavOpen(false); + setMobileSubIndex(null); + }, []); + + const [entry, setEntry] = useState(null); + + useEffect(() => { + const getContent = async () => { + const data = await ContentstackClient.getElementByTypeWithRefs( + "navigation", + locale, + [ 'menu.sub_nav', 'menu.internal_link', 'menu.sub_nav.menu_items.links.internal_link', 'menu.sub_nav.secondary_nav_cta.internal_link', 'menu.sub_nav.menu_items.tiles.internal_url', 'cta.internal_link'], + ); + + setEntry(data[0]); + }; + getContent(); + ContentstackClient.onEntryChange(() => {getContent()}); + }, [locale]); + + useEffect(() => { + closeMenus(); + }, [pathname, closeMenus]); + + useEffect(() => { + function onKey(e) { + if (e.key === "Escape") closeMenus(); + } + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [closeMenus]); + + useEffect(() => { + function onPointerDown(e) { + if (!headerRef.current?.contains(e.target)) closeMenus(); + } + document.addEventListener("mousedown", onPointerDown); + document.addEventListener("touchstart", onPointerDown); + return () => { + document.removeEventListener("mousedown", onPointerDown); + document.removeEventListener("touchstart", onPointerDown); + }; + }, [closeMenus]); + + useEffect(() => { + if (!mobileNavOpen) return; + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = prev; + }; + }, [mobileNavOpen]); + + useEffect(() => { + if (mobileNavOpen) setOpenIndex(null); + }, [mobileNavOpen]); + + + const menu = useMemo(() => (Array.isArray(entry?.menu) ? entry?.menu : [entry?.menu]), [entry?.menu]); + const secondaryNavContext = useMemo( + () => findSecondaryNavContext(menu, slugParam), + [menu, slugParam], + ); + const logoUrl = entry?.logo?.url; + const cta = entry?.cta; + const ctaItem = Array.isArray(cta) ? cta[0] : cta; + const ctaLabel = + ctaItem?.link_text || "Find a dealer"; + + const ctaLink = ctaItem?.internal_link?.[0]?.url || ctaItem?.external_link || '#' + + if (!entry) return null; + + const desktopSubNavOpen = + openIndex !== null && + Boolean(menu[openIndex] && hasSubNavigation(menu[openIndex])); + const mobileSubNavOpen = mobileSubIndex !== null; + + const showSecondaryNavUi = + secondaryNavContext && + showSecondaryNavBar(secondaryNavContext.sub) && + flattenSubNavMenuLinks(secondaryNavContext.sub).length > 0 && + !desktopSubNavOpen && + !mobileSubNavOpen; + + return ( +
+ + + {showSecondaryNavUi ? ( + + ) : null} + + {openIndex !== null && menu[openIndex] && hasSubNavigation(menu[openIndex]) ? ( +
+ +
+ ) : null} + + {mobileNavOpen ? ( +
+
    + {menu.map((item, index) => { + const expandable = hasSubNavigation(item); + const title = item?.title; + const topHref = item?.internal_link[0]?.url || item?.external_link || '#'; + const subOpen = mobileSubIndex === index; + const mobilePanelId = `${baseId}-mobile-sub-${index}`; + + return ( +
  • + {expandable ? ( + <> + + {subOpen ? ( +
    + +
    + ) : null} + + ) : ( + closeMenus()} + > + {title} + + )} +
  • + ); + })} +
+
+ ) : null} +
+ ); +} \ No newline at end of file diff --git a/src/components/PreviewVehicle/index.jsx b/src/components/PreviewVehicle/index.jsx new file mode 100644 index 0000000..9fc5f2d --- /dev/null +++ b/src/components/PreviewVehicle/index.jsx @@ -0,0 +1,239 @@ +"use client"; +import { ChevronRightIcon, ChevronDownIcon, CheckIcon } from "@heroicons/react/24/outline"; +import { useState, useEffect } from "react"; +import { useParams } from "next/navigation"; +import { ContentstackClient } from "@/lib/contentstack-client"; +import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from "@headlessui/react"; + +export default function PreviewVehicle({ content }) { + const models = Array.isArray(content?.vehicle_models) ? content.vehicle_models : []; + const modelUids = models.map(model => model.uid); + const [activeIndex, setActiveIndex] = useState(0); + const { locale } = useParams(); + const [vehicleVariations, setVehicleVariations] = useState({}); + const [selectedVariation, setSelectedVariation] = useState(null); + console.log("🚀 ~ PreviewVehicle ~ selectedVariation:", selectedVariation) + const [selectedColour, setSelectedColour] = useState(null); + const activeModel = models[activeIndex]; + + + const fetchMuxProductData = async () => { + const data = await ContentstackClient.getElementByReference('vehicle_variation', locale, [...modelUids]) + const groupedData = data.reduce((acc, item) => { + // vehicle_model is a Contentstack reference array + const uid = item.vehicle_model?.[0]?.uid ?? item.vehicle_model?.uid; + if (!uid) return acc; + if (!acc[uid]) acc[uid] = []; + acc[uid].push(item); + return acc; + }, {}); + setVehicleVariations(groupedData); + // pre-select first variation of the initially active model + const firstModelUid = models[activeIndex]?.uid; + const firstVariation = groupedData[firstModelUid]?.[0] ?? null; + setSelectedVariation(firstVariation); + setSelectedColour(firstVariation?.available_colours?.[0] ?? null); + } + + useEffect(() => { + if (window.location.pathname.includes('/mu-x')) { + fetchMuxProductData(); + } + }, []) + + return ( +
+
+ + {/* Title */} +

+ {content?.title} +

+ + {/* Model tabs */} +
+
+ {models.map((model, index) => ( + + ))} +
+
+ + {/* Active model panel */} + {activeModel && ( +
+ + {vehicleVariations?.[activeModel.uid]?.length > 1 ? ( + { + setSelectedVariation(v); + setSelectedColour(v?.available_colours?.[0] ?? null); + }} + /> + ):
+ + {selectedVariation?.heading} + +
} + + {/* Vehicle image — swaps when a colour swatch is clicked */} + {(selectedColour?.vehicle_image?.url || activeModel.image?.url) && ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {selectedColour?.colour?.[0]?.title +
+ )} + + {/* Colour swatches — always visible when colours exist */} + {selectedVariation?.available_colours?.length > 0 && ( +
+
+ {selectedVariation.available_colours.map((entry) => { + const colour = entry.colour?.[0]; + if (!colour) return null; + const isActive = entry._metadata?.uid === selectedColour?._metadata?.uid; + return ( +
+ {/* Colour name label below swatches */} + {selectedColour?.colour?.[0]?.title && ( +

+ {selectedColour.colour[0].title} +

+ )} +
+ )} + + {/* CTA */} + {content?.link?.link_text && ( + + )} +
+ )} +
+
+ ); +} + +function groupByDriveType(variations) { + const groups = []; + const seen = new Map(); + for (const v of variations) { + const driveType = v.drive_type ?? extractDriveType(v.title); + if (!seen.has(driveType)) { + seen.set(driveType, []); + groups.push({ label: driveType, items: seen.get(driveType) }); + } + seen.get(driveType).push(v); + } + return groups; +} + +function extractDriveType(title = "") { + const match = title.match(/^(4x[24])/i); + return match ? match[1].toUpperCase() : "Other"; +} + +function VariationDropdown({ variations, selected, onChange }) { + const groups = groupByDriveType(variations); + + return ( +
+ + + {selected?.title ?? "Select variant"} + + + + + + + {groups.map((group) => ( +
+ {/* Non-interactive drive type group header */} +
+ {group.label} +
+ {group.items.map((v) => ( + + + {v.heading || v.title} + + ))} +
+ ))} +
+
+
+ ); +} diff --git a/src/components/TextAndImage.jsx b/src/components/TextAndImage.jsx new file mode 100644 index 0000000..4908744 --- /dev/null +++ b/src/components/TextAndImage.jsx @@ -0,0 +1,190 @@ +"use client"; + +function getCtaData(cta) { + const item = Array.isArray(cta) ? cta[0] : cta; + if (!item) return { label: "", href: "" }; + + return { + label: item.link_text || "", + href: item.internal_link?.[0]?.url || item.external_link|| "#", + }; +} + +function CtaLink({ cta, editableAttrs, textColor }) { + const { label, href } = getCtaData(cta); + + if (!label) return null; + + return ( + + + {label} + + + + + + + ); +} + +function TextPanel({ content, backgroundColor, textColor, className = "", splitLayout = true }) { + return ( +
+
+ {content?.heading ? ( +

+ {content.heading} +

+ ) : null} + + {content?.content ? ( +
+ ) : null} + + +
+
+ ); +} + +function SplitLayout({ content, textSide, backgroundColor, textColor, imageUrl }) { + const textFirst = textSide === "left"; + const splitlayout = true ; + + return ( +
+
+ {textFirst ? ( + + ) : ( + + )} + + {textFirst ? ( + + ) : ( + + )} +
+
+ ); +} + +function ImagePanel({ imageUrl, editableAttrs }) { + return imageUrl ? ( + + ) : ( +
+ Image +
+ ); +} + +function OverlayLayout({ content, backgroundColor, textColor, imageUrl }) { + const splitlayout = false ; + return ( +
+
+ {imageUrl ? ( + + ) : ( +
+ )} + +
+ +
+
+
+ ); +} + +export default function TextAndImage({ content }) { + const layoutValue = String(content?.layout || "split").trim().toLowerCase(); + const layout = + layoutValue === "text-overlay" || layoutValue === "overlay" + ? "overlay" + : "split"; + const textAlignValue = String(content?.text_align || "left") + .trim() + .toLowerCase(); + const textSide = textAlignValue === "right" ? "right" : "left"; + const backgroundColor = content?.background_color?.hex; + const textColor = content?.text_color?.hex; + const imageUrl = content?.image?.url || ""; + + if (layout === "overlay") { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/components/UnlockAdventureSection.jsx b/src/components/UnlockAdventureSection.jsx new file mode 100644 index 0000000..371ae2b --- /dev/null +++ b/src/components/UnlockAdventureSection.jsx @@ -0,0 +1,115 @@ +"use client"; +import { ChevronRightIcon } from "@heroicons/react/24/outline"; + +export default function UnlockAdventureSection({ content }) { + const vehicles = Array.isArray(content?.vehicles) ? content.vehicles : []; + return ( +
+ +
+
+

+ {content?.title} +

+

+ {content?.description} +

+
+ +
+ {vehicles.map((vehicle, index) => ( + + ))} +
+
+
+ ); +} + +function VehicleColumn({ vehicle, content, vehicleIndex }) { + const imageSrc = vehicle?.vehicle_image?.url; + const outlineSrc = vehicle?.hover_background?.url; + const alt = + vehicle?.vehicle_image?.title || + vehicle?.vehicle_image?.filename || + "Isuzu vehicle"; + + const blockAttrs = content?.$?.[`vehicles__${vehicleIndex}`] || {}; + + return ( +
+
+ {/* Outline watermark — rendered first so it sits behind the vehicle (z-0) */} + {outlineSrc ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : null} + +
+
+ {imageSrc ? ( + // eslint-disable-next-line @next/next/no-img-element + {alt} + ) : ( +
+ Vehicle image +
+ )} + +
+
+
+ + + +
+ ); +} diff --git a/src/lib/contentstack-client.js b/src/lib/contentstack-client.js index 8632e4d..4536587 100644 --- a/src/lib/contentstack-client.js +++ b/src/lib/contentstack-client.js @@ -347,6 +347,27 @@ export const ContentstackClient = { } } return data; - } + }, + + getElementByReference: async function (type, locale, referenceUids) { + const searchQueryParams = getSearchQueryParams(); + if (inLivePreview() && !(searchQueryParams.live_preview || searchQueryParams.hash)) { + while (!Stack.live_preview?.hash) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + let data = null; + const res = await fetch(`/api/contentstack/getElementByReference`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type, locale, referenceUids, live_preview: (searchQueryParams.live_preview || searchQueryParams.hash) ? searchQueryParams : (Stack.live_preview.hash) ? Stack.live_preview : null }) + }); + if(res.ok) { + data = await res.json(); + } else { + data = null; + } + return data; + }, } \ No newline at end of file diff --git a/src/lib/cstack.js b/src/lib/cstack.js index cb171df..d9f25bc 100644 --- a/src/lib/cstack.js +++ b/src/lib/cstack.js @@ -1,4 +1,5 @@ import contentstack from '@contentstack/delivery-sdk'; +import { QueryOperation } from '@contentstack/delivery-sdk'; function deserializeVariantIds (variantsQueryParam) { if(!variantsQueryParam) return ''; @@ -281,6 +282,33 @@ const ContentstackServer = { }); }, + getElementByReference: async function (type, locale, referenceUids, live_preview, variantParam) { + const query = stack.contentType('vehicle_model').entry().query().where('uid', QueryOperation.INCLUDES, referenceUids); + + stack.livePreviewQuery(live_preview ?? {}); + + return new Promise((resolve, reject) => { + stack.contentType(type) + .entry() + .locale(locale ? locale : "en") + .includeReference('available_colours.colour') + .query().referenceIn('vehicle_model', query) + .find() + .then( + function success(data) { + resolve(data.entries); + }, + function empty() { + resolve(null); + }, + function error(err) { + console.error("error", err); + reject(err); + } + ); + }); + }, + getStack() { return stack; },