profit-planet-frontend/src/app/dashboard/page.tsx
DeathKaioken 7559466c27 i18
Co-authored-by: Copilot <copilot@github.com>
2026-05-02 21:00:08 +02:00

372 lines
15 KiB
TypeScript

'use client'
import { useEffect, useState, useCallback, useRef } from 'react'
import type { ComponentType, SVGProps } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import useAuthStore from '../store/authStore'
import PageLayout from '../components/PageLayout'
import Waves from '../components/background/waves'
import BlueBlurryBackground from '../components/background/blueblurry'
import { useTranslation } from '../i18n/useTranslation'
import { useUserStatus } from '../hooks/useUserStatus'
import {
ShoppingBagIcon,
UsersIcon,
UserCircleIcon,
StarIcon,
LinkIcon
} from '@heroicons/react/24/outline'
import {
DEFAULT_DASHBOARD_PLATFORMS,
loadDashboardPlatforms,
subscribeDashboardPlatformsUpdated,
type DashboardPlatform,
type DashboardPlatformIconName
} from '../utils/dashboardPlatforms'
export default function DashboardPage() {
const router = useRouter()
const { t } = useTranslation()
const user = useAuthStore(state => state.user)
const isAuthReady = useAuthStore(state => state.isAuthReady)
const isShopEnabled = process.env.NEXT_PUBLIC_SHOW_SHOP !== 'false'
const [platforms, setPlatforms] = useState<DashboardPlatform[]>(DEFAULT_DASHBOARD_PLATFORMS)
const [isMobile, setIsMobile] = useState(false)
const [latestNews, setLatestNews] = useState<Array<{ id: number; title: string; summary?: string; slug: string; published_at?: string | null }>>([])
const [newsLoading, setNewsLoading] = useState(false)
const [newsError, setNewsError] = useState<string | null>(null)
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)
apply()
mq.addEventListener?.('change', apply)
window.addEventListener('resize', apply, { passive: true })
return () => {
mq.removeEventListener?.('change', apply)
window.removeEventListener('resize', apply)
}
}, [])
useEffect(() => {
setPlatforms(loadDashboardPlatforms())
return subscribeDashboardPlatformsUpdated(() => setPlatforms(loadDashboardPlatforms()))
}, [])
useEffect(() => {
let active = true
;(async () => {
setNewsLoading(true)
setNewsError(null)
try {
const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'
const res = await fetch(`${BASE_URL}/api/news/active`)
if (!res.ok) throw new Error('Failed to fetch news')
const json = await res.json()
const data = Array.isArray(json.data) ? json.data : []
if (active) setLatestNews(data.slice(0, 3))
} catch (e: any) {
if (active) setNewsError(e?.message || 'Failed to load news')
} finally {
if (active) setNewsLoading(false)
}
})()
return () => { active = false }
}, [])
// Redirect if not logged in (only after auth is ready)
useEffect(() => {
if (isAuthReady && !user) {
router.push('/login')
}
}, [isAuthReady, user, router])
// NEW: block dashboard unless all quickaction steps are completed
// For guest users: only email verification is required
// For regular users: all 4 steps must be completed
useEffect(() => {
if (!isAuthReady || !user) return
if (statusLoading || !userStatus) return
const isGuest = user?.role === 'guest'
const allDone = isGuest
? !!userStatus.email_verified
: !!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">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
<p className="text-[#4A4A4A]">{t('dashboard.loading')}</p>
</div>
</div>
)
}
// 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">{t('dashboard.redirecting')}</div>
<div className="mt-1 text-xs text-gray-600">{t('dashboard.pleaseWait')}</div>
</div>
</div>
)
}
// Final guard (don't render dashboard if not all done)
if (!userStatus) return null
const isGuestUser = user?.role === 'guest'
const allDone = isGuestUser
? !!userStatus.email_verified
: !!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) {
return `${user.firstName} ${user.lastName}`
}
if (user.firstName) return user.firstName
if (user.email) return user.email.split('@')[0]
return 'User'
}
const icons: Record<DashboardPlatformIconName, ComponentType<SVGProps<SVGSVGElement>>> = {
ShoppingBagIcon,
LinkIcon,
UsersIcon,
UserCircleIcon
}
const getTranslatedOrFallback = (key: string, fallback: string) => {
const translated = t(key)
return translated === key ? fallback : translated
}
const platformTitleKeyById: Record<string, string> = {
'shop': 'dashboard.platformCards.shop.title',
'affiliate-links': 'dashboard.platformCards.affiliateLinks.title',
'referral-management': 'dashboard.platformCards.referralManagement.title',
'profile': 'dashboard.platformCards.profile.title',
}
const platformDescriptionKeyById: Record<string, string> = {
'shop': 'dashboard.platformCards.shop.description',
'affiliate-links': 'dashboard.platformCards.affiliateLinks.description',
'referral-management': 'dashboard.platformCards.referralManagement.description',
'profile': 'dashboard.platformCards.profile.description',
}
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">
{t('dashboard.welcomeBack')}, {getUserName()}! 👋
</h1>
<p className="text-gray-600 mt-2">
{t('dashboard.welcomeSubtitle')}
</p>
</div>
{/* Quick Actions */}
<div className="mb-8">
<h2 className="text-xl font-semibold text-gray-900 mb-4">{t('dashboard.platforms')}</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{platforms.filter(p => p.isActive).map((platform) => {
const Icon = icons[platform.icon]
const disabledByEnv = platform.href === '/shop' && !isShopEnabled
const isDisabled = Boolean(platform.disabled) || disabledByEnv
const translatedTitle = platformTitleKeyById[platform.id]
? getTranslatedOrFallback(platformTitleKeyById[platform.id], platform.title)
: platform.title
const translatedDescription = platformDescriptionKeyById[platform.id]
? getTranslatedOrFallback(platformDescriptionKeyById[platform.id], platform.description)
: platform.description
const disabledText = disabledByEnv
? t('dashboard.platformDisabled')
: platform.disabledText
return (
<button
key={platform.id}
onClick={() => {
if (!isDisabled) {
router.push(platform.href)
}
}}
disabled={isDisabled}
className={`bg-white rounded-lg p-6 border border-gray-200 text-left group transition-all duration-200 ${
isDisabled
? '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={`${platform.color} rounded-lg p-3 ${
isDisabled
? 'grayscale'
: 'group-hover:scale-105 transition-transform'
}`}
>
<Icon className="h-6 w-6 text-white" />
</div>
<div className="ml-4 flex-1">
<h3
className={`text-lg font-medium transition-colors ${
isDisabled
? 'text-gray-500'
: 'text-gray-900 group-hover:text-[#8D6B1D]'
}`}
>
{translatedTitle}
</h3>
<p className="text-sm text-gray-600 mt-1">
{translatedDescription}
</p>
{isDisabled && disabledText && (
<p className="mt-3 text-xs font-medium text-amber-700">
{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">{t('dashboard.goldMemberTitle')}</h2>
<p className="text-yellow-100 mt-1">
{t('dashboard.goldMemberDescription')}
</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">
{t('dashboard.viewBenefits')}
</button>
</div>
</div>
</div>
{/* Latest News */}
<div className="rounded-2xl bg-white border border-gray-200 shadow-sm p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">{t('dashboard.latestNews')}</h2>
<Link href="/news" className="text-sm font-medium text-blue-900 hover:text-blue-700">
{t('dashboard.viewAllNews')}
</Link>
</div>
{newsLoading && (
<div className="space-y-3">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="animate-pulse space-y-2">
<div className="h-4 w-2/3 bg-gray-200 rounded" />
<div className="h-3 w-1/2 bg-gray-100 rounded" />
</div>
))}
</div>
)}
{newsError && !newsLoading && (
<div className="text-sm text-red-600">{newsError}</div>
)}
{!newsLoading && !newsError && latestNews.length === 0 && (
<div className="text-sm text-gray-600">{t('dashboard.noNewsYet')}</div>
)}
{!newsLoading && !newsError && latestNews.length > 0 && (
<ul className="space-y-4">
{latestNews.map(item => (
<li key={item.id} className="group">
<Link href={`/news/${item.slug}`} className="block">
<div className="text-xs text-gray-500">
{item.published_at ? new Date(item.published_at).toLocaleDateString('de-DE') : t('dashboard.recent')}
</div>
<div className="text-sm font-semibold text-gray-900 group-hover:text-blue-700 line-clamp-2">
{item.title}
</div>
{item.summary && (
<div className="text-xs text-gray-600 line-clamp-2 mt-1">
{item.summary}
</div>
)}
</Link>
</li>
))}
</ul>
)}
</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' }}
>
<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
interactive
/>
{content}
</div>
)
}