This commit is contained in:
seaznCode 2026-01-14 16:58:27 +01:00
commit 1045debc32
24 changed files with 2174 additions and 1606 deletions

View File

@ -1,6 +1,7 @@
'use client' 'use client'
import PageLayout from '../components/PageLayout' import PageLayout from '../components/PageLayout'
import Waves from '../components/waves'
import { import {
UsersIcon, UsersIcon,
ExclamationTriangleIcon, ExclamationTriangleIcon,
@ -84,74 +85,94 @@ export default function AdminDashboardPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen"> <div
<main className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8"> className="relative w-full flex flex-col min-h-screen overflow-hidden"
{/* Header */} style={{ backgroundImage: 'none', background: 'none' }}
<header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8"> >
<div> <Waves
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Admin Dashboard</h1> className="pointer-events-none"
<p className="text-lg text-blue-700 mt-2"> lineColor="#0f172a"
Manage all administrative features, user management, permissions, and global settings. backgroundColor="rgba(245, 245, 240, 1)"
</p> waveSpeedX={0.02}
</div> waveSpeedY={0.01}
</header> waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
/>
{/* Warning banner */} <div className="relative z-10 min-h-screen flex flex-col">
<div className="rounded-2xl border border-red-300 bg-red-50 text-red-700 px-8 py-6 flex gap-3 items-start text-base mb-8 shadow"> <main className="flex-1 max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8 w-full">
<ExclamationTriangleIcon className="h-6 w-6 flex-shrink-0 text-red-500 mt-0.5" /> <div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-6 sm:p-8">
<div className="leading-relaxed"> {/* Header */}
<p className="font-semibold mb-0.5"> <header className="flex flex-col gap-4 mb-8">
Warning: Settings and actions below this point can have consequences for the entire system!
</p>
<p className="text-red-600/80 hidden sm:block">
Manage all administrative features, user management, permissions, and global settings.
</p>
</div>
</div>
{/* Stats Card */}
<div className="mb-8 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-6">
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Total Users</div>
<div className="text-xl font-semibold text-blue-900">{displayStats.totalUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Admins</div>
<div className="text-xl font-semibold text-indigo-700">{displayStats.adminUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Active</div>
<div className="text-xl font-semibold text-green-700">{displayStats.activeUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Pending Verification</div>
<div className="text-xl font-semibold text-amber-700">{displayStats.verificationPending}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Personal</div>
<div className="text-xl font-semibold text-blue-700">{displayStats.personalUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Company</div>
<div className="text-xl font-semibold text-purple-700">{displayStats.companyUsers}</div>
</div>
</div>
{/* Management Shortcuts Card */}
<div className="mb-8">
<div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg hover:shadow-xl transition">
<div className="flex items-start gap-4 mb-6">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100">
<Squares2X2Icon className="h-7 w-7 text-blue-600" />
</div>
<div> <div>
<h2 className="text-lg font-semibold text-blue-900">Management Shortcuts</h2> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Admin Dashboard</h1>
<p className="text-sm text-blue-700 mt-0.5"> <p className="text-lg text-blue-700 mt-2">
Quick access to common admin modules. Manage all administrative features, user management, permissions, and global settings.
</p>
</div>
</header>
{/* Warning banner */}
<div className="rounded-2xl border border-red-300 bg-red-50 text-red-700 px-8 py-6 flex gap-3 items-start text-base mb-8 shadow">
<ExclamationTriangleIcon className="h-6 w-6 flex-shrink-0 text-red-500 mt-0.5" />
<div className="leading-relaxed">
<p className="font-semibold mb-0.5">
Warning: Settings and actions below this point can have consequences for the entire system!
</p>
<p className="text-red-600/80 hidden sm:block">
Manage all administrative features, user management, permissions, and global settings.
</p> </p>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Stats Card */}
<div className="mb-8 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-6">
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Total Users</div>
<div className="text-xl font-semibold text-blue-900">{displayStats.totalUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Admins</div>
<div className="text-xl font-semibold text-indigo-700">{displayStats.adminUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Active</div>
<div className="text-xl font-semibold text-green-700">{displayStats.activeUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Pending Verification</div>
<div className="text-xl font-semibold text-amber-700">{displayStats.verificationPending}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Personal</div>
<div className="text-xl font-semibold text-blue-700">{displayStats.personalUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Company</div>
<div className="text-xl font-semibold text-purple-700">{displayStats.companyUsers}</div>
</div>
</div>
{/* Management Shortcuts Card */}
<div className="mb-8">
<div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg hover:shadow-xl transition">
<div className="flex items-start gap-4 mb-6">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100">
<Squares2X2Icon className="h-7 w-7 text-blue-600" />
</div>
<div>
<h2 className="text-lg font-semibold text-blue-900">Management Shortcuts</h2>
<p className="text-sm text-blue-700 mt-0.5">
Quick access to common admin modules.
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Matrix Management */} {/* Matrix Management */}
<button <button
type="button" type="button"
@ -303,80 +324,81 @@ export default function AdminDashboardPage() {
}`} }`}
/> />
</button> </button>
</div> </div>
</div>
</div>
{/* Server Status & Logs */}
<div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg hover:shadow-xl transition">
<div className="flex items-start gap-4 mb-6">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gray-100">
<ServerStackIcon className="h-7 w-7 text-gray-700" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900">
Server Status & Logs
</h2>
<p className="text-sm text-gray-500 mt-0.5">
System health, resource usage & recent error insights.
</p>
</div>
</div>
<div className="grid gap-8 lg:grid-cols-3">
{/* Metrics */}
<div className="space-y-4">
<div className="flex items-center gap-3">
<span className={`h-2.5 w-2.5 rounded-full ${serverStats.status === 'Online' ? 'bg-emerald-500' : 'bg-red-500'}`} />
<p className="text-base">
<span className="font-semibold">Server Status:</span>{' '}
<span className={serverStats.status === 'Online' ? 'text-emerald-600 font-medium' : 'text-red-600 font-medium'}>
{serverStats.status === 'Online' ? 'Server Online' : 'Offline'}
</span>
</p>
</div>
<div className="text-sm space-y-1 text-gray-600">
<p><span className="font-medium text-gray-700">Uptime:</span> {serverStats.uptime}</p>
<p><span className="font-medium text-gray-700">CPU Usage:</span> {serverStats.cpu}</p>
<p><span className="font-medium text-gray-700">Memory Usage:</span> {serverStats.memory} GB</p>
</div>
<div className="flex items-center gap-2 text-sm text-gray-500">
<CpuChipIcon className="h-4 w-4" />
<span>Autoscaled environment (mock)</span>
</div> </div>
</div> </div>
{/* Divider */} {/* Server Status & Logs */}
<div className="hidden lg:block border-l border-gray-200" /> <div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg hover:shadow-xl transition">
<div className="flex items-start gap-4 mb-6">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gray-100">
<ServerStackIcon className="h-7 w-7 text-gray-700" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900">
Server Status & Logs
</h2>
<p className="text-sm text-gray-500 mt-0.5">
System health, resource usage & recent error insights.
</p>
</div>
</div>
{/* Logs */} <div className="grid gap-8 lg:grid-cols-3">
<div className="lg:col-span-2"> {/* Metrics */}
<h3 className="text-base font-semibold text-gray-800 mb-3"> <div className="space-y-4">
Recent Error Logs <div className="flex items-center gap-3">
</h3> <span className={`h-2.5 w-2.5 rounded-full ${serverStats.status === 'Online' ? 'bg-emerald-500' : 'bg-red-500'}`} />
{serverStats.recentErrors.length === 0 && ( <p className="text-base">
<p className="text-sm text-gray-500 italic"> <span className="font-semibold">Server Status:</span>{' '}
No recent logs. <span className={serverStats.status === 'Online' ? 'text-emerald-600 font-medium' : 'text-red-600 font-medium'}>
</p> {serverStats.status === 'Online' ? 'Server Online' : 'Offline'}
)} </span>
{/* Placeholder for future logs list */} </p>
{/* TODO: Replace with mapped log entries */} </div>
<div className="mt-6"> <div className="text-sm space-y-1 text-gray-600">
<button <p><span className="font-medium text-gray-700">Uptime:</span> {serverStats.uptime}</p>
type="button" <p><span className="font-medium text-gray-700">CPU Usage:</span> {serverStats.cpu}</p>
className="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-gray-50 hover:bg-gray-100 text-gray-700 text-sm font-medium px-4 py-3 transition" <p><span className="font-medium text-gray-700">Memory Usage:</span> {serverStats.memory} GB</p>
// TODO: navigate to logs / monitoring page </div>
onClick={() => {}} <div className="flex items-center gap-2 text-sm text-gray-500">
> <CpuChipIcon className="h-4 w-4" />
View Full Logs <span>Autoscaled environment (mock)</span>
<ArrowRightIcon className="h-5 w-5" /> </div>
</button> </div>
{/* Divider */}
<div className="hidden lg:block border-l border-gray-200" />
{/* Logs */}
<div className="lg:col-span-2">
<h3 className="text-base font-semibold text-gray-800 mb-3">
Recent Error Logs
</h3>
{serverStats.recentErrors.length === 0 && (
<p className="text-sm text-gray-500 italic">
No recent logs.
</p>
)}
{/* Placeholder for future logs list */}
{/* TODO: Replace with mapped log entries */}
<div className="mt-6">
<button
type="button"
className="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-gray-50 hover:bg-gray-100 text-gray-700 text-sm font-medium px-4 py-3 transition"
// TODO: navigate to logs / monitoring page
onClick={() => {}}
>
View Full Logs
<ArrowRightIcon className="h-5 w-5" />
</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </main>
</div>
</main>
</div> </div>
</PageLayout> </PageLayout>
) )

View File

@ -2,6 +2,7 @@
import { useEffect, useState, useMemo } from 'react' import { useEffect, useState, useMemo } from 'react'
import PageLayout from '../components/PageLayout' import PageLayout from '../components/PageLayout'
import Waves from '../components/waves'
type Affiliate = { type Affiliate = {
id: string id: string
@ -89,105 +90,127 @@ export default function AffiliateLinksPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen"> <div
<main className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8"> className="relative w-full flex flex-col min-h-screen overflow-hidden"
{/* Header (aligned with management pages) */} style={{ backgroundImage: 'none', background: 'none' }}
<header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8"> >
<div> <Waves
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Affiliate Partners</h1> className="pointer-events-none"
<p className="text-lg text-blue-700 mt-2"> lineColor="#0f172a"
Discover our trusted partners and earn commissions through affiliate links. backgroundColor="rgba(245, 245, 240, 1)"
</p> waveSpeedX={0.02}
</div> waveSpeedY={0.01}
{/* NEW: Category filter */} waveAmpX={40}
<div className="flex items-center gap-2"> waveAmpY={20}
<label className="text-sm text-blue-900 font-medium">Filter by category:</label> friction={0.9}
<select tension={0.01}
value={selectedCategory} maxCursorMove={120}
onChange={(e) => setSelectedCategory(e.target.value)} xGap={12}
className="rounded-md border border-blue-200 bg-white px-3 py-1.5 text-sm text-blue-900 shadow-sm" yGap={36}
> />
{categories.map(c => (
<option key={c} value={c}>{c === 'all' ? 'All' : c}</option>
))}
</select>
</div>
</header>
{/* States */} <div className="relative z-10 min-h-screen flex flex-col">
{loading && ( <main className="flex-1 max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8 w-full">
<div className="mx-auto max-w-2xl text-center"> <div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-6 sm:p-8">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-indigo-400 border-b-transparent" /> {/* Header (aligned with management pages) */}
<p className="mt-4 text-sm text-gray-600">Loading affiliate partners...</p> <header className="flex flex-col gap-4 mb-8">
</div> <div>
)} <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Affiliate Partners</h1>
<p className="text-lg text-blue-700 mt-2">
{error && !loading && ( Discover our trusted partners and earn commissions through affiliate links.
<div className="mx-auto max-w-2xl rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 text-center"> </p>
{error} </div>
</div> {/* NEW: Category filter */}
)} <div className="flex items-center gap-2">
<label className="text-sm text-blue-900 font-medium">Filter by category:</label>
{!loading && !error && posts.length === 0 && ( <select
<div className="mx-auto max-w-2xl text-center text-sm text-gray-600"> value={selectedCategory}
No affiliate partners available at the moment. onChange={(e) => setSelectedCategory(e.target.value)}
</div> className="rounded-md border border-blue-200 bg-white px-3 py-1.5 text-sm text-blue-900 shadow-sm"
)}
{/* Cards (aligned to white panels, border, shadow) */}
{!loading && !error && posts.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{posts.map((post) => {
// NEW: highlight when matches selected category (keep all visible)
const isHighlighted = selectedCategory !== 'all' && post.category.title === selectedCategory
return (
<article
key={post.id}
className={`rounded-2xl bg-white border shadow-lg overflow-hidden flex flex-col transition
${isHighlighted ? 'border-2 border-indigo-400 ring-2 ring-indigo-200' : 'border-gray-100'}`}
> >
<div className="relative"> {categories.map(c => (
<img alt="" src={post.imageUrl} className="aspect-video w-full object-cover" /> <option key={c} value={c}>{c === 'all' ? 'All' : c}</option>
</div> ))}
<div className="p-6 flex-1 flex flex-col"> </select>
<div className="flex items-start justify-between gap-3"> </div>
<h3 className="text-xl font-semibold text-blue-900">{post.title}</h3> </header>
{post.commissionRate && (
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium border border-indigo-200 bg-indigo-50 text-indigo-700"> {/* States */}
{post.commissionRate} {loading && (
</span> <div className="mx-auto max-w-2xl text-center">
)} <div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-indigo-400 border-b-transparent" />
</div> <p className="mt-4 text-sm text-gray-600">Loading affiliate partners...</p>
<div className="mt-2 flex flex-wrap gap-2 text-xs"> </div>
<a )}
href={post.category.href}
className={`inline-flex items-center rounded-full px-2 py-0.5 border text-blue-900 {error && !loading && (
${isHighlighted ? 'border-indigo-300 bg-indigo-50' : 'border-blue-200 bg-blue-50'}`} <div className="mx-auto max-w-2xl rounded-md border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 text-center">
> {error}
{post.category.title} </div>
</a> )}
</div>
<p className="mt-3 text-sm text-gray-700 line-clamp-4">{post.description}</p> {!loading && !error && posts.length === 0 && (
<div className="mt-5 flex items-center justify-between"> <div className="mx-auto max-w-2xl text-center text-sm text-gray-600">
<a No affiliate partners available at the moment.
href={post.href} </div>
target="_blank" )}
rel="noopener noreferrer"
className="rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-medium shadow transition" {/* Cards (aligned to white panels, border, shadow) */}
> {!loading && !error && posts.length > 0 && (
Visit Affiliate Link <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
</a> {posts.map((post) => {
<span className="text-[11px] text-gray-500"> // NEW: highlight when matches selected category (keep all visible)
External partner website. const isHighlighted = selectedCategory !== 'all' && post.category.title === selectedCategory
</span> return (
</div> <article
</div> key={post.id}
</article> className={`rounded-2xl bg-white border shadow-lg overflow-hidden flex flex-col transition
) ${isHighlighted ? 'border-2 border-indigo-400 ring-2 ring-indigo-200' : 'border-gray-100'}`}
})} >
<div className="relative">
<img alt="" src={post.imageUrl} className="aspect-video w-full object-cover" />
</div>
<div className="p-6 flex-1 flex flex-col">
<div className="flex items-start justify-between gap-3">
<h3 className="text-xl font-semibold text-blue-900">{post.title}</h3>
{post.commissionRate && (
<span className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium border border-indigo-200 bg-indigo-50 text-indigo-700">
{post.commissionRate}
</span>
)}
</div>
<div className="mt-2 flex flex-wrap gap-2 text-xs">
<a
href={post.category.href}
className={`inline-flex items-center rounded-full px-2 py-0.5 border text-blue-900
${isHighlighted ? 'border-indigo-300 bg-indigo-50' : 'border-blue-200 bg-blue-50'}`}
>
{post.category.title}
</a>
</div>
<p className="mt-3 text-sm text-gray-700 line-clamp-4">{post.description}</p>
<div className="mt-5 flex items-center justify-between">
<a
href={post.href}
target="_blank"
rel="noopener noreferrer"
className="rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-medium shadow transition"
>
Visit Affiliate Link
</a>
<span className="text-[11px] text-gray-500">
External partner website.
</span>
</div>
</div>
</article>
)
})}
</div>
)}
</div> </div>
)} </main>
</main> </div>
</div> </div>
</PageLayout> </PageLayout>
) )

View File

@ -1,6 +1,7 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation';
import Header from './nav/Header'; import Header from './nav/Header';
import Footer from './Footer'; import Footer from './Footer';
import PageTransitionEffect from './animation/pageTransitionEffect'; import PageTransitionEffect from './animation/pageTransitionEffect';
@ -15,18 +16,39 @@ interface PageLayoutProps {
children: React.ReactNode; children: React.ReactNode;
showHeader?: boolean; showHeader?: boolean;
showFooter?: boolean; showFooter?: boolean;
className?: string;
contentClassName?: string;
} }
export default function PageLayout({ export default function PageLayout({
children, children,
showHeader = true, showHeader = true,
showFooter = true showFooter = true,
className = 'bg-white text-gray-900',
contentClassName = 'flex-1 relative z-10 w-full',
}: PageLayoutProps) { }: PageLayoutProps) {
const isMobile = isMobileDevice(); const isMobile = isMobileDevice();
const [isLoggingOut, setIsLoggingOut] = useState(false); // NEW 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 ( 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 && ( {showHeader && (
<div className="relative z-50 w-full flex-shrink-0"> <div className="relative z-50 w-full flex-shrink-0">
@ -35,7 +57,7 @@ export default function PageLayout({
)} )}
{/* Main content */} {/* Main content */}
<div className="flex-1 relative z-10 w-full"> <div className={contentClassName}>
<PageTransitionEffect>{children}</PageTransitionEffect> <PageTransitionEffect>{children}</PageTransitionEffect>
</div> </div>

View File

@ -0,0 +1,153 @@
'use client'
import { useRef, useEffect, useState, useMemo, useId, FC, PointerEvent } from 'react';
interface CurvedLoopProps {
marqueeText?: string;
speed?: number;
className?: string;
curveAmount?: number;
direction?: 'left' | 'right';
interactive?: boolean;
}
const CurvedLoop: FC<CurvedLoopProps> = ({
marqueeText = '',
speed = 1,
className,
curveAmount = -50,
direction = 'left',
interactive = true
}) => {
const text = useMemo(() => {
const hasTrailing = /\s|\u00A0$/.test(marqueeText);
return (hasTrailing ? marqueeText.replace(/\s+$/, '') : marqueeText) + '\u00A0';
}, [marqueeText]);
const measureRef = useRef<SVGTextElement | null>(null);
const textPathRef = useRef<SVGTextPathElement | null>(null);
const pathRef = useRef<SVGPathElement | null>(null);
const [spacing, setSpacing] = useState(0);
const [offset, setOffset] = useState(0);
const uid = useId();
const pathId = `curve-${uid}`;
const pathD = `M-100,40 Q500,${40 + curveAmount} 1540,40`;
const dragRef = useRef(false);
const lastXRef = useRef(0);
const dirRef = useRef<'left' | 'right'>(direction);
const velRef = useRef(0);
const textLength = spacing;
const totalText = textLength
? Array(Math.ceil(1800 / textLength) + 2)
.fill(text)
.join('')
: text;
const ready = spacing > 0;
useEffect(() => {
if (measureRef.current) setSpacing(measureRef.current.getComputedTextLength());
}, [text, className]);
useEffect(() => {
if (!spacing) return;
if (textPathRef.current) {
const initial = -spacing;
textPathRef.current.setAttribute('startOffset', initial + 'px');
setOffset(initial);
}
}, [spacing]);
useEffect(() => {
if (!spacing || !ready) return;
let frame = 0;
const step = () => {
if (!dragRef.current && textPathRef.current) {
const delta = dirRef.current === 'right' ? speed : -speed;
const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');
let newOffset = currentOffset + delta;
const wrapPoint = spacing;
if (newOffset <= -wrapPoint) newOffset += wrapPoint;
if (newOffset > 0) newOffset -= wrapPoint;
textPathRef.current.setAttribute('startOffset', newOffset + 'px');
setOffset(newOffset);
}
frame = requestAnimationFrame(step);
};
frame = requestAnimationFrame(step);
return () => cancelAnimationFrame(frame);
}, [spacing, speed, ready]);
const onPointerDown = (e: PointerEvent) => {
if (!interactive) return;
dragRef.current = true;
lastXRef.current = e.clientX;
velRef.current = 0;
(e.target as HTMLElement).setPointerCapture(e.pointerId);
};
const onPointerMove = (e: PointerEvent) => {
if (!interactive || !dragRef.current || !textPathRef.current) return;
const dx = e.clientX - lastXRef.current;
lastXRef.current = e.clientX;
velRef.current = dx;
const currentOffset = parseFloat(textPathRef.current.getAttribute('startOffset') || '0');
let newOffset = currentOffset + dx;
const wrapPoint = spacing;
if (newOffset <= -wrapPoint) newOffset += wrapPoint;
if (newOffset > 0) newOffset -= wrapPoint;
textPathRef.current.setAttribute('startOffset', newOffset + 'px');
setOffset(newOffset);
};
const endDrag = () => {
if (!interactive) return;
dragRef.current = false;
dirRef.current = velRef.current > 0 ? 'right' : 'left';
};
const cursorStyle = interactive ? (dragRef.current ? 'grabbing' : 'grab') : 'auto';
return (
<div
className="w-full flex items-center justify-center"
style={{ visibility: ready ? 'visible' : 'hidden', cursor: cursorStyle }}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={endDrag}
onPointerLeave={endDrag}
>
<svg
className="select-none w-full overflow-visible block aspect-[100/12] text-[2.25rem] md:text-[2.75rem] lg:text-[3rem] font-bold uppercase leading-none"
viewBox="0 0 1440 120"
preserveAspectRatio="xMidYMid meet"
>
<text
ref={measureRef}
xmlSpace="preserve"
style={{ visibility: 'hidden', opacity: 0, pointerEvents: 'none' }}
>
{text}
</text>
<defs>
<path ref={pathRef} id={pathId} d={pathD} fill="none" stroke="transparent" />
</defs>
{ready && (
<text xmlSpace="preserve" className={`fill-[#0F172A] ${className ?? ''}`}>
<textPath
ref={textPathRef}
href={`#${pathId}`}
startOffset={offset + 'px'}
xmlSpace="preserve"
>
{totalText}
</textPath>
</text>
)}
</svg>
</div>
);
};
export default CurvedLoop;

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_MEMBERSHIP = process.env.NEXT_PUBLIC_DISPLAY_MEMBERSHIP !== 'false'
const DISPLAY_ABOUT_US = process.env.NEXT_PUBLIC_DISPLAY_ABOUT_US !== '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_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' 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 [mounted, setMounted] = useState(false)
const [animateIn, setAnimateIn] = useState(false) const [animateIn, setAnimateIn] = useState(false)
const [scrollY, setScrollY] = useState(0) const [scrollY, setScrollY] = useState(0)
const [isMobile, setIsMobile] = useState(false)
const user = useAuthStore(s => s.user) const user = useAuthStore(s => s.user)
const logout = useAuthStore(s => s.logout) const logout = useAuthStore(s => s.logout)
const accessToken = useAuthStore(s => s.accessToken) const accessToken = useAuthStore(s => s.accessToken)
const refreshAuthToken = useAuthStore(s => s.refreshAuthToken) const refreshAuthToken = useAuthStore(s => s.refreshAuthToken)
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const isHome = pathname === '/' const isParallaxPage =
pathname === '/' ||
pathname === '/login' ||
pathname === '/password-reset' ||
pathname === '/register'
const parallaxEnabled = isParallaxPage && !isMobile
const headerIsFixedOverlay = isParallaxPage && !isMobile
const headerPositionClass = isParallaxPage
? (isMobile ? 'sticky top-0 w-full' : 'fixed top-0 left-0 w-full')
: 'relative'
const [hasReferralPerm, setHasReferralPerm] = useState(false) const [hasReferralPerm, setHasReferralPerm] = useState(false)
const [adminMgmtOpen, setAdminMgmtOpen] = useState(false) const [adminMgmtOpen, setAdminMgmtOpen] = useState(false)
const managementRef = useRef<HTMLDivElement | null>(null) const managementRef = useRef<HTMLDivElement | null>(null)
const [canSeeDashboard, setCanSeeDashboard] = useState(false) const [canSeeDashboard, setCanSeeDashboard] = useState(false)
const headerElRef = useRef<HTMLElement | null>(null)
const handleLogout = async () => { const handleLogout = async () => {
try { try {
@ -119,12 +132,25 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
setAnimateIn(true) setAnimateIn(true)
}, []) }, [])
// Home-page scroll listener: reveal header after first scroll with slight parallax // Detect mobile devices (for disabling parallax)
useEffect(() => {
const mq = window.matchMedia('(max-width: 768px)')
const apply = () => setIsMobile(mq.matches)
apply()
mq.addEventListener?.('change', apply)
window.addEventListener('resize', apply, { passive: true })
return () => {
mq.removeEventListener?.('change', apply)
window.removeEventListener('resize', apply)
}
}, [])
// Home + login scroll listener: reveal header after first scroll with slight parallax
useEffect(() => { useEffect(() => {
if (!mounted) return if (!mounted) return
if (!isHome) { if (!parallaxEnabled) {
// non-home: header always visible, no scroll listeners // non-parallax (and mobile): header always visible, no scroll listeners
setScrollY(100) setScrollY(100)
return return
} }
@ -149,7 +175,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
window.removeEventListener('scroll', handleScroll) window.removeEventListener('scroll', handleScroll)
window.removeEventListener('wheel', handleWheel) window.removeEventListener('wheel', handleWheel)
} }
}, [mounted, isHome]) }, [mounted, parallaxEnabled])
// Fetch user permissions and set hasReferralPerm // Fetch user permissions and set hasReferralPerm
useEffect(() => { useEffect(() => {
@ -309,22 +335,82 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
) )
const isAdmin = mounted && rawIsAdmin const isAdmin = mounted && rawIsAdmin
// Only gate visibility by scroll on home; elsewhere just use animateIn // Only gate visibility by scroll on parallax-enabled pages
const headerVisible = isHome ? animateIn && scrollY > 24 : animateIn const headerVisible = parallaxEnabled ? animateIn && scrollY > 24 : animateIn
const parallaxOffset = isHome ? Math.max(-16, -scrollY * 0.15) : 0 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 ( return (
<header <header
className={`${ ref={headerElRef}
isHome ? 'fixed top-0 left-0 w-full' : 'relative' 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%)] ${
} 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%)] ${
isAdmin ? '' : 'border-b border-white/10' isAdmin ? '' : 'border-b border-white/10'
} ${ } ${
headerVisible ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-6 pointer-events-none' headerVisible ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-6 pointer-events-none'
} transition-all duration-500 ease-out`} } transition-all duration-500 ease-out`}
style={{ style={{
background: 'linear-gradient(135deg, #0F1D37 0%, #0A162A 50%, #081224 100%)', background: 'linear-gradient(135deg, #0F1D37 0%, #0A162A 50%, #081224 100%)',
...(isHome ? { transform: `translateY(${parallaxOffset}px)` } : {}), ...(parallaxEnabled ? { transform: `translateY(${parallaxOffset}px)` } : {}),
}} }}
> >
<nav <nav
@ -886,7 +972,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
<div className="py-6 space-y-4 px-1"> <div className="py-6 space-y-4 px-1">
{/* Information disclosure */} {/* Information disclosure */}
<Disclosure as="div"> <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 Information
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none group-data-open:rotate-180" /> <ChevronDownIcon aria-hidden="true" className="size-5 flex-none group-data-open:rotate-180" />
</DisclosureButton> </DisclosureButton>
@ -908,7 +994,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
<button <button
key={link.href} key={link.href}
onClick={() => { router.push(link.href); setMobileMenuOpen(false); }} 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} {link.name}
</button> </button>
@ -916,7 +1002,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
<div className="px-3"> <div className="px-3">
<button <button
onClick={() => { router.push('/login'); setMobileMenuOpen(false); }} 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 Log in
</button> </button>

View File

@ -12,6 +12,8 @@ import { createIntlTelInput, IntlTelInputInstance } from '../../utils/phoneUtils
export type TelephoneInputHandle = { export type TelephoneInputHandle = {
getNumber: () => string getNumber: () => string
isValid: () => boolean isValid: () => boolean
// NEW: allow callers to require a selected country code
getDialCode: () => string | null
} }
interface TelephoneInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> { interface TelephoneInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
@ -24,20 +26,33 @@ interface TelephoneInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>
* Always takes full available width. * Always takes full available width.
*/ */
const TelephoneInput = forwardRef<TelephoneInputHandle, TelephoneInputProps>( 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 inputRef = useRef<HTMLInputElement | null>(null)
const itiRef = useRef<IntlTelInputInstance | null>(null) const itiRef = useRef<IntlTelInputInstance | null>(null)
const readyRef = useRef(false)
const lastSyncDigitsRef = useRef<string>('')
useEffect(() => { useEffect(() => {
let disposed = false let disposed = false
let instance: IntlTelInputInstance | null = null let instance: IntlTelInputInstance | null = null
readyRef.current = false
lastSyncDigitsRef.current = ''
const setup = async () => { const setup = async () => {
try { 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, id: rest.id,
name: rest.name, 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) { if (!inputRef.current) {
@ -48,20 +63,90 @@ const TelephoneInput = forwardRef<TelephoneInputHandle, TelephoneInputProps>(
return return
} }
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,
},
})
instance = await createIntlTelInput(inputRef.current, { instance = await createIntlTelInput(inputRef.current, {
initialCountry, initialCountry: resolvedCountry,
nationalMode: true, nationalMode: true,
strictMode: true, strictMode: true,
autoPlaceholder: 'aggressive', autoPlaceholder: 'aggressive',
validationNumberTypes: ['MOBILE'], 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) { if (disposed) {
console.log('[TelephoneInput] setup() finished but component is disposed, destroying instance', { console.log(
id: rest.id, '[TelephoneInput] setup() finished but component is disposed, destroying instance',
name: rest.name, { id: rest.id, name: rest.name }
}) )
instance.destroy() if (instance) {
instance.destroy()
}
return return
} }
@ -69,9 +154,23 @@ const TelephoneInput = forwardRef<TelephoneInputHandle, TelephoneInputProps>(
console.log('[TelephoneInput] intl-tel-input instance attached to input', { console.log('[TelephoneInput] intl-tel-input instance attached to input', {
id: rest.id, id: rest.id,
name: rest.name, 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) { } 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 () => { return () => {
disposed = true 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) { if (instance) {
console.log('[TelephoneInput] Destroying intl-tel-input instance for', { console.log('[TelephoneInput] Destroying intl-tel-input instance for', {
id: rest.id, id: rest.id,
name: rest.name, name: rest.name,
}) })
instance.destroy() 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 if (itiRef.current === instance) itiRef.current = null
} }
} }
@ -93,63 +215,99 @@ const TelephoneInput = forwardRef<TelephoneInputHandle, TelephoneInputProps>(
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
getNumber: () => { getNumber: () => {
const raw = inputRef.current?.value || '' const raw = inputRef.current?.value || ''
if (itiRef.current) { if (!itiRef.current || !readyRef.current) return raw
try {
const intl = itiRef.current.getNumber() const intl = itiRef.current.getNumber()
console.log('[TelephoneInput] getNumber()', { return intl || raw
id: rest.id, } catch {
name: rest.name, return raw
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 raw
}, },
isValid: () => { isValid: () => {
if (!itiRef.current) { const raw = inputRef.current?.value || ''
const raw = inputRef.current?.value || '' if (!itiRef.current || !readyRef.current) {
console.warn('[TelephoneInput] isValid() called before intl-tel-input ready', { return /^\+?\d{7,}$/.test(raw.replace(/\s+/g, ''))
id: rest.id,
name: rest.name,
raw,
})
return false
} }
const instance = itiRef.current try {
const intl = instance.getNumber() return itiRef.current.isValidNumber()
const valid = instance.isValidNumber() } catch {
const errorCode = typeof instance.getValidationError === 'function' return /^\+?\d{7,}$/.test(raw.replace(/\s+/g, ''))
? instance.getValidationError() }
: undefined },
const country = typeof instance.getSelectedCountryData === 'function' getDialCode: () => {
? instance.getSelectedCountryData() const iti = itiRef.current as any
: undefined const data = iti?.getSelectedCountryData?.()
const dial = data?.dialCode
console.log('[TelephoneInput] isValid() check', { return typeof dial === 'string' && dial.trim() ? dial.trim() : null
id: rest.id,
name: rest.name,
intl,
valid,
errorCode,
country,
})
return valid
}, },
})) }))
return ( return (
<div className="w-full"> <div className="w-full pp-iti-dark">
<input <input
ref={inputRef} ref={inputRef}
type="tel" 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} {...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> </div>
) )
} }

View File

@ -133,6 +133,8 @@ export interface WavesProps {
maxCursorMove?: number; maxCursorMove?: number;
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
animate?: boolean;
interactive?: boolean;
} }
const Waves: React.FC<WavesProps> = ({ const Waves: React.FC<WavesProps> = ({
@ -149,6 +151,8 @@ const Waves: React.FC<WavesProps> = ({
maxCursorMove = 100, maxCursorMove = 100,
style = {}, style = {},
className = '', className = '',
animate = true,
interactive = true,
}) => { }) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(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) { function movePoints(time: number) {
const lines = linesRef.current; const lines = linesRef.current;
const mouse = mouseRef.current; const mouse = mouseRef.current;
@ -265,6 +313,8 @@ const Waves: React.FC<WavesProps> = ({
p.wave.x = Math.cos(move) * waveAmpX; p.wave.x = Math.cos(move) * waveAmpX;
p.wave.y = Math.sin(move) * waveAmpY; p.wave.y = Math.sin(move) * waveAmpY;
if (!interactive) return;
const dx = p.x - mouse.sx; const dx = p.x - mouse.sx;
const dy = p.y - mouse.sy; const dy = p.y - mouse.sy;
const dist = Math.hypot(dx, dy); const dist = Math.hypot(dx, dy);
@ -288,75 +338,46 @@ 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) { function tick(t: number) {
const container = containerRef.current; const container = containerRef.current;
if (!container) return; if (!container) return;
const mouse = mouseRef.current; if (interactive) {
mouse.sx += (mouse.x - mouse.sx) * 0.1; const mouse = mouseRef.current;
mouse.sy += (mouse.y - mouse.sy) * 0.1; mouse.sx += (mouse.x - mouse.sx) * 0.1;
const dx = mouse.x - mouse.lx; mouse.sy += (mouse.y - mouse.sy) * 0.1;
const dy = mouse.y - mouse.ly; const dx = mouse.x - mouse.lx;
const d = Math.hypot(dx, dy); const dy = mouse.y - mouse.ly;
mouse.v = d; const d = Math.hypot(dx, dy);
mouse.vs += (d - mouse.vs) * 0.1; mouse.v = d;
mouse.vs = Math.min(100, mouse.vs); mouse.vs += (d - mouse.vs) * 0.1;
mouse.lx = mouse.x; mouse.vs = Math.min(100, mouse.vs);
mouse.ly = mouse.y; mouse.lx = mouse.x;
mouse.a = Math.atan2(dy, dx); mouse.ly = mouse.y;
container.style.setProperty('--x', `${mouse.sx}px`); mouse.a = Math.atan2(dy, dx);
container.style.setProperty('--y', `${mouse.sy}px`); container.style.setProperty('--x', `${mouse.sx}px`);
container.style.setProperty('--y', `${mouse.sy}px`);
}
movePoints(t); movePoints(t);
drawLines(); drawLines(true);
frameIdRef.current = requestAnimationFrame(tick); 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() { function onResize() {
setSize(); setSize();
setLines(); setLines();
} if (!animate) drawStatic();
function onMouseMove(e: MouseEvent) {
updateMouse(e.clientX, e.clientY);
}
function onTouchMove(e: TouchEvent) {
const touch = e.touches[0];
updateMouse(touch.clientX, touch.clientY);
} }
function updateMouse(x: number, y: number) { function updateMouse(x: number, y: number) {
if (!interactive) return;
const mouse = mouseRef.current; const mouse = mouseRef.current;
const b = boundingRef.current; const b = boundingRef.current;
mouse.x = x - b.left; 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(); setSize();
setLines(); setLines();
frameIdRef.current = requestAnimationFrame(tick);
window.addEventListener('resize', onResize); window.addEventListener('resize', onResize);
window.addEventListener('mousemove', onMouseMove); ro?.observe(container);
window.addEventListener('touchmove', onTouchMove, { passive: false });
if (interactive) {
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('touchmove', onTouchMove, { passive: true });
}
if (animate) {
frameIdRef.current = requestAnimationFrame(tick);
} else {
drawStatic();
}
return () => { return () => {
window.removeEventListener('resize', onResize); window.removeEventListener('resize', onResize);
window.removeEventListener('mousemove', onMouseMove); ro?.disconnect();
window.removeEventListener('touchmove', onTouchMove); if (interactive) {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('touchmove', onTouchMove);
}
if (frameIdRef.current !== null) cancelAnimationFrame(frameIdRef.current); if (frameIdRef.current !== null) cancelAnimationFrame(frameIdRef.current);
frameIdRef.current = null;
}; };
}, []); }, [animate, interactive]);
return ( return (
<div <div
@ -391,8 +435,14 @@ const Waves: React.FC<WavesProps> = ({
style={{ style={{
backgroundColor, backgroundColor,
...style, ...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" /> <canvas ref={canvasRef} className="block w-full h-full" />
</div> </div>

View File

@ -1,13 +1,13 @@
'use client' 'use client'
import { useEffect } from 'react' import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore' import useAuthStore from '../store/authStore'
import Header from '../components/nav/Header' import PageLayout from '../components/PageLayout'
import Footer from '../components/Footer' import Waves from '../components/waves'
import { import {
ShoppingBagIcon, ShoppingBagIcon,
UsersIcon, UsersIcon,
UserCircleIcon, UserCircleIcon,
StarIcon, StarIcon,
HeartIcon, HeartIcon,
@ -18,7 +18,21 @@ export default function DashboardPage() {
const router = useRouter() const router = useRouter()
const user = useAuthStore(state => state.user) const user = useAuthStore(state => state.user)
const isAuthReady = useAuthStore(state => state.isAuthReady) 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) // Redirect if not logged in (only after auth is ready)
useEffect(() => { useEffect(() => {
if (isAuthReady && !user) { if (isAuthReady && !user) {
@ -55,7 +69,9 @@ export default function DashboardPage() {
description: 'Explore sustainable products', description: 'Explore sustainable products',
icon: ShoppingBagIcon, icon: ShoppingBagIcon,
href: '/shop', href: '/shop',
color: 'bg-blue-500' color: 'bg-blue-500',
disabled: !isShopEnabled,
disabledText: 'This is currently disabled.'
}, },
{ {
title: 'Browse Affiliate Links', title: 'Browse Affiliate Links',
@ -112,147 +128,195 @@ export default function DashboardPage() {
] ]
return ( return (
<div className="min-h-screen flex flex-col bg-gray-50"> <div
<Header /> className="relative w-full min-h-[100dvh] flex flex-col overflow-x-hidden"
style={{ backgroundImage: 'none', background: 'none' }}
<main className="flex-1 py-8 px-4 sm:px-6 lg:px-8"> >
<div className="max-w-7xl mx-auto"> <Waves
{/* Welcome Section */} className="pointer-events-none"
<div className="mb-8"> lineColor="#0f172a"
<h1 className="text-3xl font-bold text-gray-900"> backgroundColor="rgba(245, 245, 240, 1)"
Welcome back, {getUserName()}! 👋 waveSpeedX={0.02}
</h1> waveSpeedY={0.01}
<p className="text-gray-600 mt-2"> waveAmpX={40}
Here's what's happening with your Profit Planet account waveAmpY={20}
</p> friction={0.9}
</div> tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
animate={!isMobile}
interactive={!isMobile}
/>
{/* News Section (replaces Account setup + Stats Grid) */} <div className="relative z-10 flex-1 min-h-0">
<div className="mb-10"> <PageLayout className="bg-transparent text-gray-900">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Latest News & Articles</h2> <main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div className="max-w-7xl mx-auto">
{news.map(item => ( <div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-8">
<article key={item.id} className="group relative overflow-hidden rounded-xl bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow"> {/* Welcome Section */}
{/* Image/placeholder */} <div className="mb-8">
<div className="aspect-[16/9] w-full bg-gradient-to-br from-gray-100 to-gray-200" /> <h1 className="text-3xl font-bold text-gray-900">
<div className="p-5"> Welcome back, {getUserName()}! 👋
<div className="mb-2 flex items-center gap-2"> </h1>
<span className="inline-flex items-center rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700 ring-1 ring-amber-200"> <p className="text-gray-600 mt-2">
{item.category} Here's what's happening with your Profit Planet account
</span> </p>
<span className="text-xs text-gray-500">{new Date(item.date).toLocaleDateString()}</span> </div>
</div>
<h3 className="text-base font-semibold text-gray-900 group-hover:text-[#8D6B1D] transition-colors"> {/* News Section (replaces Account setup + Stats Grid) */}
<button <div className="mb-10">
onClick={() => (window.location.href = item.href)} <h2 className="text-xl font-semibold text-gray-900 mb-4">Latest News & Articles</h2>
className="text-left w-full" <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
> {news.map(item => (
{item.title} <article key={item.id} className="group relative overflow-hidden rounded-xl bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
</button> {/* Image/placeholder */}
</h3> <div className="aspect-[16/9] w-full bg-gradient-to-br from-gray-100 to-gray-200" />
<p className="mt-2 text-sm text-gray-600 line-clamp-3">{item.excerpt}</p> <div className="p-5">
<div className="mt-4"> <div className="mb-2 flex items-center gap-2">
<button <span className="inline-flex items-center rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700 ring-1 ring-amber-200">
onClick={() => (window.location.href = item.href)} {item.category}
className="text-sm font-medium text-[#8D6B1D] hover:text-[#7A5E1A]" </span>
> <span className="text-xs text-gray-500">{new Date(item.date).toLocaleDateString()}</span>
Read more </div>
</button> <h3 className="text-base font-semibold text-gray-900 group-hover:text-[#8D6B1D] transition-colors">
</div> <button
onClick={() => (window.location.href = item.href)}
className="text-left w-full"
>
{item.title}
</button>
</h3>
<p className="mt-2 text-sm text-gray-600 line-clamp-3">{item.excerpt}</p>
<div className="mt-4">
<button
onClick={() => (window.location.href = item.href)}
className="text-sm font-medium text-[#8D6B1D] hover:text-[#7A5E1A]"
>
Read more
</button>
</div>
</div>
</article>
))}
</div> </div>
</article> </div>
))}
</div>
</div>
{/* Quick Actions */} {/* Quick Actions */}
<div className="mb-8"> <div className="mb-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Quick Actions</h2> <h2 className="text-xl font-semibold text-gray-900 mb-4">Quick Actions</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{quickActions.map((action, index) => ( {quickActions.map((action, index) => (
<button <button
key={index} key={index}
onClick={() => router.push(action.href)} onClick={() => {
className="bg-white rounded-lg p-6 shadow-sm border border-gray-200 hover:shadow-md transition-shadow text-left group" if (!action.disabled) {
> router.push(action.href)
<div className="flex items-start"> }
<div className={`${action.color} rounded-lg p-3 group-hover:scale-105 transition-transform`}> }}
<action.icon className="h-6 w-6 text-white" /> disabled={Boolean(action.disabled)}
</div> className={`bg-white rounded-lg p-6 border border-gray-200 text-left group transition-all duration-200 ${
<div className="ml-4 flex-1"> action.disabled
<h3 className="text-lg font-medium text-gray-900 group-hover:text-[#8D6B1D] transition-colors"> ? 'opacity-60 cursor-not-allowed'
{action.title} : 'shadow-sm hover:shadow-lg hover:-translate-y-1 hover:-translate-y-1 hover:-translate-y-1 transform hover:-translate-y-1'
</h3> }`}
<p className="text-sm text-gray-600 mt-1"> >
{action.description} <div className="flex items-start">
<div
className={`${action.color} rounded-lg p-3 ${
action.disabled
? 'grayscale'
: 'group-hover:scale-105 transition-transform'
}`}
>
<action.icon className="h-6 w-6 text-white" />
</div>
<div className="ml-4 flex-1">
<h3
className={`text-lg font-medium transition-colors ${
action.disabled
? 'text-gray-500'
: 'text-gray-900 group-hover:text-[#8D6B1D]'
}`}
>
{action.title}
</h3>
<p className="text-sm text-gray-600 mt-1">
{action.description}
</p>
{action.disabled && action.disabledText && (
<p className="mt-3 text-xs font-medium text-amber-700">
{action.disabledText}
</p>
)}
</div>
</div>
</button>
))}
</div>
</div>
{/* Gold Member Status */}
<div className="bg-gradient-to-r from-[#8D6B1D] to-[#B8860B] rounded-lg p-6 text-white mb-8">
<div className="flex items-center">
<StarIcon className="h-12 w-12 text-yellow-300" />
<div className="ml-4">
<h2 className="text-2xl font-bold">Gold Member Status</h2>
<p className="text-yellow-100 mt-1">
Enjoy exclusive benefits and discounts
</p> </p>
</div> </div>
<div className="ml-auto">
<button className="bg-white/20 hover:bg-white/30 px-4 py-2 rounded-lg text-white font-medium transition-colors">
View Benefits
</button>
</div>
</div> </div>
</button> </div>
))}
</div>
</div>
{/* Gold Member Status */} {/* Recent Activity */}
<div className="bg-gradient-to-r from-[#8D6B1D] to-[#B8860B] rounded-lg p-6 text-white mb-8"> <div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="flex items-center"> <h2 className="text-xl font-semibold text-gray-900 mb-4">Recent Activity</h2>
<StarIcon className="h-12 w-12 text-yellow-300" /> <div className="space-y-4">
<div className="ml-4"> <div className="flex items-center py-3 border-b border-gray-100 last:border-b-0">
<h2 className="text-2xl font-bold">Gold Member Status</h2> <div className="bg-green-100 rounded-full p-2">
<p className="text-yellow-100 mt-1"> <ShoppingBagIcon className="h-5 w-5 text-green-600" />
Enjoy exclusive benefits and discounts </div>
</p> <div className="ml-4 flex-1">
</div> <p className="text-sm font-medium text-gray-900">Order completed</p>
<div className="ml-auto"> <p className="text-sm text-gray-600">Eco-friendly water bottle</p>
<button className="bg-white/20 hover:bg-white/30 px-4 py-2 rounded-lg text-white font-medium transition-colors"> </div>
View Benefits <span className="text-sm text-gray-500">2 days ago</span>
</button> </div>
<div className="flex items-center py-3 border-b border-gray-100 last:border-b-0">
<div className="bg-blue-100 rounded-full p-2">
<HeartIcon className="h-5 w-5 text-blue-600" />
</div>
<div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-900">Added to favorites</p>
<p className="text-sm text-gray-600">Sustainable backpack</p>
</div>
<span className="text-sm text-gray-500">1 week ago</span>
</div>
<div className="flex items-center py-3">
<div className="bg-purple-100 rounded-full p-2">
<UsersIcon className="h-5 w-5 text-purple-600" />
</div>
<div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-900">Joined community</p>
<p className="text-sm text-gray-600">Eco Warriors Group</p>
</div>
<span className="text-sm text-gray-500">2 weeks ago</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </main>
</PageLayout>
{/* Recent Activity */} </div>
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Recent Activity</h2>
<div className="space-y-4">
<div className="flex items-center py-3 border-b border-gray-100 last:border-b-0">
<div className="bg-green-100 rounded-full p-2">
<ShoppingBagIcon className="h-5 w-5 text-green-600" />
</div>
<div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-900">Order completed</p>
<p className="text-sm text-gray-600">Eco-friendly water bottle</p>
</div>
<span className="text-sm text-gray-500">2 days ago</span>
</div>
<div className="flex items-center py-3 border-b border-gray-100 last:border-b-0">
<div className="bg-blue-100 rounded-full p-2">
<HeartIcon className="h-5 w-5 text-blue-600" />
</div>
<div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-900">Added to favorites</p>
<p className="text-sm text-gray-600">Sustainable backpack</p>
</div>
<span className="text-sm text-gray-500">1 week ago</span>
</div>
<div className="flex items-center py-3">
<div className="bg-purple-100 rounded-full p-2">
<UsersIcon className="h-5 w-5 text-purple-600" />
</div>
<div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-900">Joined community</p>
<p className="text-sm text-gray-600">Eco Warriors Group</p>
</div>
<span className="text-sm text-gray-500">2 weeks ago</span>
</div>
</div>
</div>
</div>
</main>
<Footer />
</div> </div>
) )
} }

View File

@ -6,32 +6,23 @@ import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
import { useLogin } from '../hooks/useLogin' import { useLogin } from '../hooks/useLogin'
import { useToast } from '../../components/toast/toastComponent' import { useToast } from '../../components/toast/toastComponent'
const GLASS_BG = 'rgba(255,255,255,0.55)'
export default function LoginForm() { export default function LoginForm() {
const [showPassword, setShowPassword] = useState(false) const [showPassword, setShowPassword] = useState(false)
const [showBall, setShowBall] = useState(true)
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
email: '', email: '',
password: '', password: '',
rememberMe: false rememberMe: false
}) })
// FIX: use a static initial width so SSR and first client render match
const [viewportWidth, setViewportWidth] = useState<number>(1200) const [viewportWidth, setViewportWidth] = useState<number>(1200)
const router = useRouter() const router = useRouter()
const { login, error, setError, loading } = useLogin() const { login, error, setError, loading } = useLogin()
const { showToast } = useToast() const { showToast } = useToast()
// Responsive ball visibility
useEffect(() => {
const handleResizeBall = () => setShowBall(window.innerWidth >= 768)
handleResizeBall()
window.addEventListener('resize', handleResizeBall)
return () => window.removeEventListener('resize', handleResizeBall)
}, [])
// Track viewport width for dynamic scaling
useEffect(() => { useEffect(() => {
const handleResize = () => setViewportWidth(window.innerWidth) const handleResize = () => setViewportWidth(window.innerWidth)
handleResize() // initialize on mount (runs only on client) handleResize()
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize)
}, []) }, [])
@ -47,22 +38,22 @@ export default function LoginForm() {
const validateForm = (): boolean => { const validateForm = (): boolean => {
if (!formData.email.trim()) { if (!formData.email.trim()) {
setError('E-Mail-Adresse ist erforderlich') setError('Email address is required')
return false return false
} }
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { 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 return false
} }
if (!formData.password.trim()) { if (!formData.password.trim()) {
setError('Passwort ist erforderlich') setError('Password is required')
return false return false
} }
if (formData.password.length < 6) { if (formData.password.length < 6) {
setError('Passwort muss mindestens 6 Zeichen lang sein') setError('Password must be at least 6 characters long')
return false return false
} }
@ -106,7 +97,7 @@ export default function LoginForm() {
// CHANGED: Wider base widths; no transform scaling // CHANGED: Wider base widths; no transform scaling
const formWidth = isMobile const formWidth = isMobile
? '94vw' ? '100%'
: isTablet : isTablet
? '80vw' ? '80vw'
: isSmallLaptop : isSmallLaptop
@ -114,7 +105,7 @@ export default function LoginForm() {
: '52vw' : '52vw'
const formMaxWidth = isMobile const formMaxWidth = isMobile
? '480px' ? '420px'
: isTablet : isTablet
? '760px' ? '760px'
: isSmallLaptop : isSmallLaptop
@ -125,145 +116,62 @@ export default function LoginForm() {
<div <div
className="w-full relative" className="w-full relative"
style={{ style={{
// CHANGED: full-height flex box for perfect vertical centering // removed full-height so curved loop is visible right under the form
minHeight: '100vh',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
// REMOVE marble image so Waves shows through // REMOVE marble image so Waves shows through
background: 'transparent', background: 'transparent',
// Subtle padding to breathe on mobile // move the card slightly down on mobile, reduce bottom padding
padding: isMobile ? '0.75rem' : '1.5rem', padding: isMobile ? '0.5rem 0.75rem 0' : '0.2rem 1.5rem 1.5rem',
}} }}
> >
<div <div
className="bg-white rounded-2xl shadow-2xl flex flex-col items-center relative border-t-4 border-[#8D6B1D]" className="rounded-3xl shadow-2xl flex flex-col items-center relative border border-white/35"
style={{ style={{
width: formWidth, width: formWidth,
maxWidth: formMaxWidth, maxWidth: formMaxWidth,
minWidth: isMobile ? '0' : '420px', minWidth: isMobile ? '0' : '420px',
// CHANGED: tighter padding; removed transform scaling // slightly tighter on mobile
padding: isMobile ? '1rem' : '2rem', padding: isMobile ? '0.75rem' : '2rem',
// more translucent, glassy background
backgroundColor: GLASS_BG,
backdropFilter: 'blur(18px)',
WebkitBackdropFilter: 'blur(18px)',
// 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)',
}} }}
> >
{/* Animated Ball - Desktop Only */} {/* Content (title + earth removed) */}
{showBall && !isMobile && ( <div
<div className="absolute -top-16 left-1/2 -translate-x-1/2 w-28 z-20"> style={{
<div className="w-28 h-28 rounded-full bg-gradient-to-br from-[#8D6B1D] via-[#A67C20] to-[#C49225] shadow-xl border-4 border-white relative"> // CHANGED: smaller margins; the card is centered now
{/* Inner small circle with cartoony Earth */} marginTop: isMobile ? '0.15rem' : isTablet ? '0.5rem' : '0.75rem',
<div className="absolute inset-0 flex items-center justify-center"> marginBottom: isMobile ? '0.75rem' : isTablet ? '1.25rem' : '1.5rem',
<div className="w-16 h-16 rounded-full bg-white/15 backdrop-blur-sm border border-white/25 flex items-center justify-center shadow-inner relative overflow-hidden"> width: '100%',
<svg }}
viewBox="0 0 64 64" >
className="w-14 h-14" {/* Title + Subtitle (restored) */}
role="img" <div className="mb-6 text-center">
aria-label="Cartoon Earth" <h1 className="text-2xl md:text-3xl font-extrabold tracking-tight text-[#0F172A] drop-shadow-sm">
> PROFIT PLANET
<defs> </h1>
<radialGradient id="earth-ocean" cx="50%" cy="40%" r="65%"> <p className="mt-1 text-sm md:text-base text-slate-700/90">
<stop offset="0%" stopColor="#3fa9f5" /> Welcome back! Log in to continue.
<stop offset="100%" stopColor="#1d5fae" /> </p>
</radialGradient>
<linearGradient id="earth-glow" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="rgba(255,255,255,0.55)" />
<stop offset="60%" stopColor="rgba(255,255,255,0)" />
</linearGradient>
</defs>
<circle cx="32" cy="32" r="30" fill="url(#earth-ocean)" />
{/* Land masses (stylized) */}
<path
fill="#4caf50"
d="M18 30c4-6 10-9 16-9 3 0 5 1 7 3 2 2 1 4-1 5-4 2-8 2-11 5-2 2-3 4-6 4-5 0-8-5-5-8Z"
/>
<path
fill="#66bb6a"
d="M40 18c3 1 6 3 7 6 1 3 0 5-2 6-2 1-3 0-5-2-3-3-6-5-6-7 0-3 3-4 6-3Z"
opacity=".9"
/>
<path
fill="#43a047"
d="M26 44c2-2 5-3 8-2 2 1 3 3 1 5-2 3-6 5-9 4-3-1-3-5 0-7Z"
opacity=".85"
/>
{/* Atmospheric rim */}
<circle
cx="32"
cy="32"
r="30"
fill="none"
stroke="rgba(255,255,255,0.35)"
strokeWidth="1.5"
/>
{/* Light sheen */}
<ellipse
cx="26"
cy="22"
rx="11"
ry="7"
fill="url(#earth-glow)"
opacity=".6"
/>
</svg>
{/* Subtle gloss overlay */}
<span className="pointer-events-none absolute inset-0 before:absolute before:inset-0 before:bg-[radial-gradient(circle_at_35%_30%,rgba(255,255,255,0.45),transparent_70%)]" />
</div>
</div>
{/* Orbiting balls (unchanged) */}
<span className="absolute left-1/2 top-1/2 w-0 h-0">
<span className="block absolute animate-orbit-1" style={{ width: 0, height: 0 }}>
<span
className="block w-3 h-3 bg-[#8D6B1D] rounded-full shadow-lg"
style={{ transform: 'translateX(44px)' }}
/>
</span>
<span className="block absolute animate-orbit-2" style={{ width: 0, height: 0 }}>
<span
className="block w-2.5 h-2.5 bg-[#A67C20] rounded-full shadow-md"
style={{ transform: 'translateX(-36px)' }}
/>
</span>
</span>
</div>
</div> </div>
)}
{/* Content */}
<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',
width: '100%',
}}>
<h1
className="mb-2 text-center font-extrabold text-[#0F172A] tracking-tight drop-shadow-lg"
style={{
// CHANGED: slightly smaller headline on mobile to reduce vertical space
fontSize: isMobile ? '1.75rem' : isTablet ? '2rem' : '2.25rem',
marginTop: isMobile ? '0.25rem' : undefined,
}}
>
Profit Planet
</h1>
<p
className="mb-6 text-center text-[#8D6B1D] font-medium"
style={{
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1.05rem',
// CHANGED: reduce bottom margin
marginBottom: isMobile ? '0.75rem' : isTablet ? '1rem' : '1rem',
}}
>
Welcome back! Login to continue.
</p>
<form <form
className="space-y-6 w-full" className={`${isMobile ? 'space-y-4' : 'space-y-6'} w-full`}
style={{ style={{
gap: isMobile ? '0.75rem' : isTablet ? '0.9rem' : '1rem', gap: isMobile ? '0.6rem' : isTablet ? '0.9rem' : '1rem',
}} }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
{/* Email Field */} {/* Email Field */}
<div> <div className="field-animated">
<label <label
htmlFor="email" htmlFor="email"
className="block text-base font-semibold text-[#0F172A] mb-1" className="block text-base font-semibold text-[#0F172A] mb-1"
@ -272,7 +180,7 @@ export default function LoginForm() {
marginBottom: isMobile ? '0.25rem' : undefined, marginBottom: isMobile ? '0.25rem' : undefined,
}} }}
> >
E-Mail-Adresse Email address
</label> </label>
<input <input
id="email" id="email"
@ -281,18 +189,18 @@ export default function LoginForm() {
autoComplete="email" autoComplete="email"
value={formData.email} value={formData.email}
onChange={handleInputChange} onChange={handleInputChange}
className="appearance-none block w-full px-4 py-3 border border-gray-300 rounded-lg placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-[#8D6B1D] text-base bg-white text-[#0F172A] transition" className="input-animated appearance-none block w-full px-4 py-3 border border-white/40 rounded-xl placeholder-slate-600/80 focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] text-base bg-white/60 text-[#0F172A] shadow-sm transition"
style={{ style={{
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem', fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
padding: isMobile ? '0.5rem 0.75rem' : isTablet ? '0.6rem 0.875rem' : '0.7rem 1rem', padding: isMobile ? '0.5rem 0.75rem' : isTablet ? '0.6rem 0.875rem' : '0.7rem 1rem',
}} }}
placeholder="deine@email.com" placeholder="you@example.com"
required required
/> />
</div> </div>
{/* Password Field */} {/* Password Field */}
<div> <div className="field-animated">
<label <label
htmlFor="password" htmlFor="password"
className="block text-base font-semibold text-[#0F172A] mb-1" className="block text-base font-semibold text-[#0F172A] mb-1"
@ -301,22 +209,26 @@ export default function LoginForm() {
marginBottom: isMobile ? '0.25rem' : undefined, marginBottom: isMobile ? '0.25rem' : undefined,
}} }}
> >
Passwort Password
</label> </label>
<div className="relative"> <div className="relative">
<input <input
id="password" id="password"
name="password" name="password"
type={showPassword ? "text" : "password"} type={showPassword ? 'text' : 'password'}
autoComplete="current-password" autoComplete="current-password"
value={formData.password} value={formData.password}
onChange={handleInputChange} onChange={handleInputChange}
className="appearance-none block w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg placeholder-slate-500 focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-[#8D6B1D] text-base bg-white text-[#0F172A] transition" className="input-animated appearance-none block w-full px-4 py-3 pr-12 border border-white/40 rounded-xl placeholder-slate-600/80 focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]/80 focus:border-[#8D6B1D] text-base bg-white/60 text-[#0F172A] shadow-sm transition"
style={{ style={{
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem', fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
padding: isMobile ? '0.5rem 2.5rem 0.5rem 0.75rem' : isTablet ? '0.6rem 2.75rem 0.6rem 0.875rem' : '0.7rem 3rem 0.7rem 1rem', padding: isMobile
? '0.5rem 2.5rem 0.5rem 0.75rem'
: isTablet
? '0.6rem 2.75rem 0.6rem 0.875rem'
: '0.7rem 3rem 0.7rem 1rem',
}} }}
placeholder="Dein Passwort" placeholder="Your password"
required required
/> />
<button <button
@ -331,35 +243,6 @@ export default function LoginForm() {
)} )}
</button> </button>
</div> </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> </div>
{/* Error Message */} {/* Error Message */}
@ -374,10 +257,10 @@ export default function LoginForm() {
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className={`w-full py-3 px-6 rounded-lg shadow-md text-base font-bold text-white transition-all duration-200 transform hover:-translate-y-0.5 ${ className={`w-full py-3 px-6 rounded-xl text-base font-semibold transition-all duration-200 transform hover:-translate-y-0.5 border ${
loading loading
? 'bg-gray-400 cursor-not-allowed' ? 'border-white/30 bg-white/20 text-slate-300 cursor-not-allowed'
: 'bg-gradient-to-r from-[#8D6B1D] via-[#A67C20] to-[#C49225] hover:from-[#7A5E1A] hover:to-[#B8851F] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2' : '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'
}`} }`}
style={{ style={{
fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem', fontSize: isMobile ? '0.95rem' : isTablet ? '1rem' : '1rem',
@ -385,12 +268,12 @@ export default function LoginForm() {
}} }}
> >
{loading ? ( {loading ? (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center gap-2">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
Anmeldung läuft... Signing in...
</div> </div>
) : ( ) : (
'Anmelden' 'Sign in'
)} )}
</button> </button>
</div> </div>
@ -402,74 +285,50 @@ export default function LoginForm() {
className="text-[#8D6B1D] hover:text-[#7A5E1A] hover:underline text-sm font-medium transition-colors" className="text-[#8D6B1D] hover:text-[#7A5E1A] hover:underline text-sm font-medium transition-colors"
onClick={() => router.push("/password-reset")} onClick={() => router.push("/password-reset")}
> >
Passwort vergessen? Forgot password?
</button> </button>
</div> </div>
</form> </form>
{/* Registration Section */}
<div
className="mt-8 w-full"
style={{
marginTop: isMobile ? '0.75rem' : isTablet ? '1rem' : '1rem',
}}
>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-gray-200"></div>
</div>
<div
className="relative flex justify-center text-base"
style={{
fontSize: isMobile ? '0.875rem' : isTablet ? '0.9rem' : undefined,
}}
>
{/* <a href="/register" className="px-3 bg-white text-[#8D6B1D]">Noch kein Account?</a> */}
</div>
</div>
<div
className="mt-7 text-center"
style={{
marginTop: isMobile ? '0.75rem' : isTablet ? '1rem' : undefined,
}}
>
<p
className="text-base text-slate-700"
style={{
fontSize: isMobile ? '0.8rem' : isTablet ? '0.85rem' : undefined,
}}
>
Profit Planet is available by invitation only.
</p>
<p
className="text-base text-[#8D6B1D] mt-2"
style={{
fontSize: isMobile ? '0.8rem' : isTablet ? '0.85rem' : undefined,
}}
>
Contact us for an invitation!
</p>
</div>
</div>
</div> </div>
{/* CSS Animations */} {/* Input animations */}
<style jsx>{` <style jsx>{`
@keyframes orbit-1 { @keyframes field-fade-in {
0% { transform: rotate(0deg); } from {
100% { transform: rotate(360deg); } opacity: 0;
transform: translateY(8px) scale(0.99);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
} }
@keyframes orbit-2 {
0% { transform: rotate(0deg); } @keyframes input-focus-pulse {
100% { transform: rotate(-360deg); } 0% {
box-shadow: 0 0 0 0 rgba(141, 107, 29, 0);
}
100% {
box-shadow: 0 0 0 3px rgba(141, 107, 29, 0.35);
}
} }
.animate-orbit-1 {
animation: orbit-1 3s linear infinite; .field-animated {
transform-origin: 0 0; animation: field-fade-in 0.45s ease-out both;
} }
.animate-orbit-2 {
animation: orbit-2 4s linear infinite; .input-animated {
transform-origin: 0 0; transition:
border-color 0.18s ease,
box-shadow 0.18s ease,
background-color 0.18s ease,
transform 0.12s ease;
}
.input-animated:focus {
animation: input-focus-pulse 0.22s ease-out;
background-color: rgba(255, 255, 255, 0.96);
transform: translateY(-1px);
} }
`}</style> `}</style>
</div> </div>

View File

@ -8,9 +8,11 @@ import useAuthStore from '../store/authStore'
import { ToastProvider } from '../components/toast/toastComponent' import { ToastProvider } from '../components/toast/toastComponent'
import PageTransitionEffect from '../components/animation/pageTransitionEffect' import PageTransitionEffect from '../components/animation/pageTransitionEffect'
import Waves from '../components/waves' import Waves from '../components/waves'
import CurvedLoop from '../components/curvedLoop'
export default function LoginPage() { export default function LoginPage() {
const [hasHydrated, setHasHydrated] = useState(false) const [hasHydrated, setHasHydrated] = useState(false)
const [isMobile, setIsMobile] = useState(false)
const router = useRouter() const router = useRouter()
const user = useAuthStore(state => state.user) const user = useAuthStore(state => state.user)
@ -19,6 +21,18 @@ export default function LoginPage() {
setHasHydrated(true) 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 // Redirect if user is already logged in
useEffect(() => { useEffect(() => {
if (user) { if (user) {
@ -47,33 +61,75 @@ export default function LoginPage() {
return ( return (
<PageTransitionEffect> <PageTransitionEffect>
<ToastProvider> <ToastProvider>
<PageLayout showFooter={true}> {/* NEW: page-level background wrapper so Waves covers everything */}
<div <div className="relative min-h-screen w-full overflow-x-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
className="relative w-full flex flex-col min-h-screen overflow-hidden" <Waves
style={{ backgroundImage: 'none', background: 'none' }} className="pointer-events-none"
> lineColor="#0f172a"
{/* Waves background */} backgroundColor="rgba(245, 245, 240, 1)"
<Waves waveSpeedX={0.02}
className="pointer-events-none" waveSpeedY={0.01}
lineColor="#0f172a" waveAmpX={40}
backgroundColor="rgba(245, 245, 240, 1)" waveAmpY={20}
waveSpeedX={0.02} friction={0.9}
waveSpeedY={0.01} tension={0.01}
waveAmpX={40} maxCursorMove={120}
waveAmpY={20} xGap={12}
friction={0.9} yGap={36}
tension={0.01} animate={!isMobile}
maxCursorMove={120} interactive={!isMobile}
xGap={12} />
yGap={36}
/> <PageLayout showFooter={true} className="bg-transparent text-gray-900">
<div className="relative z-10 flex-1 flex items-center justify-center"> {/* ...existing code... */}
<div className="w-full"> <div
<LoginForm /> className={`relative z-10 w-full flex flex-col flex-1 min-h-0 ${
</div> 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}
interactive={false}
className="tracking-[0.2em]"
/>
</div>
<div className="w-full flex items-center justify-center px-3 sm:px-0">
<LoginForm />
</div>
</div>
)}
</div> </div>
</div> {/* ...existing code... */}
</PageLayout> </PageLayout>
</div>
</ToastProvider> </ToastProvider>
</PageTransitionEffect> </PageTransitionEffect>
) )

