feat: mobile
This commit is contained in:
parent
1045debc32
commit
6dfaedcab6
@ -1,7 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import PageLayout from '../components/PageLayout'
|
import PageLayout from '../components/PageLayout'
|
||||||
import Waves from '../components/waves'
|
import Waves from '../components/background/waves'
|
||||||
|
import BlueBlurryBackground from '../components/background/blueblurry' // NEW
|
||||||
import {
|
import {
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
ExclamationTriangleIcon,
|
ExclamationTriangleIcon,
|
||||||
@ -25,12 +26,25 @@ export default function AdminDashboardPage() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { userStats, isAdmin } = useAdminUsers()
|
const { userStats, isAdmin } = useAdminUsers()
|
||||||
const [isClient, setIsClient] = useState(false)
|
const [isClient, setIsClient] = useState(false)
|
||||||
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
|
|
||||||
// Handle client-side mounting
|
// Handle client-side mounting
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsClient(true)
|
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
|
// Fallback for loading/no data
|
||||||
const displayStats = userStats || {
|
const displayStats = userStats || {
|
||||||
totalUsers: 0,
|
totalUsers: 0,
|
||||||
@ -83,96 +97,76 @@ export default function AdminDashboardPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const content = (
|
||||||
<PageLayout>
|
<div className="relative z-10 min-h-screen flex flex-col">
|
||||||
<div
|
<main className="flex-1 max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8 w-full">
|
||||||
className="relative w-full flex flex-col min-h-screen overflow-hidden"
|
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-6 sm:p-8">
|
||||||
style={{ backgroundImage: 'none', background: 'none' }}
|
{/* Header */}
|
||||||
>
|
<header className="flex flex-col gap-4 mb-8">
|
||||||
<Waves
|
<div>
|
||||||
className="pointer-events-none"
|
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Admin Dashboard</h1>
|
||||||
lineColor="#0f172a"
|
<p className="text-lg text-blue-700 mt-2">
|
||||||
backgroundColor="rgba(245, 245, 240, 1)"
|
Manage all administrative features, user management, permissions, and global settings.
|
||||||
waveSpeedX={0.02}
|
</p>
|
||||||
waveSpeedY={0.01}
|
</div>
|
||||||
waveAmpX={40}
|
</header>
|
||||||
waveAmpY={20}
|
|
||||||
friction={0.9}
|
|
||||||
tension={0.01}
|
|
||||||
maxCursorMove={120}
|
|
||||||
xGap={12}
|
|
||||||
yGap={36}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative z-10 min-h-screen flex flex-col">
|
{/* Warning banner */}
|
||||||
<main className="flex-1 max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8 w-full">
|
<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">
|
||||||
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-6 sm:p-8">
|
<ExclamationTriangleIcon className="h-6 w-6 flex-shrink-0 text-red-500 mt-0.5" />
|
||||||
{/* Header */}
|
<div className="leading-relaxed">
|
||||||
<header className="flex flex-col gap-4 mb-8">
|
<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>
|
<div>
|
||||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Admin Dashboard</h1>
|
<h2 className="text-lg font-semibold text-blue-900">Management Shortcuts</h2>
|
||||||
<p className="text-lg text-blue-700 mt-2">
|
<p className="text-sm text-blue-700 mt-0.5">
|
||||||
Manage all administrative features, user management, permissions, and global settings.
|
Quick access to common admin modules.
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Warning banner */}
|
|
||||||
<div className="rounded-2xl border border-red-300 bg-red-50 text-red-700 px-8 py-6 flex gap-3 items-start text-base mb-8 shadow">
|
|
||||||
<ExclamationTriangleIcon className="h-6 w-6 flex-shrink-0 text-red-500 mt-0.5" />
|
|
||||||
<div className="leading-relaxed">
|
|
||||||
<p className="font-semibold mb-0.5">
|
|
||||||
Warning: Settings and actions below this point can have consequences for the entire system!
|
|
||||||
</p>
|
|
||||||
<p className="text-red-600/80 hidden sm:block">
|
|
||||||
Manage all administrative features, user management, permissions, and global settings.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Stats Card */}
|
|
||||||
<div className="mb-8 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-6">
|
|
||||||
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
|
|
||||||
<div className="text-xs text-gray-500">Total Users</div>
|
|
||||||
<div className="text-xl font-semibold text-blue-900">{displayStats.totalUsers}</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
|
|
||||||
<div className="text-xs text-gray-500">Admins</div>
|
|
||||||
<div className="text-xl font-semibold text-indigo-700">{displayStats.adminUsers}</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
|
|
||||||
<div className="text-xs text-gray-500">Active</div>
|
|
||||||
<div className="text-xl font-semibold text-green-700">{displayStats.activeUsers}</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
|
|
||||||
<div className="text-xs text-gray-500">Pending Verification</div>
|
|
||||||
<div className="text-xl font-semibold text-amber-700">{displayStats.verificationPending}</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
|
|
||||||
<div className="text-xs text-gray-500">Personal</div>
|
|
||||||
<div className="text-xl font-semibold text-blue-700">{displayStats.personalUsers}</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
|
|
||||||
<div className="text-xs text-gray-500">Company</div>
|
|
||||||
<div className="text-xl font-semibold text-purple-700">{displayStats.companyUsers}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Management Shortcuts Card */}
|
|
||||||
<div className="mb-8">
|
|
||||||
<div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg hover:shadow-xl transition">
|
|
||||||
<div className="flex items-start gap-4 mb-6">
|
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100">
|
|
||||||
<Squares2X2Icon className="h-7 w-7 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-blue-900">Management Shortcuts</h2>
|
|
||||||
<p className="text-sm text-blue-700 mt-0.5">
|
|
||||||
Quick access to common admin modules.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{/* Matrix Management */}
|
{/* Matrix Management */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -324,82 +318,109 @@ export default function AdminDashboardPage() {
|
|||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Server Status & Logs */}
|
||||||
|
<div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg hover:shadow-xl transition">
|
||||||
|
<div className="flex items-start gap-4 mb-6">
|
||||||
|
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gray-100">
|
||||||
|
<ServerStackIcon className="h-7 w-7 text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900">
|
||||||
|
Server Status & Logs
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500 mt-0.5">
|
||||||
|
System health, resource usage & recent error insights.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-8 lg:grid-cols-3">
|
||||||
|
{/* Metrics */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className={`h-2.5 w-2.5 rounded-full ${serverStats.status === 'Online' ? 'bg-emerald-500' : 'bg-red-500'}`} />
|
||||||
|
<p className="text-base">
|
||||||
|
<span className="font-semibold">Server Status:</span>{' '}
|
||||||
|
<span className={serverStats.status === 'Online' ? 'text-emerald-600 font-medium' : 'text-red-600 font-medium'}>
|
||||||
|
{serverStats.status === 'Online' ? 'Server Online' : 'Offline'}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm space-y-1 text-gray-600">
|
||||||
|
<p><span className="font-medium text-gray-700">Uptime:</span> {serverStats.uptime}</p>
|
||||||
|
<p><span className="font-medium text-gray-700">CPU Usage:</span> {serverStats.cpu}</p>
|
||||||
|
<p><span className="font-medium text-gray-700">Memory Usage:</span> {serverStats.memory} GB</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||||||
|
<CpuChipIcon className="h-4 w-4" />
|
||||||
|
<span>Autoscaled environment (mock)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Server Status & Logs */}
|
{/* Divider */}
|
||||||
<div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg hover:shadow-xl transition">
|
<div className="hidden lg:block border-l border-gray-200" />
|
||||||
<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">
|
{/* Logs */}
|
||||||
{/* Metrics */}
|
<div className="lg:col-span-2">
|
||||||
<div className="space-y-4">
|
<h3 className="text-base font-semibold text-gray-800 mb-3">
|
||||||
<div className="flex items-center gap-3">
|
Recent Error Logs
|
||||||
<span className={`h-2.5 w-2.5 rounded-full ${serverStats.status === 'Online' ? 'bg-emerald-500' : 'bg-red-500'}`} />
|
</h3>
|
||||||
<p className="text-base">
|
{serverStats.recentErrors.length === 0 && (
|
||||||
<span className="font-semibold">Server Status:</span>{' '}
|
<p className="text-sm text-gray-500 italic">
|
||||||
<span className={serverStats.status === 'Online' ? 'text-emerald-600 font-medium' : 'text-red-600 font-medium'}>
|
No recent logs.
|
||||||
{serverStats.status === 'Online' ? 'Server Online' : 'Offline'}
|
</p>
|
||||||
</span>
|
)}
|
||||||
</p>
|
{/* Placeholder for future logs list */}
|
||||||
</div>
|
{/* TODO: Replace with mapped log entries */}
|
||||||
<div className="text-sm space-y-1 text-gray-600">
|
<div className="mt-6">
|
||||||
<p><span className="font-medium text-gray-700">Uptime:</span> {serverStats.uptime}</p>
|
<button
|
||||||
<p><span className="font-medium text-gray-700">CPU Usage:</span> {serverStats.cpu}</p>
|
type="button"
|
||||||
<p><span className="font-medium text-gray-700">Memory Usage:</span> {serverStats.memory} GB</p>
|
className="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-gray-50 hover:bg-gray-100 text-gray-700 text-sm font-medium px-4 py-3 transition"
|
||||||
</div>
|
// TODO: navigate to logs / monitoring page
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
onClick={() => {}}
|
||||||
<CpuChipIcon className="h-4 w-4" />
|
>
|
||||||
<span>Autoscaled environment (mock)</span>
|
View Full Logs
|
||||||
</div>
|
<ArrowRightIcon className="h-5 w-5" />
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
{/* Divider */}
|
|
||||||
<div className="hidden lg:block border-l border-gray-200" />
|
|
||||||
|
|
||||||
{/* Logs */}
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<h3 className="text-base font-semibold text-gray-800 mb-3">
|
|
||||||
Recent Error Logs
|
|
||||||
</h3>
|
|
||||||
{serverStats.recentErrors.length === 0 && (
|
|
||||||
<p className="text-sm text-gray-500 italic">
|
|
||||||
No recent logs.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{/* Placeholder for future logs list */}
|
|
||||||
{/* TODO: Replace with mapped log entries */}
|
|
||||||
<div className="mt-6">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-gray-50 hover:bg-gray-100 text-gray-700 text-sm font-medium px-4 py-3 transition"
|
|
||||||
// TODO: navigate to logs / monitoring page
|
|
||||||
onClick={() => {}}
|
|
||||||
>
|
|
||||||
View Full Logs
|
|
||||||
<ArrowRightIcon className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
</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>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useMemo } from 'react'
|
import { useEffect, useState, useMemo } from 'react'
|
||||||
import PageLayout from '../components/PageLayout'
|
import PageLayout from '../components/PageLayout'
|
||||||
import Waves from '../components/waves'
|
import Waves from '../components/background/waves'
|
||||||
|
|
||||||
type Affiliate = {
|
type Affiliate = {
|
||||||
id: string
|
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>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10">
|
<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
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
@ -94,8 +94,9 @@ export default function TutorialModal({
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative w-full max-w-5xl h-[60vh]">
|
{/* CHANGED: mobile uses max-height + scrolling */}
|
||||||
<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">
|
<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 */}
|
{/* Background Gradient */}
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 1024 1024"
|
viewBox="0 0 1024 1024"
|
||||||
@ -121,20 +122,20 @@ export default function TutorialModal({
|
|||||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Content Section - Left Half */}
|
{/* CHANGED: content scrolls on mobile */}
|
||||||
<div className="lg:flex-1 lg:max-w-md text-center lg:text-left py-8 px-6 flex flex-col justify-center">
|
<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 */}
|
{/* 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">
|
<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" />
|
<step.icon className="h-6 w-6 text-blue-600" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* CHANGED: allow wrapping on mobile (remove nowrap) */}
|
||||||
<h2 className="text-xl font-semibold tracking-tight whitespace-nowrap text-gray-800 sm:text-2xl">
|
<h2 className="text-xl font-semibold tracking-tight text-gray-800 sm:text-2xl">
|
||||||
{step.title}
|
{step.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Description */}
|
{/* CHANGED: no fixed height clipping on mobile */}
|
||||||
<p className="mt-3 text-sm text-gray-600 leading-relaxed h-12 overflow-hidden">
|
<p className="mt-3 text-sm text-gray-600 leading-relaxed sm:h-12 sm:overflow-hidden">
|
||||||
{step.description}
|
{step.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -209,8 +210,8 @@ export default function TutorialModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Visual Section - Right Half */}
|
{/* CHANGED: hide unused visual section on mobile */}
|
||||||
<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">
|
<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
|
{/* <img
|
||||||
src="/images/misc/cow.png"
|
src="/images/misc/cow.png"
|
||||||
alt="Profit Planet Mascot"
|
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,
|
Disclosure,
|
||||||
DisclosureButton,
|
DisclosureButton,
|
||||||
DisclosurePanel,
|
DisclosurePanel,
|
||||||
Popover,
|
|
||||||
PopoverButton,
|
|
||||||
PopoverGroup,
|
PopoverGroup,
|
||||||
PopoverPanel,
|
|
||||||
Transition,
|
Transition,
|
||||||
} from '@headlessui/react'
|
} from '@headlessui/react'
|
||||||
import {
|
import {
|
||||||
@ -99,22 +96,17 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
console.error('Logout failed:', err)
|
console.error('Logout failed:', err)
|
||||||
setGlobalLoggingOut?.(false)
|
setGlobalLoggingOut?.(false)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Helper to get user initials for profile icon
|
// Helper to get user initials for profile icon
|
||||||
const getUserInitials = () => {
|
const getUserInitials = () => {
|
||||||
if (!user) return 'U';
|
if (!user) return 'U'
|
||||||
if (user.firstName || user.lastName) {
|
if (user.firstName || user.lastName) {
|
||||||
return (
|
return ((user.firstName?.[0] || '') + (user.lastName?.[0] || '')).toUpperCase()
|
||||||
(user.firstName?.[0] || '') +
|
|
||||||
(user.lastName?.[0] || '')
|
|
||||||
).toUpperCase();
|
|
||||||
}
|
}
|
||||||
if (user.email) {
|
if (user.email) return user.email[0].toUpperCase()
|
||||||
return user.email[0].toUpperCase();
|
return 'U'
|
||||||
}
|
}
|
||||||
return 'U';
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initial theme (dark/light) + mark mounted + start header animation
|
// Initial theme (dark/light) + mark mounted + start header animation
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -507,71 +499,18 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
{/* Information dropdown already removed here */}
|
{/* Information dropdown already removed here */}
|
||||||
</PopoverGroup>
|
</PopoverGroup>
|
||||||
|
|
||||||
<div className="hidden lg:flex lg:flex-1 lg:justify-end lg:items-center lg:gap-x-4">
|
{/* CHANGED: remove profile icon/popover from header; keep login (when logged out) + hamburger */}
|
||||||
{/* Auth slot */}
|
<div className="hidden lg:flex lg:flex-1 lg:justify-end lg:items-center lg:gap-x-3">
|
||||||
<div className="flex items-center">
|
{!userPresent && mounted && (
|
||||||
{userPresent ? (
|
<button
|
||||||
<Popover className="relative">
|
onClick={() => router.push('/login')}
|
||||||
<PopoverButton className="flex items-center gap-x-1 text-sm font-semibold text-gray-900 dark:text-white">
|
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
||||||
<Avatar
|
>
|
||||||
src=""
|
Log in <span aria-hidden="true">→</span>
|
||||||
initials={(() => {
|
</button>
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Desktop hamburger (right side, next to login/profile) */}
|
{/* Desktop hamburger (right side) */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMobileMenuOpen(open => !open)}
|
onClick={() => setMobileMenuOpen(open => !open)}
|
||||||
@ -789,16 +728,37 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : user ? (
|
) : user ? (
|
||||||
<>
|
<>
|
||||||
{/* User info + basic nav */}
|
{/* CHANGED: include profile icon INSIDE hamburger menu */}
|
||||||
<div className="pt-6 space-y-2">
|
<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="flex items-center gap-3 border-b border-white/10 pb-4 mb-4 px-3">
|
||||||
<div className="font-medium text-white">
|
<Avatar
|
||||||
{user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : (user?.email || 'User')}
|
src=""
|
||||||
</div>
|
initials={getUserInitials()}
|
||||||
<div className="text-sm text-gray-400">
|
className="size-10 bg-gradient-to-br from-indigo-500/40 to-indigo-600/60 text-white"
|
||||||
{user?.email || 'user@example.com'}
|
/>
|
||||||
|
<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>
|
||||||
</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 && (
|
{canSeeDashboard && (
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@ -182,13 +182,29 @@ function ToastViewport() {
|
|||||||
if (!toasts.length) return null
|
if (!toasts.length) return null
|
||||||
|
|
||||||
return (
|
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">
|
<div
|
||||||
{toasts.map(t => (
|
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"
|
||||||
<ToastItem key={t.id} toast={t} onClose={() => handleClose(t.id)} />
|
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>
|
||||||
</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'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState, useCallback, useRef } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import useAuthStore from '../store/authStore'
|
import useAuthStore from '../store/authStore'
|
||||||
import PageLayout from '../components/PageLayout'
|
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 {
|
import {
|
||||||
ShoppingBagIcon,
|
ShoppingBagIcon,
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
@ -21,6 +23,18 @@ export default function DashboardPage() {
|
|||||||
const isShopEnabled = process.env.NEXT_PUBLIC_SHOW_SHOP !== 'false'
|
const isShopEnabled = process.env.NEXT_PUBLIC_SHOW_SHOP !== 'false'
|
||||||
const [isMobile, setIsMobile] = useState(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(() => {
|
useEffect(() => {
|
||||||
const mq = window.matchMedia('(max-width: 768px)')
|
const mq = window.matchMedia('(max-width: 768px)')
|
||||||
const apply = () => setIsMobile(mq.matches)
|
const apply = () => setIsMobile(mq.matches)
|
||||||
@ -40,8 +54,22 @@ export default function DashboardPage() {
|
|||||||
}
|
}
|
||||||
}, [isAuthReady, user, router])
|
}, [isAuthReady, user, router])
|
||||||
|
|
||||||
// Show loading until auth is ready or user is confirmed
|
// NEW: block dashboard unless all 4 quickaction steps are completed
|
||||||
if (!isAuthReady || !user) {
|
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 (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="text-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
|
// Get user name
|
||||||
const getUserName = () => {
|
const getUserName = () => {
|
||||||
if (user.firstName && user.lastName) {
|
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
|
<div
|
||||||
className="relative w-full min-h-[100dvh] flex flex-col overflow-x-hidden"
|
className="relative w-full min-h-[100dvh] flex flex-col overflow-x-hidden"
|
||||||
style={{ backgroundImage: 'none', background: 'none' }}
|
style={{ backgroundImage: 'none', background: 'none' }}
|
||||||
@ -145,178 +367,10 @@ export default function DashboardPage() {
|
|||||||
maxCursorMove={120}
|
maxCursorMove={120}
|
||||||
xGap={12}
|
xGap={12}
|
||||||
yGap={36}
|
yGap={36}
|
||||||
animate={!isMobile}
|
animate
|
||||||
interactive={!isMobile}
|
interactive
|
||||||
/>
|
/>
|
||||||
|
{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>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -7,7 +7,8 @@ import PageLayout from '../components/PageLayout'
|
|||||||
import useAuthStore from '../store/authStore'
|
import useAuthStore from '../store/authStore'
|
||||||
import { ToastProvider } from '../components/toast/toastComponent'
|
import { ToastProvider } from '../components/toast/toastComponent'
|
||||||
import PageTransitionEffect from '../components/animation/pageTransitionEffect'
|
import PageTransitionEffect from '../components/animation/pageTransitionEffect'
|
||||||
import Waves from '../components/waves'
|
import Waves from '../components/background/waves'
|
||||||
|
import BlueBlurryBackground from '../components/background/blueblurry' // NEW
|
||||||
import CurvedLoop from '../components/curvedLoop'
|
import CurvedLoop from '../components/curvedLoop'
|
||||||
|
|
||||||
export default function LoginPage() {
|
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 (
|
return (
|
||||||
<PageTransitionEffect>
|
<PageTransitionEffect>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
{/* NEW: page-level background wrapper so Waves covers everything */}
|
{isMobile ? (
|
||||||
<div className="relative min-h-screen w-full overflow-x-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
|
<BlueBlurryBackground animate={false} className="w-full">
|
||||||
<Waves
|
{content}
|
||||||
className="pointer-events-none"
|
</BlueBlurryBackground>
|
||||||
lineColor="#0f172a"
|
) : (
|
||||||
backgroundColor="rgba(245, 245, 240, 1)"
|
<div className="relative min-h-screen w-full overflow-x-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
|
||||||
waveSpeedX={0.02}
|
<Waves
|
||||||
waveSpeedY={0.01}
|
className="pointer-events-none"
|
||||||
waveAmpX={40}
|
lineColor="#0f172a"
|
||||||
waveAmpY={20}
|
backgroundColor="rgba(245, 245, 240, 1)"
|
||||||
friction={0.9}
|
waveSpeedX={0.02}
|
||||||
tension={0.01}
|
waveSpeedY={0.01}
|
||||||
maxCursorMove={120}
|
waveAmpX={40}
|
||||||
xGap={12}
|
waveAmpY={20}
|
||||||
yGap={36}
|
friction={0.9}
|
||||||
animate={!isMobile}
|
tension={0.01}
|
||||||
interactive={!isMobile}
|
maxCursorMove={120}
|
||||||
/>
|
xGap={12}
|
||||||
|
yGap={36}
|
||||||
<PageLayout showFooter={true} className="bg-transparent text-gray-900">
|
animate={!isMobile}
|
||||||
{/* ...existing code... */}
|
interactive={!isMobile}
|
||||||
<div
|
/>
|
||||||
className={`relative z-10 w-full flex flex-col flex-1 min-h-0 ${
|
{content}
|
||||||
isMobile ? 'overflow-y-hidden' : ''
|
</div>
|
||||||
}`}
|
)}
|
||||||
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>
|
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</PageTransitionEffect>
|
</PageTransitionEffect>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { gsap } from 'gsap';
|
import { gsap } from 'gsap';
|
||||||
import PageLayout from './components/PageLayout';
|
import PageLayout from './components/PageLayout';
|
||||||
import Crosshair from './components/Crosshair';
|
import Crosshair from './components/Crosshair';
|
||||||
import Waves from './components/waves';
|
import Waves from './components/background/waves';
|
||||||
import SplitText from './components/SplitText';
|
import SplitText from './components/SplitText';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useSearchParams, useRouter } from 'next/navigation'
|
import { useSearchParams, useRouter } from 'next/navigation'
|
||||||
import PageLayout from '../components/PageLayout'
|
import PageLayout from '../components/PageLayout'
|
||||||
import Waves from '../components/waves'
|
import Waves from '../components/background/waves'
|
||||||
import { ToastProvider, useToast } from '../components/toast/toastComponent'
|
import { ToastProvider, useToast } from '../components/toast/toastComponent'
|
||||||
|
|
||||||
function PasswordResetPageInner() {
|
function PasswordResetPageInner() {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React, { useEffect } from 'react'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import useAuthStore from '../store/authStore'
|
import useAuthStore from '../store/authStore'
|
||||||
import PageLayout from '../components/PageLayout'
|
import PageLayout from '../components/PageLayout'
|
||||||
import Waves from '../components/waves'
|
import BlueBlurryBackground from '../components/background/blueblurry'
|
||||||
import ProfileCompletion from './components/profileCompletion'
|
import ProfileCompletion from './components/profileCompletion'
|
||||||
import BasicInformation from './components/basicInformation'
|
import BasicInformation from './components/basicInformation'
|
||||||
import MediaSection from './components/mediaSection'
|
import MediaSection from './components/mediaSection'
|
||||||
@ -70,7 +70,6 @@ export default function ProfilePage() {
|
|||||||
const user = useAuthStore(state => state.user)
|
const user = useAuthStore(state => state.user)
|
||||||
const isAuthReady = useAuthStore(state => state.isAuthReady)
|
const isAuthReady = useAuthStore(state => state.isAuthReady)
|
||||||
const [hasHydrated, setHasHydrated] = React.useState(false)
|
const [hasHydrated, setHasHydrated] = React.useState(false)
|
||||||
const [isMobile, setIsMobile] = React.useState(false)
|
|
||||||
const [userId, setUserId] = React.useState<string | number | undefined>(undefined)
|
const [userId, setUserId] = React.useState<string | number | undefined>(undefined)
|
||||||
|
|
||||||
// --- declare ALL hooks before any early return (Rules of Hooks) ---
|
// --- declare ALL hooks before any early return (Rules of Hooks) ---
|
||||||
@ -222,18 +221,6 @@ export default function ProfilePage() {
|
|||||||
setEditModalValues(prev => ({ ...prev, [key]: value }))
|
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 ---
|
// --- EARLY RETURN AFTER ALL HOOKS ---
|
||||||
if (!hasHydrated || !isAuthReady || !user) {
|
if (!hasHydrated || !isAuthReady || !user) {
|
||||||
return (
|
return (
|
||||||
@ -247,149 +234,130 @@ export default function ProfilePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full min-h-screen overflow-x-hidden">
|
<PageLayout className="bg-transparent text-gray-900">
|
||||||
<Waves
|
<BlueBlurryBackground>
|
||||||
className="pointer-events-none"
|
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
|
||||||
lineColor="#0f172a"
|
<div className="max-w-4xl mx-auto">
|
||||||
backgroundColor="rgba(245, 245, 240, 1)"
|
{/* MASTER GLASS PANEL (prevents non-translucent gaps between cards) */}
|
||||||
waveSpeedX={0.02}
|
<div className="rounded-3xl bg-white/60 backdrop-blur-md border border-white/50 shadow-xl p-4 sm:p-6 lg:p-8">
|
||||||
waveSpeedY={0.01}
|
{/* Page Header */}
|
||||||
waveAmpX={40}
|
<div className="mb-8">
|
||||||
waveAmpY={20}
|
<h1 className="text-3xl font-bold text-gray-900">Profile Settings</h1>
|
||||||
friction={0.9}
|
<p className="text-gray-600 mt-2">
|
||||||
tension={0.01}
|
Manage your account information and preferences
|
||||||
maxCursorMove={120}
|
</p>
|
||||||
xGap={12}
|
</div>
|
||||||
yGap={36}
|
|
||||||
animate={!isMobile}
|
|
||||||
interactive={!isMobile}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative z-10 min-h-screen">
|
{/* Pending admin verification notice (above progress) */}
|
||||||
<PageLayout className="bg-transparent text-gray-900">
|
{profileDataApi?.userStatus && profileDataApi.userStatus.is_admin_verified === 0 && (
|
||||||
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
|
<div className="rounded-md bg-yellow-50/80 backdrop-blur border border-yellow-200 p-3 text-sm text-yellow-800 mb-2">
|
||||||
<div className="max-w-4xl mx-auto">
|
Your account is fully submitted. Our team will verify your account shortly.
|
||||||
{/* 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>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Pending admin verification notice (above progress) */}
|
<ProfileCompletion profileComplete={profileData.profileComplete} />
|
||||||
{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} />
|
{/* Basic Info + Sidebar */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 sm:gap-8 mb-8">
|
||||||
{/* Basic Info + Sidebar */}
|
{/* Basic Information */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 sm:gap-8 mb-8">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
{/* Basic Information */}
|
<BasicInformation
|
||||||
<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
|
|
||||||
profileData={profileData}
|
profileData={profileData}
|
||||||
editingBank={false} // force read-only
|
HighlightIfMissing={HighlightIfMissing}
|
||||||
bankDraft={bankDraft}
|
// Add edit button handler
|
||||||
setEditingBank={setEditingBank}
|
onEdit={() => openEditModal('basic', {
|
||||||
setBankDraft={setBankDraft}
|
firstName: profileData.firstName,
|
||||||
setBankInfo={setBankInfo}
|
lastName: profileData.lastName,
|
||||||
// onEdit disabled for now
|
phone: profileData.phone,
|
||||||
// onEdit={() => openEditModal('bank', { ... })}
|
address: profileData.address,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
{/* --- Media Section --- */}
|
</div>
|
||||||
<MediaSection documents={documents} />
|
{/* 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>
|
</div>
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Edit Modal */}
|
{/* Bank Info, Media */}
|
||||||
<EditModal
|
<div className="space-y-6 sm:space-y-8 mb-8">
|
||||||
open={editModalOpen}
|
{/* --- My Abo Section (above bank info) --- */}
|
||||||
type={editModalType}
|
<UserAbo />
|
||||||
fields={editModalType === 'basic' ? basicFields : bankFields}
|
{/* --- Edit Bank Information Section --- */}
|
||||||
values={editModalValues}
|
<BankInformation
|
||||||
onChange={handleEditModalChange}
|
profileData={profileData}
|
||||||
onSave={handleEditModalSave}
|
editingBank={false} // force read-only
|
||||||
onCancel={() => { setEditModalOpen(false); setEditModalError(null); }}
|
bankDraft={bankDraft}
|
||||||
>
|
setEditingBank={setEditingBank}
|
||||||
{/* Show error message if present */}
|
setBankDraft={setBankDraft}
|
||||||
{editModalError && (
|
setBankInfo={setBankInfo}
|
||||||
<div className="text-sm text-red-600 mb-2">{editModalError}</div>
|
// onEdit disabled for now
|
||||||
)}
|
// onEdit={() => openEditModal('bank', { ... })}
|
||||||
</EditModal>
|
/>
|
||||||
</PageLayout>
|
{/* --- Media Section --- */}
|
||||||
</div>
|
<MediaSection documents={documents} />
|
||||||
</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>
|
||||||
|
</BlueBlurryBackground>
|
||||||
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1,11 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from 'react'
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import PageLayout from '../components/PageLayout'
|
import PageLayout from '../components/PageLayout'
|
||||||
import TutorialModal, { createTutorialSteps } from '../components/TutorialModal'
|
import TutorialModal, { createTutorialSteps } from '../components/TutorialModal'
|
||||||
import useAuthStore from '../store/authStore'
|
import useAuthStore from '../store/authStore'
|
||||||
import { useUserStatus } from '../hooks/useUserStatus'
|
import { useUserStatus } from '../hooks/useUserStatus'
|
||||||
|
import BlueBlurryBackground from '../components/background/blueblurry' // NEW
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
XCircleIcon,
|
XCircleIcon,
|
||||||
@ -30,6 +31,8 @@ interface StatusItem {
|
|||||||
export default function QuickActionDashboardPage() {
|
export default function QuickActionDashboardPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = useAuthStore(s => s.user)
|
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 { userStatus, loading, error, refreshStatus } = useUserStatus()
|
||||||
const [isClient, setIsClient] = useState(false)
|
const [isClient, setIsClient] = useState(false)
|
||||||
|
|
||||||
@ -51,32 +54,66 @@ export default function QuickActionDashboardPage() {
|
|||||||
const additionalInfo = userStatus?.profile_completed || false
|
const additionalInfo = userStatus?.profile_completed || false
|
||||||
const contractSigned = userStatus?.contract_signed || 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(() => {
|
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)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
if (urlParams.get('tutorial') === 'true') {
|
const forceOpen = urlParams.get('tutorial') === 'true'
|
||||||
// Remove the parameter from URL
|
if (forceOpen) {
|
||||||
const newUrl = window.location.pathname
|
window.history.replaceState({}, '', 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)
|
|
||||||
}
|
}
|
||||||
}, [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[] = [
|
const statusItems: StatusItem[] = [
|
||||||
{
|
{
|
||||||
@ -111,23 +148,27 @@ export default function QuickActionDashboardPage() {
|
|||||||
|
|
||||||
// Action handlers - navigate to proper QuickAction pages with tutorial callback
|
// Action handlers - navigate to proper QuickAction pages with tutorial callback
|
||||||
const handleVerifyEmail = useCallback(() => {
|
const handleVerifyEmail = useCallback(() => {
|
||||||
|
if (emailVerified) return
|
||||||
router.push('/quickaction-dashboard/register-email-verify?tutorial=true')
|
router.push('/quickaction-dashboard/register-email-verify?tutorial=true')
|
||||||
}, [router])
|
}, [router, emailVerified])
|
||||||
|
|
||||||
const handleUploadId = useCallback(() => {
|
const handleUploadId = useCallback(() => {
|
||||||
|
if (idUploaded) return
|
||||||
const userType = user?.userType || 'personal'
|
const userType = user?.userType || 'personal'
|
||||||
router.push(`/quickaction-dashboard/register-upload-id/${userType}?tutorial=true`)
|
router.push(`/quickaction-dashboard/register-upload-id/${userType}?tutorial=true`)
|
||||||
}, [router, user])
|
}, [router, user, idUploaded])
|
||||||
|
|
||||||
const handleCompleteInfo = useCallback(() => {
|
const handleCompleteInfo = useCallback(() => {
|
||||||
|
if (additionalInfo) return
|
||||||
const userType = user?.userType || 'personal'
|
const userType = user?.userType || 'personal'
|
||||||
router.push(`/quickaction-dashboard/register-additional-information/${userType}?tutorial=true`)
|
router.push(`/quickaction-dashboard/register-additional-information/${userType}?tutorial=true`)
|
||||||
}, [router, user])
|
}, [router, user, additionalInfo])
|
||||||
|
|
||||||
const handleSignContract = useCallback(() => {
|
const handleSignContract = useCallback(() => {
|
||||||
|
if (contractSigned) return
|
||||||
const userType = user?.userType || 'personal'
|
const userType = user?.userType || 'personal'
|
||||||
router.push(`/quickaction-dashboard/register-sign-contract/${userType}?tutorial=true`)
|
router.push(`/quickaction-dashboard/register-sign-contract/${userType}?tutorial=true`)
|
||||||
}, [router, user])
|
}, [router, user, contractSigned])
|
||||||
|
|
||||||
// Tutorial handlers
|
// Tutorial handlers
|
||||||
const startTutorial = useCallback(() => {
|
const startTutorial = useCallback(() => {
|
||||||
@ -149,16 +190,6 @@ export default function QuickActionDashboardPage() {
|
|||||||
setCurrentTutorialStep(prev => Math.max(1, prev - 1))
|
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
|
// Create tutorial steps
|
||||||
const tutorialSteps = createTutorialSteps(
|
const tutorialSteps = createTutorialSteps(
|
||||||
emailVerified,
|
emailVerified,
|
||||||
@ -202,20 +233,27 @@ export default function QuickActionDashboardPage() {
|
|||||||
return () => clearInterval(id)
|
return () => clearInterval(id)
|
||||||
}, [isClient, emailVerified, user?.email])
|
}, [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 (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
{/* NEW: smooth redirect overlay */}
|
||||||
{/* Animated background */}
|
{redirectTo && (
|
||||||
<div className="pointer-events-none absolute inset-0 z-0">
|
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
||||||
{/* Soft gradient blobs */}
|
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||||
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
|
<div className="text-sm font-medium text-gray-900">Redirecting…</div>
|
||||||
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
|
<div className="mt-1 text-xs text-gray-600">Taking you to your dashboard</div>
|
||||||
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
|
</div>
|
||||||
{/* Subtle radial highlight */}
|
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
|
|
||||||
</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 */}
|
{/* Title */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900">
|
<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">
|
<h2 className="text-sm sm:text-base font-semibold text-gray-900 mb-5">
|
||||||
Status Overview
|
Status Overview
|
||||||
</h2>
|
</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 => {
|
{statusItems.map(item => {
|
||||||
const CompleteIcon = item.complete ? CheckCircleIcon : XCircleIcon
|
const CompleteIcon = item.complete ? CheckCircleIcon : XCircleIcon
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.key}
|
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'
|
item.complete ? 'bg-emerald-50 border-emerald-100' : 'bg-rose-50 border-rose-100'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<CompleteIcon
|
<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
|
<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.complete ? 'text-emerald-700' : 'text-rose-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
<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'
|
item.complete ? 'text-emerald-600' : 'text-rose-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -316,19 +358,21 @@ export default function QuickActionDashboardPage() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 */}
|
{/* Email Verification */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<button
|
<button
|
||||||
onClick={handleVerifyEmail}
|
onClick={handleVerifyEmail}
|
||||||
disabled={emailVerified}
|
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
|
emailVerified
|
||||||
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
|
? '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'}
|
{emailVerified ? 'Email Verified' : 'Verify Email'}
|
||||||
</button>
|
</button>
|
||||||
{/* NEW: resend feedback (only when not verified) */}
|
{/* NEW: resend feedback (only when not verified) */}
|
||||||
@ -344,26 +388,28 @@ export default function QuickActionDashboardPage() {
|
|||||||
{/* ID Upload */}
|
{/* ID Upload */}
|
||||||
<button
|
<button
|
||||||
onClick={handleUploadId}
|
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
|
idUploaded
|
||||||
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
|
? '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'}
|
{idUploaded ? 'ID Uploaded' : 'Upload ID Document'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Additional Info */}
|
{/* Additional Info */}
|
||||||
<button
|
<button
|
||||||
onClick={handleCompleteInfo}
|
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
|
additionalInfo
|
||||||
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
|
? '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'}
|
{additionalInfo ? 'Profile Completed' : 'Complete Profile'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -372,15 +418,15 @@ export default function QuickActionDashboardPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={handleSignContract}
|
onClick={handleSignContract}
|
||||||
disabled={!canSignContract || contractSigned}
|
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
|
contractSigned
|
||||||
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
|
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
|
||||||
: canSignContract
|
: 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'
|
: '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'}
|
{contractSigned ? 'Contract Signed' : 'Sign Contract'}
|
||||||
</button>
|
</button>
|
||||||
{!canSignContract && !contractSigned && (
|
{!canSignContract && !contractSigned && (
|
||||||
@ -409,7 +455,7 @@ export default function QuickActionDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</BlueBlurryBackground>
|
||||||
|
|
||||||
{/* Tutorial Modal */}
|
{/* Tutorial Modal */}
|
||||||
<TutorialModal
|
<TutorialModal
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import PageLayout from '../../../components/PageLayout'
|
import PageLayout from '../../../components/PageLayout'
|
||||||
import useAuthStore from '../../../store/authStore'
|
import useAuthStore from '../../../store/authStore'
|
||||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||||
import { useToast } from '../../../components/toast/toastComponent'
|
import { useToast } from '../../../components/toast/toastComponent'
|
||||||
|
import { ChevronDownIcon } from '@heroicons/react/20/solid' // NEW
|
||||||
|
|
||||||
interface CompanyProfileData {
|
interface CompanyProfileData {
|
||||||
companyName: string
|
companyName: string
|
||||||
@ -47,10 +48,132 @@ const init: CompanyProfileData = {
|
|||||||
emergencyPhone: ''
|
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() {
|
export default function CompanyAdditionalInformationPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const user = useAuthStore(s => s.user) // NEW
|
||||||
|
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||||
const { accessToken } = useAuthStore()
|
const { accessToken } = useAuthStore()
|
||||||
const { refreshStatus } = useUserStatus()
|
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const [form, setForm] = useState(init)
|
const [form, setForm] = useState(init)
|
||||||
@ -58,6 +181,38 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
const [success, setSuccess] = useState(false)
|
const [success, setSuccess] = useState(false)
|
||||||
const [error, setError] = useState('')
|
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 handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
const { name, value } = e.target
|
const { name, value } = e.target
|
||||||
setForm(p => ({ ...p, [name]: value }))
|
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 (
|
return (
|
||||||
<PageLayout>
|
<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="relative min-h-screen overflow-hidden bg-slate-50">
|
||||||
{/* Animated background (same as dashboard) */}
|
{/* Animated background (same as dashboard) */}
|
||||||
<div className="pointer-events-none absolute inset-0 z-0">
|
<div className="pointer-events-none absolute inset-0 z-0">
|
||||||
@ -215,7 +385,7 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
name="companyName"
|
name="companyName"
|
||||||
value={form.companyName}
|
value={form.companyName}
|
||||||
onChange={handleChange}
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -228,7 +398,7 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
value={form.vatNumber}
|
value={form.vatNumber}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="e.g. DE123456789"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -240,7 +410,7 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
name="street"
|
name="street"
|
||||||
value={form.street}
|
value={form.street}
|
||||||
onChange={handleChange}
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -252,7 +422,7 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
name="postalCode"
|
name="postalCode"
|
||||||
value={form.postalCode}
|
value={form.postalCode}
|
||||||
onChange={handleChange}
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -264,28 +434,18 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
name="city"
|
name="city"
|
||||||
value={form.city}
|
value={form.city}
|
||||||
onChange={handleChange}
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<ModernSelect
|
||||||
Country *
|
label="Country"
|
||||||
</label>
|
placeholder="Select country..."
|
||||||
<select
|
|
||||||
name="country"
|
|
||||||
value={form.country}
|
value={form.country}
|
||||||
onChange={handleChange}
|
onChange={(v) => setField('country', v)}
|
||||||
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"
|
options={COUNTRIES.map(c => ({ value: c, label: c }))}
|
||||||
required
|
/>
|
||||||
>
|
|
||||||
<option value="">Select country...</option>
|
|
||||||
{COUNTRIES.map(country => (
|
|
||||||
<option key={country} value={country}>
|
|
||||||
{country}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -307,7 +467,7 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
value={form.accountHolder}
|
value={form.accountHolder}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="Company / Holder name"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -320,7 +480,7 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
value={form.iban}
|
value={form.iban}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="DE89 3704 0044 0532 0130 00"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -333,7 +493,7 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
value={form.bic}
|
value={form.bic}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="GENODEF1XXX"
|
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>
|
||||||
</div>
|
</div>
|
||||||
@ -356,7 +516,7 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
value={form.secondPhone}
|
value={form.secondPhone}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="+49 123 456 7890"
|
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>
|
||||||
<div>
|
<div>
|
||||||
@ -368,7 +528,7 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
value={form.emergencyName}
|
value={form.emergencyName}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="Contact name"
|
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>
|
||||||
<div>
|
<div>
|
||||||
@ -380,7 +540,7 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
value={form.emergencyPhone}
|
value={form.emergencyPhone}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="+49 123 456 7890"
|
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>
|
||||||
<div className="hidden lg:block" />
|
<div className="hidden lg:block" />
|
||||||
@ -402,14 +562,15 @@ export default function CompanyAdditionalInformationPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push('/quickaction-dashboard')}
|
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
|
Back to Dashboard
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || success}
|
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'}
|
{loading ? 'Speichern…' : success ? 'Gespeichert' : 'Save & Continue'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import PageLayout from '../../../components/PageLayout'
|
import PageLayout from '../../../components/PageLayout'
|
||||||
import useAuthStore from '../../../store/authStore'
|
import useAuthStore from '../../../store/authStore'
|
||||||
import { useUserStatus } from '../../../hooks/useUserStatus'
|
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||||
import { useToast } from '../../../components/toast/toastComponent'
|
import { useToast } from '../../../components/toast/toastComponent'
|
||||||
|
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||||
|
|
||||||
interface PersonalProfileData {
|
interface PersonalProfileData {
|
||||||
dob: string
|
dob: string
|
||||||
@ -55,10 +56,153 @@ const initialData: PersonalProfileData = {
|
|||||||
emergencyPhone: ''
|
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() {
|
export default function PersonalAdditionalInformationPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const user = useAuthStore(s => s.user) // NEW
|
||||||
|
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||||
const { accessToken } = useAuthStore()
|
const { accessToken } = useAuthStore()
|
||||||
const { refreshStatus } = useUserStatus()
|
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const [form, setForm] = useState(initialData)
|
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 (
|
return (
|
||||||
<PageLayout>
|
<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="relative min-h-screen overflow-hidden bg-slate-50">
|
||||||
{/* Animated background (same as dashboard) */}
|
{/* Animated background (same as dashboard) */}
|
||||||
<div className="pointer-events-none absolute inset-0 z-0">
|
<div className="pointer-events-none absolute inset-0 z-0">
|
||||||
@ -310,28 +501,18 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
min={new Date(new Date().getFullYear() - 120, 0, 1).toISOString().split('T')[0]}
|
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]}
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<ModernSelect
|
||||||
Nationality *
|
label="Nationality"
|
||||||
</label>
|
placeholder="Select nationality..."
|
||||||
<select
|
|
||||||
name="nationality"
|
|
||||||
value={form.nationality}
|
value={form.nationality}
|
||||||
onChange={handleChange}
|
onChange={(v) => setField('nationality', v)}
|
||||||
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"
|
options={NATIONALITIES.map(n => ({ value: n, label: n }))}
|
||||||
required
|
/>
|
||||||
>
|
|
||||||
<option value="">Select nationality...</option>
|
|
||||||
{NATIONALITIES.map(nationality => (
|
|
||||||
<option key={nationality} value={nationality}>
|
|
||||||
{nationality}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:col-span-2 lg:col-span-3">
|
<div className="sm:col-span-2 lg:col-span-3">
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
@ -342,7 +523,7 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
value={form.street}
|
value={form.street}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="Street & House Number"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -355,7 +536,7 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
value={form.postalCode}
|
value={form.postalCode}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="e.g. 12345"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -368,28 +549,18 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
value={form.city}
|
value={form.city}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="e.g. Berlin"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<ModernSelect
|
||||||
Country *
|
label="Country"
|
||||||
</label>
|
placeholder="Select country..."
|
||||||
<select
|
|
||||||
name="country"
|
|
||||||
value={form.country}
|
value={form.country}
|
||||||
onChange={handleChange}
|
onChange={(v) => setField('country', v)}
|
||||||
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"
|
options={COUNTRIES.map(c => ({ value: c, label: c }))}
|
||||||
required
|
/>
|
||||||
>
|
|
||||||
<option value="">Select country...</option>
|
|
||||||
{COUNTRIES.map(country => (
|
|
||||||
<option key={country} value={country}>
|
|
||||||
{country}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -411,7 +582,7 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
value={form.accountHolder}
|
value={form.accountHolder}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="Full name"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -424,7 +595,7 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
value={form.iban}
|
value={form.iban}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="e.g. DE89 3704 0044 0532 0130 00"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -448,7 +619,7 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
value={form.secondPhone}
|
value={form.secondPhone}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="+43 660 1234567"
|
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>
|
||||||
<div>
|
<div>
|
||||||
@ -460,7 +631,7 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
value={form.emergencyName}
|
value={form.emergencyName}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="Contact name"
|
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>
|
||||||
<div>
|
<div>
|
||||||
@ -472,7 +643,7 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
value={form.emergencyPhone}
|
value={form.emergencyPhone}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder="+43 660 1234567"
|
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>
|
||||||
<div className="hidden lg:block" />
|
<div className="hidden lg:block" />
|
||||||
@ -494,14 +665,15 @@ export default function PersonalAdditionalInformationPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push('/quickaction-dashboard')}
|
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
|
Back to Dashboard
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || success}
|
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'}
|
{loading ? 'Saving…' : success ? 'Saved' : 'Save & Continue'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
import PageLayout from '../../components/PageLayout'
|
import PageLayout from '../../components/PageLayout'
|
||||||
|
import BlueBlurryBackground from '../../components/background/blueblurry' // NEW
|
||||||
import useAuthStore from '../../store/authStore'
|
import useAuthStore from '../../store/authStore'
|
||||||
import { useUserStatus } from '../../hooks/useUserStatus'
|
import { useUserStatus } from '../../hooks/useUserStatus'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
@ -9,8 +10,9 @@ import { useToast } from '../../components/toast/toastComponent'
|
|||||||
|
|
||||||
export default function EmailVerifyPage() {
|
export default function EmailVerifyPage() {
|
||||||
const user = useAuthStore(s => s.user)
|
const user = useAuthStore(s => s.user)
|
||||||
|
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||||
const token = useAuthStore(s => s.accessToken)
|
const token = useAuthStore(s => s.accessToken)
|
||||||
const { refreshStatus } = useUserStatus()
|
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus() // CHANGED
|
||||||
const [code, setCode] = useState(['', '', '', '', '', ''])
|
const [code, setCode] = useState(['', '', '', '', '', ''])
|
||||||
const [submitting, setSubmitting] = useState(false)
|
const [submitting, setSubmitting] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
@ -339,20 +341,51 @@ export default function EmailVerifyPage() {
|
|||||||
return `${m}:${String(s).padStart(2, '0')}`
|
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 (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
{/* NEW: smooth redirect overlay */}
|
||||||
{/* Animated background (same as dashboard) */}
|
{redirectTo && (
|
||||||
<div className="pointer-events-none absolute inset-0 z-0">
|
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
|
||||||
{/* Soft gradient blobs */}
|
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||||
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
|
<div className="text-sm font-medium text-gray-900">Redirecting…</div>
|
||||||
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
|
<div className="mt-1 text-xs text-gray-600">Please wait</div>
|
||||||
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
|
</div>
|
||||||
{/* Subtle radial highlight */}
|
|
||||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
|
|
||||||
</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="max-w-xl mx-auto">
|
||||||
<div className="text-center mb-10">
|
<div className="text-center mb-10">
|
||||||
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-gray-900">
|
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-gray-900">
|
||||||
@ -362,7 +395,7 @@ export default function EmailVerifyPage() {
|
|||||||
{initialEmailSent ? (
|
{initialEmailSent ? (
|
||||||
<>
|
<>
|
||||||
We sent a 6-digit code to{' '}
|
We sent a 6-digit code to{' '}
|
||||||
<span className="text-blue-700 font-medium">
|
<span className="text-[#8D6B1D] font-medium">
|
||||||
{user?.email || 'your email'}
|
{user?.email || 'your email'}
|
||||||
</span>
|
</span>
|
||||||
. Enter it below.
|
. Enter it below.
|
||||||
@ -370,7 +403,7 @@ export default function EmailVerifyPage() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
Sending verification email to{' '}
|
Sending verification email to{' '}
|
||||||
<span className="text-blue-700 font-medium">
|
<span className="text-[#8D6B1D] font-medium">
|
||||||
{user?.email || 'your email'}
|
{user?.email || 'your email'}
|
||||||
</span>
|
</span>
|
||||||
...
|
...
|
||||||
@ -379,13 +412,11 @@ export default function EmailVerifyPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Card */}
|
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit}
|
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"
|
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">
|
<fieldset disabled={submitting || success} className="space-y-8">
|
||||||
{/* Inputs */}
|
|
||||||
<div className="flex justify-center gap-2 sm:gap-3">
|
<div className="flex justify-center gap-2 sm:gap-3">
|
||||||
{code.map((v, i) => (
|
{code.map((v, i) => (
|
||||||
<input
|
<input
|
||||||
@ -401,9 +432,9 @@ export default function EmailVerifyPage() {
|
|||||||
onPaste={e => handlePaste(i, e)}
|
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
|
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
|
${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'}
|
: '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>
|
</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">
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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 ? (
|
{submitting ? (
|
||||||
<>
|
<>
|
||||||
@ -437,7 +471,7 @@ export default function EmailVerifyPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleResend}
|
onClick={handleResend}
|
||||||
disabled={!!resendCooldown || submitting || success}
|
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
|
{resendCooldown
|
||||||
? `Resend in ${formatMmSs(resendCooldown)}`
|
? `Resend in ${formatMmSs(resendCooldown)}`
|
||||||
@ -449,7 +483,7 @@ export default function EmailVerifyPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push('/quickaction-dashboard')}
|
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
|
Go to Dashboard
|
||||||
</button>
|
</button>
|
||||||
@ -458,7 +492,7 @@ export default function EmailVerifyPage() {
|
|||||||
|
|
||||||
<div className="mt-8 text-center text-xs text-gray-500">
|
<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?{' '}
|
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
|
Contact support
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
@ -466,7 +500,7 @@ export default function EmailVerifyPage() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</BlueBlurryBackground>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import PageLayout from '../../../components/PageLayout'
|
import PageLayout from '../../../components/PageLayout'
|
||||||
import useAuthStore from '../../../store/authStore'
|
import useAuthStore from '../../../store/authStore'
|
||||||
@ -10,8 +10,10 @@ import { useToast } from '../../../components/toast/toastComponent'
|
|||||||
|
|
||||||
export default function CompanySignContractPage() {
|
export default function CompanySignContractPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const user = useAuthStore(s => s.user) // NEW
|
||||||
|
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||||
const { accessToken } = useAuthStore()
|
const { accessToken } = useAuthStore()
|
||||||
const { refreshStatus } = useUserStatus()
|
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const [companyName, setCompanyName] = useState('')
|
const [companyName, setCompanyName] = useState('')
|
||||||
@ -246,15 +248,7 @@ export default function CompanySignContractPage() {
|
|||||||
|
|
||||||
// Redirect to main dashboard after short delay
|
// Redirect to main dashboard after short delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Check if we came from tutorial
|
router.push('/dashboard')
|
||||||
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')
|
|
||||||
}
|
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
} catch (error: any) {
|
} 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 (
|
return (
|
||||||
<PageLayout>
|
<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="relative min-h-screen overflow-hidden bg-slate-50">
|
||||||
<div className="pointer-events-none absolute inset-0 z-0">
|
<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" />
|
<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'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import PageLayout from '../../../components/PageLayout'
|
import PageLayout from '../../../components/PageLayout'
|
||||||
import useAuthStore from '../../../store/authStore'
|
import useAuthStore from '../../../store/authStore'
|
||||||
@ -10,8 +10,10 @@ import { useToast } from '../../../components/toast/toastComponent'
|
|||||||
|
|
||||||
export default function PersonalSignContractPage() {
|
export default function PersonalSignContractPage() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const user = useAuthStore(s => s.user) // NEW
|
||||||
|
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||||
const { accessToken } = useAuthStore()
|
const { accessToken } = useAuthStore()
|
||||||
const { refreshStatus } = useUserStatus()
|
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus() // CHANGED
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const [date, setDate] = useState('')
|
const [date, setDate] = useState('')
|
||||||
@ -160,6 +162,37 @@ export default function PersonalSignContractPage() {
|
|||||||
]).finally(() => setPreviewLoading(false))
|
]).finally(() => setPreviewLoading(false))
|
||||||
}, [accessToken])
|
}, [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 valid = () => {
|
||||||
const contractChecked = agreeContract
|
const contractChecked = agreeContract
|
||||||
const dataChecked = agreeData
|
const dataChecked = agreeData
|
||||||
@ -242,15 +275,7 @@ export default function PersonalSignContractPage() {
|
|||||||
|
|
||||||
// Redirect to main dashboard after short delay
|
// Redirect to main dashboard after short delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Check if we came from tutorial
|
router.push('/dashboard')
|
||||||
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')
|
|
||||||
}
|
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -269,6 +294,16 @@ export default function PersonalSignContractPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<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="relative min-h-screen overflow-hidden bg-slate-50">
|
||||||
{/* Animated background (same as dashboard) */}
|
{/* Animated background (same as dashboard) */}
|
||||||
<div className="pointer-events-none absolute inset-0 z-0">
|
<div className="pointer-events-none absolute inset-0 z-0">
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import PageLayout from '../../../components/PageLayout'
|
import PageLayout from '../../../components/PageLayout'
|
||||||
|
import BlueBlurryBackground from '../../../components/background/blueblurry'
|
||||||
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||||
import { useCompanyUploadId } from './hooks/useCompanyUploadId'
|
import { useCompanyUploadId } from './hooks/useCompanyUploadId'
|
||||||
import useAuthStore from '../../../store/authStore'
|
import useAuthStore from '../../../store/authStore'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState, useCallback, useRef } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||||
|
|
||||||
const DOC_TYPES = ['Personalausweis', 'Reisepass', 'Führerschein', 'Aufenthaltstitel']
|
const DOC_TYPES = ['Personalausweis', 'Reisepass', 'Führerschein', 'Aufenthaltstitel']
|
||||||
|
|
||||||
@ -26,48 +28,68 @@ export default function CompanyIdUploadPage() {
|
|||||||
} = useCompanyUploadId()
|
} = useCompanyUploadId()
|
||||||
|
|
||||||
const user = useAuthStore(s => s.user)
|
const user = useAuthStore(s => s.user)
|
||||||
|
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||||
const router = useRouter()
|
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(() => {
|
useEffect(() => {
|
||||||
const ut = (user as any)?.userType || (user as any)?.role
|
const ut = (user as any)?.userType || (user as any)?.role
|
||||||
console.log('🧭 UploadID Guard [company]: userType =', ut)
|
if (ut && ut !== 'company') smoothReplace('/quickaction-dashboard/register-upload-id/personal') // CHANGED
|
||||||
if (ut && ut !== 'company') {
|
}, [user, smoothReplace])
|
||||||
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 (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 (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="min-h-[50vh] flex items-center justify-center">
|
<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="text-center text-sm text-gray-600">
|
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||||
Redirecting to the correct upload page…
|
<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>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goBackToDashboard = () => {
|
||||||
|
// CHANGED: do NOT preserve ?tutorial=true, otherwise dashboard force-opens the tutorial again
|
||||||
|
router.push('/quickaction-dashboard')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
<BlueBlurryBackground>
|
||||||
{/* Animated background (same as dashboard) */}
|
<main className="flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
|
||||||
<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">
|
|
||||||
<form
|
<form
|
||||||
onSubmit={submit}
|
onSubmit={submit}
|
||||||
className="relative max-w-7xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10 overflow-hidden"
|
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
|
<input
|
||||||
value={idNumber}
|
value={idNumber}
|
||||||
onChange={e => setIdNumber(e.target.value)}
|
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"
|
placeholder="Enter contact person's ID number"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -105,7 +127,7 @@ export default function CompanyIdUploadPage() {
|
|||||||
<select
|
<select
|
||||||
value={idType}
|
value={idType}
|
||||||
onChange={e => setIdType(e.target.value)}
|
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
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select document type</option>
|
<option value="">Select document type</option>
|
||||||
@ -122,7 +144,7 @@ export default function CompanyIdUploadPage() {
|
|||||||
value={expiryDate}
|
value={expiryDate}
|
||||||
onChange={e => setExpiryDate(e.target.value)}
|
onChange={e => setExpiryDate(e.target.value)}
|
||||||
placeholder="tt.mm.jjjj"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-600">
|
<p className="mt-1 text-xs text-gray-600">
|
||||||
@ -139,7 +161,9 @@ export default function CompanyIdUploadPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setHasBack(v => { const next = !v; if (!next) setExtraFile(null); return next })}
|
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}
|
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'}`} />
|
<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}
|
{...dropHandlers}
|
||||||
onDrop={e => onDrop(e, 'front')}
|
onDrop={e => onDrop(e, 'front')}
|
||||||
onClick={() => openPicker('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
|
<input
|
||||||
ref={frontRef}
|
ref={frontRef}
|
||||||
@ -184,8 +208,8 @@ export default function CompanyIdUploadPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
|
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-4 transition" />
|
||||||
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
|
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
|
||||||
Click to upload front side
|
Click to upload front side
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-xs text-gray-500">
|
<p className="mt-2 text-xs text-gray-500">
|
||||||
@ -203,7 +227,7 @@ export default function CompanyIdUploadPage() {
|
|||||||
{...dropHandlers}
|
{...dropHandlers}
|
||||||
onDrop={e => onDrop(e, 'extra')}
|
onDrop={e => onDrop(e, 'extra')}
|
||||||
onClick={() => openPicker('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
|
<input
|
||||||
ref={extraRef}
|
ref={extraRef}
|
||||||
@ -233,8 +257,8 @@ export default function CompanyIdUploadPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
|
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-4 transition" />
|
||||||
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
|
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
|
||||||
Click to upload back side
|
Click to upload back side
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-xs text-gray-500">
|
<p className="mt-2 text-xs text-gray-500">
|
||||||
@ -249,11 +273,11 @@ export default function CompanyIdUploadPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info */}
|
{/* Info */}
|
||||||
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
|
<div className="mt-8 rounded-lg bg-[#8D6B1D]/10 border border-[#8D6B1D]/20 px-5 py-5">
|
||||||
<p className="text-sm font-semibold text-indigo-900 mb-3">
|
<p className="text-sm font-semibold text-[#3B2C04] mb-3">
|
||||||
Please ensure your ID documents:
|
Please ensure your ID documents:
|
||||||
</p>
|
</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>Are clearly visible and readable</li>
|
||||||
<li>Show all four corners</li>
|
<li>Show all four corners</li>
|
||||||
<li>Are not expired</li>
|
<li>Are not expired</li>
|
||||||
@ -273,11 +297,20 @@ export default function CompanyIdUploadPage() {
|
|||||||
</div>
|
</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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting || success}
|
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'}
|
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
|
||||||
</button>
|
</button>
|
||||||
@ -285,7 +318,7 @@ export default function CompanyIdUploadPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</BlueBlurryBackground>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
import { usePersonalUploadId } from './hooks/usePersonalUploadId'
|
import { usePersonalUploadId } from './hooks/usePersonalUploadId'
|
||||||
import PageLayout from '../../../components/PageLayout'
|
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 { useRouter } from 'next/navigation'
|
||||||
import useAuthStore from '../../../store/authStore'
|
import useAuthStore from '../../../store/authStore'
|
||||||
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { useUserStatus } from '../../../hooks/useUserStatus'
|
||||||
|
|
||||||
// Add back ID types for the dropdown
|
// Add back ID types for the dropdown
|
||||||
const ID_TYPES = [
|
const ID_TYPES = [
|
||||||
@ -16,29 +18,54 @@ const ID_TYPES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export default function PersonalIdUploadPage() {
|
export default function PersonalIdUploadPage() {
|
||||||
// NEW: guard company users from accessing personal page
|
|
||||||
const user = useAuthStore(s => s.user)
|
const user = useAuthStore(s => s.user)
|
||||||
|
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
|
||||||
const router = useRouter()
|
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(() => {
|
useEffect(() => {
|
||||||
const ut = (user as any)?.userType || (user as any)?.role
|
const ut = (user as any)?.userType || (user as any)?.role
|
||||||
console.log('🧭 UploadID Guard [personal]: userType =', ut)
|
if (ut && ut !== 'personal') smoothReplace('/quickaction-dashboard/register-upload-id/company') // CHANGED
|
||||||
if (ut && ut !== 'personal') {
|
}, [user, smoothReplace])
|
||||||
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 (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 (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="min-h-[50vh] flex items-center justify-center">
|
<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="text-center text-sm text-gray-600">
|
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
|
||||||
Redirecting to the correct upload page…
|
<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>
|
</div>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
@ -58,20 +85,15 @@ export default function PersonalIdUploadPage() {
|
|||||||
inputBase,
|
inputBase,
|
||||||
} = usePersonalUploadId()
|
} = usePersonalUploadId()
|
||||||
|
|
||||||
|
const goBackToDashboard = () => {
|
||||||
|
// CHANGED: do NOT preserve ?tutorial=true, otherwise dashboard force-opens the tutorial again
|
||||||
|
router.push('/quickaction-dashboard')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="relative min-h-screen overflow-hidden bg-slate-50">
|
<BlueBlurryBackground>
|
||||||
{/* Animated background (same as dashboard) */}
|
<main className="flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
|
||||||
<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">
|
|
||||||
<form
|
<form
|
||||||
onSubmit={submit}
|
onSubmit={submit}
|
||||||
className="relative max-w-7xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10 overflow-hidden"
|
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}
|
value={idNumber}
|
||||||
onChange={e => setIdNumber(e.target.value)}
|
onChange={e => setIdNumber(e.target.value)}
|
||||||
placeholder="Enter your ID number"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
<p className="mt-1 text-xs text-gray-600">
|
<p className="mt-1 text-xs text-gray-600">
|
||||||
@ -109,7 +131,7 @@ export default function PersonalIdUploadPage() {
|
|||||||
<select
|
<select
|
||||||
value={idType}
|
value={idType}
|
||||||
onChange={e => setIdType(e.target.value)}
|
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
|
required
|
||||||
>
|
>
|
||||||
<option value="">Select ID type</option>
|
<option value="">Select ID type</option>
|
||||||
@ -130,7 +152,7 @@ export default function PersonalIdUploadPage() {
|
|||||||
value={expiry}
|
value={expiry}
|
||||||
onChange={e => setExpiry(e.target.value)}
|
onChange={e => setExpiry(e.target.value)}
|
||||||
placeholder="tt.mm jjjj"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -145,7 +167,7 @@ export default function PersonalIdUploadPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setHasBack(v => !v)}
|
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 ${
|
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}
|
aria-pressed={hasBack}
|
||||||
>
|
>
|
||||||
@ -165,7 +187,7 @@ export default function PersonalIdUploadPage() {
|
|||||||
{...dropEvents}
|
{...dropEvents}
|
||||||
onDrop={e => onDrop(e, 'front')}
|
onDrop={e => onDrop(e, 'front')}
|
||||||
onClick={() => openPicker('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
|
<input
|
||||||
ref={frontInputRef}
|
ref={frontInputRef}
|
||||||
@ -195,8 +217,8 @@ export default function PersonalIdUploadPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
|
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-3 transition" />
|
||||||
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
|
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
|
||||||
Click to upload front side
|
Click to upload front side
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-xs text-gray-500">
|
<p className="mt-2 text-xs text-gray-500">
|
||||||
@ -214,7 +236,7 @@ export default function PersonalIdUploadPage() {
|
|||||||
{...dropEvents}
|
{...dropEvents}
|
||||||
onDrop={e => onDrop(e, 'back')}
|
onDrop={e => onDrop(e, 'back')}
|
||||||
onClick={() => openPicker('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
|
<input
|
||||||
ref={backInputRef}
|
ref={backInputRef}
|
||||||
@ -244,8 +266,8 @@ export default function PersonalIdUploadPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
|
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-3 transition" />
|
||||||
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
|
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
|
||||||
Click to upload back side
|
Click to upload back side
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-xs text-gray-500">
|
<p className="mt-2 text-xs text-gray-500">
|
||||||
@ -260,11 +282,11 @@ export default function PersonalIdUploadPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info Box, errors, success, submit */}
|
{/* Info Box, errors, success, submit */}
|
||||||
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
|
<div className="mt-8 rounded-lg bg-[#8D6B1D]/10 border border-[#8D6B1D]/20 px-5 py-5">
|
||||||
<p className="text-sm font-semibold text-indigo-900 mb-3">
|
<p className="text-sm font-semibold text-[#3B2C04] mb-3">
|
||||||
Please ensure your ID documents:
|
Please ensure your ID documents:
|
||||||
</p>
|
</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>Are clearly visible and readable</li>
|
||||||
<li>Show all four corners</li>
|
<li>Show all four corners</li>
|
||||||
<li>Are not expired</li>
|
<li>Are not expired</li>
|
||||||
@ -284,11 +306,20 @@ export default function PersonalIdUploadPage() {
|
|||||||
</div>
|
</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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting || success}
|
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'}
|
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
|
||||||
</button>
|
</button>
|
||||||
@ -296,7 +327,7 @@ export default function PersonalIdUploadPage() {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</BlueBlurryBackground>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline'
|
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline'
|
||||||
|
import { useToast } from '../../components/toast/toastComponent'
|
||||||
|
|
||||||
interface ReferralLink {
|
interface ReferralLink {
|
||||||
id?: string | number
|
id?: string | number
|
||||||
@ -39,6 +40,7 @@ function shortLink(href?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function ReferralLinksListWidget({ links, onDeactivate }: Props) {
|
export default function ReferralLinksListWidget({ links, onDeactivate }: Props) {
|
||||||
|
const { showToast } = useToast()
|
||||||
// Local floating tooltip (fixed) so table doesn't scroll to show it
|
// Local floating tooltip (fixed) so table doesn't scroll to show it
|
||||||
const [tooltip, setTooltip] = useState<{ visible: boolean; text: string; x: number; y: number }>({
|
const [tooltip, setTooltip] = useState<{ visible: boolean; text: string; x: number; y: number }>({
|
||||||
visible: false,
|
visible: false,
|
||||||
@ -54,6 +56,24 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
|
|||||||
}
|
}
|
||||||
const hideTooltip = () => setTooltip(t => ({ ...t, visible: false }))
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-8 bg-white rounded-lg shadow-sm border border-gray-200">
|
<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>
|
</a>
|
||||||
{/* Desktop/Tablet copy button */}
|
{/* Desktop/Tablet copy button */}
|
||||||
<button
|
<button
|
||||||
onClick={async () => { try { await navigator.clipboard.writeText(l.url || String(l.code || '')); } catch {} }}
|
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 hover:bg-gray-50"
|
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" />
|
<ClipboardDocumentIcon className="h-4 w-4" />
|
||||||
Copy
|
Copy
|
||||||
</button>
|
</button>
|
||||||
{/* Mobile: only copy button */}
|
{/* Mobile: only copy button */}
|
||||||
<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"
|
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"
|
aria-label="Copy referral link"
|
||||||
>
|
>
|
||||||
@ -197,7 +218,13 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
|
|||||||
<button
|
<button
|
||||||
disabled={l.status !== 'active'}
|
disabled={l.status !== 'active'}
|
||||||
onClick={() => onDeactivate(l)}
|
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
|
Deactivate
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -12,8 +12,10 @@ import RegisteredUserList from './components/registeredUserList'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import useAuthStore from '../store/authStore'
|
import useAuthStore from '../store/authStore'
|
||||||
import { useRegisteredUsers } from './hooks/registeredUsers'
|
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 router = useRouter()
|
||||||
const user = useAuthStore(s => s.user)
|
const user = useAuthStore(s => s.user)
|
||||||
const isAuthReady = useAuthStore(s => s.isAuthReady)
|
const isAuthReady = useAuthStore(s => s.isAuthReady)
|
||||||
@ -48,11 +50,30 @@ export default function ReferralManagementPage() {
|
|||||||
try {
|
try {
|
||||||
const tokenId = selectedLink?.tokenId ?? selectedLink?.id
|
const tokenId = selectedLink?.tokenId ?? selectedLink?.id
|
||||||
if (tokenId == null) return
|
if (tokenId == null) return
|
||||||
|
|
||||||
const res = await deactivateReferralLink(tokenId)
|
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()
|
await loadData()
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
// optional: toast error
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
title: 'Deactivate failed',
|
||||||
|
message: 'Network error while deactivating the referral link.',
|
||||||
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setDeactivatePending(false)
|
setDeactivatePending(false)
|
||||||
setDeactivateOpen(false)
|
setDeactivateOpen(false)
|
||||||
@ -71,7 +92,6 @@ export default function ReferralManagementPage() {
|
|||||||
const run = async () => {
|
const run = async () => {
|
||||||
if (!isAuthReady) return
|
if (!isAuthReady) return
|
||||||
if (!user) {
|
if (!user) {
|
||||||
console.log('🔐 referral-management: no user, redirect to /login')
|
|
||||||
router.replace('/login')
|
router.replace('/login')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -79,11 +99,15 @@ export default function ReferralManagementPage() {
|
|||||||
// Resolve user id
|
// Resolve user id
|
||||||
const uid = (user as any)?.id ?? (user as any)?._id ?? (user as any)?.userId
|
const uid = (user as any)?.id ?? (user as any)?._id ?? (user as any)?.userId
|
||||||
if (!uid) {
|
if (!uid) {
|
||||||
console.warn('⚠️ referral-management: user id missing, denying access')
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setHasReferralPerm(false)
|
setHasReferralPerm(false)
|
||||||
setIsPermChecked(true)
|
setIsPermChecked(true)
|
||||||
}
|
}
|
||||||
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
title: 'Access check failed',
|
||||||
|
message: 'User id is missing. Redirecting…',
|
||||||
|
})
|
||||||
router.replace('/dashboard')
|
router.replace('/dashboard')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -112,9 +136,7 @@ export default function ReferralManagementPage() {
|
|||||||
...(tokenToUse ? { Authorization: `Bearer ${tokenToUse}` } : {})
|
...(tokenToUse ? { Authorization: `Bearer ${tokenToUse}` } : {})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
console.log('📡 referral-management: permissions status:', res.status)
|
|
||||||
const body = await res.json().catch(() => null)
|
const body = await res.json().catch(() => null)
|
||||||
console.log('📦 referral-management: permissions body:', body)
|
|
||||||
|
|
||||||
const permsSrc = body?.data?.permissions ?? body?.permissions ?? body
|
const permsSrc = body?.data?.permissions ?? body?.permissions ?? body
|
||||||
let can = false
|
let can = false
|
||||||
@ -133,22 +155,30 @@ export default function ReferralManagementPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!can) {
|
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')
|
router.replace('/dashboard')
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('❌ referral-management: fetch permissions error:', e)
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setHasReferralPerm(false)
|
setHasReferralPerm(false)
|
||||||
setIsPermChecked(true)
|
setIsPermChecked(true)
|
||||||
}
|
}
|
||||||
|
showToast({
|
||||||
|
variant: 'error',
|
||||||
|
title: 'Permission check failed',
|
||||||
|
message: 'Could not verify permissions. Redirecting…',
|
||||||
|
})
|
||||||
router.replace('/dashboard')
|
router.replace('/dashboard')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run()
|
run()
|
||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [isAuthReady, user, accessToken, refreshAuthToken, router])
|
}, [isAuthReady, user, accessToken, refreshAuthToken, router, showToast]) // CHANGED: add showToast
|
||||||
|
|
||||||
// Helper: normalize list payload shapes
|
// Helper: normalize list payload shapes
|
||||||
const normalizeList = (raw: any): any[] => {
|
const normalizeList = (raw: any): any[] => {
|
||||||
@ -187,8 +217,22 @@ export default function ReferralManagementPage() {
|
|||||||
// Helper: fetch stats + list
|
// Helper: fetch stats + list
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
const [statsRes, listRes] = await Promise.all([fetchReferralStats(), fetchReferralList()])
|
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) {
|
if (statsRes.ok && statsRes.body) {
|
||||||
const b: any = statsRes.body?.data || statsRes.body?.stats || statsRes.body
|
const b: any = statsRes.body?.data || statsRes.body?.stats || statsRes.body
|
||||||
setStats({
|
setStats({
|
||||||
@ -259,8 +303,10 @@ export default function ReferralManagementPage() {
|
|||||||
loading={usersLoading}
|
loading={usersLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Generator */}
|
{/* Generator (wrapped for desktop hover animation) */}
|
||||||
<GenerateReferralLinkWidget onCreated={loadData} />
|
<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) */}
|
{/* Referral links list (refactored) */}
|
||||||
<ReferralLinksListWidget links={links} onDeactivate={openDeactivateModal} />
|
<ReferralLinksListWidget links={links} onDeactivate={openDeactivateModal} />
|
||||||
@ -279,3 +325,11 @@ export default function ReferralManagementPage() {
|
|||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function ReferralManagementPage() {
|
||||||
|
return (
|
||||||
|
<ToastProvider>
|
||||||
|
<ReferralManagementPageInner />
|
||||||
|
</ToastProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -8,7 +8,8 @@ import PageLayout from '../components/PageLayout'
|
|||||||
import SessionDetectedModal from './components/SessionDetectedModal'
|
import SessionDetectedModal from './components/SessionDetectedModal'
|
||||||
import InvalidRefLinkModal from './components/invalidRefLinkModal'
|
import InvalidRefLinkModal from './components/invalidRefLinkModal'
|
||||||
import { ToastProvider, useToast } from '../components/toast/toastComponent'
|
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
|
// NEW: inner component that actually uses useToast and all the logic
|
||||||
function RegisterPageInner() {
|
function RegisterPageInner() {
|
||||||
@ -161,26 +162,36 @@ function RegisterPageInner() {
|
|||||||
opacity: 'var(--pp-page-shift-opacity, 1)',
|
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) ---
|
// --- Render branches (unchanged except classNames) ---
|
||||||
|
|
||||||
if (!isRefChecked) {
|
if (!isRefChecked) {
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
|
<BackgroundShell>
|
||||||
<Waves
|
|
||||||
className="pointer-events-none"
|
|
||||||
lineColor="#0f172a"
|
|
||||||
backgroundColor="rgba(245, 245, 240, 1)"
|
|
||||||
waveSpeedX={0.02}
|
|
||||||
waveSpeedY={0.01}
|
|
||||||
waveAmpX={40}
|
|
||||||
waveAmpY={20}
|
|
||||||
friction={0.9}
|
|
||||||
tension={0.01}
|
|
||||||
maxCursorMove={120}
|
|
||||||
xGap={12}
|
|
||||||
yGap={36}
|
|
||||||
/>
|
|
||||||
<main
|
<main
|
||||||
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
|
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
|
||||||
style={mainStyle}
|
style={mainStyle}
|
||||||
@ -203,7 +214,7 @@ function RegisterPageInner() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</BackgroundShell>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -211,21 +222,7 @@ function RegisterPageInner() {
|
|||||||
if (invalidRef) {
|
if (invalidRef) {
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
|
<BackgroundShell>
|
||||||
<Waves
|
|
||||||
className="pointer-events-none"
|
|
||||||
lineColor="#0f172a"
|
|
||||||
backgroundColor="rgba(245, 245, 240, 1)"
|
|
||||||
waveSpeedX={0.02}
|
|
||||||
waveSpeedY={0.01}
|
|
||||||
waveAmpX={40}
|
|
||||||
waveAmpY={20}
|
|
||||||
friction={0.9}
|
|
||||||
tension={0.01}
|
|
||||||
maxCursorMove={120}
|
|
||||||
xGap={12}
|
|
||||||
yGap={36}
|
|
||||||
/>
|
|
||||||
<main
|
<main
|
||||||
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
|
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
|
||||||
style={mainStyle}
|
style={mainStyle}
|
||||||
@ -253,7 +250,7 @@ function RegisterPageInner() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</BackgroundShell>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -261,21 +258,7 @@ function RegisterPageInner() {
|
|||||||
// normal register
|
// normal register
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
|
<BackgroundShell>
|
||||||
<Waves
|
|
||||||
className="pointer-events-none"
|
|
||||||
lineColor="#0f172a"
|
|
||||||
backgroundColor="rgba(245, 245, 240, 1)"
|
|
||||||
waveSpeedX={0.02}
|
|
||||||
waveSpeedY={0.01}
|
|
||||||
waveAmpX={40}
|
|
||||||
waveAmpY={20}
|
|
||||||
friction={0.9}
|
|
||||||
tension={0.01}
|
|
||||||
maxCursorMove={120}
|
|
||||||
xGap={12}
|
|
||||||
yGap={36}
|
|
||||||
/>
|
|
||||||
<main
|
<main
|
||||||
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
|
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
|
||||||
style={mainStyle}
|
style={mainStyle}
|
||||||
@ -334,7 +317,7 @@ function RegisterPageInner() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</BackgroundShell>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user