feat: mobile

This commit is contained in:
DeathKaioken 2026-01-14 18:15:48 +01:00
parent 1045debc32
commit 6dfaedcab6
24 changed files with 1672 additions and 1110 deletions

View File

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

View File

@ -2,7 +2,7 @@
import { useEffect, useState, useMemo } from 'react'
import PageLayout from '../components/PageLayout'
import Waves from '../components/waves'
import Waves from '../components/background/waves'
type Affiliate = {
id: string

View File

@ -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;

View File

@ -84,7 +84,7 @@ export default function TutorialModal({
</Transition.Child>
<div className="fixed inset-0 z-10">
<div className="flex min-h-full items-center justify-center p-4">
<div className="flex min-h-full items-center justify-center p-3 sm:p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@ -94,8 +94,9 @@ export default function TutorialModal({
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative w-full max-w-5xl h-[60vh]">
<div className="relative isolate overflow-hidden bg-slate-50 h-full after:pointer-events-none after:absolute after:inset-0 after:inset-ring after:inset-ring-gray-200/50 sm:rounded-3xl after:sm:rounded-3xl lg:flex lg:gap-x-12 lg:px-8 w-full">
{/* CHANGED: mobile uses max-height + scrolling */}
<Dialog.Panel className="relative w-full max-w-5xl max-h-[88dvh] sm:h-[60vh]">
<div className="relative isolate h-full overflow-hidden rounded-2xl bg-slate-50 after:pointer-events-none after:absolute after:inset-0 after:inset-ring after:inset-ring-gray-200/50 sm:rounded-3xl after:sm:rounded-3xl lg:flex lg:gap-x-12 lg:px-8 w-full">
{/* Background Gradient */}
<svg
viewBox="0 0 1024 1024"
@ -121,20 +122,20 @@ export default function TutorialModal({
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
{/* Content Section - Left Half */}
<div className="lg:flex-1 lg:max-w-md text-center lg:text-left py-8 px-6 flex flex-col justify-center">
{/* CHANGED: content scrolls on mobile */}
<div className="lg:flex-1 lg:max-w-md text-center lg:text-left px-5 py-6 sm:px-6 sm:py-8 flex flex-col justify-start overflow-y-auto">
{/* Icon */}
<div className="mx-auto lg:mx-0 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 mb-4 ring-2 ring-blue-200">
<step.icon className="h-6 w-6 text-blue-600" aria-hidden="true" />
</div>
{/* Title */}
<h2 className="text-xl font-semibold tracking-tight whitespace-nowrap text-gray-800 sm:text-2xl">
{/* CHANGED: allow wrapping on mobile (remove nowrap) */}
<h2 className="text-xl font-semibold tracking-tight text-gray-800 sm:text-2xl">
{step.title}
</h2>
{/* Description */}
<p className="mt-3 text-sm text-gray-600 leading-relaxed h-12 overflow-hidden">
{/* CHANGED: no fixed height clipping on mobile */}
<p className="mt-3 text-sm text-gray-600 leading-relaxed sm:h-12 sm:overflow-hidden">
{step.description}
</p>
@ -209,8 +210,8 @@ export default function TutorialModal({
)}
</div>
{/* Visual Section - Right Half */}
<div className="relative lg:flex-1 mt-4 lg:mt-0 h-32 lg:h-full lg:min-h-[150px] flex items-end justify-end">
{/* CHANGED: hide unused visual section on mobile */}
<div className="relative hidden lg:flex lg:flex-1 mt-4 lg:mt-0 h-32 lg:h-full lg:min-h-[150px] items-end justify-end">
{/* <img
src="/images/misc/cow.png"
alt="Profit Planet Mascot"

View 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>
)
}

View File

@ -9,10 +9,7 @@ import {
Disclosure,
DisclosureButton,
DisclosurePanel,
Popover,
PopoverButton,
PopoverGroup,
PopoverPanel,
Transition,
} from '@headlessui/react'
import {
@ -99,22 +96,17 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
console.error('Logout failed:', err)
setGlobalLoggingOut?.(false)
}
};
}
// Helper to get user initials for profile icon
const getUserInitials = () => {
if (!user) return 'U';
if (!user) return 'U'
if (user.firstName || user.lastName) {
return (
(user.firstName?.[0] || '') +
(user.lastName?.[0] || '')
).toUpperCase();
return ((user.firstName?.[0] || '') + (user.lastName?.[0] || '')).toUpperCase()
}
if (user.email) {
return user.email[0].toUpperCase();
}
return 'U';
};
if (user.email) return user.email[0].toUpperCase()
return 'U'
}
// Initial theme (dark/light) + mark mounted + start header animation
useEffect(() => {
@ -507,71 +499,18 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
{/* Information dropdown already removed here */}
</PopoverGroup>
<div className="hidden lg:flex lg:flex-1 lg:justify-end lg:items-center lg:gap-x-4">
{/* Auth slot */}
<div className="flex items-center">
{userPresent ? (
<Popover className="relative">
<PopoverButton className="flex items-center gap-x-1 text-sm font-semibold text-gray-900 dark:text-white">
<Avatar
src=""
initials={(() => {
if (!user) return 'U'
if (user.firstName || user.lastName) {
return ((user.firstName?.[0] || '') + (user.lastName?.[0] || '')).toUpperCase()
}
return user.email ? user.email[0].toUpperCase() : 'U'
})()}
className="size-8 bg-gradient-to-br from-indigo-500/40 to-indigo-600/60 text-white"
/>
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none text-gray-500" />
</PopoverButton>
<PopoverPanel
transition
className="absolute left-0 top-full mt-2 w-64 rounded-md bg-white dark:bg-gray-900 ring-1 ring-black/10 dark:ring-white/10 shadow-lg data-closed:-translate-y-1 data-closed:opacity-0 data-enter:duration-200 data-leave:duration-150"
>
<div className="p-4">
<div className="flex flex-col border-b border-gray-200 dark:border-white/10 pb-4 mb-4">
<div className="font-medium text-gray-900 dark:text-white">
{user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : (user?.email || 'User')}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400">
{user?.email || 'user@example.com'}
</div>
</div>
{canSeeDashboard && (
<button
onClick={() => router.push('/dashboard')}
className="flex items-center gap-x-2 w-full text-left p-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-white/5 rounded-md"
>
<Bars3Icon className="size-5 text-gray-400" />
Dashboard
</button>
)}
<button
onClick={() => router.push('/profile')}
className="flex items-center gap-x-2 w-full text-left p-2 text-sm text-gray-800 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-white/5 rounded-md"
>
<UserCircleIcon className="size-5 text-gray-400" />
Profile
</button>
{/* Logout removed from profile dropdown; still available in hamburger menu bottom */}
</div>
</PopoverPanel>
</Popover>
) : mounted ? (
<button
onClick={() => router.push('/login')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
Log in <span aria-hidden="true">&rarr;</span>
</button>
) : (
<div aria-hidden="true" className="w-20 h-8 rounded-md bg-gray-200 dark:bg-gray-700/70 animate-pulse" />
)}
</div>
{/* CHANGED: remove profile icon/popover from header; keep login (when logged out) + hamburger */}
<div className="hidden lg:flex lg:flex-1 lg:justify-end lg:items-center lg:gap-x-3">
{!userPresent && mounted && (
<button
onClick={() => router.push('/login')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
>
Log in <span aria-hidden="true">&rarr;</span>
</button>
)}
{/* Desktop hamburger (right side, next to login/profile) */}
{/* Desktop hamburger (right side) */}
<button
type="button"
onClick={() => setMobileMenuOpen(open => !open)}
@ -789,16 +728,37 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
</div>
) : user ? (
<>
{/* User info + basic nav */}
{/* CHANGED: include profile icon INSIDE hamburger menu */}
<div className="pt-6 space-y-2">
<div className="flex flex-col border-b border-white/10 pb-4 mb-4 px-3">
<div className="font-medium text-white">
{user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : (user?.email || 'User')}
</div>
<div className="text-sm text-gray-400">
{user?.email || 'user@example.com'}
<div className="flex items-center gap-3 border-b border-white/10 pb-4 mb-4 px-3">
<Avatar
src=""
initials={getUserInitials()}
className="size-10 bg-gradient-to-br from-indigo-500/40 to-indigo-600/60 text-white"
/>
<div className="min-w-0">
<div className="font-medium text-white truncate">
{user?.firstName && user?.lastName ? `${user.firstName} ${user.lastName}` : (user?.email || 'User')}
</div>
<div className="text-sm text-gray-400 truncate">
{user?.email || 'user@example.com'}
</div>
</div>
</div>
{/* NEW: show Quick Action Dashboard link when user cannot access /dashboard yet */}
{!canSeeDashboard && (
<button
onClick={() => {
router.push('/quickaction-dashboard')
setMobileMenuOpen(false)
}}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
>
Startup Dashboard
</button>
)}
{canSeeDashboard && (
<button
onClick={() => {

View File

@ -182,13 +182,29 @@ function ToastViewport() {
if (!toasts.length) return null
return (
<div className="pointer-events-none fixed inset-x-4 bottom-4 z-50 flex justify-end sm:inset-x-auto sm:right-4 sm:w-auto">
<div className="flex max-h-[80vh] w-full flex-col gap-3 overflow-hidden sm:w-80">
{toasts.map(t => (
<ToastItem key={t.id} toast={t} onClose={() => handleClose(t.id)} />
))}
<>
<div
className="pp-toast-viewport pointer-events-none fixed inset-x-4 z-50 flex justify-end sm:inset-x-auto sm:right-4 sm:w-auto"
style={{ bottom: 'calc(env(safe-area-inset-bottom, 0px) + var(--pp-toast-bottom, 5rem))' }}
>
<div className="flex max-h-[80vh] w-full flex-col gap-3 overflow-hidden sm:w-80">
{toasts.map(t => (
<ToastItem key={t.id} toast={t} onClose={() => handleClose(t.id)} />
))}
</div>
</div>
</div>
<style jsx global>{`
.pp-toast-viewport {
--pp-toast-bottom: 5rem; /* desktop/tablet */
}
@media (max-width: 640px) {
.pp-toast-viewport {
--pp-toast-bottom: 7rem; /* mobile: lift higher above footer */
}
}
`}</style>
</>
)
}

View File

@ -1,10 +1,12 @@
'use client'
import { useEffect, useState } from 'react'
import { useEffect, useState, useCallback, useRef } from 'react'
import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
import PageLayout from '../components/PageLayout'
import Waves from '../components/waves'
import Waves from '../components/background/waves'
import BlueBlurryBackground from '../components/background/blueblurry'
import { useUserStatus } from '../hooks/useUserStatus'
import {
ShoppingBagIcon,
UsersIcon,
@ -21,6 +23,18 @@ export default function DashboardPage() {
const isShopEnabled = process.env.NEXT_PUBLIC_SHOW_SHOP !== 'false'
const [isMobile, setIsMobile] = useState(false)
const { userStatus, loading: statusLoading } = useUserStatus()
// NEW: smooth redirect helper
const [redirectTo, setRedirectTo] = useState<string | null>(null)
const redirectOnceRef = useRef(false)
const smoothReplace = useCallback((to: string) => {
if (redirectOnceRef.current) return
redirectOnceRef.current = true
setRedirectTo(to)
window.setTimeout(() => router.replace(to), 200)
}, [router])
useEffect(() => {
const mq = window.matchMedia('(max-width: 768px)')
const apply = () => setIsMobile(mq.matches)
@ -39,9 +53,23 @@ export default function DashboardPage() {
router.push('/login')
}
}, [isAuthReady, user, router])
// Show loading until auth is ready or user is confirmed
if (!isAuthReady || !user) {
// NEW: block dashboard unless all 4 quickaction steps are completed
useEffect(() => {
if (!isAuthReady || !user) return
if (statusLoading || !userStatus) return
const allDone =
!!userStatus.email_verified &&
!!userStatus.documents_uploaded &&
!!userStatus.profile_completed &&
!!userStatus.contract_signed
if (!allDone) smoothReplace('/quickaction-dashboard')
}, [isAuthReady, user, statusLoading, userStatus, smoothReplace])
// Show loading until auth is ready, user is confirmed, and (if logged in) status is loaded
if (!isAuthReady || !user || (statusLoading && user)) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
@ -52,6 +80,27 @@ export default function DashboardPage() {
)
}
// If redirecting away, avoid rendering dashboard content
if (redirectTo) {
return (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
<div className="text-sm font-medium text-gray-900">Redirecting</div>
<div className="mt-1 text-xs text-gray-600">Please wait</div>
</div>
</div>
)
}
// NEW: final guard (dont render dashboard if not all done)
if (!userStatus) return null
const allDone =
!!userStatus.email_verified &&
!!userStatus.documents_uploaded &&
!!userStatus.profile_completed &&
!!userStatus.contract_signed
if (!allDone) return null
// Get user name
const getUserName = () => {
if (user.firstName && user.lastName) {
@ -127,7 +176,180 @@ export default function DashboardPage() {
},
]
return (
const content = (
<div className="relative z-10 flex-1 min-h-0">
<PageLayout className="bg-transparent text-gray-900">
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-8">
{/* Welcome Section */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">
Welcome back, {getUserName()}! 👋
</h1>
<p className="text-gray-600 mt-2">
Here's what's happening with your Profit Planet account
</p>
</div>
{/* News Section (replaces Account setup + Stats Grid) */}
<div className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Latest News & Articles</h2>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{news.map(item => (
<article key={item.id} className="group relative overflow-hidden rounded-xl bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
{/* Image/placeholder */}
<div className="aspect-[16/9] w-full bg-gradient-to-br from-gray-100 to-gray-200" />
<div className="p-5">
<div className="mb-2 flex items-center gap-2">
<span className="inline-flex items-center rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700 ring-1 ring-amber-200">
{item.category}
</span>
<span className="text-xs text-gray-500">{new Date(item.date).toLocaleDateString()}</span>
</div>
<h3 className="text-base font-semibold text-gray-900 group-hover:text-[#8D6B1D] transition-colors">
<button
onClick={() => (window.location.href = item.href)}
className="text-left w-full"
>
{item.title}
</button>
</h3>
<p className="mt-2 text-sm text-gray-600 line-clamp-3">{item.excerpt}</p>
<div className="mt-4">
<button
onClick={() => (window.location.href = item.href)}
className="text-sm font-medium text-[#8D6B1D] hover:text-[#7A5E1A]"
>
Read more
</button>
</div>
</div>
</article>
))}
</div>
</div>
{/* Quick Actions */}
<div className="mb-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Quick Actions</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{quickActions.map((action, index) => (
<button
key={index}
onClick={() => {
if (!action.disabled) {
router.push(action.href)
}
}}
disabled={Boolean(action.disabled)}
className={`bg-white rounded-lg p-6 border border-gray-200 text-left group transition-all duration-200 ${
action.disabled
? 'opacity-60 cursor-not-allowed'
: 'shadow-sm hover:shadow-lg hover:-translate-y-1 hover:-translate-y-1 hover:-translate-y-1 transform hover:-translate-y-1'
}`}
>
<div className="flex items-start">
<div
className={`${action.color} rounded-lg p-3 ${
action.disabled
? 'grayscale'
: 'group-hover:scale-105 transition-transform'
}`}
>
<action.icon className="h-6 w-6 text-white" />
</div>
<div className="ml-4 flex-1">
<h3
className={`text-lg font-medium transition-colors ${
action.disabled
? 'text-gray-500'
: 'text-gray-900 group-hover:text-[#8D6B1D]'
}`}
>
{action.title}
</h3>
<p className="text-sm text-gray-600 mt-1">
{action.description}
</p>
{action.disabled && action.disabledText && (
<p className="mt-3 text-xs font-medium text-amber-700">
{action.disabledText}
</p>
)}
</div>
</div>
</button>
))}
</div>
</div>
{/* Gold Member Status */}
<div className="bg-gradient-to-r from-[#8D6B1D] to-[#B8860B] rounded-lg p-6 text-white mb-8">
<div className="flex items-center">
<StarIcon className="h-12 w-12 text-yellow-300" />
<div className="ml-4">
<h2 className="text-2xl font-bold">Gold Member Status</h2>
<p className="text-yellow-100 mt-1">
Enjoy exclusive benefits and discounts
</p>
</div>
<div className="ml-auto">
<button className="bg-white/20 hover:bg-white/30 px-4 py-2 rounded-lg text-white font-medium transition-colors">
View Benefits
</button>
</div>
</div>
</div>
{/* Recent Activity */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Recent Activity</h2>
<div className="space-y-4">
<div className="flex items-center py-3 border-b border-gray-100 last:border-b-0">
<div className="bg-green-100 rounded-full p-2">
<ShoppingBagIcon className="h-5 w-5 text-green-600" />
</div>
<div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-900">Order completed</p>
<p className="text-sm text-gray-600">Eco-friendly water bottle</p>
</div>
<span className="text-sm text-gray-500">2 days ago</span>
</div>
<div className="flex items-center py-3 border-b border-gray-100 last:border-b-0">
<div className="bg-blue-100 rounded-full p-2">
<HeartIcon className="h-5 w-5 text-blue-600" />
</div>
<div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-900">Added to favorites</p>
<p className="text-sm text-gray-600">Sustainable backpack</p>
</div>
<span className="text-sm text-gray-500">1 week ago</span>
</div>
<div className="flex items-center py-3">
<div className="bg-purple-100 rounded-full p-2">
<UsersIcon className="h-5 w-5 text-purple-600" />
</div>
<div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-900">Joined community</p>
<p className="text-sm text-gray-600">Eco Warriors Group</p>
</div>
<span className="text-sm text-gray-500">2 weeks ago</span>
</div>
</div>
</div>
</div>
</div>
</main>
</PageLayout>
</div>
)
return isMobile ? (
<BlueBlurryBackground>{content}</BlueBlurryBackground>
) : (
<div
className="relative w-full min-h-[100dvh] flex flex-col overflow-x-hidden"
style={{ backgroundImage: 'none', background: 'none' }}
@ -145,178 +367,10 @@ export default function DashboardPage() {
maxCursorMove={120}
xGap={12}
yGap={36}
animate={!isMobile}
interactive={!isMobile}
animate
interactive
/>
<div className="relative z-10 flex-1 min-h-0">
<PageLayout className="bg-transparent text-gray-900">
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-8">
{/* Welcome Section */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">
Welcome back, {getUserName()}! 👋
</h1>
<p className="text-gray-600 mt-2">
Here's what's happening with your Profit Planet account
</p>
</div>
{/* News Section (replaces Account setup + Stats Grid) */}
<div className="mb-10">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Latest News & Articles</h2>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{news.map(item => (
<article key={item.id} className="group relative overflow-hidden rounded-xl bg-white border border-gray-200 shadow-sm hover:shadow-md transition-shadow">
{/* Image/placeholder */}
<div className="aspect-[16/9] w-full bg-gradient-to-br from-gray-100 to-gray-200" />
<div className="p-5">
<div className="mb-2 flex items-center gap-2">
<span className="inline-flex items-center rounded-full bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-700 ring-1 ring-amber-200">
{item.category}
</span>
<span className="text-xs text-gray-500">{new Date(item.date).toLocaleDateString()}</span>
</div>
<h3 className="text-base font-semibold text-gray-900 group-hover:text-[#8D6B1D] transition-colors">
<button
onClick={() => (window.location.href = item.href)}
className="text-left w-full"
>
{item.title}
</button>
</h3>
<p className="mt-2 text-sm text-gray-600 line-clamp-3">{item.excerpt}</p>
<div className="mt-4">
<button
onClick={() => (window.location.href = item.href)}
className="text-sm font-medium text-[#8D6B1D] hover:text-[#7A5E1A]"
>
Read more
</button>
</div>
</div>
</article>
))}
</div>
</div>
{/* Quick Actions */}
<div className="mb-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Quick Actions</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{quickActions.map((action, index) => (
<button
key={index}
onClick={() => {
if (!action.disabled) {
router.push(action.href)
}
}}
disabled={Boolean(action.disabled)}
className={`bg-white rounded-lg p-6 border border-gray-200 text-left group transition-all duration-200 ${
action.disabled
? 'opacity-60 cursor-not-allowed'
: 'shadow-sm hover:shadow-lg hover:-translate-y-1 hover:-translate-y-1 hover:-translate-y-1 transform hover:-translate-y-1'
}`}
>
<div className="flex items-start">
<div
className={`${action.color} rounded-lg p-3 ${
action.disabled
? 'grayscale'
: 'group-hover:scale-105 transition-transform'
}`}
>
<action.icon className="h-6 w-6 text-white" />
</div>
<div className="ml-4 flex-1">
<h3
className={`text-lg font-medium transition-colors ${
action.disabled
? 'text-gray-500'
: 'text-gray-900 group-hover:text-[#8D6B1D]'
}`}
>
{action.title}
</h3>
<p className="text-sm text-gray-600 mt-1">
{action.description}
</p>
{action.disabled && action.disabledText && (
<p className="mt-3 text-xs font-medium text-amber-700">
{action.disabledText}
</p>
)}
</div>
</div>
</button>
))}
</div>
</div>
{/* Gold Member Status */}
<div className="bg-gradient-to-r from-[#8D6B1D] to-[#B8860B] rounded-lg p-6 text-white mb-8">
<div className="flex items-center">
<StarIcon className="h-12 w-12 text-yellow-300" />
<div className="ml-4">
<h2 className="text-2xl font-bold">Gold Member Status</h2>
<p className="text-yellow-100 mt-1">
Enjoy exclusive benefits and discounts
</p>
</div>
<div className="ml-auto">
<button className="bg-white/20 hover:bg-white/30 px-4 py-2 rounded-lg text-white font-medium transition-colors">
View Benefits
</button>
</div>
</div>
</div>
{/* Recent Activity */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Recent Activity</h2>
<div className="space-y-4">
<div className="flex items-center py-3 border-b border-gray-100 last:border-b-0">
<div className="bg-green-100 rounded-full p-2">
<ShoppingBagIcon className="h-5 w-5 text-green-600" />
</div>
<div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-900">Order completed</p>
<p className="text-sm text-gray-600">Eco-friendly water bottle</p>
</div>
<span className="text-sm text-gray-500">2 days ago</span>
</div>
<div className="flex items-center py-3 border-b border-gray-100 last:border-b-0">
<div className="bg-blue-100 rounded-full p-2">
<HeartIcon className="h-5 w-5 text-blue-600" />
</div>
<div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-900">Added to favorites</p>
<p className="text-sm text-gray-600">Sustainable backpack</p>
</div>
<span className="text-sm text-gray-500">1 week ago</span>
</div>
<div className="flex items-center py-3">
<div className="bg-purple-100 rounded-full p-2">
<UsersIcon className="h-5 w-5 text-purple-600" />
</div>
<div className="ml-4 flex-1">
<p className="text-sm font-medium text-gray-900">Joined community</p>
<p className="text-sm text-gray-600">Eco Warriors Group</p>
</div>
<span className="text-sm text-gray-500">2 weeks ago</span>
</div>
</div>
</div>
</div>
</div>
</main>
</PageLayout>
</div>
{content}
</div>
)
}

View File

@ -7,7 +7,8 @@ import PageLayout from '../components/PageLayout'
import useAuthStore from '../store/authStore'
import { ToastProvider } from '../components/toast/toastComponent'
import PageTransitionEffect from '../components/animation/pageTransitionEffect'
import Waves from '../components/waves'
import Waves from '../components/background/waves'
import BlueBlurryBackground from '../components/background/blueblurry' // NEW
import CurvedLoop from '../components/curvedLoop'
export default function LoginPage() {
@ -58,78 +59,82 @@ export default function LoginPage() {
)
}
const content = (
<PageLayout showFooter={true} className="bg-transparent text-gray-900">
{/* ...existing code... */}
<div
className={`relative z-10 w-full flex flex-col flex-1 min-h-0 ${isMobile ? 'overflow-y-hidden' : ''}`}
style={{ backgroundImage: 'none', background: 'none' }}
>
{/* ...existing code... */}
{isMobile ? (
// ...existing code...
<div
className="relative z-10 flex-1 min-h-0 grid place-items-center px-3"
style={{ paddingTop: '6rem', paddingBottom: '50%' }}
>
<div
className="w-full"
style={{
transform: 'translateY(clamp(10px, 2vh, 28px))',
}}
>
<LoginForm />
</div>
</div>
) : (
// ...existing code...
<div
className="relative z-10 flex-1 min-h-0 flex flex-col justify-between"
style={{ paddingTop: '0.75rem', paddingBottom: '1rem' }}
>
<div className="w-full px-4 sm:px-0">
<CurvedLoop
marqueeText="Welcome to profit planet ✦"
speed={1}
interactive={false}
className="tracking-[0.2em]"
/>
</div>
<div className="w-full flex items-center justify-center px-3 sm:px-0">
<LoginForm />
</div>
</div>
)}
</div>
{/* ...existing code... */}
</PageLayout>
)
return (
<PageTransitionEffect>
<ToastProvider>
{/* NEW: page-level background wrapper so Waves covers everything */}
<div className="relative min-h-screen w-full overflow-x-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
animate={!isMobile}
interactive={!isMobile}
/>
<PageLayout showFooter={true} className="bg-transparent text-gray-900">
{/* ...existing code... */}
<div
className={`relative z-10 w-full flex flex-col flex-1 min-h-0 ${
isMobile ? 'overflow-y-hidden' : ''
}`}
style={{ backgroundImage: 'none', background: 'none' }}
>
{/* REMOVED: Waves background moved to wrapper */}
{isMobile ? (
// ...existing code...
<div
className="relative z-10 flex-1 min-h-0 grid place-items-center px-3"
style={{ paddingTop: '6rem', paddingBottom: '0.5rem' }}
>
<div
className="w-full"
style={{
// push a bit down (visual centering with header + footer)
transform: 'translateY(clamp(10px, 2vh, 28px))',
}}
>
<LoginForm />
</div>
</div>
) : (
// ...existing code...
<div
className="relative z-10 flex-1 min-h-0 flex flex-col justify-between"
style={{ paddingTop: '0.75rem', paddingBottom: '1rem' }}
>
<div className="w-full px-4 sm:px-0">
<CurvedLoop
marqueeText="Welcome to profit planet ✦"
speed={1}
interactive={false}
className="tracking-[0.2em]"
/>
</div>
<div className="w-full flex items-center justify-center px-3 sm:px-0">
<LoginForm />
</div>
</div>
)}
</div>
{/* ...existing code... */}
</PageLayout>
</div>
{isMobile ? (
<BlueBlurryBackground animate={false} className="w-full">
{content}
</BlueBlurryBackground>
) : (
<div className="relative min-h-screen w-full overflow-x-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
animate={!isMobile}
interactive={!isMobile}
/>
{content}
</div>
)}
</ToastProvider>
</PageTransitionEffect>
)

View File

@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
import { gsap } from 'gsap';
import PageLayout from './components/PageLayout';
import Crosshair from './components/Crosshair';
import Waves from './components/waves';
import Waves from './components/background/waves';
import SplitText from './components/SplitText';
export default function HomePage() {

View File

@ -3,7 +3,7 @@
import { useState, useEffect } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
import PageLayout from '../components/PageLayout'
import Waves from '../components/waves'
import Waves from '../components/background/waves'
import { ToastProvider, useToast } from '../components/toast/toastComponent'
function PasswordResetPageInner() {

View File

@ -4,7 +4,7 @@ import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
import PageLayout from '../components/PageLayout'
import Waves from '../components/waves'
import BlueBlurryBackground from '../components/background/blueblurry'
import ProfileCompletion from './components/profileCompletion'
import BasicInformation from './components/basicInformation'
import MediaSection from './components/mediaSection'
@ -70,7 +70,6 @@ export default function ProfilePage() {
const user = useAuthStore(state => state.user)
const isAuthReady = useAuthStore(state => state.isAuthReady)
const [hasHydrated, setHasHydrated] = React.useState(false)
const [isMobile, setIsMobile] = React.useState(false)
const [userId, setUserId] = React.useState<string | number | undefined>(undefined)
// --- declare ALL hooks before any early return (Rules of Hooks) ---
@ -222,18 +221,6 @@ export default function ProfilePage() {
setEditModalValues(prev => ({ ...prev, [key]: value }))
}
useEffect(() => {
const mq = window.matchMedia('(max-width: 768px)')
const apply = () => setIsMobile(mq.matches)
apply()
mq.addEventListener?.('change', apply)
window.addEventListener('resize', apply, { passive: true })
return () => {
mq.removeEventListener?.('change', apply)
window.removeEventListener('resize', apply)
}
}, [])
// --- EARLY RETURN AFTER ALL HOOKS ---
if (!hasHydrated || !isAuthReady || !user) {
return (
@ -247,149 +234,130 @@ export default function ProfilePage() {
}
return (
<div className="relative w-full min-h-screen overflow-x-hidden">
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
animate={!isMobile}
interactive={!isMobile}
/>
<PageLayout className="bg-transparent text-gray-900">
<BlueBlurryBackground>
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
{/* MASTER GLASS PANEL (prevents non-translucent gaps between cards) */}
<div className="rounded-3xl bg-white/60 backdrop-blur-md border border-white/50 shadow-xl p-4 sm:p-6 lg:p-8">
{/* Page Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Profile Settings</h1>
<p className="text-gray-600 mt-2">
Manage your account information and preferences
</p>
</div>
<div className="relative z-10 min-h-screen">
<PageLayout className="bg-transparent text-gray-900">
<main className="py-6 sm:py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
{/* MASTER GLASS PANEL (prevents non-translucent gaps between cards) */}
<div className="rounded-3xl bg-white/60 backdrop-blur-md border border-white/50 shadow-xl p-4 sm:p-6 lg:p-8">
{/* Page Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Profile Settings</h1>
<p className="text-gray-600 mt-2">
Manage your account information and preferences
</p>
{/* Pending admin verification notice (above progress) */}
{profileDataApi?.userStatus && profileDataApi.userStatus.is_admin_verified === 0 && (
<div className="rounded-md bg-yellow-50/80 backdrop-blur border border-yellow-200 p-3 text-sm text-yellow-800 mb-2">
Your account is fully submitted. Our team will verify your account shortly.
</div>
)}
{/* Pending admin verification notice (above progress) */}
{profileDataApi?.userStatus && profileDataApi.userStatus.is_admin_verified === 0 && (
<div className="rounded-md bg-yellow-50/80 backdrop-blur border border-yellow-200 p-3 text-sm text-yellow-800 mb-2">
Your account is fully submitted. Our team will verify your account shortly.
</div>
)}
<ProfileCompletion profileComplete={profileData.profileComplete} />
<ProfileCompletion profileComplete={profileData.profileComplete} />
{/* Basic Info + Sidebar */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 sm:gap-8 mb-8">
{/* Basic Information */}
<div className="lg:col-span-2 space-y-6">
<BasicInformation
profileData={profileData}
HighlightIfMissing={HighlightIfMissing}
// Add edit button handler
onEdit={() => openEditModal('basic', {
firstName: profileData.firstName,
lastName: profileData.lastName,
phone: profileData.phone,
address: profileData.address,
})}
/>
</div>
{/* Sidebar: Account Status + Quick Actions */}
<div className="space-y-6">
{/* Account Status (make translucent) */}
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<h3 className="font-semibold text-gray-900 mb-4">Account Status</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Member Since</span>
<span className="text-sm font-medium text-gray-900">{profileData.joinDate}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Status</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gradient-to-r from-[#8D6B1D] to-[#C49225] text-white">
{profileData.memberStatus}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Profile</span>
<span className="text-sm font-medium text-green-600">Verified</span>
</div>
</div>
</div>
{/* Quick Actions (make translucent) */}
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<h3 className="font-semibold text-gray-900 mb-4">Quick Actions</h3>
<div className="space-y-3">
<button
onClick={() => router.push('/dashboard')}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
>
Go to Dashboard
</button>
<button className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors">
Download Account Data
</button>
<button className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors">
Delete Account
</button>
</div>
</div>
</div>
</div>
{/* Bank Info, Media */}
<div className="space-y-6 sm:space-y-8 mb-8">
{/* --- My Abo Section (above bank info) --- */}
<UserAbo />
{/* --- Edit Bank Information Section --- */}
<BankInformation
{/* Basic Info + Sidebar */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 sm:gap-8 mb-8">
{/* Basic Information */}
<div className="lg:col-span-2 space-y-6">
<BasicInformation
profileData={profileData}
editingBank={false} // force read-only
bankDraft={bankDraft}
setEditingBank={setEditingBank}
setBankDraft={setBankDraft}
setBankInfo={setBankInfo}
// onEdit disabled for now
// onEdit={() => openEditModal('bank', { ... })}
HighlightIfMissing={HighlightIfMissing}
// Add edit button handler
onEdit={() => openEditModal('basic', {
firstName: profileData.firstName,
lastName: profileData.lastName,
phone: profileData.phone,
address: profileData.address,
})}
/>
{/* --- Media Section --- */}
<MediaSection documents={documents} />
</div>
{/* Sidebar: Account Status + Quick Actions */}
<div className="space-y-6">
{/* Account Status (make translucent) */}
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<h3 className="font-semibold text-gray-900 mb-4">Account Status</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Member Since</span>
<span className="text-sm font-medium text-gray-900">{profileData.joinDate}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Status</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gradient-to-r from-[#8D6B1D] to-[#C49225] text-white">
{profileData.memberStatus}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Profile</span>
<span className="text-sm font-medium text-green-600">Verified</span>
</div>
</div>
</div>
{/* Quick Actions (make translucent) */}
<div className="rounded-lg bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-4 sm:p-6">
<h3 className="font-semibold text-gray-900 mb-4">Quick Actions</h3>
<div className="space-y-3">
<button
onClick={() => router.push('/dashboard')}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
>
Go to Dashboard
</button>
<button className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 rounded-lg transition-colors">
Download Account Data
</button>
<button className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg transition-colors">
Delete Account
</button>
</div>
</div>
</div>
</div>
</div>
</main>
{/* Edit Modal */}
<EditModal
open={editModalOpen}
type={editModalType}
fields={editModalType === 'basic' ? basicFields : bankFields}
values={editModalValues}
onChange={handleEditModalChange}
onSave={handleEditModalSave}
onCancel={() => { setEditModalOpen(false); setEditModalError(null); }}
>
{/* Show error message if present */}
{editModalError && (
<div className="text-sm text-red-600 mb-2">{editModalError}</div>
)}
</EditModal>
</PageLayout>
</div>
</div>
{/* Bank Info, Media */}
<div className="space-y-6 sm:space-y-8 mb-8">
{/* --- My Abo Section (above bank info) --- */}
<UserAbo />
{/* --- Edit Bank Information Section --- */}
<BankInformation
profileData={profileData}
editingBank={false} // force read-only
bankDraft={bankDraft}
setEditingBank={setEditingBank}
setBankDraft={setBankDraft}
setBankInfo={setBankInfo}
// onEdit disabled for now
// onEdit={() => openEditModal('bank', { ... })}
/>
{/* --- Media Section --- */}
<MediaSection documents={documents} />
</div>
</div>
</div>
</main>
{/* Edit Modal */}
<EditModal
open={editModalOpen}
type={editModalType}
fields={editModalType === 'basic' ? basicFields : bankFields}
values={editModalValues}
onChange={handleEditModalChange}
onSave={handleEditModalSave}
onCancel={() => { setEditModalOpen(false); setEditModalError(null); }}
>
{/* Show error message if present */}
{editModalError && (
<div className="text-sm text-red-600 mb-2">{editModalError}</div>
)}
</EditModal>
</BlueBlurryBackground>
</PageLayout>
)
}

View File

@ -1,11 +1,12 @@
'use client'
import { useState, useCallback, useEffect } from 'react'
import { useState, useCallback, useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
import PageLayout from '../components/PageLayout'
import TutorialModal, { createTutorialSteps } from '../components/TutorialModal'
import useAuthStore from '../store/authStore'
import { useUserStatus } from '../hooks/useUserStatus'
import BlueBlurryBackground from '../components/background/blueblurry' // NEW
import {
CheckCircleIcon,
XCircleIcon,
@ -30,6 +31,8 @@ interface StatusItem {
export default function QuickActionDashboardPage() {
const router = useRouter()
const user = useAuthStore(s => s.user)
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
const accessToken = useAuthStore(s => s.accessToken) // NEW
const { userStatus, loading, error, refreshStatus } = useUserStatus()
const [isClient, setIsClient] = useState(false)
@ -51,32 +54,66 @@ export default function QuickActionDashboardPage() {
const additionalInfo = userStatus?.profile_completed || false
const contractSigned = userStatus?.contract_signed || false
// Check if we should open tutorial (from URL parameter) - separate useEffect after status is loaded
// NEW: if everything is done, quickaction-dashboard is no longer accessible
const allDone = emailVerified && idUploaded && additionalInfo && contractSigned
// NEW: smooth redirect (prevents snappy double navigation)
const [redirectTo, setRedirectTo] = useState<string | null>(null)
const redirectOnceRef = useRef(false)
const smoothReplace = useCallback((to: string) => {
if (redirectOnceRef.current) return
redirectOnceRef.current = true
setRedirectTo(to)
window.setTimeout(() => router.replace(to), 200)
}, [router])
useEffect(() => {
if (!isClient || loading || !userStatus) return
if (!isClient) return
if (loading || !userStatus) return
if (allDone) smoothReplace('/dashboard') // CHANGED
}, [isClient, loading, userStatus, allDone, smoothReplace])
// NEW: decide which tutorial step to start on
const getNextTutorialStep = useCallback(() => {
const noneCompleted = !emailVerified && !idUploaded && !additionalInfo && !contractSigned
if (noneCompleted) return 1
if (!emailVerified) return 2
if (!idUploaded) return 3
if (!additionalInfo) return 4
if (!contractSigned) return 5
return 6
}, [emailVerified, idUploaded, additionalInfo, contractSigned])
// CHANGED: single auto-open mechanism (works even if tutorial_seen exists)
useEffect(() => {
if (!isClient) return
if (loading || !userStatus) return
if (allDone) return // NEW: avoid tutorial flash during redirect
if (isTutorialOpen) return
const uid =
(user as any)?.id ??
(user as any)?._id ??
(user as any)?.userId ??
(user as any)?.email ??
null
if (!uid) return
const tokenSuffix = (accessToken || '').slice(-12) || 'no-token'
const sessionKey = `pp:tutorialShown:${uid}:${tokenSuffix}`
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.get('tutorial') === 'true') {
// Remove the parameter from URL
const newUrl = window.location.pathname
window.history.replaceState({}, '', newUrl)
// Open tutorial and go to next step
setTimeout(() => {
setIsTutorialOpen(true)
// Determine next step based on completion status
if (!emailVerified) {
setCurrentTutorialStep(2)
} else if (!idUploaded) {
setCurrentTutorialStep(3)
} else if (!additionalInfo) {
setCurrentTutorialStep(4)
} else {
setCurrentTutorialStep(5)
}
}, 500)
const forceOpen = urlParams.get('tutorial') === 'true'
if (forceOpen) {
window.history.replaceState({}, '', window.location.pathname)
}
}, [isClient, loading, userStatus, emailVerified, idUploaded, additionalInfo])
const alreadyShownThisLogin = sessionStorage.getItem(sessionKey) === '1'
if (alreadyShownThisLogin && !forceOpen) return
setCurrentTutorialStep(getNextTutorialStep())
setIsTutorialOpen(true)
sessionStorage.setItem(sessionKey, '1')
}, [isClient, loading, userStatus, isTutorialOpen, user, accessToken, getNextTutorialStep])
const statusItems: StatusItem[] = [
{
@ -111,23 +148,27 @@ export default function QuickActionDashboardPage() {
// Action handlers - navigate to proper QuickAction pages with tutorial callback
const handleVerifyEmail = useCallback(() => {
if (emailVerified) return
router.push('/quickaction-dashboard/register-email-verify?tutorial=true')
}, [router])
}, [router, emailVerified])
const handleUploadId = useCallback(() => {
if (idUploaded) return
const userType = user?.userType || 'personal'
router.push(`/quickaction-dashboard/register-upload-id/${userType}?tutorial=true`)
}, [router, user])
}, [router, user, idUploaded])
const handleCompleteInfo = useCallback(() => {
if (additionalInfo) return
const userType = user?.userType || 'personal'
router.push(`/quickaction-dashboard/register-additional-information/${userType}?tutorial=true`)
}, [router, user])
}, [router, user, additionalInfo])
const handleSignContract = useCallback(() => {
if (contractSigned) return
const userType = user?.userType || 'personal'
router.push(`/quickaction-dashboard/register-sign-contract/${userType}?tutorial=true`)
}, [router, user])
}, [router, user, contractSigned])
// Tutorial handlers
const startTutorial = useCallback(() => {
@ -149,16 +190,6 @@ export default function QuickActionDashboardPage() {
setCurrentTutorialStep(prev => Math.max(1, prev - 1))
}, [])
// Auto-start tutorial for new users
useEffect(() => {
if (isClient && !hasSeenTutorial && !loading && userStatus) {
// Auto-start tutorial if user hasn't completed first step
if (!emailVerified) {
setTimeout(() => setIsTutorialOpen(true), 1000)
}
}
}, [isClient, hasSeenTutorial, loading, userStatus, emailVerified])
// Create tutorial steps
const tutorialSteps = createTutorialSteps(
emailVerified,
@ -202,20 +233,27 @@ export default function QuickActionDashboardPage() {
return () => clearInterval(id)
}, [isClient, emailVerified, user?.email])
// NEW: only logged-in users can access quickaction dashboard
useEffect(() => {
if (!isClient) return
if (!isAuthReady) return
if (!user) smoothReplace('/login')
}, [isClient, isAuthReady, user, smoothReplace])
return (
<PageLayout>
<div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Animated background */}
<div className="pointer-events-none absolute inset-0 z-0">
{/* Soft gradient blobs */}
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
{/* Subtle radial highlight */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
{/* NEW: smooth redirect overlay */}
{redirectTo && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
<div className="text-sm font-medium text-gray-900">Redirecting</div>
<div className="mt-1 text-xs text-gray-600">Taking you to your dashboard</div>
</div>
</div>
)}
<main className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<BlueBlurryBackground /* optionally: animate={!isMobile} */>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Title */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">
@ -261,28 +299,32 @@ export default function QuickActionDashboardPage() {
<h2 className="text-sm sm:text-base font-semibold text-gray-900 mb-5">
Status Overview
</h2>
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 xl:grid-cols-4">
{/* CHANGED: mobile 2x2 grid */}
<div className="grid grid-cols-2 gap-3 sm:gap-6 md:grid-cols-2 xl:grid-cols-4">
{statusItems.map(item => {
const CompleteIcon = item.complete ? CheckCircleIcon : XCircleIcon
return (
<div
key={item.key}
className={`rounded-lg px-4 py-6 flex flex-col items-center text-center border transition-colors ${
className={`rounded-lg px-3 py-4 sm:px-4 sm:py-6 flex flex-col items-center text-center border transition-colors ${
item.complete ? 'bg-emerald-50 border-emerald-100' : 'bg-rose-50 border-rose-100'
}`}
>
<CompleteIcon
className={`h-6 w-6 mb-4 ${item.complete ? 'text-emerald-600' : 'text-rose-600'}`}
className={`h-5 w-5 sm:h-6 sm:w-6 mb-3 sm:mb-4 ${
item.complete ? 'text-emerald-600' : 'text-rose-600'
}`}
/>
<span
className={`text-xs font-medium uppercase tracking-wide ${
className={`text-[11px] sm:text-xs font-medium uppercase tracking-wide ${
item.complete ? 'text-emerald-700' : 'text-rose-700'
}`}
>
{item.label}
</span>
<span
className={`mt-1 text-xs font-semibold ${
className={`mt-1 text-[11px] sm:text-xs font-semibold ${
item.complete ? 'text-emerald-600' : 'text-rose-600'
}`}
>
@ -316,19 +358,21 @@ export default function QuickActionDashboardPage() {
)}
</button>
</div>
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 xl:grid-cols-4">
{/* CHANGED: mobile 2x2 grid (order already matches desired layout) */}
<div className="grid grid-cols-2 gap-3 sm:gap-6 md:grid-cols-2 xl:grid-cols-4">
{/* Email Verification */}
<div className="flex flex-col">
<button
onClick={handleVerifyEmail}
disabled={emailVerified}
className={`relative flex flex-col items-center justify-center rounded-lg px-4 py-8 text-center border font-medium text-sm transition-all ${
className={`relative flex flex-col items-center justify-center rounded-lg px-3 py-5 sm:px-4 sm:py-8 text-center border font-medium text-xs sm:text-sm transition-all md:transition-transform md:duration-200 ${
emailVerified
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
: 'bg-blue-600 hover:bg-blue-500 text-white border-blue-600 shadow'
: 'bg-blue-600 text-white border-blue-600 shadow md:hover:-translate-y-0.5 md:hover:shadow-lg md:hover:bg-blue-500 md:active:translate-y-0'
}`}
>
<EnvelopeOpenIcon className="h-6 w-6 mb-2" />
<EnvelopeOpenIcon className="h-5 w-5 sm:h-6 sm:w-6 mb-2" />
{emailVerified ? 'Email Verified' : 'Verify Email'}
</button>
{/* NEW: resend feedback (only when not verified) */}
@ -344,26 +388,28 @@ export default function QuickActionDashboardPage() {
{/* ID Upload */}
<button
onClick={handleUploadId}
className={`relative flex flex-col items-center justify-center rounded-lg px-4 py-5 text-center border font-medium text-sm transition-all ${
disabled={idUploaded}
className={`relative flex flex-col items-center justify-center rounded-lg px-3 py-5 sm:px-4 sm:py-5 text-center border font-medium text-xs sm:text-sm transition-all md:transition-transform md:duration-200 ${
idUploaded
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
: 'bg-blue-600 hover:bg-blue-500 text-white border-blue-600 shadow'
}`}
: 'bg-blue-600 text-white border-blue-600 shadow md:hover:-translate-y-0.5 md:hover:shadow-lg md:hover:bg-blue-500 md:active:translate-y-0'
}`}
>
<ArrowUpOnSquareIcon className="h-6 w-6 mb-2" />
<ArrowUpOnSquareIcon className="h-5 w-5 sm:h-6 sm:w-6 mb-2" />
{idUploaded ? 'ID Uploaded' : 'Upload ID Document'}
</button>
{/* Additional Info */}
<button
onClick={handleCompleteInfo}
className={`relative flex flex-col items-center justify-center rounded-lg px-4 py-5 text-center border font-medium text-sm transition-all ${
disabled={additionalInfo}
className={`relative flex flex-col items-center justify-center rounded-lg px-3 py-5 sm:px-4 sm:py-5 text-center border font-medium text-xs sm:text-sm transition-all md:transition-transform md:duration-200 ${
additionalInfo
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
: 'bg-blue-600 hover:bg-blue-500 text-white border-blue-600 shadow'
: 'bg-blue-600 text-white border-blue-600 shadow md:hover:-translate-y-0.5 md:hover:shadow-lg md:hover:bg-blue-500 md:active:translate-y-0'
}`}
>
<PencilSquareIcon className="h-6 w-6 mb-2" />
<PencilSquareIcon className="h-5 w-5 sm:h-6 sm:w-6 mb-2" />
{additionalInfo ? 'Profile Completed' : 'Complete Profile'}
</button>
@ -372,15 +418,15 @@ export default function QuickActionDashboardPage() {
<button
onClick={handleSignContract}
disabled={!canSignContract || contractSigned}
className={`flex flex-col flex-1 items-center justify-center rounded-lg px-4 py-5 text-center border font-medium text-sm transition-all ${
className={`flex flex-col flex-1 items-center justify-center rounded-lg px-3 py-5 sm:px-4 sm:py-5 text-center border font-medium text-xs sm:text-sm transition-all md:transition-transform md:duration-200 ${
contractSigned
? 'bg-emerald-50 border-emerald-100 text-emerald-600 cursor-default'
: canSignContract
? 'bg-blue-600 hover:bg-blue-500 text-white border-blue-600 shadow'
? 'bg-blue-600 text-white border-blue-600 shadow md:hover:-translate-y-0.5 md:hover:shadow-lg md:hover:bg-blue-500 md:active:translate-y-0'
: 'bg-gray-300 text-gray-600 border-gray-300 cursor-not-allowed'
}`}
>
<ClipboardDocumentCheckIcon className="h-6 w-6 mb-2" />
<ClipboardDocumentCheckIcon className="h-5 w-5 sm:h-6 sm:w-6 mb-2" />
{contractSigned ? 'Contract Signed' : 'Sign Contract'}
</button>
{!canSignContract && !contractSigned && (
@ -409,7 +455,7 @@ export default function QuickActionDashboardPage() {
</div>
</div>
</main>
</div>
</BlueBlurryBackground>
{/* Tutorial Modal */}
<TutorialModal

View File

@ -1,11 +1,12 @@
'use client'
import { useState } from 'react'
import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus'
import { useToast } from '../../../components/toast/toastComponent'
import { ChevronDownIcon } from '@heroicons/react/20/solid' // NEW
interface CompanyProfileData {
companyName: string
@ -47,10 +48,132 @@ const init: CompanyProfileData = {
emergencyPhone: ''
}
function ModernSelect({
label,
placeholder = 'Select…',
value,
onChange,
options,
}: {
label: string
placeholder?: string
value: string
onChange: (next: string) => void
options: { value: string; label: string }[]
}) {
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const btnRef = useRef<HTMLButtonElement | null>(null)
const [pos, setPos] = useState({ left: 16, top: 0, width: 320 })
const selected = useMemo(() => options.find(o => o.value === value) || null, [options, value])
const filtered = useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) return options
return options.filter(o => o.label.toLowerCase().includes(q))
}, [options, query])
useEffect(() => {
if (!open) return
const update = () => {
const el = btnRef.current
if (!el) return
const r = el.getBoundingClientRect()
const padding = 16
const width = Math.min(r.width, window.innerWidth - padding * 2)
const left = Math.max(padding, Math.min(r.left, window.innerWidth - width - padding))
const top = r.bottom + 8
setPos({ left, top, width })
}
update()
window.addEventListener('resize', update)
window.addEventListener('scroll', update, true)
return () => {
window.removeEventListener('resize', update)
window.removeEventListener('scroll', update, true)
}
}, [open])
useEffect(() => {
if (!open) return
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false) }
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [open])
return (
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-1">{label} *</label>
<button
ref={btnRef}
type="button"
onClick={() => setOpen(v => !v)}
className="w-full rounded-lg border border-gray-300 bg-white/70 px-3 py-2 text-sm text-left
shadow-sm hover:border-gray-400
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent
inline-flex items-center justify-between gap-3"
aria-haspopup="listbox"
aria-expanded={open}
>
<span className={selected ? 'text-gray-900' : 'text-gray-500'}>
{selected ? selected.label : placeholder}
</span>
<ChevronDownIcon className={`h-5 w-5 text-gray-500 transition-transform ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<>
<div className="fixed inset-0 z-[90]" onClick={() => setOpen(false)} aria-hidden />
<div
className="fixed z-[100] overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl"
style={{ left: pos.left, top: pos.top, width: pos.width }}
>
<div className="p-2 border-b border-gray-100">
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search…"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
autoFocus
/>
</div>
<div className="max-h-[42vh] overflow-auto p-1">
{filtered.length === 0 ? (
<div className="px-3 py-2 text-sm text-gray-500">No results</div>
) : (
filtered.map(o => {
const active = o.value === value
return (
<button
key={o.value}
type="button"
onClick={() => { onChange(o.value); setQuery(''); setOpen(false) }}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors
${active ? 'bg-[#8D6B1D]/10 text-[#7A5E1A] font-semibold' : 'text-gray-800 hover:bg-gray-50'}`}
role="option"
aria-selected={active}
>
{o.label}
</button>
)
})
)}
</div>
</div>
</>
)}
</div>
)
}
export default function CompanyAdditionalInformationPage() {
const router = useRouter()
const user = useAuthStore(s => s.user) // NEW
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus()
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus()
const { showToast } = useToast()
const [form, setForm] = useState(init)
@ -58,6 +181,38 @@ export default function CompanyAdditionalInformationPage() {
const [success, setSuccess] = useState(false)
const [error, setError] = useState('')
// NEW: smooth redirect
const [redirectTo, setRedirectTo] = useState<string | null>(null)
const redirectOnceRef = useRef(false)
const smoothReplace = useCallback((to: string) => {
if (redirectOnceRef.current) return
redirectOnceRef.current = true
setRedirectTo(to)
window.setTimeout(() => router.replace(to), 200)
}, [router])
// NEW: hard block if step already done OR all steps done
useEffect(() => {
if (statusLoading || !userStatus) return
const allDone =
!!userStatus.email_verified &&
!!userStatus.documents_uploaded &&
!!userStatus.profile_completed &&
!!userStatus.contract_signed
if (allDone) {
smoothReplace('/dashboard') // CHANGED
} else if (userStatus.profile_completed) {
smoothReplace('/quickaction-dashboard') // CHANGED
}
}, [statusLoading, userStatus, smoothReplace])
// NEW: must be logged in
useEffect(() => {
if (!isAuthReady) return
if (!user || !accessToken) smoothReplace('/login')
}, [isAuthReady, user, accessToken, smoothReplace])
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target
setForm(p => ({ ...p, [name]: value }))
@ -178,8 +333,23 @@ export default function CompanyAdditionalInformationPage() {
}
}
const setField = (name: keyof CompanyProfileData, value: string) => {
setForm(p => ({ ...p, [name]: value }))
setError('')
}
return (
<PageLayout>
{/* NEW: smooth redirect overlay */}
{redirectTo && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
<div className="text-sm font-medium text-gray-900">Redirecting</div>
<div className="mt-1 text-xs text-gray-600">Please wait</div>
</div>
</div>
)}
<div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Animated background (same as dashboard) */}
<div className="pointer-events-none absolute inset-0 z-0">
@ -215,7 +385,7 @@ export default function CompanyAdditionalInformationPage() {
name="companyName"
value={form.companyName}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
@ -228,7 +398,7 @@ export default function CompanyAdditionalInformationPage() {
value={form.vatNumber}
onChange={handleChange}
placeholder="e.g. DE123456789"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
@ -240,7 +410,7 @@ export default function CompanyAdditionalInformationPage() {
name="street"
value={form.street}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
@ -252,7 +422,7 @@ export default function CompanyAdditionalInformationPage() {
name="postalCode"
value={form.postalCode}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
@ -264,28 +434,18 @@ export default function CompanyAdditionalInformationPage() {
name="city"
value={form.city}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<select
name="country"
<ModernSelect
label="Country"
placeholder="Select country..."
value={form.country}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
onChange={(v) => setField('country', v)}
options={COUNTRIES.map(c => ({ value: c, label: c }))}
/>
</div>
</div>
</section>
@ -307,7 +467,7 @@ export default function CompanyAdditionalInformationPage() {
value={form.accountHolder}
onChange={handleChange}
placeholder="Company / Holder name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
@ -320,7 +480,7 @@ export default function CompanyAdditionalInformationPage() {
value={form.iban}
onChange={handleChange}
placeholder="DE89 3704 0044 0532 0130 00"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
@ -333,7 +493,7 @@ export default function CompanyAdditionalInformationPage() {
value={form.bic}
onChange={handleChange}
placeholder="GENODEF1XXX"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
</div>
@ -356,7 +516,7 @@ export default function CompanyAdditionalInformationPage() {
value={form.secondPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
<div>
@ -368,7 +528,7 @@ export default function CompanyAdditionalInformationPage() {
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
<div>
@ -380,7 +540,7 @@ export default function CompanyAdditionalInformationPage() {
value={form.emergencyPhone}
onChange={handleChange}
placeholder="+49 123 456 7890"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
<div className="hidden lg:block" />
@ -402,14 +562,15 @@ export default function CompanyAdditionalInformationPage() {
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
className="inline-flex items-center rounded-md border border-[#8D6B1D]/40 px-4 py-2 text-sm font-semibold text-[#8D6B1D] bg-white hover:bg-[#8D6B1D]/10"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={loading || success}
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
className="inline-flex items-center rounded-md bg-[#8D6B1D] px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-[#7A5E1A] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Speichern…' : success ? 'Gespeichert' : 'Save & Continue'}
</button>

View File

@ -1,11 +1,12 @@
'use client'
import { useEffect, useState } from 'react'
import { useEffect, useMemo, useRef, useState, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore'
import { useUserStatus } from '../../../hooks/useUserStatus'
import { useToast } from '../../../components/toast/toastComponent'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
interface PersonalProfileData {
dob: string
@ -55,10 +56,153 @@ const initialData: PersonalProfileData = {
emergencyPhone: ''
}
type SelectOption = { value: string; label: string }
function ModernSelect({
label,
placeholder = 'Select…',
value,
onChange,
options,
}: {
label: string
placeholder?: string
value: string
onChange: (next: string) => void
options: SelectOption[]
}) {
const [open, setOpen] = useState(false)
const [query, setQuery] = useState('')
const btnRef = useRef<HTMLButtonElement | null>(null)
const [pos, setPos] = useState({ left: 16, top: 0, width: 320 })
const selected = useMemo(
() => options.find(o => o.value === value) || null,
[options, value]
)
const filtered = useMemo(() => {
const q = query.trim().toLowerCase()
if (!q) return options
return options.filter(o => o.label.toLowerCase().includes(q))
}, [options, query])
useEffect(() => {
if (!open) return
const update = () => {
const el = btnRef.current
if (!el) return
const r = el.getBoundingClientRect()
const padding = 16
const width = Math.min(r.width, window.innerWidth - padding * 2)
const left = Math.max(padding, Math.min(r.left, window.innerWidth - width - padding))
const top = r.bottom + 8
setPos({ left, top, width })
}
update()
window.addEventListener('resize', update)
window.addEventListener('scroll', update, true)
return () => {
window.removeEventListener('resize', update)
window.removeEventListener('scroll', update, true)
}
}, [open])
// close on escape
useEffect(() => {
if (!open) return
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setOpen(false)
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [open])
return (
<div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-1">{label} *</label>
<button
ref={btnRef}
type="button"
onClick={() => setOpen(v => !v)}
className="w-full rounded-lg border border-gray-300 bg-white/70 px-3 py-2 text-sm text-left
shadow-sm hover:border-gray-400
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent
inline-flex items-center justify-between gap-3"
aria-haspopup="listbox"
aria-expanded={open}
>
<span className={selected ? 'text-gray-900' : 'text-gray-500'}>
{selected ? selected.label : placeholder}
</span>
<ChevronDownIcon className={`h-5 w-5 text-gray-500 transition-transform ${open ? 'rotate-180' : ''}`} />
</button>
{open && (
<>
{/* click-away overlay */}
<div className="fixed inset-0 z-[90]" onClick={() => setOpen(false)} aria-hidden />
{/* dropdown (fixed so it “pops out under” even on mobile) */}
<div
className="fixed z-[100] overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl"
style={{ left: pos.left, top: pos.top, width: pos.width }}
>
<div className="p-2 border-b border-gray-100">
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search…"
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
autoFocus
/>
</div>
<div className="max-h-[42vh] overflow-auto p-1">
{filtered.length === 0 ? (
<div className="px-3 py-2 text-sm text-gray-500">No results</div>
) : (
filtered.map(o => {
const active = o.value === value
return (
<button
key={o.value}
type="button"
onClick={() => {
onChange(o.value)
setQuery('')
setOpen(false)
}}
className={`w-full text-left px-3 py-2 rounded-lg text-sm transition-colors
${active ? 'bg-[#8D6B1D]/10 text-[#7A5E1A] font-semibold' : 'text-gray-800 hover:bg-gray-50'}`}
role="option"
aria-selected={active}
>
{o.label}
</button>
)
})
)}
</div>
</div>
</>
)}
</div>
)
}
export default function PersonalAdditionalInformationPage() {
const router = useRouter()
const user = useAuthStore(s => s.user) // NEW
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus()
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus()
const { showToast } = useToast()
const [form, setForm] = useState(initialData)
@ -270,8 +414,55 @@ export default function PersonalAdditionalInformationPage() {
}
}
const setField = (name: keyof PersonalProfileData, value: string) => {
setForm(p => ({ ...p, [name]: value }))
setError('')
}
// NEW: smooth redirect
const [redirectTo, setRedirectTo] = useState<string | null>(null)
const redirectOnceRef = useRef(false)
const smoothReplace = useCallback((to: string) => {
if (redirectOnceRef.current) return
redirectOnceRef.current = true
setRedirectTo(to)
window.setTimeout(() => router.replace(to), 200)
}, [router])
// NEW: hard block if step already done OR all steps done
useEffect(() => {
if (statusLoading || !userStatus) return
const allDone =
!!userStatus.email_verified &&
!!userStatus.documents_uploaded &&
!!userStatus.profile_completed &&
!!userStatus.contract_signed
if (allDone) {
smoothReplace('/dashboard') // CHANGED
} else if (userStatus.profile_completed) {
smoothReplace('/quickaction-dashboard') // CHANGED
}
}, [statusLoading, userStatus, smoothReplace])
// NEW: must be logged in
useEffect(() => {
if (!isAuthReady) return
if (!user || !accessToken) smoothReplace('/login')
}, [isAuthReady, user, accessToken, smoothReplace])
return (
<PageLayout>
{/* NEW: smooth redirect overlay */}
{redirectTo && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
<div className="text-sm font-medium text-gray-900">Redirecting</div>
<div className="mt-1 text-xs text-gray-600">Please wait</div>
</div>
</div>
)}
<div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Animated background (same as dashboard) */}
<div className="pointer-events-none absolute inset-0 z-0">
@ -310,28 +501,18 @@ export default function PersonalAdditionalInformationPage() {
onChange={handleChange}
min={new Date(new Date().getFullYear() - 120, 0, 1).toISOString().split('T')[0]}
max={new Date(new Date().getFullYear() - 18, 11, 31).toISOString().split('T')[0]}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nationality *
</label>
<select
name="nationality"
<ModernSelect
label="Nationality"
placeholder="Select nationality..."
value={form.nationality}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select nationality...</option>
{NATIONALITIES.map(nationality => (
<option key={nationality} value={nationality}>
{nationality}
</option>
))}
</select>
onChange={(v) => setField('nationality', v)}
options={NATIONALITIES.map(n => ({ value: n, label: n }))}
/>
</div>
<div className="sm:col-span-2 lg:col-span-3">
<label className="block text-sm font-medium text-gray-700 mb-1">
@ -342,7 +523,7 @@ export default function PersonalAdditionalInformationPage() {
value={form.street}
onChange={handleChange}
placeholder="Street & House Number"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
@ -355,7 +536,7 @@ export default function PersonalAdditionalInformationPage() {
value={form.postalCode}
onChange={handleChange}
placeholder="e.g. 12345"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
@ -368,28 +549,18 @@ export default function PersonalAdditionalInformationPage() {
value={form.city}
onChange={handleChange}
placeholder="e.g. Berlin"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Country *
</label>
<select
name="country"
<ModernSelect
label="Country"
placeholder="Select country..."
value={form.country}
onChange={handleChange}
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
required
>
<option value="">Select country...</option>
{COUNTRIES.map(country => (
<option key={country} value={country}>
{country}
</option>
))}
</select>
onChange={(v) => setField('country', v)}
options={COUNTRIES.map(c => ({ value: c, label: c }))}
/>
</div>
</div>
</section>
@ -411,7 +582,7 @@ export default function PersonalAdditionalInformationPage() {
value={form.accountHolder}
onChange={handleChange}
placeholder="Full name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
@ -424,7 +595,7 @@ export default function PersonalAdditionalInformationPage() {
value={form.iban}
onChange={handleChange}
placeholder="e.g. DE89 3704 0044 0532 0130 00"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm tracking-wide uppercase focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
required
/>
</div>
@ -448,7 +619,7 @@ export default function PersonalAdditionalInformationPage() {
value={form.secondPhone}
onChange={handleChange}
placeholder="+43 660 1234567"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
<div>
@ -460,7 +631,7 @@ export default function PersonalAdditionalInformationPage() {
value={form.emergencyName}
onChange={handleChange}
placeholder="Contact name"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
<div>
@ -472,7 +643,7 @@ export default function PersonalAdditionalInformationPage() {
value={form.emergencyPhone}
onChange={handleChange}
placeholder="+43 660 1234567"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent"
/>
</div>
<div className="hidden lg:block" />
@ -494,14 +665,15 @@ export default function PersonalAdditionalInformationPage() {
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="inline-flex items-center rounded-md border border-gray-300 px-4 py-2 text-sm font-semibold text-gray-700 bg-white hover:bg-gray-50"
className="inline-flex items-center rounded-md border border-[#8D6B1D]/40 px-4 py-2 text-sm font-semibold text-[#8D6B1D] bg-white hover:bg-[#8D6B1D]/10"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={loading || success}
className="inline-flex items-center rounded-md bg-indigo-600 px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed"
className="inline-flex items-center rounded-md bg-[#8D6B1D] px-6 py-2.5 text-sm font-semibold text-white shadow hover:bg-[#7A5E1A] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Saving…' : success ? 'Saved' : 'Save & Continue'}
</button>

View File

@ -2,6 +2,7 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import PageLayout from '../../components/PageLayout'
import BlueBlurryBackground from '../../components/background/blueblurry' // NEW
import useAuthStore from '../../store/authStore'
import { useUserStatus } from '../../hooks/useUserStatus'
import { useRouter } from 'next/navigation'
@ -9,8 +10,9 @@ import { useToast } from '../../components/toast/toastComponent'
export default function EmailVerifyPage() {
const user = useAuthStore(s => s.user)
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
const token = useAuthStore(s => s.accessToken)
const { refreshStatus } = useUserStatus()
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus() // CHANGED
const [code, setCode] = useState(['', '', '', '', '', ''])
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState('')
@ -339,20 +341,51 @@ export default function EmailVerifyPage() {
return `${m}:${String(s).padStart(2, '0')}`
}
// NEW: hard block if step already done OR all steps done
const [redirectTo, setRedirectTo] = useState<string | null>(null)
const redirectOnceRef = useRef(false)
const smoothReplace = useCallback((to: string) => {
if (redirectOnceRef.current) return
redirectOnceRef.current = true
setRedirectTo(to)
window.setTimeout(() => router.replace(to), 200)
}, [router])
useEffect(() => {
if (statusLoading || !userStatus) return
const allDone =
!!userStatus.email_verified &&
!!userStatus.documents_uploaded &&
!!userStatus.profile_completed &&
!!userStatus.contract_signed
if (allDone) {
smoothReplace('/dashboard') // CHANGED
} else if (userStatus.email_verified) {
smoothReplace('/quickaction-dashboard') // CHANGED
}
}, [statusLoading, userStatus, smoothReplace])
// NEW: must be logged in
useEffect(() => {
if (!isAuthReady) return
if (!user || !token) smoothReplace('/login')
}, [isAuthReady, user, token, smoothReplace])
return (
<PageLayout>
<div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Animated background (same as dashboard) */}
<div className="pointer-events-none absolute inset-0 z-0">
{/* Soft gradient blobs */}
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
{/* Subtle radial highlight */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
{/* NEW: smooth redirect overlay */}
{redirectTo && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
<div className="text-sm font-medium text-gray-900">Redirecting</div>
<div className="mt-1 text-xs text-gray-600">Please wait</div>
</div>
</div>
)}
<main className="relative z-10 flex flex-col flex-1 w-full px-4 sm:px-6 py-16 sm:py-24">
<BlueBlurryBackground>
<main className="flex flex-col flex-1 w-full px-4 sm:px-6 py-16 sm:py-24">
<div className="max-w-xl mx-auto">
<div className="text-center mb-10">
<h1 className="text-3xl sm:text-4xl font-semibold tracking-tight text-gray-900">
@ -362,7 +395,7 @@ export default function EmailVerifyPage() {
{initialEmailSent ? (
<>
We sent a 6-digit code to{' '}
<span className="text-blue-700 font-medium">
<span className="text-[#8D6B1D] font-medium">
{user?.email || 'your email'}
</span>
. Enter it below.
@ -370,7 +403,7 @@ export default function EmailVerifyPage() {
) : (
<>
Sending verification email to{' '}
<span className="text-blue-700 font-medium">
<span className="text-[#8D6B1D] font-medium">
{user?.email || 'your email'}
</span>
...
@ -379,13 +412,11 @@ export default function EmailVerifyPage() {
</p>
</div>
{/* Card */}
<form
onSubmit={handleSubmit}
className="bg-white/95 backdrop-blur rounded-2xl shadow-xl ring-1 ring-black/5 px-6 py-8 sm:px-10 sm:py-10"
>
<fieldset disabled={submitting || success} className="space-y-8">
{/* Inputs */}
<div className="flex justify-center gap-2 sm:gap-3">
{code.map((v, i) => (
<input
@ -401,9 +432,9 @@ export default function EmailVerifyPage() {
onPaste={e => handlePaste(i, e)}
className={`w-12 h-14 sm:w-14 sm:h-16 text-center text-2xl font-semibold rounded-lg border transition-colors outline-none
${v
? 'border-indigo-500 ring-2 ring-indigo-400/40 bg-white text-gray-900'
? 'border-[#8D6B1D] ring-2 ring-[#8D6B1D]/25 bg-white text-gray-900'
: 'border-gray-300 bg-white/80 text-gray-700'}
focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500`}
focus:ring-2 focus:ring-[#8D6B1D] focus:border-[#8D6B1D]`}
/>
))}
</div>
@ -423,7 +454,10 @@ export default function EmailVerifyPage() {
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<button
type="submit"
className="w-full sm:w-auto inline-flex justify-center items-center rounded-lg px-6 py-3 font-semibold text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
className="w-full sm:w-auto inline-flex justify-center items-center rounded-lg px-6 py-3 font-semibold text-white
bg-[#8D6B1D] hover:bg-[#7A5E1A]
focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2
disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{submitting ? (
<>
@ -437,7 +471,7 @@ export default function EmailVerifyPage() {
type="button"
onClick={handleResend}
disabled={!!resendCooldown || submitting || success}
className="text-sm font-medium text-indigo-700 hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
className="text-sm font-medium text-[#8D6B1D] hover:underline disabled:text-gray-400 disabled:cursor-not-allowed"
>
{resendCooldown
? `Resend in ${formatMmSs(resendCooldown)}`
@ -449,7 +483,7 @@ export default function EmailVerifyPage() {
<button
type="button"
onClick={() => router.push('/quickaction-dashboard')}
className="text-sm font-medium text-gray-700 hover:underline"
className="text-sm font-medium text-[#8D6B1D] hover:underline"
>
Go to Dashboard
</button>
@ -458,7 +492,7 @@ export default function EmailVerifyPage() {
<div className="mt-8 text-center text-xs text-gray-500">
Didnt receive the email? Please check your junk/spam folder. Still having issues?{' '}
<a href="mailto:test@test.com" className="text-indigo-600 hover:underline">
<a href="mailto:test@test.com" className="text-[#8D6B1D] hover:underline">
Contact support
</a>
.
@ -466,7 +500,7 @@ export default function EmailVerifyPage() {
</form>
</div>
</main>
</div>
</BlueBlurryBackground>
</PageLayout>
)
}

View File

@ -1,6 +1,6 @@
'use client'
import { useState, useEffect, useRef } from 'react'
import { useState, useEffect, useRef, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore'
@ -10,8 +10,10 @@ import { useToast } from '../../../components/toast/toastComponent'
export default function CompanySignContractPage() {
const router = useRouter()
const user = useAuthStore(s => s.user) // NEW
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus()
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus()
const { showToast } = useToast()
const [companyName, setCompanyName] = useState('')
@ -246,15 +248,7 @@ export default function CompanySignContractPage() {
// Redirect to main dashboard after short delay
setTimeout(() => {
// Check if we came from tutorial
const urlParams = new URLSearchParams(window.location.search)
const fromTutorial = urlParams.get('tutorial') === 'true'
if (fromTutorial) {
router.push('/quickaction-dashboard?tutorial=true')
} else {
router.push('/quickaction-dashboard')
}
router.push('/dashboard')
}, 2000)
} catch (error: any) {
@ -271,8 +265,49 @@ export default function CompanySignContractPage() {
}
}
// NEW: hard block if step already done OR all steps done
const [redirectTo, setRedirectTo] = useState<string | null>(null)
const redirectOnceRef = useRef(false)
const smoothReplace = useCallback((to: string) => {
if (redirectOnceRef.current) return
redirectOnceRef.current = true
setRedirectTo(to)
window.setTimeout(() => router.replace(to), 200)
}, [router])
useEffect(() => {
if (statusLoading || !userStatus) return
const allDone =
!!userStatus.email_verified &&
!!userStatus.documents_uploaded &&
!!userStatus.profile_completed &&
!!userStatus.contract_signed
if (allDone) {
smoothReplace('/dashboard') // CHANGED
} else if (userStatus.contract_signed) {
smoothReplace('/quickaction-dashboard') // CHANGED
}
}, [statusLoading, userStatus, smoothReplace])
// NEW: must be logged in
useEffect(() => {
if (!isAuthReady) return
if (!user || !accessToken) smoothReplace('/login')
}, [isAuthReady, user, accessToken, smoothReplace])
return (
<PageLayout>
{/* NEW: smooth redirect overlay */}
{redirectTo && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
<div className="text-sm font-medium text-gray-900">Redirecting</div>
<div className="mt-1 text-xs text-gray-600">Please wait</div>
</div>
</div>
)}
<div className="relative min-h-screen overflow-hidden bg-slate-50">
<div className="pointer-events-none absolute inset-0 z-0">
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />

View File

@ -1,6 +1,6 @@
'use client'
import { useState, useEffect, useRef } from 'react'
import { useState, useEffect, useRef, useCallback } from 'react'
import { useRouter } from 'next/navigation'
import PageLayout from '../../../components/PageLayout'
import useAuthStore from '../../../store/authStore'
@ -10,8 +10,10 @@ import { useToast } from '../../../components/toast/toastComponent'
export default function PersonalSignContractPage() {
const router = useRouter()
const user = useAuthStore(s => s.user) // NEW
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
const { accessToken } = useAuthStore()
const { refreshStatus } = useUserStatus()
const { userStatus, loading: statusLoading, refreshStatus } = useUserStatus() // CHANGED
const { showToast } = useToast()
const [date, setDate] = useState('')
@ -160,6 +162,37 @@ export default function PersonalSignContractPage() {
]).finally(() => setPreviewLoading(false))
}, [accessToken])
// NEW: hard block if step already done OR all steps done
const [redirectTo, setRedirectTo] = useState<string | null>(null)
const redirectOnceRef = useRef(false)
const smoothReplace = useCallback((to: string) => {
if (redirectOnceRef.current) return
redirectOnceRef.current = true
setRedirectTo(to)
window.setTimeout(() => router.replace(to), 200)
}, [router])
// NEW: must be logged in
useEffect(() => {
if (!isAuthReady) return
if (!user || !accessToken) smoothReplace('/login')
}, [isAuthReady, user, accessToken, smoothReplace])
useEffect(() => {
if (statusLoading || !userStatus) return
const allDone =
!!userStatus.email_verified &&
!!userStatus.documents_uploaded &&
!!userStatus.profile_completed &&
!!userStatus.contract_signed
if (allDone) {
smoothReplace('/dashboard') // CHANGED
} else if (userStatus.contract_signed) {
smoothReplace('/quickaction-dashboard') // CHANGED
}
}, [statusLoading, userStatus, smoothReplace])
const valid = () => {
const contractChecked = agreeContract
const dataChecked = agreeData
@ -242,15 +275,7 @@ export default function PersonalSignContractPage() {
// Redirect to main dashboard after short delay
setTimeout(() => {
// Check if we came from tutorial
const urlParams = new URLSearchParams(window.location.search)
const fromTutorial = urlParams.get('tutorial') === 'true'
if (fromTutorial) {
router.push('/quickaction-dashboard?tutorial=true')
} else {
router.push('/quickaction-dashboard')
}
router.push('/dashboard')
}, 2000)
} catch (error: any) {
@ -269,6 +294,16 @@ export default function PersonalSignContractPage() {
return (
<PageLayout>
{/* NEW: smooth redirect overlay */}
{redirectTo && (
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
<div className="text-sm font-medium text-gray-900">Redirecting</div>
<div className="mt-1 text-xs text-gray-600">Please wait</div>
</div>
</div>
)}
<div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Animated background (same as dashboard) */}
<div className="pointer-events-none absolute inset-0 z-0">

View File

@ -1,11 +1,13 @@
'use client'
import PageLayout from '../../../components/PageLayout'
import BlueBlurryBackground from '../../../components/background/blueblurry'
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
import { useCompanyUploadId } from './hooks/useCompanyUploadId'
import useAuthStore from '../../../store/authStore'
import { useEffect, useState } from 'react'
import { useEffect, useState, useCallback, useRef } from 'react'
import { useRouter } from 'next/navigation'
import { useUserStatus } from '../../../hooks/useUserStatus'
const DOC_TYPES = ['Personalausweis', 'Reisepass', 'Führerschein', 'Aufenthaltstitel']
@ -26,48 +28,68 @@ export default function CompanyIdUploadPage() {
} = useCompanyUploadId()
const user = useAuthStore(s => s.user)
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
const router = useRouter()
const [blocked, setBlocked] = useState(false)
const { userStatus, loading: statusLoading } = useUserStatus()
// NEW: smooth redirect
const [redirectTo, setRedirectTo] = useState<string | null>(null)
const redirectOnceRef = useRef(false)
const smoothReplace = useCallback((to: string) => {
if (redirectOnceRef.current) return
redirectOnceRef.current = true
setRedirectTo(to)
window.setTimeout(() => router.replace(to), 200)
}, [router])
// Guard: only 'company' users allowed on this page
useEffect(() => {
const ut = (user as any)?.userType || (user as any)?.role
console.log('🧭 UploadID Guard [company]: userType =', ut)
if (ut && ut !== 'company') {
console.warn('🚫 UploadID Guard [company]: access denied for userType:', ut, '-> redirecting to personal upload')
setBlocked(true)
router.replace('/quickaction-dashboard/register-upload-id/personal')
} else if (ut === 'company') {
console.log('✅ UploadID Guard [company]: access granted')
}
}, [user, router])
if (ut && ut !== 'company') smoothReplace('/quickaction-dashboard/register-upload-id/personal') // CHANGED
}, [user, smoothReplace])
if (blocked) {
useEffect(() => {
if (statusLoading || !userStatus) return
const allDone =
!!userStatus.email_verified &&
!!userStatus.documents_uploaded &&
!!userStatus.profile_completed &&
!!userStatus.contract_signed
if (allDone) {
smoothReplace('/dashboard') // CHANGED
} else if (userStatus.documents_uploaded) {
smoothReplace('/quickaction-dashboard') // CHANGED
}
}, [statusLoading, userStatus, smoothReplace])
// NEW: must be logged in
useEffect(() => {
if (!isAuthReady) return
if (!user) smoothReplace('/login')
}, [isAuthReady, user, smoothReplace])
if (redirectTo) {
return (
<PageLayout>
<div className="min-h-[50vh] flex items-center justify-center">
<div className="text-center text-sm text-gray-600">
Redirecting to the correct upload page
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
<div className="text-sm font-medium text-gray-900">Redirecting</div>
<div className="mt-1 text-xs text-gray-600">Please wait</div>
</div>
</div>
</PageLayout>
)
}
const goBackToDashboard = () => {
// CHANGED: do NOT preserve ?tutorial=true, otherwise dashboard force-opens the tutorial again
router.push('/quickaction-dashboard')
}
return (
<PageLayout>
<div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Animated background (same as dashboard) */}
<div className="pointer-events-none absolute inset-0 z-0">
{/* Soft gradient blobs */}
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
{/* Subtle radial highlight */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</div>
<main className="relative z-10 flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
<BlueBlurryBackground>
<main className="flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
<form
onSubmit={submit}
className="relative max-w-7xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10 overflow-hidden"
@ -89,7 +111,7 @@ export default function CompanyIdUploadPage() {
<input
value={idNumber}
onChange={e => setIdNumber(e.target.value)}
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`}
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'} focus:ring-[#8D6B1D] focus:border-transparent`}
placeholder="Enter contact person's ID number"
required
/>
@ -105,7 +127,7 @@ export default function CompanyIdUploadPage() {
<select
value={idType}
onChange={e => setIdType(e.target.value)}
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`}
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'} focus:ring-[#8D6B1D] focus:border-transparent`}
required
>
<option value="">Select document type</option>
@ -122,7 +144,7 @@ export default function CompanyIdUploadPage() {
value={expiryDate}
onChange={e => setExpiryDate(e.target.value)}
placeholder="tt.mm.jjjj"
className={`${inputBase} ${expiryDate ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
className={`${inputBase} ${expiryDate ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80 focus:ring-[#8D6B1D] focus:border-transparent`}
required
/>
<p className="mt-1 text-xs text-gray-600">
@ -139,7 +161,9 @@ export default function CompanyIdUploadPage() {
<button
type="button"
onClick={() => setHasBack(v => { const next = !v; if (!next) setExtraFile(null); return next })}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${hasBack ? 'bg-indigo-600' : 'bg-gray-300'}`}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
hasBack ? 'bg-[#8D6B1D]' : 'bg-gray-300'
}`}
aria-pressed={hasBack}
>
<span className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ${hasBack ? 'translate-x-5' : 'translate-x-0'}`} />
@ -154,7 +178,7 @@ export default function CompanyIdUploadPage() {
{...dropHandlers}
onDrop={e => onDrop(e, 'front')}
onClick={() => openPicker('front')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-[#8D6B1D] hover:bg-[#8D6B1D]/5 cursor-pointer transition"
>
<input
ref={frontRef}
@ -184,8 +208,8 @@ export default function CompanyIdUploadPage() {
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-4 transition" />
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
Click to upload front side
</p>
<p className="mt-2 text-xs text-gray-500">
@ -203,7 +227,7 @@ export default function CompanyIdUploadPage() {
{...dropHandlers}
onDrop={e => onDrop(e, 'extra')}
onClick={() => openPicker('extra')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-[#8D6B1D] hover:bg-[#8D6B1D]/5 cursor-pointer transition"
>
<input
ref={extraRef}
@ -233,8 +257,8 @@ export default function CompanyIdUploadPage() {
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-4 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-4 transition" />
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
Click to upload back side
</p>
<p className="mt-2 text-xs text-gray-500">
@ -249,11 +273,11 @@ export default function CompanyIdUploadPage() {
</div>
{/* Info */}
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
<p className="text-sm font-semibold text-indigo-900 mb-3">
<div className="mt-8 rounded-lg bg-[#8D6B1D]/10 border border-[#8D6B1D]/20 px-5 py-5">
<p className="text-sm font-semibold text-[#3B2C04] mb-3">
Please ensure your ID documents:
</p>
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5">
<ul className="text-sm text-[#7A5E1A] space-y-1 list-disc pl-5">
<li>Are clearly visible and readable</li>
<li>Show all four corners</li>
<li>Are not expired</li>
@ -273,11 +297,20 @@ export default function CompanyIdUploadPage() {
</div>
)}
<div className="mt-8 flex justify-end">
{/* CHANGED: add "Back to Dashboard" next to submit */}
<div className="mt-8 flex items-center justify-between gap-3">
<button
type="button"
onClick={goBackToDashboard}
className="inline-flex items-center justify-center rounded-md border border-[#8D6B1D] bg-white/70 px-4 py-3 text-sm font-semibold text-[#8D6B1D] hover:bg-[#8D6B1D]/10 transition"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
className="inline-flex items-center justify-center rounded-md bg-[#8D6B1D] px-6 py-3 text-sm font-semibold text-white shadow hover:bg-[#7A5E1A] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
</button>
@ -285,7 +318,7 @@ export default function CompanyIdUploadPage() {
</div>
</form>
</main>
</div>
</BlueBlurryBackground>
</PageLayout>
)
}

View File

@ -2,10 +2,12 @@
import { usePersonalUploadId } from './hooks/usePersonalUploadId'
import PageLayout from '../../../components/PageLayout'
import { useEffect, useState } from 'react'
import BlueBlurryBackground from '../../../components/background/blueblurry'
import { useEffect, useState, useCallback, useRef } from 'react'
import { useRouter } from 'next/navigation'
import useAuthStore from '../../../store/authStore'
import { DocumentArrowUpIcon, XMarkIcon } from '@heroicons/react/24/outline'
import { useUserStatus } from '../../../hooks/useUserStatus'
// Add back ID types for the dropdown
const ID_TYPES = [
@ -16,29 +18,54 @@ const ID_TYPES = [
]
export default function PersonalIdUploadPage() {
// NEW: guard company users from accessing personal page
const user = useAuthStore(s => s.user)
const isAuthReady = useAuthStore(s => (s as any).isAuthReady) // NEW
const router = useRouter()
const [blocked, setBlocked] = useState(false)
const { userStatus, loading: statusLoading } = useUserStatus()
// NEW: smooth redirect
const [redirectTo, setRedirectTo] = useState<string | null>(null)
const redirectOnceRef = useRef(false)
const smoothReplace = useCallback((to: string) => {
if (redirectOnceRef.current) return
redirectOnceRef.current = true
setRedirectTo(to)
window.setTimeout(() => router.replace(to), 200)
}, [router])
useEffect(() => {
const ut = (user as any)?.userType || (user as any)?.role
console.log('🧭 UploadID Guard [personal]: userType =', ut)
if (ut && ut !== 'personal') {
console.warn('🚫 UploadID Guard [personal]: access denied for userType:', ut, '-> redirecting to company upload')
setBlocked(true)
router.replace('/quickaction-dashboard/register-upload-id/company')
} else if (ut === 'personal') {
console.log('✅ UploadID Guard [personal]: access granted')
}
}, [user, router])
if (ut && ut !== 'personal') smoothReplace('/quickaction-dashboard/register-upload-id/company') // CHANGED
}, [user, smoothReplace])
if (blocked) {
useEffect(() => {
if (statusLoading || !userStatus) return
const allDone =
!!userStatus.email_verified &&
!!userStatus.documents_uploaded &&
!!userStatus.profile_completed &&
!!userStatus.contract_signed
if (allDone) {
smoothReplace('/dashboard') // CHANGED
} else if (userStatus.documents_uploaded) {
smoothReplace('/quickaction-dashboard') // CHANGED
}
}, [statusLoading, userStatus, smoothReplace])
// NEW: must be logged in
useEffect(() => {
if (!isAuthReady) return
if (!user) smoothReplace('/login')
}, [isAuthReady, user, smoothReplace])
if (redirectTo) {
return (
<PageLayout>
<div className="min-h-[50vh] flex items-center justify-center">
<div className="text-center text-sm text-gray-600">
Redirecting to the correct upload page
<div className="fixed inset-0 z-[200] flex items-center justify-center bg-white/70 backdrop-blur-sm transition-opacity duration-200 opacity-100">
<div className="rounded-xl bg-white px-5 py-4 shadow ring-1 ring-black/5">
<div className="text-sm font-medium text-gray-900">Redirecting</div>
<div className="mt-1 text-xs text-gray-600">Please wait</div>
</div>
</div>
</PageLayout>
@ -58,20 +85,15 @@ export default function PersonalIdUploadPage() {
inputBase,
} = usePersonalUploadId()
const goBackToDashboard = () => {
// CHANGED: do NOT preserve ?tutorial=true, otherwise dashboard force-opens the tutorial again
router.push('/quickaction-dashboard')
}
return (
<PageLayout>
<div className="relative min-h-screen overflow-hidden bg-slate-50">
{/* Animated background (same as dashboard) */}
<div className="pointer-events-none absolute inset-0 z-0">
{/* Soft gradient blobs */}
<div className="absolute -top-32 -left-20 h-80 w-80 rounded-full bg-blue-400/25 blur-3xl animate-pulse" />
<div className="absolute top-1/4 -right-24 h-96 w-96 rounded-full bg-emerald-400/20 blur-3xl animate-pulse" />
<div className="absolute bottom-[-6rem] left-1/4 h-96 w-96 rounded-full bg-indigo-400/20 blur-3xl animate-pulse" />
{/* Subtle radial highlight */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(59,130,246,0.14),transparent_60%)]" />
</div>
<main className="relative z-10 flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
<BlueBlurryBackground>
<main className="flex flex-col flex-1 w-full px-5 lg:px-10 py-10">
<form
onSubmit={submit}
className="relative max-w-7xl w-full mx-auto bg-white rounded-2xl shadow-xl ring-1 ring-black/10 overflow-hidden"
@ -94,7 +116,7 @@ export default function PersonalIdUploadPage() {
value={idNumber}
onChange={e => setIdNumber(e.target.value)}
placeholder="Enter your ID number"
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'}`}
className={`${inputBase} ${idNumber ? 'text-gray-900' : 'text-gray-700'} focus:ring-[#8D6B1D] focus:border-transparent`}
required
/>
<p className="mt-1 text-xs text-gray-600">
@ -109,7 +131,7 @@ export default function PersonalIdUploadPage() {
<select
value={idType}
onChange={e => setIdType(e.target.value)}
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'}`}
className={`${inputBase} ${idType ? 'text-gray-900' : 'text-gray-700'} focus:ring-[#8D6B1D] focus:border-transparent`}
required
>
<option value="">Select ID type</option>
@ -130,7 +152,7 @@ export default function PersonalIdUploadPage() {
value={expiry}
onChange={e => setExpiry(e.target.value)}
placeholder="tt.mm jjjj"
className={`${inputBase} ${expiry ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80`}
className={`${inputBase} ${expiry ? 'text-gray-900' : 'text-gray-700'} appearance-none [&::-webkit-calendar-picker-indicator]:opacity-80 focus:ring-[#8D6B1D] focus:border-transparent`}
required
/>
</div>
@ -145,7 +167,7 @@ export default function PersonalIdUploadPage() {
type="button"
onClick={() => setHasBack(v => !v)}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 focus:outline-none ${
hasBack ? 'bg-indigo-600' : 'bg-gray-300'
hasBack ? 'bg-[#8D6B1D]' : 'bg-gray-300'
}`}
aria-pressed={hasBack}
>
@ -165,7 +187,7 @@ export default function PersonalIdUploadPage() {
{...dropEvents}
onDrop={e => onDrop(e, 'front')}
onClick={() => openPicker('front')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-[#8D6B1D] hover:bg-[#8D6B1D]/5 cursor-pointer transition"
>
<input
ref={frontInputRef}
@ -195,8 +217,8 @@ export default function PersonalIdUploadPage() {
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-3 transition" />
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
Click to upload front side
</p>
<p className="mt-2 text-xs text-gray-500">
@ -214,7 +236,7 @@ export default function PersonalIdUploadPage() {
{...dropEvents}
onDrop={e => onDrop(e, 'back')}
onClick={() => openPicker('back')}
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-indigo-400 hover:bg-indigo-50/40 cursor-pointer transition"
className="group relative flex flex-col items-center justify-center rounded-lg border-2 border-dashed border-gray-300 bg-gray-50/60 px-4 py-6 sm:py-10 text-center hover:border-[#8D6B1D] hover:bg-[#8D6B1D]/5 cursor-pointer transition"
>
<input
ref={backInputRef}
@ -244,8 +266,8 @@ export default function PersonalIdUploadPage() {
</div>
) : (
<>
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-indigo-500 mb-3 transition" />
<p className="text-sm font-medium text-indigo-600 group-hover:text-indigo-500">
<DocumentArrowUpIcon className="h-10 w-10 text-gray-400 group-hover:text-[#8D6B1D] mb-3 transition" />
<p className="text-sm font-medium text-[#8D6B1D] group-hover:text-[#7A5E1A]">
Click to upload back side
</p>
<p className="mt-2 text-xs text-gray-500">
@ -260,11 +282,11 @@ export default function PersonalIdUploadPage() {
</div>
{/* Info Box, errors, success, submit */}
<div className="mt-8 rounded-lg bg-indigo-50/60 border border-indigo-100 px-5 py-5">
<p className="text-sm font-semibold text-indigo-900 mb-3">
<div className="mt-8 rounded-lg bg-[#8D6B1D]/10 border border-[#8D6B1D]/20 px-5 py-5">
<p className="text-sm font-semibold text-[#3B2C04] mb-3">
Please ensure your ID documents:
</p>
<ul className="text-sm text-indigo-800 space-y-1 list-disc pl-5">
<ul className="text-sm text-[#7A5E1A] space-y-1 list-disc pl-5">
<li>Are clearly visible and readable</li>
<li>Show all four corners</li>
<li>Are not expired</li>
@ -284,11 +306,20 @@ export default function PersonalIdUploadPage() {
</div>
)}
<div className="mt-8 flex justify-end">
{/* CHANGED: add "Back to Dashboard" next to submit */}
<div className="mt-8 flex items-center justify-between gap-3">
<button
type="button"
onClick={goBackToDashboard}
className="inline-flex items-center justify-center rounded-md border border-[#8D6B1D] bg-white/70 px-4 py-3 text-sm font-semibold text-[#8D6B1D] hover:bg-[#8D6B1D]/10 transition"
>
Back to Dashboard
</button>
<button
type="submit"
disabled={submitting || success}
className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-6 py-3 text-sm font-semibold text-white shadow hover:bg-indigo-500 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
className="inline-flex items-center justify-center rounded-md bg-[#8D6B1D] px-6 py-3 text-sm font-semibold text-white shadow hover:bg-[#7A5E1A] focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition"
>
{submitting ? 'Uploading…' : success ? 'Saved' : 'Upload & Continue'}
</button>
@ -296,7 +327,7 @@ export default function PersonalIdUploadPage() {
</div>
</form>
</main>
</div>
</BlueBlurryBackground>
</PageLayout>
)
}

View File

@ -2,6 +2,7 @@
import React, { useState } from 'react'
import { ClipboardDocumentIcon } from '@heroicons/react/24/outline'
import { useToast } from '../../components/toast/toastComponent'
interface ReferralLink {
id?: string | number
@ -39,6 +40,7 @@ function shortLink(href?: string) {
}
export default function ReferralLinksListWidget({ links, onDeactivate }: Props) {
const { showToast } = useToast()
// Local floating tooltip (fixed) so table doesn't scroll to show it
const [tooltip, setTooltip] = useState<{ visible: boolean; text: string; x: number; y: number }>({
visible: false,
@ -54,6 +56,24 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
}
const hideTooltip = () => setTooltip(t => ({ ...t, visible: false }))
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
showToast({
variant: 'success',
title: 'Copied',
message: 'Link copied to Zwischenablage.',
duration: 2500,
})
} catch {
showToast({
variant: 'error',
title: 'Copy failed',
message: 'Could not copy link to clipboard.',
})
}
}
return (
<>
<div className="mt-8 bg-white rounded-lg shadow-sm border border-gray-200">
@ -147,15 +167,16 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
</a>
{/* Desktop/Tablet copy button */}
<button
onClick={async () => { try { await navigator.clipboard.writeText(l.url || String(l.code || '')); } catch {} }}
className="hidden md:inline-flex items-center gap-1 rounded border border-gray-300 px-2 py-1 text-xs text-gray-700 hover:bg-gray-50"
onClick={() => copyToClipboard(l.url || String(l.code || ''))}
className="hidden md:inline-flex items-center gap-1 rounded border border-gray-300 px-2 py-1 text-xs text-gray-700
transition-all duration-150 hover:bg-gray-50 hover:shadow-sm hover:-translate-y-0.5 active:translate-y-0"
>
<ClipboardDocumentIcon className="h-4 w-4" />
Copy
</button>
{/* Mobile: only copy button */}
<button
onClick={async () => { try { await navigator.clipboard.writeText(l.url || String(l.code || '')); } catch {} }}
onClick={() => copyToClipboard(l.url || String(l.code || ''))}
className="inline-flex md:hidden items-center gap-2 rounded border border-gray-300 px-3 py-2 text-xs text-gray-700 hover:bg-gray-50"
aria-label="Copy referral link"
>
@ -197,7 +218,13 @@ export default function ReferralLinksListWidget({ links, onDeactivate }: Props)
<button
disabled={l.status !== 'active'}
onClick={() => onDeactivate(l)}
className="inline-flex items-center rounded-md border border-red-300 px-3 py-1.5 text-sm text-red-700 hover:bg-red-50 disabled:opacity-50"
className="
inline-flex items-center rounded-md border border-red-300 px-3 py-1.5 text-sm text-red-700
transition-all duration-150
md:hover:-translate-y-0.5 md:hover:shadow-sm md:hover:bg-red-50
active:translate-y-0
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:shadow-none
"
>
Deactivate
</button>

View File

@ -12,8 +12,10 @@ import RegisteredUserList from './components/registeredUserList'
import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
import { useRegisteredUsers } from './hooks/registeredUsers'
import { ToastProvider, useToast } from '../components/toast/toastComponent' // NEW
export default function ReferralManagementPage() {
function ReferralManagementPageInner() {
const { showToast } = useToast() // NEW
const router = useRouter()
const user = useAuthStore(s => s.user)
const isAuthReady = useAuthStore(s => s.isAuthReady)
@ -48,11 +50,30 @@ export default function ReferralManagementPage() {
try {
const tokenId = selectedLink?.tokenId ?? selectedLink?.id
if (tokenId == null) return
const res = await deactivateReferralLink(tokenId)
console.log('✅ Deactivate result:', res)
if (res?.ok) {
showToast({
variant: 'success',
title: 'Link deactivated',
message: 'The referral link has been deactivated successfully.',
})
} else {
showToast({
variant: 'error',
title: 'Deactivate failed',
message: (res as any)?.body?.message || 'Could not deactivate the referral link.',
})
}
await loadData()
} catch (e) {
// optional: toast error
} catch (e: any) {
showToast({
variant: 'error',
title: 'Deactivate failed',
message: 'Network error while deactivating the referral link.',
})
} finally {
setDeactivatePending(false)
setDeactivateOpen(false)
@ -71,7 +92,6 @@ export default function ReferralManagementPage() {
const run = async () => {
if (!isAuthReady) return
if (!user) {
console.log('🔐 referral-management: no user, redirect to /login')
router.replace('/login')
return
}
@ -79,11 +99,15 @@ export default function ReferralManagementPage() {
// Resolve user id
const uid = (user as any)?.id ?? (user as any)?._id ?? (user as any)?.userId
if (!uid) {
console.warn('⚠️ referral-management: user id missing, denying access')
if (!cancelled) {
setHasReferralPerm(false)
setIsPermChecked(true)
}
showToast({
variant: 'error',
title: 'Access check failed',
message: 'User id is missing. Redirecting…',
})
router.replace('/dashboard')
return
}
@ -112,9 +136,7 @@ export default function ReferralManagementPage() {
...(tokenToUse ? { Authorization: `Bearer ${tokenToUse}` } : {})
}
})
console.log('📡 referral-management: permissions status:', res.status)
const body = await res.json().catch(() => null)
console.log('📦 referral-management: permissions body:', body)
const permsSrc = body?.data?.permissions ?? body?.permissions ?? body
let can = false
@ -133,22 +155,30 @@ export default function ReferralManagementPage() {
}
if (!can) {
console.log('⛔ referral-management: missing permission, redirect to /dashboard')
showToast({
variant: 'warning',
title: 'Access denied',
message: 'You do not have permission to access Referral Management.',
})
router.replace('/dashboard')
}
} catch (e) {
console.error('❌ referral-management: fetch permissions error:', e)
if (!cancelled) {
setHasReferralPerm(false)
setIsPermChecked(true)
}
showToast({
variant: 'error',
title: 'Permission check failed',
message: 'Could not verify permissions. Redirecting…',
})
router.replace('/dashboard')
}
}
run()
return () => { cancelled = true }
}, [isAuthReady, user, accessToken, refreshAuthToken, router])
}, [isAuthReady, user, accessToken, refreshAuthToken, router, showToast]) // CHANGED: add showToast
// Helper: normalize list payload shapes
const normalizeList = (raw: any): any[] => {
@ -187,8 +217,22 @@ export default function ReferralManagementPage() {
// Helper: fetch stats + list
const loadData = async () => {
const [statsRes, listRes] = await Promise.all([fetchReferralStats(), fetchReferralList()])
console.log('✅ Referral stats fetched:', statsRes)
console.log('✅ Referral list fetched:', listRes)
if (!statsRes.ok) {
showToast({
variant: 'error',
title: 'Load failed',
message: 'Could not load referral statistics.',
})
}
if (!listRes.ok) {
showToast({
variant: 'error',
title: 'Load failed',
message: 'Could not load referral links.',
})
}
if (statsRes.ok && statsRes.body) {
const b: any = statsRes.body?.data || statsRes.body?.stats || statsRes.body
setStats({
@ -259,8 +303,10 @@ export default function ReferralManagementPage() {
loading={usersLoading}
/>
{/* Generator */}
<GenerateReferralLinkWidget onCreated={loadData} />
{/* Generator (wrapped for desktop hover animation) */}
<div className="transition-all duration-200 md:hover:-translate-y-0.5 md:hover:shadow-md md:hover:shadow-black/5">
<GenerateReferralLinkWidget onCreated={loadData} />
</div>
{/* Referral links list (refactored) */}
<ReferralLinksListWidget links={links} onDeactivate={openDeactivateModal} />
@ -278,4 +324,12 @@ export default function ReferralManagementPage() {
/>
</PageLayout>
)
}
export default function ReferralManagementPage() {
return (
<ToastProvider>
<ReferralManagementPageInner />
</ToastProvider>
)
}

View File

@ -8,7 +8,8 @@ import PageLayout from '../components/PageLayout'
import SessionDetectedModal from './components/SessionDetectedModal'
import InvalidRefLinkModal from './components/invalidRefLinkModal'
import { ToastProvider, useToast } from '../components/toast/toastComponent'
import Waves from '../components/waves'
import Waves from '../components/background/waves'
import BlueBlurryBackground from '../components/background/blueblurry' // NEW
// NEW: inner component that actually uses useToast and all the logic
function RegisterPageInner() {
@ -161,26 +162,36 @@ function RegisterPageInner() {
opacity: 'var(--pp-page-shift-opacity, 1)',
}
const BackgroundShell = ({ children }: { children: React.ReactNode }) => {
return isMobile ? (
<BlueBlurryBackground className="min-h-screen w-full">{children}</BlueBlurryBackground>
) : (
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
/>
{children}
</div>
)
}
// --- Render branches (unchanged except classNames) ---
if (!isRefChecked) {
return (
<PageLayout>
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
/>
<BackgroundShell>
<main
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
style={mainStyle}
@ -203,7 +214,7 @@ function RegisterPageInner() {
</div>
</div>
</main>
</div>
</BackgroundShell>
</PageLayout>
)
}
@ -211,21 +222,7 @@ function RegisterPageInner() {
if (invalidRef) {
return (
<PageLayout>
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
/>
<BackgroundShell>
<main
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
style={mainStyle}
@ -253,7 +250,7 @@ function RegisterPageInner() {
</div>
</div>
</main>
</div>
</BackgroundShell>
</PageLayout>
)
}
@ -261,21 +258,7 @@ function RegisterPageInner() {
// normal register
return (
<PageLayout>
<div className="relative w-full flex flex-col min-h-screen overflow-hidden" style={{ backgroundImage: 'none', background: 'none' }}>
<Waves
className="pointer-events-none"
lineColor="#0f172a"
backgroundColor="rgba(245, 245, 240, 1)"
waveSpeedX={0.02}
waveSpeedY={0.01}
waveAmpX={40}
waveAmpY={20}
friction={0.9}
tension={0.01}
maxCursorMove={120}
xGap={12}
yGap={36}
/>
<BackgroundShell>
<main
className="relative z-10 flex flex-col flex-1 items-center justify-start sm:justify-center pb-10 sm:pb-20"
style={mainStyle}
@ -334,7 +317,7 @@ function RegisterPageInner() {
</div>
</div>
</main>
</div>
</BackgroundShell>
</PageLayout>
)
}