View File

@ -11,10 +11,33 @@ import SplitText from './components/SplitText';
export default function HomePage() { export default function HomePage() {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const [isHover, setIsHover] = useState(false); 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(); 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 = () => { const handleLoginClick = () => {
if (!containerRef.current) { // Mobile: no page fade animation
if (isMobile || !containerRef.current) {
router.push('/login'); router.push('/login');
return; 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(() => { useEffect(() => {
if (isMobile) return;
const resetHover = () => setIsHover(false); const resetHover = () => setIsHover(false);
window.addEventListener('wheel', resetHover, { passive: true }); window.addEventListener('wheel', resetHover, { passive: true });
window.addEventListener('scroll', resetHover, { passive: true }); window.addEventListener('scroll', resetHover, { passive: true });
@ -36,7 +60,10 @@ export default function HomePage() {
window.removeEventListener('wheel', resetHover); window.removeEventListener('wheel', resetHover);
window.removeEventListener('scroll', resetHover); window.removeEventListener('scroll', resetHover);
}; };
}, []); }, [isMobile]);
// Prevent any home UI flash on mobile
if (isMobile) return null;
return ( return (
<PageLayout> <PageLayout>
@ -44,7 +71,7 @@ export default function HomePage() {
ref={containerRef} ref={containerRef}
className="min-h-screen flex items-center justify-center relative overflow-hidden bg-black text-white" className="min-h-screen flex items-center justify-center relative overflow-hidden bg-black text-white"
> >
{/* Waves background (reverted settings) */} {/* Waves background */}
<Waves <Waves
className="pointer-events-none" className="pointer-events-none"
lineColor="#0f172a" lineColor="#0f172a"
@ -58,36 +85,45 @@ export default function HomePage() {
maxCursorMove={120} maxCursorMove={120}
xGap={12} xGap={12}
yGap={36} yGap={36}
animate={!isMobile}
interactive={!isMobile}
/> />
<h1 className="z-10"> <h1 className="z-10">
<a <a
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
onClick={handleLoginClick} onClick={handleLoginClick}
onMouseEnter={isMobile ? undefined : () => setIsHover(true)}
onMouseLeave={isMobile ? undefined : () => setIsHover(false)}
className="cursor-pointer" className="cursor-pointer"
> >
<SplitText {isMobile ? (
key={isHover ? 'login' : 'profit-planet'} <span className="block text-5xl sm:text-6xl font-bold text-gray-500 text-center px-4">
text={isHover ? 'LOGIN' : 'PROFIT PLANET'} PROFIT PLANET
tag="span" </span>
className={`text-9xl md:text-9xl font-bold transition-colors duration-300 ${ ) : (
isHover ? 'text-black' : 'text-gray-500' <SplitText
}`} key={isHover ? 'login' : 'profit-planet'}
delay={100} text={isHover ? 'LOGIN' : 'PROFIT PLANET'}
duration={0.6} tag="span"
ease="power3.out" className={`text-7xl sm:text-8xl md:text-9xl font-bold transition-colors duration-300 ${
splitType="chars" isHover ? 'text-black' : 'text-gray-500'
from={{ opacity: 0, y: 40 }} }`}
to={{ opacity: 1, y: 0 }} delay={100}
threshold={0.1} duration={0.6}
rootMargin="-100px" ease="power3.out"
textAlign="center" splitType="chars"
/> from={{ opacity: 0, y: 40 }}
to={{ opacity: 1, y: 0 }}
threshold={0.1}
rootMargin="-100px"
textAlign="center"
/>
)}
</a> </a>
</h1> </h1>
<Crosshair containerRef={containerRef} color="#0f172a" /> {/* No parallax/crosshair on mobile */}
{!isMobile && <Crosshair containerRef={containerRef} color="#0f172a" />}
</div> </div>
</PageLayout> </PageLayout>
); );

View File

@ -3,8 +3,10 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useSearchParams, useRouter } from 'next/navigation' import { useSearchParams, useRouter } from 'next/navigation'
import PageLayout from '../components/PageLayout' 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 searchParams = useSearchParams()
const router = useRouter() const router = useRouter()
const token = searchParams.get('token') const token = searchParams.get('token')
@ -22,6 +24,7 @@ export default function PasswordResetPage() {
const [resetLoading, setResetLoading] = useState(false) const [resetLoading, setResetLoading] = useState(false)
const [resetSuccess, setResetSuccess] = useState(false) const [resetSuccess, setResetSuccess] = useState(false)
const [resetError, setResetError] = useState('') const [resetError, setResetError] = useState('')
const { showToast } = useToast()
// Basic validators // Basic validators
const validEmail = (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val) const validEmail = (val: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
@ -40,7 +43,13 @@ export default function PasswordResetPage() {
e.preventDefault() e.preventDefault()
if (requestLoading) return if (requestLoading) return
if (!validEmail(email)) { 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 return
} }
setRequestError('') setRequestError('')
@ -49,8 +58,19 @@ export default function PasswordResetPage() {
// TODO: call API endpoint: POST /auth/password-reset/request // TODO: call API endpoint: POST /auth/password-reset/request
await new Promise(r => setTimeout(r, 1100)) await new Promise(r => setTimeout(r, 1100))
setRequestSuccess(true) setRequestSuccess(true)
showToast({
variant: 'success',
title: 'Password reset email',
message: 'If this email exists, a reset link has been sent.',
})
} catch { } 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 { } finally {
setRequestLoading(false) setRequestLoading(false)
} }
@ -60,11 +80,23 @@ export default function PasswordResetPage() {
e.preventDefault() e.preventDefault()
if (resetLoading) return if (resetLoading) return
if (!validPassword(password)) { 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 return
} }
if (password !== confirmPassword) { 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 return
} }
setResetError('') setResetError('')
@ -73,89 +105,92 @@ export default function PasswordResetPage() {
// TODO: call API endpoint: POST /auth/password-reset/confirm { token, password } // TODO: call API endpoint: POST /auth/password-reset/confirm { token, password }
await new Promise(r => setTimeout(r, 1200)) await new Promise(r => setTimeout(r, 1200))
setResetSuccess(true) setResetSuccess(true)
showToast({
variant: 'success',
title: 'Password updated',
message: 'Your password has been changed. Redirecting to login...',
})
} catch { } 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 { } finally {
setResetLoading(false) setResetLoading(false)
} }
} }
const passwordHints = [ const passwordHints = [
{ label: 'Mindestens 8 Zeichen', pass: password.length >= 8 }, { label: 'At least 8 characters', pass: password.length >= 8 },
{ label: 'Großbuchstabe (A-Z)', pass: /[A-Z]/.test(password) }, { label: 'Uppercase letter (A-Z)', pass: /[A-Z]/.test(password) },
{ label: 'Kleinbuchstabe (a-z)', pass: /[a-z]/.test(password) }, { label: 'Lowercase letter (a-z)', pass: /[a-z]/.test(password) },
{ label: 'Ziffer (0-9)', pass: /\d/.test(password) }, { label: 'Number (0-9)', pass: /\d/.test(password) },
{ label: 'Sonderzeichen (!@#$...)', pass: /[\W_]/.test(password) } { label: 'Special character (!@#$...)', pass: /[\W_]/.test(password) }
] ]
return ( return (
<PageLayout> <PageLayout>
<main className="relative flex flex-col flex-1 pt-20 sm:pt-28 pb-12 sm:pb-16 overflow-hidden"> <div
{/* Background Pattern */} className="relative w-full flex flex-col min-h-screen overflow-hidden"
<svg style={{ backgroundImage: 'none', background: 'none' }}
aria-hidden="true" >
className="absolute inset-0 -z-10 h-full w-full stroke-white/10" <Waves
> className="pointer-events-none"
<defs> lineColor="#0f172a"
<pattern backgroundColor="rgba(245, 245, 240, 1)"
x="50%" waveSpeedX={0.02}
y={-1} waveSpeedY={0.01}
id="affiliate-pattern" waveAmpX={40}
width={200} waveAmpY={20}
height={200} friction={0.9}
patternUnits="userSpaceOnUse" tension={0.01}
> maxCursorMove={120}
<path d="M.5 200V.5H200" fill="none" stroke="rgba(255,255,255,0.05)" /> xGap={12}
</pattern> yGap={36}
</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"
>
<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"
/>
</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" />
{/* 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 */} {/* push content a bit further down while still centering */}
<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"> <main className="relative z-10 flex flex-col flex-1 items-center justify-center pt-32 sm:pt-0 pb-8 sm:pb-10">
<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" /> {/* Widened container to match header */}
<div className="relative"> <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 && ( {!token && (
<form onSubmit={handleRequestSubmit} className="space-y-6"> <form onSubmit={handleRequestSubmit} className="space-y-6">
<div> <div>
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2" htmlFor="email"> <label className="block text-sm font-semibold text-[#0F172A] mb-2" htmlFor="email">
E-Mail-Adresse Email address
</label> </label>
<input <input
id="email" id="email"
type="email" type="email"
value={email} value={email}
onChange={e => { setEmail(e.target.value); setRequestError(''); setRequestSuccess(false)}} 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" 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="dein.email@example.com" placeholder="your.email@example.com"
required required
/> />
</div> </div>
@ -168,17 +203,17 @@ export default function PasswordResetPage() {
{requestSuccess && ( {requestSuccess && (
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700"> <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> </div>
)} )}
<button <button
type="submit" type="submit"
disabled={requestLoading} 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 requestLoading
? 'bg-gray-400 cursor-wait' ? 'border-white/30 bg-white/20 text-slate-300 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/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 ? ( {requestLoading ? (
@ -187,18 +222,18 @@ export default function PasswordResetPage() {
Senden... Senden...
</> </>
) : ( ) : (
'Zurücksetzlink anfordern' 'Request reset link'
)} )}
</button> </button>
<div className="text-center text-sm text-gray-600 dark:text-gray-400"> <div className="text-center text-sm text-gray-700">
Erinnerst du dich?{' '} Remember it now?{' '}
<button <button
type="button" type="button"
onClick={() => router.push('/login')} 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> </button>
</div> </div>
</form> </form>
@ -208,8 +243,8 @@ export default function PasswordResetPage() {
<form onSubmit={handleResetSubmit} className="space-y-6"> <form onSubmit={handleResetSubmit} className="space-y-6">
<div className="grid gap-6 sm:grid-cols-2"> <div className="grid gap-6 sm:grid-cols-2">
<div className="sm:col-span-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"> <label className="block text-sm font-medium text-gray-900 mb-2" htmlFor="password">
Neues Passwort New password
</label> </label>
<div className="relative"> <div className="relative">
<input <input
@ -217,16 +252,16 @@ export default function PasswordResetPage() {
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={password} value={password}
onChange={e => { setPassword(e.target.value); setResetError(''); setResetSuccess(false)}} 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" 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="••••••••" placeholder="Your new password"
required required
/> />
<button <button
type="button" type="button"
onClick={() => setShowPassword(p => !p)} 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> </button>
</div> </div>
<div className="mt-3 grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-2 text-xs"> <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>
<div className="sm:col-span-2"> <div className="sm:col-span-2">
<label className="block text-sm font-medium text-gray-900 dark:text-gray-100 mb-2" htmlFor="confirm"> <label className="block text-sm font-medium text-gray-900 mb-2" htmlFor="confirm">
Passwort bestätigen Confirm password
</label> </label>
<input <input
id="confirm" id="confirm"
type={showPassword ? 'text' : 'password'} type={showPassword ? 'text' : 'password'}
value={confirmPassword} value={confirmPassword}
onChange={e => { setConfirmPassword(e.target.value); setResetError(''); setResetSuccess(false)}} 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" 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="Bestätigung" placeholder="Confirm password"
required required
/> />
{confirmPassword && password !== confirmPassword && ( {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>
</div> </div>
@ -270,37 +305,37 @@ export default function PasswordResetPage() {
)} )}
{resetSuccess && ( {resetSuccess && (
<div className="rounded-lg border border-green-300 bg-green-50 px-4 py-3 text-sm text-green-700"> <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> </div>
)} )}
<button <button
type="submit" type="submit"
disabled={resetLoading} 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 resetLoading
? 'bg-gray-400 cursor-wait' ? 'border-white/30 bg-white/20 text-slate-300 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/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 ? ( {resetLoading ? (
<> <>
<span className="size-4 mr-2 rounded-full border-2 border-white border-b-transparent animate-spin" /> <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> </button>
<div className="text-center text-sm text-gray-600 dark:text-gray-400"> <div className="text-center text-sm text-gray-600 dark:text-gray-400">
Link abgelaufen?{' '} Link expired?{' '}
<button <button
type="button" type="button"
onClick={() => router.push('/password-reset')} onClick={() => router.push('/password-reset')}
className="text-indigo-600 dark:text-indigo-400 hover:underline font-medium" className="text-indigo-600 dark:text-indigo-400 hover:underline font-medium"
> >
Erneut anfordern Request again
</button> </button>
</div> </div>
</form> </form>
@ -308,7 +343,16 @@ export default function PasswordResetPage() {
</div> </div>
</div> </div>
</div> </div>
</main> </main>
</div>
</PageLayout> </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, setBankInfo: (v: { accountHolder: string, iban: string }) => void,
onEdit?: () => void onEdit?: () => void
}) { }) {
// editing disabled for now; keep props to avoid refactors
const accountHolder = profileData.accountHolder || ''
const iban = profileData.iban || ''
return ( 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"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Bank Information</h2> <h2 className="text-lg font-semibold text-gray-900">Bank Information</h2>
{!editingBank && ( <span className="text-xs text-gray-500">Editing disabled</span>
<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>
)}
</div> </div>
<form
className="space-y-4" <div className="space-y-4">
onSubmit={e => {
e.preventDefault()
setBankInfo(bankDraft)
setEditingBank(false)
}}
>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Account Holder</label> <label className="block text-sm font-medium text-gray-700 mb-1">Account Holder</label>
<input <input
type="text" type="text"
className="w-full p-2 border border-gray-200 rounded-lg bg-gray-50 text-gray-900" className="w-full p-2 border border-white/60 rounded-lg bg-white/50 text-gray-900"
value={editingBank ? bankDraft.accountHolder : (profileData.accountHolder || '')} value={accountHolder}
onChange={e => setBankDraft({ ...bankDraft, accountHolder: e.target.value })} disabled
disabled={!editingBank} placeholder="Not provided"
placeholder={profileData.accountHolder ? '' : 'Not provided'}
/> />
{!editingBank && !profileData.accountHolder && ( {!accountHolder && <div className="mt-1 text-sm italic text-gray-400">Not provided</div>}
<div className="mt-1 text-sm italic text-gray-400">Not provided</div>
)}
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">IBAN</label> <label className="block text-sm font-medium text-gray-700 mb-1">IBAN</label>
<input <input
type="text" type="text"
className="w-full p-2 border border-gray-200 rounded-lg bg-gray-50 text-gray-900" className="w-full p-2 border border-white/60 rounded-lg bg-white/50 text-gray-900"
value={editingBank ? bankDraft.iban : (profileData.iban || '')} value={iban}
onChange={e => setBankDraft({ ...bankDraft, iban: e.target.value })} disabled
disabled={!editingBank} placeholder="Not provided"
placeholder={profileData.iban ? '' : 'Not provided'}
/> />
{!editingBank && !profileData.iban && ( {!iban && <div className="mt-1 text-sm italic text-gray-400">Not provided</div>}
<div className="mt-1 text-sm italic text-gray-400">Not provided</div>
)}
</div> </div>
{editingBank && ( </div>
<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> </div>
) )
} }

View File

@ -7,7 +7,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
onEdit?: () => void onEdit?: () => void
}) { }) {
return ( 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"> <div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-gray-900">Basic Information</h2> <h2 className="text-lg font-semibold text-gray-900">Basic Information</h2>
<button <button
@ -25,7 +25,7 @@ export default function BasicInformation({ profileData, HighlightIfMissing, onEd
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
First Name First Name
</label> </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" /> <UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.firstName}> <HighlightIfMissing value={profileData.firstName}>
<span className="text-gray-900">{profileData.firstName}</span> <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"> <label className="block text-sm font-medium text-gray-700 mb-1">
Last Name Last Name
</label> </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" /> <UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.lastName}> <HighlightIfMissing value={profileData.lastName}>
<span className="text-gray-900">{profileData.lastName}</span> <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"> <label className="block text-sm font-medium text-gray-700 mb-1">
Contact Person Contact Person
</label> </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" /> <UserCircleIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.contactPersonName}> <HighlightIfMissing value={profileData.contactPersonName}>
<span className="text-gray-900">{profileData.contactPersonName}</span> <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"> <label className="block text-sm font-medium text-gray-700 mb-1">
Email Address Email Address
</label> </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" /> <EnvelopeIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.email}> <HighlightIfMissing value={profileData.email}>
<span className="text-gray-900">{profileData.email}</span> <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"> <label className="block text-sm font-medium text-gray-700 mb-1">
Phone Number Phone Number
</label> </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" /> <PhoneIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.phone}> <HighlightIfMissing value={profileData.phone}>
<span className="text-gray-900">{profileData.phone}</span> <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"> <label className="block text-sm font-medium text-gray-700 mb-1">
Address Address
</label> </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" /> <MapPinIcon className="h-5 w-5 text-gray-400 mr-3" />
<HighlightIfMissing value={profileData.address}> <HighlightIfMissing value={profileData.address}>
<span className="text-gray-900">{profileData.address}</span> <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({ export default function EditModal({
open, open,
@ -19,16 +19,54 @@ export default function EditModal({
onCancel: () => void, onCancel: () => void,
children?: React.ReactNode 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(() => { useEffect(() => {
const body = document.body
const html = document.documentElement
if (open) { if (open) {
document.body.style.overflow = 'hidden'; 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 { } else {
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
} }
return () => { 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]); }, [open]);
// Animation state // Animation state
@ -52,9 +90,15 @@ export default function EditModal({
}`} }`}
> >
<div <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' 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"> <h2 className="text-xl font-semibold mb-4 text-gray-900">
Edit {type === 'basic' ? 'Basic Information' : 'Bank Information'} Edit {type === 'basic' ? 'Basic Information' : 'Bank Information'}

View File

@ -3,7 +3,7 @@ import React from 'react'
export default function MediaSection({ documents }: { documents: any[] }) { export default function MediaSection({ documents }: { documents: any[] }) {
const hasDocuments = Array.isArray(documents) && documents.length > 0; const hasDocuments = Array.isArray(documents) && documents.length > 0;
return ( 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> <h2 className="text-lg font-semibold text-gray-900 mb-4">Media & Documents</h2>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
{hasDocuments ? ( {hasDocuments ? (

View File

@ -2,7 +2,7 @@ import React from 'react';
export default function ProfileCompletion({ profileComplete }: { profileComplete: number }) { export default function ProfileCompletion({ profileComplete }: { profileComplete: number }) {
return ( 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"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">Profile Completion</h2> <h2 className="text-lg font-semibold text-gray-900">Profile Completion</h2>
<span className="text-sm font-medium text-[#8D6B1D]"> <span className="text-sm font-medium text-[#8D6B1D]">

View File

@ -8,7 +8,7 @@ export default function UserAbo() {
return ( return (
<section className="space-y-4"> <section className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">My Subscriptions</h2> <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 Loading subscriptions
</div> </div>
</section> </section>
@ -19,7 +19,7 @@ export default function UserAbo() {
return ( return (
<section className="space-y-4"> <section className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">My Subscriptions</h2> <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} {error}
</div> </div>
</section> </section>
@ -30,11 +30,11 @@ export default function UserAbo() {
<section className="space-y-4"> <section className="space-y-4">
<h2 className="text-lg font-semibold text-gray-900">My Subscriptions</h2> <h2 className="text-lg font-semibold text-gray-900">My Subscriptions</h2>
{(!abos || abos.length === 0) ? ( {(!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. No subscriptions yet.
</div> </div>
) : ( ) : (
<div className="grid gap-4"> <div className="grid gap-3 sm:gap-4">
{abos.map(abo => { {abos.map(abo => {
const status = (abo.status || 'active') as 'active' | 'paused' | 'canceled' const status = (abo.status || 'active') as 'active' | 'paused' | 'canceled'
const nextBilling = abo.nextBillingAt ? new Date(abo.nextBillingAt).toLocaleDateString() : '—' const nextBilling = abo.nextBillingAt ? new Date(abo.nextBillingAt).toLocaleDateString() : '—'
@ -53,7 +53,7 @@ export default function UserAbo() {
</span> </span>
)) ))
return ( 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 className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium text-gray-900">{abo.name || 'Coffee Subscription'}</p> <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 React, { useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore' import useAuthStore from '../store/authStore'
import Header from '../components/nav/Header' import PageLayout from '../components/PageLayout'
import Footer from '../components/Footer' import Waves from '../components/waves'
import ProfileCompletion from './components/profileCompletion' import ProfileCompletion from './components/profileCompletion'
import BasicInformation from './components/basicInformation' import BasicInformation from './components/basicInformation'
import MediaSection from './components/mediaSection' import MediaSection from './components/mediaSection'
import BankInformation from './components/bankInformation' import BankInformation from './components/bankInformation'
import EditModal from './components/editModal' import EditModal from './components/editModal'
import UserAbo from './components/userAbo' import UserAbo from './components/userAbo'
import { import { getProfileCompletion } from './hooks/getProfileCompletion'
UserCircleIcon, import { useProfileData } from './hooks/getProfileData'
EnvelopeIcon, import { useMedia } from './hooks/getMedia'
PhoneIcon, import { editProfileBasic } from './hooks/editProfile'
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';
// Helper to display missing fields in subtle gray italic (no yellow highlight) // Helper to display missing fields in subtle gray italic (no yellow highlight)
function HighlightIfMissing({ value, children }: { value: any, children: React.ReactNode }) { function HighlightIfMissing({ value, children }: { value: any, children: React.ReactNode }) {
@ -60,85 +52,97 @@ const defaultProfileData = {
userType: '', 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() { export default function ProfilePage() {
const router = useRouter() const router = useRouter()
const user = useAuthStore(state => state.user) 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 // --- declare ALL hooks before any early return (Rules of Hooks) ---
useEffect(() => { const [refreshKey, setRefreshKey] = React.useState(0)
if (user?.id) setUserId(user.id); const [showRefreshing, setShowRefreshing] = React.useState(false)
}, [user]); const [completionLoading, setCompletionLoading] = React.useState(false)
// Add refresh key and UI states for smooth refresh // Progress bar state (MOVED ABOVE EARLY RETURN)
const [refreshKey, setRefreshKey] = React.useState(0); const [progressPercent, setProgressPercent] = React.useState<number>(0)
const [showRefreshing, setShowRefreshing] = React.useState(false); const [completedSteps, setCompletedSteps] = React.useState<string[]>([])
const [completionLoading, setCompletionLoading] = React.useState(false); const [allSteps, setAllSteps] = React.useState<string[]>([])
// Fetch profile data on page load/navigation, now with refreshKey // Bank/edit state (keep, but bank editing disabled)
const { data: profileDataApi, loading: profileLoading, error: profileError } = useProfileData(userId, refreshKey); 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 [editModalOpen, setEditModalOpen] = React.useState(false)
const { data: mediaData, loading: mediaLoading, error: mediaError } = useMedia(userId, refreshKey); 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(() => { setHasHydrated(true) }, [])
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(() => { useEffect(() => {
if (!user) { if (user?.id) setUserId(user.id)
router.push('/login'); }, [user])
return;
} // Fetch hooks can run with undefined userId; they should handle it internally
async function fetchCompletion() { const { data: profileDataApi, loading: profileLoading } = useProfileData(userId, refreshKey)
setCompletionLoading(true); const { data: mediaData, loading: mediaLoading } = useMedia(userId, refreshKey)
const progress = await getProfileCompletion();
// progress can be percent or object // 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 (progress && typeof progress === 'object') {
// If not admin-verified, cap progress below 100 to reflect pending verification const pct = progress.progressPercent ?? 0
const pct = progress.progressPercent ?? 0; const isAdminVerified = Boolean(profileDataApi?.userStatus?.is_admin_verified)
// Try to read admin verification from profileDataApi if available; otherwise assume false until data loads setProgressPercent(isAdminVerified ? pct : Math.min(pct, 95))
const isAdminVerified = Boolean(profileDataApi?.userStatus?.is_admin_verified); setCompletedSteps(progress.completedSteps ?? [])
setProgressPercent(isAdminVerified ? pct : Math.min(pct, 95)); setAllSteps(progress.steps?.map((s: any) => s.name || s.title || '') ?? [])
setCompletedSteps(progress.completedSteps ?? []);
setAllSteps(progress.steps?.map((s: any) => s.name || s.title || '') ?? []);
} else if (typeof progress === 'number') { } 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(() => { useEffect(() => {
const verified = Boolean(profileDataApi?.userStatus?.is_admin_verified); const verified = Boolean(profileDataApi?.userStatus?.is_admin_verified)
if (verified) { if (verified) setProgressPercent(prev => (prev < 100 ? 100 : prev))
setProgressPercent(prev => (prev < 100 ? 100 : prev)); }, [profileDataApi?.userStatus?.is_admin_verified])
}
}, [profileDataApi?.userStatus?.is_admin_verified]);
// Use API profile data if available, fallback to mock
const profileData = React.useMemo(() => { const profileData = React.useMemo(() => {
if (!profileDataApi) { if (!profileDataApi) {
return { return {
@ -150,13 +154,13 @@ export default function ProfilePage() {
joinDate: 'Oktober 2024', joinDate: 'Oktober 2024',
memberStatus: 'Gold Member', memberStatus: 'Gold Member',
profileComplete: progressPercent, profileComplete: progressPercent,
accountHolder: '', // Always empty string if not provided accountHolder: '',
iban: '', iban: '',
contactPersonName: '', contactPersonName: '',
userType: user?.userType || '', userType: user?.userType || '',
}; }
} }
const { user: apiUser = {}, profile: apiProfile = {}, userStatus = {} } = profileDataApi; const { user: apiUser = {}, profile: apiProfile = {}, userStatus = {} } = profileDataApi
return { return {
firstName: apiUser.firstName ?? apiProfile.first_name ?? '', firstName: apiUser.firstName ?? apiProfile.first_name ?? '',
lastName: apiUser.lastName ?? apiProfile.last_name ?? '', lastName: apiUser.lastName ?? apiProfile.last_name ?? '',
@ -168,305 +172,224 @@ export default function ProfilePage() {
: '', : '',
memberStatus: userStatus.status ?? '', memberStatus: userStatus.status ?? '',
profileComplete: progressPercent, profileComplete: progressPercent,
accountHolder: apiProfile.account_holder_name ?? '', // Only use account_holder_name accountHolder: apiProfile.account_holder_name ?? '',
iban: apiUser.iban ?? '', iban: apiUser.iban ?? '',
contactPersonName: apiProfile.contact_person_name ?? '', contactPersonName: apiProfile.contact_person_name ?? '',
userType: apiUser.userType ?? '', 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);
}
// 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.');
}
} }
} }, [profileDataApi, user, progressPercent])
// Modal change handler const documents = Array.isArray(mediaData?.documents) ? mediaData.documents : []
function handleEditModalChange(key: string, value: string) {
setEditModalValues(prev => ({ ...prev, [key]: value }));
}
// Hide overlay when all data re-fetches complete
useEffect(() => { useEffect(() => {
if (showRefreshing && !profileLoading && !mediaLoading && !completionLoading) { if (showRefreshing && !profileLoading && !mediaLoading && !completionLoading) {
const t = setTimeout(() => setShowRefreshing(false), 200); // small delay for smoothness const t = setTimeout(() => setShowRefreshing(false), 200)
return () => clearTimeout(t); 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 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>
)
}
return ( return (
<div className="min-h-screen flex flex-col bg-gray-50" suppressHydrationWarning> <div className="relative w-full min-h-screen overflow-x-hidden">
<Header /> <Waves
<main className="flex-1 py-8 px-4 sm:px-6 lg:px-8"> className="pointer-events-none"
<div className="max-w-4xl mx-auto"> lineColor="#0f172a"
{loadingUser && ( backgroundColor="rgba(245, 245, 240, 1)"
<div className="flex items-center justify-center py-20"> waveSpeedX={0.02}
<div className="text-center"> waveSpeedY={0.01}
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div> waveAmpX={40}
<p className="text-[#4A4A4A]">Loading...</p> waveAmpY={20}
</div> friction={0.9}
</div> tension={0.01}
)} maxCursorMove={120}
{!loadingUser && ( xGap={12}
<> yGap={36}
{/* Page Header */} animate={!isMobile}
<div className="mb-8"> interactive={!isMobile}
<h1 className="text-3xl font-bold text-gray-900">Profile Settings</h1> />
<p className="text-gray-600 mt-2">
Manage your account information and preferences
</p>
</div>
{/* Pending admin verification notice (above progress) */} <div className="relative z-10 min-h-screen">
{profileDataApi?.userStatus && profileDataApi.userStatus.is_admin_verified === 0 && ( <PageLayout className="bg-transparent text-gray-900">
<div className="rounded-md bg-yellow-50 border border-yellow-200 p-3 text-sm text-yellow-800 mb-2"> <main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
Your account is fully submitted. Our team will verify your account shortly. <div className="max-w-4xl mx-auto">
</div> {/* 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">
{/* Profile Completion Progress Bar */} {/* Page Header */}
<ProfileCompletion <div className="mb-8">
profileComplete={profileData.profileComplete} <h1 className="text-3xl font-bold text-gray-900">Profile Settings</h1>
/> <p className="text-gray-600 mt-2">
Manage your account information and preferences
</p>
</div>
{/* Basic Info + Sidebar */} {/* Pending admin verification notice (above progress) */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mb-8"> {profileDataApi?.userStatus && profileDataApi.userStatus.is_admin_verified === 0 && (
{/* Basic Information */} <div className="rounded-md bg-yellow-50/80 backdrop-blur border border-yellow-200 p-3 text-sm text-yellow-800 mb-2">
<div className="lg:col-span-2 space-y-6"> Your account is fully submitted. Our team will verify your account shortly.
<BasicInformation
profileData={profileData}
HighlightIfMissing={HighlightIfMissing}
// Add edit button handler
onEdit={() => openEditModal('basic', {
firstName: profileData.firstName,
lastName: profileData.lastName,
email: profileData.email,
phone: profileData.phone,
address: profileData.address,
})}
/>
</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">
<h3 className="font-semibold text-gray-900 mb-4">Account Status</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Member Since</span>
<span className="text-sm font-medium text-gray-900">{profileData.joinDate}</span>
</div> </div>
)}
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Status</span> <ProfileCompletion profileComplete={profileData.profileComplete} />
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gradient-to-r from-[#8D6B1D] to-[#C49225] text-white">
{profileData.memberStatus} {/* Basic Info + Sidebar */}
</span> <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
profileData={profileData}
HighlightIfMissing={HighlightIfMissing}
// Add edit button handler
onEdit={() => openEditModal('basic', {
firstName: profileData.firstName,
lastName: profileData.lastName,
phone: profileData.phone,
address: profileData.address,
})}
/>
</div> </div>
{/* Sidebar: Account Status + Quick Actions */}
<div className="flex items-center justify-between"> <div className="space-y-6">
<span className="text-sm text-gray-600">Profile</span> {/* Account Status (make translucent) */}
<span className="text-sm font-medium text-green-600">Verified</span> <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">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Member Since</span>
<span className="text-sm font-medium text-gray-900">{profileData.joinDate}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Status</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gradient-to-r from-[#8D6B1D] to-[#C49225] text-white">
{profileData.memberStatus}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Profile</span>
<span className="text-sm font-medium text-green-600">Verified</span>
</div>
</div>
</div>
{/* 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">
<button
onClick={() => router.push('/dashboard')}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
>
Go to Dashboard
</button>
<button className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors">
Download Account Data
</button>
<button className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors">
Delete Account
</button>
</div>
</div>
</div> </div>
</div> </div>
</div>
{/* Quick Actions */} {/* Bank Info, Media */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <div className="space-y-6 sm:space-y-8 mb-8">
<h3 className="font-semibold text-gray-900 mb-4">Quick Actions</h3> {/* --- My Abo Section (above bank info) --- */}
<UserAbo />
<div className="space-y-3"> {/* --- Edit Bank Information Section --- */}
<button <BankInformation
onClick={() => router.push('/dashboard')} profileData={profileData}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors" editingBank={false} // force read-only
> bankDraft={bankDraft}
Go to Dashboard setEditingBank={setEditingBank}
</button> setBankDraft={setBankDraft}
<button className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"> setBankInfo={setBankInfo}
Download Account Data // onEdit disabled for now
</button> // onEdit={() => openEditModal('bank', { ... })}
<button className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors"> />
Delete Account {/* --- Media Section --- */}
</button> <MediaSection documents={documents} />
</div> </div>
</div> </div>
</div> </div>
</div> </main>
{/* Bank Info, Media */} {/* Edit Modal */}
<div className="space-y-8 mb-8"> <EditModal
{/* --- My Abo Section (above bank info) --- */} open={editModalOpen}
<UserAbo /> type={editModalType}
{/* --- Edit Bank Information Section --- */} fields={editModalType === 'basic' ? basicFields : bankFields}
<BankInformation values={editModalValues}
profileData={profileData} onChange={handleEditModalChange}
editingBank={editingBank} onSave={handleEditModalSave}
bankDraft={bankDraft} onCancel={() => { setEditModalOpen(false); setEditModalError(null); }}
setEditingBank={setEditingBank} >
setBankDraft={setBankDraft} {/* Show error message if present */}
setBankInfo={setBankInfo} {editModalError && (
// Add edit button handler <div className="text-sm text-red-600 mb-2">{editModalError}</div>
onEdit={() => openEditModal('bank', { )}
accountHolder: profileData.accountHolder, </EditModal>
iban: profileData.iban, </PageLayout>
})} </div>
/>
{/* --- 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
open={editModalOpen}
type={editModalType}
fields={editModalType === 'basic' ? basicFields : bankFields}
values={editModalValues}
onChange={handleEditModalChange}
onSave={handleEditModalSave}
onCancel={() => { setEditModalOpen(false); setEditModalError(null); }}
>
{/* Show error message if present */}
{editModalError && (
<div className="text-sm text-red-600 mb-2">{editModalError}</div>
)}
</EditModal>
</div> </div>
) )
} }

View File

@ -142,17 +142,16 @@ export default function RegisterForm({
} }
const phoneApi = personalPhoneRef.current const phoneApi = personalPhoneRef.current
const dialCode = phoneApi?.getDialCode?.()
const intlNumber = phoneApi?.getNumber() || '' const intlNumber = phoneApi?.getNumber() || ''
const valid = phoneApi?.isValid() ?? false const valid = phoneApi?.isValid() ?? false
console.log('[RegisterForm] validatePersonalForm phone check', { if (!dialCode) {
rawState: personalForm.phoneNumber, setError('Please select a country code from the dropdown before continuing.')
intlFromApi: intlNumber, return false
isValidFromApi: valid, }
})
if (!intlNumber) { if (!intlNumber) {
setError('Please enter your phone number including country code.') setError('Please enter your phone number.')
return false return false
} }
if (!valid) { if (!valid) {
@ -191,22 +190,20 @@ export default function RegisterForm({
const companyApi = companyPhoneRef.current const companyApi = companyPhoneRef.current
const contactApi = contactPhoneRef.current const contactApi = contactPhoneRef.current
const companyDialCode = companyApi?.getDialCode?.()
const contactDialCode = contactApi?.getDialCode?.()
const companyNumber = companyApi?.getNumber() || '' const companyNumber = companyApi?.getNumber() || ''
const contactNumber = contactApi?.getNumber() || '' const contactNumber = contactApi?.getNumber() || ''
const companyValid = companyApi?.isValid() ?? false const companyValid = companyApi?.isValid() ?? false
const contactValid = contactApi?.isValid() ?? false const contactValid = contactApi?.isValid() ?? false
console.log('[RegisterForm] validateCompanyForm phone check', { if (!companyDialCode || !contactDialCode) {
rawCompany: companyForm.companyPhone, setError('Please select country codes (dropdown) for both company and contact phone numbers.')
rawContact: companyForm.contactPersonPhone, return false
intlCompany: companyNumber, }
intlContact: contactNumber,
companyValid,
contactValid,
})
if (!companyNumber || !contactNumber) { 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 return false
} }
if (!companyValid || !contactValid) { if (!companyValid || !contactValid) {
@ -394,7 +391,8 @@ export default function RegisterForm({
} }
return ( 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 */} {/* Header */}
<div className="mb-6 text-center"> <div className="mb-6 text-center">
<h2 className="text-2xl sm:text-3xl font-extrabold text-[#0F172A] mb-2"> <h2 className="text-2xl sm:text-3xl font-extrabold text-[#0F172A] mb-2">
@ -409,7 +407,7 @@ export default function RegisterForm({
{/* Mode Toggle */} {/* Mode Toggle */}
<div className="flex justify-center mb-8"> <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 <button
className={`px-6 py-2 rounded-md font-semibold text-sm transition-all duration-200 ${ className={`px-6 py-2 rounded-md font-semibold text-sm transition-all duration-200 ${
mode === 'personal' mode === 'personal'
@ -437,7 +435,7 @@ export default function RegisterForm({
{/* Error Message */} {/* Error Message */}
{error && ( {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> <p className="text-red-600 text-sm font-medium">{error}</p>
</div> </div>
)} )}
@ -457,7 +455,7 @@ export default function RegisterForm({
name="firstName" name="firstName"
value={personalForm.firstName} value={personalForm.firstName}
onChange={handlePersonalChange} 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 required
/> />
</div> </div>
@ -472,7 +470,7 @@ export default function RegisterForm({
name="lastName" name="lastName"
value={personalForm.lastName} value={personalForm.lastName}
onChange={handlePersonalChange} 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 required
/> />
</div> </div>
@ -489,7 +487,7 @@ export default function RegisterForm({
name="email" name="email"
value={personalForm.email} value={personalForm.email}
onChange={handlePersonalChange} 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 required
/> />
</div> </div>
@ -504,7 +502,7 @@ export default function RegisterForm({
name="confirmEmail" name="confirmEmail"
value={personalForm.confirmEmail} value={personalForm.confirmEmail}
onChange={handlePersonalChange} 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 required
/> />
</div> </div>
@ -518,7 +516,8 @@ export default function RegisterForm({
id="phoneNumber" id="phoneNumber"
name="phoneNumber" name="phoneNumber"
ref={personalPhoneRef} ref={personalPhoneRef}
placeholder="+49 123 456 7890" autoComplete="tel"
placeholder="e.g. +43 676 1234567"
required required
onChange={e => onChange={e =>
setPersonalForm(prev => ({ ...prev, phoneNumber: (e.target as HTMLInputElement).value })) setPersonalForm(prev => ({ ...prev, phoneNumber: (e.target as HTMLInputElement).value }))
@ -538,7 +537,8 @@ export default function RegisterForm({
name="password" name="password"
value={personalForm.password} value={personalForm.password}
onChange={handlePersonalChange} 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 required
/> />
<button <button
@ -566,7 +566,8 @@ export default function RegisterForm({
name="confirmPassword" name="confirmPassword"
value={personalForm.confirmPassword} value={personalForm.confirmPassword}
onChange={handlePersonalChange} 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 required
/> />
</div> </div>
@ -604,7 +605,7 @@ export default function RegisterForm({
name="companyName" name="companyName"
value={companyForm.companyName} value={companyForm.companyName}
onChange={handleCompanyChange} 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 required
/> />
</div> </div>
@ -619,7 +620,7 @@ export default function RegisterForm({
name="contactPersonName" name="contactPersonName"
value={companyForm.contactPersonName} value={companyForm.contactPersonName}
onChange={handleCompanyChange} 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 required
/> />
</div> </div>
@ -636,7 +637,7 @@ export default function RegisterForm({
name="companyEmail" name="companyEmail"
value={companyForm.companyEmail} value={companyForm.companyEmail}
onChange={handleCompanyChange} 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 required
/> />
</div> </div>
@ -651,7 +652,7 @@ export default function RegisterForm({
name="confirmCompanyEmail" name="confirmCompanyEmail"
value={companyForm.confirmCompanyEmail} value={companyForm.confirmCompanyEmail}
onChange={handleCompanyChange} 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 required
/> />
</div> </div>
@ -666,7 +667,8 @@ export default function RegisterForm({
id="companyPhone" id="companyPhone"
name="companyPhone" name="companyPhone"
ref={companyPhoneRef} ref={companyPhoneRef}
placeholder="+49 123 456 7890" autoComplete="tel"
placeholder="e.g. +43 1 234567"
required required
onChange={e => onChange={e =>
setCompanyForm(prev => ({ ...prev, companyPhone: (e.target as HTMLInputElement).value })) setCompanyForm(prev => ({ ...prev, companyPhone: (e.target as HTMLInputElement).value }))
@ -682,7 +684,8 @@ export default function RegisterForm({
id="contactPersonPhone" id="contactPersonPhone"
name="contactPersonPhone" name="contactPersonPhone"
ref={contactPhoneRef} ref={contactPhoneRef}
placeholder="+49 123 456 7890" autoComplete="tel"
placeholder="e.g. +43 676 1234567"
required required
onChange={e => onChange={e =>
setCompanyForm(prev => ({ setCompanyForm(prev => ({
@ -706,7 +709,8 @@ export default function RegisterForm({
name="password" name="password"
value={companyForm.password} value={companyForm.password}
onChange={handleCompanyChange} 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 required
/> />
<button <button
@ -734,7 +738,8 @@ export default function RegisterForm({
name="confirmPassword" name="confirmPassword"
value={companyForm.confirmPassword} value={companyForm.confirmPassword}
onChange={handleCompanyChange} 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 required
/> />
</div> </div>

View File

@ -17,38 +17,39 @@ export default function SessionDetectedModal({
onCancel, onCancel,
inline = false inline = false
}: SessionDetectedModalProps) { }: SessionDetectedModalProps) {
// Make inline + non-inline consistent
if (!open) return null
if (inline) { if (inline) {
// Inline wrapper removed: parent already wraps/centers
return ( return (
// removed flex-1 and min-h to avoid extra white gap <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="w-full flex justify-center items-center py-8"> <div className="flex gap-4">
<div className="bg-white px-6 py-6 rounded-xl shadow-xl max-w-lg w-full border border-gray-200"> <div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-orange-100/80">
<div className="flex gap-4"> <ExclamationTriangleIcon className="h-6 w-6 text-orange-600" aria-hidden="true" />
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-orange-100"> </div>
<ExclamationTriangleIcon className="h-6 w-6 text-orange-600" aria-hidden="true" /> <div>
</div> <h3 className="text-base font-semibold leading-6 text-[#0F172A]">
<div> Active session detected
<h3 className="text-base font-semibold leading-6 text-[#0F172A]"> </h3>
Active session detected <p className="mt-2 text-sm text-[#4A4A4A]">
</h3> You are already logged in. To register, you must first log out or you can go to the dashboard.
<p className="mt-2 text-sm text-[#4A4A4A]"> </p>
You are already logged in. To register, you must first log out or you can go to the dashboard. <div className="mt-5 flex flex-col sm:flex-row gap-3 sm:justify-end">
</p> <button
<div className="mt-5 flex flex-col sm:flex-row gap-3 sm:justify-end"> type="button"
<button onClick={onCancel}
type="button" 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"
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" Go to dashboard
> </button>
Go to dashboard <button
</button> type="button"
<button onClick={onLogout}
type="button" className="inline-flex justify-center rounded-md bg-[#8D6B1D] px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#7A5E1A] transition-colors"
onClick={onLogout} >
className="inline-flex justify-center rounded-md bg-[#8D6B1D] px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-[#7A5E1A] transition-colors" Log out and register
> </button>
Log out and register
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -82,7 +83,9 @@ export default function SessionDetectedModal({
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" 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="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"> <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 <ExclamationTriangleIcon

View File

@ -21,30 +21,32 @@ export default function InvalidRefLinkModal({
if (!open) return null if (!open) return null
const Content = ( 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"> <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> <div>
<h3 className="text-lg font-semibold text-gray-900">Invalid invitation link</h3> <h3 className="text-lg font-semibold text-[#0F172A]">Invalid invitation link</h3>
<p className="mt-1 text-sm text-gray-600"> <p className="mt-1 text-sm text-slate-700">
This registration link is invalid or no longer active. Please request a new link. This registration link is invalid or no longer active. Please request a new link.
</p> </p>
{token ? ( {token && (
<p className="mt-2 text-xs text-gray-500"> <p className="mt-2 text-xs text-slate-500">
Token: <span className="font-mono break-all">{token}</span> Token: <span className="font-mono break-all">{token}</span>
</p> </p>
) : null} )}
<div className="mt-4 flex items-center gap-2"> <div className="mt-4 flex flex-wrap items-center gap-2">
<button <button
onClick={onGoHome} 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 Go to homepage
</button> </button>
{onClose && ( {onClose && (
<button <button
onClick={onClose} 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 Close
</button> </button>
@ -55,13 +57,7 @@ export default function InvalidRefLinkModal({
</div> </div>
) )
if (inline) { if (inline) return Content
return (
<div className="w-full flex items-center justify-center py-16">
{Content}
</div>
)
}
return Content return Content
} }

View File

@ -1,20 +1,23 @@
'use client' 'use client'
import { useEffect, useState } from 'react' import { useEffect, useState, type CSSProperties } from 'react'
import { useSearchParams, useRouter } from 'next/navigation' import { useSearchParams, useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore' import useAuthStore from '../store/authStore'
import RegisterForm from './components/RegisterForm' import RegisterForm from './components/RegisterForm'
import PageLayout from '../components/PageLayout' import PageLayout from '../components/PageLayout'
import SessionDetectedModal from './components/SessionDetectedModal' import SessionDetectedModal from './components/SessionDetectedModal'
import InvalidRefLinkModal from './components/invalidRefLinkModal' 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 searchParams = useSearchParams()
const refToken = searchParams.get('ref') const refToken = searchParams.get('ref')
const [registered, setRegistered] = useState(false) const [registered, setRegistered] = useState(false)
const [mode, setMode] = useState<'personal' | 'company'>('personal') const [mode, setMode] = useState<'personal' | 'company'>('personal')
const router = useRouter() const router = useRouter()
const { showToast } = useToast()
// Auth state // Auth state
const user = useAuthStore(state => state.user) const user = useAuthStore(state => state.user)
@ -24,7 +27,7 @@ export default function RegisterPage() {
const [showSessionModal, setShowSessionModal] = useState(false) const [showSessionModal, setShowSessionModal] = useState(false)
const [sessionCleared, setSessionCleared] = useState(false) const [sessionCleared, setSessionCleared] = useState(false)
// NEW: Referral validation state // Referral validation state
const [isRefChecked, setIsRefChecked] = useState(false) const [isRefChecked, setIsRefChecked] = useState(false)
const [invalidRef, setInvalidRef] = useState(false) const [invalidRef, setInvalidRef] = useState(false)
const [refInfo, setRefInfo] = useState<{ const [refInfo, setRefInfo] = useState<{
@ -34,42 +37,43 @@ export default function RegisterPage() {
usesRemaining?: number usesRemaining?: number
} | null>(null) } | null>(null)
// Redirect to login after simulated registration // Redirect after registration
useEffect(() => { useEffect(() => {
if (registered) { if (registered) {
const t = setTimeout(() => router.push('/login'), 4000) // was 1200 const t = setTimeout(() => router.push('/login'), 4000)
return () => clearTimeout(t) return () => clearTimeout(t)
} }
}, [registered, router]) }, [registered, router])
// NEW: Validate referral token (must exist and be valid) // Validate referral token
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
const validateRef = async () => { const validateRef = async () => {
if (!refToken) { if (!refToken) {
console.warn('⚠️ Register: Missing ?ref token in URL')
if (!cancelled) { if (!cancelled) {
setInvalidRef(true) setInvalidRef(true)
setIsRefChecked(true) setIsRefChecked(true)
} }
showToast({
variant: 'error',
title: 'Invitation error',
message: 'No invitation token found in the link.'
})
return return
} }
const base = process.env.NEXT_PUBLIC_API_BASE_URL || '' const base = process.env.NEXT_PUBLIC_API_BASE_URL || ''
const url = `${base}/api/referral/info/${encodeURIComponent(refToken)}` const url = `${base}/api/referral/info/${encodeURIComponent(refToken)}`
console.log('🌐 Register: fetching referral info:', url)
try { try {
const res = await fetch(url, { method: 'GET', credentials: 'include' }) const res = await fetch(url, { method: 'GET', credentials: 'include' })
console.log('📡 Register: referral info status:', res.status)
const body = await res.json().catch(() => null) const body = await res.json().catch(() => null)
console.log('📦 Register: referral info body:', body)
const success = !!body?.success const success = !!body?.success
const isUnlimited = !!body?.isUnlimited 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) const isActive = success && (isUnlimited || usesRemaining > 0)
if (!cancelled) { if (!cancelled) {
@ -81,28 +85,46 @@ export default function RegisterPage() {
usesRemaining usesRemaining
}) })
setInvalidRef(false) setInvalidRef(false)
showToast({
variant: 'success',
title: 'Invitation verified',
message: 'Your invitation link is valid. You can register now.'
})
} else { } else {
console.warn('⛔ Register: referral not active/invalid')
setInvalidRef(true) setInvalidRef(true)
showToast({
variant: 'error',
title: 'Invalid invitation',
message: 'This invitation link is invalid or no longer active.'
})
} }
setIsRefChecked(true) setIsRefChecked(true)
} }
} catch (e) { } catch {
console.error('❌ Register: referral info fetch error:', e)
if (!cancelled) { if (!cancelled) {
setInvalidRef(true) setInvalidRef(true)
setIsRefChecked(true) setIsRefChecked(true)
} }
showToast({
variant: 'error',
title: 'Network error',
message: 'Could not validate the invitation link. Please try again.'
})
} }
} }
validateRef() validateRef()
return () => { cancelled = true } return () => {
}, [refToken]) 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(() => { useEffect(() => {
if (isRefChecked && !invalidRef && user && !sessionCleared) setShowSessionModal(true) if (isRefChecked && !invalidRef && user && !sessionCleared) {
setShowSessionModal(true)
}
}, [isRefChecked, invalidRef, user, sessionCleared]) }, [isRefChecked, invalidRef, user, sessionCleared])
const handleLogout = async () => { const handleLogout = async () => {
@ -116,79 +138,110 @@ export default function RegisterPage() {
router.push('/dashboard') 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) { if (!isRefChecked) {
return ( return (
<ToastProvider> <PageLayout>
<PageLayout> <div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<main className="w-full flex flex-col flex-1 items-center justify-center py-24 min-h-screen"> <Waves
<div className="text-center"> className="pointer-events-none"
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div> lineColor="#0f172a"
<p className="text-slate-700">Checking invitation link</p> 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> </div>
</main> </main>
</PageLayout> </div>
</ToastProvider> </PageLayout>
) )
} }
// NEW: Invalid referral link state — show modal instead of form with same background as register form
if (invalidRef) { if (invalidRef) {
return ( return (
<ToastProvider> <PageLayout>
<PageLayout> <div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<main className="w-full flex flex-col flex-1 gap-10 min-h-screen"> <Waves
{/* make wrapper flex-1 so background reaches the footer */} className="pointer-events-none"
<div className="relative flex-1 overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24"> lineColor="#0f172a"
{/* Pattern */} backgroundColor="rgba(245, 245, 240, 1)"
<svg waveSpeedX={0.02}
aria-hidden="true" waveSpeedY={0.01}
className="absolute inset-0 -z-10 h-full w-full stroke-white/10" waveAmpX={40}
> waveAmpY={20}
<defs> friction={0.9}
<pattern tension={0.01}
id="register-pattern" maxCursorMove={120}
x="50%" xGap={12}
y={-1} yGap={36}
width={200} />
height={200} <main
patternUnits="userSpaceOnUse" className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
> style={mainStyle}
<path >
d="M.5 200V.5H200" <div className="mx-auto max-w-7xl px-6 lg:px-10 flex flex-col w-full">
fill="none"
stroke="rgba(255,255,255,0.05)"
/>
</pattern>
</defs>
<rect
fill="url(#register-pattern)"
width="100%"
height="100%"
strokeWidth={0}
/>
</svg>
{/* Colored blur */}
<div <div
aria-hidden="true" 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"
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" style={{
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 <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" />
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50" <div className="relative flex items-center justify-center">
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%)'
}}
/>
</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">
<InvalidRefLinkModal <InvalidRefLinkModal
inline inline
open open
@ -200,111 +253,97 @@ export default function RegisterPage() {
</div> </div>
</div> </div>
</main> </main>
</PageLayout> </div>
</ToastProvider> </PageLayout>
) )
} }
// normal register
return ( return (
<ToastProvider> <PageLayout>
<PageLayout> <div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<main className="w-full flex flex-col flex-1 gap-10 min-h-screen"> <Waves
{/* Background section wrapper */} className="pointer-events-none"
{/* make wrapper flex-1 so background reaches the footer */} lineColor="#0f172a"
<div className="relative flex-1 overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24"> backgroundColor="rgba(245, 245, 240, 1)"
{/* Pattern */} waveSpeedX={0.02}
<svg waveSpeedY={0.01}
aria-hidden="true" waveAmpX={40}
className="absolute inset-0 -z-10 h-full w-full stroke-white/10" waveAmpY={20}
> friction={0.9}
<defs> tension={0.01}
<pattern maxCursorMove={120}
id="register-pattern" xGap={12}
x="50%" yGap={36}
y={-1} />
width={200} <main
height={200} className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
patternUnits="userSpaceOnUse" style={mainStyle}
> >
<path <div className="mx-auto max-w-7xl px-6 lg:px-10 flex flex-col w-full">
d="M.5 200V.5H200"
fill="none"
stroke="rgba(255,255,255,0.05)"
/>
</pattern>
</defs>
<rect
fill="url(#register-pattern)"
width="100%"
height="100%"
strokeWidth={0}
/>
</svg>
{/* Colored blur */}
<div <div
aria-hidden="true" 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"
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" style={{
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 <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" />
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50" <div className="relative">
style={{ <div className="mx-auto max-w-2xl text-center mb-8">
clipPath: <h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-[#0F172A]">
'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%)' Register now
}} </h1>
/> <p className="mt-3 text-slate-700 text-base sm:text-lg">
</div> Create your personal or company account with Profit Planet.
</p>
</div>
{/* Additional background layers */} <div className="mt-2">
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" /> {showSessionModal ? (
<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="flex items-center justify-center">
<SessionDetectedModal
<div className="mx-auto max-w-7xl px-6 lg:px-8 relative z-10"> inline
{/* Heading (optional adjusted to registration context) */} open
<div className="mx-auto max-w-2xl text-center mb-10"> onLogout={handleLogout}
<h1 className="text-4xl font-semibold tracking-tight text-white sm:text-5xl"> onCancel={handleCancel}
Register now
</h1>
<p className="mt-2 text-lg/8 text-gray-200">
Create your personal or company account with Profit Planet.
</p>
</div>
{/* Content area */}
<div className="flex flex-col flex-1">
{showSessionModal ? (
<div className="flex flex-1 items-center justify-center">
<SessionDetectedModal
inline
open
onLogout={handleLogout}
onCancel={handleCancel}
/>
</div>
) : (
<>
{/* Register form (only if ref valid) */}
{(!user || sessionCleared) && (
<RegisterForm
mode={mode}
setMode={setMode}
refToken={refToken}
onRegistered={() => setRegistered(true)}
referrerEmail={refInfo?.referrerEmail}
/> />
)} </div>
{registered && ( ) : (
<div className="mt-6 mx-auto text-center text-sm text-gray-200"> <>
Registration successful redirecting... {(!user || sessionCleared) && (
</div> <RegisterForm
)} mode={mode}
</> setMode={setMode}
)} refToken={refToken}
onRegistered={() => setRegistered(true)}
referrerEmail={refInfo?.referrerEmail}
/>
)}
{registered && (
<div className="mt-6 mx-auto text-center text-sm text-gray-700">
Registration successful redirecting...
</div>
)}
</>
)}
</div>
</div> </div>
</div> </div>
</div> </div>
</main> </main>
</PageLayout> </div>
</PageLayout>
)
}
// NEW: default export only provides the ToastProvider wrapper
export default function RegisterPage() {
return (
<ToastProvider>
<RegisterPageInner />
</ToastProvider> </ToastProvider>
) )
} }

View File

@ -11,15 +11,16 @@
const ITI_CDN_CSS = const ITI_CDN_CSS =
'https://cdn.jsdelivr.net/npm/intl-tel-input@25.15.0/build/css/intlTelInput.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 = const ITI_CDN_JS =
'https://cdn.jsdelivr.net/npm/intl-tel-input@25.15.0/build/js/intlTelInput.min.js' 'https://cdn.jsdelivr.net/npm/intl-tel-input@25.15.0/build/js/intlTelInputWithUtils.min.js'
const ITI_CDN_UTILS =
'https://cdn.jsdelivr.net/npm/intl-tel-input@25.15.0/build/js/utils.js'
export type IntlTelInputInstance = { export type IntlTelInputInstance = {
destroy: () => void destroy: () => void
getNumber: () => string getNumber: () => string
isValidNumber: () => boolean isValidNumber: () => boolean
setNumber?: (number: string) => void
getValidationError?: () => number getValidationError?: () => number
getSelectedCountryData?: () => { name: string; iso2: string; dialCode: string } getSelectedCountryData?: () => { name: string; iso2: string; dialCode: string }
promise?: Promise<unknown> promise?: Promise<unknown>
@ -27,7 +28,9 @@ export type IntlTelInputInstance = {
declare global { declare global {
interface Window { interface Window {
intlTelInput?: (input: HTMLInputElement, options: any) => IntlTelInputInstance intlTelInput?: ((input: HTMLInputElement, options: any) => IntlTelInputInstance) & {
getInstance?: (input: HTMLInputElement) => IntlTelInputInstance | null
}
} }
} }
@ -70,26 +73,36 @@ async function loadIntlTelInputFromCdn(): Promise<
document.head.appendChild(style) document.head.appendChild(style)
} }
// JS once // JS once (but replace if the existing script points to a different bundle)
if (window.intlTelInput) { if (window.intlTelInput) {
console.log('[phoneUtils] intl-tel-input already loaded on window') console.log('[phoneUtils] intl-tel-input already loaded on window')
return window.intlTelInput 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) => { await new Promise<void>((resolve, reject) => {
const existing = document.querySelector<HTMLScriptElement>( const existing = document.querySelector<HTMLScriptElement>(
'script[data-intl-tel-input-js="true"]' 'script[data-intl-tel-input-js="true"]'
) )
if (existing) { if (existing) {
console.log('[phoneUtils] Reusing existing intl-tel-input <script> tag, waiting for load') const existingSrc = existing.getAttribute('src') || ''
existing.addEventListener('load', () => resolve(), { once: true }) if (existingSrc !== ITI_CDN_JS) {
existing.addEventListener( console.warn('[phoneUtils] Replacing existing intl-tel-input script with different src', {
'error', existingSrc,
() => reject(new Error('Failed to load intl-tel-input')), desiredSrc: ITI_CDN_JS,
{ once: true } })
) existing.remove()
return } else {
console.log('[phoneUtils] Reusing existing intl-tel-input <script> tag, waiting for load')
existing.addEventListener('load', () => resolve(), { once: true })
existing.addEventListener(
'error',
() => reject(new Error('Failed to load intl-tel-input')),
{ once: true }
)
return
}
} }
const script = document.createElement('script') const script = document.createElement('script')
@ -97,22 +110,22 @@ async function loadIntlTelInputFromCdn(): Promise<
script.async = true script.async = true
script.dataset.intlTelInputJs = 'true' script.dataset.intlTelInputJs = 'true'
script.onload = () => { script.onload = () => {
console.log('[phoneUtils] intl-tel-input core script loaded successfully') console.log('[phoneUtils] intl-tel-input script loaded successfully')
resolve() resolve()
} }
script.onerror = () => { 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')) reject(new Error('Failed to load intl-tel-input'))
} }
document.head.appendChild(script) document.head.appendChild(script)
}) })
if (!window.intlTelInput) { 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') 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 return window.intlTelInput
} }
@ -123,7 +136,10 @@ export async function ensureIntlCoreLoaded(): Promise<
(input: HTMLInputElement, options: any) => IntlTelInputInstance (input: HTMLInputElement, options: any) => IntlTelInputInstance
> { > {
if (!intlLoaderPromise) { if (!intlLoaderPromise) {
intlLoaderPromise = loadIntlTelInputFromCdn() intlLoaderPromise = loadIntlTelInputFromCdn().catch(err => {
intlLoaderPromise = null
throw err
})
} }
return intlLoaderPromise return intlLoaderPromise
} }
@ -138,25 +154,26 @@ export async function createIntlTelInput(
): Promise<IntlTelInputInstance> { ): Promise<IntlTelInputInstance> {
const intlTelInput = await ensureIntlCoreLoaded() 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, id: input.id,
name: input.name, 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, { const instance = intlTelInput(input, {
...options, ...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 const anyInst = instance as any