profit-planet-frontend/src/app/register/page.tsx
2026-01-14 18:15:48 +01:00

332 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useEffect, useState, type CSSProperties } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
import RegisterForm from './components/RegisterForm'
import PageLayout from '../components/PageLayout'
import SessionDetectedModal from './components/SessionDetectedModal'
import InvalidRefLinkModal from './components/invalidRefLinkModal'
import { ToastProvider, useToast } from '../components/toast/toastComponent'
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() {
const searchParams = useSearchParams()
const refToken = searchParams.get('ref')
const [registered, setRegistered] = useState(false)
const [mode, setMode] = useState<'personal' | 'company'>('personal')
const router = useRouter()
const { showToast } = useToast()
// Auth state
const user = useAuthStore(state => state.user)
const logout = useAuthStore(state => state.logout)
// Session management
const [showSessionModal, setShowSessionModal] = useState(false)
const [sessionCleared, setSessionCleared] = useState(false)
// Referral validation state
const [isRefChecked, setIsRefChecked] = useState(false)
const [invalidRef, setInvalidRef] = useState(false)
const [refInfo, setRefInfo] = useState<{
referrerName?: string
referrerEmail?: string
isUnlimited?: boolean
usesRemaining?: number
} | null>(null)
// Redirect after registration
useEffect(() => {
if (registered) {
const t = setTimeout(() => router.push('/login'), 4000)
return () => clearTimeout(t)
}
}, [registered, router])
// Validate referral token
useEffect(() => {
let cancelled = false
const validateRef = async () => {
if (!refToken) {
if (!cancelled) {
setInvalidRef(true)
setIsRefChecked(true)
}
showToast({
variant: 'error',
title: 'Invitation error',
message: 'No invitation token found in the link.'
})
return
}
const base = process.env.NEXT_PUBLIC_API_BASE_URL || ''
const url = `${base}/api/referral/info/${encodeURIComponent(refToken)}`
try {
const res = await fetch(url, { method: 'GET', credentials: 'include' })
const body = await res.json().catch(() => null)
const success = !!body?.success
const isUnlimited = !!body?.isUnlimited
const usesRemaining =
typeof body?.usesRemaining === 'number' ? body.usesRemaining : 0
const isActive = success && (isUnlimited || usesRemaining > 0)
if (!cancelled) {
if (isActive) {
setRefInfo({
referrerName: body?.referrerName,
referrerEmail: body?.referrerEmail,
isUnlimited,
usesRemaining
})
setInvalidRef(false)
showToast({
variant: 'success',
title: 'Invitation verified',
message: 'Your invitation link is valid. You can register now.'
})
} else {
setInvalidRef(true)
showToast({
variant: 'error',
title: 'Invalid invitation',
message: 'This invitation link is invalid or no longer active.'
})
}
setIsRefChecked(true)
}
} catch {
if (!cancelled) {
setInvalidRef(true)
setIsRefChecked(true)
}
showToast({
variant: 'error',
title: 'Network error',
message: 'Could not validate the invitation link. Please try again.'
})
}
}
validateRef()
return () => {
cancelled = true
}
// showToast intentionally omitted to avoid effect re-run loops (provider value can change)
}, [refToken]) // note: showToast intentionally omitted to avoid effect re-run loops
// Detect existing logged-in session
useEffect(() => {
if (isRefChecked && !invalidRef && user && !sessionCleared) {
setShowSessionModal(true)
}
}, [isRefChecked, invalidRef, user, sessionCleared])
const handleLogout = async () => {
await logout()
setSessionCleared(true)
setShowSessionModal(false)
}
const handleCancel = () => {
setShowSessionModal(false)
router.push('/dashboard')
}
const [isMobile, setIsMobile] = useState(false)
useEffect(() => {
const mq = window.matchMedia('(max-width: 768px)')
const apply = () => setIsMobile(mq.matches)
apply()
mq.addEventListener?.('change', apply)
window.addEventListener('resize', apply, { passive: true })
return () => {
mq.removeEventListener?.('change', apply)
window.removeEventListener('resize', apply)
}
}, [])
const mainStyle: CSSProperties = {
paddingTop: isMobile
? 'calc(var(--pp-header-spacer, 0px) + clamp(1.25rem, 3.5vh, 2.25rem))'
: 'calc(var(--pp-header-spacer, 0px) + clamp(5rem, 8vh, 7rem))',
transition: 'padding-top 260ms ease, opacity 260ms ease',
willChange: 'padding-top, opacity',
opacity: 'var(--pp-page-shift-opacity, 1)',
}
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>
<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}
>
<div className="mx-auto max-w-7xl px-6 lg:px-10 flex flex-col w-full">
<div
className="mx-auto w-full max-w-md rounded-3xl shadow-2xl relative border border-white/35 overflow-hidden p-6 sm:p-8"
style={{
backgroundColor: 'rgba(255,255,255,0.55)',
backdropFilter: 'blur(18px)',
WebkitBackdropFilter: 'blur(18px)',
boxShadow: '0 18px 45px rgba(15,23,42,0.45)',
}}
>
<div className="absolute -top-24 -right-24 size-56 rounded-full bg-gradient-to-br from-indigo-500/10 to-fuchsia-500/10 blur-3xl pointer-events-none" />
<div className="relative text-center">
<div className="animate-spin rounded-full h-10 w-10 border-2 border-b-transparent border-[#8D6B1D] mx-auto mb-4" />
<p className="text-slate-700">Checking invitation link</p>
</div>
</div>
</div>
</main>
</BackgroundShell>
</PageLayout>
)
}
if (invalidRef) {
return (
<PageLayout>
<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}
>
<div className="mx-auto max-w-7xl px-6 lg:px-10 flex flex-col w-full">
<div
className="mx-auto w-full max-w-3xl min-h-[520px] sm:min-h-[560px] rounded-3xl shadow-2xl relative border border-white/35 overflow-hidden p-6 sm:p-10 md:py-12 md:px-14"
style={{
backgroundColor: 'rgba(255,255,255,0.55)', // Use a translucent white for glass effect
backdropFilter: 'blur(18px)', // Glass blur
WebkitBackdropFilter: 'blur(18px)',
boxShadow: '0 18px 45px rgba(15,23,42,0.45)',
}}
>
<div className="absolute -top-24 -right-24 size-56 rounded-full bg-gradient-to-br from-indigo-500/10 to-fuchsia-500/10 blur-3xl pointer-events-none" />
<div className="relative flex items-center justify-center">
<InvalidRefLinkModal
inline
open
token={refToken}
onGoHome={() => router.push('/')}
onClose={() => router.push('/')}
/>
</div>
</div>
</div>
</main>
</BackgroundShell>
</PageLayout>
)
}
// normal register
return (
<PageLayout>
<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}
>
<div className="mx-auto max-w-7xl px-6 lg:px-10 flex flex-col w-full">
<div
className="mx-auto w-full max-w-4xl min-h-[520px] sm:min-h-[560px] rounded-3xl shadow-2xl relative border border-white/35 overflow-hidden p-6 sm:p-10 md:py-12 md:px-14"
style={{
backgroundColor: 'rgba(255, 255, 255, 0.9)', // Use a translucent white for glass effect
backdropFilter: 'blur(40px)', // Glass blur
WebkitBackdropFilter: 'blur(40px)',
boxShadow: '0 18px 45px rgba(15,23,42,0.45)',
}}
>
<div className="absolute -top-24 -right-24 size-56 rounded-full bg-gradient-to-br from-indigo-500/10 to-fuchsia-500/10 blur-3xl pointer-events-none" />
<div className="relative">
<div className="mx-auto max-w-2xl text-center mb-8">
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-[#0F172A]">
Register now
</h1>
<p className="mt-3 text-slate-700 text-base sm:text-lg">
Create your personal or company account with Profit Planet.
</p>
</div>
<div className="mt-2">
{showSessionModal ? (
<div className="flex items-center justify-center">
<SessionDetectedModal
inline
open
onLogout={handleLogout}
onCancel={handleCancel}
/>
</div>
) : (
<>
{(!user || sessionCleared) && (
<RegisterForm
mode={mode}
setMode={setMode}
refToken={refToken}
onRegistered={() => setRegistered(true)}
referrerEmail={refInfo?.referrerEmail}
/>
)}
{registered && (
<div className="mt-6 mx-auto text-center text-sm text-gray-700">
Registration successful redirecting...
</div>
)}
</>
)}
</div>
</div>
</div>
</div>
</main>
</BackgroundShell>
</PageLayout>
)
}
// NEW: default export only provides the ToastProvider wrapper
export default function RegisterPage() {
return (
<ToastProvider>
<RegisterPageInner />
</ToastProvider>
)
}