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 */} -
-
- -

) diff --git a/src/app/quickaction-dashboard/register-sign-contract/company/page.tsx b/src/app/quickaction-dashboard/register-sign-contract/company/page.tsx index 19f01b3..a8bbe9a 100644 --- a/src/app/quickaction-dashboard/register-sign-contract/company/page.tsx +++ b/src/app/quickaction-dashboard/register-sign-contract/company/page.tsx @@ -6,11 +6,13 @@ import PageLayout from '../../../components/PageLayout' import useAuthStore from '../../../store/authStore' import { useUserStatus } from '../../../hooks/useUserStatus' import { API_BASE_URL } from '../../../utils/api' +import { useToast } from '../../../components/toast/toastComponent' // NEW export default function CompanySignContractPage() { const router = useRouter() const { accessToken } = useAuthStore() const { refreshStatus } = useUserStatus() + const { showToast } = useToast() // NEW const [companyName, setCompanyName] = useState('') const [repName, setRepName] = useState('') @@ -77,7 +79,7 @@ export default function CompanySignContractPage() { e.preventDefault() if (!valid()) { // Detailed error message to help debug - const issues = [] + const issues: string[] = [] if (companyName.trim().length < 3) issues.push('Company name (min 3 characters)') if (repName.trim().length < 3) issues.push('Representative name (min 3 characters)') if (repTitle.trim().length < 2) issues.push('Representative title (min 2 characters)') @@ -86,12 +88,24 @@ export default function CompanySignContractPage() { if (!agreeData) issues.push('Privacy policy accepted') if (!confirmSignature) issues.push('Electronic signature confirmed') - setError(`Please complete: ${issues.join(', ')}`) + const msg = `Please complete: ${issues.join(', ')}` + setError(msg) + showToast({ + variant: 'error', + title: 'Missing information', + message: msg, + }) 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 } @@ -137,6 +151,11 @@ export default function CompanySignContractPage() { } setSuccess(true) + showToast({ + variant: 'success', + title: 'Contract signed', + message: 'Your company contract has been signed successfully.', + }) // Refresh user status to update contract signed state await refreshStatus() @@ -156,7 +175,13 @@ export default function CompanySignContractPage() { } catch (error: any) { console.error('Contract signing error:', error) - setError(error.message || 'Signature failed. Please try again.') + const msg = error.message || 'Signature failed. Please try again.' + setError(msg) + showToast({ + variant: 'error', + title: 'Signature failed', + message: msg, + }) } finally { setSubmitting(false) } @@ -164,255 +189,255 @@ export default function CompanySignContractPage() { return ( -
- {/* Background */} -
-
- -