diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 5a6727a..8227eb4 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,6 +1,7 @@ 'use client' import PageLayout from '../components/PageLayout' +import Waves from '../components/waves' import { UsersIcon, ExclamationTriangleIcon, @@ -84,74 +85,94 @@ export default function AdminDashboardPage() { return ( -
-
- {/* Header */} -
-
-

Admin Dashboard

-

- Manage all administrative features, user management, permissions, and global settings. -

-
-
+
+ - {/* Warning banner */} -
- -
-

- Warning: Settings and actions below this point can have consequences for the entire system! -

-

- Manage all administrative features, user management, permissions, and global settings. -

-
-
- - {/* Stats Card */} -
-
-
Total Users
-
{displayStats.totalUsers}
-
-
-
Admins
-
{displayStats.adminUsers}
-
-
-
Active
-
{displayStats.activeUsers}
-
-
-
Pending Verification
-
{displayStats.verificationPending}
-
-
-
Personal
-
{displayStats.personalUsers}
-
-
-
Company
-
{displayStats.companyUsers}
-
-
- - {/* Management Shortcuts Card */} -
-
-
-
- -
+
+
+
+ {/* Header */} +
-

Management Shortcuts

-

- Quick access to common admin modules. +

Admin Dashboard

+

+ Manage all administrative features, user management, permissions, and global settings. +

+
+
+ + {/* Warning banner */} +
+ +
+

+ Warning: Settings and actions below this point can have consequences for the entire system! +

+

+ Manage all administrative features, user management, permissions, and global settings.

-
+ + {/* Stats Card */} +
+
+
Total Users
+
{displayStats.totalUsers}
+
+
+
Admins
+
{displayStats.adminUsers}
+
+
+
Active
+
{displayStats.activeUsers}
+
+
+
Pending Verification
+
{displayStats.verificationPending}
+
+
+
Personal
+
{displayStats.personalUsers}
+
+
+
Company
+
{displayStats.companyUsers}
+
+
+ + {/* Management Shortcuts Card */} +
+
+
+
+ +
+
+

Management Shortcuts

+

+ Quick access to common admin modules. +

+
+
+
{/* Matrix Management */} -
-
-
- - {/* Server Status & Logs */} -
-
-
- -
-
-

- Server Status & Logs -

-

- System health, resource usage & recent error insights. -

-
-
- -
- {/* Metrics */} -
-
- -

- Server Status:{' '} - - {serverStats.status === 'Online' ? 'Server Online' : 'Offline'} - -

-
-
-

Uptime: {serverStats.uptime}

-

CPU Usage: {serverStats.cpu}

-

Memory Usage: {serverStats.memory} GB

-
-
- - Autoscaled environment (mock) +
- {/* Divider */} -
+ {/* Server Status & Logs */} +
+
+
+ +
+
+

+ Server Status & Logs +

+

+ System health, resource usage & recent error insights. +

+
+
- {/* Logs */} -
-

- Recent Error Logs -

- {serverStats.recentErrors.length === 0 && ( -

- No recent logs. -

- )} - {/* Placeholder for future logs list */} - {/* TODO: Replace with mapped log entries */} -
- +
+ {/* Metrics */} +
+
+ +

+ Server Status:{' '} + + {serverStats.status === 'Online' ? 'Server Online' : 'Offline'} + +

+
+
+

Uptime: {serverStats.uptime}

+

CPU Usage: {serverStats.cpu}

+

Memory Usage: {serverStats.memory} GB

+
+
+ + Autoscaled environment (mock) +
+
+ + {/* Divider */} +
+ + {/* Logs */} +
+

+ Recent Error Logs +

+ {serverStats.recentErrors.length === 0 && ( +

+ No recent logs. +

+ )} + {/* Placeholder for future logs list */} + {/* TODO: Replace with mapped log entries */} +
+ +
+
-
- -
+
+
) diff --git a/src/app/affiliate-links/page.tsx b/src/app/affiliate-links/page.tsx index 6887837..173aaf1 100644 --- a/src/app/affiliate-links/page.tsx +++ b/src/app/affiliate-links/page.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useMemo } from 'react' import PageLayout from '../components/PageLayout' +import Waves from '../components/waves' type Affiliate = { id: string @@ -89,105 +90,127 @@ export default function AffiliateLinksPage() { return ( -
-
- {/* Header (aligned with management pages) */} -
-
-

Affiliate Partners

-

- Discover our trusted partners and earn commissions through affiliate links. -

-
- {/* NEW: Category filter */} -
- - -
-
+
+ - {/* States */} - {loading && ( -
-
-

Loading affiliate partners...

-
- )} - - {error && !loading && ( -
- {error} -
- )} - - {!loading && !error && posts.length === 0 && ( -
- No affiliate partners available at the moment. -
- )} - - {/* Cards (aligned to white panels, border, shadow) */} - {!loading && !error && posts.length > 0 && ( -
- {posts.map((post) => { - // NEW: highlight when matches selected category (keep all visible) - const isHighlighted = selectedCategory !== 'all' && post.category.title === selectedCategory - return ( -
+
+
+ {/* Header (aligned with management pages) */} +
+
+

Affiliate Partners

+

+ Discover our trusted partners and earn commissions through affiliate links. +

+
+ {/* NEW: Category filter */} +
+ + +
+
+ + {/* States */} + {loading && ( +
+
+

Loading affiliate partners...

+
+ )} + + {error && !loading && ( +
+ {error} +
+ )} + + {!loading && !error && posts.length === 0 && ( +
+ No affiliate partners available at the moment. +
+ )} + + {/* Cards (aligned to white panels, border, shadow) */} + {!loading && !error && posts.length > 0 && ( +
+ {posts.map((post) => { + // NEW: highlight when matches selected category (keep all visible) + const isHighlighted = selectedCategory !== 'all' && post.category.title === selectedCategory + return ( + + ) + })} +
+ )}
- )} -
+
+
) diff --git a/src/app/components/PageLayout.tsx b/src/app/components/PageLayout.tsx index 11f1a9e..4dd2272 100644 --- a/src/app/components/PageLayout.tsx +++ b/src/app/components/PageLayout.tsx @@ -1,6 +1,7 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { usePathname } from 'next/navigation'; import Header from './nav/Header'; import Footer from './Footer'; import PageTransitionEffect from './animation/pageTransitionEffect'; @@ -15,18 +16,39 @@ interface PageLayoutProps { children: React.ReactNode; showHeader?: boolean; showFooter?: boolean; + className?: string; + contentClassName?: string; } export default function PageLayout({ children, showHeader = true, - showFooter = true + showFooter = true, + className = 'bg-white text-gray-900', + contentClassName = 'flex-1 relative z-10 w-full', }: PageLayoutProps) { const isMobile = isMobileDevice(); const [isLoggingOut, setIsLoggingOut] = useState(false); // NEW + const pathname = usePathname(); + + // Global scrollbar restore / leak cleanup (runs on navigation) + useEffect(() => { + const html = document.documentElement; + const body = document.body; + + // ensure a visible/stable vertical scrollbar on desktop + html.style.overflowY = 'scroll'; + body.style.overflowY = 'auto'; + + // clear common scroll-lock leftovers (gap where scrollbar should be) + if (html.style.overflow === 'hidden') html.style.overflow = ''; + if (body.style.overflow === 'hidden') body.style.overflow = ''; + html.style.paddingRight = ''; + body.style.paddingRight = ''; + }, [pathname]); return ( -
+
{showHeader && (
@@ -35,7 +57,7 @@ export default function PageLayout({ )} {/* Main content */} -
+
{children}
diff --git a/src/app/components/curvedLoop.tsx b/src/app/components/curvedLoop.tsx new file mode 100644 index 0000000..ce2c37e --- /dev/null +++ b/src/app/components/curvedLoop.tsx @@ -0,0 +1,153 @@ +'use client' + +import { useRef, useEffect, useState, useMemo, useId, FC, PointerEvent } from 'react'; + +interface CurvedLoopProps { + marqueeText?: string; + speed?: number; + className?: string; + curveAmount?: number; + direction?: 'left' | 'right'; + interactive?: boolean; +} + +const CurvedLoop: FC = ({ + marqueeText = '', + speed = 1, + className, + curveAmount = -50, + direction = 'left', + interactive = true +}) => { + const text = useMemo(() => { + const hasTrailing = /\s|\u00A0$/.test(marqueeText); + return (hasTrailing ? marqueeText.replace(/\s+$/, '') : marqueeText) + '\u00A0'; + }, [marqueeText]); + + const measureRef = useRef(null); + const textPathRef = useRef(null); + const pathRef = useRef(null); + const [spacing, setSpacing] = useState(0); + const [offset, setOffset] = useState(0); + const uid = useId(); + const pathId = `curve-${uid}`; + const pathD = `M-100,40 Q500,${40 + curveAmount} 1540,40`; + + const dragRef = useRef(false); + const lastXRef = useRef(0); + const dirRef = useRef<'left' | 'right'>(direction); + const velRef = useRef(0); + + const textLength = spacing; + const totalText = textLength + ? Array(Math.ceil(1800 / textLength) + 2) + .fill(text) + .join('') + : text; + const ready = spacing > 0; + + useEffect(() => { + if (measureRef.current) setSpacing(measureRef.current.getComputedTextLength()); + }, [text, className]); + + useEffect(() => { + if (!spacing) return; + if (textPathRef.current) { + const initial = -spacing; + textPathRef.current.setAttribute('startOffset', initial + 'px'); + setOffset(initial); + } + }, [spacing]); + + useEffect(() => { + if (!spacing || !ready) return; + let frame = 0; + const step = () => { + if (!dragRef.current && textPathRef.current) { + const delta = dirRef.current === 'right' ? speed : -speed; + const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0'); + let newOffset = currentOffset + delta; + const wrapPoint = spacing; + if (newOffset <= -wrapPoint) newOffset += wrapPoint; + if (newOffset > 0) newOffset -= wrapPoint; + textPathRef.current.setAttribute('startOffset', newOffset + 'px'); + setOffset(newOffset); + } + frame = requestAnimationFrame(step); + }; + frame = requestAnimationFrame(step); + return () => cancelAnimationFrame(frame); + }, [spacing, speed, ready]); + + const onPointerDown = (e: PointerEvent) => { + if (!interactive) return; + dragRef.current = true; + lastXRef.current = e.clientX; + velRef.current = 0; + (e.target as HTMLElement).setPointerCapture(e.pointerId); + }; + + const onPointerMove = (e: PointerEvent) => { + if (!interactive || !dragRef.current || !textPathRef.current) return; + const dx = e.clientX - lastXRef.current; + lastXRef.current = e.clientX; + velRef.current = dx; + const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0'); + let newOffset = currentOffset + dx; + const wrapPoint = spacing; + if (newOffset <= -wrapPoint) newOffset += wrapPoint; + if (newOffset > 0) newOffset -= wrapPoint; + textPathRef.current.setAttribute('startOffset', newOffset + 'px'); + setOffset(newOffset); + }; + + const endDrag = () => { + if (!interactive) return; + dragRef.current = false; + dirRef.current = velRef.current > 0 ? 'right' : 'left'; + }; + + const cursorStyle = interactive ? (dragRef.current ? 'grabbing' : 'grab') : 'auto'; + + return ( +
+ + + {text} + + + + + {ready && ( + + + {totalText} + + + )} + +
+ ); +}; + +export default CurvedLoop; diff --git a/src/app/components/nav/Header.tsx b/src/app/components/nav/Header.tsx index 3a5f846..de17930 100644 --- a/src/app/components/nav/Header.tsx +++ b/src/app/components/nav/Header.tsx @@ -30,7 +30,7 @@ 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_ABONEMENTS = process.env.NEXT_PUBLIC_DISPLAY_ABONEMMENTS !== 'false' +const DISPLAY_ABONEMENTS = process.env.NEXT_PUBLIC_DISPLAY_ABONEMENTS !== 'false' const DISPLAY_POOLS = process.env.NEXT_PUBLIC_DISPLAY_POOLS !== 'false' @@ -62,18 +62,31 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) { const [mounted, setMounted] = useState(false) const [animateIn, setAnimateIn] = useState(false) const [scrollY, setScrollY] = useState(0) + const [isMobile, setIsMobile] = useState(false) const user = useAuthStore(s => s.user) const logout = useAuthStore(s => s.logout) const accessToken = useAuthStore(s => s.accessToken) const refreshAuthToken = useAuthStore(s => s.refreshAuthToken) const router = useRouter() const pathname = usePathname() - const isHome = pathname === '/' + const isParallaxPage = + pathname === '/' || + pathname === '/login' || + pathname === '/password-reset' || + pathname === '/register' + + const parallaxEnabled = isParallaxPage && !isMobile + const headerIsFixedOverlay = isParallaxPage && !isMobile + + const headerPositionClass = isParallaxPage + ? (isMobile ? 'sticky top-0 w-full' : 'fixed top-0 left-0 w-full') + : 'relative' const [hasReferralPerm, setHasReferralPerm] = useState(false) const [adminMgmtOpen, setAdminMgmtOpen] = useState(false) const managementRef = useRef(null) const [canSeeDashboard, setCanSeeDashboard] = useState(false) + const headerElRef = useRef(null) const handleLogout = async () => { try { @@ -119,12 +132,25 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) { setAnimateIn(true) }, []) - // Home-page scroll listener: reveal header after first scroll with slight parallax + // Detect mobile devices (for disabling parallax) + useEffect(() => { + const mq = window.matchMedia('(max-width: 768px)') + const apply = () => setIsMobile(mq.matches) + apply() + mq.addEventListener?.('change', apply) + window.addEventListener('resize', apply, { passive: true }) + return () => { + mq.removeEventListener?.('change', apply) + window.removeEventListener('resize', apply) + } + }, []) + + // Home + login scroll listener: reveal header after first scroll with slight parallax useEffect(() => { if (!mounted) return - if (!isHome) { - // non-home: header always visible, no scroll listeners + if (!parallaxEnabled) { + // non-parallax (and mobile): header always visible, no scroll listeners setScrollY(100) return } @@ -149,7 +175,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) { window.removeEventListener('scroll', handleScroll) window.removeEventListener('wheel', handleWheel) } - }, [mounted, isHome]) + }, [mounted, parallaxEnabled]) // Fetch user permissions and set hasReferralPerm useEffect(() => { @@ -309,22 +335,82 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) { ) const isAdmin = mounted && rawIsAdmin - // Only gate visibility by scroll on home; elsewhere just use animateIn - const headerVisible = isHome ? animateIn && scrollY > 24 : animateIn - const parallaxOffset = isHome ? Math.max(-16, -scrollY * 0.15) : 0 + // Only gate visibility by scroll on parallax-enabled pages + const headerVisible = parallaxEnabled ? animateIn && scrollY > 24 : animateIn + const parallaxOffset = parallaxEnabled ? Math.max(-16, -scrollY * 0.15) : 0 + + // When the fixed header becomes visible, expose its height as a CSS variable so + // pages (e.g. /register) can pad their content and avoid being overlapped. + useEffect(() => { + if (!mounted) return + + let raf1 = 0 + let raf2 = 0 + + const applySpacer = () => { + // Only reserve space when header is fixed/overlaying content. + if (!headerIsFixedOverlay) { + document.documentElement.style.setProperty('--pp-header-spacer', '0px') + return + } + + const h = headerElRef.current?.getBoundingClientRect().height ?? 0 + const spacer = headerVisible ? `${Math.ceil(h)}px` : '0px' + document.documentElement.style.setProperty('--pp-header-spacer', spacer) + } + + const applyShiftFade = () => { + if (!headerVisible) { + document.documentElement.style.setProperty('--pp-page-shift-opacity', '1') + return + } + + // Less noticeable dip to avoid "too translucent" look during the shift. + document.documentElement.style.setProperty('--pp-page-shift-opacity', '0.99') + raf1 = window.requestAnimationFrame(() => { + raf2 = window.requestAnimationFrame(() => { + document.documentElement.style.setProperty('--pp-page-shift-opacity', '1') + }) + }) + } + + applySpacer() + applyShiftFade() + + const onResize = () => applySpacer() + window.addEventListener('resize', onResize, { passive: true }) + + return () => { + window.removeEventListener('resize', onResize) + if (raf1) cancelAnimationFrame(raf1) + if (raf2) cancelAnimationFrame(raf2) + } + }, [mounted, headerVisible, headerIsFixedOverlay]) + + // Hard cleanup: if any scroll-lock left padding/overflow on /, remove it when the drawer is closed. + useEffect(() => { + if (mobileMenuOpen) return + const html = document.documentElement + const body = document.body + + body.style.paddingRight = '' + html.style.paddingRight = '' + + if (body.style.overflow === 'hidden') body.style.overflow = '' + if (html.style.overflow === 'hidden') html.style.overflow = '' + }, [mobileMenuOpen]) return (
) } \ No newline at end of file diff --git a/src/app/login/components/LoginForm.tsx b/src/app/login/components/LoginForm.tsx index 0a95e7b..457aace 100644 --- a/src/app/login/components/LoginForm.tsx +++ b/src/app/login/components/LoginForm.tsx @@ -6,32 +6,23 @@ import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline' import { useLogin } from '../hooks/useLogin' import { useToast } from '../../components/toast/toastComponent' +const GLASS_BG = 'rgba(255,255,255,0.55)' + export default function LoginForm() { const [showPassword, setShowPassword] = useState(false) - const [showBall, setShowBall] = useState(true) const [formData, setFormData] = useState({ email: '', password: '', rememberMe: false }) - // 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(() => { - const handleResizeBall = () => setShowBall(window.innerWidth >= 768) - handleResizeBall() - window.addEventListener('resize', handleResizeBall) - return () => window.removeEventListener('resize', handleResizeBall) - }, []) - // Track viewport width for dynamic scaling useEffect(() => { const handleResize = () => setViewportWidth(window.innerWidth) - handleResize() // initialize on mount (runs only on client) + handleResize() window.addEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize) }, []) @@ -47,22 +38,22 @@ export default function LoginForm() { const validateForm = (): boolean => { if (!formData.email.trim()) { - setError('E-Mail-Adresse ist erforderlich') + setError('Email address is required') return false } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { - setError('Bitte gib eine gültige E-Mail-Adresse ein') + setError('Please enter a valid email address') return false } if (!formData.password.trim()) { - setError('Passwort ist erforderlich') + setError('Password is required') return false } if (formData.password.length < 6) { - setError('Passwort muss mindestens 6 Zeichen lang sein') + setError('Password must be at least 6 characters long') return false } @@ -106,7 +97,7 @@ export default function LoginForm() { // CHANGED: Wider base widths; no transform scaling const formWidth = isMobile - ? '94vw' + ? '100%' : isTablet ? '80vw' : isSmallLaptop @@ -114,7 +105,7 @@ export default function LoginForm() { : '52vw' const formMaxWidth = isMobile - ? '480px' + ? '420px' : isTablet ? '760px' : isSmallLaptop @@ -125,145 +116,62 @@ export default function LoginForm() {
- {/* Animated Ball - Desktop Only */} - {showBall && !isMobile && ( -
-
- {/* Inner small circle with cartoony Earth */} -
-
- - - - - - - - - - - - - {/* Land masses (stylized) */} - - - - {/* Atmospheric rim */} - - {/* Light sheen */} - - - {/* Subtle gloss overlay */} - -
-
- {/* Orbiting balls (unchanged) */} - - - - - - - - -
+ {/* Content (title + earth removed) */} +
+ {/* Title + Subtitle (restored) */} +
+

+ PROFIT PLANET +

+

+ Welcome back! Log in to continue. +

- )} - - {/* Content */} -
-

- Profit Planet -

-

- Welcome back! Login to continue. -

{/* Email Field */} -
+
{/* Password Field */} -
+
- - {/* Remember Me & Show Password */} -
-
- - -
-
- setShowPassword(e.target.checked)} - /> - -
-
{/* Error Message */} @@ -374,10 +257,10 @@ export default function LoginForm() {
@@ -402,74 +285,50 @@ export default function LoginForm() { className="text-[#8D6B1D] hover:text-[#7A5E1A] hover:underline text-sm font-medium transition-colors" onClick={() => router.push("/password-reset")} > - Passwort vergessen? + Forgot password?
- - {/* Registration Section */} -
-
-
-
-
-
- {/* Noch kein Account? */} -
-
-
-

- Profit Planet is available by invitation only. -

-

- Contact us for an invitation! -

-
-
- {/* CSS Animations */} + {/* Input animations */}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index fb08020..fafe75f 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -8,9 +8,11 @@ import useAuthStore from '../store/authStore' import { ToastProvider } from '../components/toast/toastComponent' import PageTransitionEffect from '../components/animation/pageTransitionEffect' import Waves from '../components/waves' +import CurvedLoop from '../components/curvedLoop' export default function LoginPage() { const [hasHydrated, setHasHydrated] = useState(false) + const [isMobile, setIsMobile] = useState(false) const router = useRouter() const user = useAuthStore(state => state.user) @@ -19,6 +21,18 @@ export default function LoginPage() { setHasHydrated(true) }, []) + useEffect(() => { + const mq = window.matchMedia('(max-width: 768px)') + const apply = () => setIsMobile(mq.matches) + apply() + mq.addEventListener?.('change', apply) + window.addEventListener('resize', apply, { passive: true }) + return () => { + mq.removeEventListener?.('change', apply) + window.removeEventListener('resize', apply) + } + }, []) + // Redirect if user is already logged in useEffect(() => { if (user) { @@ -47,33 +61,75 @@ export default function LoginPage() { return ( - -
- {/* Waves background */} - -
-
- -
+ {/* NEW: page-level background wrapper so Waves covers everything */} +
+ + + + {/* ...existing code... */} +
+ {/* REMOVED: Waves background moved to wrapper */} + + {isMobile ? ( + // ...existing code... +
+
+ +
+
+ ) : ( + // ...existing code... +
+
+ +
+ +
+ +
+
+ )}
-
- + {/* ...existing code... */} + +
) diff --git a/src/app/page.tsx b/src/app/page.tsx index d1b36ad..3c1ac01 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -11,10 +11,33 @@ import SplitText from './components/SplitText'; export default function HomePage() { const containerRef = useRef(null); const [isHover, setIsHover] = useState(false); + const [isMobile, setIsMobile] = useState(() => { + if (typeof window === 'undefined') return false; + return window.matchMedia('(max-width: 768px)').matches; + }); const router = useRouter(); + // Mobile: instantly redirect to login + useEffect(() => { + if (!isMobile) return; + router.replace('/login'); + }, [isMobile, router]); + + // Keep breakpoint updated (resize/orientation) + useEffect(() => { + const mq = window.matchMedia('(max-width: 768px)'); + const apply = () => setIsMobile(mq.matches); + mq.addEventListener?.('change', apply); + window.addEventListener('resize', apply, { passive: true }); + return () => { + mq.removeEventListener?.('change', apply); + window.removeEventListener('resize', apply); + }; + }, []); + const handleLoginClick = () => { - if (!containerRef.current) { + // Mobile: no page fade animation + if (isMobile || !containerRef.current) { router.push('/login'); return; } @@ -27,8 +50,9 @@ export default function HomePage() { }); }; - // Ensure LOGIN never stays stuck after scrolling / wheel + // Ensure LOGIN never stays stuck after scrolling / wheel (desktop only) useEffect(() => { + if (isMobile) return; const resetHover = () => setIsHover(false); window.addEventListener('wheel', resetHover, { passive: true }); window.addEventListener('scroll', resetHover, { passive: true }); @@ -36,7 +60,10 @@ export default function HomePage() { window.removeEventListener('wheel', resetHover); window.removeEventListener('scroll', resetHover); }; - }, []); + }, [isMobile]); + + // Prevent any home UI flash on mobile + if (isMobile) return null; return ( @@ -44,7 +71,7 @@ export default function HomePage() { ref={containerRef} className="min-h-screen flex items-center justify-center relative overflow-hidden bg-black text-white" > - {/* Waves background (reverted settings) */} + {/* Waves background */}

setIsHover(true)} - onMouseLeave={() => setIsHover(false)} onClick={handleLoginClick} + onMouseEnter={isMobile ? undefined : () => setIsHover(true)} + onMouseLeave={isMobile ? undefined : () => setIsHover(false)} className="cursor-pointer" > - + {isMobile ? ( + + PROFIT PLANET + + ) : ( + + )}

- + {/* No parallax/crosshair on mobile */} + {!isMobile && }
); diff --git a/src/app/password-reset/page.tsx b/src/app/password-reset/page.tsx index f1acaae..605b660 100644 --- a/src/app/password-reset/page.tsx +++ b/src/app/password-reset/page.tsx @@ -3,8 +3,10 @@ import { useState, useEffect } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import PageLayout from '../components/PageLayout' +import Waves from '../components/waves' +import { ToastProvider, useToast } from '../components/toast/toastComponent' -export default function PasswordResetPage() { +function PasswordResetPageInner() { const searchParams = useSearchParams() const router = useRouter() const token = searchParams.get('token') @@ -22,6 +24,7 @@ export default function PasswordResetPage() { const [resetLoading, setResetLoading] = useState(false) const [resetSuccess, setResetSuccess] = useState(false) const [resetError, setResetError] = useState('') + const { showToast } = useToast() // Basic validators const validEmail = (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val) @@ -40,7 +43,13 @@ export default function PasswordResetPage() { e.preventDefault() if (requestLoading) return if (!validEmail(email)) { - setRequestError('Bitte eine gültige E-Mail eingeben.') + const msg = 'Please enter a valid email address.' + setRequestError(msg) + showToast({ + variant: 'error', + title: 'Invalid email', + message: msg, + }) return } setRequestError('') @@ -49,8 +58,19 @@ export default function PasswordResetPage() { // TODO: call API endpoint: POST /auth/password-reset/request await new Promise(r => setTimeout(r, 1100)) setRequestSuccess(true) + showToast({ + variant: 'success', + title: 'Password reset email', + message: 'If this email exists, a reset link has been sent.', + }) } catch { - setRequestError('Anfrage fehlgeschlagen. Bitte erneut versuchen.') + const msg = 'Request failed. Please try again.' + setRequestError(msg) + showToast({ + variant: 'error', + title: 'Request failed', + message: msg, + }) } finally { setRequestLoading(false) } @@ -60,11 +80,23 @@ export default function PasswordResetPage() { e.preventDefault() if (resetLoading) return if (!validPassword(password)) { - setResetError('Passwort erfüllt nicht die Anforderungen.') + const msg = 'Password does not meet the requirements.' + setResetError(msg) + showToast({ + variant: 'error', + title: 'Invalid password', + message: msg, + }) return } if (password !== confirmPassword) { - setResetError('Passwörter stimmen nicht überein.') + const msg = 'Passwords do not match.' + setResetError(msg) + showToast({ + variant: 'error', + title: 'Passwords do not match', + message: msg, + }) return } setResetError('') @@ -73,89 +105,92 @@ export default function PasswordResetPage() { // TODO: call API endpoint: POST /auth/password-reset/confirm { token, password } await new Promise(r => setTimeout(r, 1200)) setResetSuccess(true) + showToast({ + variant: 'success', + title: 'Password updated', + message: 'Your password has been changed. Redirecting to login...', + }) } catch { - setResetError('Zurücksetzen fehlgeschlagen. Bitte erneut versuchen.') + const msg = 'Reset failed. Please try again.' + setResetError(msg) + showToast({ + variant: 'error', + title: 'Reset failed', + message: msg, + }) } finally { setResetLoading(false) } } const passwordHints = [ - { label: 'Mindestens 8 Zeichen', pass: password.length >= 8 }, - { label: 'Großbuchstabe (A-Z)', pass: /[A-Z]/.test(password) }, - { label: 'Kleinbuchstabe (a-z)', pass: /[a-z]/.test(password) }, - { label: 'Ziffer (0-9)', pass: /\d/.test(password) }, - { label: 'Sonderzeichen (!@#$...)', pass: /[\W_]/.test(password) } + { label: 'At least 8 characters', pass: password.length >= 8 }, + { label: 'Uppercase letter (A-Z)', pass: /[A-Z]/.test(password) }, + { label: 'Lowercase letter (a-z)', pass: /[a-z]/.test(password) }, + { label: 'Number (0-9)', pass: /\d/.test(password) }, + { label: 'Special character (!@#$...)', pass: /[\W_]/.test(password) } ] return ( -
- {/* Background Pattern */} - - {/* Colored Blur Effect */} -
+
) +} + +export default function PasswordResetPage() { + return ( + + + + ) } \ No newline at end of file diff --git a/src/app/profile/components/bankInformation.tsx b/src/app/profile/components/bankInformation.tsx index af97c81..918d456 100644 --- a/src/app/profile/components/bankInformation.tsx +++ b/src/app/profile/components/bankInformation.tsx @@ -17,74 +17,42 @@ export default function BankInformation({ setBankInfo: (v: { accountHolder: string, iban: string }) => void, onEdit?: () => void }) { + // editing disabled for now; keep props to avoid refactors + const accountHolder = profileData.accountHolder || '' + const iban = profileData.iban || '' + return ( -
+

Bank Information

- {!editingBank && ( - - )} + Editing disabled
-
{ - e.preventDefault() - setBankInfo(bankDraft) - setEditingBank(false) - }} - > + +
setBankDraft({ ...bankDraft, accountHolder: e.target.value })} - disabled={!editingBank} - placeholder={profileData.accountHolder ? '' : 'Not provided'} + className="w-full p-2 border border-white/60 rounded-lg bg-white/50 text-gray-900" + value={accountHolder} + disabled + placeholder="Not provided" /> - {!editingBank && !profileData.accountHolder && ( -
Not provided
- )} + {!accountHolder &&
Not provided
}
+
setBankDraft({ ...bankDraft, iban: e.target.value })} - disabled={!editingBank} - placeholder={profileData.iban ? '' : 'Not provided'} + className="w-full p-2 border border-white/60 rounded-lg bg-white/50 text-gray-900" + value={iban} + disabled + placeholder="Not provided" /> - {!editingBank && !profileData.iban && ( -
Not provided
- )} + {!iban &&
Not provided
}
- {editingBank && ( -
- - -
- )} - +
) } diff --git a/src/app/profile/components/basicInformation.tsx b/src/app/profile/components/basicInformation.tsx index fc8c98c..6d8c6e2 100644 --- a/src/app/profile/components/basicInformation.tsx +++ b/src/app/profile/components/basicInformation.tsx @@ -7,7 +7,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd onEdit?: () => void }) { return ( -
+

Basic Information

+ + +
+
-
- {/* Quick Actions */} -
-

Quick Actions

- -
- - - + + {/* Bank Info, Media */} +
+ {/* --- My Abo Section (above bank info) --- */} + + {/* --- Edit Bank Information Section --- */} + openEditModal('bank', { ... })} + /> + {/* --- Media Section --- */} +
-
+ - {/* Bank Info, Media */} -
- {/* --- My Abo Section (above bank info) --- */} - - {/* --- Edit Bank Information Section --- */} - openEditModal('bank', { - accountHolder: profileData.accountHolder, - iban: profileData.iban, - })} - /> - {/* --- Media Section --- */} - -
- - {/* Account Settings */} -
-

Account Settings

-
-
-
-

Email Notifications

-

Receive updates about orders and promotions

-
- -
- -
-
-

SMS Notifications

-

Get text messages for important updates

-
- -
- -
-
-

Two-Factor Authentication

-

Add extra security to your account

-
- -
-
-
- - )} -
- -
) } \ No newline at end of file diff --git a/src/app/register/components/RegisterForm.tsx b/src/app/register/components/RegisterForm.tsx index e21d74a..a59b7e6 100644 --- a/src/app/register/components/RegisterForm.tsx +++ b/src/app/register/components/RegisterForm.tsx @@ -142,17 +142,16 @@ export default function RegisterForm({ } const phoneApi = personalPhoneRef.current + const dialCode = phoneApi?.getDialCode?.() const intlNumber = phoneApi?.getNumber() || '' const valid = phoneApi?.isValid() ?? false - console.log('[RegisterForm] validatePersonalForm phone check', { - rawState: personalForm.phoneNumber, - intlFromApi: intlNumber, - isValidFromApi: valid, - }) - + if (!dialCode) { + setError('Please select a country code from the dropdown before continuing.') + return false + } if (!intlNumber) { - setError('Please enter your phone number including country code.') + setError('Please enter your phone number.') return false } if (!valid) { @@ -191,22 +190,20 @@ export default function RegisterForm({ const companyApi = companyPhoneRef.current const contactApi = contactPhoneRef.current + const companyDialCode = companyApi?.getDialCode?.() + const contactDialCode = contactApi?.getDialCode?.() + const companyNumber = companyApi?.getNumber() || '' const contactNumber = contactApi?.getNumber() || '' const companyValid = companyApi?.isValid() ?? false const contactValid = contactApi?.isValid() ?? false - console.log('[RegisterForm] validateCompanyForm phone check', { - rawCompany: companyForm.companyPhone, - rawContact: companyForm.contactPersonPhone, - intlCompany: companyNumber, - intlContact: contactNumber, - companyValid, - contactValid, - }) - + if (!companyDialCode || !contactDialCode) { + setError('Please select country codes (dropdown) for both company and contact phone numbers.') + return false + } if (!companyNumber || !contactNumber) { - setError('Please enter both company and contact phone numbers including country codes.') + setError('Please enter both company and contact phone numbers.') return false } if (!companyValid || !contactValid) { @@ -394,7 +391,8 @@ export default function RegisterForm({ } return ( -
+ // softened outer container, no own solid white card – parent provides glass card +
{/* Header */}

@@ -409,7 +407,7 @@ export default function RegisterForm({ {/* Mode Toggle */}
-
+
@@ -472,7 +470,7 @@ export default function RegisterForm({ name="lastName" value={personalForm.lastName} onChange={handlePersonalChange} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" + className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" required />
@@ -489,7 +487,7 @@ export default function RegisterForm({ name="email" value={personalForm.email} onChange={handlePersonalChange} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" + className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" required />
@@ -504,7 +502,7 @@ export default function RegisterForm({ name="confirmEmail" value={personalForm.confirmEmail} onChange={handlePersonalChange} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" + className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" required />

@@ -518,7 +516,8 @@ export default function RegisterForm({ id="phoneNumber" name="phoneNumber" ref={personalPhoneRef} - placeholder="+49 123 456 7890" + autoComplete="tel" + placeholder="e.g. +43 676 1234567" required onChange={e => setPersonalForm(prev => ({ ...prev, phoneNumber: (e.target as HTMLInputElement).value })) @@ -538,7 +537,8 @@ export default function RegisterForm({ name="password" value={personalForm.password} onChange={handlePersonalChange} - className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" + autoComplete="new-password" + className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" required />
@@ -604,7 +605,7 @@ export default function RegisterForm({ name="companyName" value={companyForm.companyName} onChange={handleCompanyChange} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" + className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" required />
@@ -619,7 +620,7 @@ export default function RegisterForm({ name="contactPersonName" value={companyForm.contactPersonName} onChange={handleCompanyChange} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" + className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" required /> @@ -636,7 +637,7 @@ export default function RegisterForm({ name="companyEmail" value={companyForm.companyEmail} onChange={handleCompanyChange} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" + className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" required /> @@ -651,7 +652,7 @@ export default function RegisterForm({ name="confirmCompanyEmail" value={companyForm.confirmCompanyEmail} onChange={handleCompanyChange} - className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" + className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" required /> @@ -666,7 +667,8 @@ export default function RegisterForm({ id="companyPhone" name="companyPhone" ref={companyPhoneRef} - placeholder="+49 123 456 7890" + autoComplete="tel" + placeholder="e.g. +43 1 234567" required onChange={e => setCompanyForm(prev => ({ ...prev, companyPhone: (e.target as HTMLInputElement).value })) @@ -682,7 +684,8 @@ export default function RegisterForm({ id="contactPersonPhone" name="contactPersonPhone" ref={contactPhoneRef} - placeholder="+49 123 456 7890" + autoComplete="tel" + placeholder="e.g. +43 676 1234567" required onChange={e => setCompanyForm(prev => ({ @@ -706,7 +709,8 @@ export default function RegisterForm({ name="password" value={companyForm.password} onChange={handleCompanyChange} - className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" + autoComplete="new-password" + className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary" required /> - - +
+
+
+
+
+

+ Active session detected +

+

+ You are already logged in. To register, you must first log out or you can go to the dashboard. +

+
+ +
@@ -82,7 +83,9 @@ export default function SessionDetectedModal({ leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - +
+
- +
+ +
-

Invalid invitation link

-

+

Invalid invitation link

+

This registration link is invalid or no longer active. Please request a new link.

- {token ? ( -

+ {token && ( +

Token: {token}

- ) : null} -
+ )} +
{onClose && ( @@ -55,13 +57,7 @@ export default function InvalidRefLinkModal({
) - if (inline) { - return ( -
- {Content} -
- ) - } + if (inline) return Content return Content } \ No newline at end of file diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx index 143e462..300fcf7 100644 --- a/src/app/register/page.tsx +++ b/src/app/register/page.tsx @@ -1,20 +1,23 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useState, type CSSProperties } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import useAuthStore from '../store/authStore' import RegisterForm from './components/RegisterForm' import PageLayout from '../components/PageLayout' import SessionDetectedModal from './components/SessionDetectedModal' import InvalidRefLinkModal from './components/invalidRefLinkModal' -import { ToastProvider } from '../components/toast/toastComponent' +import { ToastProvider, useToast } from '../components/toast/toastComponent' +import Waves from '../components/waves' -export default function RegisterPage() { +// NEW: inner component that actually uses useToast and all the logic +function RegisterPageInner() { const searchParams = useSearchParams() const refToken = searchParams.get('ref') const [registered, setRegistered] = useState(false) const [mode, setMode] = useState<'personal' | 'company'>('personal') const router = useRouter() + const { showToast } = useToast() // Auth state const user = useAuthStore(state => state.user) @@ -24,7 +27,7 @@ export default function RegisterPage() { const [showSessionModal, setShowSessionModal] = useState(false) const [sessionCleared, setSessionCleared] = useState(false) - // NEW: Referral validation state + // Referral validation state const [isRefChecked, setIsRefChecked] = useState(false) const [invalidRef, setInvalidRef] = useState(false) const [refInfo, setRefInfo] = useState<{ @@ -34,42 +37,43 @@ export default function RegisterPage() { usesRemaining?: number } | null>(null) - // Redirect to login after simulated registration + // Redirect after registration useEffect(() => { if (registered) { - const t = setTimeout(() => router.push('/login'), 4000) // was 1200 + const t = setTimeout(() => router.push('/login'), 4000) return () => clearTimeout(t) } }, [registered, router]) - // NEW: Validate referral token (must exist and be valid) + // Validate referral token useEffect(() => { let cancelled = false const validateRef = async () => { if (!refToken) { - console.warn('⚠️ Register: Missing ?ref token in URL') if (!cancelled) { setInvalidRef(true) setIsRefChecked(true) } + showToast({ + variant: 'error', + title: 'Invitation error', + message: 'No invitation token found in the link.' + }) return } const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' const url = `${base}/api/referral/info/${encodeURIComponent(refToken)}` - console.log('🌐 Register: fetching referral info:', url) try { const res = await fetch(url, { method: 'GET', credentials: 'include' }) - console.log('📡 Register: referral info status:', res.status) const body = await res.json().catch(() => null) - console.log('📦 Register: referral info body:', body) const success = !!body?.success const isUnlimited = !!body?.isUnlimited - const usesRemaining = typeof body?.usesRemaining === 'number' ? body.usesRemaining : 0 - + const usesRemaining = + typeof body?.usesRemaining === 'number' ? body.usesRemaining : 0 const isActive = success && (isUnlimited || usesRemaining > 0) if (!cancelled) { @@ -81,28 +85,46 @@ export default function RegisterPage() { usesRemaining }) setInvalidRef(false) + showToast({ + variant: 'success', + title: 'Invitation verified', + message: 'Your invitation link is valid. You can register now.' + }) } else { - console.warn('⛔ Register: referral not active/invalid') setInvalidRef(true) + showToast({ + variant: 'error', + title: 'Invalid invitation', + message: 'This invitation link is invalid or no longer active.' + }) } setIsRefChecked(true) } - } catch (e) { - console.error('❌ Register: referral info fetch error:', e) + } catch { if (!cancelled) { setInvalidRef(true) setIsRefChecked(true) } + showToast({ + variant: 'error', + title: 'Network error', + message: 'Could not validate the invitation link. Please try again.' + }) } } validateRef() - return () => { cancelled = true } - }, [refToken]) + return () => { + cancelled = true + } + // showToast intentionally omitted to avoid effect re-run loops (provider value can change) + }, [refToken]) // note: showToast intentionally omitted to avoid effect re-run loops - // Detect existing logged-in session (only if ref is valid) + // Detect existing logged-in session useEffect(() => { - if (isRefChecked && !invalidRef && user && !sessionCleared) setShowSessionModal(true) + if (isRefChecked && !invalidRef && user && !sessionCleared) { + setShowSessionModal(true) + } }, [isRefChecked, invalidRef, user, sessionCleared]) const handleLogout = async () => { @@ -116,79 +138,110 @@ export default function RegisterPage() { router.push('/dashboard') } - // NEW: Gate rendering until referral check is done + const [isMobile, setIsMobile] = useState(false) + + useEffect(() => { + const mq = window.matchMedia('(max-width: 768px)') + const apply = () => setIsMobile(mq.matches) + apply() + mq.addEventListener?.('change', apply) + window.addEventListener('resize', apply, { passive: true }) + return () => { + mq.removeEventListener?.('change', apply) + window.removeEventListener('resize', apply) + } + }, []) + + const mainStyle: CSSProperties = { + paddingTop: isMobile + ? 'calc(var(--pp-header-spacer, 0px) + clamp(1.25rem, 3.5vh, 2.25rem))' + : 'calc(var(--pp-header-spacer, 0px) + clamp(5rem, 8vh, 7rem))', + transition: 'padding-top 260ms ease, opacity 260ms ease', + willChange: 'padding-top, opacity', + opacity: 'var(--pp-page-shift-opacity, 1)', + } + + // --- Render branches (unchanged except classNames) --- + if (!isRefChecked) { return ( - - -
-
-
-

Checking invitation link…

+ +
+ +
+
+
+
+
+
+

Checking invitation link…

+
+
- - +
+
) } - // NEW: Invalid referral link state — show modal instead of form with same background as register form if (invalidRef) { return ( - - -
- {/* make wrapper flex-1 so background reaches the footer */} -
- {/* Pattern */} - - - {/* Colored blur */} + +
+ +
+
- - +
+
) } + // normal register return ( - - -
- {/* Background section wrapper */} - {/* make wrapper flex-1 so background reaches the footer */} -
- {/* Pattern */} - - - {/* Colored blur */} + +
+ +
+
- +
+
+ ) +} + +// NEW: default export only provides the ToastProvider wrapper +export default function RegisterPage() { + return ( + + ) } \ No newline at end of file diff --git a/src/app/utils/phoneUtils.ts b/src/app/utils/phoneUtils.ts index 7342419..14952d1 100644 --- a/src/app/utils/phoneUtils.ts +++ b/src/app/utils/phoneUtils.ts @@ -11,15 +11,16 @@ const ITI_CDN_CSS = 'https://cdn.jsdelivr.net/npm/intl-tel-input@25.15.0/build/css/intlTelInput.css' + +// Use the official bundle that includes utils to avoid "getCoreNumber" being undefined. const ITI_CDN_JS = - 'https://cdn.jsdelivr.net/npm/intl-tel-input@25.15.0/build/js/intlTelInput.min.js' -const ITI_CDN_UTILS = - 'https://cdn.jsdelivr.net/npm/intl-tel-input@25.15.0/build/js/utils.js' + 'https://cdn.jsdelivr.net/npm/intl-tel-input@25.15.0/build/js/intlTelInputWithUtils.min.js' export type IntlTelInputInstance = { destroy: () => void getNumber: () => string isValidNumber: () => boolean + setNumber?: (number: string) => void getValidationError?: () => number getSelectedCountryData?: () => { name: string; iso2: string; dialCode: string } promise?: Promise @@ -27,7 +28,9 @@ export type IntlTelInputInstance = { declare global { interface Window { - intlTelInput?: (input: HTMLInputElement, options: any) => IntlTelInputInstance + intlTelInput?: ((input: HTMLInputElement, options: any) => IntlTelInputInstance) & { + getInstance?: (input: HTMLInputElement) => IntlTelInputInstance | null + } } } @@ -70,26 +73,36 @@ async function loadIntlTelInputFromCdn(): Promise< document.head.appendChild(style) } - // JS once + // JS once (but replace if the existing script points to a different bundle) if (window.intlTelInput) { console.log('[phoneUtils] intl-tel-input already loaded on window') return window.intlTelInput } - console.log('[phoneUtils] Loading intl-tel-input core (no utils) from CDN…') + console.log('[phoneUtils] Loading intl-tel-input (with utils) from CDN…') await new Promise((resolve, reject) => { const existing = document.querySelector( 'script[data-intl-tel-input-js="true"]' ) + if (existing) { - console.log('[phoneUtils] Reusing existing intl-tel-input