feat: mobile
This commit is contained in:
parent
1045debc32
commit
6dfaedcab6
@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import PageLayout from '../components/PageLayout'
|
||||
import Waves from '../components/waves'
|
||||
import Waves from '../components/background/waves'
|
||||
import BlueBlurryBackground from '../components/background/blueblurry' // NEW
|
||||
import {
|
||||
UsersIcon,
|
||||
ExclamationTriangleIcon,
|
||||
@ -25,12 +26,25 @@ export default function AdminDashboardPage() {
|
||||
const router = useRouter()
|
||||
const { userStats, isAdmin } = useAdminUsers()
|
||||
const [isClient, setIsClient] = useState(false)
|
||||
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
|
||||
// Handle client-side mounting
|
||||
useEffect(() => {
|
||||
setIsClient(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)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Fallback for loading/no data
|
||||
const displayStats = userStats || {
|
||||
totalUsers: 0,
|
||||
@ -83,96 +97,76 @@ export default function AdminDashboardPage() {
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div
|
||||
className="relative w-full flex flex-col min-h-screen overflow-hidden"
|
||||
style={{ backgroundImage: 'none', background: 'none' }}
|
||||
>
|
||||
<Waves
|
||||
className="pointer-events-none"
|
||||
lineColor="#0f172a"
|
||||
backgroundColor="rgba(245, 245, 240, 1)"
|
||||
waveSpeedX={0.02}
|
||||
waveSpeedY={0.01}
|
||||
waveAmpX={40}
|
||||
waveAmpY={20}
|
||||
friction={0.9}
|
||||
tension={0.01}
|
||||
maxCursorMove={120}
|
||||
xGap={12}
|
||||
yGap={36}
|
||||
/>
|
||||
const content = (
|
||||
<div className="relative z-10 min-h-screen flex flex-col">
|
||||
<main className="flex-1 max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8 w-full">
|
||||
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-6 sm:p-8">
|
||||
{/* Header */}
|
||||
<header className="flex flex-col gap-4 mb-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Admin Dashboard</h1>
|
||||
<p className="text-lg text-blue-700 mt-2">
|
||||
Manage all administrative features, user management, permissions, and global settings.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="relative z-10 min-h-screen flex flex-col">
|
||||
<main className="flex-1 max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8 w-full">
|
||||
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-6 sm:p-8">
|
||||
{/* Header */}
|
||||
<header className="flex flex-col gap-4 mb-8">
|
||||
{/* 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>
|
||||
</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>
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Admin Dashboard</h1>
|
||||
<p className="text-lg text-blue-700 mt-2">
|
||||
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.
|
||||
<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>
|
||||
|
||||
{/* 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">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Matrix Management */}
|
||||
<button
|
||||
type="button"
|
||||
@ -324,82 +318,109 @@ export default function AdminDashboardPage() {
|
||||
}`}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
{/* Divider */}
|
||||
<div className="hidden lg:block border-l border-gray-200" />
|
||||
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
{/* 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>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{isMobile ? (
|
||||
<BlueBlurryBackground>{content}</BlueBlurryBackground>
|
||||
) : (
|
||||
<div
|
||||
className="relative w-full flex flex-col min-h-screen overflow-hidden"
|
||||
style={{ backgroundImage: 'none', background: 'none' }}
|
||||
>
|
||||
<Waves
|
||||
className="pointer-events-none"
|
||||
lineColor="#0f172a"
|
||||
backgroundColor="rgba(245, 245, 240, 1)"
|
||||
waveSpeedX={0.02}
|
||||
waveSpeedY={0.01}
|
||||
waveAmpX={40}
|
||||
waveAmpY={20}
|
||||
friction={0.9}
|
||||
tension={0.01}
|
||||
maxCursorMove={120}
|
||||
xGap={12}
|
||||
yGap={36}
|
||||
/>
|
||||
{content}
|
||||
</div>
|
||||
)}
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import PageLayout from '../components/PageLayout'
|
||||
import Waves from '../components/waves'
|
||||
import Waves from '../components/background/waves'
|
||||
|
||||
type Affiliate = {
|
||||
id: string
|
||||
|
||||
@ -1,109 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
// Utility to detect mobile devices
|
||||
function isMobileDevice() {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
return /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent
|
||||
);
|
||||
}
|
||||
|
||||
function GlobalAnimatedBackground() {
|
||||
// Always use dashboard style for a uniform look
|
||||
const bgGradient = "linear-gradient(135deg, #1e293b 0%, #334155 100%)";
|
||||
|
||||
// Detect small screens (mobile/tablet)
|
||||
const isMobile = isMobileDevice();
|
||||
|
||||
if (isMobile) {
|
||||
// Render only the static background gradient and overlay, no animation
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 0,
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
background: bgGradient,
|
||||
transition: "background 0.5s",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-900 via-blue-600 to-blue-400 opacity-80"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
inset: 0,
|
||||
zIndex: 0,
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
background: bgGradient,
|
||||
transition: "background 0.5s",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Overlays */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-900 via-blue-600 to-blue-400 opacity-80"></div>
|
||||
<div className="absolute top-10 left-10 w-64 h-1 bg-blue-300 opacity-50 animate-slide-loop"></div>
|
||||
<div className="absolute bottom-20 right-20 w-48 h-1 bg-blue-200 opacity-40 animate-slide-loop"></div>
|
||||
<div className="absolute top-1/3 left-1/4 w-72 h-1 bg-blue-400 opacity-30 animate-slide-loop"></div>
|
||||
<div className="absolute top-16 left-1/3 w-32 h-32 bg-blue-500 rounded-full opacity-50 animate-float"></div>
|
||||
<div className="absolute bottom-24 right-1/4 w-40 h-40 bg-blue-600 rounded-full opacity-40 animate-float"></div>
|
||||
<div className="absolute top-1/2 left-1/2 w-24 h-24 bg-blue-700 rounded-full opacity-30 animate-float"></div>
|
||||
<div className="absolute top-1/4 left-1/5 w-20 h-20 bg-blue-300 rounded-lg opacity-40 animate-float-slow"></div>
|
||||
<div className="absolute bottom-1/3 right-1/3 w-28 h-28 bg-blue-400 rounded-lg opacity-30 animate-float-slow"></div>
|
||||
<style>
|
||||
{`
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
@keyframes float-slow {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
@keyframes slide-loop {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100vw);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
.animate-float-slow {
|
||||
animation: float-slow 8s ease-in-out infinite;
|
||||
}
|
||||
.animate-slide-loop {
|
||||
animation: slide-loop 12s linear infinite;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GlobalAnimatedBackground;
|
||||
@ -84,7 +84,7 @@ export default function TutorialModal({
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10">
|
||||
<div className="flex min-h-full items-center justify-center p-4">
|
||||
<div className="flex min-h-full items-center justify-center p-3 sm:p-4">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
@ -94,8 +94,9 @@ export default function TutorialModal({
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative w-full max-w-5xl h-[60vh]">
|
||||
<div className="relative isolate overflow-hidden bg-slate-50 h-full after:pointer-events-none after:absolute after:inset-0 after:inset-ring after:inset-ring-gray-200/50 sm:rounded-3xl after:sm:rounded-3xl lg:flex lg:gap-x-12 lg:px-8 w-full">
|
||||
{/* CHANGED: mobile uses max-height + scrolling */}
|
||||
<Dialog.Panel className="relative w-full max-w-5xl max-h-[88dvh] sm:h-[60vh]">
|
||||
<div className="relative isolate h-full overflow-hidden rounded-2xl bg-slate-50 after:pointer-events-none after:absolute after:inset-0 after:inset-ring after:inset-ring-gray-200/50 sm:rounded-3xl after:sm:rounded-3xl lg:flex lg:gap-x-12 lg:px-8 w-full">
|
||||
{/* Background Gradient */}
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
@ -121,20 +122,20 @@ export default function TutorialModal({
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{/* Content Section - Left Half */}
|
||||
<div className="lg:flex-1 lg:max-w-md text-center lg:text-left py-8 px-6 flex flex-col justify-center">
|
||||
{/* CHANGED: content scrolls on mobile */}
|
||||
<div className="lg:flex-1 lg:max-w-md text-center lg:text-left px-5 py-6 sm:px-6 sm:py-8 flex flex-col justify-start overflow-y-auto">
|
||||
{/* Icon */}
|
||||
<div className="mx-auto lg:mx-0 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 mb-4 ring-2 ring-blue-200">
|
||||
<step.icon className="h-6 w-6 text-blue-600" aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h2 className="text-xl font-semibold tracking-tight whitespace-nowrap text-gray-800 sm:text-2xl">
|
||||
{/* CHANGED: allow wrapping on mobile (remove nowrap) */}
|
||||
<h2 className="text-xl font-semibold tracking-tight text-gray-800 sm:text-2xl">
|
||||
{step.title}
|
||||
</h2>
|
||||
|
||||
{/* Description */}
|
||||
<p className="mt-3 text-sm text-gray-600 leading-relaxed h-12 overflow-hidden">
|
||||
{/* CHANGED: no fixed height clipping on mobile */}
|
||||
<p className="mt-3 text-sm text-gray-600 leading-relaxed sm:h-12 sm:overflow-hidden">
|
||||
{step.description}
|
||||
</p>
|
||||
|
||||
@ -209,8 +210,8 @@ export default function TutorialModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Visual Section - Right Half */}
|
||||
<div className="relative lg:flex-1 mt-4 lg:mt-0 h-32 lg:h-full lg:min-h-[150px] flex items-end justify-end">
|
||||
{/* CHANGED: hide unused visual section on mobile */}
|
||||
<div className="relative hidden lg:flex lg:flex-1 mt-4 lg:mt-0 h-32 lg:h-full lg:min-h-[150px] items-end justify-end">
|
||||
{/* <img
|
||||
src="/images/misc/cow.png"
|
||||
alt="Profit Planet Mascot"
|
||||
|
||||
35
src/app/components/background/blueblurry.tsx
Normal file
35
src/app/components/background/blueblurry.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
contentClassName?: string
|
||||
/** disables blob animations (useful for mobile/perf) */
|
||||
animate?: boolean
|
||||
}
|
||||
|
||||
export default function BlueBlurryBackground({
|
||||
children,
|
||||
className = '',
|
||||
contentClassName = '',
|
||||
animate = true,
|
||||
}: Props) {
|
||||
const pulse = animate ? 'animate-pulse' : ''
|
||||
|
||||
return (
|
||||
<div className={`relative min-h-[100dvh] overflow-x-hidden bg-slate-50 ${className}`}>
|
||||
{/* background layer (FIXED so it never gets clipped by inner layout containers) */}
|
||||
<div className="pointer-events-none fixed inset-0 z-0" aria-hidden>
|
||||
<div className={`absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl ${pulse}`} />
|
||||
<div className={`absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl ${pulse}`} />
|
||||
<div className={`absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl ${pulse}`} />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
|
||||
</div>
|
||||
|
||||
{/* content layer */}
|
||||
<div className={`relative z-10 min-h-[100dvh] ${contentClassName}`}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -9,10 +9,7 @@ import {
|
||||
Disclosure,
|
||||
DisclosureButton,
|
||||
DisclosurePanel,
|
||||
Popover,
|
||||
PopoverButton,
|
||||
PopoverGroup,
|
||||
PopoverPanel,
|
||||
Transition,
|
||||
} from '@headlessui/react'
|
||||
import {
|
||||
@ -99,22 +96,17 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
||||
console.error('Logout failed:', err)
|
||||
setGlobalLoggingOut?.(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to get user initials for profile icon
|
||||
const getUserInitials = () => {
|
||||
if (!user) return 'U';
|
||||
if (!user) return 'U'
|
||||
if (user.firstName || user.lastName) {
|
||||
return (
|
||||
(user.firstName?.[0] || '') +
|
||||
(user.lastName?.[0] || '')
|
||||
).toUpperCase();
|
||||
return ((user.firstName?.[0] || '') + (user.lastName?.[0] || '')).toUpperCase()
|
||||
}
|
||||
if (user.email) {
|
||||
return user.email[0].toUpperCase();
|
||||
}
|
||||
return 'U';
|
||||
};
|
||||
if (user.email) return user.email[0].toUpperCase()
|
||||
return 'U'
|
||||
}
|
||||
|
||||
// Initial theme (dark/light) + mark mounted + start header animation
|
||||
useEffect(() => {
|
||||
@ -507,71 +499,18 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
||||
{/* Information dropdown already removed here */}
|
||||
</PopoverGroup>
|
||||
|
||||
<div className="hidden lg:flex lg:flex-1 lg:justify-end lg:items-center lg:gap-x-4">
|
||||
{/* Auth slot */}
|
||||
<div className="flex items-center">
|
||||
{userPresent ? (
|
||||
<Popover className="relative">
|
||||
<PopoverButton className="flex items-center gap-x-1 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<Avatar
|
||||
src=""
|
||||
initials={(() => {
|
||||
if (!user) return 'U'
|
||||
if (user.firstName || user.lastName) {
|
||||
return ((user.firstName?.[0] || '') + (user.lastName?.[0] || '')).toUpperCase()
|
||||
}
|
||||
return user.email ? user.email[0].toUpperCase() : 'U'
|
||||
})()}
|
||||
className="size-8 bg-gradient-to-br from-indigo-500/40 to-indigo-600/60 text-white"
|
||||
/>
|
||||
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none text-gray-500" />
|
||||
</PopoverButton>
|
||||
<PopoverPanel
|
||||
transition
|
||||
className="absolute left-0 top-full mt-2 w-64 rounded-md bg-white dark:bg-gray-900 ring-1 ring-black/10 dark:ring-white/10 shadow-lg data-closed:-translate-y-1 data-closed:opacity-0 data-enter:duration-200 data-leave:duration-150"
|
||||
>
|
||||
<div className="p-4">
|
||||
<div className="flex flex-col border-b border-gray-200 dark:border-white/10 pb-4 mb-4">
|
||||
<div className="font-medium text-gray-900 dark:text-white">
|
||||
{user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : (user?.email || 'User')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{user?.email || 'user@example.com'}
|
||||
</div>
|
||||
</div>
|
||||
{canSeeDashboard && (
|
||||
<button
|
||||
onClick={() => router.push('/dashboard')}
|
||||
className="flex items-center gap-x-2 w-full text-left p-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-white/5 rounded-md"
|
||||
>
|
||||
<Bars3Icon className="size-5 text-gray-400" />
|
||||
Dashboard
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => router.push('/profile')}
|
||||
className="flex items-center gap-x-2 w-full text-left p-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-white/5 rounded-md"
|
||||
>
|
||||
<UserCircleIcon className="size-5 text-gray-400" />
|
||||
Profile
|
||||
</button>
|
||||
{/* Logout removed from profile dropdown; still available in hamburger menu bottom */}
|
||||
</div>
|
||||
</PopoverPanel>
|
||||
</Popover>
|
||||
) : mounted ? (
|
||||
<button
|
||||
onClick={() => router.push('/login')}
|
||||
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||
>
|
||||
Log in <span aria-hidden="true">→</span>
|
||||
</button>
|
||||
) : (
|
||||
<div aria-hidden="true" className="w-20 h-8 rounded-md bg-gray-200 dark:bg-gray-700/70 animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
{/* CHANGED: remove profile icon/popover from header; keep login (when logged out) + hamburger */}
|
||||
<div className="hidden lg:flex lg:flex-1 lg:justify-end lg:items-center lg:gap-x-3">
|
||||
{!userPresent && mounted && (
|
||||
<button
|
||||
onClick={() => router.push('/login')}
|
||||
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||
>
|
||||
Log in <span aria-hidden="true">→</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Desktop hamburger (right side, next to login/profile) */}
|
||||
{/* Desktop hamburger (right side) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileMenuOpen(open => !open)}
|
||||
@ -789,16 +728,37 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
||||
</div>
|
||||
) : user ? (
|
||||
<>
|
||||
{/* User info + basic nav */}
|
||||
{/* CHANGED: include profile icon INSIDE hamburger menu */}
|
||||
<div className="pt-6 space-y-2">
|
||||
<div className="flex flex-col border-b border-white/10 pb-4 mb-4 px-3">
|
||||
<div className="font-medium text-white">
|
||||
{user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : (user?.email || 'User')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{user?.email || 'user@example.com'}
|
||||
<div className="flex items-center gap-3 border-b border-white/10 pb-4 mb-4 px-3">
|
||||
<Avatar
|
||||
src=""
|
||||
initials={getUserInitials()}
|
||||
className="size-10 bg-gradient-to-br from-indigo-500/40 to-indigo-600/60 text-white"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<div className="font-medium text-white truncate">
|
||||
{user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : (user?.email || 'User')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400 truncate">
|
||||
{user?.email || 'user@example.com'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* NEW: show Quick Action Dashboard link when user cannot access /dashboard yet */}
|
||||
{!canSeeDashboard && (
|
||||
<button
|
||||
onClick={() => {
|
||||
router.push('/quickaction-dashboard')
|
||||
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"
|
||||
>
|
||||
Startup Dashboard
|
||||
</button>
|
||||
)}
|
||||
|
||||
{canSeeDashboard && (
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@ -182,13 +182,29 @@ function ToastViewport() {
|
||||
if (!toasts.length) return null
|
||||
|
||||
return (
|
||||
<div className="pointer-events-none fixed inset-x-4 bottom-4 z-50 flex justify-end sm:inset-x-auto sm:right-4 sm:w-auto">
|
||||
<div className="flex max-h-[80vh] w-full flex-col gap-3 overflow-hidden sm:w-80">
|
||||
{toasts.map(t => (
|
||||
<ToastItem key={t.id} toast={t} onClose={() => handleClose(t.id)} />
|
||||
))}
|
||||
<>
|
||||
<div
|
||||
className="pp-toast-viewport pointer-events-none fixed inset-x-4 z-50 flex justify-end sm:inset-x-auto sm:right-4 sm:w-auto"
|
||||
style={{ bottom: 'calc(env(safe-area-inset-bottom, 0px) + var(--pp-toast-bottom, 5rem))' }}
|
||||
>
|
||||
<div className="flex max-h-[80vh] w-full flex-col gap-3 overflow-hidden sm:w-80">
|
||||
{toasts.map(t => (
|
||||
<ToastItem key={t.id} toast={t} onClose={() => handleClose(t.id)} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx global>{`
|
||||
.pp-toast-viewport {
|
||||
--pp-toast-bottom: 5rem; /* desktop/tablet */
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.pp-toast-viewport {
|
||||
--pp-toast-bottom: 7rem; /* mobile: lift higher above footer */
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useState, useCallback, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import useAuthStore from '../store/authStore'
|
||||
import PageLayout from '../components/PageLayout'
|
||||
import Waves from '../components/waves'
|
||||
import Waves from '../components/background/waves'
|
||||
import BlueBlurryBackground from '../components/background/blueblurry'
|
||||
import { useUserStatus } from '../hooks/useUserStatus'
|
||||
import {
|
||||
ShoppingBagIcon,
|
||||
UsersIcon,
|
||||
@ -21,6 +23,18 @@ export default function DashboardPage() {
|
||||
const isShopEnabled = process.env.NEXT_PUBLIC_SHOW_SHOP !== 'false'
|
||||
const [isMobile, setIsMobile] = useState(false)
|
||||
|
||||
const { userStatus, loading: statusLoading } = useUserStatus()
|
||||
|
||||
// NEW: smooth redirect helper
|
||||
const [redirectTo, setRedirectTo] = useState<string | null>(null)
|
||||
const redirectOnceRef = useRef(false)
|
||||
const smoothReplace = useCallback((to: string) => {
|
||||
if (redirectOnceRef.current) return
|
||||
redirectOnceRef.current = true
|
||||
setRedirectTo(to)
|
||||
window.setTimeout(() => router.replace(to), 200)
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
const mq = window.matchMedia('(max-width: 768px)')
|
||||
const apply = () => setIsMobile(mq.matches)
|
||||
@ -39,9 +53,23 @@ export default function DashboardPage() {
|
||||
router.push('/login')
|
||||
}
|
||||
}, [isAuthReady, user, router])
|
||||
|
||||
// Show loading until auth is ready or user is confirmed
|
||||
if (!isAuthReady || !user) {
|
||||
|
||||
// NEW: block dashboard unless all 4 quickaction steps are completed
|
||||
useEffect(() => {
|
||||
if (!isAuthReady || !user) return
|
||||
if (statusLoading || !userStatus) return
|
||||
|
||||
const allDone =
|
||||
!!userStatus.email_verified &&
|
||||
!!userStatus.documents_uploaded &&
|
||||
!!userStatus.profile_completed &&
|
||||
!!userStatus.contract_signed
|
||||
|
||||
if (!allDone) smoothReplace('/quickaction-dashboard')
|
||||
}, [isAuthReady, user, statusLoading, userStatus, smoothReplace])
|
||||
|
||||
// Show loading until auth is ready, user is confirmed, and (if logged in) status is loaded
|
||||
if (!isAuthReady || !user || (statusLoading && user)) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
@ -52,6 +80,27 @@ export default function DashboardPage() {
|
||||
)
|
||||
}
|
||||
|
||||
// If redirecting away, avoid rendering dashboard content
|
||||
if (redirectTo) {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
||||
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||
<div className="text-sm font-medium text-gray-900">Redirecting…</div>
|
||||
<div className="mt-1 text-xs text-gray-600">Please wait</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// NEW: final guard (don’t render dashboard if not all done)
|
||||
if (!userStatus) return null
|
||||
const allDone =
|
||||
!!userStatus.email_verified &&
|
||||
!!userStatus.documents_uploaded &&
|
||||
!!userStatus.profile_completed &&
|
||||
!!userStatus.contract_signed
|
||||
if (!allDone) return null
|
||||
|
||||
// Get user name
|
||||
const getUserName = () => {
|
||||
if (user.firstName && user.lastName) {
|
||||
@ -127,7 +176,180 @@ export default function DashboardPage() {
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
const content = (
|
||||
<div className="relative z-10 flex-1 min-h-0">
|
||||
<PageLayout className="bg-transparent text-gray-900">
|
||||
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-8">
|
||||
{/* Welcome Section */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
Welcome back, {getUserName()}! 👋
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Here's what's happening with your Profit Planet account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* News Section (replaces Account setup + Stats Grid) */}
|
||||
<div className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Latest News & Articles</h2>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{news.map(item => (
|
||||
<article key={item.id} className="group relative overflow-hidden rounded-xl bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
{/* Image/placeholder */}
|
||||
<div className="aspect-[16/9] w-full bg-gradient-to-br from-gray-100 to-gray-200" />
|
||||
<div className="p-5">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<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">
|
||||
{item.category}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">{new Date(item.date).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-gray-900 group-hover:text-[#8D6B1D] transition-colors">
|
||||
<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>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="mb-8">
|
||||
<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">
|
||||
{quickActions.map((action, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => {
|
||||
if (!action.disabled) {
|
||||
router.push(action.href)
|
||||
}
|
||||
}}
|
||||
disabled={Boolean(action.disabled)}
|
||||
className={`bg-white rounded-lg p-6 border border-gray-200 text-left group transition-all duration-200 ${
|
||||
action.disabled
|
||||
? 'opacity-60 cursor-not-allowed'
|
||||
: 'shadow-sm hover:shadow-lg hover:-translate-y-1 hover:-translate-y-1 hover:-translate-y-1 transform hover:-translate-y-1'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<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>
|
||||
</div>
|
||||
</main>
|
||||
</PageLayout>
|
||||
</div>
|
||||
)
|
||||
|
||||
return isMobile ? (
|
||||
<BlueBlurryBackground>{content}</BlueBlurryBackground>
|
||||
) : (
|
||||
<div
|
||||
className="relative w-full min-h-[100dvh] flex flex-col overflow-x-hidden"
|
||||
style={{ backgroundImage: 'none', background: 'none' }}
|
||||
@ -145,178 +367,10 @@ export default function DashboardPage() {
|
||||
maxCursorMove={120}
|
||||
xGap={12}
|
||||
yGap={36}
|
||||
animate={!isMobile}
|
||||
interactive={!isMobile}
|
||||
animate
|
||||
interactive
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex-1 min-h-0">
|
||||
<PageLayout className="bg-transparent text-gray-900">
|
||||
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-8">
|
||||
{/* Welcome Section */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
Welcome back, {getUserName()}! 👋
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Here's what's happening with your Profit Planet account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* News Section (replaces Account setup + Stats Grid) */}
|
||||
<div className="mb-10">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-4">Latest News & Articles</h2>
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{news.map(item => (
|
||||
<article key={item.id} className="group relative overflow-hidden rounded-xl bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
|
||||
{/* Image/placeholder */}
|
||||
<div className="aspect-[16/9] w-full bg-gradient-to-br from-gray-100 to-gray-200" />
|
||||
<div className="p-5">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<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">
|
||||
{item.category}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">{new Date(item.date).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-gray-900 group-hover:text-[#8D6B1D] transition-colors">
|
||||
<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>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="mb-8">
|
||||
<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">
|
||||
{quickActions.map((action, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => {
|
||||
if (!action.disabled) {
|
||||
router.push(action.href)
|
||||
}
|
||||
}}
|
||||
disabled={Boolean(action.disabled)}
|
||||
className={`bg-white rounded-lg p-6 border border-gray-200 text-left group transition-all duration-200 ${
|
||||
action.disabled
|
||||
? 'opacity-60 cursor-not-allowed'
|
||||
: 'shadow-sm hover:shadow-lg hover:-translate-y-1 hover:-translate-y-1 hover:-translate-y-1 transform hover:-translate-y-1'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<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>
|
||||
</div>
|
||||
</main>
|
||||
</PageLayout>
|
||||
</div>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -7,7 +7,8 @@ import PageLayout from '../components/PageLayout'
|
||||
import useAuthStore from '../store/authStore'
|
||||
import { ToastProvider } from '../components/toast/toastComponent'
|
||||
import PageTransitionEffect from '../components/animation/pageTransitionEffect'
|
||||
import Waves from '../components/waves'
|
||||
import Waves from '../components/background/waves'
|
||||
import BlueBlurryBackground from '../components/background/blueblurry' // NEW
|
||||
import CurvedLoop from '../components/curvedLoop'
|
||||
|
||||
export default function LoginPage() {
|
||||
@ -58,78 +59,82 @@ export default function LoginPage() {
|
||||
)
|
||||
}
|
||||
|
||||
const content = (
|
||||
<PageLayout showFooter={true} className="bg-transparent text-gray-900">
|
||||
{/* ...existing code... */}
|
||||
<div
|
||||
className={`relative z-10 w-full flex flex-col flex-1 min-h-0 ${isMobile ? 'overflow-y-hidden' : ''}`}
|
||||
style={{ backgroundImage: 'none', background: 'none' }}
|
||||
>
|
||||
{/* ...existing code... */}
|
||||
{isMobile ? (
|
||||
// ...existing code...
|
||||
<div
|
||||
className="relative z-10 flex-1 min-h-0 grid place-items-center px-3"
|
||||
style={{ paddingTop: '6rem', paddingBottom: '50%' }}
|
||||
>
|
||||
<div
|
||||
className="w-full"
|
||||
style={{
|
||||
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>
|
||||
{/* ...existing code... */}
|
||||
</PageLayout>
|
||||
)
|
||||
|
||||
return (
|
||||
<PageTransitionEffect>
|
||||
<ToastProvider>
|
||||
{/* NEW: page-level background wrapper so Waves covers everything */}
|
||||
<div className="relative min-h-screen w-full overflow-x-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
|
||||
<Waves
|
||||
className="pointer-events-none"
|
||||
lineColor="#0f172a"
|
||||
backgroundColor="rgba(245, 245, 240, 1)"
|
||||
waveSpeedX={0.02}
|
||||
waveSpeedY={0.01}
|
||||
waveAmpX={40}
|
||||
waveAmpY={20}
|
||||
friction={0.9}
|
||||
tension={0.01}
|
||||
maxCursorMove={120}
|
||||
xGap={12}
|
||||
yGap={36}
|
||||
animate={!isMobile}
|
||||
interactive={!isMobile}
|
||||
/>
|
||||
|
||||
<PageLayout showFooter={true} className="bg-transparent text-gray-900">
|
||||
{/* ...existing code... */}
|
||||
<div
|
||||
className={`relative z-10 w-full flex flex-col flex-1 min-h-0 ${
|
||||
isMobile ? 'overflow-y-hidden' : ''
|
||||
}`}
|
||||
style={{ backgroundImage: 'none', background: 'none' }}
|
||||
>
|
||||
{/* REMOVED: Waves background moved to wrapper */}
|
||||
|
||||
{isMobile ? (
|
||||
// ...existing code...
|
||||
<div
|
||||
className="relative z-10 flex-1 min-h-0 grid place-items-center px-3"
|
||||
style={{ paddingTop: '6rem', paddingBottom: '0.5rem' }}
|
||||
>
|
||||
<div
|
||||
className="w-full"
|
||||
style={{
|
||||
// push a bit down (visual centering with header + footer)
|
||||
transform: 'translateY(clamp(10px, 2vh, 28px))',
|
||||
}}
|
||||
>
|
||||
<LoginForm />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// ...existing code...
|
||||
<div
|
||||
className="relative z-10 flex-1 min-h-0 flex flex-col justify-between"
|
||||
style={{ paddingTop: '0.75rem', paddingBottom: '1rem' }}
|
||||
>
|
||||
<div className="w-full px-4 sm:px-0">
|
||||
<CurvedLoop
|
||||
marqueeText="Welcome to profit planet ✦"
|
||||
speed={1}
|
||||
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>
|
||||
{/* ...existing code... */}
|
||||
</PageLayout>
|
||||
</div>
|
||||
{isMobile ? (
|
||||
<BlueBlurryBackground animate={false} className="w-full">
|
||||
{content}
|
||||
</BlueBlurryBackground>
|
||||
) : (
|
||||
<div className="relative min-h-screen w-full overflow-x-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
|
||||
<Waves
|
||||
className="pointer-events-none"
|
||||
lineColor="#0f172a"
|
||||
backgroundColor="rgba(245, 245, 240, 1)"
|
||||
waveSpeedX={0.02}
|
||||
waveSpeedY={0.01}
|
||||
waveAmpX={40}
|
||||
waveAmpY={20}
|
||||
friction={0.9}
|
||||
tension={0.01}
|
||||
maxCursorMove={120}
|
||||
xGap={12}
|
||||
yGap={36}
|
||||
animate={!isMobile}
|
||||
interactive={!isMobile}
|
||||
/>
|
||||
{content}
|
||||
</div>
|
||||
)}
|
||||
</ToastProvider>
|
||||
</PageTransitionEffect>
|
||||
)
|
||||
|
||||
@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
|
||||
import { gsap } from 'gsap';
|
||||
import PageLayout from './components/PageLayout';
|
||||
import Crosshair from './components/Crosshair';
|
||||
import Waves from './components/waves';
|
||||
import Waves from './components/background/waves';
|
||||
import SplitText from './components/SplitText';
|
||||
|
||||
export default function HomePage() {
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import PageLayout from '../components/PageLayout'
|
||||
import Waves from '../components/waves'
|
||||
import Waves from '../components/background/waves'
|
||||
import { ToastProvider, useToast } from '../components/toast/toastComponent'
|
||||
|
||||
function PasswordResetPageInner() {
|
||||
|
||||
@ -4,7 +4,7 @@ import React, { useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import useAuthStore from '../store/authStore'
|
||||
import PageLayout from '../components/PageLayout'
|
||||
import Waves from '../components/waves'
|
||||
import BlueBlurryBackground from '../components/background/blueblurry'
|
||||
import ProfileCompletion from './components/profileCompletion'
|
||||
import BasicInformation from './components/basicInformation'
|
||||
import MediaSection from './components/mediaSection'
|
||||
@ -70,7 +70,6 @@ export default function ProfilePage() {
|
||||
const user = useAuthStore(state => state.user)
|
||||
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)
|
||||
|
||||
// --- declare ALL hooks before any early return (Rules of Hooks) ---
|
||||
@ -222,18 +221,6 @@ export default function ProfilePage() {
|
||||
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 (
|
||||
@ -247,149 +234,130 @@ export default function ProfilePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative w-full min-h-screen overflow-x-hidden">
|
||||
<Waves
|
||||
className="pointer-events-none"
|
||||
lineColor="#0f172a"
|
||||
backgroundColor="rgba(245, 245, 240, 1)"
|
||||
waveSpeedX={0.02}
|
||||
waveSpeedY={0.01}
|
||||
waveAmpX={40}
|
||||
waveAmpY={20}
|
||||
friction={0.9}
|
||||
tension={0.01}
|
||||
maxCursorMove={120}
|
||||
xGap={12}
|
||||
yGap={36}
|
||||
animate={!isMobile}
|
||||
interactive={!isMobile}
|
||||
/>
|
||||
<PageLayout className="bg-transparent text-gray-900">
|
||||
<BlueBlurryBackground>
|
||||
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* MASTER GLASS PANEL (prevents non-translucent gaps between cards) */}
|
||||
<div className="rounded-3xl bg-white/60 backdrop-blur-md border border-white/50 shadow-xl p-4 sm:p-6 lg:p-8">
|
||||
{/* Page Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Profile Settings</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage your account information and preferences
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 min-h-screen">
|
||||
<PageLayout className="bg-transparent text-gray-900">
|
||||
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* MASTER GLASS PANEL (prevents non-translucent gaps between cards) */}
|
||||
<div className="rounded-3xl bg-white/60 backdrop-blur-md border border-white/50 shadow-xl p-4 sm:p-6 lg:p-8">
|
||||
{/* Page Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Profile Settings</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage your account information and preferences
|
||||
</p>
|
||||
{/* Pending admin verification notice (above progress) */}
|
||||
{profileDataApi?.userStatus && profileDataApi.userStatus.is_admin_verified === 0 && (
|
||||
<div className="rounded-md bg-yellow-50/80 backdrop-blur border border-yellow-200 p-3 text-sm text-yellow-800 mb-2">
|
||||
Your account is fully submitted. Our team will verify your account shortly.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pending admin verification notice (above progress) */}
|
||||
{profileDataApi?.userStatus && profileDataApi.userStatus.is_admin_verified === 0 && (
|
||||
<div className="rounded-md bg-yellow-50/80 backdrop-blur border border-yellow-200 p-3 text-sm text-yellow-800 mb-2">
|
||||
Your account is fully submitted. Our team will verify your account shortly.
|
||||
</div>
|
||||
)}
|
||||
<ProfileCompletion profileComplete={profileData.profileComplete} />
|
||||
|
||||
<ProfileCompletion profileComplete={profileData.profileComplete} />
|
||||
|
||||
{/* Basic Info + Sidebar */}
|
||||
<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>
|
||||
{/* Sidebar: Account Status + Quick Actions */}
|
||||
<div className="space-y-6">
|
||||
{/* Account Status (make translucent) */}
|
||||
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Account Status</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<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>
|
||||
|
||||
{/* Bank Info, Media */}
|
||||
<div className="space-y-6 sm:space-y-8 mb-8">
|
||||
{/* --- My Abo Section (above bank info) --- */}
|
||||
<UserAbo />
|
||||
{/* --- Edit Bank Information Section --- */}
|
||||
<BankInformation
|
||||
{/* Basic Info + Sidebar */}
|
||||
<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}
|
||||
editingBank={false} // force read-only
|
||||
bankDraft={bankDraft}
|
||||
setEditingBank={setEditingBank}
|
||||
setBankDraft={setBankDraft}
|
||||
setBankInfo={setBankInfo}
|
||||
// onEdit disabled for now
|
||||
// onEdit={() => openEditModal('bank', { ... })}
|
||||
HighlightIfMissing={HighlightIfMissing}
|
||||
// Add edit button handler
|
||||
onEdit={() => openEditModal('basic', {
|
||||
firstName: profileData.firstName,
|
||||
lastName: profileData.lastName,
|
||||
phone: profileData.phone,
|
||||
address: profileData.address,
|
||||
})}
|
||||
/>
|
||||
{/* --- Media Section --- */}
|
||||
<MediaSection documents={documents} />
|
||||
</div>
|
||||
{/* Sidebar: Account Status + Quick Actions */}
|
||||
<div className="space-y-6">
|
||||
{/* Account Status (make translucent) */}
|
||||
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
|
||||
<h3 className="font-semibold text-gray-900 mb-4">Account Status</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<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>
|
||||
</main>
|
||||
|
||||
{/* 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>
|
||||
</PageLayout>
|
||||
</div>
|
||||
</div>
|
||||
{/* Bank Info, Media */}
|
||||
<div className="space-y-6 sm:space-y-8 mb-8">
|
||||
{/* --- My Abo Section (above bank info) --- */}
|
||||
<UserAbo />
|
||||
{/* --- Edit Bank Information Section --- */}
|
||||
<BankInformation
|
||||
profileData={profileData}
|
||||
editingBank={false} // force read-only
|
||||
bankDraft={bankDraft}
|
||||
setEditingBank={setEditingBank}
|
||||
setBankDraft={setBankDraft}
|
||||
setBankInfo={setBankInfo}
|
||||
// onEdit disabled for now
|
||||
// onEdit={() => openEditModal('bank', { ... })}
|
||||
/>
|
||||
{/* --- Media Section --- */}
|
||||
<MediaSection documents={documents} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* 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>
|
||||
</BlueBlurryBackground>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import PageLayout from '../components/PageLayout'
|
||||
import TutorialModal, { createTutorialSteps } from '../components/TutorialModal'
|
||||
import useAuthStore from '../store/authStore'
|
||||
import { useUserStatus } from '../hooks/useUserStatus'
|
||||
import BlueBlurryBackground from '../components/background/blueblurry' // NEW
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
XCircleIcon,
|
||||
@ -30,6 +31,8 @@ interface StatusItem {
|
||||
export default function QuickActionDashboardPage() {
|
||||
const router = useRouter()
|
||||
const user = useAuthStore(s => s.user)
|
||||
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||
const accessToken = useAuthStore(s => s.accessToken) // NEW
|
||||
const { userStatus, loading, error, refreshStatus } = useUserStatus()
|
||||
const [isClient, setIsClient] = useState(false)
|
||||
|
||||
@ -51,32 +54,66 @@ export default function QuickActionDashboardPage() {
|
||||
const additionalInfo = userStatus?.profile_completed || false
|
||||
const contractSigned = userStatus?.contract_signed || false
|
||||
|
||||
// Check if we should open tutorial (from URL parameter) - separate useEffect after status is loaded
|
||||
// NEW: if everything is done, quickaction-dashboard is no longer accessible
|
||||
const allDone = emailVerified && idUploaded && additionalInfo && contractSigned
|
||||
// NEW: smooth redirect (prevents snappy double navigation)
|
||||
const [redirectTo, setRedirectTo] = useState<string | null>(null)
|
||||
const redirectOnceRef = useRef(false)
|
||||
const smoothReplace = useCallback((to: string) => {
|
||||
if (redirectOnceRef.current) return
|
||||
redirectOnceRef.current = true
|
||||
setRedirectTo(to)
|
||||
window.setTimeout(() => router.replace(to), 200)
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isClient || loading || !userStatus) return
|
||||
|
||||
if (!isClient) return
|
||||
if (loading || !userStatus) return
|
||||
if (allDone) smoothReplace('/dashboard') // CHANGED
|
||||
}, [isClient, loading, userStatus, allDone, smoothReplace])
|
||||
|
||||
// NEW: decide which tutorial step to start on
|
||||
const getNextTutorialStep = useCallback(() => {
|
||||
const noneCompleted = !emailVerified && !idUploaded && !additionalInfo && !contractSigned
|
||||
if (noneCompleted) return 1
|
||||
if (!emailVerified) return 2
|
||||
if (!idUploaded) return 3
|
||||
if (!additionalInfo) return 4
|
||||
if (!contractSigned) return 5
|
||||
return 6
|
||||
}, [emailVerified, idUploaded, additionalInfo, contractSigned])
|
||||
|
||||
// CHANGED: single auto-open mechanism (works even if tutorial_seen exists)
|
||||
useEffect(() => {
|
||||
if (!isClient) return
|
||||
if (loading || !userStatus) return
|
||||
if (allDone) return // NEW: avoid tutorial flash during redirect
|
||||
if (isTutorialOpen) return
|
||||
|
||||
const uid =
|
||||
(user as any)?.id ??
|
||||
(user as any)?._id ??
|
||||
(user as any)?.userId ??
|
||||
(user as any)?.email ??
|
||||
null
|
||||
if (!uid) return
|
||||
|
||||
const tokenSuffix = (accessToken || '').slice(-12) || 'no-token'
|
||||
const sessionKey = `pp:tutorialShown:${uid}:${tokenSuffix}`
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
if (urlParams.get('tutorial') === 'true') {
|
||||
// Remove the parameter from URL
|
||||
const newUrl = window.location.pathname
|
||||
window.history.replaceState({}, '', newUrl)
|
||||
|
||||
// Open tutorial and go to next step
|
||||
setTimeout(() => {
|
||||
setIsTutorialOpen(true)
|
||||
// Determine next step based on completion status
|
||||
if (!emailVerified) {
|
||||
setCurrentTutorialStep(2)
|
||||
} else if (!idUploaded) {
|
||||
setCurrentTutorialStep(3)
|
||||
} else if (!additionalInfo) {
|
||||
setCurrentTutorialStep(4)
|
||||
} else {
|
||||
setCurrentTutorialStep(5)
|
||||
}
|
||||
}, 500)
|
||||
const forceOpen = urlParams.get('tutorial') === 'true'
|
||||
if (forceOpen) {
|
||||
window.history.replaceState({}, '', window.location.pathname)
|
||||
}
|
||||
}, [isClient, loading, userStatus, emailVerified, idUploaded, additionalInfo])
|
||||
|
||||
const alreadyShownThisLogin = sessionStorage.getItem(sessionKey) === '1'
|
||||
if (alreadyShownThisLogin && !forceOpen) return
|
||||
|
||||
setCurrentTutorialStep(getNextTutorialStep())
|
||||
setIsTutorialOpen(true)
|
||||
sessionStorage.setItem(sessionKey, '1')
|
||||
}, [isClient, loading, userStatus, isTutorialOpen, user, accessToken, getNextTutorialStep])
|
||||
|
||||
const statusItems: StatusItem[] = [
|
||||
{
|
||||
@ -111,23 +148,27 @@ export default function QuickActionDashboardPage() {
|
||||
|
||||
// Action handlers - navigate to proper QuickAction pages with tutorial callback
|
||||
const handleVerifyEmail = useCallback(() => {
|
||||
if (emailVerified) return
|
||||
router.push('/quickaction-dashboard/register-email-verify?tutorial=true')
|
||||
}, [router])
|
||||
}, [router, emailVerified])
|
||||
|
||||
const handleUploadId = useCallback(() => {
|
||||
if (idUploaded) return
|
||||
const userType = user?.userType || 'personal'
|
||||
router.push(`/quickaction-dashboard/register-upload-id/${userType}?tutorial=true`)
|
||||
}, [router, user])
|
||||
}, [router, user, idUploaded])
|
||||
|
||||
const handleCompleteInfo = useCallback(() => {
|
||||
if (additionalInfo) return
|
||||
const userType = user?.userType || 'personal'
|
||||
router.push(`/quickaction-dashboard/register-additional-information/${userType}?tutorial=true`)
|
||||
}, [router, user])
|
||||
}, [router, user, additionalInfo])
|
||||
|
||||
const handleSignContract = useCallback(() => {
|
||||
if (contractSigned) return
|
||||
const userType = user?.userType || 'personal'
|
||||
router.push(`/quickaction-dashboard/register-sign-contract/${userType}?tutorial=true`)
|
||||
}, [router, user])
|
||||
}, [router, user, contractSigned])
|
||||
|
||||
// Tutorial handlers
|
||||
const startTutorial = useCallback(() => {
|
||||
@ -149,16 +190,6 @@ export default function QuickActionDashboardPage() {
|
||||
setCurrentTutorialStep(prev => Math.max(1, prev - 1))
|
||||
}, [])
|
||||
|
||||
// Auto-start tutorial for new users
|
||||
useEffect(() => {
|
||||
if (isClient && !hasSeenTutorial && !loading && userStatus) {
|
||||
// Auto-start tutorial if user hasn't completed first step
|
||||
if (!emailVerified) {
|
||||
setTimeout(() => setIsTutorialOpen(true), 1000)
|
||||
}
|
||||
}
|
||||
}, [isClient, hasSeenTutorial, loading, userStatus, emailVerified])
|
||||
|
||||
// Create tutorial steps
|
||||
const tutorialSteps = createTutorialSteps(
|
||||
emailVerified,
|
||||
@ -202,20 +233,27 @@ export default function QuickActionDashboardPage() {
|
||||
return () => clearInterval(id)
|
||||
}, [isClient, emailVerified, user?.email])
|
||||
|
||||
// NEW: only logged-in users can access quickaction dashboard
|
||||
useEffect(() => {
|
||||
if (!isClient) return
|
||||
if (!isAuthReady) return
|
||||
if (!user) smoothReplace('/login')
|
||||
}, [isClient, isAuthReady, user, smoothReplace])
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background */}
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
{/* Soft gradient blobs */}
|
||||
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
|
||||
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
|
||||
{/* Subtle radial highlight */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
|
||||
{/* NEW: smooth redirect overlay */}
|
||||
{redirectTo && (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
||||
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||
<div className="text-sm font-medium text-gray-900">Redirecting…</div>
|
||||
<div className="mt-1 text-xs text-gray-600">Taking you to your dashboard</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<main className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<BlueBlurryBackground /* optionally: animate={!isMobile} */>
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Title */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
@ -261,28 +299,32 @@ export default function QuickActionDashboardPage() {
|
||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 mb-5">
|
||||
Status Overview
|
||||
</h2>
|
||||
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 xl:grid-cols-4">
|
||||
|
||||
{/* CHANGED: mobile 2x2 grid */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
{statusItems.map(item => {
|
||||
const CompleteIcon = item.complete ? CheckCircleIcon : XCircleIcon
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
className={`rounded-lg px-4 py-6 flex flex-col items-center text-center border transition-colors ${
|
||||
className={`rounded-lg px-3 py-4 sm:px-4 sm:py-6 flex flex-col items-center text-center border transition-colors ${
|
||||
item.complete ? 'bg-emerald-50 border-emerald-100' : 'bg-rose-50 border-rose-100'
|
||||
}`}
|
||||
>
|
||||
<CompleteIcon
|
||||
className={`h-6 w-6 mb-4 ${item.complete ? 'text-emerald-600' : 'text-rose-600'}`}
|
||||
className={`h-5 w-5 sm:h-6 sm:w-6 mb-3 sm:mb-4 ${
|
||||
item.complete ? 'text-emerald-600' : 'text-rose-600'
|
||||
}`}
|
||||
/>
|
||||
<span
|
||||
className={`text-xs font-medium uppercase tracking-wide ${
|
||||
className={`text-[11px] sm:text-xs font-medium uppercase tracking-wide ${
|
||||
item.complete ? 'text-emerald-700' : 'text-rose-700'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
<span
|
||||
className={`mt-1 text-xs font-semibold ${
|
||||
className={`mt-1 text-[11px] sm:text-xs font-semibold ${
|
||||
item.complete ? 'text-emerald-600' : 'text-rose-600'
|
||||
}`}
|
||||
>
|
||||
@ -316,19 +358,21 @@ export default function QuickActionDashboardPage() {
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 xl:grid-cols-4">
|
||||
|
||||
{/* CHANGED: mobile 2x2 grid (order already matches desired layout) */}
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
{/* Email Verification */}
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
onClick={handleVerifyEmail}
|
||||
disabled={emailVerified}
|
||||
className={`relative flex flex-col items-center justify-center rounded-lg px-4 py-8 text-center border font-medium text-sm transition-all ${
|
||||
className={`relative flex flex-col items-center justify-center rounded-lg px-3 py-5 sm:px-4 sm:py-8 text-center border font-medium text-xs sm:text-sm transition-all md:transition-transform md:duration-200 ${
|
||||
emailVerified
|
||||
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
|
||||
: 'bg-blue-600 hover:bg-blue-500 text-white border-blue-600 shadow'
|
||||
: 'bg-blue-600 text-white border-blue-600 shadow md:hover:-translate-y-0.5 md:hover:shadow-lg md:hover:bg-blue-500 md:active:translate-y-0'
|
||||
}`}
|
||||
>
|
||||
<EnvelopeOpenIcon className="h-6 w-6 mb-2" />
|
||||
<EnvelopeOpenIcon className="h-5 w-5 sm:h-6 sm:w-6 mb-2" />
|
||||
{emailVerified ? 'Email Verified' : 'Verify Email'}
|
||||
</button>
|
||||
{/* NEW: resend feedback (only when not verified) */}
|
||||
@ -344,26 +388,28 @@ export default function QuickActionDashboardPage() {
|
||||
{/* ID Upload */}
|
||||
<button
|
||||
onClick={handleUploadId}
|
||||
className={`relative flex flex-col items-center justify-center rounded-lg px-4 py-5 text-center border font-medium text-sm transition-all ${
|
||||
disabled={idUploaded}
|
||||
className={`relative flex flex-col items-center justify-center rounded-lg px-3 py-5 sm:px-4 sm:py-5 text-center border font-medium text-xs sm:text-sm transition-all md:transition-transform md:duration-200 ${
|
||||
idUploaded
|
||||
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
|
||||
: 'bg-blue-600 hover:bg-blue-500 text-white border-blue-600 shadow'
|
||||
}`}
|
||||
: 'bg-blue-600 text-white border-blue-600 shadow md:hover:-translate-y-0.5 md:hover:shadow-lg md:hover:bg-blue-500 md:active:translate-y-0'
|
||||
}`}
|
||||
>
|
||||
<ArrowUpOnSquareIcon className="h-6 w-6 mb-2" />
|
||||
<ArrowUpOnSquareIcon className="h-5 w-5 sm:h-6 sm:w-6 mb-2" />
|
||||
{idUploaded ? 'ID Uploaded' : 'Upload ID Document'}
|
||||
</button>
|
||||
|
||||
{/* Additional Info */}
|
||||
<button
|
||||
onClick={handleCompleteInfo}
|
||||
className={`relative flex flex-col items-center justify-center rounded-lg px-4 py-5 text-center border font-medium text-sm transition-all ${
|
||||
disabled={additionalInfo}
|
||||
className={`relative flex flex-col items-center justify-center rounded-lg px-3 py-5 sm:px-4 sm:py-5 text-center border font-medium text-xs sm:text-sm transition-all md:transition-transform md:duration-200 ${
|
||||
additionalInfo
|
||||
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
|
||||
: 'bg-blue-600 hover:bg-blue-500 text-white border-blue-600 shadow'
|
||||
: 'bg-blue-600 text-white border-blue-600 shadow md:hover:-translate-y-0.5 md:hover:shadow-lg md:hover:bg-blue-500 md:active:translate-y-0'
|
||||
}`}
|
||||
>
|
||||
<PencilSquareIcon className="h-6 w-6 mb-2" />
|
||||
<PencilSquareIcon className="h-5 w-5 sm:h-6 sm:w-6 mb-2" />
|
||||
{additionalInfo ? 'Profile Completed' : 'Complete Profile'}
|
||||
</button>
|
||||
|
||||
@ -372,15 +418,15 @@ export default function QuickActionDashboardPage() {
|
||||
<button
|
||||
onClick={handleSignContract}
|
||||
disabled={!canSignContract || contractSigned}
|
||||
className={`flex flex-col flex-1 items-center justify-center rounded-lg px-4 py-5 text-center border font-medium text-sm transition-all ${
|
||||
className={`flex flex-col flex-1 items-center justify-center rounded-lg px-3 py-5 sm:px-4 sm:py-5 text-center border font-medium text-xs sm:text-sm transition-all md:transition-transform md:duration-200 ${
|
||||
contractSigned
|
||||
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
|
||||
: canSignContract
|
||||
? 'bg-blue-600 hover:bg-blue-500 text-white border-blue-600 shadow'
|
||||
? 'bg-blue-600 text-white border-blue-600 shadow md:hover:-translate-y-0.5 md:hover:shadow-lg md:hover:bg-blue-500 md:active:translate-y-0'
|
||||
: 'bg-gray-300 text-gray-600 border-gray-300 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
<ClipboardDocumentCheckIcon className="h-6 w-6 mb-2" />
|
||||
<ClipboardDocumentCheckIcon className="h-5 w-5 sm:h-6 sm:w-6 mb-2" />
|
||||
{contractSigned ? 'Contract Signed' : 'Sign Contract'}
|
||||
</button>
|
||||
{!canSignContract && !contractSigned && (
|
||||
@ -409,7 +455,7 @@ export default function QuickActionDashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</BlueBlurryBackground>
|
||||
|
||||
{/* Tutorial Modal */}
|
||||
<TutorialModal
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import PageLayout from '../../../components/PageLayout'
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||
import { useToast } from '../../../components/toast/toastComponent'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid' // NEW
|
||||
|
||||
interface CompanyProfileData {
|
||||
companyName: string
|
||||
@ -47,10 +48,132 @@ const init: CompanyProfileData = {
|
||||
emergencyPhone: ''
|
||||
}
|
||||
|
||||
function ModernSelect({
|
||||
label,
|
||||
placeholder = 'Select…',
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
}: {
|
||||
label: string
|
||||
placeholder?: string
|
||||
value: string
|
||||
onChange: (next: string) => void
|
||||
options: { value: string; label: string }[]
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const btnRef = useRef<HTMLButtonElement | null>(null)
|
||||
const [pos, setPos] = useState({ left: 16, top: 0, width: 320 })
|
||||
|
||||
const selected = useMemo(() => options.find(o => o.value === value) || null, [options, value])
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase()
|
||||
if (!q) return options
|
||||
return options.filter(o => o.label.toLowerCase().includes(q))
|
||||
}, [options, query])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const update = () => {
|
||||
const el = btnRef.current
|
||||
if (!el) return
|
||||
const r = el.getBoundingClientRect()
|
||||
const padding = 16
|
||||
const width = Math.min(r.width, window.innerWidth - padding * 2)
|
||||
const left = Math.max(padding, Math.min(r.left, window.innerWidth - width - padding))
|
||||
const top = r.bottom + 8
|
||||
setPos({ left, top, width })
|
||||
}
|
||||
update()
|
||||
window.addEventListener('resize', update)
|
||||
window.addEventListener('scroll', update, true)
|
||||
return () => {
|
||||
window.removeEventListener('resize', update)
|
||||
window.removeEventListener('scroll', update, true)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false) }
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">{label} *</label>
|
||||
|
||||
<button
|
||||
ref={btnRef}
|
||||
type="button"
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white/70 px-3 py-2 text-sm text-left
|
||||
shadow-sm hover:border-gray-400
|
||||
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent
|
||||
inline-flex items-center justify-between gap-3"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span className={selected ? 'text-gray-900' : 'text-gray-500'}>
|
||||
{selected ? selected.label : placeholder}
|
||||
</span>
|
||||
<ChevronDownIcon className={`h-5 w-5 text-gray-500 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-[90]" onClick={() => setOpen(false)} aria-hidden />
|
||||
<div
|
||||
className="fixed z-[100] overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl"
|
||||
style={{ left: pos.left, top: pos.top, width: pos.width }}
|
||||
>
|
||||
<div className="p-2 border-b border-gray-100">
|
||||
<input
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
placeholder="Search…"
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-[42vh] overflow-auto p-1">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-gray-500">No results</div>
|
||||
) : (
|
||||
filtered.map(o => {
|
||||
const active = o.value === value
|
||||
return (
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
onClick={() => { onChange(o.value); setQuery(''); setOpen(false) }}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors
|
||||
${active ? 'bg-[#8D6B1D]/10 text-[#7A5E1A] font-semibold' : 'text-gray-800 hover:bg-gray-50'}`}
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function CompanyAdditionalInformationPage() {
|
||||
const router = useRouter()
|
||||
const user = useAuthStore(s => s.user) // NEW
|
||||
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||
const { accessToken } = useAuthStore()
|
||||
const { refreshStatus } = useUserStatus()
|
||||
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const [form, setForm] = useState(init)
|
||||
@ -58,6 +181,38 @@ export default function CompanyAdditionalInformationPage() {
|
||||
const [success, setSuccess] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
// NEW: smooth redirect
|
||||
const [redirectTo, setRedirectTo] = useState<string | null>(null)
|
||||
const redirectOnceRef = useRef(false)
|
||||
const smoothReplace = useCallback((to: string) => {
|
||||
if (redirectOnceRef.current) return
|
||||
redirectOnceRef.current = true
|
||||
setRedirectTo(to)
|
||||
window.setTimeout(() => router.replace(to), 200)
|
||||
}, [router])
|
||||
|
||||
// NEW: hard block if step already done OR all steps done
|
||||
useEffect(() => {
|
||||
if (statusLoading || !userStatus) return
|
||||
const allDone =
|
||||
!!userStatus.email_verified &&
|
||||
!!userStatus.documents_uploaded &&
|
||||
!!userStatus.profile_completed &&
|
||||
!!userStatus.contract_signed
|
||||
|
||||
if (allDone) {
|
||||
smoothReplace('/dashboard') // CHANGED
|
||||
} else if (userStatus.profile_completed) {
|
||||
smoothReplace('/quickaction-dashboard') // CHANGED
|
||||
}
|
||||
}, [statusLoading, userStatus, smoothReplace])
|
||||
|
||||
// NEW: must be logged in
|
||||
useEffect(() => {
|
||||
if (!isAuthReady) return
|
||||
if (!user || !accessToken) smoothReplace('/login')
|
||||
}, [isAuthReady, user, accessToken, smoothReplace])
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target
|
||||
setForm(p => ({ ...p, [name]: value }))
|
||||
@ -178,8 +333,23 @@ export default function CompanyAdditionalInformationPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const setField = (name: keyof CompanyProfileData, value: string) => {
|
||||
setForm(p => ({ ...p, [name]: value }))
|
||||
setError('')
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* NEW: smooth redirect overlay */}
|
||||
{redirectTo && (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
||||
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||
<div className="text-sm font-medium text-gray-900">Redirecting…</div>
|
||||
<div className="mt-1 text-xs text-gray-600">Please wait</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
@ -215,7 +385,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
name="companyName"
|
||||
value={form.companyName}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -228,7 +398,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
value={form.vatNumber}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g. DE123456789"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -240,7 +410,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
name="street"
|
||||
value={form.street}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -252,7 +422,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
name="postalCode"
|
||||
value={form.postalCode}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -264,28 +434,18 @@ export default function CompanyAdditionalInformationPage() {
|
||||
name="city"
|
||||
value={form.city}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Country *
|
||||
</label>
|
||||
<select
|
||||
name="country"
|
||||
<ModernSelect
|
||||
label="Country"
|
||||
placeholder="Select country..."
|
||||
value={form.country}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
required
|
||||
>
|
||||
<option value="">Select country...</option>
|
||||
{COUNTRIES.map(country => (
|
||||
<option key={country} value={country}>
|
||||
{country}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
onChange={(v) => setField('country', v)}
|
||||
options={COUNTRIES.map(c => ({ value: c, label: c }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -307,7 +467,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
value={form.accountHolder}
|
||||
onChange={handleChange}
|
||||
placeholder="Company / Holder name"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -320,7 +480,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
value={form.iban}
|
||||
onChange={handleChange}
|
||||
placeholder="DE89 3704 0044 0532 0130 00"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -333,7 +493,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
value={form.bic}
|
||||
onChange={handleChange}
|
||||
placeholder="GENODEF1XXX"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -356,7 +516,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
value={form.secondPhone}
|
||||
onChange={handleChange}
|
||||
placeholder="+49 123 456 7890"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -368,7 +528,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
value={form.emergencyName}
|
||||
onChange={handleChange}
|
||||
placeholder="Contact name"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -380,7 +540,7 @@ export default function CompanyAdditionalInformationPage() {
|
||||
value={form.emergencyPhone}
|
||||
onChange={handleChange}
|
||||
placeholder="+49 123 456 7890"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden lg:block" />
|
||||
@ -402,14 +562,15 @@ export default function CompanyAdditionalInformationPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/quickaction-dashboard')}
|
||||
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
|
||||
className="inline-flex items-center rounded-md border border-[#8D6B1D]/40 px-4 py-2 text-sm font-semibold text-[#8D6B1D] bg-white hover:bg-[#8D6B1D]/10"
|
||||
>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || success}
|
||||
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
className="inline-flex items-center rounded-md bg-[#8D6B1D] px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-[#7A5E1A] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Speichern…' : success ? 'Gespeichert' : 'Save & Continue'}
|
||||
</button>
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import PageLayout from '../../../components/PageLayout'
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||
import { useToast } from '../../../components/toast/toastComponent'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
|
||||
interface PersonalProfileData {
|
||||
dob: string
|
||||
@ -55,10 +56,153 @@ const initialData: PersonalProfileData = {
|
||||
emergencyPhone: ''
|
||||
}
|
||||
|
||||
type SelectOption = { value: string; label: string }
|
||||
|
||||
function ModernSelect({
|
||||
label,
|
||||
placeholder = 'Select…',
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
}: {
|
||||
label: string
|
||||
placeholder?: string
|
||||
value: string
|
||||
onChange: (next: string) => void
|
||||
options: SelectOption[]
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const btnRef = useRef<HTMLButtonElement | null>(null)
|
||||
const [pos, setPos] = useState({ left: 16, top: 0, width: 320 })
|
||||
|
||||
const selected = useMemo(
|
||||
() => options.find(o => o.value === value) || null,
|
||||
[options, value]
|
||||
)
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase()
|
||||
if (!q) return options
|
||||
return options.filter(o => o.label.toLowerCase().includes(q))
|
||||
}, [options, query])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
const update = () => {
|
||||
const el = btnRef.current
|
||||
if (!el) return
|
||||
const r = el.getBoundingClientRect()
|
||||
|
||||
const padding = 16
|
||||
const width = Math.min(r.width, window.innerWidth - padding * 2)
|
||||
const left = Math.max(padding, Math.min(r.left, window.innerWidth - width - padding))
|
||||
const top = r.bottom + 8
|
||||
|
||||
setPos({ left, top, width })
|
||||
}
|
||||
|
||||
update()
|
||||
window.addEventListener('resize', update)
|
||||
window.addEventListener('scroll', update, true)
|
||||
return () => {
|
||||
window.removeEventListener('resize', update)
|
||||
window.removeEventListener('scroll', update, true)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
// close on escape
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false)
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [open])
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">{label} *</label>
|
||||
|
||||
<button
|
||||
ref={btnRef}
|
||||
type="button"
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className="w-full rounded-lg border border-gray-300 bg-white/70 px-3 py-2 text-sm text-left
|
||||
shadow-sm hover:border-gray-400
|
||||
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent
|
||||
inline-flex items-center justify-between gap-3"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span className={selected ? 'text-gray-900' : 'text-gray-500'}>
|
||||
{selected ? selected.label : placeholder}
|
||||
</span>
|
||||
<ChevronDownIcon className={`h-5 w-5 text-gray-500 transition-transform ${open ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<>
|
||||
{/* click-away overlay */}
|
||||
<div className="fixed inset-0 z-[90]" onClick={() => setOpen(false)} aria-hidden />
|
||||
|
||||
{/* dropdown (fixed so it “pops out under” even on mobile) */}
|
||||
<div
|
||||
className="fixed z-[100] overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl"
|
||||
style={{ left: pos.left, top: pos.top, width: pos.width }}
|
||||
>
|
||||
<div className="p-2 border-b border-gray-100">
|
||||
<input
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
placeholder="Search…"
|
||||
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm
|
||||
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[42vh] overflow-auto p-1">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="px-3 py-2 text-sm text-gray-500">No results</div>
|
||||
) : (
|
||||
filtered.map(o => {
|
||||
const active = o.value === value
|
||||
return (
|
||||
<button
|
||||
key={o.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange(o.value)
|
||||
setQuery('')
|
||||
setOpen(false)
|
||||
}}
|
||||
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors
|
||||
${active ? 'bg-[#8D6B1D]/10 text-[#7A5E1A] font-semibold' : 'text-gray-800 hover:bg-gray-50'}`}
|
||||
role="option"
|
||||
aria-selected={active}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PersonalAdditionalInformationPage() {
|
||||
const router = useRouter()
|
||||
const user = useAuthStore(s => s.user) // NEW
|
||||
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||
const { accessToken } = useAuthStore()
|
||||
const { refreshStatus } = useUserStatus()
|
||||
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const [form, setForm] = useState(initialData)
|
||||
@ -270,8 +414,55 @@ export default function PersonalAdditionalInformationPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const setField = (name: keyof PersonalProfileData, value: string) => {
|
||||
setForm(p => ({ ...p, [name]: value }))
|
||||
setError('')
|
||||
}
|
||||
|
||||
// NEW: smooth redirect
|
||||
const [redirectTo, setRedirectTo] = useState<string | null>(null)
|
||||
const redirectOnceRef = useRef(false)
|
||||
const smoothReplace = useCallback((to: string) => {
|
||||
if (redirectOnceRef.current) return
|
||||
redirectOnceRef.current = true
|
||||
setRedirectTo(to)
|
||||
window.setTimeout(() => router.replace(to), 200)
|
||||
}, [router])
|
||||
|
||||
// NEW: hard block if step already done OR all steps done
|
||||
useEffect(() => {
|
||||
if (statusLoading || !userStatus) return
|
||||
const allDone =
|
||||
!!userStatus.email_verified &&
|
||||
!!userStatus.documents_uploaded &&
|
||||
!!userStatus.profile_completed &&
|
||||
!!userStatus.contract_signed
|
||||
|
||||
if (allDone) {
|
||||
smoothReplace('/dashboard') // CHANGED
|
||||
} else if (userStatus.profile_completed) {
|
||||
smoothReplace('/quickaction-dashboard') // CHANGED
|
||||
}
|
||||
}, [statusLoading, userStatus, smoothReplace])
|
||||
|
||||
// NEW: must be logged in
|
||||
useEffect(() => {
|
||||
if (!isAuthReady) return
|
||||
if (!user || !accessToken) smoothReplace('/login')
|
||||
}, [isAuthReady, user, accessToken, smoothReplace])
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* NEW: smooth redirect overlay */}
|
||||
{redirectTo && (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
||||
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||
<div className="text-sm font-medium text-gray-900">Redirecting…</div>
|
||||
<div className="mt-1 text-xs text-gray-600">Please wait</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
@ -310,28 +501,18 @@ export default function PersonalAdditionalInformationPage() {
|
||||
onChange={handleChange}
|
||||
min={new Date(new Date().getFullYear() - 120, 0, 1).toISOString().split('T')[0]}
|
||||
max={new Date(new Date().getFullYear() - 18, 11, 31).toISOString().split('T')[0]}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nationality *
|
||||
</label>
|
||||
<select
|
||||
name="nationality"
|
||||
<ModernSelect
|
||||
label="Nationality"
|
||||
placeholder="Select nationality..."
|
||||
value={form.nationality}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
required
|
||||
>
|
||||
<option value="">Select nationality...</option>
|
||||
{NATIONALITIES.map(nationality => (
|
||||
<option key={nationality} value={nationality}>
|
||||
{nationality}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
onChange={(v) => setField('nationality', v)}
|
||||
options={NATIONALITIES.map(n => ({ value: n, label: n }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2 lg:col-span-3">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
@ -342,7 +523,7 @@ export default function PersonalAdditionalInformationPage() {
|
||||
value={form.street}
|
||||
onChange={handleChange}
|
||||
placeholder="Street & House Number"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -355,7 +536,7 @@ export default function PersonalAdditionalInformationPage() {
|
||||
value={form.postalCode}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g. 12345"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -368,28 +549,18 @@ export default function PersonalAdditionalInformationPage() {
|
||||
value={form.city}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g. Berlin"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Country *
|
||||
</label>
|
||||
<select
|
||||
name="country"
|
||||
<ModernSelect
|
||||
label="Country"
|
||||
placeholder="Select country..."
|
||||
value={form.country}
|
||||
onChange={handleChange}
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
required
|
||||
>
|
||||
<option value="">Select country...</option>
|
||||
{COUNTRIES.map(country => (
|
||||
<option key={country} value={country}>
|
||||
{country}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
onChange={(v) => setField('country', v)}
|
||||
options={COUNTRIES.map(c => ({ value: c, label: c }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -411,7 +582,7 @@ export default function PersonalAdditionalInformationPage() {
|
||||
value={form.accountHolder}
|
||||
onChange={handleChange}
|
||||
placeholder="Full name"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -424,7 +595,7 @@ export default function PersonalAdditionalInformationPage() {
|
||||
value={form.iban}
|
||||
onChange={handleChange}
|
||||
placeholder="e.g. DE89 3704 0044 0532 0130 00"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -448,7 +619,7 @@ export default function PersonalAdditionalInformationPage() {
|
||||
value={form.secondPhone}
|
||||
onChange={handleChange}
|
||||
placeholder="+43 660 1234567"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -460,7 +631,7 @@ export default function PersonalAdditionalInformationPage() {
|
||||
value={form.emergencyName}
|
||||
onChange={handleChange}
|
||||
placeholder="Contact name"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@ -472,7 +643,7 @@ export default function PersonalAdditionalInformationPage() {
|
||||
value={form.emergencyPhone}
|
||||
onChange={handleChange}
|
||||
placeholder="+43 660 1234567"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
||||
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden lg:block" />
|
||||
@ -494,14 +665,15 @@ export default function PersonalAdditionalInformationPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/quickaction-dashboard')}
|
||||
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
|
||||
className="inline-flex items-center rounded-md border border-[#8D6B1D]/40 px-4 py-2 text-sm font-semibold text-[#8D6B1D] bg-white hover:bg-[#8D6B1D]/10"
|
||||
>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || success}
|
||||
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
className="inline-flex items-center rounded-md bg-[#8D6B1D] px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-[#7A5E1A] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Saving…' : success ? 'Saved' : 'Save & Continue'}
|
||||
</button>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import PageLayout from '../../components/PageLayout'
|
||||
import BlueBlurryBackground from '../../components/background/blueblurry' // NEW
|
||||
import useAuthStore from '../../store/authStore'
|
||||
import { useUserStatus } from '../../hooks/useUserStatus'
|
||||
import { useRouter } from 'next/navigation'
|
||||
@ -9,8 +10,9 @@ import { useToast } from '../../components/toast/toastComponent'
|
||||
|
||||
export default function EmailVerifyPage() {
|
||||
const user = useAuthStore(s => s.user)
|
||||
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||
const token = useAuthStore(s => s.accessToken)
|
||||
const { refreshStatus } = useUserStatus()
|
||||
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus() // CHANGED
|
||||
const [code, setCode] = useState(['', '', '', '', '', ''])
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
@ -339,20 +341,51 @@ export default function EmailVerifyPage() {
|
||||
return `${m}:${String(s).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// NEW: hard block if step already done OR all steps done
|
||||
const [redirectTo, setRedirectTo] = useState<string | null>(null)
|
||||
const redirectOnceRef = useRef(false)
|
||||
const smoothReplace = useCallback((to: string) => {
|
||||
if (redirectOnceRef.current) return
|
||||
redirectOnceRef.current = true
|
||||
setRedirectTo(to)
|
||||
window.setTimeout(() => router.replace(to), 200)
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
if (statusLoading || !userStatus) return
|
||||
const allDone =
|
||||
!!userStatus.email_verified &&
|
||||
!!userStatus.documents_uploaded &&
|
||||
!!userStatus.profile_completed &&
|
||||
!!userStatus.contract_signed
|
||||
|
||||
if (allDone) {
|
||||
smoothReplace('/dashboard') // CHANGED
|
||||
} else if (userStatus.email_verified) {
|
||||
smoothReplace('/quickaction-dashboard') // CHANGED
|
||||
}
|
||||
}, [statusLoading, userStatus, smoothReplace])
|
||||
|
||||
// NEW: must be logged in
|
||||
useEffect(() => {
|
||||
if (!isAuthReady) return
|
||||
if (!user || !token) smoothReplace('/login')
|
||||
}, [isAuthReady, user, token, smoothReplace])
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
{/* Soft gradient blobs */}
|
||||
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
|
||||
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
|
||||
{/* Subtle radial highlight */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
|
||||
{/* NEW: smooth redirect overlay */}
|
||||
{redirectTo && (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
||||
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||
<div className="text-sm font-medium text-gray-900">Redirecting…</div>
|
||||
<div className="mt-1 text-xs text-gray-600">Please wait</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<main className="relative z-10 flex flex-col flex-1 w-full px-4 sm:px-6 py-16 sm:py-24">
|
||||
<BlueBlurryBackground>
|
||||
<main className="flex flex-col flex-1 w-full px-4 sm:px-6 py-16 sm:py-24">
|
||||
<div className="max-w-xl mx-auto">
|
||||
<div className="text-center mb-10">
|
||||
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-gray-900">
|
||||
@ -362,7 +395,7 @@ export default function EmailVerifyPage() {
|
||||
{initialEmailSent ? (
|
||||
<>
|
||||
We sent a 6-digit code to{' '}
|
||||
<span className="text-blue-700 font-medium">
|
||||
<span className="text-[#8D6B1D] font-medium">
|
||||
{user?.email || 'your email'}
|
||||
</span>
|
||||
. Enter it below.
|
||||
@ -370,7 +403,7 @@ export default function EmailVerifyPage() {
|
||||
) : (
|
||||
<>
|
||||
Sending verification email to{' '}
|
||||
<span className="text-blue-700 font-medium">
|
||||
<span className="text-[#8D6B1D] font-medium">
|
||||
{user?.email || 'your email'}
|
||||
</span>
|
||||
...
|
||||
@ -379,13 +412,11 @@ export default function EmailVerifyPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="bg-white/95 backdrop-blur rounded-2xl shadow-xl ring-1 ring-black/5 px-6 py-8 sm:px-10 sm:py-10"
|
||||
>
|
||||
<fieldset disabled={submitting || success} className="space-y-8">
|
||||
{/* Inputs */}
|
||||
<div className="flex justify-center gap-2 sm:gap-3">
|
||||
{code.map((v, i) => (
|
||||
<input
|
||||
@ -401,9 +432,9 @@ export default function EmailVerifyPage() {
|
||||
onPaste={e => handlePaste(i, e)}
|
||||
className={`w-12 h-14 sm:w-14 sm:h-16 text-center text-2xl font-semibold rounded-lg border transition-colors outline-none
|
||||
${v
|
||||
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white text-gray-900'
|
||||
? 'border-[#8D6B1D] ring-2 ring-[#8D6B1D]/25 bg-white text-gray-900'
|
||||
: 'border-gray-300 bg-white/80 text-gray-700'}
|
||||
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500`}
|
||||
focus:ring-2 focus:ring-[#8D6B1D] focus:border-[#8D6B1D]`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -423,7 +454,10 @@ export default function EmailVerifyPage() {
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full sm:w-auto inline-flex justify-center items-center rounded-lg px-6 py-3 font-semibold text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
className="w-full sm:w-auto inline-flex justify-center items-center rounded-lg px-6 py-3 font-semibold text-white
|
||||
bg-[#8D6B1D] hover:bg-[#7A5E1A]
|
||||
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2
|
||||
disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{submitting ? (
|
||||
<>
|
||||
@ -437,7 +471,7 @@ export default function EmailVerifyPage() {
|
||||
type="button"
|
||||
onClick={handleResend}
|
||||
disabled={!!resendCooldown || submitting || success}
|
||||
className="text-sm font-medium text-indigo-700 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
|
||||
className="text-sm font-medium text-[#8D6B1D] hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
|
||||
>
|
||||
{resendCooldown
|
||||
? `Resend in ${formatMmSs(resendCooldown)}`
|
||||
@ -449,7 +483,7 @@ export default function EmailVerifyPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push('/quickaction-dashboard')}
|
||||
className="text-sm font-medium text-gray-700 hover:underline"
|
||||
className="text-sm font-medium text-[#8D6B1D] hover:underline"
|
||||
>
|
||||
Go to Dashboard
|
||||
</button>
|
||||
@ -458,7 +492,7 @@ export default function EmailVerifyPage() {
|
||||
|
||||
<div className="mt-8 text-center text-xs text-gray-500">
|
||||
Didn’t receive the email? Please check your junk/spam folder. Still having issues?{' '}
|
||||
<a href="mailto:test@test.com" className="text-indigo-600 hover:underline">
|
||||
<a href="mailto:test@test.com" className="text-[#8D6B1D] hover:underline">
|
||||
Contact support
|
||||
</a>
|
||||
.
|
||||
@ -466,7 +500,7 @@ export default function EmailVerifyPage() {
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</BlueBlurryBackground>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import PageLayout from '../../../components/PageLayout'
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
@ -10,8 +10,10 @@ import { useToast } from '../../../components/toast/toastComponent'
|
||||
|
||||
export default function CompanySignContractPage() {
|
||||
const router = useRouter()
|
||||
const user = useAuthStore(s => s.user) // NEW
|
||||
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||
const { accessToken } = useAuthStore()
|
||||
const { refreshStatus } = useUserStatus()
|
||||
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus()
|
||||
const { showToast } = useToast()
|
||||
|
||||
const [companyName, setCompanyName] = useState('')
|
||||
@ -246,15 +248,7 @@ export default function CompanySignContractPage() {
|
||||
|
||||
// Redirect to main dashboard after short delay
|
||||
setTimeout(() => {
|
||||
// Check if we came from tutorial
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const fromTutorial = urlParams.get('tutorial') === 'true'
|
||||
|
||||
if (fromTutorial) {
|
||||
router.push('/quickaction-dashboard?tutorial=true')
|
||||
} else {
|
||||
router.push('/quickaction-dashboard')
|
||||
}
|
||||
router.push('/dashboard')
|
||||
}, 2000)
|
||||
|
||||
} catch (error: any) {
|
||||
@ -271,8 +265,49 @@ export default function CompanySignContractPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: hard block if step already done OR all steps done
|
||||
const [redirectTo, setRedirectTo] = useState<string | null>(null)
|
||||
const redirectOnceRef = useRef(false)
|
||||
const smoothReplace = useCallback((to: string) => {
|
||||
if (redirectOnceRef.current) return
|
||||
redirectOnceRef.current = true
|
||||
setRedirectTo(to)
|
||||
window.setTimeout(() => router.replace(to), 200)
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
if (statusLoading || !userStatus) return
|
||||
const allDone =
|
||||
!!userStatus.email_verified &&
|
||||
!!userStatus.documents_uploaded &&
|
||||
!!userStatus.profile_completed &&
|
||||
!!userStatus.contract_signed
|
||||
|
||||
if (allDone) {
|
||||
smoothReplace('/dashboard') // CHANGED
|
||||
} else if (userStatus.contract_signed) {
|
||||
smoothReplace('/quickaction-dashboard') // CHANGED
|
||||
}
|
||||
}, [statusLoading, userStatus, smoothReplace])
|
||||
|
||||
// NEW: must be logged in
|
||||
useEffect(() => {
|
||||
if (!isAuthReady) return
|
||||
if (!user || !accessToken) smoothReplace('/login')
|
||||
}, [isAuthReady, user, accessToken, smoothReplace])
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* NEW: smooth redirect overlay */}
|
||||
{redirectTo && (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
||||
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||
<div className="text-sm font-medium text-gray-900">Redirecting…</div>
|
||||
<div className="mt-1 text-xs text-gray-600">Please wait</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import PageLayout from '../../../components/PageLayout'
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
@ -10,8 +10,10 @@ import { useToast } from '../../../components/toast/toastComponent'
|
||||
|
||||
export default function PersonalSignContractPage() {
|
||||
const router = useRouter()
|
||||
const user = useAuthStore(s => s.user) // NEW
|
||||
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||
const { accessToken } = useAuthStore()
|
||||
const { refreshStatus } = useUserStatus()
|
||||
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus() // CHANGED
|
||||
const { showToast } = useToast()
|
||||
|
||||
const [date, setDate] = useState('')
|
||||
@ -160,6 +162,37 @@ export default function PersonalSignContractPage() {
|
||||
]).finally(() => setPreviewLoading(false))
|
||||
}, [accessToken])
|
||||
|
||||
// NEW: hard block if step already done OR all steps done
|
||||
const [redirectTo, setRedirectTo] = useState<string | null>(null)
|
||||
const redirectOnceRef = useRef(false)
|
||||
const smoothReplace = useCallback((to: string) => {
|
||||
if (redirectOnceRef.current) return
|
||||
redirectOnceRef.current = true
|
||||
setRedirectTo(to)
|
||||
window.setTimeout(() => router.replace(to), 200)
|
||||
}, [router])
|
||||
|
||||
// NEW: must be logged in
|
||||
useEffect(() => {
|
||||
if (!isAuthReady) return
|
||||
if (!user || !accessToken) smoothReplace('/login')
|
||||
}, [isAuthReady, user, accessToken, smoothReplace])
|
||||
|
||||
useEffect(() => {
|
||||
if (statusLoading || !userStatus) return
|
||||
const allDone =
|
||||
!!userStatus.email_verified &&
|
||||
!!userStatus.documents_uploaded &&
|
||||
!!userStatus.profile_completed &&
|
||||
!!userStatus.contract_signed
|
||||
|
||||
if (allDone) {
|
||||
smoothReplace('/dashboard') // CHANGED
|
||||
} else if (userStatus.contract_signed) {
|
||||
smoothReplace('/quickaction-dashboard') // CHANGED
|
||||
}
|
||||
}, [statusLoading, userStatus, smoothReplace])
|
||||
|
||||
const valid = () => {
|
||||
const contractChecked = agreeContract
|
||||
const dataChecked = agreeData
|
||||
@ -242,15 +275,7 @@ export default function PersonalSignContractPage() {
|
||||
|
||||
// Redirect to main dashboard after short delay
|
||||
setTimeout(() => {
|
||||
// Check if we came from tutorial
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const fromTutorial = urlParams.get('tutorial') === 'true'
|
||||
|
||||
if (fromTutorial) {
|
||||
router.push('/quickaction-dashboard?tutorial=true')
|
||||
} else {
|
||||
router.push('/quickaction-dashboard')
|
||||
}
|
||||
router.push('/dashboard')
|
||||
}, 2000)
|
||||
|
||||
} catch (error: any) {
|
||||
@ -269,6 +294,16 @@ export default function PersonalSignContractPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* NEW: smooth redirect overlay */}
|
||||
{redirectTo && (
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
||||
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||
<div className="text-sm font-medium text-gray-900">Redirecting…</div>
|
||||
<div className="mt-1 text-xs text-gray-600">Please wait</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import PageLayout from '../../../components/PageLayout'
|
||||
import BlueBlurryBackground from '../../../components/background/blueblurry'
|
||||
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { useCompanyUploadId } from './hooks/useCompanyUploadId'
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useEffect, useState, useCallback, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||
|
||||
const DOC_TYPES = ['Personalausweis', 'Reisepass', 'Führerschein', 'Aufenthaltstitel']
|
||||
|
||||
@ -26,48 +28,68 @@ export default function CompanyIdUploadPage() {
|
||||
} = useCompanyUploadId()
|
||||
|
||||
const user = useAuthStore(s => s.user)
|
||||
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||
const router = useRouter()
|
||||
const [blocked, setBlocked] = useState(false)
|
||||
const { userStatus, loading: statusLoading } = useUserStatus()
|
||||
|
||||
// NEW: smooth redirect
|
||||
const [redirectTo, setRedirectTo] = useState<string | null>(null)
|
||||
const redirectOnceRef = useRef(false)
|
||||
const smoothReplace = useCallback((to: string) => {
|
||||
if (redirectOnceRef.current) return
|
||||
redirectOnceRef.current = true
|
||||
setRedirectTo(to)
|
||||
window.setTimeout(() => router.replace(to), 200)
|
||||
}, [router])
|
||||
|
||||
// Guard: only 'company' users allowed on this page
|
||||
useEffect(() => {
|
||||
const ut = (user as any)?.userType || (user as any)?.role
|
||||
console.log('🧭 UploadID Guard [company]: userType =', ut)
|
||||
if (ut && ut !== 'company') {
|
||||
console.warn('🚫 UploadID Guard [company]: access denied for userType:', ut, '-> redirecting to personal upload')
|
||||
setBlocked(true)
|
||||
router.replace('/quickaction-dashboard/register-upload-id/personal')
|
||||
} else if (ut === 'company') {
|
||||
console.log('✅ UploadID Guard [company]: access granted')
|
||||
}
|
||||
}, [user, router])
|
||||
if (ut && ut !== 'company') smoothReplace('/quickaction-dashboard/register-upload-id/personal') // CHANGED
|
||||
}, [user, smoothReplace])
|
||||
|
||||
if (blocked) {
|
||||
useEffect(() => {
|
||||
if (statusLoading || !userStatus) return
|
||||
const allDone =
|
||||
!!userStatus.email_verified &&
|
||||
!!userStatus.documents_uploaded &&
|
||||
!!userStatus.profile_completed &&
|
||||
!!userStatus.contract_signed
|
||||
|
||||
if (allDone) {
|
||||
smoothReplace('/dashboard') // CHANGED
|
||||
} else if (userStatus.documents_uploaded) {
|
||||
smoothReplace('/quickaction-dashboard') // CHANGED
|
||||
}
|
||||
}, [statusLoading, userStatus, smoothReplace])
|
||||
|
||||
// NEW: must be logged in
|
||||
useEffect(() => {
|
||||
if (!isAuthReady) return
|
||||
if (!user) smoothReplace('/login')
|
||||
}, [isAuthReady, user, smoothReplace])
|
||||
|
||||
if (redirectTo) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="min-h-[50vh] flex items-center justify-center">
|
||||
<div className="text-center text-sm text-gray-600">
|
||||
Redirecting to the correct upload page…
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
||||
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||
<div className="text-sm font-medium text-gray-900">Redirecting…</div>
|
||||
<div className="mt-1 text-xs text-gray-600">Please wait</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
const goBackToDashboard = () => {
|
||||
// CHANGED: do NOT preserve ?tutorial=true, otherwise dashboard force-opens the tutorial again
|
||||
router.push('/quickaction-dashboard')
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
{/* Soft gradient blobs */}
|
||||
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
|
||||
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
|
||||
{/* Subtle radial highlight */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
|
||||
</div>
|
||||
|
||||
<main className="relative z-10 flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
|
||||
<BlueBlurryBackground>
|
||||
<main className="flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
|
||||
<form
|
||||
onSubmit={submit}
|
||||
className="relative max-w-7xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10 overflow-hidden"
|
||||
@ -89,7 +111,7 @@ export default function CompanyIdUploadPage() {
|
||||
<input
|
||||
value={idNumber}
|
||||
onChange={e => setIdNumber(e.target.value)}
|
||||
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`}
|
||||
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'} focus:ring-[#8D6B1D] focus:border-transparent`}
|
||||
placeholder="Enter contact person's ID number"
|
||||
required
|
||||
/>
|
||||
@ -105,7 +127,7 @@ export default function CompanyIdUploadPage() {
|
||||
<select
|
||||
value={idType}
|
||||
onChange={e => setIdType(e.target.value)}
|
||||
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`}
|
||||
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'} focus:ring-[#8D6B1D] focus:border-transparent`}
|
||||
required
|
||||
>
|
||||
<option value="">Select document type</option>
|
||||
@ -122,7 +144,7 @@ export default function CompanyIdUploadPage() {
|
||||
value={expiryDate}
|
||||
onChange={e => setExpiryDate(e.target.value)}
|
||||
placeholder="tt.mm.jjjj"
|
||||
className={`${inputBase} ${expiryDate ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
|
||||
className={`${inputBase} ${expiryDate ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80 focus:ring-[#8D6B1D] focus:border-transparent`}
|
||||
required
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-600">
|
||||
@ -139,7 +161,9 @@ export default function CompanyIdUploadPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setHasBack(v => { const next = !v; if (!next) setExtraFile(null); return next })}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${hasBack ? 'bg-indigo-600' : 'bg-gray-300'}`}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
|
||||
hasBack ? 'bg-[#8D6B1D]' : 'bg-gray-300'
|
||||
}`}
|
||||
aria-pressed={hasBack}
|
||||
>
|
||||
<span className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${hasBack ? 'translate-x-5' : 'translate-x-0'}`} />
|
||||
@ -154,7 +178,7 @@ export default function CompanyIdUploadPage() {
|
||||
{...dropHandlers}
|
||||
onDrop={e => onDrop(e, 'front')}
|
||||
onClick={() => openPicker('front')}
|
||||
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
|
||||
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-[#8D6B1D] hover:bg-[#8D6B1D]/5 cursor-pointer transition"
|
||||
>
|
||||
<input
|
||||
ref={frontRef}
|
||||
@ -184,8 +208,8 @@ export default function CompanyIdUploadPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
|
||||
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
|
||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-4 transition" />
|
||||
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
|
||||
Click to upload front side
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
@ -203,7 +227,7 @@ export default function CompanyIdUploadPage() {
|
||||
{...dropHandlers}
|
||||
onDrop={e => onDrop(e, 'extra')}
|
||||
onClick={() => openPicker('extra')}
|
||||
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
|
||||
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-[#8D6B1D] hover:bg-[#8D6B1D]/5 cursor-pointer transition"
|
||||
>
|
||||
<input
|
||||
ref={extraRef}
|
||||
@ -233,8 +257,8 @@ export default function CompanyIdUploadPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
|
||||
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
|
||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-4 transition" />
|
||||
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
|
||||
Click to upload back side
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
@ -249,11 +273,11 @@ export default function CompanyIdUploadPage() {
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
|
||||
<p className="text-sm font-semibold text-indigo-900 mb-3">
|
||||
<div className="mt-8 rounded-lg bg-[#8D6B1D]/10 border border-[#8D6B1D]/20 px-5 py-5">
|
||||
<p className="text-sm font-semibold text-[#3B2C04] mb-3">
|
||||
Please ensure your ID documents:
|
||||
</p>
|
||||
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5">
|
||||
<ul className="text-sm text-[#7A5E1A] space-y-1 list-disc pl-5">
|
||||
<li>Are clearly visible and readable</li>
|
||||
<li>Show all four corners</li>
|
||||
<li>Are not expired</li>
|
||||
@ -273,11 +297,20 @@ export default function CompanyIdUploadPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex justify-end">
|
||||
{/* CHANGED: add "Back to Dashboard" next to submit */}
|
||||
<div className="mt-8 flex items-center justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBackToDashboard}
|
||||
className="inline-flex items-center justify-center rounded-md border border-[#8D6B1D] bg-white/70 px-4 py-3 text-sm font-semibold text-[#8D6B1D] hover:bg-[#8D6B1D]/10 transition"
|
||||
>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || success}
|
||||
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
|
||||
className="inline-flex items-center justify-center rounded-md bg-[#8D6B1D] px-6 py-3 text-sm font-semibold text-white shadow hover:bg-[#7A5E1A] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
|
||||
>
|
||||
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
|
||||
</button>
|
||||
@ -285,7 +318,7 @@ export default function CompanyIdUploadPage() {
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</BlueBlurryBackground>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,10 +2,12 @@
|
||||
|
||||
import { usePersonalUploadId } from './hooks/usePersonalUploadId'
|
||||
import PageLayout from '../../../components/PageLayout'
|
||||
import { useEffect, useState } from 'react'
|
||||
import BlueBlurryBackground from '../../../components/background/blueblurry'
|
||||
import { useEffect, useState, useCallback, useRef } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import useAuthStore from '../../../store/authStore'
|
||||
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||
|
||||
// Add back ID types for the dropdown
|
||||
const ID_TYPES = [
|
||||
@ -16,29 +18,54 @@ const ID_TYPES = [
|
||||
]
|
||||
|
||||
export default function PersonalIdUploadPage() {
|
||||
// NEW: guard company users from accessing personal page
|
||||
const user = useAuthStore(s => s.user)
|
||||
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||
const router = useRouter()
|
||||
const [blocked, setBlocked] = useState(false)
|
||||
const { userStatus, loading: statusLoading } = useUserStatus()
|
||||
|
||||
// NEW: smooth redirect
|
||||
const [redirectTo, setRedirectTo] = useState<string | null>(null)
|
||||
const redirectOnceRef = useRef(false)
|
||||
const smoothReplace = useCallback((to: string) => {
|
||||
if (redirectOnceRef.current) return
|
||||
redirectOnceRef.current = true
|
||||
setRedirectTo(to)
|
||||
window.setTimeout(() => router.replace(to), 200)
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
const ut = (user as any)?.userType || (user as any)?.role
|
||||
console.log('🧭 UploadID Guard [personal]: userType =', ut)
|
||||
if (ut && ut !== 'personal') {
|
||||
console.warn('🚫 UploadID Guard [personal]: access denied for userType:', ut, '-> redirecting to company upload')
|
||||
setBlocked(true)
|
||||
router.replace('/quickaction-dashboard/register-upload-id/company')
|
||||
} else if (ut === 'personal') {
|
||||
console.log('✅ UploadID Guard [personal]: access granted')
|
||||
}
|
||||
}, [user, router])
|
||||
if (ut && ut !== 'personal') smoothReplace('/quickaction-dashboard/register-upload-id/company') // CHANGED
|
||||
}, [user, smoothReplace])
|
||||
|
||||
if (blocked) {
|
||||
useEffect(() => {
|
||||
if (statusLoading || !userStatus) return
|
||||
const allDone =
|
||||
!!userStatus.email_verified &&
|
||||
!!userStatus.documents_uploaded &&
|
||||
!!userStatus.profile_completed &&
|
||||
!!userStatus.contract_signed
|
||||
|
||||
if (allDone) {
|
||||
smoothReplace('/dashboard') // CHANGED
|
||||
} else if (userStatus.documents_uploaded) {
|
||||
smoothReplace('/quickaction-dashboard') // CHANGED
|
||||
}
|
||||
}, [statusLoading, userStatus, smoothReplace])
|
||||
|
||||
// NEW: must be logged in
|
||||
useEffect(() => {
|
||||
if (!isAuthReady) return
|
||||
if (!user) smoothReplace('/login')
|
||||
}, [isAuthReady, user, smoothReplace])
|
||||
|
||||
if (redirectTo) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="min-h-[50vh] flex items-center justify-center">
|
||||
<div className="text-center text-sm text-gray-600">
|
||||
Redirecting to the correct upload page…
|
||||
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
||||
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||
<div className="text-sm font-medium text-gray-900">Redirecting…</div>
|
||||
<div className="mt-1 text-xs text-gray-600">Please wait</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
@ -58,20 +85,15 @@ export default function PersonalIdUploadPage() {
|
||||
inputBase,
|
||||
} = usePersonalUploadId()
|
||||
|
||||
const goBackToDashboard = () => {
|
||||
// CHANGED: do NOT preserve ?tutorial=true, otherwise dashboard force-opens the tutorial again
|
||||
router.push('/quickaction-dashboard')
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
||||
{/* Animated background (same as dashboard) */}
|
||||
<div className="pointer-events-none absolute inset-0 z-0">
|
||||
{/* Soft gradient blobs */}
|
||||
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
|
||||
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
|
||||
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
|
||||
{/* Subtle radial highlight */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
|
||||
</div>
|
||||
|
||||
<main className="relative z-10 flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
|
||||
<BlueBlurryBackground>
|
||||
<main className="flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
|
||||
<form
|
||||
onSubmit={submit}
|
||||
className="relative max-w-7xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10 overflow-hidden"
|
||||
@ -94,7 +116,7 @@ export default function PersonalIdUploadPage() {
|
||||
value={idNumber}
|
||||
onChange={e => setIdNumber(e.target.value)}
|
||||
placeholder="Enter your ID number"
|
||||
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`}
|
||||
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'} focus:ring-[#8D6B1D] focus:border-transparent`}
|
||||
required
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-600">
|
||||
@ -109,7 +131,7 @@ export default function PersonalIdUploadPage() {
|
||||
<select
|
||||
value={idType}
|
||||
onChange={e => setIdType(e.target.value)}
|
||||
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`}
|
||||
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'} focus:ring-[#8D6B1D] focus:border-transparent`}
|
||||
required
|
||||
>
|
||||
<option value="">Select ID type</option>
|
||||
@ -130,7 +152,7 @@ export default function PersonalIdUploadPage() {
|
||||
value={expiry}
|
||||
onChange={e => setExpiry(e.target.value)}
|
||||
placeholder="tt.mm jjjj"
|
||||
className={`${inputBase} ${expiry ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
|
||||
className={`${inputBase} ${expiry ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80 focus:ring-[#8D6B1D] focus:border-transparent`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@ -145,7 +167,7 @@ export default function PersonalIdUploadPage() {
|
||||
type="button"
|
||||
onClick={() => setHasBack(v => !v)}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
|
||||
hasBack ? 'bg-indigo-600' : 'bg-gray-300'
|
||||
hasBack ? 'bg-[#8D6B1D]' : 'bg-gray-300'
|
||||
}`}
|
||||
aria-pressed={hasBack}
|
||||
>
|
||||
@ -165,7 +187,7 @@ export default function PersonalIdUploadPage() {
|
||||
{...dropEvents}
|
||||
onDrop={e => onDrop(e, 'front')}
|
||||
onClick={() => openPicker('front')}
|
||||
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
|
||||
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-[#8D6B1D] hover:bg-[#8D6B1D]/5 cursor-pointer transition"
|
||||
>
|
||||
<input
|
||||
ref={frontInputRef}
|
||||
@ -195,8 +217,8 @@ export default function PersonalIdUploadPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
|
||||
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
|
||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-3 transition" />
|
||||
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
|
||||
Click to upload front side
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
@ -214,7 +236,7 @@ export default function PersonalIdUploadPage() {
|
||||
{...dropEvents}
|
||||
onDrop={e => onDrop(e, 'back')}
|
||||
onClick={() => openPicker('back')}
|
||||
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
|
||||
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-[#8D6B1D] hover:bg-[#8D6B1D]/5 cursor-pointer transition"
|
||||
>
|
||||
<input
|
||||
ref={backInputRef}
|
||||
@ -244,8 +266,8 @@ export default function PersonalIdUploadPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
|
||||
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
|
||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-3 transition" />
|
||||
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
|
||||
Click to upload back side
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-gray-500">
|
||||
@ -260,11 +282,11 @@ export default function PersonalIdUploadPage() {
|
||||
</div>
|
||||
|
||||
{/* Info Box, errors, success, submit */}
|
||||
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
|
||||
<p className="text-sm font-semibold text-indigo-900 mb-3">
|
||||
<div className="mt-8 rounded-lg bg-[#8D6B1D]/10 border border-[#8D6B1D]/20 px-5 py-5">
|
||||
<p className="text-sm font-semibold text-[#3B2C04] mb-3">
|
||||
Please ensure your ID documents:
|
||||
</p>
|
||||
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5">
|
||||
<ul className="text-sm text-[#7A5E1A] space-y-1 list-disc pl-5">
|
||||
<li>Are clearly visible and readable</li>
|
||||
<li>Show all four corners</li>
|
||||
<li>Are not expired</li>
|
||||
@ -284,11 +306,20 @@ export default function PersonalIdUploadPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-8 flex justify-end">
|
||||
{/* CHANGED: add "Back to Dashboard" next to submit */}
|
||||
<div className="mt-8 flex items-center justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBackToDashboard}
|
||||
className="inline-flex items-center justify-center rounded-md border border-[#8D6B1D] bg-white/70 px-4 py-3 text-sm font-semibold text-[#8D6B1D] hover:bg-[#8D6B1D]/10 transition"
|
||||
>
|
||||
Back to Dashboard
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || success}
|
||||
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
|
||||
className="inline-flex items-center justify-center rounded-md bg-[#8D6B1D] px-6 py-3 text-sm font-semibold text-white shadow hover:bg-[#7A5E1A] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
|
||||
>
|
||||
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
|
||||
</button>
|
||||
@ -296,7 +327,7 @@ export default function PersonalIdUploadPage() {
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
</BlueBlurryBackground>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useState } from 'react'
|
||||
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline'
|
||||
import { useToast } from '../../components/toast/toastComponent'
|
||||
|
||||
interface ReferralLink {
|
||||
id?: string | number
|
||||
@ -39,6 +40,7 @@ function shortLink(href?: string) {
|
||||
}
|
||||
|
||||
export default function ReferralLinksListWidget({ links, onDeactivate }: Props) {
|
||||
const { showToast } = useToast()
|
||||
// Local floating tooltip (fixed) so table doesn't scroll to show it
|
||||
const [tooltip, setTooltip] = useState<{ visible: boolean; text: string; x: number; y: number }>({
|
||||
visible: false,
|
||||
@ -54,6 +56,24 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
|
||||
}
|
||||
const hideTooltip = () => setTooltip(t => ({ ...t, visible: false }))
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Copied',
|
||||
message: 'Link copied to Zwischenablage.',
|
||||
duration: 2500,
|
||||
})
|
||||
} catch {
|
||||
showToast({
|
||||
variant: 'error',
|
||||
title: 'Copy failed',
|
||||
message: 'Could not copy link to clipboard.',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-8 bg-white rounded-lg shadow-sm border border-gray-200">
|
||||
@ -147,15 +167,16 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
|
||||
</a>
|
||||
{/* Desktop/Tablet copy button */}
|
||||
<button
|
||||
onClick={async () => { try { await navigator.clipboard.writeText(l.url || String(l.code || '')); } catch {} }}
|
||||
className="hidden md:inline-flex items-center gap-1 rounded border border-gray-300 px-2 py-1 text-xs text-gray-700 hover:bg-gray-50"
|
||||
onClick={() => copyToClipboard(l.url || String(l.code || ''))}
|
||||
className="hidden md:inline-flex items-center gap-1 rounded border border-gray-300 px-2 py-1 text-xs text-gray-700
|
||||
transition-all duration-150 hover:bg-gray-50 hover:shadow-sm hover:-translate-y-0.5 active:translate-y-0"
|
||||
>
|
||||
<ClipboardDocumentIcon className="h-4 w-4" />
|
||||
Copy
|
||||
</button>
|
||||
{/* Mobile: only copy button */}
|
||||
<button
|
||||
onClick={async () => { try { await navigator.clipboard.writeText(l.url || String(l.code || '')); } catch {} }}
|
||||
onClick={() => copyToClipboard(l.url || String(l.code || ''))}
|
||||
className="inline-flex md:hidden items-center gap-2 rounded border border-gray-300 px-3 py-2 text-xs text-gray-700 hover:bg-gray-50"
|
||||
aria-label="Copy referral link"
|
||||
>
|
||||
@ -197,7 +218,13 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
|
||||
<button
|
||||
disabled={l.status !== 'active'}
|
||||
onClick={() => onDeactivate(l)}
|
||||
className="inline-flex items-center rounded-md border border-red-300 px-3 py-1.5 text-sm text-red-700 hover:bg-red-50 disabled:opacity-50"
|
||||
className="
|
||||
inline-flex items-center rounded-md border border-red-300 px-3 py-1.5 text-sm text-red-700
|
||||
transition-all duration-150
|
||||
md:hover:-translate-y-0.5 md:hover:shadow-sm md:hover:bg-red-50
|
||||
active:translate-y-0
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:shadow-none
|
||||
"
|
||||
>
|
||||
Deactivate
|
||||
</button>
|
||||
|
||||
@ -12,8 +12,10 @@ import RegisteredUserList from './components/registeredUserList'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import useAuthStore from '../store/authStore'
|
||||
import { useRegisteredUsers } from './hooks/registeredUsers'
|
||||
import { ToastProvider, useToast } from '../components/toast/toastComponent' // NEW
|
||||
|
||||
export default function ReferralManagementPage() {
|
||||
function ReferralManagementPageInner() {
|
||||
const { showToast } = useToast() // NEW
|
||||
const router = useRouter()
|
||||
const user = useAuthStore(s => s.user)
|
||||
const isAuthReady = useAuthStore(s => s.isAuthReady)
|
||||
@ -48,11 +50,30 @@ export default function ReferralManagementPage() {
|
||||
try {
|
||||
const tokenId = selectedLink?.tokenId ?? selectedLink?.id
|
||||
if (tokenId == null) return
|
||||
|
||||
const res = await deactivateReferralLink(tokenId)
|
||||
console.log('✅ Deactivate result:', res)
|
||||
|
||||
if (res?.ok) {
|
||||
showToast({
|
||||
variant: 'success',
|
||||
title: 'Link deactivated',
|
||||
message: 'The referral link has been deactivated successfully.',
|
||||
})
|
||||
} else {
|
||||
showToast({
|
||||
variant: 'error',
|
||||
title: 'Deactivate failed',
|
||||
message: (res as any)?.body?.message || 'Could not deactivate the referral link.',
|
||||
})
|
||||
}
|
||||
|
||||
await loadData()
|
||||
} catch (e) {
|
||||
// optional: toast error
|
||||
} catch (e: any) {
|
||||
showToast({
|
||||
variant: 'error',
|
||||
title: 'Deactivate failed',
|
||||
message: 'Network error while deactivating the referral link.',
|
||||
})
|
||||
} finally {
|
||||
setDeactivatePending(false)
|
||||
setDeactivateOpen(false)
|
||||
@ -71,7 +92,6 @@ export default function ReferralManagementPage() {
|
||||
const run = async () => {
|
||||
if (!isAuthReady) return
|
||||
if (!user) {
|
||||
console.log('🔐 referral-management: no user, redirect to /login')
|
||||
router.replace('/login')
|
||||
return
|
||||
}
|
||||
@ -79,11 +99,15 @@ export default function ReferralManagementPage() {
|
||||
// Resolve user id
|
||||
const uid = (user as any)?.id ?? (user as any)?._id ?? (user as any)?.userId
|
||||
if (!uid) {
|
||||
console.warn('⚠️ referral-management: user id missing, denying access')
|
||||
if (!cancelled) {
|
||||
setHasReferralPerm(false)
|
||||
setIsPermChecked(true)
|
||||
}
|
||||
showToast({
|
||||
variant: 'error',
|
||||
title: 'Access check failed',
|
||||
message: 'User id is missing. Redirecting…',
|
||||
})
|
||||
router.replace('/dashboard')
|
||||
return
|
||||
}
|
||||
@ -112,9 +136,7 @@ export default function ReferralManagementPage() {
|
||||
...(tokenToUse ? { Authorization: `Bearer ${tokenToUse}` } : {})
|
||||
}
|
||||
})
|
||||
console.log('📡 referral-management: permissions status:', res.status)
|
||||
const body = await res.json().catch(() => null)
|
||||
console.log('📦 referral-management: permissions body:', body)
|
||||
|
||||
const permsSrc = body?.data?.permissions ?? body?.permissions ?? body
|
||||
let can = false
|
||||
@ -133,22 +155,30 @@ export default function ReferralManagementPage() {
|
||||
}
|
||||
|
||||
if (!can) {
|
||||
console.log('⛔ referral-management: missing permission, redirect to /dashboard')
|
||||
showToast({
|
||||
variant: 'warning',
|
||||
title: 'Access denied',
|
||||
message: 'You do not have permission to access Referral Management.',
|
||||
})
|
||||
router.replace('/dashboard')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('❌ referral-management: fetch permissions error:', e)
|
||||
if (!cancelled) {
|
||||
setHasReferralPerm(false)
|
||||
setIsPermChecked(true)
|
||||
}
|
||||
showToast({
|
||||
variant: 'error',
|
||||
title: 'Permission check failed',
|
||||
message: 'Could not verify permissions. Redirecting…',
|
||||
})
|
||||
router.replace('/dashboard')
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
return () => { cancelled = true }
|
||||
}, [isAuthReady, user, accessToken, refreshAuthToken, router])
|
||||
}, [isAuthReady, user, accessToken, refreshAuthToken, router, showToast]) // CHANGED: add showToast
|
||||
|
||||
// Helper: normalize list payload shapes
|
||||
const normalizeList = (raw: any): any[] => {
|
||||
@ -187,8 +217,22 @@ export default function ReferralManagementPage() {
|
||||
// Helper: fetch stats + list
|
||||
const loadData = async () => {
|
||||
const [statsRes, listRes] = await Promise.all([fetchReferralStats(), fetchReferralList()])
|
||||
console.log('✅ Referral stats fetched:', statsRes)
|
||||
console.log('✅ Referral list fetched:', listRes)
|
||||
|
||||
if (!statsRes.ok) {
|
||||
showToast({
|
||||
variant: 'error',
|
||||
title: 'Load failed',
|
||||
message: 'Could not load referral statistics.',
|
||||
})
|
||||
}
|
||||
if (!listRes.ok) {
|
||||
showToast({
|
||||
variant: 'error',
|
||||
title: 'Load failed',
|
||||
message: 'Could not load referral links.',
|
||||
})
|
||||
}
|
||||
|
||||
if (statsRes.ok && statsRes.body) {
|
||||
const b: any = statsRes.body?.data || statsRes.body?.stats || statsRes.body
|
||||
setStats({
|
||||
@ -259,8 +303,10 @@ export default function ReferralManagementPage() {
|
||||
loading={usersLoading}
|
||||
/>
|
||||
|
||||
{/* Generator */}
|
||||
<GenerateReferralLinkWidget onCreated={loadData} />
|
||||
{/* Generator (wrapped for desktop hover animation) */}
|
||||
<div className="transition-all duration-200 md:hover:-translate-y-0.5 md:hover:shadow-md md:hover:shadow-black/5">
|
||||
<GenerateReferralLinkWidget onCreated={loadData} />
|
||||
</div>
|
||||
|
||||
{/* Referral links list (refactored) */}
|
||||
<ReferralLinksListWidget links={links} onDeactivate={openDeactivateModal} />
|
||||
@ -278,4 +324,12 @@ export default function ReferralManagementPage() {
|
||||
/>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default function ReferralManagementPage() {
|
||||
return (
|
||||
<ToastProvider>
|
||||
<ReferralManagementPageInner />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
@ -8,7 +8,8 @@ import PageLayout from '../components/PageLayout'
|
||||
import SessionDetectedModal from './components/SessionDetectedModal'
|
||||
import InvalidRefLinkModal from './components/invalidRefLinkModal'
|
||||
import { ToastProvider, useToast } from '../components/toast/toastComponent'
|
||||
import Waves from '../components/waves'
|
||||
import Waves from '../components/background/waves'
|
||||
import BlueBlurryBackground from '../components/background/blueblurry' // NEW
|
||||
|
||||
// NEW: inner component that actually uses useToast and all the logic
|
||||
function RegisterPageInner() {
|
||||
@ -161,26 +162,36 @@ function RegisterPageInner() {
|
||||
opacity: 'var(--pp-page-shift-opacity, 1)',
|
||||
}
|
||||
|
||||
const BackgroundShell = ({ children }: { children: React.ReactNode }) => {
|
||||
return isMobile ? (
|
||||
<BlueBlurryBackground className="min-h-screen w-full">{children}</BlueBlurryBackground>
|
||||
) : (
|
||||
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
|
||||
<Waves
|
||||
className="pointer-events-none"
|
||||
lineColor="#0f172a"
|
||||
backgroundColor="rgba(245, 245, 240, 1)"
|
||||
waveSpeedX={0.02}
|
||||
waveSpeedY={0.01}
|
||||
waveAmpX={40}
|
||||
waveAmpY={20}
|
||||
friction={0.9}
|
||||
tension={0.01}
|
||||
maxCursorMove={120}
|
||||
xGap={12}
|
||||
yGap={36}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Render branches (unchanged except classNames) ---
|
||||
|
||||
if (!isRefChecked) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
|
||||
<Waves
|
||||
className="pointer-events-none"
|
||||
lineColor="#0f172a"
|
||||
backgroundColor="rgba(245, 245, 240, 1)"
|
||||
waveSpeedX={0.02}
|
||||
waveSpeedY={0.01}
|
||||
waveAmpX={40}
|
||||
waveAmpY={20}
|
||||
friction={0.9}
|
||||
tension={0.01}
|
||||
maxCursorMove={120}
|
||||
xGap={12}
|
||||
yGap={36}
|
||||
/>
|
||||
<BackgroundShell>
|
||||
<main
|
||||
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
|
||||
style={mainStyle}
|
||||
@ -203,7 +214,7 @@ function RegisterPageInner() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</BackgroundShell>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
@ -211,21 +222,7 @@ function RegisterPageInner() {
|
||||
if (invalidRef) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
|
||||
<Waves
|
||||
className="pointer-events-none"
|
||||
lineColor="#0f172a"
|
||||
backgroundColor="rgba(245, 245, 240, 1)"
|
||||
waveSpeedX={0.02}
|
||||
waveSpeedY={0.01}
|
||||
waveAmpX={40}
|
||||
waveAmpY={20}
|
||||
friction={0.9}
|
||||
tension={0.01}
|
||||
maxCursorMove={120}
|
||||
xGap={12}
|
||||
yGap={36}
|
||||
/>
|
||||
<BackgroundShell>
|
||||
<main
|
||||
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
|
||||
style={mainStyle}
|
||||
@ -253,7 +250,7 @@ function RegisterPageInner() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</BackgroundShell>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
@ -261,21 +258,7 @@ function RegisterPageInner() {
|
||||
// normal register
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
|
||||
<Waves
|
||||
className="pointer-events-none"
|
||||
lineColor="#0f172a"
|
||||
backgroundColor="rgba(245, 245, 240, 1)"
|
||||
waveSpeedX={0.02}
|
||||
waveSpeedY={0.01}
|
||||
waveAmpX={40}
|
||||
waveAmpY={20}
|
||||
friction={0.9}
|
||||
tension={0.01}
|
||||
maxCursorMove={120}
|
||||
xGap={12}
|
||||
yGap={36}
|
||||
/>
|
||||
<BackgroundShell>
|
||||
<main
|
||||
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
|
||||
style={mainStyle}
|
||||
@ -334,7 +317,7 @@ function RegisterPageInner() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</BackgroundShell>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user