feat: mobile register / login

This commit is contained in:
DeathKaioken 2026-01-14 16:16:38 +01:00
parent 7defd9e596
commit 86c953654f
21 changed files with 1652 additions and 1226 deletions

View File

@ -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 (
<div className="min-h-screen w-full flex flex-col bg-white text-gray-900">
<div className={`min-h-screen w-full flex flex-col ${className}`}>
{showHeader && (
<div className="relative z-50 w-full flex-shrink-0">
@ -35,7 +57,7 @@ export default function PageLayout({
)}
{/* Main content */}
<div className="flex-1 relative z-10 w-full">
<div className={contentClassName}>
<PageTransitionEffect>{children}</PageTransitionEffect>
</div>

View File

@ -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<HTMLDivElement | null>(null)
const [canSeeDashboard, setCanSeeDashboard] = useState(false)
const headerElRef = useRef<HTMLElement | null>(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 <body>/<html>, 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 (
<header
className={`${
isParallaxPage ? 'fixed top-0 left-0 w-full' : 'relative'
} isolate z-10 shadow-lg shadow-black/30 after:pointer-events-none after:absolute after:inset-0 after:-z-10 after:bg-[radial-gradient(circle_at_20%_20%,rgba(56,124,255,0.18),transparent_55%),radial-gradient(circle_at_80%_35%,rgba(139,92,246,0.16),transparent_60%)] ${
ref={headerElRef}
className={`${headerPositionClass} isolate z-30 shadow-lg shadow-black/30 after:pointer-events-none after:absolute after:inset-0 after:-z-10 after:bg-[radial-gradient(circle_at_20%_20%,rgba(56,124,255,0.18),transparent_55%),radial-gradient(circle_at_80%_35%,rgba(139,92,246,0.16),transparent_60%)] ${
isAdmin ? '' : 'border-b border-white/10'
} ${
headerVisible ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-6 pointer-events-none'
} transition-all duration-500 ease-out`}
style={{
background: 'linear-gradient(135deg, #0F1D37 0%, #0A162A 50%, #081224 100%)',
...(isParallaxPage ? { transform: `translateY(${parallaxOffset}px)` } : {}),
...(parallaxEnabled ? { transform: `translateY(${parallaxOffset}px)` } : {}),
}}
>
<nav
@ -886,7 +972,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
<div className="py-6 space-y-4 px-1">
{/* Information disclosure */}
<Disclosure as="div">
<DisclosureButton className="group flex w-full items-center justify-between rounded-lg py-2 px-3 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5">
<DisclosureButton className="group flex w-full items-center justify-between rounded-lg py-2 px-3 textbase/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5">
Information
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none group-data-open:rotate-180" />
</DisclosureButton>
@ -908,7 +994,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
<button
key={link.href}
onClick={() => { router.push(link.href); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
className="block rounded-lg px-3 py-2 textbase/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
{link.name}
</button>
@ -916,7 +1002,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
<div className="px-3">
<button
onClick={() => { router.push('/login'); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2.5 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
className="block rounded-lg px-3 py-2.5 textbase/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
Log in
</button>

View File

@ -12,6 +12,8 @@ import { createIntlTelInput, IntlTelInputInstance } from '../../utils/phoneUtils
export type TelephoneInputHandle = {
getNumber: () => string
isValid: () => boolean
// NEW: allow callers to require a selected country code
getDialCode: () => string | null
}
interface TelephoneInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
@ -24,20 +26,33 @@ interface TelephoneInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>
* Always takes full available width.
*/
const TelephoneInput = forwardRef<TelephoneInputHandle, TelephoneInputProps>(
({ initialCountry = (process.env.NEXT_PUBLIC_GEO_FALLBACK_COUNTRY || 'DE').toLowerCase(), ...rest }, ref) => {
({ initialCountry, ...rest }, ref) => {
const inputRef = useRef<HTMLInputElement | null>(null)
const itiRef = useRef<IntlTelInputInstance | null>(null)
const readyRef = useRef(false)
const lastSyncDigitsRef = useRef<string>('')
useEffect(() => {
let disposed = false
let instance: IntlTelInputInstance | null = null
readyRef.current = false
lastSyncDigitsRef.current = ''
const setup = async () => {
try {
console.log('[TelephoneInput] setup() start for', {
const fallbackCountry =
(process.env.NEXT_PUBLIC_GEO_FALLBACK_COUNTRY || 'DE').toLowerCase()
const resolvedCountry = (initialCountry || fallbackCountry).toLowerCase()
console.log('[TelephoneInput] EFFECT setup() ENTER', {
id: rest.id,
name: rest.name,
initialCountry,
initialCountryProp: initialCountry,
fallbackCountry,
resolvedCountry,
hasWindow: typeof window !== 'undefined',
hasIntlTelInputOnWindow: typeof (window as any)?.intlTelInput === 'function',
hasIntlTelInputGlobals: typeof (window as any)?.intlTelInputGlobals !== 'undefined',
})
if (!inputRef.current) {
@ -48,20 +63,90 @@ const TelephoneInput = forwardRef<TelephoneInputHandle, TelephoneInputProps>(
return
}
instance = await createIntlTelInput(inputRef.current, {
initialCountry,
console.log('[TelephoneInput] calling createIntlTelInput with options', {
id: rest.id,
name: rest.name,
options: {
initialCountry: resolvedCountry,
nationalMode: true,
strictMode: true,
autoPlaceholder: 'aggressive',
validationNumberTypes: ['MOBILE'],
// Help keep display consistent once utils is available
formatOnDisplay: true,
formatAsYouType: true,
// NEW: keep dropdown anchored (no fullscreen takeover on mobile)
useFullscreenPopup: false,
},
})
if (disposed) {
console.log('[TelephoneInput] setup() finished but component is disposed, destroying instance', {
id: rest.id,
name: rest.name,
instance = await createIntlTelInput(inputRef.current, {
initialCountry: resolvedCountry,
nationalMode: true,
strictMode: true,
autoPlaceholder: 'aggressive',
validationNumberTypes: ['MOBILE'],
// Help keep display consistent once utils is available
formatOnDisplay: true,
formatAsYouType: true,
// NEW: keep dropdown anchored (no fullscreen takeover on mobile)
useFullscreenPopup: false,
})
// Sync selected country/flag from typed dial code (e.g. +43 => AT),
// but only once the user has typed enough digits to avoid cursor-jank.
const inputEl = inputRef.current
const syncFromValue = () => {
if (!inputEl || !instance) return
const raw = (inputEl.value || '').trim()
if (!raw) return
// normalize "00" prefix to "+"
const normalized = raw.startsWith('00') ? `+${raw.slice(2)}` : raw
if (!normalized.startsWith('+')) return
const digits = normalized.replace(/\D/g, '')
if (digits.length < 4) return // wait until "+CCx" at least
if (digits === lastSyncDigitsRef.current) return
lastSyncDigitsRef.current = digits
try {
instance.setNumber?.(`+${digits}`)
} catch {
// ignore
}
}
// Mark ready once the plugin finishes any async init work.
const anyInstance = instance as any
if (anyInstance?.promise && typeof anyInstance.promise.then === 'function') {
anyInstance.promise
.then(() => {
readyRef.current = true
// resync once utils/formatting is definitely available
try { syncFromValue() } catch {}
})
.catch(() => {})
} else {
readyRef.current = true
try { syncFromValue() } catch {}
}
inputEl.addEventListener('input', syncFromValue)
inputEl.addEventListener('blur', syncFromValue)
// one initial sync (covers paste/autofill after mount)
syncFromValue()
if (disposed) {
console.log(
'[TelephoneInput] setup() finished but component is disposed, destroying instance',
{ id: rest.id, name: rest.name }
)
if (instance) {
instance.destroy()
}
return
}
@ -69,9 +154,23 @@ const TelephoneInput = forwardRef<TelephoneInputHandle, TelephoneInputProps>(
console.log('[TelephoneInput] intl-tel-input instance attached to input', {
id: rest.id,
name: rest.name,
inputCurrentValue: inputRef.current.value,
})
// cleanup listeners when disposed
const prevCleanup = () => {
inputEl.removeEventListener('input', syncFromValue)
inputEl.removeEventListener('blur', syncFromValue)
}
;(anyInstance.__pp_cleanup as undefined | (() => void))?.()
anyInstance.__pp_cleanup = prevCleanup
} catch (e) {
console.error('[TelephoneInput] Failed to init intl-tel-input:', e)
console.error('[TelephoneInput] Failed to init intl-tel-input:', {
id: rest.id,
name: rest.name,
error: e,
stack: (e as any)?.stack,
})
}
}
@ -79,12 +178,35 @@ const TelephoneInput = forwardRef<TelephoneInputHandle, TelephoneInputProps>(
return () => {
disposed = true
readyRef.current = false
// remove listeners (if we attached them)
try {
;(instance as any)?.__pp_cleanup?.()
} catch {
// ignore
}
console.log('[TelephoneInput] EFFECT cleanup ENTER', {
id: rest.id,
name: rest.name,
hadInstance: !!instance,
hadItiRef: !!itiRef.current,
})
if (instance) {
console.log('[TelephoneInput] Destroying intl-tel-input instance for', {
id: rest.id,
name: rest.name,
})
try {
instance.destroy()
} catch (e) {
console.error('[TelephoneInput] Error while destroying instance', {
id: rest.id,
name: rest.name,
error: e,
})
}
if (itiRef.current === instance) itiRef.current = null
}
}
@ -93,63 +215,99 @@ const TelephoneInput = forwardRef<TelephoneInputHandle, TelephoneInputProps>(
useImperativeHandle(ref, () => ({
getNumber: () => {
const raw = inputRef.current?.value || ''
if (itiRef.current) {
if (!itiRef.current || !readyRef.current) return raw
try {
const intl = itiRef.current.getNumber()
console.log('[TelephoneInput] getNumber()', {
id: rest.id,
name: rest.name,
raw,
intl,
})
return intl
}
console.warn(
'[TelephoneInput] getNumber() called before intl-tel-input ready, returning raw value',
{ id: rest.id, name: rest.name, raw }
)
return intl || raw
} catch {
return raw
}
},
isValid: () => {
if (!itiRef.current) {
const raw = inputRef.current?.value || ''
console.warn('[TelephoneInput] isValid() called before intl-tel-input ready', {
id: rest.id,
name: rest.name,
raw,
})
return false
if (!itiRef.current || !readyRef.current) {
return /^\+?\d{7,}$/.test(raw.replace(/\s+/g, ''))
}
const instance = itiRef.current
const intl = instance.getNumber()
const valid = instance.isValidNumber()
const errorCode = typeof instance.getValidationError === 'function'
? instance.getValidationError()
: undefined
const country = typeof instance.getSelectedCountryData === 'function'
? instance.getSelectedCountryData()
: undefined
console.log('[TelephoneInput] isValid() check', {
id: rest.id,
name: rest.name,
intl,
valid,
errorCode,
country,
})
return valid
try {
return itiRef.current.isValidNumber()
} catch {
return /^\+?\d{7,}$/.test(raw.replace(/\s+/g, ''))
}
},
getDialCode: () => {
const iti = itiRef.current as any
const data = iti?.getSelectedCountryData?.()
const dial = data?.dialCode
return typeof dial === 'string' && dial.trim() ? dial.trim() : null
},
}))
return (
<div className="w-full">
<div className="w-full pp-iti-dark">
<input
ref={inputRef}
type="tel"
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 ${rest.className || ''}`}
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 ${rest.className || ''}`}
{...rest}
/>
{/* UPDATED: also cover mobile/fullscreen dropdown container (often appended to body) */}
<style jsx global>{`
/* Scoped (works when dropdown stays inside the component) */
.pp-iti-dark .iti__country-list {
color: #0f172a;
background: #ffffff;
}
.pp-iti-dark .iti__country,
.pp-iti-dark .iti__country-name,
.pp-iti-dark .iti__dial-code,
.pp-iti-dark .iti__selected-dial-code,
.pp-iti-dark .iti__search-input {
color: #0f172a !important;
}
.pp-iti-dark .iti__dial-code {
color: #334155 !important;
}
.pp-iti-dark .iti__country.iti__highlight {
background: #e2e8f0;
}
.pp-iti-dark .iti__divider {
border-color: #e5e7eb;
}
/* Global (mobile fullscreen popup / container appended to body) */
.iti--container,
.iti--container .iti__country-list,
.iti-mobile .iti__country-list {
background: #ffffff !important;
color: #0f172a !important;
}
.iti--container .iti__country,
.iti--container .iti__country-name,
.iti--container .iti__dial-code,
.iti--container .iti__selected-dial-code,
.iti--container .iti__search-input,
.iti-mobile .iti__country-name,
.iti-mobile .iti__dial-code,
.iti-mobile .iti__search-input {
color: #0f172a !important;
}
.iti--container .iti__dial-code,
.iti-mobile .iti__dial-code {
color: #334155 !important;
}
.iti--container .iti__country.iti__highlight,
.iti-mobile .iti__country.iti__highlight {
background: #e2e8f0 !important;
}
/* NEW: ensure dropdown scrolls instead of growing (anchored dropdown UX) */
.iti__country-list {
max-height: min(320px, 45vh) !important;
overflow-y: auto !important;
-webkit-overflow-scrolling: touch;
}
`}</style>
</div>
)
}

View File

@ -133,6 +133,8 @@ export interface WavesProps {
maxCursorMove?: number;
style?: CSSProperties;
className?: string;
animate?: boolean;
interactive?: boolean;
}
const Waves: React.FC<WavesProps> = ({
@ -149,6 +151,8 @@ const Waves: React.FC<WavesProps> = ({
maxCursorMove = 100,
style = {},
className = '',
animate = true,
interactive = true,
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
@ -253,6 +257,50 @@ const Waves: React.FC<WavesProps> = ({
}
}
function moved(point: Point, withCursor = true): { x: number; y: number } {
const x = point.x + point.wave.x + (withCursor ? point.cursor.x : 0);
const y = point.y + point.wave.y + (withCursor ? point.cursor.y : 0);
return { x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 };
}
function drawLines(withCursor = true) {
const { width, height } = boundingRef.current;
const ctx = ctxRef.current;
if (!ctx) return;
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
ctx.strokeStyle = configRef.current.lineColor;
linesRef.current.forEach(points => {
let p1 = moved(points[0], false);
ctx.moveTo(p1.x, p1.y);
points.forEach((p, idx) => {
const isLast = idx === points.length - 1;
p1 = moved(p, withCursor && !isLast);
const p2 = moved(points[idx + 1] || points[points.length - 1], withCursor && !isLast);
ctx.lineTo(p1.x, p1.y);
if (isLast) ctx.moveTo(p2.x, p2.y);
});
});
ctx.stroke();
}
function drawStatic() {
linesRef.current.forEach(pts => {
pts.forEach(p => {
p.wave.x = 0;
p.wave.y = 0;
p.cursor.x = 0;
p.cursor.y = 0;
p.cursor.vx = 0;
p.cursor.vy = 0;
});
});
drawLines(false);
}
function movePoints(time: number) {
const lines = linesRef.current;
const mouse = mouseRef.current;
@ -265,6 +313,8 @@ const Waves: React.FC<WavesProps> = ({
p.wave.x = Math.cos(move) * waveAmpX;
p.wave.y = Math.sin(move) * waveAmpY;
if (!interactive) return;
const dx = p.x - mouse.sx;
const dy = p.y - mouse.sy;
const dist = Math.hypot(dx, dy);
@ -288,40 +338,11 @@ const Waves: React.FC<WavesProps> = ({
});
}
function moved(point: Point, withCursor = true): { x: number; y: number } {
const x = point.x + point.wave.x + (withCursor ? point.cursor.x : 0);
const y = point.y + point.wave.y + (withCursor ? point.cursor.y : 0);
return { x: Math.round(x * 10) / 10, y: Math.round(y * 10) / 10 };
}
function drawLines() {
const { width, height } = boundingRef.current;
const ctx = ctxRef.current;
if (!ctx) return;
ctx.clearRect(0, 0, width, height);
ctx.beginPath();
ctx.strokeStyle = configRef.current.lineColor;
linesRef.current.forEach(points => {
let p1 = moved(points[0], false);
ctx.moveTo(p1.x, p1.y);
points.forEach((p, idx) => {
const isLast = idx === points.length - 1;
p1 = moved(p, !isLast);
const p2 = moved(points[idx + 1] || points[points.length - 1], !isLast);
ctx.lineTo(p1.x, p1.y);
if (isLast) ctx.moveTo(p2.x, p2.y);
});
});
ctx.stroke();
}
function tick(t: number) {
const container = containerRef.current;
if (!container) return;
if (interactive) {
const mouse = mouseRef.current;
mouse.sx += (mouse.x - mouse.sx) * 0.1;
mouse.sy += (mouse.y - mouse.sy) * 0.1;
@ -336,27 +357,27 @@ const Waves: React.FC<WavesProps> = ({
mouse.a = Math.atan2(dy, dx);
container.style.setProperty('--x', `${mouse.sx}px`);
container.style.setProperty('--y', `${mouse.sy}px`);
}
movePoints(t);
drawLines();
drawLines(true);
frameIdRef.current = requestAnimationFrame(tick);
}
// NEW: react to parent size changes (content height, header/footer, etc.)
const ro =
typeof ResizeObserver !== 'undefined'
? new ResizeObserver(() => onResize())
: null;
function onResize() {
setSize();
setLines();
}
function onMouseMove(e: MouseEvent) {
updateMouse(e.clientX, e.clientY);
}
function onTouchMove(e: TouchEvent) {
const touch = e.touches[0];
updateMouse(touch.clientX, touch.clientY);
if (!animate) drawStatic();
}
function updateMouse(x: number, y: number) {
if (!interactive) return;
const mouse = mouseRef.current;
const b = boundingRef.current;
mouse.x = x - b.left;
@ -370,20 +391,43 @@ const Waves: React.FC<WavesProps> = ({
}
}
function onMouseMove(e: MouseEvent) {
updateMouse(e.clientX, e.clientY);
}
function onTouchMove(e: TouchEvent) {
const touch = e.touches[0];
if (touch) updateMouse(touch.clientX, touch.clientY);
}
setSize();
setLines();
frameIdRef.current = requestAnimationFrame(tick);
window.addEventListener('resize', onResize);
ro?.observe(container);
if (interactive) {
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('touchmove', onTouchMove, { passive: false });
window.addEventListener('touchmove', onTouchMove, { passive: true });
}
if (animate) {
frameIdRef.current = requestAnimationFrame(tick);
} else {
drawStatic();
}
return () => {
window.removeEventListener('resize', onResize);
ro?.disconnect();
if (interactive) {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('touchmove', onTouchMove);
}
if (frameIdRef.current !== null) cancelAnimationFrame(frameIdRef.current);
frameIdRef.current = null;
};
}, []);
}, [animate, interactive]);
return (
<div
@ -391,8 +435,14 @@ const Waves: React.FC<WavesProps> = ({
style={{
backgroundColor,
...style,
position: 'absolute',
inset: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
zIndex: 0,
}}
className={`fixed inset-0 w-full h-full overflow-hidden ${className}`}
className={`w-full h-full overflow-hidden ${className}`}
>
<canvas ref={canvasRef} className="block w-full h-full" />
</div>

View File

@ -1,10 +1,9 @@
'use client'
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
import Header from '../components/nav/Header'
import Footer from '../components/Footer'
import PageLayout from '../components/PageLayout'
import Waves from '../components/waves'
import {
ShoppingBagIcon,
@ -20,6 +19,19 @@ export default function DashboardPage() {
const user = useAuthStore(state => state.user)
const isAuthReady = useAuthStore(state => state.isAuthReady)
const isShopEnabled = process.env.NEXT_PUBLIC_SHOW_SHOP !== 'false'
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)
}
}, [])
// Redirect if not logged in (only after auth is ready)
useEffect(() => {
@ -117,7 +129,7 @@ export default function DashboardPage() {
return (
<div
className="relative w-full flex flex-col min-h-screen overflow-hidden"
className="relative w-full min-h-[100dvh] flex flex-col overflow-x-hidden"
style={{ backgroundImage: 'none', background: 'none' }}
>
<Waves
@ -133,14 +145,15 @@ export default function DashboardPage() {
maxCursorMove={120}
xGap={12}
yGap={36}
animate={!isMobile}
interactive={!isMobile}
/>
<div className="relative z-10 min-h-screen flex flex-col">
<Header />
<main className="flex-1 py-8 px-4 sm:px-6 lg:px-8">
<div className="relative z-10 flex-1 min-h-0">
<PageLayout className="bg-transparent text-gray-900">
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-6 sm:p-8">
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-8">
{/* Welcome Section */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">
@ -302,8 +315,7 @@ export default function DashboardPage() {
</div>
</div>
</main>
<Footer />
</PageLayout>
</div>
</div>
)

View File

@ -6,6 +6,8 @@ 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 [formData, setFormData] = useState({
@ -36,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
}
@ -95,7 +97,7 @@ export default function LoginForm() {
// CHANGED: Wider base widths; no transform scaling
const formWidth = isMobile
? '94vw'
? '100%'
: isTablet
? '80vw'
: isSmallLaptop
@ -103,7 +105,7 @@ export default function LoginForm() {
: '52vw'
const formMaxWidth = isMobile
? '480px'
? '420px'
: isTablet
? '760px'
: isSmallLaptop
@ -120,8 +122,8 @@ export default function LoginForm() {
justifyContent: 'center',
// REMOVE marble image so Waves shows through
background: 'transparent',
// Reduced top padding so the curved loop is closer to the form
padding: isMobile ? '0.5rem 0.75rem 0.75rem' : '0.2rem 1.5rem 1.5rem',
// move the card slightly down on mobile, reduce bottom padding
padding: isMobile ? '0.5rem 0.75rem 0' : '0.2rem 1.5rem 1.5rem',
}}
>
<div
@ -130,21 +132,24 @@ export default function LoginForm() {
width: formWidth,
maxWidth: formMaxWidth,
minWidth: isMobile ? '0' : '420px',
// CHANGED: tighter padding; removed transform scaling
padding: isMobile ? '1rem' : '2rem',
// slightly tighter on mobile
padding: isMobile ? '0.75rem' : '2rem',
// more translucent, glassy background
backgroundColor: 'rgba(255,255,255,0.55)',
backgroundColor: GLASS_BG,
backdropFilter: 'blur(18px)',
WebkitBackdropFilter: 'blur(18px)',
boxShadow: '0 18px 45px rgba(15,23,42,0.45)',
// smoother / less bottom-heavy shadow on mobile
boxShadow: isMobile
? '0 10px 22px rgba(15,23,42,0.18), 0 2px 6px rgba(15,23,42,0.12)'
: '0 18px 45px rgba(15,23,42,0.45)',
}}
>
{/* Content (title + earth removed) */}
<div
style={{
// CHANGED: smaller margins; the card is centered now
marginTop: isMobile ? '0.25rem' : isTablet ? '0.5rem' : '0.75rem',
marginBottom: isMobile ? '1rem' : isTablet ? '1.25rem' : '1.5rem',
marginTop: isMobile ? '0.15rem' : isTablet ? '0.5rem' : '0.75rem',
marginBottom: isMobile ? '0.75rem' : isTablet ? '1.25rem' : '1.5rem',
width: '100%',
}}
>
@ -154,14 +159,14 @@ export default function LoginForm() {
PROFIT PLANET
</h1>
<p className="mt-1 text-sm md:text-base text-slate-700/90">
Welcome back! Login to continue.
Welcome back! Log in to continue.
</p>
</div>
<form
className="space-y-6 w-full"
className={`${isMobile ? 'space-y-4' : 'space-y-6'} w-full`}
style={{
gap: isMobile ? '0.75rem' : isTablet ? '0.9rem' : '1rem',
gap: isMobile ? '0.6rem' : isTablet ? '0.9rem' : '1rem',
}}
onSubmit={handleSubmit}
>
@ -175,7 +180,7 @@ export default function LoginForm() {
marginBottom: isMobile ? '0.25rem' : undefined,
}}
>
E-Mail-Adresse
Email address
</label>
<input
id="email"
@ -189,7 +194,7 @@ export default function LoginForm() {
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
padding: isMobile ? '0.5rem 0.75rem' : isTablet ? '0.6rem 0.875rem' : '0.7rem 1rem',
}}
placeholder="deine@email.com"
placeholder="you@example.com"
required
/>
</div>
@ -204,7 +209,7 @@ export default function LoginForm() {
marginBottom: isMobile ? '0.25rem' : undefined,
}}
>
Passwort
Password
</label>
<div className="relative">
<input
@ -223,7 +228,7 @@ export default function LoginForm() {
? '0.6rem 2.75rem 0.6rem 0.875rem'
: '0.7rem 3rem 0.7rem 1rem',
}}
placeholder="Dein Passwort"
placeholder="Your password"
required
/>
<button
@ -238,35 +243,6 @@ export default function LoginForm() {
)}
</button>
</div>
{/* Remember Me & Show Password */}
<div className="mt-2 flex items-center justify-between">
<div className="flex items-center">
<input
id="rememberMe"
name="rememberMe"
type="checkbox"
checked={formData.rememberMe}
onChange={handleInputChange}
className="h-4 w-4 text-[#8D6B1D] border-2 border-gray-300 rounded focus:ring-[#8D6B1D] focus:ring-2"
/>
<label htmlFor="rememberMe" className="ml-2 text-sm text-slate-700">
Angemeldet bleiben
</label>
</div>
<div className="flex items-center">
<input
id="show-password"
type="checkbox"
className="h-4 w-4 border-2 border-gray-300 rounded focus:ring-[#8D6B1D] focus:ring-2"
checked={showPassword}
onChange={(e) => setShowPassword(e.target.checked)}
/>
<label htmlFor="show-password" className="ml-2 text-sm text-slate-700">
Passwort anzeigen
</label>
</div>
</div>
</div>
{/* Error Message */}
@ -294,10 +270,10 @@ export default function LoginForm() {
{loading ? (
<div className="flex items-center justify-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
Anmeldung läuft...
Signing in...
</div>
) : (
'Anmelden'
'Sign in'
)}
</button>
</div>
@ -309,7 +285,7 @@ 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?
</button>
</div>
</form>

View File

@ -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,12 +61,8 @@ export default function LoginPage() {
return (
<PageTransitionEffect>
<ToastProvider>
<PageLayout showFooter={true}>
<div
className="relative w-full flex flex-col min-h-screen overflow-hidden"
style={{ backgroundImage: 'none', background: 'none' }}
>
{/* Waves background */}
{/* NEW: page-level background wrapper so Waves covers everything */}
<div className="relative min-h-screen w-full overflow-x-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<Waves
className="pointer-events-none"
lineColor="#0f172a"
@ -67,9 +76,43 @@ export default function LoginPage() {
maxCursorMove={120}
xGap={12}
yGap={36}
animate={!isMobile}
interactive={!isMobile}
/>
<div className="relative z-10 flex-1 flex flex-col justify-start space-y-4 pt-10 pb-10">
<div className="w-full">
<PageLayout showFooter={true} className="bg-transparent text-gray-900">
{/* ...existing code... */}
<div
className={`relative z-10 w-full flex flex-col flex-1 min-h-0 ${
isMobile ? 'overflow-y-hidden' : ''
}`}
style={{ backgroundImage: 'none', background: 'none' }}
>
{/* REMOVED: Waves background moved to wrapper */}
{isMobile ? (
// ...existing code...
<div
className="relative z-10 flex-1 min-h-0 grid place-items-center px-3"
style={{ paddingTop: '6rem', paddingBottom: '0.5rem' }}
>
<div
className="w-full"
style={{
// push a bit down (visual centering with header + footer)
transform: 'translateY(clamp(10px, 2vh, 28px))',
}}
>
<LoginForm />
</div>
</div>
) : (
// ...existing code...
<div
className="relative z-10 flex-1 min-h-0 flex flex-col justify-between"
style={{ paddingTop: '0.75rem', paddingBottom: '1rem' }}
>
<div className="w-full px-4 sm:px-0">
<CurvedLoop
marqueeText="Welcome to profit planet ✦"
speed={1}
@ -77,12 +120,16 @@ export default function LoginPage() {
className="tracking-[0.2em]"
/>
</div>
<div className="w-full flex items-center justify-center">
<div className="w-full flex items-center justify-center px-3 sm:px-0">
<LoginForm />
</div>
</div>
)}
</div>
{/* ...existing code... */}
</PageLayout>
</div>
</ToastProvider>
</PageTransitionEffect>
)

View File

@ -11,10 +11,33 @@ import SplitText from './components/SplitText';
export default function HomePage() {
const containerRef = useRef<HTMLDivElement | null>(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 (
<PageLayout>
@ -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 */}
<Waves
className="pointer-events-none"
lineColor="#0f172a"
@ -58,20 +85,27 @@ export default function HomePage() {
maxCursorMove={120}
xGap={12}
yGap={36}
animate={!isMobile}
interactive={!isMobile}
/>
<h1 className="z-10">
<a
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
onClick={handleLoginClick}
onMouseEnter={isMobile ? undefined : () => setIsHover(true)}
onMouseLeave={isMobile ? undefined : () => setIsHover(false)}
className="cursor-pointer"
>
{isMobile ? (
<span className="block text-5xl sm:text-6xl font-bold text-gray-500 text-center px-4">
PROFIT PLANET
</span>
) : (
<SplitText
key={isHover ? 'login' : 'profit-planet'}
text={isHover ? 'LOGIN' : 'PROFIT PLANET'}
tag="span"
className={`text-9xl md:text-9xl font-bold transition-colors duration-300 ${
className={`text-7xl sm:text-8xl md:text-9xl font-bold transition-colors duration-300 ${
isHover ? 'text-black' : 'text-gray-500'
}`}
delay={100}
@ -84,10 +118,12 @@ export default function HomePage() {
rootMargin="-100px"
textAlign="center"
/>
)}
</a>
</h1>
<Crosshair containerRef={containerRef} color="#0f172a" />
{/* No parallax/crosshair on mobile */}
{!isMobile && <Crosshair containerRef={containerRef} color="#0f172a" />}
</div>
</PageLayout>
);

View File

@ -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 (
<PageLayout>
<main className="relative flex flex-col flex-1 pt-20 sm:pt-28 pb-12 sm:pb-16 overflow-hidden">
{/* Background Pattern */}
<svg
aria-hidden="true"
className="absolute inset-0 -z-10 h-full w-full stroke-white/10"
>
<defs>
<pattern
x="50%"
y={-1}
id="affiliate-pattern"
width={200}
height={200}
patternUnits="userSpaceOnUse"
>
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" />
</pattern>
</defs>
<rect fill="url(#affiliate-pattern)" width="100%" height="100%" strokeWidth={0} />
</svg>
{/* Colored Blur Effect */}
<div
aria-hidden="true"
className="absolute top-0 right-0 left-1/2 -z-10 -ml-24 transform-gpu overflow-hidden blur-3xl lg:ml-24 xl:ml-48"
className="relative w-full flex flex-col min-h-screen overflow-hidden"
style={{ backgroundImage: 'none', background: 'none' }}
>
<div
style={{
clipPath:
'polygon(63.1% 29.5%, 100% 17.1%, 76.6% 3%, 48.4% 0%, 44.6% 4.7%, 54.5% 25.3%, 59.8% 49%, 55.2% 57.8%, 44.4% 57.2%, 27.8% 47.9%, 35.1% 81.5%, 0% 97.7%, 39.2% 100%, 35.2% 81.4%, 97.2% 52.8%, 63.1% 29.5%)',
}}
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
/>
</div>
{/* Gradient base */}
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
{/* push content a bit further down while still centering */}
<main className="relative z-10 flex flex-col flex-1 items-center justify-center pt-32 sm:pt-0 pb-8 sm:pb-10">
{/* Widened container to match header */}
<div className="mx-auto max-w-7xl px-6 lg:px-10 flex-1 flex flex-col w-full">
<div className="mx-auto max-w-2xl text-center mb-10">
<h1 className="text-4xl font-semibold tracking-tight text-white sm:text-5xl">
Passwort zurücksetzen
</h1>
<p className="mt-3 text-gray-300 text-lg/7">
{!token
? 'Fordere einen Link zum Zurücksetzen deines Passworts an.'
: 'Lege ein neues sicheres Passwort fest.'}
</p>
</div>
{/* Wider form card */}
<div className="mx-auto w-full max-w-3xl bg-white dark:bg-gray-900 rounded-2xl shadow-2xl ring-1 ring-gray-200 dark:ring-white/10 p-6 sm:p-10 md:py-12 md:px-14 relative overflow-hidden">
<div className="mx-auto max-w-7xl px-6 lg:px-10 flex flex-col w-full">
{/* Translucent form card (matching login glass style) */}
<div
className="mx-auto w-full max-w-3xl rounded-3xl shadow-2xl relative border border-white/35 overflow-hidden p-6 sm:p-10 md:py-12 md:px-14"
style={{
backgroundColor: 'rgba(255,255,255,0.55)',
backdropFilter: 'blur(18px)',
WebkitBackdropFilter: 'blur(18px)',
boxShadow: '0 18px 45px rgba(15,23,42,0.45)',
}}
>
<div className="absolute -top-24 -right-24 size-56 rounded-full bg-gradient-to-br from-indigo-500/10 to-fuchsia-500/10 blur-3xl pointer-events-none" />
<div className="relative">
<div className="mx-auto max-w-2xl text-center mb-8">
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-[#0F172A]">
Reset password
</h1>
<p className="mt-3 text-slate-700 text-base sm:text-lg">
{!token
? 'Request a link to reset your password.'
: 'Set a new secure password.'}
</p>
</div>
{!token && (
<form onSubmit={handleRequestSubmit} className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2" htmlFor="email">
E-Mail-Adresse
<label className="block text-sm font-semibold text-[#0F172A] mb-2" htmlFor="email">
Email address
</label>
<input
id="email"
type="email"
value={email}
onChange={e => { setEmail(e.target.value); setRequestError(''); setRequestSuccess(false)}}
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-3 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
placeholder="dein.email@example.com"
className="w-full rounded-xl border border-white/40 bg-white/60 px-4 py-3 text-[#0F172A] placeholder-slate-600/80 shadow-sm focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] transition"
placeholder="your.email@example.com"
required
/>
</div>
@ -168,17 +203,17 @@ export default function PasswordResetPage() {
{requestSuccess && (
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
E-Mail gesendet (falls Adresse existiert). Prüfe dein Postfach.
Email sent (if the address exists). Please check your inbox.
</div>
)}
<button
type="submit"
disabled={requestLoading}
className={`w-full flex items-center justify-center rounded-lg px-5 py-3 font-semibold text-white transition-colors ${
className={`w-full flex items-center justify-center rounded-xl px-6 py-3 text-base font-semibold transition-all duration-200 transform hover:-translate-y-0.5 border ${
requestLoading
? 'bg-gray-400 cursor-wait'
: 'bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-900'
? 'border-white/30 bg-white/20 text-slate-300 cursor-wait'
: 'border-white/55 bg-white/30 text-[#0F172A] shadow-[0_10px_30px_rgba(15,23,42,0.45)] hover:bg-white/40 hover:shadow-[0_16px_40px_rgba(15,23,42,0.6)] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80'
}`}
>
{requestLoading ? (
@ -187,18 +222,18 @@ export default function PasswordResetPage() {
Senden...
</>
) : (
'Zurücksetzlink anfordern'
'Request reset link'
)}
</button>
<div className="text-center text-sm text-gray-600 dark:text-gray-400">
Erinnerst du dich?{' '}
<div className="text-center text-sm text-gray-700">
Remember it now?{' '}
<button
type="button"
onClick={() => router.push('/login')}
className="text-indigo-600 dark:text-indigo-400 hover:underline font-medium"
className="text-[#8D6B1D] hover:text-[#7A5E1A] hover:underline font-medium"
>
Zum Login
Back to login
</button>
</div>
</form>
@ -208,8 +243,8 @@ export default function PasswordResetPage() {
<form onSubmit={handleResetSubmit} className="space-y-6">
<div className="grid gap-6 sm:grid-cols-2">
<div className="sm:col-span-2">
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2" htmlFor="password">
Neues Passwort
<label className="block text-sm font-medium text-gray-900 mb-2" htmlFor="password">
New password
</label>
<div className="relative">
<input
@ -217,16 +252,16 @@ export default function PasswordResetPage() {
type={showPassword ? 'text' : 'password'}
value={password}
onChange={e => { setPassword(e.target.value); setResetError(''); setResetSuccess(false)}}
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-3 pr-12 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
placeholder="••••••••"
className="w-full rounded-xl border border-white/40 bg-white/60 px-4 py-3 pr-12 text-[#0F172A] placeholder-slate-600/80 shadow-sm focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] transition"
placeholder="Your new password"
required
/>
<button
type="button"
onClick={() => setShowPassword(p => !p)}
className="absolute inset-y-0 right-0 px-3 text-xs font-medium text-indigo-600 dark:text-indigo-400 hover:underline"
className="absolute inset-y-0 right-0 px-3 text-xs font-medium text-indigo-700 hover:underline"
>
{showPassword ? 'Verbergen' : 'Anzeigen'}
{showPassword ? 'Hide' : 'Show'}
</button>
</div>
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-xs">
@ -245,20 +280,20 @@ export default function PasswordResetPage() {
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2" htmlFor="confirm">
Passwort bestätigen
<label className="block text-sm font-medium text-gray-900 mb-2" htmlFor="confirm">
Confirm password
</label>
<input
id="confirm"
type={showPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={e => { setConfirmPassword(e.target.value); setResetError(''); setResetSuccess(false)}}
className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 px-4 py-3 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
placeholder="Bestätigung"
className="w-full rounded-xl border border-white/40 bg-white/60 px-4 py-3 text-[#0F172A] placeholder-slate-600/80 shadow-sm focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] transition"
placeholder="Confirm password"
required
/>
{confirmPassword && password !== confirmPassword && (
<p className="mt-2 text-xs text-red-500">Passwörter stimmen nicht überein.</p>
<p className="mt-2 text-xs text-red-500">Passwords do not match.</p>
)}
</div>
</div>
@ -270,37 +305,37 @@ export default function PasswordResetPage() {
)}
{resetSuccess && (
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700">
Passwort gespeichert. Weiterleitung zum Login...
Password saved. Redirecting to login...
</div>
)}
<button
type="submit"
disabled={resetLoading}
className={`w-full flex items-center justify-center rounded-lg px-5 py-3 font-semibold text-white transition-colors ${
className={`w-full flex items-center justify-center rounded-xl px-6 py-3 text-base font-semibold transition-all duration-200 transform hover:-translate-y-0.5 border ${
resetLoading
? 'bg-gray-400 cursor-wait'
: 'bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-900'
? 'border-white/30 bg-white/20 text-slate-300 cursor-wait'
: 'border-white/55 bg-white/30 text-[#0F172A] shadow-[0_10px_30px_rgba(15,23,42,0.45)] hover:bg-white/40 hover:shadow-[0_16px_40px_rgba(15,23,42,0.6)] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80'
}`}
>
{resetLoading ? (
<>
<span className="size-4 mr-2 rounded-full border-2 border-white border-b-transparent animate-spin" />
Speichern...
Saving...
</>
) : (
'Neues Passwort setzen'
'Set new password'
)}
</button>
<div className="text-center text-sm text-gray-600 dark:text-gray-400">
Link abgelaufen?{' '}
Link expired?{' '}
<button
type="button"
onClick={() => router.push('/password-reset')}
className="text-indigo-600 dark:text-indigo-400 hover:underline font-medium"
>
Erneut anfordern
Request again
</button>
</div>
</form>
@ -309,6 +344,15 @@ export default function PasswordResetPage() {
</div>
</div>
</main>
</div>
</PageLayout>
)
}
export default function PasswordResetPage() {
return (
<ToastProvider>
<PasswordResetPageInner />
</ToastProvider>
)
}

View File

@ -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 (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Bank Information</h2>
{!editingBank && (
<button
className="flex items-center text-[#8D6B1D] hover:text-[#7A5E1A] transition-colors"
onClick={onEdit}
>
<svg className="h-4 w-4 mr-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M16.862 3.487a2.1 2.1 0 013.03 2.91l-9.193 9.193a2.1 2.1 0 01-.595.395l-3.03 1.212a.525.525 0 01-.684-.684l1.212-3.03a2.1 2.1 0 01.395-.595l9.193-9.193z"></path></svg>
Edit
</button>
)}
<span className="text-xs text-gray-500">Editing disabled</span>
</div>
<form
className="space-y-4"
onSubmit={e => {
e.preventDefault()
setBankInfo(bankDraft)
setEditingBank(false)
}}
>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Account Holder</label>
<input
type="text"
className="w-full p-2 border border-gray-200 rounded-lg bg-gray-50 text-gray-900"
value={editingBank ? bankDraft.accountHolder : (profileData.accountHolder || '')}
onChange={e => 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 && (
<div className="mt-1 text-sm italic text-gray-400">Not provided</div>
)}
{!accountHolder && <div className="mt-1 text-sm italic text-gray-400">Not provided</div>}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">IBAN</label>
<input
type="text"
className="w-full p-2 border border-gray-200 rounded-lg bg-gray-50 text-gray-900"
value={editingBank ? bankDraft.iban : (profileData.iban || '')}
onChange={e => 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 && (
<div className="mt-1 text-sm italic text-gray-400">Not provided</div>
)}
{!iban && <div className="mt-1 text-sm italic text-gray-400">Not provided</div>}
</div>
{editingBank && (
<div className="flex gap-2 mt-2">
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-[#8D6B1D] rounded-lg hover:bg-[#7A5E1A] transition"
>
Save
</button>
<button
type="button"
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 transition"
onClick={() => setEditingBank(false)}
>
Cancel
</button>
</div>
)}
</form>
</div>
)
}

View File

@ -7,7 +7,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
onEdit?: () => void
}) {
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-gray-900">Basic Information</h2>
<button
@ -25,7 +25,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
<label className="block text-sm font-medium text-gray-700 mb-1">
First Name
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<div className="flex items-center p-3 bg-white/50 border border-white/60 rounded-lg">
<UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.firstName}>
<span className="text-gray-900">{profileData.firstName}</span>
@ -36,7 +36,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
<label className="block text-sm font-medium text-gray-700 mb-1">
Last Name
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<div className="flex items-center p-3 bg-white/50 border border-white/60 rounded-lg">
<UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.lastName}>
<span className="text-gray-900">{profileData.lastName}</span>
@ -50,7 +50,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
<label className="block text-sm font-medium text-gray-700 mb-1">
Contact Person
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<div className="flex items-center p-3 bg-white/50 border border-white/60 rounded-lg">
<UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.contactPersonName}>
<span className="text-gray-900">{profileData.contactPersonName}</span>
@ -62,7 +62,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
<label className="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<div className="flex items-center p-3 bg-white/50 border border-white/60 rounded-lg">
<EnvelopeIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.email}>
<span className="text-gray-900">{profileData.email}</span>
@ -74,7 +74,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
<label className="block text-sm font-medium text-gray-700 mb-1">
Phone Number
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<div className="flex items-center p-3 bg-white/50 border border-white/60 rounded-lg">
<PhoneIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.phone}>
<span className="text-gray-900">{profileData.phone}</span>
@ -85,7 +85,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
<label className="block text-sm font-medium text-gray-700 mb-1">
Address
</label>
<div className="flex items-center p-3 bg-gray-50 border border-gray-200 rounded-lg">
<div className="flex items-center p-3 bg-white/50 border border-white/60 rounded-lg">
<MapPinIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.address}>
<span className="text-gray-900">{profileData.address}</span>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useState, useRef } from 'react'
export default function EditModal({
open,
@ -19,16 +19,54 @@ export default function EditModal({
onCancel: () => void,
children?: React.ReactNode
}) {
// Prevent background scroll when modal is open
// Prevent background scroll when modal is open (and avoid leaving a right-gap)
const prevStylesRef = useRef<{
bodyOverflow: string
bodyPaddingRight: string
htmlOverflow: string
htmlPaddingRight: string
}>({
bodyOverflow: '',
bodyPaddingRight: '',
htmlOverflow: '',
htmlPaddingRight: '',
})
useEffect(() => {
const body = document.body
const html = document.documentElement
if (open) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
prevStylesRef.current = {
bodyOverflow: body.style.overflow || '',
bodyPaddingRight: body.style.paddingRight || '',
htmlOverflow: html.style.overflow || '',
htmlPaddingRight: html.style.paddingRight || '',
}
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
// lock scroll (some libs lock html, some lock body)
body.style.overflow = 'hidden'
html.style.overflow = 'hidden'
// prevent layout shift + ensure we can restore cleanly
const pr = scrollbarWidth > 0 ? `${scrollbarWidth}px` : ''
body.style.paddingRight = pr
html.style.paddingRight = pr
} else {
body.style.overflow = prevStylesRef.current.bodyOverflow
body.style.paddingRight = prevStylesRef.current.bodyPaddingRight
html.style.overflow = prevStylesRef.current.htmlOverflow
html.style.paddingRight = prevStylesRef.current.htmlPaddingRight
}
return () => {
document.body.style.overflow = '';
};
body.style.overflow = prevStylesRef.current.bodyOverflow
body.style.paddingRight = prevStylesRef.current.bodyPaddingRight
html.style.overflow = prevStylesRef.current.htmlOverflow
html.style.paddingRight = prevStylesRef.current.htmlPaddingRight
}
}, [open]);
// Animation state
@ -52,9 +90,15 @@ export default function EditModal({
}`}
>
<div
className={`bg-white rounded-lg shadow-lg p-6 w-full max-w-md transform transition-all duration-200 ${
className={`rounded-lg shadow-lg p-4 sm:p-6 w-[calc(100%-2rem)] max-w-md max-h-[85dvh] overflow-y-auto transform transition-all duration-200 ${
open ? 'scale-100 opacity-100' : 'scale-95 opacity-0'
}`}
style={{
backgroundColor: 'rgba(255,255,255,0.78)',
backdropFilter: 'blur(14px)',
WebkitBackdropFilter: 'blur(14px)',
border: '1px solid rgba(255,255,255,0.55)',
}}
>
<h2 className="text-xl font-semibold mb-4 text-gray-900">
Edit {type === 'basic' ? 'Basic Information' : 'Bank Information'}

View File

@ -3,7 +3,7 @@ import React from 'react'
export default function MediaSection({ documents }: { documents: any[] }) {
const hasDocuments = Array.isArray(documents) && documents.length > 0;
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Media & Documents</h2>
<div className="overflow-x-auto">
{hasDocuments ? (

View File

@ -2,7 +2,7 @@ import React from 'react';
export default function ProfileCompletion({ profileComplete }: { profileComplete: number }) {
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6 mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Profile Completion</h2>
<span className="text-sm font-medium text-[#8D6B1D]">

View File

@ -8,7 +8,7 @@ export default function UserAbo() {
return (
<section className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">My Subscriptions</h2>
<div className="rounded-md border border-gray-200 bg-white p-4 text-sm text-gray-600">
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">
Loading subscriptions
</div>
</section>
@ -19,7 +19,7 @@ export default function UserAbo() {
return (
<section className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">My Subscriptions</h2>
<div className="rounded-md border border-red-200 bg-red-50 p-4 text-sm text-red-700">
<div className="rounded-md border border-red-200 bg-red-50/80 backdrop-blur-md p-4 text-sm text-red-700">
{error}
</div>
</section>
@ -30,11 +30,11 @@ export default function UserAbo() {
<section className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">My Subscriptions</h2>
{(!abos || abos.length === 0) ? (
<div className="rounded-md border border-gray-200 bg-white p-4 text-sm text-gray-600">
<div className="rounded-md border border-white/60 bg-white/70 backdrop-blur-md p-4 text-sm text-gray-600 shadow-lg">
No subscriptions yet.
</div>
) : (
<div className="grid gap-4">
<div className="grid gap-3 sm:gap-4">
{abos.map(abo => {
const status = (abo.status || 'active') as 'active' | 'paused' | 'canceled'
const nextBilling = abo.nextBillingAt ? new Date(abo.nextBillingAt).toLocaleDateString() : '—'
@ -53,7 +53,7 @@ export default function UserAbo() {
</span>
))
return (
<div key={abo.id} className="rounded-lg border border-gray-200 bg-white p-4 shadow-sm">
<div key={abo.id} className="rounded-lg border border-white/60 bg-white/70 backdrop-blur-md p-4 shadow-lg">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-gray-900">{abo.name || 'Coffee Subscription'}</p>

View File

@ -3,26 +3,18 @@
import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
import Header from '../components/nav/Header'
import Footer from '../components/Footer'
import PageLayout from '../components/PageLayout'
import Waves from '../components/waves'
import ProfileCompletion from './components/profileCompletion'
import BasicInformation from './components/basicInformation'
import MediaSection from './components/mediaSection'
import BankInformation from './components/bankInformation'
import EditModal from './components/editModal'
import UserAbo from './components/userAbo'
import {
UserCircleIcon,
EnvelopeIcon,
PhoneIcon,
MapPinIcon,
PencilIcon,
CheckCircleIcon
} from '@heroicons/react/24/outline'
import { getProfileCompletion } from './hooks/getProfileCompletion';
import { useProfileData } from './hooks/getProfileData';
import { useMedia } from './hooks/getMedia';
import { editProfileBasic, editProfileBank } from './hooks/editProfile';
import { getProfileCompletion } from './hooks/getProfileCompletion'
import { useProfileData } from './hooks/getProfileData'
import { useMedia } from './hooks/getMedia'
import { editProfileBasic } from './hooks/editProfile'
// Helper to display missing fields in subtle gray italic (no yellow highlight)
function HighlightIfMissing({ value, children }: { value: any, children: React.ReactNode }) {
@ -60,85 +52,97 @@ const defaultProfileData = {
userType: '',
};
// Define fields for EditModal
const basicFields = [
{ key: 'firstName', label: 'First Name', type: 'text' },
{ key: 'lastName', label: 'Last Name', type: 'text' },
{ key: 'phone', label: 'Phone', type: 'text' },
{ key: 'address', label: 'Address', type: 'text' },
];
const bankFields = [
{ key: 'accountHolder', label: 'Account Holder', type: 'text' },
{ key: 'iban', label: 'IBAN', type: 'text' },
];
export default function ProfilePage() {
const router = useRouter()
const user = useAuthStore(state => state.user)
const [userId, setUserId] = React.useState<string | number | undefined>(undefined);
const isAuthReady = useAuthStore(state => state.isAuthReady)
const [hasHydrated, setHasHydrated] = React.useState(false)
const [isMobile, setIsMobile] = React.useState(false)
const [userId, setUserId] = React.useState<string | number | undefined>(undefined)
// Update userId when user changes
useEffect(() => {
if (user?.id) setUserId(user.id);
}, [user]);
// --- declare ALL hooks before any early return (Rules of Hooks) ---
const [refreshKey, setRefreshKey] = React.useState(0)
const [showRefreshing, setShowRefreshing] = React.useState(false)
const [completionLoading, setCompletionLoading] = React.useState(false)
// Add refresh key and UI states for smooth refresh
const [refreshKey, setRefreshKey] = React.useState(0);
const [showRefreshing, setShowRefreshing] = React.useState(false);
const [completionLoading, setCompletionLoading] = React.useState(false);
// Progress bar state (MOVED ABOVE EARLY RETURN)
const [progressPercent, setProgressPercent] = React.useState<number>(0)
const [completedSteps, setCompletedSteps] = React.useState<string[]>([])
const [allSteps, setAllSteps] = React.useState<string[]>([])
// Fetch profile data on page load/navigation, now with refreshKey
const { data: profileDataApi, loading: profileLoading, error: profileError } = useProfileData(userId, refreshKey);
// Bank/edit state (keep, but bank editing disabled)
const [bankInfo, setBankInfo] = React.useState({ accountHolder: '', iban: '' })
const [editingBank, setEditingBank] = React.useState(false)
const [bankDraft, setBankDraft] = React.useState(bankInfo)
// Fetch media/documents for user, now with refreshKey
const { data: mediaData, loading: mediaLoading, error: mediaError } = useMedia(userId, refreshKey);
const [editModalOpen, setEditModalOpen] = React.useState(false)
const [editModalType, setEditModalType] = React.useState<'basic' | 'bank'>('basic')
const [editModalValues, setEditModalValues] = React.useState<Record<string, string>>({})
const [editModalError, setEditModalError] = React.useState<string | null>(null)
// Redirect if not logged in
useEffect(() => {
if (!user) {
router.push('/login')
}
}, [user, router])
// Don't render if no user
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
<p className="text-[#4A4A4A]">Loading...</p>
</div>
</div>
)
}
// Progress bar state
const [progressPercent, setProgressPercent] = React.useState<number>(0);
const [completedSteps, setCompletedSteps] = React.useState<string[]>([]);
const [allSteps, setAllSteps] = React.useState<string[]>([]);
useEffect(() => { setHasHydrated(true) }, [])
useEffect(() => {
if (!user) {
router.push('/login');
return;
}
async function fetchCompletion() {
setCompletionLoading(true);
const progress = await getProfileCompletion();
// progress can be percent or object
if (user?.id) setUserId(user.id)
}, [user])
// Fetch hooks can run with undefined userId; they should handle it internally
const { data: profileDataApi, loading: profileLoading } = useProfileData(userId, refreshKey)
const { data: mediaData, loading: mediaLoading } = useMedia(userId, refreshKey)
// Redirect only after hydration + auth ready
useEffect(() => {
if (!hasHydrated || !isAuthReady) return
if (!user) router.replace('/login')
}, [hasHydrated, isAuthReady, user, router])
// Completion fetch (gated inside effect)
useEffect(() => {
if (!hasHydrated || !isAuthReady || !user) return
let cancelled = false
;(async () => {
setCompletionLoading(true)
const progress = await getProfileCompletion()
if (cancelled) return
if (progress && typeof progress === 'object') {
// If not admin-verified, cap progress below 100 to reflect pending verification
const pct = progress.progressPercent ?? 0;
// Try to read admin verification from profileDataApi if available; otherwise assume false until data loads
const isAdminVerified = Boolean(profileDataApi?.userStatus?.is_admin_verified);
setProgressPercent(isAdminVerified ? pct : Math.min(pct, 95));
setCompletedSteps(progress.completedSteps ?? []);
setAllSteps(progress.steps?.map((s: any) => s.name || s.title || '') ?? []);
const pct = progress.progressPercent ?? 0
const isAdminVerified = Boolean(profileDataApi?.userStatus?.is_admin_verified)
setProgressPercent(isAdminVerified ? pct : Math.min(pct, 95))
setCompletedSteps(progress.completedSteps ?? [])
setAllSteps(progress.steps?.map((s: any) => s.name || s.title || '') ?? [])
} else if (typeof progress === 'number') {
setProgressPercent(progress);
setProgressPercent(progress)
}
setCompletionLoading(false);
}
fetchCompletion();
}, [user, router, refreshKey]);
// If admin verification flips to true, ensure progress shows 100%
setCompletionLoading(false)
})()
return () => {
cancelled = true
}
}, [hasHydrated, isAuthReady, user, refreshKey, profileDataApi?.userStatus?.is_admin_verified])
useEffect(() => {
const verified = Boolean(profileDataApi?.userStatus?.is_admin_verified);
if (verified) {
setProgressPercent(prev => (prev < 100 ? 100 : prev));
}
}, [profileDataApi?.userStatus?.is_admin_verified]);
const verified = Boolean(profileDataApi?.userStatus?.is_admin_verified)
if (verified) setProgressPercent(prev => (prev < 100 ? 100 : prev))
}, [profileDataApi?.userStatus?.is_admin_verified])
// Use API profile data if available, fallback to mock
const profileData = React.useMemo(() => {
if (!profileDataApi) {
return {
@ -150,13 +154,13 @@ export default function ProfilePage() {
joinDate: 'Oktober 2024',
memberStatus: 'Gold Member',
profileComplete: progressPercent,
accountHolder: '', // Always empty string if not provided
accountHolder: '',
iban: '',
contactPersonName: '',
userType: user?.userType || '',
};
}
const { user: apiUser = {}, profile: apiProfile = {}, userStatus = {} } = profileDataApi;
}
const { user: apiUser = {}, profile: apiProfile = {}, userStatus = {} } = profileDataApi
return {
firstName: apiUser.firstName ?? apiProfile.first_name ?? '',
lastName: apiUser.lastName ?? apiProfile.last_name ?? '',
@ -168,132 +172,105 @@ export default function ProfilePage() {
: '',
memberStatus: userStatus.status ?? '',
profileComplete: progressPercent,
accountHolder: apiProfile.account_holder_name ?? '', // Only use account_holder_name
accountHolder: apiProfile.account_holder_name ?? '',
iban: apiUser.iban ?? '',
contactPersonName: apiProfile.contact_person_name ?? '',
userType: apiUser.userType ?? '',
};
}, [profileDataApi, user, progressPercent]);
// Dummy data for new sections
const documents = Array.isArray(mediaData?.documents) ? mediaData.documents : [];
// Adjusted bankInfo state to only have accountHolder and iban, always strings
const [bankInfo, setBankInfo] = React.useState({
accountHolder: '',
iban: '',
});
const [editingBank, setEditingBank] = React.useState(false);
const [bankDraft, setBankDraft] = React.useState(bankInfo)
// Modal state
const [editModalOpen, setEditModalOpen] = React.useState(false);
const [editModalType, setEditModalType] = React.useState<'basic' | 'bank'>('basic');
const [editModalValues, setEditModalValues] = React.useState<Record<string, string>>({});
// Modal error state
const [editModalError, setEditModalError] = React.useState<string | null>(null);
// Modal field definitions
const basicFields = [
{ key: 'firstName', label: 'First Name' },
{ key: 'lastName', label: 'Last Name' },
{ key: 'email', label: 'Email Address', type: 'email' },
{ key: 'phone', label: 'Phone Number' },
{ key: 'address', label: 'Address' },
];
const bankFields = [
{ key: 'accountHolder', label: 'Account Holder' },
{ key: 'iban', label: 'IBAN' },
];
// Modal open handlers
function openEditModal(type: 'basic' | 'bank', values: Record<string, string>) {
setEditModalType(type);
setEditModalValues(values);
setEditModalOpen(true);
}
}, [profileDataApi, user, progressPercent])
// Modal save handler (calls API)
async function handleEditModalSave() {
setEditModalError(null);
if (editModalType === 'basic') {
const payload: Partial<typeof defaultProfileData> = {};
(['firstName', 'lastName', 'email', 'phone', 'address'] as const).forEach(key => {
if (editModalValues[key] !== getProfileField(profileData, key)) {
payload[key] = editModalValues[key]?.trim();
}
});
const res = await editProfileBasic(payload);
if (res.success) {
setEditModalOpen(false);
// Start smooth refresh with overlay spinner
setShowRefreshing(true);
setRefreshKey(k => k + 1);
} else if (res.status === 409) {
setEditModalError('Email already in use.');
} else if (res.status === 401) {
router.push('/login');
} else {
setEditModalError(res.error || 'Failed to update profile.');
}
} else {
const payload: Partial<typeof defaultProfileData> = {};
(['accountHolder', 'iban'] as const).forEach(key => {
if (editModalValues[key] !== getProfileField(profileData, key)) {
payload[key] = editModalValues[key]?.trim();
}
});
const res = await editProfileBank(payload);
if (res.success) {
setBankInfo({
accountHolder: res.data?.profile?.account_holder_name ?? '',
iban: res.data?.user?.iban ?? '',
});
setEditModalOpen(false);
// Start smooth refresh with overlay spinner
setShowRefreshing(true);
setRefreshKey(k => k + 1);
} else if (res.status === 400 && res.error?.toLowerCase().includes('iban')) {
setEditModalError('Invalid IBAN.');
} else if (res.status === 401) {
router.push('/login');
} else {
setEditModalError(res.error || 'Failed to update bank info.');
}
}
}
const documents = Array.isArray(mediaData?.documents) ? mediaData.documents : []
// Modal change handler
function handleEditModalChange(key: string, value: string) {
setEditModalValues(prev => ({ ...prev, [key]: value }));
}
// Hide overlay when all data re-fetches complete
useEffect(() => {
if (showRefreshing && !profileLoading && !mediaLoading && !completionLoading) {
const t = setTimeout(() => setShowRefreshing(false), 200); // small delay for smoothness
return () => clearTimeout(t);
const t = setTimeout(() => setShowRefreshing(false), 200)
return () => clearTimeout(t)
}
}, [showRefreshing, profileLoading, mediaLoading, completionLoading]);
}, [showRefreshing, profileLoading, mediaLoading, completionLoading])
const loadingUser = !user;
function openEditModal(type: 'basic' | 'bank', values: Record<string, string>) {
setEditModalType(type)
setEditModalValues(values)
setEditModalOpen(true)
}
async function handleEditModalSave() {
setEditModalError(null)
if (editModalType === 'basic') {
const payload: Partial<typeof defaultProfileData> = {}
;(['firstName', 'lastName', 'phone', 'address'] as const).forEach(key => {
if (editModalValues[key] !== getProfileField(profileData, key)) {
payload[key] = editModalValues[key]?.trim()
}
})
const res = await editProfileBasic(payload)
if (res.success) {
setEditModalOpen(false)
setShowRefreshing(true)
setRefreshKey(k => k + 1)
} else if (res.status === 401) {
router.push('/login')
} else {
setEditModalError(res.error || 'Failed to update profile.')
}
} else {
setEditModalError('Bank information editing is disabled for now.')
}
}
function handleEditModalChange(key: string, value: string) {
setEditModalValues(prev => ({ ...prev, [key]: value }))
}
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)
}
}, [])
// --- EARLY RETURN AFTER ALL HOOKS ---
if (!hasHydrated || !isAuthReady || !user) {
return (
<div className="min-h-screen flex flex-col bg-gray-50" suppressHydrationWarning>
<Header />
<main className="flex-1 py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
{loadingUser && (
<div className="flex items-center justify-center py-20">
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
<p className="text-[#4A4A4A]">Loading...</p>
</div>
</div>
)}
{!loadingUser && (
<>
)
}
return (
<div className="relative w-full min-h-screen overflow-x-hidden">
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
animate={!isMobile}
interactive={!isMobile}
/>
<div className="relative z-10 min-h-screen">
<PageLayout className="bg-transparent text-gray-900">
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
{/* MASTER GLASS PANEL (prevents non-translucent gaps between cards) */}
<div className="rounded-3xl bg-white/60 backdrop-blur-md border border-white/50 shadow-xl p-4 sm:p-6 lg:p-8">
{/* Page Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Profile Settings</h1>
@ -304,17 +281,15 @@ export default function ProfilePage() {
{/* Pending admin verification notice (above progress) */}
{profileDataApi?.userStatus && profileDataApi.userStatus.is_admin_verified === 0 && (
<div className="rounded-md bg-yellow-50 border border-yellow-200 p-3 text-sm text-yellow-800 mb-2">
<div className="rounded-md bg-yellow-50/80 backdrop-blur border border-yellow-200 p-3 text-sm text-yellow-800 mb-2">
Your account is fully submitted. Our team will verify your account shortly.
</div>
)}
{/* Profile Completion Progress Bar */}
<ProfileCompletion
profileComplete={profileData.profileComplete}
/>
<ProfileCompletion profileComplete={profileData.profileComplete} />
{/* Basic Info + Sidebar */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 sm:gap-8 mb-8">
{/* Basic Information */}
<div className="lg:col-span-2 space-y-6">
<BasicInformation
@ -324,7 +299,6 @@ export default function ProfilePage() {
onEdit={() => openEditModal('basic', {
firstName: profileData.firstName,
lastName: profileData.lastName,
email: profileData.email,
phone: profileData.phone,
address: profileData.address,
})}
@ -332,8 +306,8 @@ export default function ProfilePage() {
</div>
{/* Sidebar: Account Status + Quick Actions */}
<div className="space-y-6">
{/* Account Status */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
{/* Account Status (make translucent) */}
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<h3 className="font-semibold text-gray-900 mb-4">Account Status</h3>
<div className="space-y-3">
@ -355,8 +329,8 @@ export default function ProfilePage() {
</div>
</div>
</div>
{/* Quick Actions */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
{/* Quick Actions (make translucent) */}
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<h3 className="font-semibold text-gray-900 mb-4">Quick Actions</h3>
<div className="space-y-3">
@ -378,79 +352,26 @@ export default function ProfilePage() {
</div>
{/* Bank Info, Media */}
<div className="space-y-8 mb-8">
<div className="space-y-6 sm:space-y-8 mb-8">
{/* --- My Abo Section (above bank info) --- */}
<UserAbo />
{/* --- Edit Bank Information Section --- */}
<BankInformation
profileData={profileData}
editingBank={editingBank}
editingBank={false} // force read-only
bankDraft={bankDraft}
setEditingBank={setEditingBank}
setBankDraft={setBankDraft}
setBankInfo={setBankInfo}
// Add edit button handler
onEdit={() => openEditModal('bank', {
accountHolder: profileData.accountHolder,
iban: profileData.iban,
})}
// onEdit disabled for now
// onEdit={() => openEditModal('bank', { ... })}
/>
{/* --- Media Section --- */}
<MediaSection documents={documents} />
</div>
{/* Account Settings */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Account Settings</h2>
<div className="space-y-4">
<div className="flex items-center justify-between py-3 border-b border-gray-100">
<div>
<p className="font-medium text-gray-900">Email Notifications</p>
<p className="text-sm text-gray-600">Receive updates about orders and promotions</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" defaultChecked />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#8D6B1D]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#8D6B1D]"></div>
</label>
</div>
<div className="flex items-center justify-between py-3 border-b border-gray-100">
<div>
<p className="font-medium text-gray-900">SMS Notifications</p>
<p className="text-sm text-gray-600">Get text messages for important updates</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-[#8D6B1D]/20 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-[#8D6B1D]"></div>
</label>
</div>
<div className="flex items-center justify-between py-3">
<div>
<p className="font-medium text-gray-900">Two-Factor Authentication</p>
<p className="text-sm text-gray-600">Add extra security to your account</p>
</div>
<button className="px-4 py-2 text-sm font-medium text-[#8D6B1D] border border-[#8D6B1D] rounded-lg hover:bg-[#8D6B1D]/10 transition-colors">
Enable
</button>
</div>
</div>
</div>
</>
)}
</div>
</main>
<Footer />
{/* Global refreshing overlay */}
{showRefreshing && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white/60 backdrop-blur-sm">
<div className="flex flex-col items-center">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-[#8D6B1D]/30 border-t-[#8D6B1D] mb-3"></div>
<p className="text-sm text-gray-700">Updating...</p>
</div>
</div>
)}
{/* Edit Modal */}
<EditModal
@ -467,6 +388,8 @@ export default function ProfilePage() {
<div className="text-sm text-red-600 mb-2">{editModalError}</div>
)}
</EditModal>
</PageLayout>
</div>
</div>
)
}

View File

@ -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 (
<div className="w-full max-w-4xl mx-auto bg-white rounded-2xl shadow-2xl px-6 py-8 sm:px-12 sm:py-10">
// softened outer container, no own solid white card parent provides glass card
<div className="w-full">
{/* Header */}
<div className="mb-6 text-center">
<h2 className="text-2xl sm:text-3xl font-extrabold text-[#0F172A] mb-2">
@ -409,7 +407,7 @@ export default function RegisterForm({
{/* Mode Toggle */}
<div className="flex justify-center mb-8">
<div className="bg-gray-100 p-1 rounded-lg">
<div className="bg-white/40 backdrop-blur-[18px] border border-white/35 shadow-sm p-1 rounded-lg">
<button
className={`px-6 py-2 rounded-md font-semibold text-sm transition-all duration-200 ${
mode === 'personal'
@ -437,7 +435,7 @@ export default function RegisterForm({
{/* Error Message */}
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="mb-6 p-4 bg-red-50/70 backdrop-blur-[18px] border border-red-200/70 rounded-lg">
<p className="text-red-600 text-sm font-medium">{error}</p>
</div>
)}
@ -457,7 +455,7 @@ export default function RegisterForm({
name="firstName"
value={personalForm.firstName}
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
/>
</div>
@ -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
/>
</div>
@ -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
/>
</div>
@ -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
/>
</div>
@ -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
/>
<button
@ -566,7 +566,8 @@ export default function RegisterForm({
name="confirmPassword"
value={personalForm.confirmPassword}
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"
autoComplete="new-password"
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
/>
</div>
@ -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
/>
</div>
@ -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
/>
</div>
@ -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
/>
</div>
@ -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
/>
</div>
@ -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
/>
<button
@ -734,7 +738,8 @@ export default function RegisterForm({
name="confirmPassword"
value={companyForm.confirmPassword}
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"
autoComplete="new-password"
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
/>
</div>

View File

@ -17,13 +17,15 @@ export default function SessionDetectedModal({
onCancel,
inline = false
}: SessionDetectedModalProps) {
// Make inline + non-inline consistent
if (!open) return null
if (inline) {
// Inline wrapper removed: parent already wraps/centers
return (
// removed flex-1 and min-h to avoid extra white gap
<div className="w-full flex justify-center items-center py-8">
<div className="bg-white px-6 py-6 rounded-xl shadow-xl max-w-lg w-full border border-gray-200">
<div className="max-w-lg w-full rounded-2xl border border-amber-200/70 bg-white/70 backdrop-blur-xl shadow-2xl px-6 py-6">
<div className="flex gap-4">
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-orange-100">
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-orange-100/80">
<ExclamationTriangleIcon className="h-6 w-6 text-orange-600" aria-hidden="true" />
</div>
<div>
@ -37,7 +39,7 @@ export default function SessionDetectedModal({
<button
type="button"
onClick={onCancel}
className="inline-flex justify-center rounded-md bg-white px-4 py-2 text-sm font-medium text-[#4A4A4A] shadow-sm ring-1 ring-gray-300 hover:bg-gray-50 transition-colors"
className="inline-flex justify-center rounded-md bg-white/80 px-4 py-2 text-sm font-medium text-[#4A4A4A] shadow-sm ring-1 ring-gray-300/70 hover:bg-gray-50 transition-colors"
>
Go to dashboard
</button>
@ -52,7 +54,6 @@ export default function SessionDetectedModal({
</div>
</div>
</div>
</div>
)
}
@ -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"
>
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<Dialog.Panel
className="relative transform overflow-hidden rounded-2xl border border-white/30 bg-white/70 backdrop-blur-xl px-4 pb-4 pt-5 text-left shadow-2xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6"
>
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-orange-100 sm:mx-0 sm:h-10 sm:w-10">
<ExclamationTriangleIcon

View File

@ -21,30 +21,32 @@ export default function InvalidRefLinkModal({
if (!open) return null
const Content = (
<div className="w-full max-w-md rounded-lg border border-red-200 bg-white p-6 shadow-md">
<div className="w-full max-w-md rounded-2xl border border-red-300/70 bg-white/60 backdrop-blur-xl shadow-2xl p-6 sm:p-7">
<div className="flex items-start gap-3">
<ExclamationTriangleIcon className="h-6 w-6 text-red-600 shrink-0" />
<div className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-red-100/80">
<ExclamationTriangleIcon className="h-6 w-6 text-red-600" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">Invalid invitation link</h3>
<p className="mt-1 text-sm text-gray-600">
<h3 className="text-lg font-semibold text-[#0F172A]">Invalid invitation link</h3>
<p className="mt-1 text-sm text-slate-700">
This registration link is invalid or no longer active. Please request a new link.
</p>
{token ? (
<p className="mt-2 text-xs text-gray-500">
{token && (
<p className="mt-2 text-xs text-slate-500">
Token: <span className="font-mono break-all">{token}</span>
</p>
) : null}
<div className="mt-4 flex items-center gap-2">
)}
<div className="mt-4 flex flex-wrap items-center gap-2">
<button
onClick={onGoHome}
className="inline-flex items-center rounded-md bg-[#8D6B1D] px-3 py-2 text-sm text-white hover:bg-[#7A5E1A]"
className="inline-flex items-center rounded-md bg-[#8D6B1D] px-3.5 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#7A5E1A] transition-colors"
>
Go to homepage
</button>
{onClose && (
<button
onClick={onClose}
className="inline-flex items-center rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-800 hover:bg-gray-50"
className="inline-flex items-center rounded-md border border-slate-300/80 bg-white/70 px-3.5 py-2 text-sm font-semibold text-slate-800 hover:bg-slate-50/90 transition-colors"
>
Close
</button>
@ -55,13 +57,7 @@ export default function InvalidRefLinkModal({
</div>
)
if (inline) {
return (
<div className="w-full flex items-center justify-center py-16">
{Content}
</div>
)
}
if (inline) return Content
return Content
}

View File

@ -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 (
<ToastProvider>
<PageLayout>
<main className="w-full flex flex-col flex-1 items-center justify-center py-24 min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
/>
<main
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
style={mainStyle}
>
<div className="mx-auto max-w-7xl px-6 lg:px-10 flex flex-col w-full">
<div
className="mx-auto w-full max-w-md rounded-3xl shadow-2xl relative border border-white/35 overflow-hidden p-6 sm:p-8"
style={{
backgroundColor: 'rgba(255,255,255,0.55)',
backdropFilter: 'blur(18px)',
WebkitBackdropFilter: 'blur(18px)',
boxShadow: '0 18px 45px rgba(15,23,42,0.45)',
}}
>
<div className="absolute -top-24 -right-24 size-56 rounded-full bg-gradient-to-br from-indigo-500/10 to-fuchsia-500/10 blur-3xl pointer-events-none" />
<div className="relative text-center">
<div className="animate-spin rounded-full h-10 w-10 border-2 border-b-transparent border-[#8D6B1D] mx-auto mb-4" />
<p className="text-slate-700">Checking invitation link</p>
</div>
</div>
</div>
</main>
</div>
</PageLayout>
</ToastProvider>
)
}
// NEW: Invalid referral link state — show modal instead of form with same background as register form
if (invalidRef) {
return (
<ToastProvider>
<PageLayout>
<main className="w-full flex flex-col flex-1 gap-10 min-h-screen">
{/* make wrapper flex-1 so background reaches the footer */}
<div className="relative flex-1 overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24">
{/* Pattern */}
<svg
aria-hidden="true"
className="absolute inset-0 -z-10 h-full w-full stroke-white/10"
>
<defs>
<pattern
id="register-pattern"
x="50%"
y={-1}
width={200}
height={200}
patternUnits="userSpaceOnUse"
>
<path
d="M.5 200V.5H200"
fill="none"
stroke="rgba(255,255,255,0.05)"
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
/>
</pattern>
</defs>
<rect
fill="url(#register-pattern)"
width="100%"
height="100%"
strokeWidth={0}
/>
</svg>
{/* Colored blur */}
<div
aria-hidden="true"
className="absolute top-0 right-0 left-1/2 -z-10 -ml-24 transform-gpu overflow-hidden blur-3xl lg:ml-24 xl:ml-48"
<main
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
style={mainStyle}
>
<div className="mx-auto max-w-7xl px-6 lg:px-10 flex flex-col w-full">
<div
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
className="mx-auto w-full max-w-3xl min-h-[520px] sm:min-h-[560px] rounded-3xl shadow-2xl relative border border-white/35 overflow-hidden p-6 sm:p-10 md:py-12 md:px-14"
style={{
clipPath:
'polygon(63.1% 29.5%, 100% 17.1%, 76.6% 3%, 48.4% 0%, 44.6% 4.7%, 54.5% 25.3%, 59.8% 49%, 55.2% 57.8%, 44.4% 57.2%, 27.8% 47.9%, 35.1% 81.5%, 0% 97.7%, 39.2% 100%, 35.2% 81.4%, 97.2% 52.8%, 63.1% 29.5%)'
backgroundColor: 'rgba(255,255,255,0.55)', // Use a translucent white for glass effect
backdropFilter: 'blur(18px)', // Glass blur
WebkitBackdropFilter: 'blur(18px)',
boxShadow: '0 18px 45px rgba(15,23,42,0.45)',
}}
/>
</div>
{/* Additional background layers */}
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_30%_20%,rgba(255,255,255,0.1),transparent_65%)]" />
<div className="mx-auto max-w-7xl px-6 lg:px-8 relative z-10">
<div className="flex flex-col flex-1 items-center justify-center">
>
<div className="absolute -top-24 -right-24 size-56 rounded-full bg-gradient-to-br from-indigo-500/10 to-fuchsia-500/10 blur-3xl pointer-events-none" />
<div className="relative flex items-center justify-center">
<InvalidRefLinkModal
inline
open
@ -200,80 +253,57 @@ export default function RegisterPage() {
</div>
</div>
</main>
</div>
</PageLayout>
</ToastProvider>
)
}
// normal register
return (
<ToastProvider>
<PageLayout>
<main className="w-full flex flex-col flex-1 gap-10 min-h-screen">
{/* Background section wrapper */}
{/* make wrapper flex-1 so background reaches the footer */}
<div className="relative flex-1 overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24">
{/* Pattern */}
<svg
aria-hidden="true"
className="absolute inset-0 -z-10 h-full w-full stroke-white/10"
>
<defs>
<pattern
id="register-pattern"
x="50%"
y={-1}
width={200}
height={200}
patternUnits="userSpaceOnUse"
>
<path
d="M.5 200V.5H200"
fill="none"
stroke="rgba(255,255,255,0.05)"
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
/>
</pattern>
</defs>
<rect
fill="url(#register-pattern)"
width="100%"
height="100%"
strokeWidth={0}
/>
</svg>
{/* Colored blur */}
<div
aria-hidden="true"
className="absolute top-0 right-0 left-1/2 -z-10 -ml-24 transform-gpu overflow-hidden blur-3xl lg:ml-24 xl:ml-48"
<main
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
style={mainStyle}
>
<div className="mx-auto max-w-7xl px-6 lg:px-10 flex flex-col w-full">
<div
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
className="mx-auto w-full max-w-4xl min-h-[520px] sm:min-h-[560px] rounded-3xl shadow-2xl relative border border-white/35 overflow-hidden p-6 sm:p-10 md:py-12 md:px-14"
style={{
clipPath:
'polygon(63.1% 29.5%, 100% 17.1%, 76.6% 3%, 48.4% 0%, 44.6% 4.7%, 54.5% 25.3%, 59.8% 49%, 55.2% 57.8%, 44.4% 57.2%, 27.8% 47.9%, 35.1% 81.5%, 0% 97.7%, 39.2% 100%, 35.2% 81.4%, 97.2% 52.8%, 63.1% 29.5%)'
backgroundColor: 'rgba(255, 255, 255, 0.9)', // Use a translucent white for glass effect
backdropFilter: 'blur(40px)', // Glass blur
WebkitBackdropFilter: 'blur(40px)',
boxShadow: '0 18px 45px rgba(15,23,42,0.45)',
}}
/>
</div>
{/* Additional background layers */}
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_30%_20%,rgba(255,255,255,0.1),transparent_65%)]" />
<div className="mx-auto max-w-7xl px-6 lg:px-8 relative z-10">
{/* Heading (optional adjusted to registration context) */}
<div className="mx-auto max-w-2xl text-center mb-10">
<h1 className="text-4xl font-semibold tracking-tight text-white sm:text-5xl">
>
<div className="absolute -top-24 -right-24 size-56 rounded-full bg-gradient-to-br from-indigo-500/10 to-fuchsia-500/10 blur-3xl pointer-events-none" />
<div className="relative">
<div className="mx-auto max-w-2xl text-center mb-8">
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-[#0F172A]">
Register now
</h1>
<p className="mt-2 text-lg/8 text-gray-200">
<p className="mt-3 text-slate-700 text-base sm:text-lg">
Create your personal or company account with Profit Planet.
</p>
</div>
{/* Content area */}
<div className="flex flex-col flex-1">
<div className="mt-2">
{showSessionModal ? (
<div className="flex flex-1 items-center justify-center">
<div className="flex items-center justify-center">
<SessionDetectedModal
inline
open
@ -283,7 +313,6 @@ export default function RegisterPage() {
</div>
) : (
<>
{/* Register form (only if ref valid) */}
{(!user || sessionCleared) && (
<RegisterForm
mode={mode}
@ -294,7 +323,7 @@ export default function RegisterPage() {
/>
)}
{registered && (
<div className="mt-6 mx-auto text-center text-sm text-gray-200">
<div className="mt-6 mx-auto text-center text-sm text-gray-700">
Registration successful redirecting...
</div>
)}
@ -303,8 +332,18 @@ export default function RegisterPage() {
</div>
</div>
</div>
</div>
</main>
</div>
</PageLayout>
)
}
// NEW: default export only provides the ToastProvider wrapper
export default function RegisterPage() {
return (
<ToastProvider>
<RegisterPageInner />
</ToastProvider>
)
}

View File

@ -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<unknown>
@ -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,18 +73,27 @@ 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<void>((resolve, reject) => {
const existing = document.querySelector<HTMLScriptElement>(
'script[data-intl-tel-input-js="true"]'
)
if (existing) {
const existingSrc = existing.getAttribute('src') || ''
if (existingSrc !== ITI_CDN_JS) {
console.warn('[phoneUtils] Replacing existing intl-tel-input script with different src', {
existingSrc,
desiredSrc: ITI_CDN_JS,
})
existing.remove()
} else {
console.log('[phoneUtils] Reusing existing intl-tel-input <script> tag, waiting for load')
existing.addEventListener('load', () => resolve(), { once: true })
existing.addEventListener(
@ -91,28 +103,29 @@ async function loadIntlTelInputFromCdn(): Promise<
)
return
}
}
const script = document.createElement('script')
script.src = ITI_CDN_JS
script.async = true
script.dataset.intlTelInputJs = 'true'
script.onload = () => {
console.log('[phoneUtils] intl-tel-input core script loaded successfully')
console.log('[phoneUtils] intl-tel-input script loaded successfully')
resolve()
}
script.onerror = () => {
console.error('[phoneUtils] intl-tel-input core script failed to load')
console.error('[phoneUtils] intl-tel-input script failed to load')
reject(new Error('Failed to load intl-tel-input'))
}
document.head.appendChild(script)
})
if (!window.intlTelInput) {
console.error('[phoneUtils] window.intlTelInput missing after core script load')
console.error('[phoneUtils] window.intlTelInput missing after script load')
throw new Error('intl-tel-input not found on window after script load')
}
console.log('[phoneUtils] window.intlTelInput is ready (core only, utils will be loaded via loadUtils)')
console.log('[phoneUtils] window.intlTelInput is ready (with utils bundle)')
return window.intlTelInput
}
@ -123,7 +136,10 @@ export async function ensureIntlCoreLoaded(): Promise<
(input: HTMLInputElement, options: any) => IntlTelInputInstance
> {
if (!intlLoaderPromise) {
intlLoaderPromise = loadIntlTelInputFromCdn()
intlLoaderPromise = loadIntlTelInputFromCdn().catch(err => {
intlLoaderPromise = null
throw err
})
}
return intlLoaderPromise
}
@ -138,25 +154,26 @@ export async function createIntlTelInput(
): Promise<IntlTelInputInstance> {
const intlTelInput = await ensureIntlCoreLoaded()
console.log('[phoneUtils] Creating intl-tel-input instance with loadUtils', {
console.log('[phoneUtils] Creating intl-tel-input instance', {
id: input.id,
name: input.name,
})
// Defensive: if an instance already exists on this input (common in dev/StrictMode),
// destroy it to avoid stale state (e.g. wrong selected country/flag).
try {
const anyFactory = intlTelInput as any
const existing =
typeof anyFactory?.getInstance === 'function' ? anyFactory.getInstance(input) : null
if (existing && typeof existing.destroy === 'function') {
existing.destroy()
}
} catch {
// ignore
}
const instance = intlTelInput(input, {
...options,
loadUtils: () => {
console.log('[phoneUtils] loadUtils() called for', { id: input.id, name: input.name })
// docs: load utils from CDN via dynamic import
return import(/* @vite-ignore */ ITI_CDN_UTILS).then(mod => {
console.log('[phoneUtils] utils.js module loaded for', {
id: input.id,
name: input.name,
keys: Object.keys(mod || {}),
})
return mod
})
},
})
const anyInst = instance as any