diff --git a/.env.sample b/.env.sample
deleted file mode 100644
index 7eff2b7..0000000
--- a/.env.sample
+++ /dev/null
@@ -1,50 +0,0 @@
-# Get from stack > settings > Stack > API Credentials
-CONTENTSTACK_API_KEY=YOUR_API_KEY
-
-# Generate from stack > settings > Tokens > + Delivery Token
-CONTENTSTACK_DELIVERY_TOKEN=YOUR_DELIVERY_TOKEN
-CONTENTSTACK_PREVIEW_TOKEN=YOUR_PREVIEW_TOKEN
-
-# Get all present publishing enviorments from stack > settings > Environments
-CONTENTSTACK_ENVIRONMENT=YOUR_CONTENTSTACK_ENVIRONMENT
-
-# For Europe, set region as EU and host as eu-cdn.contentstack.com.
-# For Azure North America, set region as AZURE_NA and host as azure-na-cdn.contentstack.com.
-# For Azure Europe, set the region as AZURE_EU and host as azure-eu-cdn.contentstack.com.
-# For GCP North America, set the region as GCP_NA and host as gcp-na-cdn.contentstack.com.
-# For GCP Europe, set the region as GCP_EU and host as gcp-eu-cdn.contentstack.com.
-# For AWS Australia, set the region as AU and host as au-cdn.contentstack.com.
-CONTENTSTACK_HOST=YOUR_CONTENTSTACK_HOST #Default is aws-us which is cdn.contentstack.io
-CONTENTSTACK_REGION=YOUR_CONTENTSTACK_REGION #Default is us
-
-# US (North America, or NA): rest-preview.contentstack.com
-# Europe (EU): eu-rest-preview.contentstack.com
-# Azure North America (AZURE NA): azure-na-rest-preview.contentstack.com
-# Azure Europe (Azure EU):azure-eu-rest-preview.contentstack.com
-# GCP NA: gcp-na-rest-preview.contentstack.com
-CONTENTSTACK_PREVIEW_HOST=YOUR_CONTENSTACK_PREVIEW_HOST #Default is aws-us which is rest-preview.contentstack.com
-
-# Get all branches from stack > settings > Branches. Choose the one your are working on.
-CONTENTSTACK_BRANCH=YOUR_CONTENTSTACK_BRANCH #Default is main
-
-# Get from organization > personalize (from sidebar) > select the personalize project connected with your stack or create a new personalize project > settings > general > Project Details > uid
-CONTENTSTACK_PERSONALIZATION=YOUR_CONTENTSTACK_PERSONALIZATION
-
-# AWS US (North America, or NA): https://personalize-edge.contentstack.com
-# AWS Europe (EU): https://eu-personalize-edge.contentstack.com
-# Azure North America (Azure NA): https://azure-na-personalize-edge.contentstack.com
-# Azure Europe (Azure EU): https://azure-eu-personalize-edge.contentstack.com
-# GCP North America (GCP NA): https://gcp-na-personalize-edge.contentstack.com
-# GCP Europe (GCP EU): https://gcp-eu-personalize-edge.contentstack.com
-# AWS Australia (AU): https://au-personalize-edge.contentstack.com
-CONTENTSTACK_PERSONALIZE_EDGE_API_URL=YOUR_CONTENTSTACK_PERSONALIZE_EDGE_API_URL #Default is aws-us which is https://personalize-edge.contentstack.com
-
-# Get from organization > data and insights (from sidebar)
-LYTICS_TAG=YOUR_LYTICS_TAG
-
-# Enable or disable live preview entirely.
-LIVE_PREVIEW_ENABLED=true
-
-# If hosting the production website on launch use HOSTING=launch, this will enable you to get benefits of launch's edge functions and middleware support
-HOSTING=local
-
diff --git a/.gitignore b/.gitignore
index a8f86bc..0673806 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,10 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+# logs
+logs/
+*.log
+
+# backups
+_backup_*/
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..9ac9549
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1,4 @@
+@awesome.me:registry=https://npm.fontawesome.com/
+@fortawesome:registry=https://npm.fontawesome.com/
+//npm.fontawesome.com/:_authToken=B92EF01D-F140-4E0D-81F0-5DE5026EC013
+//npm.pkg.github.com/:_authToken=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
diff --git a/README.md b/README.md
index e215bc4..dacce8f 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,29 @@
-This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
+## Red Panda Resort
-## Getting Started
+#### Setup .env
+In your destination stack, navigate to Settings > Tokens > + Delivery Token
-First, run the development server:
+Select `main` branch and the `preview` enviornment. Make sure 'Create Preview Token' is enabled. Click 'Generate Token' and copy tokens into `.env`. This is what your final `.env` should look like:
-```bash
-npm run dev
-# or
-yarn dev
-# or
-pnpm dev
-# or
-bun dev
+```ts
+CONTENTSTACK_API_KEY='YOUR_API_KEY'
+CONTENTSTACK_DELIVERY_TOKEN='YOUR_DELIVERY_TOKEN'
+CONTENTSTACK_ENVIRONMENT='preview'
+CONTENTSTACK_PREVIEW_TOKEN='YOUR_PREVIEW_TOKEN'
+CONTENTSTACK_PERSONALIZATION="YOUR_PERSONALIZE_PROJECT_UID"
```
-Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
-
-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
-
-This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
-
-## Learn More
-
-To learn more about Next.js, take a look at the following resources:
+#### Install Packages and Run Locally
+First, install all npm packages by running:
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
+```bash
+npm i
+```
-## Deploy on Vercel
+Next, run the app locally:
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+```bash
+npm run dev
+```
-Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
diff --git a/next.config.ts b/next.config.ts
index 6adbb4e..75deb7c 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -20,6 +20,10 @@ const nextConfig: NextConfig = {
LYTICS_TAG: process.env.LYTICS_TAG,
LIVE_PREVIEW_ENABLED: process.env.LIVE_PREVIEW_ENABLED,
HOSTING: process.env.HOSTING,
+ CONTENTSTACK_AUTOMATIONS_API_URL: process.env.CONTENTSTACK_AUTOMATIONS_API_URL,
+ LYTICS_API_KEY: process.env.LYTICS_API_KEY,
+ LYTICS_COLLECTION_ID: process.env.LYTICS_COLLECTION_ID,
+ CONTENTSTACK_TERM: process.env.CONTENTSTACK_TERM,
}
};
diff --git a/package-lock.json b/package-lock.json
index db3ff76..6323291 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,15 +8,36 @@
"name": "demo-base",
"version": "0.1.0",
"dependencies": {
+ "@awesome.me/kit-610837e1f9": "^1.0.10",
"@contentstack/delivery-sdk": "^4.10.1",
+ "@contentstack/json-rte-serializer": "^3.0.4",
"@contentstack/live-preview-utils": "^4.1.1",
"@contentstack/personalize-edge-sdk": "^1.0.16",
"@contentstack/utils": "^1.4.4",
+ "@fortawesome/fontawesome-free": "^6.7.1",
+ "@fortawesome/react-fontawesome": "^0.2.2",
+ "@headlessui/react": "^2.2.0",
+ "@heroicons/react": "^2.1.3",
+ "@supabase/ssr": "^0.5.2",
+ "@supabase/supabase-js": "^2.49.1",
+ "@uiw/react-json-view": "^2.0.0-alpha.39",
+ "accept-language-parser": "^1.5.0",
"contentstack": "^3.26.2",
+ "dayjs": "^1.11.13",
+ "detect-ua": "^1.0.2",
+ "framer-motion": "^12.0.0-alpha.0",
+ "html-react-parser": "^5.2.6",
+ "jsonpointer": "^5.0.1",
+ "murmurhash3js": "^3.0.1",
"next": "15.5.4",
"next-intl": "^4.3.9",
"react": "19.1.0",
- "react-dom": "19.1.0"
+ "react-dom": "19.1.0",
+ "react-icons": "^5.5.0",
+ "react-tailwindcss-datepicker": "^2.0.0",
+ "styled-components": "^6.1.13",
+ "swr": "^2.2.5",
+ "url-parse": "^1.5.10"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -43,6 +64,28 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/@awesome.me/kit-610837e1f9": {
+ "version": "1.0.16",
+ "resolved": "https://npm.fontawesome.com/@awesome.me/kit-610837e1f9/-/kit-610837e1f9-1.0.16.tgz",
+ "integrity": "sha512-jLDsrvPwzddCO/1Bj4Hjoz978ospHT4n1AerRG9BhTxlzK7FFdXyAELdPsWokHAeLH0TDgFBzWXc6Oo21s/kgA==",
+ "license": "UNLICENSED",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "^6.7.2"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
+ "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@contentstack/core": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@contentstack/core/-/core-1.3.2.tgz",
@@ -69,6 +112,26 @@
"humps": "^2.0.1"
}
},
+ "node_modules/@contentstack/json-rte-serializer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@contentstack/json-rte-serializer/-/json-rte-serializer-3.0.4.tgz",
+ "integrity": "sha512-dvJYLApd7O/aw2tufA3810LwDRk0LHO53V4tBY5kYRoJkpiuZZOEmP/vUD7gh8lP4mT5RBoL4f6s9gBa6uvWzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "array-flat-polyfill": "^1.0.1",
+ "lodash": "^4.17.21",
+ "lodash.clonedeep": "^4.5.0",
+ "lodash.flatten": "^4.4.0",
+ "lodash.isempty": "^4.4.0",
+ "lodash.isequal": "^4.5.0",
+ "lodash.isobject": "^3.0.2",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isundefined": "^3.0.1",
+ "lodash.kebabcase": "^4.1.1",
+ "slate": "^0.103.0",
+ "uuid": "^8.3.2"
+ }
+ },
"node_modules/@contentstack/live-preview-utils": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@contentstack/live-preview-utils/-/live-preview-utils-4.1.1.tgz",
@@ -141,6 +204,28 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
+ "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
+ "license": "MIT"
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
@@ -304,6 +389,34 @@
"@floating-ui/utils": "^0.2.10"
}
},
+ "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/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.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.4"
+ },
+ "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",
@@ -370,6 +483,87 @@
"tslib": "2"
}
},
+ "node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "6.7.2",
+ "resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
+ "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-free": {
+ "version": "6.7.2",
+ "resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
+ "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
+ "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-svg-core": {
+ "version": "7.1.0",
+ "resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz",
+ "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "7.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-svg-core/node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "7.1.0",
+ "resolved": "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz",
+ "integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/react-fontawesome": {
+ "version": "0.2.6",
+ "resolved": "https://npm.fontawesome.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.6.tgz",
+ "integrity": "sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7",
+ "react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "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",
@@ -1119,6 +1313,7 @@
"resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.3.2.tgz",
"integrity": "sha512-naxcJgUJ6BTOROJ7C3QML7KvwKwCXQJYTc5L/b0eEsdYgPB6SxwoQ1vDGcS0Q7GVjAenVq/tXrybVdFShHYZWg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@preact/signals-core": "^1.7.0"
},
@@ -1140,6 +1335,103 @@
"url": "https://opencollective.com/preact"
}
},
+ "node_modules/@react-aria/focus": {
+ "version": "3.21.2",
+ "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz",
+ "integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/interactions": "^3.25.6",
+ "@react-aria/utils": "^3.31.0",
+ "@react-types/shared": "^3.32.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.25.6",
+ "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz",
+ "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.10",
+ "@react-aria/utils": "^3.31.0",
+ "@react-stately/flags": "^3.1.2",
+ "@react-types/shared": "^3.32.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.31.0",
+ "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.31.0.tgz",
+ "integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@react-aria/ssr": "^3.9.10",
+ "@react-stately/flags": "^3.1.2",
+ "@react-stately/utils": "^3.10.8",
+ "@react-types/shared": "^3.32.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.10.8",
+ "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz",
+ "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
+ "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.32.1",
+ "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz",
+ "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==",
+ "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",
@@ -1173,6 +1465,99 @@
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
"license": "MIT"
},
+ "node_modules/@supabase/auth-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.81.1.tgz",
+ "integrity": "sha512-K20GgiSm9XeRLypxYHa5UCnybWc2K0ok0HLbqCej/wRxDpJxToXNOwKt0l7nO8xI1CyQ+GrNfU6bcRzvdbeopQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.81.1.tgz",
+ "integrity": "sha512-sYgSO3mlgL0NvBFS3oRfCK4OgKGQwuOWJLzfPyWg0k8MSxSFSDeN/JtrDJD5GQrxskP6c58+vUzruBJQY78AqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.81.1.tgz",
+ "integrity": "sha512-DePpUTAPXJyBurQ4IH2e42DWoA+/Qmr5mbgY4B6ZcxVc/ZUKfTVK31BYIFBATMApWraFc8Q/Sg+yxtfJ3E0wSg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.81.1.tgz",
+ "integrity": "sha512-ViQ+Kxm8BuUP/TcYmH9tViqYKGSD1LBjdqx2p5J+47RES6c+0QHedM0PPAjthMdAHWyb2LGATE9PD2++2rO/tw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/phoenix": "^1.6.6",
+ "@types/ws": "^8.18.1",
+ "tslib": "2.8.1",
+ "ws": "^8.18.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/ssr": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.5.2.tgz",
+ "integrity": "sha512-n3plRhr2Bs8Xun1o4S3k1CDv17iH5QY9YcoEvXX3bxV1/5XSasA0mNXYycFmADIdtdE6BG9MRjP5CGIs8qxC8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cookie": "^0.6.0",
+ "cookie": "^0.7.0"
+ },
+ "peerDependencies": {
+ "@supabase/supabase-js": "^2.43.4"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.81.1.tgz",
+ "integrity": "sha512-UNmYtjnZnhouqnbEMC1D5YJot7y0rIaZx7FG2Fv8S3hhNjcGVvO+h9We/tggi273BFkiahQPS/uRsapo1cSapw==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.81.1",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.81.1.tgz",
+ "integrity": "sha512-KSdY7xb2L0DlLmlYzIOghdw/na4gsMcqJ8u4sD6tOQJr+x3hLujU9s4R8N3ob84/1bkvpvlU5PYKa1ae+OICnw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@supabase/auth-js": "2.81.1",
+ "@supabase/functions-js": "2.81.1",
+ "@supabase/postgrest-js": "2.81.1",
+ "@supabase/realtime-js": "2.81.1",
+ "@supabase/storage-js": "2.81.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1513,6 +1898,33 @@
"tailwindcss": "4.1.16"
}
},
+ "node_modules/@tanstack/react-virtual": {
+ "version": "3.13.12",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
+ "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/virtual-core": "3.13.12"
+ },
+ "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.12",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
+ "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
+ "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",
@@ -1524,6 +1936,12 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1549,18 +1967,24 @@
"version": "20.19.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz",
"integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
+ "node_modules/@types/phoenix": {
+ "version": "1.6.6",
+ "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
+ "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -1575,6 +1999,12 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/stylis": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
+ "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==",
+ "license": "MIT"
+ },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -1582,6 +2012,15 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@types/ws": {
+ "version": "8.18.1",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+ "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz",
@@ -1628,6 +2067,7 @@
"integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.3",
"@typescript-eslint/types": "8.46.3",
@@ -1870,6 +2310,20 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/@uiw/react-json-view": {
+ "version": "2.0.0-alpha.39",
+ "resolved": "https://registry.npmjs.org/@uiw/react-json-view/-/react-json-view-2.0.0-alpha.39.tgz",
+ "integrity": "sha512-D9MHNan56WhtdAsmjtE9x18YLY0JSMnh0a6Ji0/2sVXCF456ZVumYLdx2II7hLQOgRMa4QMaHloytpTUHxsFRw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://jaywcjlove.github.io/#/sponsor"
+ },
+ "peerDependencies": {
+ "@babel/runtime": ">=7.10.0",
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0"
+ }
+ },
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz",
@@ -2139,12 +2593,19 @@
"win32"
]
},
+ "node_modules/accept-language-parser": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/accept-language-parser/-/accept-language-parser-1.5.0.tgz",
+ "integrity": "sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw==",
+ "license": "MIT"
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2229,6 +2690,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/array-flat-polyfill": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-flat-polyfill/-/array-flat-polyfill-1.0.1.tgz",
+ "integrity": "sha512-hfJmKupmQN0lwi0xG6FQ5U8Rd97RnIERplymOv/qpq8AoNKPPAnxJadjFA23FNWm88wykh9HmpLJUUwUtNU/iw==",
+ "license": "CC0-1.0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/array-includes": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz",
@@ -2426,6 +2896,7 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
@@ -2544,6 +3015,15 @@
"node": ">=6"
}
},
+ "node_modules/camelize": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
+ "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/caniuse-lite": {
"version": "1.0.30001753",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz",
@@ -2593,6 +3073,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",
@@ -2647,6 +3136,15 @@
"node": ">= 10.14.2"
}
},
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/cross-fetch": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz",
@@ -2671,11 +3169,32 @@
"node": ">= 8"
}
},
+ "node_modules/css-color-keywords": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
+ "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/css-to-react-native": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
+ "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "camelize": "^1.0.0",
+ "css-color-keywords": "^1.0.0",
+ "postcss-value-parser": "^4.0.2"
+ }
+ },
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
@@ -2742,7 +3261,8 @@
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/debug": {
"version": "4.4.3",
@@ -2846,6 +3366,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2856,6 +3385,12 @@
"node": ">=8"
}
},
+ "node_modules/detect-ua": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/detect-ua/-/detect-ua-1.0.2.tgz",
+ "integrity": "sha512-EsqFycxcggrkzRm3IaHr1nP1pSCDVLHW3haUvaSqws70B277a1SKk5hFtuJo3BKuPwTTRGTGsYYiJwS/97ptEg==",
+ "license": "MIT"
+ },
"node_modules/doctrine": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -2869,6 +3404,59 @@
"node": ">=0.10.0"
}
},
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/dom-serializer/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
"node_modules/dompurify": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz",
@@ -2878,6 +3466,20 @@
"@types/trusted-types": "^2.0.7"
}
},
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2913,6 +3515,18 @@
"node": ">=10.13.0"
}
},
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/es-abstract": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz",
@@ -3111,6 +3725,7 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3284,6 +3899,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -3706,6 +4322,33 @@
"node": ">= 6"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.23.24",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
+ "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.23.23",
+ "motion-utils": "^12.23.6",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3999,6 +4642,56 @@
"node": ">= 0.4"
}
},
+ "node_modules/html-dom-parser": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/html-dom-parser/-/html-dom-parser-5.1.1.tgz",
+ "integrity": "sha512-+o4Y4Z0CLuyemeccvGN4bAO20aauB2N9tFEAep5x4OW34kV4PTarBHm6RL02afYt2BMKcr0D2Agep8S3nJPIBg==",
+ "license": "MIT",
+ "dependencies": {
+ "domhandler": "5.0.3",
+ "htmlparser2": "10.0.0"
+ }
+ },
+ "node_modules/html-react-parser": {
+ "version": "5.2.8",
+ "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-5.2.8.tgz",
+ "integrity": "sha512-09WaI81tbpwhXWeMe1m9VptZVJUcigo0l59zVt+2HUIQT7+baU38/oNhllj6MKhOuGXqh0nrlwOgxbxbm6xXHw==",
+ "license": "MIT",
+ "dependencies": {
+ "domhandler": "5.0.3",
+ "html-dom-parser": "5.1.1",
+ "react-property": "2.0.2",
+ "style-to-js": "1.1.19"
+ },
+ "peerDependencies": {
+ "@types/react": "0.14 || 15 || 16 || 17 || 18 || 19",
+ "react": "0.14 || 15 || 16 || 17 || 18 || 19"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz",
+ "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.1",
+ "entities": "^6.0.0"
+ }
+ },
"node_modules/humps": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz",
@@ -4030,6 +4723,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -4057,6 +4760,12 @@
"node": ">=0.8.19"
}
},
+ "node_modules/inline-style-parser": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.6.tgz",
+ "integrity": "sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg==",
+ "license": "MIT"
+ },
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -4364,6 +5073,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -4555,13 +5273,12 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4605,6 +5322,15 @@
"json5": "lib/cli.js"
}
},
+ "node_modules/jsonpointer": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
+ "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -4963,6 +5689,55 @@
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.flatten": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
+ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isempty": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz",
+ "integrity": "sha512-oKMuF3xEeqDltrGMfDxAPGIVMSSRv8tbRSODbrs4KGsRRLEhrW8N8Rd4DRgB2+621hY8A8XwwrTVhXWpxFvMzg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isobject": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-3.0.2.tgz",
+ "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isundefined": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz",
+ "integrity": "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.kebabcase": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz",
+ "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==",
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -4974,7 +5749,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -5070,6 +5844,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/motion-dom": {
+ "version": "12.23.23",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
+ "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.23.6"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.23.6",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
+ "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5077,6 +5866,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/murmurhash3js": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/murmurhash3js/-/murmurhash3js-3.0.1.tgz",
+ "integrity": "sha512-KL8QYUaxq7kUbcl0Yto51rMcYt7E/4N4BG3/c96Iqw1PQrTRspu8Cpx4TZ4Nunib1d4bEkIH3gjCYlP2RLBdow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
@@ -5267,7 +6065,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -5551,11 +6348,18 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
"node_modules/preact": {
"version": "10.19.5",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.19.5.tgz",
"integrity": "sha512-OPELkDmSVbKjbFqF9tgvOowiiQ9TmsJljIzXRyNE8nGiis94pwv1siF78rQkAP1Q1738Ce6pellRg/Ns/CtHqQ==",
"license": "MIT",
+ "peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -5575,7 +6379,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -5614,6 +6417,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+ "license": "MIT"
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -5640,6 +6449,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"
}
@@ -5649,6 +6459,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"
},
@@ -5656,13 +6467,37 @@
"react": "^19.1.0"
}
},
+ "node_modules/react-icons": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
+ "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
+ "node_modules/react-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.2.tgz",
+ "integrity": "sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug==",
+ "license": "MIT"
+ },
+ "node_modules/react-tailwindcss-datepicker": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/react-tailwindcss-datepicker/-/react-tailwindcss-datepicker-2.0.0.tgz",
+ "integrity": "sha512-HADddzZjeOIMxkKkueAyQmiAExLXYcwgPG0Qv1oP9cjCx4/jrhbbOAbazgjJmxfhGxUw7e0Goqs+DY3htKlhAA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "dayjs": "^1.11.12",
+ "react": "^17.0.2 || ^18.2.0 || ^19.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -5707,6 +6542,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -5906,6 +6747,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/shallowequal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
+ "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
+ "license": "MIT"
+ },
"node_modules/sharp": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz",
@@ -6044,6 +6891,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/slate": {
+ "version": "0.103.0",
+ "resolved": "https://registry.npmjs.org/slate/-/slate-0.103.0.tgz",
+ "integrity": "sha512-eCUOVqUpADYMZ59O37QQvUdnFG+8rin0OGQAXNHvHbQeVJ67Bu0spQbcy621vtf8GQUXTEQBlk6OP9atwwob4w==",
+ "license": "MIT",
+ "dependencies": {
+ "immer": "^10.0.3",
+ "is-plain-object": "^5.0.0",
+ "tiny-warning": "^1.0.3"
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -6210,6 +7068,86 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/style-to-js": {
+ "version": "1.1.19",
+ "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.19.tgz",
+ "integrity": "sha512-Ev+SgeqiNGT1ufsXyVC5RrJRXdrkRJ1Gol9Qw7Pb72YCKJXrBvP0ckZhBeVSrw2m06DJpei2528uIpjMb4TsoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "1.0.12"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.12.tgz",
+ "integrity": "sha512-ddJqYnoT4t97QvN2C95bCgt+m7AAgXjVnkk/jxAfmp7EAB8nnqqZYEbMd3em7/vEomDb2LAQKAy1RFfv41mdNw==",
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.2.6"
+ }
+ },
+ "node_modules/styled-components": {
+ "version": "6.1.19",
+ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.19.tgz",
+ "integrity": "sha512-1v/e3Dl1BknC37cXMhwGomhO8AkYmN41CqyX9xhUDxry1ns3BFQy2lLDRQXJRdVVWB9OHemv/53xaStimvWyuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/is-prop-valid": "1.2.2",
+ "@emotion/unitless": "0.8.1",
+ "@types/stylis": "4.2.5",
+ "css-to-react-native": "3.2.0",
+ "csstype": "3.1.3",
+ "postcss": "8.4.49",
+ "shallowequal": "1.1.0",
+ "stylis": "4.3.2",
+ "tslib": "2.6.2"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/styled-components"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0",
+ "react-dom": ">= 16.8.0"
+ }
+ },
+ "node_modules/styled-components/node_modules/postcss": {
+ "version": "8.4.49",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+ "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.7",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/styled-components/node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+ "license": "0BSD"
+ },
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@@ -6233,6 +7171,12 @@
}
}
},
+ "node_modules/stylis": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
+ "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==",
+ "license": "MIT"
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -6259,6 +7203,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/swr": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz",
+ "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/tabbable": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz",
+ "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==",
+ "license": "MIT"
+ },
"node_modules/tailwindcss": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
@@ -6280,6 +7243,12 @@
"url": "https://opencollective.com/webpack"
}
},
+ "node_modules/tiny-warning": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -6321,6 +7290,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -6476,6 +7446,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6507,7 +7478,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/unrs-resolver": {
@@ -6555,6 +7525,16 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "license": "MIT",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
"node_modules/use-intl": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.4.0.tgz",
@@ -6569,6 +7549,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": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -6709,6 +7698,27 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 67b239a..a956e31 100644
--- a/package.json
+++ b/package.json
@@ -9,15 +9,36 @@
"lint": "eslint"
},
"dependencies": {
+ "@awesome.me/kit-610837e1f9": "^1.0.10",
"@contentstack/delivery-sdk": "^4.10.1",
+ "@contentstack/json-rte-serializer": "^3.0.4",
"@contentstack/live-preview-utils": "^4.1.1",
"@contentstack/personalize-edge-sdk": "^1.0.16",
"@contentstack/utils": "^1.4.4",
+ "@fortawesome/fontawesome-free": "^6.7.1",
+ "@fortawesome/react-fontawesome": "^0.2.2",
+ "@headlessui/react": "^2.2.0",
+ "@heroicons/react": "^2.1.3",
+ "@supabase/ssr": "^0.5.2",
+ "@supabase/supabase-js": "^2.49.1",
+ "@uiw/react-json-view": "^2.0.0-alpha.39",
+ "accept-language-parser": "^1.5.0",
"contentstack": "^3.26.2",
+ "dayjs": "^1.11.13",
+ "detect-ua": "^1.0.2",
+ "framer-motion": "^12.0.0-alpha.0",
+ "html-react-parser": "^5.2.6",
+ "jsonpointer": "^5.0.1",
+ "murmurhash3js": "^3.0.1",
"next": "15.5.4",
"next-intl": "^4.3.9",
"react": "19.1.0",
- "react-dom": "19.1.0"
+ "react-dom": "19.1.0",
+ "react-icons": "^5.5.0",
+ "react-tailwindcss-datepicker": "^2.0.0",
+ "styled-components": "^6.1.13",
+ "swr": "^2.2.5",
+ "url-parse": "^1.5.10"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
diff --git a/src/app/[locale]/account/changepassword/actions.js b/src/app/[locale]/account/changepassword/actions.js
new file mode 100644
index 0000000..053ad35
--- /dev/null
+++ b/src/app/[locale]/account/changepassword/actions.js
@@ -0,0 +1,22 @@
+'use server'
+
+import { revalidatePath } from 'next/cache'
+import { redirect } from 'next/navigation'
+import { createClient } from '@/utils/supabase/server'
+
+export async function changePassword(formData) {
+ const supabase = await createClient()
+
+ const passwords = {
+ password: formData.get('password'),
+ }
+
+ const { error } = await supabase.auth.updateUser({ password: passwords.password })
+
+ if(error){
+ redirect('/error');
+ }
+
+ revalidatePath('/', 'layout')
+ redirect('/account/login')
+}
\ No newline at end of file
diff --git a/src/app/[locale]/account/changepassword/page.jsx b/src/app/[locale]/account/changepassword/page.jsx
new file mode 100644
index 0000000..fa38a04
--- /dev/null
+++ b/src/app/[locale]/account/changepassword/page.jsx
@@ -0,0 +1,71 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { changePassword } from "./actions";
+import Link from "next/link";
+
+export default function ChangePassword() {
+ const [password, setPassword] = useState("");
+ const [cpassword, setCPassword] = useState("");
+ const [showError, setShowError] = useState(false);
+
+ useEffect(() => {
+ if(password !== cpassword)
+ setShowError(true);
+ else
+ setShowError(false);
+ }, [password, cpassword])
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/account/layout.js b/src/app/[locale]/account/layout.js
new file mode 100644
index 0000000..629a786
--- /dev/null
+++ b/src/app/[locale]/account/layout.js
@@ -0,0 +1,16 @@
+
+import { createClient } from '@/utils/supabase/server'
+
+export default async function RootLayout({ children }) {
+ const supabase = await createClient()
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/app/[locale]/account/login/actions.js b/src/app/[locale]/account/login/actions.js
new file mode 100644
index 0000000..a4d04e1
--- /dev/null
+++ b/src/app/[locale]/account/login/actions.js
@@ -0,0 +1,61 @@
+'use server'
+import { revalidatePath } from 'next/cache'
+import { redirect } from 'next/navigation'
+import { createClient } from '@/utils/supabase/server'
+
+export async function login(formData) {
+ const supabase = await createClient()
+
+ const data = {
+ email: formData.get('email'),
+ password: formData.get('password'),
+ }
+
+ const { error } = await supabase.auth.signInWithPassword(data)
+
+ if (error) {
+ redirect('/account/login?result=invalid')
+ }
+
+ revalidatePath('/', 'layout')
+ redirect('/')
+}
+
+export async function signup(formData) {
+ const supabase = await createClient()
+
+ const data = {
+ email: formData.get('register_email'),
+ password: formData.get('register_password'),
+ }
+
+ const { data: result, error } = await supabase.auth.signUp(data)
+
+ if (error) {
+ redirect('/error')
+ }
+ else if(result?.user?.identities?.length === 0){
+ redirect('/account/login?result=exists')
+ }
+ else{
+ revalidatePath('/', 'layout')
+ redirect('/account/registered')
+ }
+}
+
+export async function signout(){
+ const supabase = await createClient()
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser()
+
+ if (user) {
+ await supabase.auth.signOut()
+ }
+
+ revalidatePath('/', 'layout')
+ return NextResponse.redirect(new URL('/login', req.url), {
+ status: 302,
+ })
+}
\ No newline at end of file
diff --git a/src/app/[locale]/account/login/page.jsx b/src/app/[locale]/account/login/page.jsx
new file mode 100644
index 0000000..658dfa3
--- /dev/null
+++ b/src/app/[locale]/account/login/page.jsx
@@ -0,0 +1,130 @@
+"use client"
+import { useEffect, useState } from "react"
+import { useSearchParams, usePathname } from "next/navigation";
+import { login, signup } from "./actions"
+import { ArrowLeftIcon } from "@heroicons/react/24/outline";
+import Link from "next/link";
+
+export default function LoginPage() {
+ const [showRegister, setShowRegister] = useState(false);
+ const [showInvalid, setShowInvalid] = useState(false);
+ const [userExists, setUserExists] = useState(false);
+ const params = useSearchParams();
+ const pathname = usePathname();
+
+ useEffect(() => {
+
+ if (params.get('result') === 'invalid'){
+ setShowInvalid(true);
+ console.log('invalid is set to true')
+ }
+ else if(params.get('result') === 'exists'){
+ setUserExists(true);
+ }
+ }, [pathname, params])
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/account/registered/page.jsx b/src/app/[locale]/account/registered/page.jsx
new file mode 100644
index 0000000..29be03e
--- /dev/null
+++ b/src/app/[locale]/account/registered/page.jsx
@@ -0,0 +1,13 @@
+import Link from 'next/link';
+
+export default function RegisteredPage(){
+ return(
+
+
+
+
A signup confirmation has been sent to yourm email address.
+
Return to Login
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/account/resetpassword/page.jsx b/src/app/[locale]/account/resetpassword/page.jsx
new file mode 100644
index 0000000..cb6aa7e
--- /dev/null
+++ b/src/app/[locale]/account/resetpassword/page.jsx
@@ -0,0 +1,74 @@
+"use client"
+
+import { useState } from "react";
+import { createClient } from '@/utils/supabase/client'
+import Link from "next/link";
+
+export default function ResetPassword() {
+ const [email, setEmail] = useState("");
+ const [showSent, setShowSent] = useState(false);
+ let [supabase] = useState(() => createClient());
+
+ async function resetSubmit(e) {
+ e.preventDefault();
+
+ const { data, error } = await supabase.auth.resetPasswordForEmail(email)
+
+ if (error) {
+ alert("Error sending email")
+ }
+
+ setShowSent(true);
+ }
+
+
+ return (
+
+
+
+
+
+
+
+ {!showSent &&
+
+ }
+ {showSent &&
+
+ }
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/articles/[title]/page.js b/src/app/[locale]/articles/[title]/page.js
new file mode 100644
index 0000000..e7a8629
--- /dev/null
+++ b/src/app/[locale]/articles/[title]/page.js
@@ -0,0 +1,117 @@
+"use client";
+import Header from "@/components/header";
+import { useState, useEffect } from "react";
+import Footer from "@/components/footer";
+import { ContentstackClient } from "@/lib/contentstack-client";
+import Link from "next/link";
+import { useParams } from "next/navigation";
+import { useDataContext } from "@/context/data.context";
+
+export default function ArticlesList({ }) {
+ const [entry, setEntry] = useState({});
+ const [articles, setArticles] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const params = useParams();
+ const initialData = useDataContext();
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrl(
+ "article_list",
+ `/articles/${params.title}`,
+ params.locale,
+ );
+ setEntry(entry[0]);
+
+ if (entry[0]?.taxonomy_category?.length > 0) {
+ const articles = await ContentstackClient.getElementByTypeByTaxonomy(
+ "article",
+ params.locale,
+ entry[0].taxonomy_category
+ );
+ setArticles(articles);
+ }
+ setIsLoading(false);
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ if (isLoading) return;
+
+ return (
+ <>
+
+ {console.log("articles in component",articles)}
+
+
+
+
+ {entry?.header}
+
+
+ {entry?.subtext}
+
+
+
+ {entry?.taxonomy_category?.length === 0 &&
+
+
Please select a taxonomy in the form.
+
+ }
+ {entry?.taxonomy_category?.length > 0 &&
+
+ {articles?.map((article) => (
+
+
+
+
+
+
+
+
+
+
+
Editorial Staff
+
+ {article.taxonomies?.map((tax, tdx) => {
+ return (
+
+ {tax.term_uid}
+
+ );
+ })}
+
+
+
+
+ {article.title}
+
+
+
+ {article.teaser}
+
+
+
+
+ ))}
+
+ }
+
+
+
+ >
+ );
+}
diff --git a/src/app/[locale]/articles/categories/[title]/page.js b/src/app/[locale]/articles/categories/[title]/page.js
new file mode 100644
index 0000000..35c9f07
--- /dev/null
+++ b/src/app/[locale]/articles/categories/[title]/page.js
@@ -0,0 +1,100 @@
+"use client";
+import Header from "@/components/header";
+import { useState, useEffect } from "react";
+import Footer from "@/components/footer";
+import { ContentstackClient } from "@/lib/contentstack-client";
+import Link from "next/link";
+import { useParams } from "next/navigation";
+
+export default function ArticleCategory({ }) {
+ const [entries, setEntries] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const params = useParams();
+
+ const getContent = async () => {
+ const entries = await ContentstackClient.getElementByTypeByTaxonomy(
+ "article",
+ params.locale,
+ [params.title]
+ );
+ setEntries(entries[0]);
+ setIsLoading(false);
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ if (isLoading) return;
+
+ return (
+ <>
+
+
+
+
+
+ {params.title}
+
+
+ All articles with {params.title} tag
+
+
+
+
+ {entries.map((article) => (
+
+
+
+
+
+
+
+
+
+
+
+
+ Editorial Staff
+
+
+ {article.taxonomies.map((tax, tdx) => {
+ return (
+
+ {tax.term_uid}
+
+ );
+ })}
+
+
+
+
+
+ {article.title}
+
+
+
+ {article.teaser}
+
+
+
+
+ ))}
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/[locale]/articles/entry/[title]/page.js b/src/app/[locale]/articles/entry/[title]/page.js
new file mode 100644
index 0000000..c36be82
--- /dev/null
+++ b/src/app/[locale]/articles/entry/[title]/page.js
@@ -0,0 +1,110 @@
+"use client";
+import { useState, useEffect } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client";
+import Footer from "@/components/footer";
+import Header from "@/components/header";
+import { jsonToHtml } from "@contentstack/json-rte-serializer";
+import Link from "next/link";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faLocationDot, faUser, faCalendar } from "@awesome.me/kit-610837e1f9/icons/classic/light";
+import { useParams } from "next/navigation";
+// import { useJstag } from "@/context/lyticsTracking";
+
+export default function Page({ }) {
+ const [entry, setEntry] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const params = useParams();
+ //const jstag = useJstag();
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrl(
+ "article",
+ "/articles/entry/" + params.title,
+ params.locale
+ );
+ setEntry(entry[0]);
+ setIsLoading(false);
+ //jstag.send({"taxonomy" : entry?.taxonomies[0]?.term_uid});
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ if (isLoading)
+ return;
+
+ return (
+
+
+
+
+
+
+ ARTICLES / {entry?.headline}
+
+ {!entry?.banner_image?.url &&
+
+ }
+ {entry?.banner_image?.url &&
+
+ }
+
+
+ {entry?.headline}
+
+
+ {entry?.teaser}
+
+ {entry?.isevent && <>
+
+ {
+ entry?.event_details?.event_date ?
+
+
+
+ {new Date(entry?.event_details?.event_date).toLocaleDateString(params.locale, { year: 'numeric', month: 'long', day: 'numeric' })} - {new Date(entry?.event_details?.event_date).toLocaleTimeString(params.locale, { hour: '2-digit', minute: '2-digit' })}
+
+
+ : null
+ }
+ {
+ entry?.event_details?.event_type ?
+
+
+
+ {entry?.event_details?.event_type?.replace(/-/g, ' ')}
+
+
+ : null
+ }
+
+ {entry?.event_details?.venue &&
+
+
+ {entry.event_details.venue}
+
+
}
+ >}
+
+ {entry?.article_body &&
+
+ {entry.article_body.children.length === 1 && entry.article_body.children[0].children[0].text === "" &&
+
Article body
+ }
+ {entry.article_body &&
+
+ }
+
+ }
+
+
+
+
+
+ );
+}
diff --git a/src/app/[locale]/articles/page.js b/src/app/[locale]/articles/page.js
new file mode 100644
index 0000000..95446d2
--- /dev/null
+++ b/src/app/[locale]/articles/page.js
@@ -0,0 +1,107 @@
+"use client";
+import {
+ useEffect,
+ useState,
+} from 'react';
+import Footer from '@/components/footer';
+import Header from '@/components/header';
+import { ContentstackClient } from '@/lib/contentstack-client';
+import Link from 'next/link';
+import { useParams } from 'next/navigation';
+
+export default function AllArticles({ }) {
+ const [entries, setEntries] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const params = useParams();
+
+ const getContent = async () => {
+ const entries = await ContentstackClient.getElementByType(
+ "article",
+ params.locale
+ );
+
+ setEntries(entries[0]);
+ setIsLoading(false);
+
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+
+ if (isLoading) return;
+
+ return (
+ <>
+
+
+
+
+
+
+ {entries.map((article) => (
+
+
+
+
+
+
+
+
+
+
+
+
+ Editorial Staff
+
+ {article?.taxonomies?.length > 0 && (
+
+ {article.taxonomies.map((tax, tdx) => {
+ return (
+
+ {tax.term_uid}
+
+ );
+ })}
+
+ )}
+
+
+
+
+
+ {article.title}
+
+
+
+ {article.teaser}
+
+
+
+
+ ))}
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/[locale]/deals/page.js b/src/app/[locale]/deals/page.js
new file mode 100644
index 0000000..63fbf9c
--- /dev/null
+++ b/src/app/[locale]/deals/page.js
@@ -0,0 +1,372 @@
+"use client"
+import {
+ useEffect,
+ useState,
+} from 'react';
+import Datepicker from 'react-tailwindcss-datepicker';
+import Footer from '@/components/footer';
+import Header from '@/components/header';
+import Marquee from '@/components/marquee';
+import { ContentstackClient } from '@/lib/contentstack-client';
+import { useParams } from 'next/navigation';
+
+const flights =
+ {
+ ORD: [
+ {
+ airport: "ORD",
+ destination: "MLE",
+ deptTime: "7:40 PM",
+ deptDate: "",
+ arrTime: "7:50 AM",
+ price: "$1,044",
+ duration: "26h 10m",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/blt1aa9b466083f438d/66d36cf9c2de8a7455c03767/AA_sq.svg"
+ },
+ {
+ airport: "ORD",
+ destination: "MLE",
+ deptTime: "7:40 PM",
+ deptDate: "",
+ arrTime: "7:50 AM",
+ price: "$1,453",
+ duration: "26h 10m",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/blt1aa9b466083f438d/66d36cf9c2de8a7455c03767/AA_sq.svg"
+ },
+ {
+ airport: "ORD",
+ destination: "MLE",
+ deptTime: "7:40 PM",
+ deptDate: "",
+ arrTime: "7:50 AM",
+ price: "$1,238",
+ duration: "26h 10m",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/blt1aa9b466083f438d/66d36cf9c2de8a7455c03767/AA_sq.svg"
+ }
+ ],
+ AMS: [
+ {
+ airport: "AMS",
+ destination: "MLE",
+ deptTime: "11:45",
+ deptDate: "",
+ arrTime: "13:00",
+ price: "EUR 1.318",
+ duration: "22h15",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/bltcaf8153fbb0225d9/66d36cf9b6489f29eae6921e/KL_sq.svg"
+ },
+ {
+ airport: "AMS",
+ destination: "MLE",
+ deptTime: "11:45",
+ deptDate: "",
+ arrTime: "13:00",
+ price: "EUR 1.318",
+ duration: "22h15",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/bltcaf8153fbb0225d9/66d36cf9b6489f29eae6921e/KL_sq.svg"
+ },
+ {
+ airport: "AMS",
+ destination: "MLE",
+ deptTime: "11:45",
+ deptDate: "",
+ arrTime: "13:00",
+ price: "EUR 1.318",
+ duration: "22h15",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/bltcaf8153fbb0225d9/66d36cf9b6489f29eae6921e/KL_sq.svg"
+ }
+ ],
+ CDG: [
+ {
+ airport: "CDG",
+ destination: "MLE",
+ deptTime: "13:45",
+ deptDate: "",
+ arrTime: "15:20",
+ price: "826 EUR",
+ duration: "22h35",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/bltc9682714f481f40a/66d36cf970edf864460441f0/AF_sq.svg"
+ },
+ {
+ airport: "CDG",
+ destination: "MLE",
+ deptTime: "13:45",
+ deptDate: "",
+ arrTime: "15:20",
+ price: "894 EUR",
+ duration: "22h35",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/bltc9682714f481f40a/66d36cf970edf864460441f0/AF_sq.svg"
+ },
+ {
+ airport: "CDG",
+ destination: "MLE",
+ deptTime: "13:45",
+ deptDate: "",
+ arrTime: "15:20",
+ price: "826 EUR",
+ duration: "22h35",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/bltc9682714f481f40a/66d36cf970edf864460441f0/AF_sq.svg"
+ }
+ ],
+ LHR: [
+ {
+ airport: "LHR",
+ destination: "MLE",
+ deptTime: "10:50",
+ deptDate: "",
+ arrTime: "08:25",
+ price: "£353",
+ duration: "17h 35m",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/bltb1c2c07c68cae1b6/66d36cf9063d8defac044804/BA_sq.svg"
+ },
+ {
+ airport: "LHR",
+ destination: "MLE",
+ deptTime: "14:10",
+ deptDate: "",
+ arrTime: "07:50",
+ price: "£383",
+ duration: "13h 40m",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/bltb1c2c07c68cae1b6/66d36cf9063d8defac044804/BA_sq.svg"
+ },
+ {
+ airport: "LHR",
+ destination: "MLE",
+ deptTime: "10:50",
+ deptDate: "",
+ arrTime: "08:25",
+ price: "£353",
+ duration: "17h 35m",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/bltb1c2c07c68cae1b6/66d36cf9063d8defac044804/BA_sq.svg"
+ }
+ ],
+ SYD: [
+ {
+ airport: "SYD",
+ destination: "MLE",
+ deptTime: "8:10 PM",
+ deptDate: "",
+ arrTime: "3:00 PM",
+ price: "AU$2,037",
+ duration: "19h 35m",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/blt1cfb529b963d4aa6/66d3738aa671311ec2a5ead0/EK_sq.svg"
+ },
+ {
+ airport: "SYD",
+ destination: "MLE",
+ deptTime: "7:32 PM",
+ deptDate: "",
+ arrTime: "3:00 PM",
+ price: "AU$2,248",
+ duration: "19h 35m",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/blt1cfb529b963d4aa6/66d3738aa671311ec2a5ead0/EK_sq.svg"
+ },
+ {
+ airport: "SYD",
+ destination: "MLE",
+ deptTime: "8:10 PM",
+ deptDate: "",
+ arrTime: "3:00 PM",
+ price: "AU$2,631",
+ duration: "19h 35m",
+ stops: 1,
+ logo: "https://demos-images.contentstack.com/v3/assets/blt7f5210b0ccd136f9/blt1cfb529b963d4aa6/66d3738aa671311ec2a5ead0/EK_sq.svg"
+ }
+ ]
+ }
+
+
+export default function Page({ }) {
+ const [deptDate, setDeptDate] = useState({ startDate: null, endDate: null });
+ const [formValid, setFormValid] = useState(false);
+ const [deptCode, setDeptCode] = useState();
+ const [passengers, setPassengers] = useState(2);
+ const [searchResults, setSearchResults] = useState([]);
+ const [entry, setEntry] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const params = useParams();
+
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByTypeWtihRefs(
+ "flights",
+ params.locale,
+ [
+
+ ]
+ );
+ setEntry(entry[0][0]);
+ setIsLoading(false);
+ };
+
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+
+ useEffect(() => {
+ if (deptCode !== undefined && deptDate.startDate !== null && passengers > 0)
+ setFormValid(true);
+ else
+ setFormValid(false);
+ }, [deptDate, deptCode, passengers])
+
+ useEffect(() => {
+ console.log('search changed', searchResults)
+ }, [searchResults])
+
+ const search = () => {
+ let temp = flights[deptCode];
+ if (!temp)
+ return;
+
+ let localeString = "";
+ if (deptCode === "ORD")
+ localeString = "en-us";
+ else if (deptCode === "AMS")
+ localeString = "nl-NL";
+ else if (deptCode === "CDG")
+ localeString = "fr-FR";
+ else if (deptCode === "LHR")
+ localeString = "en-UK";
+ else if (deptCode === "SYD")
+ localeString = "en-AU";
+
+ for (let x = 0; x < temp?.length; x++) {
+ let dDate = new Date(deptDate.startDate);
+ dDate.setDate(dDate.getDate() + x);
+ temp[x].deptDate = dDate.toLocaleDateString(localeString);
+ }
+ setSearchResults(temp);
+ }
+
+ if (isLoading) return;
+
+ return (
+
+
+
+
+
+
+
{entry?.headline}
+
+
+
{entry?.search?.info}
+
+
+
+
+
+ Departing Airport
+
+
+ setDeptCode(e.target.value)}
+ defaultValue={"DEFAULT"}
+ >
+ Select Airport
+ AMS
+ CDG
+ LHR
+ ORD
+ SYD
+
+
+
+
+
+
+ Departure Date
+
+
+ setDeptDate(val)}
+ />
+
+
+
+
+
+ Passengers
+
+
+
+
+ setPassengers(e.target.value)}
+ className="block w-full px-2 rounded-md border-0 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
+ />
+
+
search()} className="bg-[#D71A21] w-1/2 text-white ml-2.5 mt-2 rounded disabled:bg-[#909090]" disabled={!formValid} >SEARCH
+
+
+
+
+
+
+
+ {searchResults.map((item, index) => (
+
+
+
+
{`${item.deptDate} ${item.airport} -> ${item.destination}`}
+
+
+
DEPART
+
{item.deptTime}
+
+
+
DURATION
+
{item.duration}
+
+
+
ARRIVE
+
{item.arrTime}
+
+
+
+
+
+ ))}
+
+
+ {entry?.modular_blocks?.map((block, index) => {
+ if(block.hasOwnProperty("marquee"))
+ return
+ })}
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/events/page.js b/src/app/[locale]/events/page.js
new file mode 100644
index 0000000..40caaa5
--- /dev/null
+++ b/src/app/[locale]/events/page.js
@@ -0,0 +1,590 @@
+"use client";
+import { useState, useEffect } from "react";
+import { useDataContext } from "@/context/data.context";
+import { useParams } from "next/navigation";
+import Header from "@/components/header";
+import Footer from "@/components/footer";
+import { ContentstackClient } from "@/lib/contentstack-client"
+
+// Sample events data for Red Panda Resort
+// This array contains 20 resort-appropriate events with title, description, dateTime, location, and optional image
+export const resortEvents = [
+ {
+ title: "Sunrise Yoga on the Beach",
+ description: "Start your day with a peaceful yoga session on our pristine white sand beach. Our certified instructor will guide you through gentle poses as the sun rises over the Indian Ocean. All levels welcome. Mats and towels provided.",
+ dateTime: "Daily at 6:30 AM",
+ location: "Beach Pavilion",
+ image: "/images/events/sunrise-yoga.jpg"
+ },
+ {
+ title: "Snorkeling Adventure to Coral Gardens",
+ description: "Explore the vibrant underwater world of the Maldives with our guided snorkeling tour. Discover colorful coral reefs teeming with tropical fish, sea turtles, and marine life. Equipment included. Suitable for beginners and experienced snorkelers.",
+ dateTime: "Daily at 9:00 AM and 2:00 PM",
+ location: "Water Sports Center",
+ image: "/images/events/snorkeling.jpg"
+ },
+ {
+ title: "Maldivian Cooking Class",
+ description: "Learn to prepare authentic Maldivian dishes with our executive chef. Discover the secrets of local spices and traditional cooking methods. Includes lunch featuring the dishes you prepare. Limited to 12 participants.",
+ dateTime: "Tuesdays and Fridays at 11:00 AM",
+ location: "Culinary Studio",
+ image: "/images/events/cooking-class.jpg"
+ },
+ {
+ title: "Sunset Dolphin Watching Cruise",
+ description: "Embark on a magical journey to spot playful dolphins in their natural habitat. Enjoy complimentary champagne and canapés as you watch the sun set over the horizon. This is one of our most popular experiences.",
+ dateTime: "Daily at 5:30 PM",
+ location: "Main Jetty",
+ image: "/images/events/dolphin-cruise.jpg"
+ },
+ {
+ title: "Beachside BBQ Dinner",
+ description: "Indulge in a sumptuous beachside barbecue featuring fresh seafood, grilled meats, and local specialties. Enjoy live music and traditional Maldivian entertainment under the stars.",
+ dateTime: "Wednesdays and Saturdays at 7:00 PM",
+ location: "Beach Restaurant",
+ image: "/images/events/beach-bbq.jpg"
+ },
+ {
+ title: "Spa Wellness Workshop",
+ description: "Join our wellness expert for a workshop on mindfulness, meditation, and stress relief techniques. Learn practices you can take home with you. Includes a complimentary 30-minute spa treatment.",
+ dateTime: "Mondays and Thursdays at 10:00 AM",
+ location: "Spa & Wellness Center",
+ image: "/images/events/spa-workshop.jpg"
+ },
+ {
+ title: "Stargazing Experience",
+ description: "Experience the breathtaking night sky of the Maldives with our astronomy expert. Learn about constellations, planets, and celestial phenomena while enjoying tropical cocktails. Telescopes provided.",
+ dateTime: "Fridays at 8:00 PM (weather permitting)",
+ location: "Observatory Deck",
+ image: "/images/events/stargazing.jpg"
+ },
+ {
+ title: "Stand-Up Paddleboard Yoga",
+ description: "Combine the tranquility of yoga with the gentle movement of paddleboarding. This unique class takes place on calm lagoon waters, offering a peaceful and challenging workout.",
+ dateTime: "Daily at 7:30 AM",
+ location: "Lagoon",
+ image: "/images/events/paddleboard-yoga.jpg"
+ },
+ {
+ title: "Traditional Maldivian Bodu Beru Performance",
+ description: "Experience the vibrant rhythms and energetic dance of traditional Maldivian Bodu Beru. This cultural performance showcases local music and dance traditions passed down through generations.",
+ dateTime: "Sundays and Thursdays at 7:30 PM",
+ location: "Main Restaurant",
+ image: "/images/events/bodu-beru.jpg"
+ },
+ {
+ title: "Private Island Picnic",
+ description: "Escape to a secluded sandbank for a romantic private picnic. Includes gourmet lunch, champagne, and return boat transfer. Perfect for couples celebrating special occasions.",
+ dateTime: "Available daily, booking required",
+ location: "Private Sandbank",
+ image: "/images/events/private-picnic.jpg"
+ },
+ {
+ title: "Manta Ray Snorkeling Expedition",
+ description: "Swim alongside majestic manta rays in their natural habitat. Our marine biologist guide will share insights about these gentle giants. Includes boat transfer and equipment. Advanced booking recommended.",
+ dateTime: "Mondays, Wednesdays, and Saturdays at 8:00 AM",
+ location: "Manta Point (30-minute boat ride)",
+ image: "/images/events/manta-ray.jpg"
+ },
+ {
+ title: "Wine Tasting Evening",
+ description: "Join our sommelier for an exclusive wine tasting featuring selections from around the world, perfectly paired with artisanal cheeses and charcuterie. Learn about wine regions and tasting notes.",
+ dateTime: "Fridays at 6:30 PM",
+ location: "Wine Cellar",
+ image: "/images/events/wine-tasting.jpg"
+ },
+ {
+ title: "Sunset Fishing Trip",
+ description: "Experience traditional Maldivian fishing techniques on our sunset fishing excursion. Catch your dinner and have it prepared by our chefs. All equipment provided. Suitable for all skill levels.",
+ dateTime: "Daily at 4:30 PM",
+ location: "Fishing Boat",
+ image: "/images/events/fishing.jpg"
+ },
+ {
+ title: "Beach Volleyball Tournament",
+ description: "Join fellow guests for a friendly beach volleyball tournament. Teams are formed on-site, and all skill levels are welcome. Prizes for winners. Refreshments provided.",
+ dateTime: "Sundays at 3:00 PM",
+ location: "Beach Volleyball Court",
+ image: "/images/events/volleyball.jpg"
+ },
+ {
+ title: "Underwater Photography Workshop",
+ description: "Learn to capture stunning underwater images with our professional photographer. Covers equipment, techniques, and composition. Includes a guided snorkeling session to practice your skills.",
+ dateTime: "Tuesdays at 10:00 AM",
+ location: "Water Sports Center",
+ image: "/images/events/underwater-photography.jpg"
+ },
+ {
+ title: "Live Acoustic Music Night",
+ description: "Relax with live acoustic music performed by talented local and international artists. Enjoy your favorite cocktails while listening to soothing melodies in our beachfront bar.",
+ dateTime: "Daily at 8:00 PM",
+ location: "Beach Bar",
+ image: "/images/events/live-music.jpg"
+ },
+ {
+ title: "Kids' Marine Discovery Program",
+ description: "Educational and fun activities for children to learn about marine life, coral reefs, and ocean conservation. Includes interactive games, crafts, and supervised snorkeling in the lagoon.",
+ dateTime: "Daily at 10:00 AM",
+ location: "Kids' Club",
+ image: "/images/events/kids-marine.jpg"
+ },
+ {
+ title: "Sunrise Kayaking Tour",
+ description: "Paddle through calm waters as the sun rises, exploring hidden coves and pristine beaches. Includes light breakfast and refreshments. Single and double kayaks available.",
+ dateTime: "Daily at 6:00 AM",
+ location: "Water Sports Center",
+ image: "/images/events/kayaking.jpg"
+ },
+ {
+ title: "Cocktail Making Class",
+ description: "Learn to craft tropical cocktails with our expert mixologist. Discover the art of mixology using local ingredients and traditional techniques. Includes tastings and recipe cards to take home.",
+ dateTime: "Wednesdays and Saturdays at 5:00 PM",
+ location: "Beach Bar",
+ image: "/images/events/cocktail-class.jpg"
+ },
+ {
+ title: "Full Moon Beach Party",
+ description: "Celebrate the full moon with a special beach party featuring live DJ, fire dancers, and a themed buffet. Dance under the stars on our pristine beach. Open to all guests.",
+ dateTime: "Monthly on full moon (check calendar)",
+ location: "Main Beach",
+ image: "/images/events/full-moon-party.jpg"
+ },
+ {
+ title: "Reef Conservation Workshop",
+ description: "Learn about coral reef ecosystems and participate in our conservation efforts. Includes a presentation by our marine biologist and an opportunity to help with reef restoration activities.",
+ dateTime: "Thursdays at 2:00 PM",
+ location: "Marine Center",
+ image: "/images/events/reef-conservation.jpg"
+ }
+];
+
+// Helper function to get event days from Contentstack recur array
+function getEventDays(recur) {
+ if (!recur || !Array.isArray(recur)) {
+ return [];
+ }
+ return recur;
+}
+
+// Helper function to format time from Contentstack date field (always GMT+5 Maldives timezone)
+function getEventTime(date) {
+ if (!date) return '';
+ // Parse the date string as UTC and convert to Maldives timezone (GMT+5)
+ const dateObj = new Date(date);
+ // Format directly in Maldives timezone
+ return dateObj.toLocaleTimeString('en-US', {
+ hour: 'numeric',
+ minute: '2-digit',
+ hour12: true,
+ timeZone: 'Indian/Maldives'
+ });
+}
+
+// Helper function to format dateTime string for display
+function getEventDateTimeString(event) {
+ const time = getEventTime(event.date);
+ const days = event.recur || [];
+
+ if (days.length === 7) {
+ return `Daily at ${time}`;
+ } else if (days.length === 1) {
+ return `${days[0]}s at ${time}`;
+ } else if (days.length > 1) {
+ const dayNames = days.map(day => day + 's').join(', ');
+ return `${dayNames} at ${time}`;
+ }
+ return time;
+}
+
+const DEFAULT_EVENT_IMAGE = "https://images.contentstack.io/v3/assets/bltc991c0dda4197336/bltd14b8a03a4e57aad/6719553776705e041bfbd297/villas.jpeg";
+
+export default function Page(){
+ const params = useParams();
+ const [viewMode, setViewMode] = useState('list'); // 'list' or 'calendar'
+ const [selectedEvent, setSelectedEvent] = useState(null);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [entry, setEntry] = useState({});
+ const [events, setEvents] = useState([]); // Will be used once JSON structure is provided
+ const [isLoading, setIsLoading] = useState(true);
+ const initialData = useDataContext();
+
+ useEffect(() => {
+ const fetchData = async () => {
+ // Fetch events page content
+ const data = await ContentstackClient.getElementByType("events_page", params.locale, null);
+ if(data) {
+ setEntry(data[0]);
+ console.log("Events Page Entry:", data[0]);
+ } else {
+ setEntry(null);
+ }
+
+ // Fetch events from Contentstack
+ const eventsData = await ContentstackClient.getElementByType("event", params.locale, null);
+ if(eventsData) {
+ console.log("Events from Contentstack:", eventsData);
+ console.log("Events array:", eventsData);
+ setEvents(eventsData);
+ } else {
+ console.log("No events found in Contentstack");
+ setEvents([]);
+ }
+
+ setIsLoading(false);
+ }
+
+ ContentstackClient.onEntryChange(fetchData);
+ }, [params.locale, initialData]);
+
+ const openModal = (event) => {
+ setSelectedEvent(event);
+ setIsModalOpen(true);
+ };
+
+ const closeModal = () => {
+ setIsModalOpen(false);
+ setSelectedEvent(null);
+ };
+
+ // Handle ESC key to close modal and prevent body scroll
+ useEffect(() => {
+ if (isModalOpen) {
+ document.body.style.overflow = 'hidden';
+ } else {
+ document.body.style.overflow = 'unset';
+ }
+
+ const handleEscape = (e) => {
+ if (e.key === 'Escape' && isModalOpen) {
+ closeModal();
+ }
+ };
+ document.addEventListener('keydown', handleEscape);
+ return () => {
+ document.removeEventListener('keydown', handleEscape);
+ document.body.style.overflow = 'unset';
+ };
+ }, [isModalOpen]);
+
+ // Helper function to get audiences from taxonomies
+ const getAudiences = (event) => {
+ if (!event.taxonomies || !Array.isArray(event.taxonomies)) {
+ return [];
+ }
+ const audienceTaxonomies = event.taxonomies.filter(
+ tax => tax.taxonomy_uid === 'event_audiences'
+ );
+ return audienceTaxonomies.map(tax => tax.term_uid);
+ };
+
+ // Transform Contentstack events to match expected format
+ const transformedEvents = events.map(event => ({
+ title: event.title,
+ description: event.description,
+ location: event.location,
+ dateTime: getEventDateTimeString(event),
+ date: event.date,
+ recur: event.recur || [],
+ image: event.image?.url || null,
+ uid: event.uid,
+ audiences: getAudiences(event),
+ taxonomies: event.taxonomies || []
+ }));
+
+ // Use Contentstack events if available, otherwise fall back to hardcoded
+ const displayEvents = transformedEvents.length > 0 ? transformedEvents : resortEvents;
+
+ // Organize events by day for calendar view
+ const eventsByDay = {};
+ const weekDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+
+ weekDays.forEach(day => {
+ eventsByDay[day] = [];
+ });
+
+ displayEvents.forEach(event => {
+ const days = event.recur && event.recur.length > 0
+ ? event.recur
+ : getEventDays(event.dateTime);
+ days.forEach(day => {
+ if (eventsByDay[day]) {
+ eventsByDay[day].push(event);
+ }
+ });
+ });
+
+ // Sort events by time within each day (chronologically from earliest to latest)
+ Object.keys(eventsByDay).forEach(day => {
+ eventsByDay[day].sort((a, b) => {
+ // Use the date field for accurate time comparison
+ const dateA = a.date ? new Date(a.date) : null;
+ const dateB = b.date ? new Date(b.date) : null;
+
+ if (dateA && dateB) {
+ // Compare dates directly for accurate chronological sorting
+ return dateA.getTime() - dateB.getTime();
+ }
+
+ // Fallback to string comparison if dates aren't available
+ const timeA = a.date ? getEventTime(a.date) : getEventTime(a.dateTime);
+ const timeB = b.date ? getEventTime(b.date) : getEventTime(b.dateTime);
+ return timeA.localeCompare(timeB);
+ });
+ });
+
+ if (isLoading) return;
+
+ return(
+ <>
+
+
+
+
+
+ {entry?.headline}
+
+ {entry?.details && (
+
+ {entry?.details}
+
+ )}
+
+
+ {/* View Toggle */}
+
+
+
setViewMode('list')}
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
+ viewMode === 'list'
+ ? 'bg-gray-900 text-white'
+ : 'text-gray-700 hover:bg-gray-100'
+ }`}
+ >
+
+
+
setViewMode('calendar')}
+ className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
+ viewMode === 'calendar'
+ ? 'bg-gray-900 text-white'
+ : 'text-gray-700 hover:bg-gray-100'
+ }`}
+ >
+
+
+
+
+
+ {/* List View */}
+ {viewMode === 'list' && (
+
+
+ {displayEvents.map((event, index) => (
+
+ {/* Timeline indicator */}
+
+
+ {/* Image section */}
+
+
+
+
+ {/* Content section */}
+
+
+
+
+
+
+
{event.dateTime}
+
+
+
+
+
+
+
+
{event.location}
+
+
+ {event.audiences && event.audiences.length > 0 && (
+
+
+
+
+
{event.audiences.join(', ')}
+
+ )}
+
+
+
+ {event.title}
+
+
+
+ {event.description}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Calendar View */}
+ {viewMode === 'calendar' && (
+
+ {/* Day Headers Row */}
+
+ {weekDays.map((day) => (
+
+ ))}
+
+
+ {/* Events Grid */}
+
+ {weekDays.map((day) => (
+
+
+ {eventsByDay[day].length > 0 ? (
+ eventsByDay[day].map((event, eventIndex) => (
+
+
+ {event.date ? getEventTime(event.date) : getEventTime(event.dateTime)}
+
+
+ {event.title}
+
+
+ {event.location}
+
+ {event.audiences && event.audiences.length > 0 && (
+
+ {event.audiences.join(', ')}
+
+ )}
+
openModal(event)}
+ className="mt-auto text-left text-xs font-medium text-blue-600 hover:text-blue-800 underline self-start"
+ >
+ View Details
+
+
+ ))
+ ) : (
+
+
+ No events scheduled
+
+
+ )}
+
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Event Details Modal */}
+ {isModalOpen && selectedEvent && (
+
+ {/* Background overlay */}
+
+
+ {/* Modal panel */}
+
+
+ {/* Close button */}
+
+
+
+
+
+
+ {/* Image */}
+
+
+
+
+ {/* Content */}
+
+
+ {selectedEvent.title}
+
+
+
+
+
+
+
+
{selectedEvent.dateTime}
+
+
+
+
+
+
+
+
{selectedEvent.location}
+
+
+ {selectedEvent.audiences && selectedEvent.audiences.length > 0 && (
+
+
+
+
+
{selectedEvent.audiences.join(', ')}
+
+ )}
+
+
+
+
+ {selectedEvent.description}
+
+
+
+
+
+
+ )}
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/experiences/page.js b/src/app/[locale]/experiences/page.js
new file mode 100644
index 0000000..b6c1518
--- /dev/null
+++ b/src/app/[locale]/experiences/page.js
@@ -0,0 +1,46 @@
+"use client"
+import { useState, useEffect } from 'react';
+import { ContentstackClient } from '@/lib/contentstack-client'
+import Header from "@/components/header";
+import { useParams } from 'next/navigation';
+
+export default function Page({ }){
+ const [entry, setEntry] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const params = useParams();
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrl('experiences', '/experiences', params.locale);
+ setEntry(entry);
+ setIsLoading(false);
+ }
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ if(isLoading)
+ return;
+
+ return(
+
+ )
+}
diff --git a/src/app/[locale]/faq/[title]/page.js b/src/app/[locale]/faq/[title]/page.js
new file mode 100644
index 0000000..bbf9f05
--- /dev/null
+++ b/src/app/[locale]/faq/[title]/page.js
@@ -0,0 +1,79 @@
+"use client";
+import { useState, useEffect } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client";
+import Header from "@/components/header";
+import Footer from "@/components/footer";
+import { ChevronDownIcon } from '@heroicons/react/24/outline';
+import { useParams } from "next/navigation";
+
+export default function Page({ }) {
+ const [entry, setEntry] = useState({});
+ const [expandedId, setExpendedId] = useState("")
+ const params = useParams();
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrl(
+ "faq",
+ "/faq/" + params.title,
+ params.locale,
+ // [
+ // 'modular_blocks.hero_banner.hero_banner',
+ // 'modular_blocks.articles.articles'
+ // ]
+ );
+ setEntry(entry[0]);
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ function questionClicked(id) {
+ if (expandedId === id)
+ setExpendedId("");
+ else
+ setExpendedId(id);
+ }
+
+ return (
+
+
+
+
+
Frequently asked questions
+
+
+
+
+ {entry?.categories?.map((item, index) => (
+
+
{item.name}
+
+
+ {item.faqs?.map((faq, fIdx) => (
+
questionClicked(faq._metadata.uid)}>
+
+
+
+ ))}
+
+
+ ))}
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/faqs/[title]/page.js b/src/app/[locale]/faqs/[title]/page.js
new file mode 100644
index 0000000..ad58bb1
--- /dev/null
+++ b/src/app/[locale]/faqs/[title]/page.js
@@ -0,0 +1,106 @@
+"use client";
+import { useState, useEffect } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client";
+import Header from "@/components/header";
+import styled, {StyleSheetManager} from "styled-components";
+import isPropValid from "@emotion/is-prop-valid";
+import Footer from "@/components/footer";
+import { useRouter } from "next/navigation";
+import { ExclamationCircleIcon } from '@heroicons/react/24/outline';
+import { useParams } from "next/navigation";
+
+const HoverAnchor = styled.a`
+ &:hover {
+ background: ${props => props.hover};
+ color: ${props => props.hover_text};
+ };
+ background: ${props => props.bg};
+ color: ${props => props.text}
+`
+
+HoverAnchor.defaultProps = {
+ $hover: "#000000",
+ $hover_text: "#FFFFFF",
+ $bg: "#ABABAB",
+ $text: "#000000"
+}
+
+export default function Page({ }) {
+ const [search, setSearch] = useState("")
+ const [entry, setEntry] = useState({});
+ const [notFoundVisible, setNotFoundVisible] = useState(false)
+ const router = useRouter();
+ const params = useParams();
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrlWithRefs(
+ "faq",
+ "/faq/" + params.title,
+ params.locale,
+ [
+ ]
+ );
+ setEntry(entry);
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ async function searchForAnswer() {
+ const regex = new RegExp(search, 'i');
+ let found = false;
+ entry.categories.forEach((category) => {
+ category.faqs.forEach((faq) => {
+ if(regex.test(faq.question) || regex.test(faq.answer)){
+ if(!found)
+ router.push(params.title + "/section/" + category._metadata.uid + "/" + faq._metadata.uid);
+ found = true;
+ return;
+ }
+ })
+ })
+ if(!found)
+ setNotFoundVisible(true);
+ }
+
+ return (
+
+
+
+
+
+
{entry?.headline}
+
+ {setSearch(e.target.value); setNotFoundVisible(false)}}
+ placeholder={entry?.placeholder}
+ className="border py-3 px-4 w-[600px]"
+ {...entry?.$?.placeholder}
+ />
+
+ searchForAnswer()} >{entry?.button_text}
+
+
+
+
+
+ {entry?.categories?.map((item, index) => (
+
+
+ {item.name}
+
+
+ ))}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/faqs/[title]/section/[id]/[qid]/page.js b/src/app/[locale]/faqs/[title]/section/[id]/[qid]/page.js
new file mode 100644
index 0000000..d577574
--- /dev/null
+++ b/src/app/[locale]/faqs/[title]/section/[id]/[qid]/page.js
@@ -0,0 +1,70 @@
+"use client";
+import { useState, useEffect } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client";
+import Header from "@/components/header";
+import Footer from "@/components/footer";
+import Link from "next/link";
+import { useParams } from "next/navigation";
+
+export default function FaqSection({ }){
+ const [entry, setEntry] = useState({});
+ const [category, setCategory] = useState({});
+ const params = useParams();
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrlWithRefs(
+ "faq",
+ "/faq/" + params.title,
+ params.locale,
+ [
+ ]
+ );
+
+ const cat = entry?.categories?.find(c => c._metadata.uid === params.id)
+ setCategory(cat);
+ const q = cat.faqs.find(q => q._metadata.uid === params.qid);
+ setEntry(q);
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ return(
+
+
+
+
+
+
Red Panda Maldives
+
>
+
{category?.name}
+
+
+
+
+
+
{category?.name}
+ {category?.faqs?.map((item, index) => (
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/faqs/[title]/section/[id]/page.js b/src/app/[locale]/faqs/[title]/section/[id]/page.js
new file mode 100644
index 0000000..9f66873
--- /dev/null
+++ b/src/app/[locale]/faqs/[title]/section/[id]/page.js
@@ -0,0 +1,55 @@
+"use client";
+import { useState, useEffect } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client";
+import Header from "@/components/header";
+import Footer from "@/components/footer";
+import Link from "next/link";
+import { useParams } from "next/navigation";
+
+export default function FaqSection({ }){
+ const [entry, setEntry] = useState({});
+ const params = useParams();
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrlWithRefs(
+ "faq",
+ "/faq/" + params.title,
+ params.locale,
+ [
+ ]
+ );
+ const cat = entry?.categories?.find(c => c._metadata.uid === params.id)
+ setEntry(cat);
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ return(
+
+
+
+
+
+
Red Panda Maldives
+
>
+
{entry?.name}
+
+
+
{entry?.name}
+
+
+ {entry?.faqs?.map((item, index) => (
+
+ ))}
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/layout.jsx b/src/app/[locale]/layout.jsx
index 1f702f5..64f4703 100644
--- a/src/app/[locale]/layout.jsx
+++ b/src/app/[locale]/layout.jsx
@@ -13,7 +13,8 @@ const fetchData = cache(async (locale) => {
});
export const generateMetadata = async ({ params }) => {
- const { locale } = await params;
+ const parameters = await params;
+ const locale = parameters.locale;
const data = await fetchData(locale);
const entry = data?.[0];
@@ -36,7 +37,8 @@ export default async function RootLayout({
children,
params,
}) {
- const { locale } = await params;
+ const parameters = await params;
+ const locale = parameters.locale;
const data = await fetchData(locale);
return (
@@ -44,4 +46,4 @@ export default async function RootLayout({
{children}
);
-}
+}
\ No newline at end of file
diff --git a/src/app/[locale]/mobile/[title]/page.js b/src/app/[locale]/mobile/[title]/page.js
new file mode 100644
index 0000000..4da823a
--- /dev/null
+++ b/src/app/[locale]/mobile/[title]/page.js
@@ -0,0 +1,238 @@
+"use client"
+import {
+ useEffect,
+ useState,
+} from 'react';
+import BackgroundAndButtons from '@/components/mobile/backgroundAndButtons';
+import ButtonCTA from '@/components/mobile/buttonCTA';
+import IconGrid from '@/components/mobile/iconGrid';
+import { ContentstackClient } from "@/lib/contentstack-client"
+import { usePersonalize } from '@/context/personalize.context';
+import { createClient } from '@/utils/supabase/client';
+import { faUser } from '@awesome.me/kit-610837e1f9/icons/classic/light';
+import {
+ faBars,
+ faCheck,
+ faUser as loggedIn,
+} from '@awesome.me/kit-610837e1f9/icons/classic/solid';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import {
+ Popover,
+ PopoverButton,
+ PopoverPanel,
+} from '@headlessui/react';
+import Link from 'next/link';
+import { useParams } from 'next/navigation';
+
+export default function Mobile({ }) {
+ const [entry, setEntry] = useState({});
+ const [user, setUser] = useState({});
+ const [profiles, setProfiles] = useState([]);
+ const [selectedProfile, setSelectedProfile] = useState("");
+ const params = useParams();
+
+ const personalizeSDK = usePersonalize();
+
+ const supabase = createClient();
+
+ const getUser = async () => {
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ setUser(user);
+ return (user);
+ }
+
+ async function logout() {
+ if (user) {
+ await supabase.auth.signOut();
+ }
+ localStorage.setItem('profile', "");
+ await personalizeSDK.set({ "client_type": "" });
+
+ router.push("/account/login");
+ }
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrl(
+ "mobile",
+ "/mobile/" + params.title,
+ params.locale
+ );
+ setEntry(entry);
+ };
+
+ useEffect(() => {
+ async function getUserId(){
+ let currentUser = await getUser();
+ if(currentUser){
+ getProfiles(currentUser.id);
+ }
+ }
+ getUserId()
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ const getProfiles = async (user_id) => {
+ let result = await fetch(`/api/profiles/${user_id}`, {
+ method: 'GET',
+ })
+ .then((response) => {
+ if (response.ok)
+ return response.json();
+ else
+ return Promise.reject(response);
+ })
+ .then((result) => {
+ console.log('profiles', result);
+ let tempProfiles = [];
+ for (const profile of result.profiles) {
+ tempProfiles.push({
+ fname: profile.first_name,
+ lname: profile.last_name,
+ audience: profile.audience,
+ id: profile.id
+ })
+ }
+ setProfiles(tempProfiles);
+ let saved = localStorage.getItem('profile');
+ if (saved) {
+ setSelectedProfile(tempProfiles.find(p => p.audience === saved).fname);
+ }
+
+ })
+ .catch((error) => {
+ console.log(error);
+ })
+ }
+
+ const changeProfile = async (name) => {
+ setSelectedProfile(name);
+
+ if (name === ""){
+ localStorage.setItem('profile', "");
+ await personalizeSDK.set({ "client_type": "" });
+ }
+ else {
+ const profile = profiles.find(p => p.fname === name);
+ localStorage.setItem('profile', profile.audience);
+ await personalizeSDK.set({ "client_type": profile.audience });
+ }
+ window.location.reload();
+ }
+
+ return (
+
+
+
+
+ {!user &&
+
+
+
+
+
+
+
+ Log In
+
+
+
+ }
+ {user &&
+
+
+
+
+
+
+ changeProfile("")}
+ >
+
+ Anonymous
+
+ {profiles?.map((profile, index) => (
+ changeProfile(profile.fname)}
+ >
+
+ {profile.fname}
+
+ ))}
+
+ MANAGE PROFILES
+
+
+ LOG OUT
+
+
+
+ }
+
+
+
+
+
{entry?.header?.text}
+
{entry?.header?.button_text}
+
+
+
+
+ {entry?.modular_block_1?.map((block, index) => (
+
+ {block.hasOwnProperty('image') &&
+
+ {!block.image?.image?.url &&
+
+ }
+ {block.image?.image?.url &&
+
+ }
+
+ }
+ {block.hasOwnProperty('button_cta') &&
+
+ }
+ {block.hasOwnProperty('icon_grid') &&
+
+ }
+ {block.hasOwnProperty('background_and_buttons') &&
+
+ }
+
+ ))}
+
+
+
+ {entry?.modular_block_2?.map((block, index) => (
+
+ {block.hasOwnProperty('image') &&
+
+ }
+ {block.hasOwnProperty('button_cta') &&
+
+ }
+ {block.hasOwnProperty('icon_grid') &&
+
+ }
+
+ ))}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/page.js b/src/app/[locale]/page.js
new file mode 100644
index 0000000..c6c4bc9
--- /dev/null
+++ b/src/app/[locale]/page.js
@@ -0,0 +1,133 @@
+"use client";
+import { useState, useEffect } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client";
+import Cards from "@/components/cards";
+import Footer from "@/components/footer";
+import HalfSquares from "@/components/halfSquares";
+import Hero from "@/components/hero";
+import ImageGrid from "@/components/imageGrid";
+import Reviews from "@/components/reviews";
+import TextBlock from "@/components/textBlock";
+import ProductFeature from "@/components/productFeature";
+import Tabs from "@/components/tabs";
+import Marquee from "@/components/marquee";
+import LeadCapture from "@/components/leadCapture";
+import { createClient } from '@/utils/supabase/client'
+import CategoryBanner from "@/components/categoryBanner";
+import Agent from "@/components/agent";
+import RecommendationsBanner from "@/components/recommendationsBanner";
+import { useParams } from "next/navigation";
+
+export default function Home({ }) {
+ const [entry, setEntry] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const [user, setUser] = useState({});
+ const supabase = createClient();
+ const params = useParams();
+
+ const getUser = async () => {
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+ setUser(user);
+ }
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByTypeWithRefs(
+ "homepage",
+ params.locale,
+ [
+ 'modular_blocks.review.reference',
+ 'modular_blocks.image_grid.image.page',
+ 'hero.hero_banner',
+ 'hero.page',
+ 'modular_blocks.review.testimonials',
+ 'modular_blocks.review.testimonials.reviews.review',
+ 'modular_blocks.product_banner.plp',
+ 'modular_blocks.cards.card.page',
+ 'modular_blocks.text_and_image.page'
+ ]
+ );
+ console.log("homepage", entry[0]);
+ setEntry(entry[0]);
+ setIsLoading(false);
+ };
+
+ useEffect(() => {
+ getUser();
+ getContent();
+ ContentstackClient.onEntryChange(() => {
+ getContent();
+ });
+ }, []);
+
+ if (isLoading) return;
+
+ return (
+ <>
+
+
+
+ {entry?.modular_blocks.map((block, index) => (
+
+ {block.hasOwnProperty("text_block") && (
+
+ )}
+ {block.hasOwnProperty("cards") && (
+
+ )}
+ {block.hasOwnProperty("image_grid") && (
+
+ )}
+ {block.hasOwnProperty("review") && (
+
+ )}
+ {block.hasOwnProperty("text_and_image") && (
+
+ )}
+ {block.hasOwnProperty("product_banner") && (
+
+ )}
+ {block.hasOwnProperty("category_banner") && (
+
+ )}
+ {block.hasOwnProperty("tabs") && (
+
+ )}
+ {block.hasOwnProperty("marquee") && (
+
+ )}
+ {block.hasOwnProperty("lead_capture") && (
+
+ )}
+ {block.hasOwnProperty("agent") && (
+
+ )}
+ {block.hasOwnProperty("recommendations_banner") && (
+
+ )}
+
+ ))}
+
+
+
+
+ >
+ );
+}
diff --git a/src/app/[locale]/page.jsx b/src/app/[locale]/page.jsx
deleted file mode 100644
index 7c6cc3b..0000000
--- a/src/app/[locale]/page.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-"use client";
-import { useDataContext } from "@/context/data.context";
-import { ContentstackClient } from "@/lib/contentstack-client";
-import { useState, useEffect, use } from "react";
-
-export default function Home({ params }) {
- const { locale } = use(params);
- const initialData = useDataContext();
-
- 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);
- }
- }
-
- ContentstackClient.onEntryChange(fetchData);
- }, [locale, initialData]);
-
- return (
-
-
{entry?.title}
-
- );
-}
diff --git a/src/app/[locale]/page/[title]/page.js b/src/app/[locale]/page/[title]/page.js
new file mode 100644
index 0000000..d52a7ce
--- /dev/null
+++ b/src/app/[locale]/page/[title]/page.js
@@ -0,0 +1,59 @@
+"use client";
+import { useState, useEffect } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client"
+import PageHero from "@/components/pageHero";
+import TextSection from "@/components/textSection";
+import People from "@/components/people";
+import Hero from "@/components/hero";
+import LandingPageGrid from "@/components/landingPageGrid";
+import ArticleBanner from "@/components/articleBanner";
+import { useParams } from "next/navigation";
+
+
+export default function Page({ }) {
+ const [entry, setEntry] = useState({});
+ const [isKiosk, setIsKiosk] = useState(false);
+ const params = useParams();
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrlWithRefs(
+ "page",
+ "/page/" + params.title,
+ params.locale,
+ [
+ 'modular_blocks.hero_banner.hero_banner',
+ 'modular_blocks.articles.articles'
+ ]
+ );
+ setEntry(entry);
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ return (
+
+ {entry?.modular_blocks?.map((block, index) => {
+ if (block.hasOwnProperty("hero")) {
+ return
;
+ }
+ else if (block.hasOwnProperty("people")) {
+ return
;
+ }
+ else if (block.hasOwnProperty("text_block")) {
+ return
;
+ }
+ else if (block.hasOwnProperty("hero_banner")) {
+ return
+ }
+ else if (block.hasOwnProperty("image_grid")) {
+ return
+ }
+ else if (block.hasOwnProperty("articles")){
+ return
+ }
+ })}
+
+ );
+}
diff --git a/src/app/[locale]/pages/[title]/page.js b/src/app/[locale]/pages/[title]/page.js
new file mode 100644
index 0000000..3b0b5fd
--- /dev/null
+++ b/src/app/[locale]/pages/[title]/page.js
@@ -0,0 +1,121 @@
+"use client";
+import { useState, useEffect } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client"
+import Footer from "@/components/footer";
+import Header from "@/components/header";
+import PageHero from "@/components/pageHero";
+import TextSection from "@/components/textSection";
+import People from "@/components/people";
+import Hero from "@/components/hero";
+import ArticleBanner from "@/components/articleBanner";
+import ImageGrid from "@/components/imageGrid";
+import Tabs from "@/components/tabs";
+import Marquee from "@/components/marquee";
+import FormBuilder from "@/components/formBuilder";
+import Cards from "@/components/cards";
+import Reviews from "@/components/reviews";
+import CategoryBanner from "@/components/categoryBanner";
+import Agent from "@/components/agent";
+import LeadCapture from "@/components/leadCapture";
+import ProductFeature from "@/components/productFeature";
+import RecommendationsBanner from "@/components/recommendationsBanner";
+import { useParams } from "next/navigation";
+
+export default function Page({ }) {
+ const [entry, setEntry] = useState({});
+ const [isKiosk, setIsKiosk] = useState(false);
+ const params = useParams();
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrlWithRefs(
+ "page",
+ "/pages/" + params.title,
+ params.locale,
+ [
+ 'modular_blocks.hero.hero',
+ 'modular_blocks.hero_banner.hero',
+ 'modular_blocks.articles.articles',
+ 'modular_blocks.review.reference',
+ 'modular_blocks.image_grid.image.page',
+ 'modular_blocks.review.testimonials',
+ 'modular_blocks.review.testimonials.reviews.review',
+ 'modular_blocks.product_banner.plp',
+ 'modular_blocks.cards.card.page',
+ 'modular_blocks.text_and_image.page'
+ ]
+ );
+ setEntry(entry);
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ return (
+
+ {entry?._applied_variants?.title !== "cs76fdee0e83c5c333" &&
+
+ }
+
+ {entry?.modular_blocks?.map((block, index) => (
+
+ {block.hasOwnProperty("hero") &&
+
+ }
+ {block.hasOwnProperty("people") &&
+
+ }
+ {block.hasOwnProperty("text_block") &&
+
+ }
+ {block.hasOwnProperty("cards") &&
+
+ }
+ {block.hasOwnProperty("review") &&
+
+ }
+ {block.hasOwnProperty("product_banner") &&
+
+ }
+ {block.hasOwnProperty("category_banner") &&
+
+ }
+ {block.hasOwnProperty("lead_capture") &&
+
+ }
+ {block.hasOwnProperty("agent") &&
+
+ }
+ {block.hasOwnProperty("hero_banner") &&
+
+ }
+ {block.hasOwnProperty("image_grid") &&
+
+ }
+ {block.hasOwnProperty("articles") &&
+
+ }
+ {block.hasOwnProperty("tabs") &&
+
+ }
+ {block.hasOwnProperty("marquee") &&
+
+ }
+ {block.hasOwnProperty("form_builder") &&
+
+ }
+ {block.hasOwnProperty("recommendations_banner") && (
+
+ )}
+
+ ))}
+
+
+
+ {entry?._applied_variants?.title !== "cs76fdee0e83c5c333" &&
+
+ }
+
+
+ );
+}
diff --git a/src/app/[locale]/pdp/[id]/page.js b/src/app/[locale]/pdp/[id]/page.js
new file mode 100644
index 0000000..9d4e939
--- /dev/null
+++ b/src/app/[locale]/pdp/[id]/page.js
@@ -0,0 +1,457 @@
+"use client";
+import { useState, useEffect } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client"
+import Footer from "@/components/footer";
+import Header from "@/components/header";
+import { XMarkIcon } from "@heroicons/react/24/outline";
+import { ArrowLeftIcon } from "@heroicons/react/20/solid";
+import { useRouter } from "next/navigation";
+import { LyticsTracking, useEntity, useJstag } from "@/context/lyticsTracking";
+import { Dialog, DialogPanel, DialogBackdrop } from "@headlessui/react";
+import PageHero from "@/components/pageHero";
+import TextSection from "@/components/textSection";
+import People from "@/components/people";
+import Hero from "@/components/hero";
+import ArticleBanner from "@/components/articleBanner";
+import ImageGrid from "@/components/imageGrid";
+import Tabs from "@/components/tabs";
+import Marquee from "@/components/marquee";
+import FormBuilder from "@/components/formBuilder";
+import Cards from "@/components/cards";
+import Reviews from "@/components/reviews";
+import CategoryBanner from "@/components/categoryBanner";
+import Agent from "@/components/agent";
+import LeadCapture from "@/components/leadCapture";
+import ProductFeature from "@/components/productFeature";
+import RecommendationsBanner from "@/components/recommendationsBanner";
+import { useParams } from "next/navigation";
+import Link from "next/link";
+
+
+export default function Page({ }) {
+ const [entry, setEntry] = useState({});
+ const [product, setProduct] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const [imageIndex, setImageIndex] = useState(0);
+ const [purchaseOpen, setPurchaseOpen] = useState(0);
+ const [inputValue, setInputValue] = useState("");
+ const lyticsProfileData = useEntity();
+ const [isOpen, setIsOpen] = useState(false);
+ const jstag = useJstag();
+ const params = useParams();
+ const router = useRouter();
+
+ const handleGoBack = () => {
+ router.back();
+ };
+
+ const handleChange = (event) => {
+ event.preventDefault();
+ setInputValue(event.target.value);
+ };
+
+ function buyClick(price) {
+ jstag.send({
+ shopify_total_spend: price, //pulls price of product entry and increments field
+ _e: "purchase", //sends event named purchase to Data Cloud
+ email: inputValue, // pulls input value from form and sends to Data Cloud
+ });
+ jstag.call("resetPolling"); // resets polling to fetch profile quicker
+ setInputValue("");
+ }
+
+ function getRandomNumberBetween15And20() {
+ return Math.floor(Math.random() * (20 - 15 + 1)) + 15;
+ }
+
+
+ const getContent = async () => {
+ if (params.id === "untitled" || !params.id) return;
+ let theEntry = await ContentstackClient.getPDPbyProduct("pdp", params.id, params.locale);
+ if (!theEntry) {
+ theEntry = await ContentstackClient.getElementByUrlWithRefs(
+ "pdp",
+ "/pdp/" + params.id,
+ params.locale,
+ [
+ 'modular_blocks.hero.hero',
+ 'modular_blocks.hero_banner.hero',
+ 'modular_blocks.articles.articles',
+ 'modular_blocks.review.reference',
+ 'modular_blocks.image_grid.image.page',
+ 'modular_blocks.review.testimonials',
+ 'modular_blocks.review.testimonials.reviews.review',
+ 'modular_blocks.product_banner.plp',
+ 'modular_blocks.cards.card.page',
+ 'modular_blocks.text_and_image.page'
+ ]
+ );
+ if (theEntry?.product?.data.length > 0 && theEntry?.product?.data[0].url)
+ getProduct(theEntry.product.data[0].url);
+ else getProduct(params.id);
+ } else {
+ getProduct(params.id);
+ }
+
+ console.log("pdp", theEntry);
+
+ setEntry(theEntry);
+ if (theEntry || product) setIsLoading(false);
+ };
+
+ const getProduct = async (id) => {
+ console.log("fetching by url", id);
+ let result = await fetch("/api/products/" + id, {
+ method: "GET",
+ })
+ .then((response) => response.json())
+ .then((result) => {
+ console.log('fetch result', result);
+ setProduct(result.product);
+ setIsLoading(false);
+ })
+ .catch((error) => console.error(error));
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ useEffect(() => {
+ const tags = entry?.tags
+ if(tags){
+ Object.keys(tags).forEach(key => {
+ if(tags[key] === 'adventure'){
+ const number = getRandomNumberBetween15And20();
+ if ((!lyticsProfileData?.data?.user?.likely_premier_score) || (lyticsProfileData?.data?.user?.likely_premier_score <= 80)){
+ jstag.send({likely_premier_score: number});
+ jstag.call('resetPolling');
+ }
+ }
+ });
+ }
+ }, [entry]);
+
+ useEffect(() => {
+ //console.log("lytics use effect", lyticsProfileData);
+ if (lyticsProfileData?.data?.user?.segments?.length > 0) {
+ if (
+ lyticsProfileData.data.user.segments.includes("likely_premier_customer")
+ ) {
+ //console.log("found premier");
+ if (!isOpen && localStorage.getItem("offerShown") !== "true") {
+ setIsOpen(true);
+ localStorage.setItem("offerShown", "true");
+ }
+ }
+ }
+ }, [lyticsProfileData]);
+
+ //console.log(entry);
+ //console.log("product", product);
+ return (
+
+
+
+
+
+
+
handleGoBack()}
+ className="flex mb-4 items-center text-cyan-600 hover:text-[#D1A261]"
+ >
+
+ Back to previous products
+
+
+ {product?.images?.length > 0 && (
+
0
+ ? entry?.images[imageIndex].image?.url
+ : product?.images[imageIndex].path
+ }
+ />
+ )}
+
+
+ {(entry?.images?.length === 0 || !entry?.images) && (
+
+ {product?.images?.map((image, index) => (
+
setImageIndex(index)}
+ >
+
+
+ ))}
+
+ )}
+ {entry?.images?.length > 0 && (
+
+ {entry?.images?.map((image, index) => (
+
setImageIndex(index)}
+ >
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ {entry?.product_name ? entry?.product_name : product?.name}
+
+
+
{entry?.teaser ? entry?.teaser : product?.custom_data?.find(obj => obj.key === "teaser")?.value}
+
+ {(product?.price || entry?.price) && (
+
+ {entry?.price ? entry?.price : product?.price}
+
+ )}
+
+ setPurchaseOpen(true)}
+ >
+ Book Now
+
+
+
+
+
+
+
+ Description
+
+
+
+
+
+
+
setPurchaseOpen(false)}
+ />
+
+ Your purchase:
+
+
+ {entry?.product_name ? entry?.product_name : product?.name}
+
+
+
+
+ {product?.price && (
+
+
{entry?.price ? entry?.price : product?.price}
+
+ {entry?.price ? entry?.price : product?.price}
+
+
+ )}
+
+
+
+
+
+ {/* */}
+ Use card on file
+
+
+
+
+
...5309
+
+
+ buyClick(entry?.price ? entry?.price : product?.price)}
+ className="bg-black text-white rounded-lg py-4 w-full mt-10 font-semibold tracking-wide border-black border-2 hover:bg-white hover:text-black"
+ >
+ Complete Purchase
+
+
+
+
+
+
+
+
setIsOpen(false)}
+ className="relative z-50"
+ >
+
+
+
+
+
+
+
+
+
+
+
+ Exclusive Package Access
+
+
+ Red Panda Resort is your gateway to adventure. Explore luxury
+ packages for every level of adventurer
+
+
+
+ Show me
+
+
+
+
+
+
+
+
+
+ {entry?.modular_blocks?.map((block, index) => (
+
+ {block.hasOwnProperty("hero") && (
+
+ )}
+ {block.hasOwnProperty("people") && (
+
+ )}
+ {block.hasOwnProperty("text_block") && (
+
+ )}
+ {block.hasOwnProperty("cards") &&
+
+ }
+ {block.hasOwnProperty("review") &&
+
+ }
+ {block.hasOwnProperty("product_banner") &&
+
+ }
+ {block.hasOwnProperty("category_banner") &&
+
+ }
+ {block.hasOwnProperty("lead_capture") &&
+
+ }
+ {block.hasOwnProperty("agent") &&
+
+ }
+ {block.hasOwnProperty("hero_banner") && (
+
+ )}
+ {block.hasOwnProperty("image_grid") && (
+
+ )}
+ {block.hasOwnProperty("articles") && (
+
+ )}
+ {block.hasOwnProperty("tabs") && (
+
+ )}
+ {block.hasOwnProperty("marquee") && (
+
+ )}
+ {block.hasOwnProperty("form_builder") && (
+
+ )}
+ {block.hasOwnProperty("recommendations_banner") && (
+
+ )}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/app/[locale]/plp/[title]/page.js b/src/app/[locale]/plp/[title]/page.js
new file mode 100644
index 0000000..69b88b3
--- /dev/null
+++ b/src/app/[locale]/plp/[title]/page.js
@@ -0,0 +1,679 @@
+"use client";
+import { useState, useEffect, useRef } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client"
+import Footer from "@/components/footer";
+import Header from "@/components/header";
+import {
+ XMarkIcon,
+ ArrowRightIcon,
+ ArrowLeftIcon,
+} from "@heroicons/react/24/outline";
+import Link from "next/link";
+import PageHero from "@/components/pageHero";
+import TextSection from "@/components/textSection";
+import People from "@/components/people";
+import Hero from "@/components/hero";
+import ArticleBanner from "@/components/articleBanner";
+import ImageGrid from "@/components/imageGrid";
+import Tabs from "@/components/tabs";
+import Marquee from "@/components/marquee";
+import FormBuilder from "@/components/formBuilder";
+import Cards from "@/components/cards";
+import Reviews from "@/components/reviews";
+import CategoryBanner from "@/components/categoryBanner";
+import Agent from "@/components/agent";
+import LeadCapture from "@/components/leadCapture";
+import ProductFeature from "@/components/productFeature";
+import ResortPackage from "@/components/resortPackage";
+import { AnimatePresence, motion } from "framer-motion";
+import TypingIndicator from '@/components/typingIndicator';
+import { useJstag } from "@/context/lyticsTracking";
+import RecommendationsBanner from "@/components/recommendationsBanner";
+import { useParams } from "next/navigation";
+
+export default function PLP({ }) {
+ const [entry, setEntry] = useState({});
+ const [products, setProducts] = useState({});
+ const [category, setCategory] = useState({});
+ const [filterOpen, setFilterOpen] = useState(false);
+ const [itemOpen, setItemOpen] = useState(false);
+ const [filterText, setFilterText] = useState("Filter");
+ const [filter, setFilter] = useState([]);
+ const [counts, setCounts] = useState({});
+ const [chatOpen, setChatOpen] = useState(true);
+ const [messages, setMessages] = useState([]);
+ const [chatInput, setChatInput] = useState("");
+ const jstag = useJstag();
+ const params = useParams();
+
+ const messagesEndRef = useRef(null);
+
+ // Seed a welcome message from the assistant when chat opens for the first time
+ const INITIAL_ASSISTANT_MESSAGE =
+ "It looks like you’re interested in one of our adventure packages. Our most popular package is the 4 day adventure. Would you be interested in customizing your activities?";
+
+ useEffect(() => {
+ if (chatOpen && messages.length === 0) {
+ setMessages([{ role: "assistant", message: INITIAL_ASSISTANT_MESSAGE }]);
+ }
+ }, [chatOpen]);
+
+ const getContent = async () => {
+ const thePLP = await ContentstackClient.getPLPbyCategory(
+ "plp",
+ params.title,
+ params.locale
+ );
+ getCategoryByURL(params.title)
+ if (!thePLP) {
+ const entry = await ContentstackClient.getElementByUrlWithRefs(
+ "plp",
+ "/plp/" + params.title,
+ params.locale,
+ [
+ 'modular_blocks.hero.hero',
+ 'modular_blocks.hero_banner.hero',
+ 'modular_blocks.articles.articles',
+ 'modular_blocks.review.reference',
+ 'modular_blocks.image_grid.image.page',
+ 'modular_blocks.review.testimonials',
+ 'modular_blocks.review.testimonials.reviews.review',
+ 'modular_blocks.product_banner.plp',
+ 'modular_blocks.cards.card.page',
+ 'modular_blocks.text_and_image.page',
+ 'modular_blocks.resort_package.resort_package',
+ 'modular_blocks.resort_package.resort_package.products'
+ ]
+ );
+ //console.log('plp', entry);
+ setEntry(entry[0]);
+ if (entry[0]?.product_category?.data[0]?.id) {
+ getProducts(entry[0]?.product_category?.data.map(obj => obj.id));
+ }
+ else {
+ // setProducts({})
+ getProductsByURL(params.title)
+ }
+ } else {
+ setEntry(thePLP[0]);
+ if (thePLP[0]?.product_category?.data[0]?.id) {
+ getProducts(thePLP[0]?.product_category?.data.map(obj => obj.id));
+ }
+ else {
+ // setProducts({})
+ getProductsByURL(params.title)
+ }
+ }
+ };
+
+ const getCategoryByURL = async (id) => {
+ let result = await fetch('/api/category/' + id, {
+ method: "GET"
+ })
+ .then((response) => response.json())
+ .then((result) => {
+ if (result.category.length > 0) {
+ setCategory(result?.category[0]);
+ }
+ })
+ .catch((error) => console.error(error));
+ }
+
+ const getProducts = async (ids) => {
+ //console.log('id list', ids);
+ let tempProds = [];
+ for(const id of ids){
+ let result = await fetch('/api/categories/' + id, {
+ method: "GET"
+ })
+ .then((response) => response.json())
+ .then((result) => {
+ // let tempCounts = {};
+ // result.products?.forEach((product, index) => {
+ // const filters = product.custom_data?.filter?.split(' ');
+ // filters?.forEach((f, i) => {
+ // if(tempCounts[f])
+ // tempCounts[f] = tempCounts[f] + 1;
+ // else
+ // tempCounts[f] = 1;
+ // })
+ // })
+ // setCounts(tempCounts);
+
+ //setProducts(result);
+ tempProds.push(...result.products)
+ //console.log(result);
+ })
+ .catch((error) => console.error(error));
+ }
+ setProducts({products: tempProds})
+ }
+
+ const getProductsByURL = async (id) => {
+ let result = await fetch('/api/category/' + id, {
+ method: "GET"
+ })
+ .then((response) => response.json())
+ .then((result) => {
+ if (result.category.length > 0) {
+ fetch('/api/categories/' + result.category[0].id, {
+ method: "GET"
+ })
+ .then((response) => response.json())
+ .then((result) => {
+ setProducts(result);
+ })
+ .catch((error) => console.error(error));
+ }
+ })
+ .catch((error) => console.error(error));
+ }
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ jstag.send({lead_score: 25});
+ jstag.call('resetPolling');
+ //fetchTaxonomyContent("6_day");
+ }, [])
+
+ const handleFilterChange = (item, isOn) => {
+ if (isOn) {
+ let temp = [...filter];
+ temp.push(item);
+ setFilter(temp);
+ } else {
+ const fils = filter.filter((f) => f !== item);
+ setFilter(fils);
+ }
+ };
+ const isInFilter = (terms) => {
+ if (filter.length === 0) return true;
+ if (!terms) return false;
+ let filterTerms = terms.split(" ");
+ for (let x = 0; x < filterTerms.length; x++) {
+ if (filter.includes(filterTerms[x])) return true;
+ }
+ };
+ //console.log("entry", entry)
+ //console.log(category)
+ // console.log(products)
+
+ useEffect(() => {
+ if (messagesEndRef.current) {
+ messagesEndRef.current.scrollTop = messagesEndRef.current.scrollHeight;
+ }
+ }, [messages]);
+
+ const handleEnterKey = async (e) => {
+ jstag.call('resetPolling');
+ if (e.key === "Enter" && chatInput.trim()) {
+ const userText = chatInput.trim();
+ const newConversation = [
+ ...messages,
+ { role: "user", message: userText },
+ ];
+ //setMessages(newConversation);
+ const typing = [
+ ...newConversation, {role: "waiting"}
+ ]
+ setMessages(typing);
+ setChatInput("");
+
+ // Call chatbot API with mapped messages ({ role, content })
+ try {
+ const response = await fetch('/api/chatbot', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ messages: newConversation.map(({ role, message }) => ({ role, content: message }))
+ })
+ });
+ const data = await response.json();
+
+ let assistantText = null;
+ if (Array.isArray(data)) {
+ const tool = data.find((d) => d?.type === 'tool_use' && d?.name === 'json_response');
+ //console.log('tool', tool);
+ //fetchTaxonomyContent(tool?.input?.term, tool.input.replaced_term);
+ assistantText = tool?.input?.response ?? null;
+
+ const term = tool?.input?.term ?? null;
+ const replaced_term = tool?.input?.replaced_term ?? null;
+ console.log('term', term);
+ console.log('replaced_term', replaced_term);
+
+ if (term && replaced_term) {
+ fetchTaxonomyContent(term, replaced_term);
+ if(term === 'scuba'){
+ jstag.send({padi_certified: true});
+ jstag.call('resetPolling');
+ }
+ }
+ } else if (data && typeof data === 'object') {
+ // fallback if the automation returns an object with an output property
+ assistantText = data.output ?? null;
+ }
+
+ setMessages((prev) => [
+ ...prev.slice(0, -1),
+ { role: 'assistant', message: assistantText || 'Sorry, I could not understand the response.' }
+ ]);
+ } catch (err) {
+ setMessages((prev) => [
+ ...prev,
+ { role: 'assistant', message: 'Something went wrong. Please try again.' }
+ ]);
+ }
+ }
+ };
+
+ const fetchTaxonomyContent = async (term, replaceTerm) => {
+ try {
+ if (term === "generic") return;
+
+ const scrollY = window.scrollY;
+
+ const result = await Stack.getElementsByTaxonomy(params.locale, term, [
+ "products",
+ ]);
+ console.log(result[0][0]);
+ console.log('terms', term, replaceTerm);
+ let tempEntry = { ...entry };
+ if (result[0][0]._content_type_uid === "resort_package") {
+ let foundIndex = entry.modular_blocks.findIndex((comp) =>
+ comp.hasOwnProperty("resort_package")
+ );
+ let obj = {};
+ obj["resort_package"] = {};
+ obj["resort_package"]["resort_package"] = [result[0][0]];
+ obj["wasReplaced"] = true;
+
+ if (foundIndex > -1) tempEntry.modular_blocks[foundIndex] = obj;
+ else tempEntry.modular_blocks = [...tempEntry.modular_blocks, obj];
+ } else if (result[0][0]._content_type_uid === "pdp") {
+ let tempEntry = { ...entry }; // clone entry
+
+ let foundIndex = tempEntry.modular_blocks.findIndex((comp) =>
+ comp.hasOwnProperty("resort_package")
+ );
+
+ if (foundIndex > -1) {
+ let resortPackageBlock = tempEntry.modular_blocks[foundIndex];
+ let products =
+ resortPackageBlock.resort_package.resort_package[0].products;
+
+ // Find index of product to replace
+ let termUids = products.map((pdp) =>
+ pdp.taxonomies && pdp.taxonomies.length > 0
+ ? pdp.taxonomies[0].term_uid
+ : null
+ );
+ let foundPdpIndex = termUids.indexOf(replaceTerm);
+
+ if (foundPdpIndex > -1) {
+ // Replace the specific product
+ products[foundPdpIndex] = result[0][0];
+ resortPackageBlock.wasReplaced = true;
+ }
+ }
+ }
+ setEntry(tempEntry);
+ //console.log("temp test", tempEntry);
+ requestAnimationFrame(() => window.scrollTo(0, scrollY));
+ } catch (e) {
+ console.log("error:", e);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+ {entry?.modular_blocks?.map((block, index) => {
+ const metadata = entry?.$?.[`modular_blocks__${index}`];
+ const uniqueKey = `${Object.keys(block)[0]}_${index}_${
+ block.wasReplaced ? "replaced" : "original"
+ }`;
+
+ const blockContent = (
+ <>
+ {block.hasOwnProperty("resort_package") && (
+
p.uid).join("_")}`}
+ // initial={{ opacity: 0, y: 20 }}
+ // animate={{ opacity: 1, y: 0 }}
+ // exit={{ opacity: 0, y: -20 }}
+ // transition={{ duration: 0.5 }}
+ >
+
+
+ )}
+ {block.hasOwnProperty("hero") && (
+
+ )}
+ {block.hasOwnProperty("people") && (
+
+ )}
+ {block.hasOwnProperty("text_block") && (
+
+ )}
+ {block.hasOwnProperty("cards") && (
+
+ )}
+ {block.hasOwnProperty("review") && (
+
+ )}
+ {block.hasOwnProperty("product_banner") && (
+
+ )}
+ {block.hasOwnProperty("category_banner") && (
+
+ )}
+ {block.hasOwnProperty("lead_capture") && (
+
+ )}
+ {block.hasOwnProperty("agent") && (
+
+ )}
+ {block.hasOwnProperty("hero_banner") && (
+
+ )}
+ {block.hasOwnProperty("image_grid") && (
+
+ )}
+ {block.hasOwnProperty("articles") && (
+
+ )}
+ {block.hasOwnProperty("tabs") && (
+
+ )}
+ {block.hasOwnProperty("marquee") && (
+
+ )}
+ {block.hasOwnProperty("form_builder") && (
+
+ )}
+ {block.hasOwnProperty("recommendations_banner") && (
+
+ )}
+ >
+ );
+
+ return block.wasReplaced ? (
+
+ {blockContent}
+
+ ) : (
+
+ {blockContent}
+
+ );
+ })}
+
+
+ {/* {entry?.image?.url &&
+
+
+ } */}
+
+ {entry?.headline ? entry?.headline : category?.name}
+
+ {/*
setFilterOpen(true)}
+ >
+
+
*/}
+
+
+
{
+ setItemOpen(false);
+ setFilterText("Filter");
+ }}
+ />
+ {filterText}
+ setFilterOpen(false)}
+ />
+
+
+
+ {entry?.filters
+ ?.filter((f) => f.category === filterText)
+ .map((option, index) => (
+
+ {option.option_items.map((optionItem, itemIndex) => (
+
+
+
+ handleFilterChange(
+ optionItem.product_category_value,
+ e.target.checked
+ )
+ }
+ />
+
{optionItem.option_name}
+
+
+ [{" "}
+ {counts[optionItem.product_category_value]
+ ? counts[optionItem.product_category_value]
+ : 0}{" "}
+ ]
+
+
+ ))}
+
+ ))}
+
+ {entry?.filters?.map((filter, index) => (
+
{
+ setItemOpen(true);
+ setFilterText(filter.category);
+ }}
+ >
+
+ {filter.category}
+
+
+
+ ))}
+
+
+ Reset
+
+
+
setFilterOpen(false)}
+ >
+
+ {products?.products?.map((item, index) => {
+ if (isInFilter(item?.custom_data?.filter)) {
+ return (
+
+
+
+
+ {/*
+
1 ? item.images[1].url : ""} />
+
*/}
+
+
+
{item?.name}
+
{item.custom_data?.find(obj => obj.key === "teaser")?.value}
+
+ {item?.price}
+
+
+
+
+
{item?.name}
+
{item.price}
+
+
+ );
+ }
+ })}
+
+
+
+
+ {!chatOpen && entry?.show_agent && (
+
setChatOpen(true)}
+ >
+
Create Your Dream Vacation
+
+ )}
+
+ {chatOpen && entry?.show_agent && (
+
+
+
Concierge
+
setChatOpen(false)}
+ >
+
+
+
+
+
+ {messages.map((message, index) => (
+
+
+ {message.role === "waiting" &&
+
+ }
+
+ {message.message}
+
+
+
+ ))}
+
+
+
+ setChatInput(e.target.value)}
+ placeholder="How can I help?"
+ />
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/[locale]/profiles/page.js b/src/app/[locale]/profiles/page.js
new file mode 100644
index 0000000..11fbb6e
--- /dev/null
+++ b/src/app/[locale]/profiles/page.js
@@ -0,0 +1,324 @@
+"use client"
+import { createClient } from '@/utils/supabase/client'
+import Header from "@/components/header";
+import { useState, useEffect } from 'react';
+import { ContentstackClient } from '@/lib/contentstack-client'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faCircleUser, faTrash, faPlus, faCheck } from "@awesome.me/kit-610837e1f9/icons/classic/solid";
+import { useParams } from 'next/navigation';
+
+export default function Profiles({ }) {
+ const [profiles, setProfiles] = useState([]);
+ const [user, setUser] = useState({});
+ const [audiences, setAudiences] = useState([]);
+ const [saving, setSaving] = useState(-1);
+ const [deleting, setDeleting] = useState(-1);
+ const supabase = createClient();
+ const params = useParams();
+
+ const getUser = async () => {
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ setUser(user);
+ return user.id;
+ }
+
+ const getAudiences = async () => {
+ const entry = await ContentstackClient.getElementByType("config", "en");
+ setAudiences(entry[0][0].audience);
+ }
+
+ const getProfiles = async (user_id) => {
+ let result = await fetch(`/api/profiles/${user_id}`, {
+ method: 'GET',
+ })
+ .then((response) => {
+ if (response.ok)
+ return response.json();
+ else
+ return Promise.reject(response);
+ })
+ .then((result) => {
+ let tempProfiles = [];
+ for(const profile of result.profiles){
+ tempProfiles.push({
+ fname: profile.first_name,
+ lname: profile.last_name,
+ audience: profile.audience,
+ id: profile.id,
+ image: profile.avatar_url ? profile.avatar_url : '',
+ file: ''
+ })
+ }
+ setProfiles(tempProfiles);
+ })
+ .catch((error) => {
+ console.log(error);
+ })
+ }
+
+ useEffect(() => {
+ getAudiences();
+ async function getUserId(){
+ let currentUser = await getUser();
+ getProfiles(currentUser);
+ }
+ getUserId()
+ }, []);
+
+ const handleFieldChange = (id, key, value, checked) => {
+ if(key === 'audience'){
+ let auds = profiles.find(p => p.id === id).audience;
+ if(checked)
+ auds += "|" + value;
+ else
+ auds = auds.replace('|' + value, '');
+ setProfiles(profiles =>
+ profiles.map(profile =>
+ profile.id === id ? { ...profile, ['audience']: auds } : profile
+ )
+ )
+ }
+ else{
+ setProfiles(profiles =>
+ profiles.map(profile =>
+ profile.id === id ? { ...profile, [key]: value } : profile
+ )
+ )
+ }
+ }
+
+ const addProfile = () => {
+ setProfiles([...profiles,
+ {
+ id: Math.random(),
+ fname: '',
+ lname: '',
+ audience: '',
+ isNew: true,
+ file: null,
+ image: ''
+ }
+ ])
+ }
+
+ const deleteProfile = async (id) => {
+ var index = profiles.findIndex(p => p.id === id);
+ if(profiles[index].isNew){
+ const temp = profiles;
+ temp.splice(index, 1);
+ setProfiles([...temp]);
+ }
+ else{
+ let result = await fetch(`/api/profiles/${user.id}/${id}`, {
+ method: "DELETE",
+ })
+ .then((response) => {
+ if (response.ok)
+ return response.json();
+ else
+ return Promise.reject(response);
+ })
+ .then((result) => {
+ if (result.result === 'error')
+ console.log('error', result);
+ else {
+ const temp = profiles;
+ temp.splice(index, 1);
+ setProfiles([...temp]);
+ }
+ setDeleting(-1);
+ })
+ .catch((error) => {
+ console.log('error', error);
+ setDeleting(-1);
+ })
+ }
+ }
+
+ const saveProfile = async (id) => {
+ setSaving(id);
+ const profile = profiles.find(p => p.id === id);
+
+ const formData = new FormData();
+ formData.append('profile', JSON.stringify({
+ fname: profile.fname,
+ lname: profile.lname,
+ id: profile.id,
+ audience: profile.audience,
+ isNew: profile.isNew
+ }))
+
+ if(profile.file)
+ formData.append('file', profile.file, profile.file.name);
+
+ let result = await fetch(`/api/profiles/${user.id}`, {
+ method: "POST",
+ body: formData,
+ })
+ .then((response) => {
+ if (response.ok)
+ return response.json();
+ else
+ return Promise.reject(response);
+ })
+ .then((result) => {
+ if (result.result === 'error')
+ console.log('error', result);
+ else {
+ if(profile.isNew){
+ handleFieldChange(id, 'id', result.profiles[0].id);
+ }
+ }
+ setTimeout(() => {
+ setSaving(-1);
+ }, 1000)
+ })
+ .catch((error) => {
+ console.log('error', error);
+ setTimeout(() => {
+ setSaving(-1);
+ }, 1000)
+ })
+ }
+
+ const testFunc = (id) => {
+ console.log('this is the id', id);
+ }
+
+ return (
+
+
+
+
+
+
+
You must be logged in.
+
+
+
+
+ {profiles.map((profile, index) => (
+
+
+
{profile.fname === "" ? (profile.isNew ? "New Profile": "Enter Name") : profile.fname}
+
+
Are you sure?
+ {deleting === profile.id &&
+
deleteProfile(profile.id)}
+ className="text-white hover:text-red-400"
+ >
+
+
+ }
+ {deleting !== profile.id &&
+
setDeleting(profile.id)}
+ className="text-white hover:text-red-400"
+ >
+
+
+ }
+
+
+
+
+
First Name*
+
+ handleFieldChange(profile.id, 'fname', e.target.value)}
+ className="border p-2 w-full"
+ />
+
+
+
+
+
Last Name
+
+ handleFieldChange(profile.id, 'lname', e.target.value)}
+ className="border p-2 w-full"
+ />
+
+
+
+
+
+
+ {profile.image !== '' &&
+
+ }
+ {profile.image === '' &&
+
+ }
+
+
{
+ console.log('in on change', index, profiles[index].id);
+ let selectedFiles = e.target.files;
+ if (selectedFiles && selectedFiles[0]) {
+ let blobUrl = URL.createObjectURL(selectedFiles[0]);
+ handleFieldChange(profile.id, 'image', blobUrl);
+ handleFieldChange(profile.id, 'file', e.target.files[0]);
+ }
+ }}
+ />
+
+
+
+
+
Audiences
+ {audiences.map((audience, audienceIdx) => (
+
+ handleFieldChange(profile.id, 'audience', e.target.value, e.target.checked)}
+ />
+ {audience}
+
+ ))}
+
+
+ saveProfile(profile.id)}
+ className="border border-blue-500 rounded py-2 px-5 hover:bg-blue-500 hover:text-white"
+ >
+ Save
+
+
+
+
+ ))}
+
+
+ addProfile()}
+ className="text-neutral-700 border border-neutral-700 rounded py-2 px-16"
+ >
+
+
+
+
+
+
+ )
+}
+
diff --git a/src/app/[locale]/rewards/page.js b/src/app/[locale]/rewards/page.js
new file mode 100644
index 0000000..fbdd715
--- /dev/null
+++ b/src/app/[locale]/rewards/page.js
@@ -0,0 +1,175 @@
+"use client"
+import {
+ Fragment,
+ useEffect,
+ useState,
+} from 'react';
+import Footer from '@/components/footer';
+import Header from '@/components/header';
+import { ContentstackClient } from '../../../lib/contentstack-client';
+import { usePersonalize } from '@/context/personalize.context';
+import {
+ Dialog,
+ Transition,
+} from '@headlessui/react';
+import { CheckIcon } from '@heroicons/react/24/outline';
+import { useParams } from 'next/navigation';
+
+export default function Page({ }) {
+ const [isLoading, setIsLoading] = useState(true);
+ const [dialogOpen, setDialogOpen] = useState(false);
+ const [category, setCategory] = useState('None');
+ const [entry, setEntry] = useState({});
+ const params = useParams();
+ const personalizeSDK = usePersonalize()
+
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrl('rewards', '/rewards', params.locale);
+ setEntry(entry[0]);
+ setIsLoading(false);
+ }
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ async function submit (e) {
+ e.preventDefault();
+
+ await personalizeSDK.set({"client_type": category})
+ setDialogOpen(true);
+ }
+
+ if(isLoading)
+ return;
+
+ return (
+
+
+ {console.log(entry)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{entry?.submit_modal?.headline}
+
+
{entry?.submit_modal?.body}
+
+
+
+
+ setDialogOpen(false)}
+ >
+ {entry?.submit_modal?.button_text}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{entry?.headline}
+
{entry?.body}
+
+
+
+
+
+
{entry?.form?.headline}
+
{entry?.form?.body}
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/section/[id]/[qid]/page.js b/src/app/[locale]/section/[id]/[qid]/page.js
new file mode 100644
index 0000000..f38678d
--- /dev/null
+++ b/src/app/[locale]/section/[id]/[qid]/page.js
@@ -0,0 +1,68 @@
+"use client";
+import { useState, useEffect } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client"
+import Header from "@/components/header";
+import Footer from "@/components/footer";
+import Link from "next/link";
+
+export default function FaqSection({ params }){
+ const [entry, setEntry] = useState({});
+ const [category, setCategory] = useState({});
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrlWithRefs(
+ "faq",
+ "/faq/" + params.title,
+ params.locale,
+ [
+ ]
+ );
+
+ const cat = entry?.categories?.find(c => c._metadata.uid === params.id)
+ setCategory(cat);
+ const q = cat.faqs.find(q => q._metadata.uid === params.qid);
+ setEntry(q);
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ return(
+
+
+
+
+
+
Red Panda Maldives
+
>
+
{category?.name}
+
+
+
+
+
+
{category?.name}
+ {category?.faqs?.map((item, index) => (
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/section/[id]/page.js b/src/app/[locale]/section/[id]/page.js
new file mode 100644
index 0000000..9bac032
--- /dev/null
+++ b/src/app/[locale]/section/[id]/page.js
@@ -0,0 +1,53 @@
+"use client";
+import { useState, useEffect } from "react";
+import { ContentstackClient } from "@/lib/contentstack-client";
+import Header from "@/components/header";
+import Footer from "@/components/footer";
+import Link from "next/link";
+
+export default function FaqSection({ params }){
+ const [entry, setEntry] = useState({});
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByUrlWithRefs(
+ "faq",
+ "/faq/" + params.title,
+ params.locale,
+ [
+ ]
+ );
+ const cat = entry?.categories?.find(c => c._metadata.uid === params.id)
+ setEntry(cat);
+ };
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ }, []);
+
+ return(
+
+
+
+
+
+
Red Panda Maldives
+
>
+
{entry?.name}
+
+
+
{entry?.name}
+
+
+ {entry?.faqs?.map((item, index) => (
+
+ ))}
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/api/categories/[id]/route.js b/src/app/api/categories/[id]/route.js
new file mode 100644
index 0000000..52699d0
--- /dev/null
+++ b/src/app/api/categories/[id]/route.js
@@ -0,0 +1,19 @@
+import { createClient } from "@/utils/supabase/server"
+
+export async function GET(request, { params }) {
+ const supabase = await createClient()
+ const { id } = await params;
+
+ console.log("params", id)
+ const { data: products, error } = await supabase.rpc(
+ "get_products_by_category_with_custom_data",
+ { categoryid: id}
+ );
+
+ if (error) {
+ console.log("Error getting product:", error);
+ return Response.json({ error: error }, { status: 500 });
+ } else {
+ return Response.json({ products: products });
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/category/[id]/route.js b/src/app/api/category/[id]/route.js
new file mode 100644
index 0000000..7ee7156
--- /dev/null
+++ b/src/app/api/category/[id]/route.js
@@ -0,0 +1,19 @@
+import { createClient } from "@/utils/supabase/server";
+
+export async function GET(request, { params }) {
+ const supabase = await createClient();
+ const { id } = await params;
+
+ const { data: category, error } = await supabase
+ .from("categories")
+ .select("id, name, description, url")
+ .eq("url", id);
+
+ if (error) {
+ console.log("Error getting product:", error);
+ return Response.json({ error: error }, { status: 500 });
+ } else {
+ return Response.json({ category: category });
+ }
+}
+
diff --git a/src/app/api/chatbot/route.js b/src/app/api/chatbot/route.js
new file mode 100644
index 0000000..1b2fe3b
--- /dev/null
+++ b/src/app/api/chatbot/route.js
@@ -0,0 +1,34 @@
+export async function POST(req) {
+ try {
+ const body = await req.json();
+ const messages = Array.isArray(body?.messages) ? body.messages : [];
+
+ const payload = { messages };
+
+ const headers = {
+ 'Content-Type': 'application/json',
+ };
+
+ // Optional auth header if your automation requires it
+ if (process.env.AUTOMATIONS_API_TOKEN) {
+ headers['Authorization'] = `Bearer ${process.env.AUTOMATIONS_API_TOKEN}`;
+ }
+
+ const automationUrl = process.env.CONTENTSTACK_AUTOMATIONS_API_URL;
+
+ const response = await fetch(automationUrl, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(payload),
+ });
+
+ const data = await response.json();
+ return Response.json(data, { status: response.ok ? 200 : response.status });
+ } catch (error) {
+ return Response.json(
+ { error: 'Failed to call chatbot automation', details: String(error) },
+ { status: 500 }
+ );
+ }
+}
+
diff --git a/src/app/api/contentstack/getElementByTypeByTaxonomy/route.js b/src/app/api/contentstack/getElementByTypeByTaxonomy/route.js
index 793ae42..15c9ade 100644
--- a/src/app/api/contentstack/getElementByTypeByTaxonomy/route.js
+++ b/src/app/api/contentstack/getElementByTypeByTaxonomy/route.js
@@ -2,6 +2,7 @@ import ContentstackServer from "@/lib/cstack";
export async function POST(request) {
try {
+ console.log("route running")
const variantParam = request.headers.get('x-personalize-variants');
const { type, locale, term, live_preview } = await request.json();
const res = await ContentstackServer.getElementByTypeByTaxonomy(type, locale, term, live_preview, variantParam);
diff --git a/src/app/api/formBuilder/route.js b/src/app/api/formBuilder/route.js
new file mode 100644
index 0000000..68a3a25
--- /dev/null
+++ b/src/app/api/formBuilder/route.js
@@ -0,0 +1,20 @@
+import { NextResponse } from "next/server";
+
+export async function POST(req, res){
+ const formdata = await req.formData();
+ console.log(formdata);
+
+ var object = {};
+ formdata.forEach((value, key) => object[key] = value);
+ var json = JSON.stringify(object);
+
+ let automateResponse = await fetch('https://app.contentstack.com/automations-api/run/63c03644823b435f93ea88e04d5a7f20', {
+ method: 'POST',
+ body: json
+ })
+
+ let response = await automateResponse.json();
+
+ return NextResponse.json({result: "success"});
+
+}
\ No newline at end of file
diff --git a/src/app/api/products/[id]/route.js b/src/app/api/products/[id]/route.js
new file mode 100644
index 0000000..c277953
--- /dev/null
+++ b/src/app/api/products/[id]/route.js
@@ -0,0 +1,29 @@
+import { createClient } from "@/utils/supabase/server";
+
+export async function GET(request, { params }) {
+ const supabase = await createClient();
+ const { id } = await params;
+
+ console.log("params", id);
+
+ const { data: products, error } = await supabase.rpc("get_product_by_url", {
+ itemurl: id,
+ storeid: process.env.RED_PANDA_COMMERCE_ID ? process.env.RED_PANDA_COMMERCE_ID : "eda80b80-465f-4735-b0bf-c737c7bf6075"
+ });
+
+ if (products.length > 0 ) {
+ const { data: images, images_error } = await supabase
+ .from("product_images")
+ .select("path, ordernum")
+ .order("ordernum", {ascending: true})
+ .eq("product_id", products[0].product_id);
+
+ products[0].images = images
+ }
+ if (error) {
+ console.log("Error getting product:", error);
+ return Response.json({ error: error }, { status: 500 });
+ } else {
+ return Response.json({ product: products[0] });
+ }
+}
diff --git a/src/app/api/profiles/[user_id]/[profile_id]/route.js b/src/app/api/profiles/[user_id]/[profile_id]/route.js
new file mode 100644
index 0000000..e3b1748
--- /dev/null
+++ b/src/app/api/profiles/[user_id]/[profile_id]/route.js
@@ -0,0 +1,20 @@
+import { NextResponse } from "next/server";
+import { createClient } from '@/utils/supabase/server'
+
+export async function DELETE(req, { params }) {
+ const supabase = await createClient()
+
+ const { profile_id } = await params;
+
+ const { data: profiles, error, status } = await supabase
+ .from('profiles')
+ .delete()
+ .eq('id', profile_id);
+
+ if (error) {
+ console.log('Error deleting profiles:', error);
+ return NextResponse.json({ result: "error", error: error });
+ }
+ else
+ return NextResponse.json({ profiles: profiles });
+}
\ No newline at end of file
diff --git a/src/app/api/profiles/[user_id]/route.js b/src/app/api/profiles/[user_id]/route.js
new file mode 100644
index 0000000..2b69287
--- /dev/null
+++ b/src/app/api/profiles/[user_id]/route.js
@@ -0,0 +1,100 @@
+import { NextResponse } from "next/server";
+import { createClient } from '@/utils/supabase/client'
+
+export async function GET(req, { params }) {
+ const supabase = await createClient()
+
+ const { user_id } = await params;
+
+ const { data: profiles, error, status } = await supabase
+ .from('profiles')
+ .select('id, first_name, last_name, avatar_url, audience')
+ .eq('user_id', user_id)
+ .order('updated_at', { ascending: true });
+
+ if (error) {
+ console.log('Error getting profiles:', error);
+ return NextResponse.json({ result: "error", error: error });
+ }
+ else
+ return NextResponse.json({ profiles: profiles });
+}
+
+export async function POST(req, { params }) {
+ const supabase = await createClient()
+
+ const { user_id } = await params;
+ const form = await req.formData();
+ const profile = JSON.parse(form.get('profile'));
+ const file = form.get('file');
+
+ let path = '';
+ if(file){
+ const ext = file.name.split('.').pop();
+ const randomFile = Math.floor(Math.random() * (99999999 - 10000000 + 1)) + 10000000;
+ path = `${randomFile}.${ext}`
+
+ const { error: uploadError} = await supabase.storage.from(user_id).upload(path, file);
+
+ if (uploadError?.statusCode === '404') {
+ const {data: bucket, error: bucketError} = await supabase.storage.createBucket(user_id, {
+ public: true
+ })
+ const { error: uploadError2} = await supabase.storage.from(user_id).upload(path, file);
+ if(uploadError2)
+ console.log('error', uploadError2);
+ }
+ }
+
+ if (profile.isNew) {
+ const { data: profile_id, error, status } = await supabase
+ .from('profiles')
+ .insert({
+ first_name: profile.fname,
+ last_name: profile.lname,
+ audience: profile.audience,
+ user_id: user_id,
+ avatar_url: file ? `https://wtxrbukgejhzplfwzamn.supabase.co/storage/v1/object/public/${user_id}/${path}` : null
+ })
+ .select('id');
+
+ if (error) {
+ console.log('Error adding profile:', error);
+ return NextResponse.json({ result: "error", error: error });
+ }
+ else
+ return NextResponse.json({ profiles: profile_id });
+ }
+ else {
+ let upObj = {}
+ if(file){
+ upObj = {
+ first_name: profile.fname,
+ last_name: profile.lname,
+ audience: profile.audience,
+ user_id: user_id,
+ avatar_url: `https://wtxrbukgejhzplfwzamn.supabase.co/storage/v1/object/public/${user_id}/${path}`
+ }
+ }
+ else{
+ upObj = {
+ first_name: profile.fname,
+ last_name: profile.lname,
+ audience: profile.audience,
+ user_id: user_id
+ }
+ }
+
+ const { data: profile_result, error, status } = await supabase
+ .from('profiles')
+ .update(upObj)
+ .eq('id', profile.id)
+
+ if (error) {
+ console.log('Error adding profile:', error);
+ return NextResponse.json({ result: "error", error: error });
+ }
+ else
+ return NextResponse.json({ profiles: profile_result });
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/recommendations/[id]/route.js b/src/app/api/recommendations/[id]/route.js
new file mode 100644
index 0000000..2a05fba
--- /dev/null
+++ b/src/app/api/recommendations/[id]/route.js
@@ -0,0 +1,30 @@
+export async function GET(request, { params }) {
+ const { id } = params;
+
+ const res = await fetch(
+ `https://api.lytics.io/api/content/recommend/user/_uids/${id}?contentsegment=${process.env.LYTICS_COLLECTION_ID}&limit=5&shuffle=true`,
+ {
+ method: 'GET',
+ headers: {
+ accept: 'application/json',
+ Authorization: process.env.LYTICS_API_KEY,
+ cache: 'no-store'
+ },
+ }
+ );
+
+ if (!res.ok) {
+ return new Response(
+ JSON.stringify({ error: 'Failed to fetch from Lytics API' }),
+ { status: res.status }
+ );
+ }
+
+ const data = await res.json();
+ return new Response(JSON.stringify(data), {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+}
\ No newline at end of file
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
index 718d6fe..104f46b 100644
Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ
diff --git a/src/app/globals.css b/src/app/globals.css
index a461c50..ffe7584 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1 +1,155 @@
-@import "tailwindcss";
\ No newline at end of file
+@import "tailwindcss";
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@theme {
+ /* FONT FAMILIES */
+ --font-poppins: var(--font-poppins);
+ --font-cinzel: var(--font-cinzel);
+ --font-montserrat: var(--font-monteserrat);
+ --font-roboto: var(--font-roboto);
+ --font-playfair: var(--font-playfair);
+ --font-raleway: var(--font-raleway);
+ --font-opensans: var(--font-opensans);
+ --font-spectral: var(--font-spectral);
+ --font-rokkitt: var(--font-rokkitt);
+ --font-cormorant: var(--font-cormorant);
+ --font-lexend: var(--font-lexend);
+
+ /* Background gradients */
+ --bg-gradient-radial: radial-gradient(var(--tw-gradient-stops));
+ --bg-gradient-conic: conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops));
+
+ /* Animations */
+ --animate-fadeIn: fadeIn 1s ease-in-out;
+ --animate-infinite-scroll: infinite-scroll 60s linear infinite;
+
+ /* Keyframes */
+ @keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+ }
+
+ @keyframes infinite-scroll {
+ 0% { transform: translateX(0%); }
+ 100% { transform: translateX(-100%); }
+ }
+
+ @keyframes bounce {
+ 0%, 100% {
+ transform: translateY(-50%);
+ animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
+ }
+ 50% {
+ transform: none;
+ animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
+ }
+ }
+}
+
+@theme{
+ --container-8xl: 1440px;
+}
+
+:root:has(.no-doc-scroll){
+ overflow:hidden;
+ margin-right: 14px;
+}
+
+.temp > p{
+ margin-bottom: 25px;
+}
+
+.two-cols {
+ -webkit-columns: 40px 2;
+ /* Chrome, Safari, Opera */
+ -moz-columns: 60px 2;
+ /* Firefox */
+ columns: 60px 2;
+}
+
+h1, h2, h3, h4, h5 {
+ text-transform: uppercase;
+ font-weight: 400;
+ letter-spacing: 0.1em;
+ line-height: 1.5;
+}
+
+.save-the-turtles * {
+ font-family: var(--font-lexend);
+}
+
+h1 {
+ font-size: 3rem;
+}
+
+h2 {
+ font-size: 2.25rem;
+}
+
+h3 {
+ font-size: 2rem;
+}
+
+p, label{
+ font-size: 1.125rem;
+ font-family: var(--font-poppins);
+ font-weight: 200;
+ letter-spacing: .05rem;
+}
+
+strong {
+ font-weight: 600;
+}
+
+.json-rte > p {
+ font-weight: 300;
+ margin-bottom: 2rem;
+}
+.json-rte > :is(li, ol, ul) {
+ margin-bottom: 2rem;
+ margin-left: 2rem;
+ font-size: 1.125rem;
+ font-family: var(--font-poppins);
+ font-weight: 300;
+ letter-spacing: .05rem;
+}
+
+.json-rte > ol {
+ list-style-position:inside;
+ list-style-type: decimal;
+}
+
+.json-rte > ul {
+ list-style-position:inside;
+ list-style-type: disc;
+}
+
+ .article > p {
+ font-weight: 300;
+ margin-bottom: 2rem;
+ font-size: 1rem;
+ letter-spacing: .02rem;
+ }
+
+ .article > :is(h1, h2, h3, h4, h5) {
+ font-family: var(--font-poppins);
+ font-weight: 300;
+ text-transform: none;
+ }
+
+ .article > ol{
+ list-style-position:inside;
+ list-style-type: decimal;
+ }
+
+ .article > ul {
+ list-style-position:inside;
+ list-style-type: disc;
+ }
+
+ .article > :is(li, ol, ul) {
+ font-size: 1rem;
+ letter-spacing: .02rem;
+ }
diff --git a/src/app/layout.jsx b/src/app/layout.jsx
index f5fdc28..ba3cee8 100644
--- a/src/app/layout.jsx
+++ b/src/app/layout.jsx
@@ -1,9 +1,128 @@
import { cache } from "react";
import { headers } from "next/headers";
import "./globals.css";
+import Script from 'next/script';
import ContentstackServer from "@/lib/cstack";
import { PersonalizeProvider } from "@/context/personalize.context";
import { LyticsTracking } from "@/context/lyticsTracking";
+import {
+ Cinzel,
+ Cormorant,
+ Inter,
+ Montserrat,
+ Open_Sans,
+ Playfair_Display,
+ Poppins,
+ Raleway,
+ Roboto,
+ Rokkitt,
+ Spectral,
+} from "next/font/google";
+import Head from "next/head";
+
+const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
+
+const poppins = Poppins({
+ subsets: ["latin"],
+ variable: "--font-poppins",
+ weight: ["100","200","300","400","500","600","700","800","900"],
+});
+
+const cinzel = Cinzel({
+ subsets: ["latin"],
+ variable: "--font-cinzel",
+ weight: ["400","500","600","700","800","900"],
+});
+
+const roboto = Roboto({
+ subsets: ["latin"],
+ variable: "--font-roboto",
+ weight: ["100","300","400","500","700","900"],
+});
+
+const playfair_display = Playfair_Display({
+ subsets: ["latin"],
+ variable: "--font-playfair",
+ weight: ["400","500","600","700","800","900"],
+});
+
+const montserrat = Montserrat({
+ subsets: ["latin"],
+ variable: "--font-montserrat",
+ weight: ["400","500","600","700","800","900"],
+});
+
+const raleway = Raleway({
+ subsets: ["latin"],
+ variable: "--font-raleway",
+ weight: ["100","200","300","400","500","600","700","800","900"],
+});
+
+const open_sans = Open_Sans({
+ subsets: ["latin"],
+ variable: "--font-opensans",
+ weight: ["300","400","500","600","700","800"],
+});
+
+const spectral = Spectral({
+ subsets: ["latin"],
+ variable: "--font-spectral",
+ weight: ["200","300","400","500","600","700","800"],
+});
+
+const rokkitt = Rokkitt({
+ subsets: ["latin"],
+ variable: "--font-rokkitt",
+ weight: ["100","200","300","400","500","600","700","800","900"],
+});
+
+const cormorant = Cormorant({
+ subsets: ["latin"],
+ variable: "--font-cormorant",
+ weight: ["300","400","500","600","700"],
+});
+
+
+// -------------------------------
+// SERVER FETCH FOR CONFIG
+// -------------------------------
+const getConfig = cache(async () => {
+ const res = await fetch(
+ "https://cdn.contentstack.io/v3/content_types/config/entries",
+ {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ api_key: process.env.CONTENTSTACK_API_KEY,
+ access_token: process.env.CONTENTSTACK_DELIVERY_TOKEN,
+ },
+ next: { revalidate: 60 }, // optional caching
+ }
+ );
+
+ const json = await res.json();
+ return json.entries?.[0] || {};
+});
+
+// -------------------------------
+// FONT PICKER (SSR SAFE)
+// -------------------------------
+function fontPicker(fontName) {
+ const map = {
+ Poppins: "var(--font-poppins)",
+ Cinzel: "var(--font-cinzel)",
+ Roboto: "var(--font-roboto)",
+ Playfair_Display: "var(--font-playfair)",
+ Montserrat: "var(--font-montserrat)",
+ Raleway: "var(--font-raleway)",
+ Open_Sans: "var(--font-opensans)",
+ Spectral: "var(--font-spectral)",
+ Rokkitt: "var(--font-rokkitt)",
+ Cormorant: "var(--font-cormorant)",
+ };
+
+ return map[fontName] || "inherit";
+}
const fetchData = cache(async (locale) => {
const headersList = await headers();
@@ -14,7 +133,8 @@ const fetchData = cache(async (locale) => {
});
export const generateMetadata = async ({ params }) => {
- const { locale } = await params;
+ const parameters = await params;
+ const locale = parameters.locale
const data = await fetchData(locale);
const entry = data?.[0]?.[0];
@@ -37,11 +157,43 @@ export default async function RootLayout({
children,
params,
}) {
- const { locale } = await params;
+ const parameters = await params;
+ const locale = parameters.locale
+ const config = await getConfig();
+
+ const headerFont = fontPicker(config.header_font);
+ const buttonFont = fontPicker(config.button_font);
+ const paragraphFont = fontPicker(config.paragraph_font);
return (
-
{process.env.LYTICS_TAG && }
{process.env.CONTENTSTACK_PERSONALIZATION ?
@@ -50,4 +202,4 @@ export default async function RootLayout({
);
-}
+}
\ No newline at end of file
diff --git a/src/components/agent.js b/src/components/agent.js
new file mode 100644
index 0000000..9b97249
--- /dev/null
+++ b/src/components/agent.js
@@ -0,0 +1,18 @@
+import parse from "html-react-parser";
+
+export default function Agent({ agentData }) {
+ if (agentData?.agent?.code) {
+ return parse(agentData?.agent?.code);
+ }
+ return (
+
+
+
Component Generator
+
Your custom component will appear here once generated. Create dynamic, personalized content powered by AI.
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/articleBanner.js b/src/components/articleBanner.js
new file mode 100644
index 0000000..1fb4332
--- /dev/null
+++ b/src/components/articleBanner.js
@@ -0,0 +1,85 @@
+import { cslp } from "@/lib/cstack";
+import Link from "next/link";
+
+export default function ArticleBanner({ content }) {
+ return (
+
+
{content?.heading}
+ {content?.articles?.length === 0 &&
+
+ }
+
+ {content?.articles?.map((article, index) => (
+
+
+
+
+
+
+
+
+
+
+ Editorial Staff
+
+ {article?.taxonomies?.map((tax, tdx) => {
+ return (
+
+ {tax?.term_uid}
+
+ );
+ })}
+
+
+
+
+
+ {article?.title}
+
+
+
+ {article?.teaser}
+
+
+
+
+ ))}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/cards.js b/src/components/cards.js
new file mode 100644
index 0000000..294172a
--- /dev/null
+++ b/src/components/cards.js
@@ -0,0 +1,55 @@
+import { cslp } from "@/lib/cstack";
+import Link from "next/link";
+
+export default function Cards({ content }) {
+
+
+ return (
+
+ {(content?.card && content?.card?.length === 0) &&
+
+
+ }
+ {(content?.card && content?.card?.length > 0) &&
+
+ {content?.card?.map((data, index) => (
+
+ {data?.image?.url &&
+
+
+
+
+ }
+ {!data?.image?.url &&
+
+ }
+
+
+
+ {data?.headline}
+
+
+
+ {data?.body}
+
+
+
+ {data?.page && (
+ 0 && data?.page[0]?.url) ? data?.page[0]?.url : '/'}>
+
+ {data?.button_text}
+
+
+ )}
+
+
+ ))}
+
+ }
+
+ );
+}
diff --git a/src/components/categoryBanner.js b/src/components/categoryBanner.js
new file mode 100644
index 0000000..a64b0bb
--- /dev/null
+++ b/src/components/categoryBanner.js
@@ -0,0 +1,35 @@
+import { ArrowRightIcon } from "@heroicons/react/20/solid";
+import Link from "next/link";
+
+export default function CategoryBanner({ content }){
+ console.log("category banner", content);
+ return(
+
+
{content?.title}
+
{content?.description}
+
+ {content?.categories?.data?.length === 0 &&
+
+
+ }
+ {content?.categories?.data?.length > 0 &&
+
+ {content?.categories?.data?.map((item, index) => (
+
+
+
{item?.name}
+ {/*
{item?.price}
*/}
+
+ ))}
+
+ }
+
+ {content?.plp &&
+
0 && content?.plp[0].url) ? content.plp[0].url : "#"} className="flex mt-5 items-center text-cyan-600 hover:text-[#D1A261]" {...content?.$?.plp}>
+
0 && content?.plp[0].url) ? content.plp[0].url : "#"} className="inline-block" {...content?.$?.plp_link_text}>{content?.plp_link_text}
+
+
+ }
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/footer.js b/src/components/footer.js
new file mode 100644
index 0000000..4f907e8
--- /dev/null
+++ b/src/components/footer.js
@@ -0,0 +1,98 @@
+import { useRouter } from 'next/navigation';
+
+const navigation = [
+ {
+ name: 'Facebook',
+ href: '#',
+ icon: (props) => (
+
+
+
+ ),
+ },
+ {
+ name: 'Instagram',
+ href: '#',
+ icon: (props) => (
+
+
+
+ ),
+ },
+ {
+ name: 'X',
+ href: '#',
+ icon: (props) => (
+
+
+
+ ),
+ },
+ {
+ name: 'GitHub',
+ href: '#',
+ icon: (props) => (
+
+
+
+ ),
+ },
+ {
+ name: 'YouTube',
+ href: '#',
+ icon: (props) => (
+
+
+
+ ),
+ },
+ ]
+
+ export default function Footer() {
+ const router = useRouter();
+ const today = new Date();
+ const year = today.getUTCFullYear();
+ function resetSegment(){
+ localStorage.removeItem('offerShown');
+ window.scrollTo(0,0);
+ router.refresh();
+ }
+
+ return (
+
+
+
+ {(navigation && navigation.length > 0) && (
+ navigation.map((item) => (
+
+ {item?.name}
+
+
+ )))}
+
+
+
+ © {year} Contentstack Inc. All rights reserved.
+
+
+
+
+ )
+ }
+
\ No newline at end of file
diff --git a/src/components/formBuilder.js b/src/components/formBuilder.js
new file mode 100644
index 0000000..7fb9f6c
--- /dev/null
+++ b/src/components/formBuilder.js
@@ -0,0 +1,179 @@
+import { cslp } from "@/lib/cstack";
+
+export default function FormBuilder({ content }) {
+
+
+ async function sendForm(e) {
+ e.preventDefault();
+
+ const formData = new FormData(e.target);
+
+ let result = await fetch('/api/formBuilder/', {
+ method: "POST",
+ body: formData
+ })
+ .then((response) => response.json())
+ .then((result) => {
+ console.log("email result", result);
+ })
+ .catch((error) => console.error(error));
+ }
+
+ return (
+
+ {content && (
+
+
+
{content?.title}
+
{content?.description}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/halfSquares.js b/src/components/halfSquares.js
new file mode 100644
index 0000000..1ecd6a6
--- /dev/null
+++ b/src/components/halfSquares.js
@@ -0,0 +1,44 @@
+import Link from "next/link";
+export default function HalfSquares({ content }) {
+ return (
+
+
+ {!content?.image?.url &&
+
+ }
+
+
+
+
{content?.headline}
+
+
{content?.body}
+
+ {content.page && (
+
0 && content?.page[0]?.url) ? content?.page[0]?.url : '#'}>
+
+ {content?.button_text}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/header.js b/src/components/header.js
new file mode 100644
index 0000000..c9c6be4
--- /dev/null
+++ b/src/components/header.js
@@ -0,0 +1,368 @@
+"use client";
+import { Fragment, useEffect, useState} from 'react';
+import { cslp } from '@/lib/cstack';
+import { useRouter } from 'next/navigation';
+import { ContentstackClient } from "@/lib/contentstack-client";
+import { usePersonalize } from '@/context/personalize.context';
+import { createClient } from '@/utils/supabase/client';
+import { faCheck, faCircleUser as loggedIn, faCircleQuestion } from '@awesome.me/kit-610837e1f9/icons/classic/solid';
+import { faCircleUser as loggedOut } from '@awesome.me/kit-610837e1f9/icons/classic/thin';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Disclosure, Popover, PopoverButton, PopoverPanel, Transition } from '@headlessui/react';
+import { ChevronDownIcon } from '@heroicons/react/20/solid';
+import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+import { useJstag } from '../context/lyticsTracking';
+import { useParams } from 'next/navigation';
+
+export default function Header({ color, locale }) {
+ const [menuOpen, setMenuOpen] = useState(false);
+ const [entry, setEntry] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const [profiles, setProfiles] = useState([]);
+ const [selectedProfile, setSelectedProfile] = useState('');
+ const [avatar, setAvatar] = useState('');
+ const router = useRouter();
+ const [user, setUser] = useState({});
+ const pathname = usePathname();
+ const slug = pathname.slice(3);
+ const jstag = useJstag();
+ const personalizeSDK = usePersonalize();
+ const params = useParams();
+
+ const supabase = createClient();
+
+ const getUser = async () => {
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ setUser(user);
+ return (user);
+ }
+
+ async function logout() {
+ if (user) {
+ await supabase.auth.signOut();
+ }
+ localStorage.setItem('profile', "");
+ await personalizeSDK.set({ "client_type": "" });
+
+ router.push("/account/login");
+ }
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByTypeWithRefs("header", locale, [
+ "menu_items.page",
+ "menu_items.sub_items.page",
+ ]);
+ setEntry(entry[0]);
+ setIsLoading(false);
+ };
+
+ const getProfiles = async (user_id) => {
+ let result = await fetch(`/api/profiles/${user_id}`, {
+ method: 'GET',
+ })
+ .then((response) => {
+ if (response.ok)
+ return response.json();
+ else
+ return Promise.reject(response);
+ })
+ .then((result) => {
+ let tempProfiles = [];
+ for (const profile of result.profiles) {
+ tempProfiles.push({
+ fname: profile.first_name,
+ lname: profile.last_name,
+ audience: profile.audience,
+ id: profile.id,
+ avatar: profile.avatar_url
+ })
+ }
+ setProfiles(tempProfiles);
+ let saved = localStorage.getItem('profile');
+ if (saved) {
+ setSelectedProfile(saved);
+ setAvatar(tempProfiles.find(p => p.fname === saved).avatar);
+ }
+
+ })
+ .catch((error) => {
+ console.log(error);
+ })
+ }
+
+ useEffect(() => {
+ async function getUserId() {
+ let currentUser = await getUser();
+ if (currentUser) {
+ getProfiles(currentUser.id);
+ }
+ }
+ getUserId()
+ ContentstackClient.onEntryChange(getContent);
+ jstag.call("resetPolling");
+ }, []);
+
+ if (isLoading) return;
+
+ function changeLang(language) {
+ const path = language + slug;
+ router.push("/" + path);
+ }
+
+ const changeProfile = async (name) => {
+ setSelectedProfile(name);
+
+ if (name === ""){
+ localStorage.setItem('profile', "");
+ await personalizeSDK.set({ "client_type": "" });
+ }
+ else {
+ const profile = profiles.find(p => p.fname === name);
+ localStorage.setItem('profile', profile.fname);
+ console.log('audience', profile.audience);
+ await personalizeSDK.set({ "client_type": profile.audience });
+ }
+ window.location.reload();
+ }
+
+ function classNames(...classes) {
+ return classes.filter(Boolean).join(" ");
+ }
+
+ return (
+
+
+ {color === "white" && (
+
+ )}
+ {color !== "white" && (
+
+ )}
+
+
+
+ setMenuOpen(true)}>
+
+
+
+
+
+ {(entry?.menu_items && entry?.menu_items?.length > 0) && (
+ entry?.menu_items?.map((item, index) => {
+ if (item?.sub_items?.length > 0) {
+ return (
+
+
+
+
+
+
+ {(item?.sub_items && item?.sub_items?.length > 0) && (
+ item.sub_items.map((sub, subIdx) => (
+ sub?.page && (
+ 0 ? sub.page[0].url : "#"}
+ className="text-nowrap font-light"
+ {...sub.$?.text}
+ >
+ {sub.text}
+
+ )
+ )))}
+
+
+
+
+ );
+ } else {
+ return (
+
+
+ {item?.page && (
+ 0 && item?.page[0].url) ? item.page[0].url : "#"}
+ {...item.$?.text}
+ >
+ {item.text}
+
+ )}
+
+
+ );
+ }
+ }))}
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ changeLang(e.target.value);
+ }}
+ >
+ EN
+ ES
+ FR
+ DE
+
+
+
+ {!user &&
+
+
+
+
+
+
+
+ Log In
+
+
+
+ }
+ {user &&
+
+
+ {avatar &&
+
+ }
+ {!avatar &&
+
+ }
+
+
+
+ changeProfile("")}
+ >
+
+ Anonymous
+
+ {profiles?.map((profile, index) => (
+ changeProfile(profile.fname)}
+ >
+
+ {profile.fname}
+
+ ))}
+
+ MANAGE PROFILES
+
+
+ LOG OUT
+
+
+
+ }
+
+
+
+
+
+ setMenuOpen(false)}
+ >
+
+
+
+
+ {entry?.menu_items?.map((item, index) => {
+ if (item.sub_items?.length > 0) {
+ return (
+
+ {({ open }) => (
+ <>
+
+ {item.text}
+
+
+
+ {item.sub_items.map((item, index) => (
+
+ {item.text}
+
+ ))}
+
+ >
+ )}
+
+ );
+ } else {
+ return (
+ 0 && item?.page[0].url) ? item.page[0].url : "#"}
+ >
+ {item.text}
+
+ );
+ }
+ })}
+
+
+
+ );
+}
diff --git a/src/components/hero.js b/src/components/hero.js
new file mode 100644
index 0000000..832b520
--- /dev/null
+++ b/src/components/hero.js
@@ -0,0 +1,160 @@
+"use client"
+import Link from "next/link";
+import Header from "./header";
+import { motion } from "framer-motion";
+import { usePathname } from "next/navigation";
+export default function Hero({ content, locale, withHeader, cslp }) {
+ const pathname = usePathname();
+ if (!content || content?.length === 0) return
;
+
+ let positionClass = "";
+ let headlineClass = "";
+ let bodyClass = "";
+ let buttonClass = "";
+
+
+ if (content && content?.length > 0){
+ if (content[0].text_position === "Top Left") {
+ positionClass = "top-16 left-16";
+ } else if (content[0].text_position === "Top Center") {
+ positionClass = "top-16 left-1/2 transform -translate-x-1/2 ";
+ } else if (content[0].text_position === "Top Right") {
+ positionClass = "top-16 right-16";
+ } else if (content[0].text_position === "Left") {
+ positionClass = "top-1/2 left-16 transform -translate-y-1/2";
+ } else if (content[0]?.text_position === "Center") {
+ positionClass =
+ "top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2";
+ } else if (content[0]?.text_position === "Right") {
+ positionClass = "top-1/2 right-16 transform -translate-y-1/2";
+ headlineClass = "text-right";
+ bodyClass = "text-right";
+ buttonClass = "justify-end";
+ } else if (content[0].text_position === "Bottom Left") {
+ positionClass = "bottom-16 left-16";
+ } else if (content[0]?.text_position === "Bottom Center") {
+ positionClass = "bottom-16 left-1/2 transform -translate-x-1/2";
+ } else if (content[0].text_position === "Bottom Right") {
+ positionClass = "bottom-16 right-16";
+ }
+ }
+
+ if (content && content?.length > 0){
+ if (content[0].alignment === "Left") {
+ headlineClass = "text-left";
+ bodyClass = "text-left";
+ buttonClass = "justify-start";
+ } else if (content[0].alignment === "Center") {
+ headlineClass = "text-center";
+ bodyClass = "";
+ buttonClass = "justify-center";
+ } else if (content[0].alignment === "Right") {
+ headlineClass = "text-right";
+ bodyClass = "text-right";
+ buttonClass = "justify-end";
+ }
+ }
+
+ if(content && content?.length) {
+ if (content[0].header_overlay !== true) {
+ withHeader = false
+ }
+ }
+
+ return (
+ <>
+ {(!withHeader && pathname === `/${locale}`) && }
+
+
+
+ {content?.map((hero, index) => {
+ return (
+
+
+
+ {withHeader ?
: <>>}
+
+
+
+
+ {hero?.header}
+
+
+
+ {hero?.body}
+
+
+ {
+ hero.button_text !== "" && (
+
+ {hero?.page && (
+ 0 && hero?.page[0]?.url) ? hero.page[0].url : "#"
+ }
+ className="rounded-md button px-8 py-4 text-md tracking-widest uppercase font-bold text-white shadow-sm ring-2 ring-inset ring-gray-300 hover:text-neutral-700 hover:bg-gray-50"
+ {...hero?.$?.button_text}
+ >
+ {hero.button_text}
+
+ )}
+
+ )
+ }
+
+
+
+
+ );
+ })}
+
+
+ >
+ );
+}
diff --git a/src/components/imageGrid.js b/src/components/imageGrid.js
new file mode 100644
index 0000000..5f31cb7
--- /dev/null
+++ b/src/components/imageGrid.js
@@ -0,0 +1,305 @@
+import { cslp } from "@/lib/cstack";
+import Link from "next/link";
+
+export default function ImageGrid({ content }) {
+ return (
+
+ {(content?.image && content?.image?.length === 0) && (
+
+ )}
+ {(content?.image && content?.image?.length === 1) && (
+
+ {!content.image[0].image?.url && (
+
+ )}
+ {(content?.image && content?.image?.length > 0 && content.image[0].image?.url) && (
+
+
0 && content?.image[0]?.page[0]?.url)
+ ? content?.image[0]?.page[0].url
+ : "#"
+ }
+ >
+
+
+
+
+ {content.image[0].text}
+
+
+
+
+ )}
+
+ )}
+ {content?.image?.length === 2 && (
+
+ {[0, 1].map((item, index) => {
+ if (!content?.image[index]?.image?.url)
+ return (
+
+ );
+ else {
+ return (
+
+ {(content?.image && content?.image?.length > 0 && content?.image[index]?.page) && (
+
0 && content?.image[index]?.page[0].url)
+ ? content?.image[index]?.page[0].url
+ : "#"
+ }
+ >
+
+
+
+
+
+ {content.image[index].text}
+
+
+
+ )}
+
+ );
+ }
+ })}
+
+ )}
+ {content?.image?.length === 3 && (
+
+ {[0, 1, 2].map((item, index) => {
+ if (!content.image[index].image?.url) {
+ return (
+
+ );
+ } else {
+ return (
+
+ {(content?.image && content?.image?.length > 0 && content?.image[index]?.page) && (
+
0 && content?.image[index]?.page[0].url)
+ ? content?.image[index]?.page[0].url
+ : "#"
+ }
+ >
+
+
+
+
+
+ {content.image[index].text}
+
+
+
+ )}
+
+ );
+ }
+ })}
+
+ )}
+ {content?.image?.length >= 4 && (
+
+ {[0, 1, 2, 4].map((item, index) => {
+ if (!content.image[index].image?.url) {
+ return (
+
+ );
+ } else {
+ return (
+
+ {(content?.image && content?.image?.length > 0 && content?.image[index]?.page) && (
+
0 && content?.image[index]?.page[0].url)
+ ? content?.image[index]?.page[0].url
+ : "#"
+ }
+ >
+
+
+
+
+
+ {content.image[index].text}
+
+
+
+ )}
+
+ );
+ }
+ })}
+
+ )}
+
+ );
+}
diff --git a/src/components/landingPageGrid.js b/src/components/landingPageGrid.js
new file mode 100644
index 0000000..0eb1fd4
--- /dev/null
+++ b/src/components/landingPageGrid.js
@@ -0,0 +1,134 @@
+
+export default function LandingPageGrid({ content, isKiosk }) {
+ return (
+
+
+ {content?.images?.length === 0 &&
+
+
+
+ }
+ {content?.images?.length === 1 &&
+
+ {!content.images[0].image?.url &&
+
+ }
+ {content.images[0].image?.url &&
+
+
+
{content.images[0].text}
+
+ }
+
+ }
+ {content?.images?.length === 2 &&
+
+ {[0, 1].map((item, index) => {
+ if (!content.images[index].image?.url)
+ return (
+
+ )
+ else {
+ return (
+
+
+
{content.images[index].text}
+
+ )
+ }
+ })}
+
+
+ }
+ {content?.images?.length === 3 &&
+
+ {[0, 1, 2].map((item, index) => {
+ if (!content.images[index].image?.url) {
+ return (
+
+ )
+ }
+ else {
+ return (
+
+
+
{content.images[index].text}
+
+ )
+ }
+ })}
+
+ }
+ {content?.images?.length >= 4 &&
+
+ {[0, 1, 2, 4].map((item, index) => {
+ if (!content.images[index].image?.url) {
+ return (
+
+ )
+ }
+ else {
+ return (
+
+
+
{content.images[index].text}
+
+ )
+ }
+ })}
+
+ }
+
+
+
+ {(content?.images && content?.images?.length > 0) && (
+ content?.images?.map((item, index) => (
+
1 ? "hidden" : "")}>
+
+
{item?.text}
+
+ )))}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/leadCapture.js b/src/components/leadCapture.js
new file mode 100644
index 0000000..fabf118
--- /dev/null
+++ b/src/components/leadCapture.js
@@ -0,0 +1,47 @@
+"use client"
+import { useState } from 'react';
+
+export default function LeadCapture({ content }) {
+ const [inputValue, setInputValue] = useState("");
+
+ function clickHandle(){
+ jstag.send({"email" : inputValue});
+ setInputValue("");
+ }
+ const handleChange = (event) => {
+ event.preventDefault();
+ setInputValue(event.target.value);
+ };
+
+ return (
+
+
+
+
+
+ {content?.title}
+
+
{content?.description}
+
+
+
+
+ {content?.button_text}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/lyticsExtension.js b/src/components/lyticsExtension.js
new file mode 100644
index 0000000..48341b8
--- /dev/null
+++ b/src/components/lyticsExtension.js
@@ -0,0 +1,583 @@
+import { useEffect, useState, Fragment} from "react";
+import { XMarkIcon, FlagIcon, LockClosedIcon } from "@heroicons/react/24/outline";
+import { useEntity, useRecommendations } from "@/utils/lyticsTracking";
+import { useParams } from "next/navigation";
+import { ContentstackClient } from "@/lib/contentstack-client";
+import { usePathname } from "next/navigation";
+import { Menu, MenuItem, MenuButton, MenuItems } from '@headlessui/react';
+import { ChevronDownIcon } from '@heroicons/react/20/solid';
+import Link from "next/link";
+import JsonView from '@uiw/react-json-view';
+import { lightTheme } from '@uiw/react-json-view/light';
+
+export default function LyticsExtension({ }) {
+ const [statsOpen, setStatsOpen] = useState(false);
+ const lyticsProfileData = useEntity();
+ const [selectedTab, setSelectedTab] = useState("Summary");
+ const pathname = usePathname();
+ const [entry, setEntry] = useState({});
+ const [isLoading, setIsLoading] = useState(true);
+ const [headerOverlay, setHeaderOverlay] = useState(false);
+ const recommendations = useRecommendations();
+ const [menuOpen, setMenuOpen] = useState(false);
+ const params = useParams();
+
+
+ const getContent = async () => {
+ const entry = await ContentstackClient.getElementByTypeWithRefs(
+ "homepage",
+ params.locale,
+ [
+ 'modular_blocks.review.reference',
+ 'modular_blocks.image_grid.image.page',
+ 'hero.hero_banner',
+ 'hero.page',
+ 'modular_blocks.review.testimonials',
+ 'modular_blocks.review.testimonials.reviews.review',
+ 'modular_blocks.product_banner.plp',
+ 'modular_blocks.cards.card.page',
+ 'modular_blocks.text_and_image.page'
+ ]
+ );
+ setEntry(entry[0]);
+ setIsLoading(false);
+ };
+
+
+
+ //function to convert lytics intrests in two to decimal points
+ function moveDecimalPoint(num) {
+ if (num >= 1) {
+ return "100";
+ }
+ const str = num.toString();
+ const decimalIndex = str.indexOf(".");
+
+ return str.slice(decimalIndex + 1, decimalIndex + 3);
+ }
+
+ function addCommas(numStr) {
+ return numStr.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+ }
+
+ useEffect(() => {
+ ContentstackClient.onEntryChange(getContent);
+ //console.log("state set in lyticsExtension", lyticsProfileData);
+ if((pathname === `/${params.locale}`) && (entry?.hero?.length > 0)) {
+ //console.log(headerOverlay)
+ if((entry?.hero[0]?.header_overlay) && (headerOverlay === false)) {
+ setHeaderOverlay(true);
+ }
+ } else if((pathname !== `/${params.locale}`) && (headerOverlay === true)) {
+ setHeaderOverlay(false);
+ }
+
+ }, [lyticsProfileData]);
+
+
+ if (!lyticsProfileData) return null;
+
+ let max100 = Math.min(lyticsProfileData?.data?.user?.likely_premier_score, 100);
+
+ const tabs = [
+ { name: 'Summary', isVisible: true },
+ {
+ name: 'All Attributes',
+ isVisible: true,
+ label: `Attributes (${Object.keys(lyticsProfileData?.data?.user || {}).length})`
+ },
+ {
+ name: 'Customer',
+ isVisible: !!lyticsProfileData?.data?.user?.email
+ },
+ {
+ name: 'Content Recommendations',
+ label: 'Recommendations',
+ isVisible: recommendations?.length > 0
+ }
+ ];
+
+ const getLabel = (tab) => tab.label || tab.name;
+
+ const selectedLabel = getLabel(tabs.find((tab) => tab.name === selectedTab));
+
+ return (
+
+
+ setStatsOpen(true)}
+ >
+
+
+
+
+
+
+
+
+
+ Contentstack Dev Tools
+
+
+
setStatsOpen(false)}
+ >
+
+
+
+
+
+
+
+
+
+ setMenuOpen(true)}className="inline-flex justify-center w-full px-4 py-3 text-sm font-semibold border border-neutral rounded-md shadow-sm text-[#6351e3] bg-white hover:bg-gray-50 font-roboto tracking-wide">
+ {selectedLabel}
+
+
+
+
+
+ {tabs
+ .filter((tab) => tab.isVisible)
+ .map((tab) => (
+
+ {
+ setSelectedTab(tab.name);
+ }}
+ className={`w-full text-center px-4 py-3 text-sm font-semibold font-roboto tracking-wide hover:'bg-gray-100 ${
+ selectedTab === tab.name
+ ? 'text-[#6351e3]'
+ : 'text-[#ababab]'
+ }`}
+ >
+ {getLabel(tab)}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ Lytics ID
+
+
+ {lyticsProfileData?.data?.user?._id}
+
+
+
+ {lyticsProfileData?.data?.user?._uid &&
+
+
+ Last _UID
+ (Cookie)
+
+
+ {lyticsProfileData?.data?.user?._uid}
+
+
+ }
+
+
+
+
+ Audiences
+
+
+ {lyticsProfileData?.data?.user?.segments?.map(
+ (item, index) => (
+
+ {item}
+
+ )
+ )}
+
+
+ {lyticsProfileData?.data?.user?.likely_premier_score && (
+
+
+ Lookalike Models
+
+
+
+
+
Premier Likelihood
+
{max100}
+
+
+
+
+ )}
+
+
+
+ Behavioral Scores
+
+
+
+
+
Consistency
+
{lyticsProfileData?.data?.user?.score_consistency}
+
+
+
+
+
Frequency
+
{lyticsProfileData?.data?.user?.score_frequency}
+
+
+
+
+
Intensity
+
{lyticsProfileData?.data?.user?.score_intensity}
+
+
+
+
+
Maturity
+
{lyticsProfileData?.data?.user?.score_maturity}
+
+
+
+
+
Momentum
+
{lyticsProfileData?.data?.user?.score_momentum}
+
+
+
+
+
Propensity
+
{lyticsProfileData?.data?.user?.score_propensity}
+
+
+
+
+
Quantity
+
{lyticsProfileData?.data?.user?.score_quantity}
+
+
+
+
+
Recency
+
{lyticsProfileData?.data?.user?.score_recency}
+
+
+
+
+
Volatility
+
{lyticsProfileData?.data?.user?.score_volatility}
+
+
+
+
+
+
+
+ Interests
+
+
+
+ {lyticsProfileData?.data?.user.lytics_content ?
+ Object.keys(lyticsProfileData?.data?.user.lytics_content).map(
+ (item, index) => (
+
+
{item}
+
{moveDecimalPoint(lyticsProfileData?.data?.user?.lytics_content[item])}
+
+
+ )
+ ) : (
+
+
+
+ Browse more content to unlock interests
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
Membership Status
+ {lyticsProfileData?.data?.user?.user_attributes?.membership_status &&
+
+ {lyticsProfileData?.data?.user?.user_attributes?.membership_status}
+
+ }
+
+
+
+
+
+
Total Spend
+ {lyticsProfileData?.data?.user?.total_spend &&
+
+ ${(lyticsProfileData?.data?.user?.total_spend)}
+
+ }
+
+
+
+
+
+
Full Name
+
+ {lyticsProfileData?.data?.user?.salesforce_lead_name}
+
+
+
+
Phone Number
+
+ {lyticsProfileData?.data?.user?.salesforce_lead_phone}
+
+
+
+
Postal Code
+
+ {lyticsProfileData?.data?.user?.salesforce_lead_postal_code}
+
+
+
+
Marketing Opt Out
+
+ {lyticsProfileData?.data?.user?.salesforce_lead_has_opted_out_of_email ? "True" : "False"}
+
+
+
+
+
Company
+
+ {lyticsProfileData?.data?.user?.salesforce_lead_company}
+
+
+
+
Industry
+
+ {lyticsProfileData?.data?.user?.salesforce_lead_industry}
+
+
+
+
Number Of Employees
+
+ {lyticsProfileData?.data?.user?.salesforce_lead_number_of_employees}
+
+
+
+
+
+
+
Past 12 months
+
+ {lyticsProfileData?.data?.user?.lead_booking_count_12m}
+
+
+
+
Nights
+
+ {lyticsProfileData?.data?.user?.lead_booking_count_nights}
+
+
+
+
Lifetime
+
+ {lyticsProfileData?.data?.user?.lead_booking_count_lifetime}
+
+
+
+
Lifetime Nights
+
+ {lyticsProfileData?.data?.user?.lead_booking_count_lifetime_nights}
+
+
+
+
+
+
+
+
+
+
+
+ Recommendations
+
+ {recommendations?.length > 0 && (
+
+ {recommendations?.map((rec, index) => (
+
+ {rec?.banner_image && (
+
+
+
+
+
+ {rec?.headline}
+
+
+
+
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/marquee.js b/src/components/marquee.js
new file mode 100644
index 0000000..c6f57dc
--- /dev/null
+++ b/src/components/marquee.js
@@ -0,0 +1,46 @@
+import { cslp } from "@/lib/cstack";
+
+export default function Marquee({ content }) {
+ return (
+
+ {content?.images?.length === 0 &&
+
+
+ }
+
+ {(content?.images && content?.images?.length > 0) && (
+ content?.images?.map((image, index) => (
+
+ {!image.image?.url &&
+
+ }
+ {image?.image?.url &&
+
+ }
+
+ )))}
+
+
+ {(content?.images && content?.images?.length > 0) && (
+ content?.images?.map((image, index) => (
+
+ {!image.image?.url &&
+
+ }
+ {image?.image?.url &&
+
+ }
+
+ )))}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/mobile/backgroundAndButtons.js b/src/components/mobile/backgroundAndButtons.js
new file mode 100644
index 0000000..817bdfd
--- /dev/null
+++ b/src/components/mobile/backgroundAndButtons.js
@@ -0,0 +1,20 @@
+
+export default function BackgroundAndButtons({ content }){
+ return(
+
+
+
{content?.category}
+
{content?.title}
+
{content?.details}
+
+ {content?.button_1_text}
+ {content?.button_2_text}
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/mobile/buttonCTA.js b/src/components/mobile/buttonCTA.js
new file mode 100644
index 0000000..5deb32b
--- /dev/null
+++ b/src/components/mobile/buttonCTA.js
@@ -0,0 +1,37 @@
+
+export default function ButtonCTA({ content }){
+ return(
+
+ {!content?.image?.url &&
+
+ }
+ {content?.image?.url &&
+
+ }
+
+
{content?.headline}
+
{content?.details}
+
+
+ {content?.button_1_text}
+
+
+
+ {content?.button_2_text}
+
+
+
{content?.footer_text}
+
{content.footer_link_text}
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/mobile/iconGrid.js b/src/components/mobile/iconGrid.js
new file mode 100644
index 0000000..c6c5d82
--- /dev/null
+++ b/src/components/mobile/iconGrid.js
@@ -0,0 +1,28 @@
+
+export default function IconGrid({ content }){
+ return(
+
+
+ {content?.items?.map((item, index) => (
+
+ {!item.image?.url &&
+
+ }
+ {item.image?.url &&
+
+ }
+
+
+
{item.headline}
+
{item.details}
+
+
+ ))}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/pageHero.js b/src/components/pageHero.js
new file mode 100644
index 0000000..3f96f28
--- /dev/null
+++ b/src/components/pageHero.js
@@ -0,0 +1,244 @@
+"use client";
+
+export default function PageHero({ content }) {
+ return (
+
+
+ {content?.style === "Full Image" && (
+
+ {!content?.image?.url && (
+
+ )}
+ {content?.layout === "Text Right" && (
+
+
+
+ {content?.headline}
+
+
+ {content?.details}
+
+
+
+ )}
+ {content?.layout === "Text Left" && (
+
+
+
+ {content?.headline}
+
+
+ {content?.details}
+
+
+
+ )}
+ {content?.layout === "Center" && (
+
+
+
+ {content?.headline}
+
+
+ {content?.details}
+
+
+
+ )}
+
+ )}
+
+ {content?.style !== "Full Image" && (
+
+ {(content?.layout === "Text Right" ||
+ content?.layout === "Center") && (
+
+
+ {content?.image?.url && (
+
+ )}
+ {!content?.image?.url && (
+
+ )}
+
+
+
+
+ {content?.headline}
+
+
+ {content?.details}
+
+
+
+ )}
+ {content?.layout === "Text Left" && (
+
+
+
+
+ {content?.headline}
+
+
+ {content?.details}
+
+
+
+
+
+ {content?.image?.url && (
+
+ )}
+ {!content?.image?.url && (
+
+ )}
+
+
+ )}
+
+ )}
+
+
+ {content?.image?.url && (
+
+ )}
+ {!content?.image?.url && (
+
+ )}
+
+
+
+ {content?.headline}
+
+
+ {content?.details}
+
+
+
+
+
+ );
+}
diff --git a/src/components/people.js b/src/components/people.js
new file mode 100644
index 0000000..29a52b5
--- /dev/null
+++ b/src/components/people.js
@@ -0,0 +1,28 @@
+export default function People({ content, isKiosk }) {
+ return (
+
+
{content?.heading}
+
+ {content?.people?.people?.length === 0 &&
+ [0, 1, 2].map((item, index) => (
+
+ ))
+ }
+ {(content?.people?.people && content?.people?.people?.length > 0) && (
+ content?.people?.people?.map((person, index) => (
+
+
+
{person.name}
+
{person.title}
+
+ )))}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/productFeature.js b/src/components/productFeature.js
new file mode 100644
index 0000000..842bc1a
--- /dev/null
+++ b/src/components/productFeature.js
@@ -0,0 +1,34 @@
+import { ArrowRightIcon } from "@heroicons/react/20/solid";
+import Link from "next/link";
+
+export default function ProductFeature({ content }){
+ return(
+
+
{content?.title}
+
{content?.description}
+
+ {content?.products?.data?.length === 0 &&
+
+
+ }
+ {(content?.products?.data && content?.products?.data?.length > 0) &&
+
+ {content?.products?.data?.map((item, index) => (
+
+
+
{item?.name}
+
{item?.price}
+
+ ))}
+
+ }
+
+ {content?.plp && (
+
0 && content.plp[0].url) ? content.plp[0].url : "#"} className="flex mt-5 items-center text-cyan-600 hover:text-[#D1A261]" {...content?.$?.plp}>
+
0 && content.plp[0].url) ? content.plp[0].url : "#"} className="inline-block" {...content?.$?.plp_link_text}>{content?.plp_link_text}
+
+
+ )}
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/recommendationsBanner.js b/src/components/recommendationsBanner.js
new file mode 100644
index 0000000..cadf497
--- /dev/null
+++ b/src/components/recommendationsBanner.js
@@ -0,0 +1,66 @@
+import { cslp } from "@/lib/cstack";
+import { useRecommendations } from "@/context/lyticsTracking";
+import Link from "next/link";
+
+export default function RecommendationsBanner({ content }) {
+ const recommendations = useRecommendations();
+
+ return (
+
+
{content?.heading}
+
+ {(recommendations && recommendations?.length > 0) && (
+ recommendations?.map((article, index) => (
+
+ 0) ? article.url : "#")}>
+
+
+
+
+
+
+
+
+ Editorial Staff
+
+ {(article?.taxonomies && article?.taxonomies?.length > 0) && (
+ article?.taxonomies?.map((tax, tdx) => {
+ return (
+
+ {tax?.term_uid}
+
+ );
+ }))}
+
+
+
+
+
+ {article?.title}
+
+
+
+ {article?.teaser}
+
+
+
+
+ ))
+)}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/resortPackage.js b/src/components/resortPackage.js
new file mode 100644
index 0000000..c5d120e
--- /dev/null
+++ b/src/components/resortPackage.js
@@ -0,0 +1,331 @@
+"use client"
+import { Dialog, DialogPanel, DialogBackdrop } from '@headlessui/react'
+import { XMarkIcon } from "@heroicons/react/24/outline";
+import { useState } from 'react';
+
+export default function ResortPackage({ content }) {
+ const [activityModalOpen, setActivityModalOpen] = useState(false);
+ const [modalContent, setModalContent] = useState(0);
+ const [imageIndex, setImageIndex] = useState(0);
+ const [bookOpen, setBookOpen] = useState(false);
+ const [isProcessing, setIsProcessing] = useState(false);
+ const [isSuccess, setIsSuccess] = useState(false);
+
+ function clickHandle(index) {
+ setModalContent(index);
+ setActivityModalOpen(true);
+ };
+
+ function closeHandle() {
+ setImageIndex(0);
+ setActivityModalOpen(false);
+ }
+
+
+ return (
+
+
+
+
+
{content?.title}
+
{content?.price}
+
{content?.description}
+
Included Activities:
+
+
+ {(content?.products && content?.products.length > 0) && (
+ content?.products?.map((item, index) => (
+
clickHandle(index)}>
+ {(item?.images && item?.images.length > 0) &&
+
+ }
+
{item?.title}
+
+ ))
+ )}
+
+
+
+
+
+
setBookOpen(true)}>BOOK
+
+
+
+
+
+
+
+ {content?.proudcts && content?.products?.length > 0 && (
+
{content?.products[modalContent]?.title}
+ )}
+
closeHandle()} />
+
+
+
+
+
+ {((content?.products && content?.products?.length > 0) && (content?.products[modalContent]?.images && content?.products[modalContent]?.images?.length > 0)) &&
+ content?.products[modalContent].images.map((item, index) => (
+
setImageIndex(index)} className="size-[80px] border flex items-center-justify-center p-1">
+
+
+ ))}
+
+ {((content?.products && content?.products?.length > 0) && (content?.products[modalContent]?.images && content?.products[modalContent]?.images?.length > 0)) && (
+
+ )}
+
+
+
+
+
+
+
+
setBookOpen(false)} className="relative z-50">
+
+
+
+
+ {/* Close Button */}
+ setBookOpen(false)}
+ className="absolute top-4 right-4 text-neutral-500 hover:text-neutral-800 focus:outline-none"
+ >
+
+ Close
+
+
+ {/* Header */}
+
+
+
+
+ {/* Body */}
+
+ {/* Left: Activities + Payment */}
+
+ Activities
+
+ {(content?.products && content?.products.length > 0) && (
+
+ {content.products.map((item, index) => (
+
+ {item.title}
+
+ ))}
+
+ )}
+
+ {/* Payment Summary */}
+
+ Package price
+ {content?.price}
+
+
+ {/* Payment Form */}
+ {
+ e.preventDefault();
+ const email = e.target.email.value; // grab value of the email input
+ console.log('buy clicked');
+ setIsProcessing(true)
+ setIsSuccess(false)
+ console.log("sending tag info")
+ const changedPrice = content?.price.slice(1);
+ jstag.send({
+ total_spent: changedPrice, //pulls price of product entry and increments field
+ _e: "purchase", //sends event named purchase to Data Cloud
+ email: email, // pulls input value from form and sends to Data Cloud
+ "attr.membership_status" : "Diamond"
+ });
+ jstag.call("resetPolling"); // resets polling to fetch profile quicker
+
+ setTimeout(() => {
+ setIsProcessing(false)
+ setIsSuccess(true)
+ e.target.reset();
+ }, 3000)
+ //buyClick(content?.price, email);
+
+ // handle submit
+ }}
+ className="mt-4 border border-neutral-300 bg-neutral-50 p-5 space-y-4"
+ aria-describedby="payment-note"
+ >
+
+ Payment Details
+
+
+ {/* Email Address */}
+
+
+ Email address
+
+
+
+
+
+
+ Name on card
+
+
+
+
+
+
+ Card number
+
+
+
Numbers only • No dashes
+
+
+
+
+
+ Expiration (MM/YY)
+
+
+
+
+
+
+ CVC
+
+ 3–4 digits
+
+
+
+
+
+
+
+ ZIP / Postal code
+
+
+
+
+
+
+
+ Save card for future payments
+
+
+ {isProcessing ? 'Processing...' : content?.price}
+ {/*
+ {content?.price}
+ */}
+
+
+
+ Demo only — do not enter real card details.
+
+
+
+
+ {/* Right: Hero Image + Price Tag */}
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/reviews.js b/src/components/reviews.js
new file mode 100644
index 0000000..6425770
--- /dev/null
+++ b/src/components/reviews.js
@@ -0,0 +1,67 @@
+import StarRating from "@/helpers/StarRating";
+import Image from "next/image";
+import { useEffect, useState } from "react";
+
+export default function Reviews({ content }) {
+ const [activeIndex, setActiveIndex] = useState(0);
+
+ useEffect(() => {
+ if (content?.testimonials && content?.testimonials?.length > 0){
+ const timer = setTimeout(() => {
+ activeIndex === content?.testimonials[0]?.reviews?.length - 1 ? setActiveIndex(0) : setActiveIndex(activeIndex + 1);
+ }, 10000);
+ return () => {
+ clearTimeout(timer);
+ }}
+ }, [activeIndex])
+
+ if (content == null) {
+ return <>>;
+ }
+
+ return (
+
+
+ {(content?.testimonials && content?.testimonials?.length === 0)&&
+
+ }
+ {(content?.testimonials && content?.testimonials?.length > 0) &&
+
+
+
+
{content?.testimonials[0].headline}
+
{content?.testimonials[0].body}
+
+ {(content?.testimonials[0]?.ratings && content?.testimonials[0].ratings?.length > 0) && (
+ content?.testimonials[0].ratings?.map((rating, index) => (
+
+ )))}
+
+
+
+
{content?.testimonials[0].review_title}
+
+ {content?.testimonials[0].reviews?.map((review, index) => (
+
+
+
+
{review.review[0].reviewer_name}
+
{review.review[0].reviewer_city}
+
+ ))}
+
+
+ }
+
+
+ );
+}
diff --git a/src/components/tabs.js b/src/components/tabs.js
new file mode 100644
index 0000000..375af29
--- /dev/null
+++ b/src/components/tabs.js
@@ -0,0 +1,111 @@
+import { useState } from "react";
+import { cslp } from "@/lib/cstack";
+
+export default function Tabs({ content }) {
+ const [selectedTab, setSelectedTab] = useState(0);
+
+ function tabClicked(tab) {
+ setSelectedTab(tab);
+ }
+
+ return (
+ <>
+
+ {content?.tabs?.length === 0 && (
+
+ )}
+ {(content?.tabs && content?.tabs.length > 0) && (
+
+ {content?.tabs?.map((item, index) => (
+
+
+
+
+
+ {item?.title}
+
+
+
+ {item?.body}
+
+
+
+
+ {!item.image?.url && (
+
+ )}
+ {item?.image?.url && (
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+
+ {(content?.tabs && content?.tabs.length > 0) && (
+
+ {content?.tabs?.map((item, index) => (
+ tabClicked(index)}
+ className={
+ selectedTab === index
+ ? "border-b-4 border-black text-2xl max-md:max-w-[25%]"
+ : "border-b-4 border-transparent transition-all hover:border-b-4 hover:border-black text-2xl max-md:max-w-[25%]"
+ }
+ {...item?.$?.tab_text}
+ >
+ {item?.tab_text}
+
+ ))}
+
+ )}
+
+ >
+ );
+}
diff --git a/src/components/textBlock.js b/src/components/textBlock.js
new file mode 100644
index 0000000..d997c18
--- /dev/null
+++ b/src/components/textBlock.js
@@ -0,0 +1,18 @@
+
+export default function TextBlock({ content }) {
+ return (
+
+
+
+ {content?.headline}
+
+
+ {content?.body}
+
+
+ );
+}
diff --git a/src/components/textSection.js b/src/components/textSection.js
new file mode 100644
index 0000000..04f24d4
--- /dev/null
+++ b/src/components/textSection.js
@@ -0,0 +1,14 @@
+export default function TextSection({ content }) {
+ return (
+
+ );
+}
diff --git a/src/components/typingIndicator.js b/src/components/typingIndicator.js
new file mode 100644
index 0000000..a248bcf
--- /dev/null
+++ b/src/components/typingIndicator.js
@@ -0,0 +1,28 @@
+import { useEffect, useRef } from 'react';
+
+export default function TypingIndicator({ isTyping }) {
+ const dotsRef = useRef([]);
+
+ useEffect(() => {
+ if (!isTyping) {
+ dotsRef.current.forEach(dot => dot.classList.remove('animate-bounce'));
+ } else {
+ dotsRef.current.forEach((dot, i) => {
+ dot.classList.add('animate-bounce');
+ dot.style.animationDelay = `${i * 0.2}s`;
+ });
+ }
+ }, [isTyping]);
+
+ return (
+
+ {[0, 1, 2].map((_, i) => (
+ (dotsRef.current[i] = el)}
+ className="w-1 h-1 bg-gray-500 rounded-full"
+ />
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/src/context/lyticsTracking.js b/src/context/lyticsTracking.js
index 7abf287..9f30735 100644
--- a/src/context/lyticsTracking.js
+++ b/src/context/lyticsTracking.js
@@ -31,6 +31,81 @@ export const useJstag = () => {
return undefined;
};
+export const useRecommendations = () => {
+ const jstag = useJstag();
+ const [recommendations, setRecommendations] = useState({});
+ const [cstackRecs, setCstackRecs ] = useState({});
+ const [queryRefresher, setQueryRefresher] = useState(1);
+ const [queryTrigger, setQueryTrigger] = useState(1);
+ const params = useParams();
+
+ useEffect(() => {
+ setQueryRefresher(queryRefresher + 1);
+ if((queryRefresher % 3 === 0)){
+ setQueryTrigger(queryTrigger + 1); // tracker to refresh lytics recommendation api query every 3 path changes
+ }
+ }, [params])
+
+ const fetchRecommendations = async (id) => {
+ if (process.env.LYTICS_COLLECTION_ID){try {
+ const res = await fetch(`/api/recommendations/${id}`);
+ if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`);
+ const data = await res.json();
+ //console.log("ran recommendations query")
+ setRecommendations(data);
+ console.log("recommendations from d&i api", data)
+ return data;
+ } catch (error) {
+ console.error("Failed to fetch recommendations:", error);
+ }}
+};
+
+ async function getElementsByUrls(urls, locale, references = []) {
+ console.log("ran contentstack query")
+ const promises = urls.map((url) => {
+ return Stack.getElementByUrlWithRefs("article", `/${url?.path}`, locale, references);
+ });
+
+ try {
+ const results = await Promise.all(promises);
+ setCstackRecs(results);
+ return results; // array of entries
+ } catch (error) {
+ console.error('Error fetching one or more entries:', error);
+ throw error;
+ }
+}
+
+ useEffect(() => {
+ if (jstag) {
+ jstag.getid(function (id) {
+ console.log("setting cookie");
+ fetchRecommendations(id);
+ });
+ }
+ }, [queryTrigger]);
+
+ useEffect(() => {
+ if (recommendations?.data){
+ const items = Object.values(recommendations.data).map(item => {
+ const urlParts = item.url.split('/');
+ const path = urlParts.slice(2).join('/'); // remove first 2 segments
+ const aspect = item?.aspects?.[0] || null;
+
+ return {
+ path,
+ aspect
+ };
+ });
+ //console.log("items", items)
+ getElementsByUrls(items, params?.locales, []);
+ }
+
+ }, [recommendations]);
+
+ return cstackRecs
+};
+
export const useEntity = () => {
const jstag = useJstag();
const [entity, setEntity] = useState(null);
diff --git a/src/helpers/StarRating.js b/src/helpers/StarRating.js
new file mode 100644
index 0000000..a9d10c3
--- /dev/null
+++ b/src/helpers/StarRating.js
@@ -0,0 +1,50 @@
+import { FaStar, FaStarHalf } from "react-icons/fa";
+
+export default function StarRating({ rating, color, size }) {
+
+ if (rating == '3.5') {
+ return (
+
+
+
+
+
+ )
+ }
+
+ if (rating == '4') {
+ return (
+
+
+
+
+
+
+ )
+ }
+
+ if (rating == '4.5') {
+ return (
+
+
+
+
+
+
+
+ )
+ }
+
+ if (rating == '5') {
+ return (
+
+
+
+
+
+
+
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/src/lib/contentstack-client.js b/src/lib/contentstack-client.js
index 9b30113..ea23f99 100644
--- a/src/lib/contentstack-client.js
+++ b/src/lib/contentstack-client.js
@@ -177,6 +177,7 @@ export const ContentstackClient = {
},
getElementByType: async function (type, locale, initialData) {
+ console.log("getElementByType", type, locale, initialData);
const searchQueryParams = getSearchQueryParams();
if (inLivePreview() && !(searchQueryParams.live_preview || searchQueryParams.hash)) {
while (!ContentstackLivePreview?.hash) {
diff --git a/src/lib/csgraphql.js b/src/lib/csgraphql.js
new file mode 100644
index 0000000..c85a6b0
--- /dev/null
+++ b/src/lib/csgraphql.js
@@ -0,0 +1,115 @@
+export const getSEODataByUrl = async (contentType, url, locale) => {
+ const graphqlQuery = `
+ query MyQuery {
+ all_${contentType}(
+ locale: "${locale}"
+ where: {url: "${url}"}) {
+ items {
+ seo {
+ canonical_url
+ description
+ title
+ no_follow
+ no_index
+ show_in_site_search
+ show_in_sitemap
+ og_meta_tags {
+ description
+ title
+ imageConnection {
+ edges {
+ node {
+ url
+ title
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }`;
+
+ const graphqlResponse = await fetch(`https://graphql.contentstack.com/stacks/${process.env.CONTENTSTACK_API_KEY}?environment=${process.env.CONTENTSTACK_ENVIRONMENT}`, {
+ method: "POST",
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'access_token': process.env.CONTENTSTACK_DELIVERY_TOKEN,
+ 'branch': process.env.CONTENTSTACK_BRANCH || 'main'
+ },
+ body: JSON.stringify({
+ query: graphqlQuery,
+ variables: null,
+ operationName: "MyQuery"
+ }),
+ });
+
+ if (!graphqlResponse.ok) {
+ console.error('GraphQL request failed:', graphqlResponse.status, graphqlResponse.statusText);
+ return null;
+ }
+
+ const graphqlData = await graphqlResponse.json();
+ return graphqlData;
+}
+
+export const getSEODataByTypeByTaxonomyLocation= async (contentType, locale, terms) => {
+ const termsArray = Array.isArray(terms) ? JSON.stringify(terms) : `["${terms}"]`;
+
+ const graphqlQuery = `
+ query MyQuery {
+ all_${contentType}(
+ locale: "${locale}"
+ where: {taxonomies: {locations: {term_in: ${termsArray}}}}
+ ) {
+ total
+ items {
+ seo {
+ canonical_url
+ description
+ title
+ no_follow
+ no_index
+ show_in_site_search
+ show_in_sitemap
+ og_meta_tags {
+ description
+ title
+ imageConnection {
+ edges {
+ node {
+ url
+ title
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }`;
+
+ const graphqlResponse = await fetch(`https://graphql.contentstack.com/stacks/${process.env.CONTENTSTACK_API_KEY}?environment=${process.env.CONTENTSTACK_ENVIRONMENT}`, {
+ method: "POST",
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'access_token': process.env.CONTENTSTACK_DELIVERY_TOKEN,
+ 'branch': process.env.CONTENTSTACK_BRANCH || 'main'
+ },
+ body: JSON.stringify({
+ query: graphqlQuery,
+ variables: null,
+ operationName: "MyQuery"
+ }),
+ });
+
+ if (!graphqlResponse.ok) {
+ console.error('GraphQL request failed:', graphqlResponse.status, graphqlResponse.statusText);
+ return null;
+ }
+
+ const graphqlData = await graphqlResponse.json();
+ return graphqlData;
+}
diff --git a/src/lib/cstack.js b/src/lib/cstack.js
index f93cbab..2c8cca3 100644
--- a/src/lib/cstack.js
+++ b/src/lib/cstack.js
@@ -81,7 +81,7 @@ const ContentstackServer = {
},
getElementByUrl(type, url, locale, live_preview, variantParam) {
- stack.livePreviewQuery(live_preview ?? {});
+ stack.livePreviewQuery(live_preview ?? {});
return new Promise((resolve, reject) => {
stack.contentType(type)
.entry()
@@ -103,7 +103,8 @@ const ContentstackServer = {
}
);
});
- },
+},
+
getElementByUrlWithRefs(type, url, locale, references, live_preview, variantParam) {
stack.livePreviewQuery(live_preview ?? {});
@@ -114,7 +115,8 @@ const ContentstackServer = {
.variants(deserializeVariantIds(variantParam))
.includeReference(...references)
.addParams({ "include_applied_variants": "true" })
- .query({ "url": { $eq: url } })
+ .query({ url: url })
+ .includeReference(...references)
.find()
.then(
function success(data) {
diff --git a/src/lib/redirects.js b/src/lib/redirects.js
new file mode 100644
index 0000000..b411eef
--- /dev/null
+++ b/src/lib/redirects.js
@@ -0,0 +1,72 @@
+/**
+ * Fetches redirect rules from Contentstack
+ * Returns an array of redirect objects with from and to paths
+ */
+export async function getRedirects() {
+ const graphqlQuery = `
+ query GetRedirects {
+ all_redirects {
+ items {
+ paths {
+ from
+ to
+ }
+ }
+ }
+ }
+ `;
+
+ try {
+ const graphqlResponse = await fetch(
+ `https://graphql.contentstack.com/stacks/${process.env.CONTENTSTACK_API_KEY}?environment=${process.env.CONTENTSTACK_ENVIRONMENT}`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ access_token: process.env.CONTENTSTACK_DELIVERY_TOKEN,
+ branch: process.env.CONTENTSTACK_BRANCH || "main",
+ },
+ body: JSON.stringify({
+ query: graphqlQuery,
+ variables: null,
+ operationName: "GetRedirects",
+ }),
+ // Cache for 60 seconds, revalidate in background
+ next: { revalidate: 60 },
+ }
+ );
+
+ if (!graphqlResponse.ok) {
+ console.error(
+ "GraphQL request failed:",
+ graphqlResponse.status,
+ graphqlResponse.statusText
+ );
+ return [];
+ }
+
+ const graphqlData = await graphqlResponse.json();
+
+ if (graphqlData.errors) {
+ console.error("GraphQL errors:", graphqlData.errors);
+ return [];
+ }
+
+ // Flatten the array of paths from all redirect entries
+ const redirectEntries = graphqlData?.data?.all_redirects?.items || [];
+ const allRedirects = [];
+
+ for (const entry of redirectEntries) {
+ if (entry.paths && Array.isArray(entry.paths)) {
+ allRedirects.push(...entry.paths);
+ }
+ }
+
+ return allRedirects;
+ } catch (error) {
+ console.error("Error fetching redirects:", error);
+ return [];
+ }
+}
+
diff --git a/src/utils/supabase/client.js b/src/utils/supabase/client.js
new file mode 100644
index 0000000..e316b4f
--- /dev/null
+++ b/src/utils/supabase/client.js
@@ -0,0 +1,9 @@
+import { createBrowserClient } from '@supabase/ssr'
+
+export function createClient() {
+ // Create a supabase client on the browser with project's credentials
+ return createBrowserClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
+ )
+}
\ No newline at end of file
diff --git a/src/utils/supabase/middleware.js b/src/utils/supabase/middleware.js
new file mode 100644
index 0000000..52c11e9
--- /dev/null
+++ b/src/utils/supabase/middleware.js
@@ -0,0 +1,38 @@
+import { createServerClient } from '@supabase/ssr'
+import { NextResponse } from 'next/server'
+
+export async function updateSession(request) {
+ let supabaseResponse = NextResponse.next({
+ request,
+ })
+
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
+ {
+ cookies: {
+ getAll() {
+ return request.cookies.getAll()
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
+ supabaseResponse = NextResponse.next({
+ request,
+ })
+ cookiesToSet.forEach(({ name, value, options }) =>
+ supabaseResponse.cookies.set(name, value, options)
+ )
+ },
+ },
+ }
+ )
+
+ // refreshing the auth token
+ const user = await supabase.auth.getUser()
+
+ if(request.nextUrl.pathname.startsWith('/changepassword') && user.error){
+ return NextResponse.redirect(new URL('/login', request.url));
+ }
+
+ return supabaseResponse
+}
\ No newline at end of file
diff --git a/src/utils/supabase/server.js b/src/utils/supabase/server.js
new file mode 100644
index 0000000..65f9d8d
--- /dev/null
+++ b/src/utils/supabase/server.js
@@ -0,0 +1,31 @@
+import { createServerClient } from '@supabase/ssr'
+import { cookies } from 'next/headers'
+
+export async function createClient() {
+ const cookieStore = await cookies()
+
+ // Create a server's supabase client with newly configured cookie,
+ // which could be used to maintain user's session
+ return createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
+ {
+ cookies: {
+ getAll() {
+ return cookieStore.getAll()
+ },
+ setAll(cookiesToSet) {
+ try {
+ cookiesToSet.forEach(({ name, value, options }) =>
+ cookieStore.set(name, value, options)
+ )
+ } catch {
+ // The `setAll` method was called from a Server Component.
+ // This can be ignored if you have middleware refreshing
+ // user sessions.
+ }
+ },
+ },
+ }
+ )
+}
\ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..580fc18
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,63 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: [
+ "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
+ "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
+ "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
+ "./src/**/*.{js,jsx,ts,tsx}",
+ "./node_modules/react-tailwindcss-datepicker/dist/index.esm.{js,ts}",
+ ],
+ darkMode: 'false',
+ theme: {
+ extend: {
+ fontFamily: {
+ poppins: ['var(--font-poppins)'],
+ cinzel: ['var(--font-cinzel)'],
+ monteserrat: ['var(--font-monteserrat)'],
+ roboto: ['var(--font-roboto)'],
+ playfair: ['var(--font-playfair)'],
+ raleway: ['var(--font-raleway)'],
+ opensans: ['var(--font-opensans)'],
+ spectral: ['var(--font-spectral)'],
+ rokkitt: ['var(--font-rokkitt)'],
+ cormorant: ['var(--font-cormorant)'],
+ lexend: ['var(--font-lexend)'],
+ },
+ backgroundImage: {
+ "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
+ "gradient-conic":
+ "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
+ },
+ maxWidth: {
+ '8xl': '1440px'
+ },
+ animation: {
+ fadeIn: 'fadeIn 1s ease-in-out',
+ 'infinite-scroll': 'infinite-scroll 60s linear infinite',
+ },
+ keyframes: theme => ({
+ fadeIn: {
+ 'from': { opacity: '0' },
+ 'to': { opacity: '1' }
+ },
+ 'infinite-scroll': {
+ '0%': { transform: 'translateX(0%)' },
+ '100%': { transform: 'translateX(-100%)' },
+ },
+ bounce: {
+ '0%, 100%': {
+ transform: 'translateY(-50%)', // Increased bounce height
+ animationTimingFunction: 'cubic-bezier(0.8, 0, 1, 1)',
+ },
+ '50%': {
+ transform: 'none',
+ animationTimingFunction: 'cubic-bezier(0, 0, 0.2, 1)',
+ },
+ },
+ })
+ },
+ },
+ plugins: [
+
+ ],
+};
diff --git a/tsconfig.json b/tsconfig.json
index 8a04dbb..ddb4485 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -22,6 +22,6 @@
"@/*": ["./src/*"]
}
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/app/[locale]/layout.jsx", "src/app/[locale]/page.jsx"],
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/app/[locale]/layout.jsx", "src/app/[locale]/(configured)/page.jsx"],
"exclude": ["node_modules"]
}