diff --git a/package-lock.json b/package-lock.json
index 50b8b48..f54968f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -45,6 +45,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.21",
+ "baseline-browser-mapping": "^2.9.14",
"eslint": "^9",
"eslint-config-next": "15.5.4",
"eslint-plugin-react-hooks": "^5.2.0",
@@ -96,6 +97,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -506,6 +508,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
},
@@ -529,6 +532,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18"
}
@@ -2660,9 +2664,9 @@
}
},
"node_modules/@next/env": {
- "version": "16.0.7",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz",
- "integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==",
+ "version": "16.1.1",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz",
+ "integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
@@ -2676,9 +2680,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.0.7",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz",
- "integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==",
+ "version": "16.1.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz",
+ "integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==",
"cpu": [
"arm64"
],
@@ -2692,9 +2696,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.0.7",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz",
- "integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==",
+ "version": "16.1.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz",
+ "integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==",
"cpu": [
"x64"
],
@@ -2708,9 +2712,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.0.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz",
- "integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==",
+ "version": "16.1.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz",
+ "integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==",
"cpu": [
"arm64"
],
@@ -2724,9 +2728,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.0.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz",
- "integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==",
+ "version": "16.1.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz",
+ "integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==",
"cpu": [
"arm64"
],
@@ -2740,9 +2744,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.0.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz",
- "integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==",
+ "version": "16.1.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz",
+ "integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==",
"cpu": [
"x64"
],
@@ -2756,9 +2760,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.0.7",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz",
- "integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==",
+ "version": "16.1.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz",
+ "integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==",
"cpu": [
"x64"
],
@@ -2772,9 +2776,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.0.7",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz",
- "integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==",
+ "version": "16.1.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz",
+ "integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==",
"cpu": [
"arm64"
],
@@ -2788,9 +2792,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.0.7",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz",
- "integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==",
+ "version": "16.1.1",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz",
+ "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==",
"cpu": [
"x64"
],
@@ -3567,6 +3571,7 @@
"integrity": "sha512-+kLxJpaJzXybyDyFXYADyP1cznTO8HSuBpenGlnKOAkH4hyNINiywvXS/tGJhsrGGP/gM185RA3xpjY0Yg4erA==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -3633,6 +3638,7 @@
"integrity": "sha512-EHrrEsyhOhxYt8MTg4zTF+DJMuNBzWwgvvOYNj/zm1vnaD/IC5zCXFehZv94Piqa2cRFfXrTFxIvO95L7Qc/cw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.44.1",
"@typescript-eslint/types": "8.44.1",
@@ -4156,6 +4162,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4525,9 +4532,9 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.9",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz",
- "integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==",
+ "version": "2.9.14",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
+ "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@@ -4603,6 +4610,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -4996,7 +5004,8 @@
"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",
@@ -5420,6 +5429,7 @@
"integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5594,6 +5604,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -7691,13 +7702,14 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "16.0.7",
- "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz",
- "integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==",
+ "version": "16.1.1",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz",
+ "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.0.7",
+ "@next/env": "16.1.1",
"@swc/helpers": "0.5.15",
+ "baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@@ -7709,14 +7721,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.0.7",
- "@next/swc-darwin-x64": "16.0.7",
- "@next/swc-linux-arm64-gnu": "16.0.7",
- "@next/swc-linux-arm64-musl": "16.0.7",
- "@next/swc-linux-x64-gnu": "16.0.7",
- "@next/swc-linux-x64-musl": "16.0.7",
- "@next/swc-win32-arm64-msvc": "16.0.7",
- "@next/swc-win32-x64-msvc": "16.0.7",
+ "@next/swc-darwin-arm64": "16.1.1",
+ "@next/swc-darwin-x64": "16.1.1",
+ "@next/swc-linux-arm64-gnu": "16.1.1",
+ "@next/swc-linux-arm64-musl": "16.1.1",
+ "@next/swc-linux-x64-gnu": "16.1.1",
+ "@next/swc-linux-x64-musl": "16.1.1",
+ "@next/swc-win32-arm64-msvc": "16.1.1",
+ "@next/swc-win32-x64-msvc": "16.1.1",
"sharp": "^0.34.4"
},
"peerDependencies": {
@@ -8113,6 +8125,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -8831,6 +8844,7 @@
"integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@@ -8923,6 +8937,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8932,6 +8947,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -8958,6 +8974,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -9764,7 +9781,8 @@
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/tapable": {
"version": "2.2.3",
@@ -9862,6 +9880,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -10038,6 +10057,7 @@
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/package.json b/package.json
index eebdb6b..0135661 100644
--- a/package.json
+++ b/package.json
@@ -46,6 +46,7 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.21",
+ "baseline-browser-mapping": "^2.9.14",
"eslint": "^9",
"eslint-config-next": "15.5.4",
"eslint-plugin-react-hooks": "^5.2.0",
diff --git a/src/app/components/nav/Header.tsx b/src/app/components/nav/Header.tsx
index fd98cd8..fd42b80 100644
--- a/src/app/components/nav/Header.tsx
+++ b/src/app/components/nav/Header.tsx
@@ -27,26 +27,42 @@ import {
} from '@heroicons/react/24/outline'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import useAuthStore from '../../store/authStore';
-import { Avatar } from '../avatar';
+import { Avatar } from '../avatar'
+// ENV-BASED FEATURE FLAGS (string envs: treat "false" as off, everything else as on)
+const DISPLAY_NEWS = process.env.NEXT_PUBLIC_DISPLAY_NEWS !== 'false'
+const DISPLAY_MEMBERSHIP = process.env.NEXT_PUBLIC_DISPLAY_MEMBERSHIP !== 'false'
+const DISPLAY_ABOUT_US = process.env.NEXT_PUBLIC_DISPLAY_ABOUT_US !== 'false'
+const DISPLAY_MATRIX = process.env.NEXT_PUBLIC_DISPLAY_MATRIX !== 'false'
+const DISPLAY_ABONEMMENTS = process.env.NEXT_PUBLIC_DISPLAY_ABONEMMENTS !== 'false'
+const DISPLAY_POOLS = process.env.NEXT_PUBLIC_DISPLAY_POOLS !== 'false'
+
+// Replace current shopItems / informationItems / navLinks block
+// ...existing code...
// Replace current shopItems definition with detailed version (adds icon & description)
const shopItems = [
{ name: 'VIP', href: '/shop/vip', description: 'Exclusive VIP shop', icon: ShoppingBagIcon },
{ name: 'Public', href: '/shop/public', description: 'Open catalog for everyone', icon: UsersIcon },
-];
+]
+// Information dropdown, controlled by env flags
const informationItems = [
{ name: 'Affiliate-Links', href: '/affiliate-links', description: 'Browse our partner links' },
- { name: 'Memberships', href: '/memberships', description: 'Explore membership options' },
- { name: 'About us', href: '/about-us', description: 'Learn more about us' },
-];
+ ...(DISPLAY_MEMBERSHIP
+ ? [{ name: 'Memberships', href: '/memberships', description: 'Explore membership options' }]
+ : []),
+ ...(DISPLAY_ABOUT_US
+ ? [{ name: 'About us', href: '/about-us', description: 'Learn more about us' }]
+ : []),
+]
+// Top-level navigation links, controlled by env flags
const navLinks = [
- { name: 'News', href: '/news' },
-];
+ ...(DISPLAY_NEWS ? [{ name: 'News', href: '/news' }] : []),
+]
// Toggle visibility of Shop navigation across header (desktop + mobile)
-const showShop = false;
+const showShop = false
export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
@@ -312,18 +328,24 @@ export default function Header() {
>
Referral Management
-
-
+
+ {DISPLAY_MATRIX && (
+
+ )}
+
+ {DISPLAY_ABONEMMENTS && (
+
+ )}
>
)}
@@ -479,10 +501,7 @@ export default function Header() {
User Verify
{/* Updated Management dropdown */}
-
+
-
+
+ {DISPLAY_MATRIX && (
+
+ )}
+
-
-
-
+
+ {DISPLAY_ABONEMMENTS && (
+ <>
+
+
+ >
+ )}
+
+ {DISPLAY_POOLS && (
+
+ )}
+
-
+
+ {DISPLAY_NEWS && (
+
+ )}
)}
@@ -735,18 +770,22 @@ export default function Header() {
>
Referral Management
-
-
+ {DISPLAY_MATRIX && (
+
+ )}
+ {DISPLAY_ABONEMMENTS && (
+
+ )}
>
)}
diff --git a/src/app/components/toast/toastComponent.tsx b/src/app/components/toast/toastComponent.tsx
new file mode 100644
index 0000000..3c4ac45
--- /dev/null
+++ b/src/app/components/toast/toastComponent.tsx
@@ -0,0 +1,264 @@
+'use client'
+
+import React, {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+ type ReactNode
+} from 'react'
+import { createRoot } from 'react-dom/client'
+
+type ToastVariant = 'success' | 'error' | 'info' | 'warning'
+
+export interface ToastOptions {
+ id?: string
+ title?: string
+ message: string
+ variant?: ToastVariant
+ duration?: number // ms, default 4000
+}
+
+// add optional closing flag for exit animation
+interface ToastInternal extends ToastOptions {
+ id: string
+ closing?: boolean
+}
+
+interface ToastContextValue {
+ showToast: (options: ToastOptions) => void
+}
+
+// increase fade duration
+const TOAST_ANIMATION_MS = 400 // fade-out duration in ms
+
+// --- global toast store so toasts survive route changes ---
+let globalToasts: ToastInternal[] = []
+type ToastListener = (toasts: ToastInternal[]) => void
+const toastListeners = new Set()
+const toastTimeouts = new Map>()
+
+// NEW: portal state (single global React root)
+let toastPortalContainer: HTMLDivElement | null = null
+let toastPortalRoot: ReturnType | null = null
+let toastPortalMounted = false
+
+function notifyToastListeners() {
+ for (const listener of toastListeners) {
+ listener(globalToasts)
+ }
+}
+
+function removeToastInternal(id: string) {
+ // clear auto-dismiss timer
+ const timeout = toastTimeouts.get(id)
+ if (timeout) {
+ clearTimeout(timeout)
+ toastTimeouts.delete(id)
+ }
+
+ const existing = globalToasts.find(t => t.id === id)
+ if (!existing) return
+
+ if (existing.closing) {
+ // already closing: hard-remove immediately
+ globalToasts = globalToasts.filter(t => t.id !== id)
+ notifyToastListeners()
+ return
+ }
+
+ // mark as closing to trigger fade-out
+ globalToasts = globalToasts.map(t =>
+ t.id === id ? { ...t, closing: true } : t
+ )
+ notifyToastListeners()
+
+ // remove after animation finishes
+ setTimeout(() => {
+ globalToasts = globalToasts.filter(t => t.id !== id)
+ notifyToastListeners()
+ }, TOAST_ANIMATION_MS)
+}
+
+function addToast(options: ToastOptions) {
+ const id = options.id ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`
+ const toast: ToastInternal = {
+ id,
+ variant: options.variant ?? 'info',
+ duration: options.duration ?? 4000,
+ ...options
+ }
+
+ globalToasts = [...globalToasts, toast]
+ notifyToastListeners()
+
+ if (toast.duration && toast.duration > 0) {
+ const timeout = setTimeout(() => {
+ removeToastInternal(id)
+ }, toast.duration)
+ toastTimeouts.set(id, timeout)
+ }
+}
+
+// NEW: mount a global portal once per browser session
+function ensureToastPortalMounted() {
+ if (toastPortalMounted) return
+ if (typeof document === 'undefined') return
+
+ toastPortalMounted = true
+ toastPortalContainer = document.createElement('div')
+ document.body.appendChild(toastPortalContainer)
+ toastPortalRoot = createRoot(toastPortalContainer)
+
+ // Defer actual render to avoid triggering nested updates during React render
+ setTimeout(() => {
+ if (!toastPortalRoot) return
+ toastPortalRoot.render()
+ }, 0)
+}
+
+// --- context & provider ---
+
+const ToastContext = createContext(undefined)
+
+export function useToast(): ToastContextValue {
+ const ctx = useContext(ToastContext)
+ if (ctx) {
+ // Normal path: inside
+ return ctx
+ }
+
+ // Fallback path: no provider mounted, still use global store/portal
+ if (typeof window !== 'undefined') {
+ ensureToastPortalMounted()
+ }
+
+ return {
+ showToast: (options: ToastOptions) => {
+ addToast(options)
+ }
+ }
+}
+
+interface ToastProviderProps {
+ children: ReactNode
+}
+
+export function ToastProvider({ children }: ToastProviderProps) {
+ // ensure the global portal is mounted when any provider appears
+ useEffect(() => {
+ ensureToastPortalMounted()
+ }, [])
+
+ const showToast = useCallback((options: ToastOptions) => {
+ addToast(options)
+ }, [])
+
+ return (
+
+ {children}
+
+ )
+}
+
+// NEW: global viewport rendered via portal, independent of pages/providers
+function ToastViewport() {
+ const [toasts, setToasts] = useState(globalToasts)
+
+ useEffect(() => {
+ const listener: ToastListener = (next) => setToasts(next)
+ toastListeners.add(listener)
+ setToasts(globalToasts)
+ return () => {
+ toastListeners.delete(listener)
+ }
+ }, [])
+
+ const handleClose = useCallback((id: string) => {
+ removeToastInternal(id)
+ }, [])
+
+ if (!toasts.length) return null
+
+ return (
+
+
+ {toasts.map(t => (
+ handleClose(t.id)} />
+ ))}
+
+
+ )
+}
+
+interface ToastItemProps {
+ toast: ToastInternal
+ onClose: () => void
+}
+
+function ToastItem({ toast, onClose }: ToastItemProps) {
+ const { title, message, variant } = toast
+
+ // local visible state for entry animation
+ const [visible, setVisible] = useState(false)
+
+ useEffect(() => {
+ const frame = requestAnimationFrame(() => setVisible(true))
+ return () => cancelAnimationFrame(frame)
+ }, [])
+
+ const variantClasses: Record = {
+ success: 'border-emerald-400/80 bg-emerald-900/90',
+ error: 'border-red-400/80 bg-red-900/90',
+ info: 'border-sky-400/80 bg-slate-900/90',
+ warning: 'border-amber-400/80 bg-amber-900/90'
+ }
+
+ const iconBg: Record = {
+ success: 'bg-emerald-500/10 text-emerald-300',
+ error: 'bg-red-500/10 text-red-300',
+ info: 'bg-sky-500/10 text-sky-300',
+ warning: 'bg-amber-500/10 text-amber-300'
+ }
+
+ const isClosing = !!toast.closing
+ const motionClasses = isClosing
+ ? 'opacity-0 translate-y-2'
+ : visible
+ ? 'opacity-100 translate-y-0'
+ : 'opacity-0 translate-y-2'
+
+ return (
+
+
+ {/* Simple dot indicator */}
+
+
+
+ {title && (
+
+ {title}
+
+ )}
+
{message}
+
+
+
+ )
+}
diff --git a/src/app/login/components/LoginForm.tsx b/src/app/login/components/LoginForm.tsx
index 0763f60..336e31c 100644
--- a/src/app/login/components/LoginForm.tsx
+++ b/src/app/login/components/LoginForm.tsx
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
import { useLogin } from '../hooks/useLogin'
+import { useToast } from '../../components/toast/toastComponent'
export default function LoginForm() {
const [showPassword, setShowPassword] = useState(false)
@@ -13,11 +14,11 @@ export default function LoginForm() {
password: '',
rememberMe: false
})
- const [viewportWidth, setViewportWidth] = useState(
- typeof window !== 'undefined' ? window.innerWidth : 1200
- )
+ // FIX: use a static initial width so SSR and first client render match
+ const [viewportWidth, setViewportWidth] = useState(1200)
const router = useRouter()
const { login, error, setError, loading } = useLogin()
+ const { showToast } = useToast()
// Responsive ball visibility
useEffect(() => {
@@ -30,6 +31,7 @@ export default function LoginForm() {
// Track viewport width for dynamic scaling
useEffect(() => {
const handleResize = () => setViewportWidth(window.innerWidth)
+ handleResize() // initialize on mount (runs only on client)
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
@@ -72,11 +74,29 @@ export default function LoginForm() {
if (!validateForm()) return
- await login({
+ const result = await login({
email: formData.email,
password: formData.password,
rememberMe: formData.rememberMe
})
+
+ if (result?.success) {
+ showToast({
+ variant: 'success',
+ title: 'Login successful',
+ message: 'You are now logged in.'
+ })
+
+ const redirectPath = (result as any).redirectPath || '/dashboard'
+ // instant redirect; toast persists via global store
+ router.push(redirectPath)
+ } else {
+ showToast({
+ variant: 'error',
+ title: 'Login failed',
+ message: result?.error || 'Login failed. Please check your credentials and try again.'
+ })
+ }
}
// Dynamic breakpoints
diff --git a/src/app/login/hooks/useLogin.ts b/src/app/login/hooks/useLogin.ts
index 481d246..43727af 100644
--- a/src/app/login/hooks/useLogin.ts
+++ b/src/app/login/hooks/useLogin.ts
@@ -114,7 +114,7 @@ export function useLogin() {
status: progressData.status
})
- // Redirect to dashboard only if all steps completed AND status is active
+ // Redirect decision logic (keep as-is, but do not push here)
if (allStepsCompleted && isActive) {
redirectPath = '/dashboard'
console.log('✅ User fully onboarded, redirecting to dashboard')
@@ -128,10 +128,8 @@ export function useLogin() {
console.error('❌ Error fetching user status-progress:', statusError)
}
- // Redirect based on status check
- router.push(redirectPath)
-
- return { success: true, user: data.user }
+ // NOTE: no router.push here; caller will handle redirect after showing toast
+ return { success: true, user: data.user, redirectPath }
} else {
throw new Error(data.message || 'Login failed')
}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index 5e56bf3..1e677c0 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -6,19 +6,26 @@ import LoginForm from './components/LoginForm'
import PageLayout from '../components/PageLayout'
import useAuthStore from '../store/authStore'
import GlobalAnimatedBackground from '../background/GlobalAnimatedBackground'
+import { ToastProvider } from '../components/toast/toastComponent'
export default function LoginPage() {
const [showBackground, setShowBackground] = useState(false)
+ const [hasHydrated, setHasHydrated] = useState(false)
const router = useRouter()
const user = useAuthStore(state => state.user)
-
- // Check if user is already logged in
+
+ // Mark when the component has hydrated on the client
+ useEffect(() => {
+ setHasHydrated(true)
+ }, [])
+
+ // Redirect if user is already logged in
useEffect(() => {
if (user) {
router.push('/dashboard')
}
}, [user, router])
-
+
// Responsive background detection
useEffect(() => {
const handleResize = () => setShowBackground(window.innerWidth >= 768)
@@ -26,37 +33,41 @@ export default function LoginPage() {
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
-
- // Don't render if user is already logged in
- if (user) {
+
+ // Don't render if user is already logged in (only after hydration to avoid SSR mismatch)
+ if (hasHydrated && user) {
return (
-
-
-
-
-
You are already logged in. Redirecting...
+
+
+
+
+
+
You are already logged in. Redirecting...
+
-
-
+
+
)
}
return (
-
-
-
-
-
+
+
)
}
\ No newline at end of file
diff --git a/src/app/quickaction-dashboard/page.tsx b/src/app/quickaction-dashboard/page.tsx
index 6475caf..747457d 100644
--- a/src/app/quickaction-dashboard/page.tsx
+++ b/src/app/quickaction-dashboard/page.tsx
@@ -204,8 +204,18 @@ export default function QuickActionDashboardPage() {
return (
-
-
+
+ {/* Animated background */}
+
+ {/* Soft gradient blobs */}
+
+
+
+ {/* Subtle radial highlight */}
+
+
+
+
{/* Title */}
diff --git a/src/app/quickaction-dashboard/register-additional-information/company/page.tsx b/src/app/quickaction-dashboard/register-additional-information/company/page.tsx
index 4e0792c..5fc2ade 100644
--- a/src/app/quickaction-dashboard/register-additional-information/company/page.tsx
+++ b/src/app/quickaction-dashboard/register-additional-information/company/page.tsx
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus'
+import { useToast } from '../../../components/toast/toastComponent'
interface CompanyProfileData {
companyName: string
@@ -50,6 +51,7 @@ export default function CompanyAdditionalInformationPage() {
const router = useRouter()
const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus()
+ const { showToast } = useToast()
const [form, setForm] = useState(init)
const [loading, setLoading] = useState(false)
@@ -68,12 +70,24 @@ export default function CompanyAdditionalInformationPage() {
]
for (const k of required) {
if (!form[k].trim()) {
- setError('Bitte alle Pflichtfelder ausfüllen.')
+ const msg = 'Bitte alle Pflichtfelder ausfüllen.'
+ setError(msg)
+ showToast({
+ variant: 'error',
+ title: 'Missing information',
+ message: msg,
+ })
return false
}
}
if (!/^([A-Z]{2}\d{2}[A-Z0-9]{10,30})$/i.test(form.iban.replace(/\s+/g,''))) {
- setError('Ungültige IBAN.')
+ const msg = 'Ungültige IBAN.'
+ setError(msg)
+ showToast({
+ variant: 'error',
+ title: 'Invalid IBAN',
+ message: msg,
+ })
return false
}
setError('')
@@ -86,7 +100,13 @@ export default function CompanyAdditionalInformationPage() {
if (!validate()) return
if (!accessToken) {
- setError('Not authenticated. Please log in again.')
+ const msg = 'Not authenticated. Please log in again.'
+ setError(msg)
+ showToast({
+ variant: 'error',
+ title: 'Authentication error',
+ message: msg,
+ })
return
}
@@ -122,6 +142,11 @@ export default function CompanyAdditionalInformationPage() {
}
setSuccess(true)
+ showToast({
+ variant: 'success',
+ title: 'Profile saved',
+ message: 'Your company profile has been saved successfully.',
+ })
// Refresh user status to update profile completion state
await refreshStatus()
@@ -141,7 +166,13 @@ export default function CompanyAdditionalInformationPage() {
} catch (error: any) {
console.error('Company profile save error:', error)
- setError(error.message || 'Speichern fehlgeschlagen.')
+ const msg = error.message || 'Speichern fehlgeschlagen.'
+ setError(msg)
+ showToast({
+ variant: 'error',
+ title: 'Save failed',
+ message: msg,
+ })
} finally {
setLoading(false)
}
@@ -149,243 +180,243 @@ export default function CompanyAdditionalInformationPage() {
return (
-
- {/* Background */}
-
-
-
-
+
+ {/* Animated background (same as dashboard) */}
+
+ {/* Soft gradient blobs */}
+
+
+
+ {/* Subtle radial highlight */}
+
-