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/nav/Header.tsx b/src/app/components/nav/Header.tsx index 8d7dba0..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 isParallaxPage = pathname === '/' || pathname === '/login' + 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) }, []) + // 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 (!isParallaxPage) { - // non-parallax pages: 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, isParallaxPage]) + }, [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 = isParallaxPage ? animateIn && scrollY > 24 : animateIn - const parallaxOffset = isParallaxPage ? 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 (
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 4e7f263..fafe75f 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -12,6 +12,7 @@ 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) @@ -20,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) { @@ -48,41 +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

-
- -
-
-
- - )} - - -