372 lines
15 KiB
TypeScript
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>
|
|
)
|
|
} |