diff --git a/package-lock.json b/package-lock.json
index f54968f..0402af3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -21,7 +21,7 @@
"clsx": "^2.1.1",
"country-flag-icons": "^1.5.21",
"country-select-js": "^2.1.0",
- "intl-tel-input": "^25.10.11",
+ "intl-tel-input": "^25.15.0",
"motion": "^12.23.22",
"next": "^16.0.7",
"pdfjs-dist": "^5.4.149",
@@ -6504,9 +6504,9 @@
}
},
"node_modules/intl-tel-input": {
- "version": "25.11.2",
- "resolved": "https://registry.npmjs.org/intl-tel-input/-/intl-tel-input-25.11.2.tgz",
- "integrity": "sha512-3a9+bbtR6s7E8TjZauqodMz+MRMd31OcUhTJuQOg95lA+viZ53OTU8XzVuyldEE089nMtLhPF1NbRU1ff2Sf7g==",
+ "version": "25.15.0",
+ "resolved": "https://registry.npmjs.org/intl-tel-input/-/intl-tel-input-25.15.0.tgz",
+ "integrity": "sha512-ux50qr6qCAWrd3BMioVN3te54h2+2pobvgq7vbKQ1GUP5P8XUcVuxKN/5bSxZuOYJqyqeCRqfcH7zlYErFZMtw==",
"license": "MIT"
},
"node_modules/is-array-buffer": {
diff --git a/package.json b/package.json
index 0135661..a876b2d 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,7 @@
"clsx": "^2.1.1",
"country-flag-icons": "^1.5.21",
"country-select-js": "^2.1.0",
- "intl-tel-input": "^25.10.11",
+ "intl-tel-input": "^25.15.0",
"motion": "^12.23.22",
"next": "^16.0.7",
"pdfjs-dist": "^5.4.149",
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index 343d6f9..5a6727a 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -15,6 +15,11 @@ import { useMemo, useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useAdminUsers } from '../hooks/useAdminUsers'
+// env-based feature flags
+const DISPLAY_MATRIX = process.env.NEXT_PUBLIC_DISPLAY_MATRIX !== 'false'
+const DISPLAY_ABONEMENTS = process.env.NEXT_PUBLIC_DISPLAY_ABONEMMENTS !== 'false'
+const DISPLAY_NEWS = process.env.NEXT_PUBLIC_DISPLAY_NEWS !== 'false'
+
export default function AdminDashboardPage() {
const router = useRouter()
const { userStats, isAdmin } = useAdminUsers()
@@ -147,45 +152,92 @@ export default function AdminDashboardPage() {
+ {/* Matrix Management */}
+
+ {/* Coffee Subscription Management */}
+
+ {/* Contract Management (unchanged) */}
);
}
\ No newline at end of file
diff --git a/src/app/components/nav/Header.tsx b/src/app/components/nav/Header.tsx
index 72dc6af..9d3467e 100644
--- a/src/app/components/nav/Header.tsx
+++ b/src/app/components/nav/Header.tsx
@@ -53,11 +53,14 @@ const navLinks = [
// Toggle visibility of Shop navigation across header (desktop + mobile)
const showShop = false
-export default function Header() {
+interface HeaderProps {
+ setGlobalLoggingOut?: (value: boolean) => void; // NEW
+}
+
+export default function Header({ setGlobalLoggingOut }: HeaderProps) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [mounted, setMounted] = useState(false)
const [animateIn, setAnimateIn] = useState(false)
- const [loggingOut, setLoggingOut] = useState(false) // NEW
const user = useAuthStore(s => s.user)
const logout = useAuthStore(s => s.logout)
const accessToken = useAuthStore(s => s.accessToken)
@@ -67,16 +70,18 @@ export default function Header() {
const [hasReferralPerm, setHasReferralPerm] = useState(false)
const [adminMgmtOpen, setAdminMgmtOpen] = useState(false)
const managementRef = useRef(null)
+ const [canSeeDashboard, setCanSeeDashboard] = useState(false)
const handleLogout = async () => {
try {
- setLoggingOut(true) // NEW: start logout transition
+ // start global logout transition
+ setGlobalLoggingOut?.(true)
await logout()
setMobileMenuOpen(false)
router.push('/login')
} catch (err) {
console.error('Logout failed:', err)
- setLoggingOut(false) // reset if something goes wrong
+ setGlobalLoggingOut?.(false)
}
};
@@ -189,11 +194,78 @@ export default function Header() {
return () => { cancelled = true }
}, [mounted, user, accessToken, refreshAuthToken])
+ // NEW: fetch onboarding status to decide if dashboard should be visible
+ useEffect(() => {
+ let cancelled = false
+
+ const fetchOnboardingStatus = async () => {
+ if (!mounted || !user) {
+ if (!cancelled) setCanSeeDashboard(false)
+ return
+ }
+
+ let tokenToUse = accessToken
+ try {
+ if (!tokenToUse && refreshAuthToken) {
+ const ok = await refreshAuthToken()
+ if (ok) tokenToUse = useAuthStore.getState().accessToken
+ }
+ } catch (e) {
+ console.error('❌ Header: refreshAuthToken (status-progress) error:', e)
+ }
+
+ if (!tokenToUse) {
+ if (!cancelled) setCanSeeDashboard(false)
+ return
+ }
+
+ try {
+ const statusUrl = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/user/status-progress`
+ console.log('🌐 Header: fetching status-progress:', statusUrl)
+
+ const res = await fetch(statusUrl, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${tokenToUse}`,
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ })
+
+ if (!res.ok) {
+ console.warn('⚠️ Header: status-progress failed with', res.status)
+ if (!cancelled) setCanSeeDashboard(false)
+ return
+ }
+
+ const statusData = await res.json().catch(() => null)
+ const progressData = statusData?.progress || statusData || {}
+ const steps = progressData.steps || []
+ const allStepsCompleted =
+ steps.length === 4 && steps.every((step: any) => step?.completed === true)
+ const isActive = progressData.status === 'active'
+
+ if (!cancelled) {
+ setCanSeeDashboard(allStepsCompleted && isActive)
+ }
+ } catch (e) {
+ console.error('❌ Header: status-progress fetch error:', e)
+ if (!cancelled) setCanSeeDashboard(false)
+ }
+ }
+
+ fetchOnboardingStatus()
+ return () => {
+ cancelled = true
+ }
+ }, [mounted, user, accessToken, refreshAuthToken])
+
const isLoggedIn = !!user
const userPresent = mounted && isLoggedIn
- // NEW: detect admin role across common shapes
+ // NEW: detect admin role across common shapes, but only after mount
const isAdmin =
+ mounted &&
!!user &&
(
(user as any)?.role === 'admin' ||
@@ -235,13 +307,32 @@ export default function Header() {
setMobileMenuOpen(true)}
+ onClick={() => setMobileMenuOpen(open => !open)}
aria-expanded={mobileMenuOpen}
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-gray-400 transition-transform duration-300 ease-out data-[open=true]:rotate-90"
data-open={mobileMenuOpen ? 'true' : 'false'}
>
- Open main menu
-
+
+ {mobileMenuOpen ? 'Close main menu' : 'Open main menu'}
+
+
+
+
+
@@ -317,13 +408,15 @@ export default function Header() {
{user?.email || 'user@example.com'}
- 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"
- >
-
- Dashboard
-
+ {canSeeDashboard && (
+ 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"
+ >
+
+ Dashboard
+
+ )}
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"
@@ -350,12 +443,31 @@ export default function Header() {
{/* Desktop hamburger (right side, next to login/profile) */}
setMobileMenuOpen(true)}
+ onClick={() => setMobileMenuOpen(open => !open)}
aria-expanded={mobileMenuOpen}
className="inline-flex items-center justify-center rounded-md p-2.5 text-gray-300 hover:text-white hover:bg-white/10 transition-colors"
>
- Open main menu
-
+
+ {mobileMenuOpen ? 'Close main menu' : 'Open main menu'}
+
+
+
+
+
@@ -486,24 +598,24 @@ export default function Header() {
{/* Side drawer menu: mobile + desktop */}
-
- {/* Logout transition overlay */}
- {loggingOut && (
-
- )}
)
}
\ No newline at end of file
diff --git a/src/app/components/phone/telephoneInput.tsx b/src/app/components/phone/telephoneInput.tsx
new file mode 100644
index 0000000..ea9b89e
--- /dev/null
+++ b/src/app/components/phone/telephoneInput.tsx
@@ -0,0 +1,160 @@
+'use client'
+
+import React, {
+ forwardRef,
+ useEffect,
+ useImperativeHandle,
+ useRef,
+ InputHTMLAttributes,
+} from 'react'
+import { createIntlTelInput, IntlTelInputInstance } from '../../utils/phoneUtils'
+
+export type TelephoneInputHandle = {
+ getNumber: () => string
+ isValid: () => boolean
+}
+
+interface TelephoneInputProps extends Omit, 'type'> {
+ /** e.g. "de" */
+ initialCountry?: string
+}
+
+/**
+ * Reusable telephone input with intl-tel-input.
+ * Always takes full available width.
+ */
+const TelephoneInput = forwardRef(
+ ({ initialCountry = (process.env.NEXT_PUBLIC_GEO_FALLBACK_COUNTRY || 'DE').toLowerCase(), ...rest }, ref) => {
+ const inputRef = useRef(null)
+ const itiRef = useRef(null)
+
+ useEffect(() => {
+ let disposed = false
+ let instance: IntlTelInputInstance | null = null
+
+ const setup = async () => {
+ try {
+ console.log('[TelephoneInput] setup() start for', {
+ id: rest.id,
+ name: rest.name,
+ initialCountry,
+ })
+
+ if (!inputRef.current) {
+ console.warn('[TelephoneInput] setup() aborted: inputRef is null', {
+ id: rest.id,
+ name: rest.name,
+ })
+ return
+ }
+
+ instance = await createIntlTelInput(inputRef.current, {
+ initialCountry,
+ nationalMode: true,
+ strictMode: true,
+ autoPlaceholder: 'aggressive',
+ validationNumberTypes: ['MOBILE'],
+ })
+
+ if (disposed) {
+ console.log('[TelephoneInput] setup() finished but component is disposed, destroying instance', {
+ id: rest.id,
+ name: rest.name,
+ })
+ instance.destroy()
+ return
+ }
+
+ itiRef.current = instance
+ console.log('[TelephoneInput] intl-tel-input instance attached to input', {
+ id: rest.id,
+ name: rest.name,
+ })
+ } catch (e) {
+ console.error('[TelephoneInput] Failed to init intl-tel-input:', e)
+ }
+ }
+
+ setup()
+
+ return () => {
+ disposed = true
+ if (instance) {
+ console.log('[TelephoneInput] Destroying intl-tel-input instance for', {
+ id: rest.id,
+ name: rest.name,
+ })
+ instance.destroy()
+ if (itiRef.current === instance) itiRef.current = null
+ }
+ }
+ }, [initialCountry, rest.id, rest.name])
+
+ useImperativeHandle(ref, () => ({
+ getNumber: () => {
+ const raw = inputRef.current?.value || ''
+ if (itiRef.current) {
+ const intl = itiRef.current.getNumber()
+ console.log('[TelephoneInput] getNumber()', {
+ id: rest.id,
+ name: rest.name,
+ raw,
+ intl,
+ })
+ return intl
+ }
+ console.warn(
+ '[TelephoneInput] getNumber() called before intl-tel-input ready, returning raw value',
+ { id: rest.id, name: rest.name, raw }
+ )
+ return raw
+ },
+ isValid: () => {
+ if (!itiRef.current) {
+ const raw = inputRef.current?.value || ''
+ console.warn('[TelephoneInput] isValid() called before intl-tel-input ready', {
+ id: rest.id,
+ name: rest.name,
+ raw,
+ })
+ return false
+ }
+ const instance = itiRef.current
+ const intl = instance.getNumber()
+ const valid = instance.isValidNumber()
+ const errorCode = typeof instance.getValidationError === 'function'
+ ? instance.getValidationError()
+ : undefined
+ const country = typeof instance.getSelectedCountryData === 'function'
+ ? instance.getSelectedCountryData()
+ : undefined
+
+ console.log('[TelephoneInput] isValid() check', {
+ id: rest.id,
+ name: rest.name,
+ intl,
+ valid,
+ errorCode,
+ country,
+ })
+
+ return valid
+ },
+ }))
+
+ return (
+
+
+
+ )
+ }
+)
+
+TelephoneInput.displayName = 'TelephoneInput'
+
+export default TelephoneInput
diff --git a/src/app/register/components/RegisterForm.tsx b/src/app/register/components/RegisterForm.tsx
index 2cb7adb..e21d74a 100644
--- a/src/app/register/components/RegisterForm.tsx
+++ b/src/app/register/components/RegisterForm.tsx
@@ -1,9 +1,10 @@
'use client'
-import { useState, useEffect } from 'react'
+import { useState, useEffect, useRef } from 'react'
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline'
import { useRegister } from '../hooks/useRegister'
import { useToast } from '../../components/toast/toastComponent'
+import TelephoneInput, { TelephoneInputHandle } from '../../components/phone/telephoneInput'
interface RegisterFormProps {
mode: 'personal' | 'company'
@@ -70,7 +71,12 @@ export default function RegisterForm({
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [formFade, setFormFade] = useState('fade-in')
-
+
+ // Phone input refs (to access intl-tel-input via TelephoneInput)
+ const personalPhoneRef = useRef(null)
+ const companyPhoneRef = useRef(null)
+ const contactPhoneRef = useRef(null)
+
// Hook for backend calls
const { registerPersonalReferral, registerCompanyReferral, error: regError } = useRegister()
const { showToast } = useToast()
@@ -114,8 +120,7 @@ export default function RegisterForm({
const validatePersonalForm = (): boolean => {
if (!personalForm.firstName.trim() || !personalForm.lastName.trim() ||
!personalForm.email.trim() || !personalForm.confirmEmail.trim() ||
- !personalForm.password.trim() || !personalForm.confirmPassword.trim() ||
- !personalForm.phoneNumber.trim()
+ !personalForm.password.trim() || !personalForm.confirmPassword.trim()
) {
setError('All fields are required')
return false
@@ -135,6 +140,25 @@ export default function RegisterForm({
setError('Password must be at least 8 characters long and contain uppercase and lowercase letters, numbers and special characters')
return false
}
+
+ const phoneApi = personalPhoneRef.current
+ const intlNumber = phoneApi?.getNumber() || ''
+ const valid = phoneApi?.isValid() ?? false
+
+ console.log('[RegisterForm] validatePersonalForm phone check', {
+ rawState: personalForm.phoneNumber,
+ intlFromApi: intlNumber,
+ isValidFromApi: valid,
+ })
+
+ if (!intlNumber) {
+ setError('Please enter your phone number including country code.')
+ return false
+ }
+ if (!valid) {
+ setError('Please enter a valid mobile phone number.')
+ return false
+ }
setError('')
return true
@@ -143,8 +167,7 @@ export default function RegisterForm({
const validateCompanyForm = (): boolean => {
if (!companyForm.companyName.trim() || !companyForm.companyEmail.trim() ||
!companyForm.confirmCompanyEmail.trim() || !companyForm.contactPersonName.trim() ||
- !companyForm.password.trim() || !companyForm.confirmPassword.trim() ||
- !companyForm.companyPhone.trim() || !companyForm.contactPersonPhone.trim()
+ !companyForm.password.trim() || !companyForm.confirmPassword.trim()
) {
setError('All fields are required')
return false
@@ -164,6 +187,32 @@ export default function RegisterForm({
setError('Password must be at least 8 characters long and contain uppercase and lowercase letters, numbers and special characters')
return false
}
+
+ const companyApi = companyPhoneRef.current
+ const contactApi = contactPhoneRef.current
+
+ const companyNumber = companyApi?.getNumber() || ''
+ const contactNumber = contactApi?.getNumber() || ''
+ const companyValid = companyApi?.isValid() ?? false
+ const contactValid = contactApi?.isValid() ?? false
+
+ console.log('[RegisterForm] validateCompanyForm phone check', {
+ rawCompany: companyForm.companyPhone,
+ rawContact: companyForm.contactPersonPhone,
+ intlCompany: companyNumber,
+ intlContact: contactNumber,
+ companyValid,
+ contactValid,
+ })
+
+ if (!companyNumber || !contactNumber) {
+ setError('Please enter both company and contact phone numbers including country codes.')
+ return false
+ }
+ if (!companyValid || !contactValid) {
+ setError('Please enter valid phone numbers for company and contact person.')
+ return false
+ }
setError('')
return true
@@ -180,13 +229,20 @@ export default function RegisterForm({
setError('')
try {
+ const normalizedPhone =
+ personalPhoneRef.current?.getNumber() || personalForm.phoneNumber
+
+ console.log('[RegisterForm] handlePersonalSubmit normalized phone', {
+ normalizedPhone,
+ })
+
const res = await registerPersonalReferral({
refToken: refToken || '',
firstName: personalForm.firstName,
lastName: personalForm.lastName,
email: personalForm.email,
password: personalForm.password,
- phone: personalForm.phoneNumber,
+ phone: normalizedPhone,
})
if (res.ok) {
showToast({
@@ -227,14 +283,24 @@ export default function RegisterForm({
setError('')
try {
+ const normalizedCompanyPhone =
+ companyPhoneRef.current?.getNumber() || companyForm.companyPhone
+ const normalizedContactPhone =
+ contactPhoneRef.current?.getNumber() || companyForm.contactPersonPhone
+
+ console.log('[RegisterForm] handleCompanySubmit normalized phones', {
+ normalizedCompanyPhone,
+ normalizedContactPhone,
+ })
+
const res = await registerCompanyReferral({
refToken: refToken || '',
companyEmail: companyForm.companyEmail,
password: companyForm.password,
companyName: companyForm.companyName,
- companyPhone: companyForm.companyPhone,
+ companyPhone: normalizedCompanyPhone,
contactPersonName: companyForm.contactPersonName,
- contactPersonPhone: companyForm.contactPersonPhone,
+ contactPersonPhone: normalizedContactPhone,
})
if (res.ok) {
showToast({
@@ -314,7 +380,10 @@ export default function RegisterForm({
Password requirements:
{rules.map((rule, index) => (
- -
+
-
{rule.test ? '✓' : '○'}
{rule.text}
@@ -331,7 +400,6 @@ export default function RegisterForm({
Registration for Profit Planet
- {/* Replace generic invite with referrer email inside the form */}
{referrerEmail && (
You were invited by {referrerEmail}!
@@ -446,15 +514,15 @@ export default function RegisterForm({
-
+ setPersonalForm(prev => ({ ...prev, phoneNumber: (e.target as HTMLInputElement).value }))
+ }
/>
@@ -594,31 +662,34 @@ export default function RegisterForm({
-
+ setCompanyForm(prev => ({ ...prev, companyPhone: (e.target as HTMLInputElement).value }))
+ }
/>
-
+
-
+ setCompanyForm(prev => ({
+ ...prev,
+ contactPersonPhone: (e.target as HTMLInputElement).value,
+ }))
+ }
/>
@@ -705,4 +776,4 @@ export default function RegisterForm({
)
-}
\ No newline at end of file
+}
diff --git a/src/app/utils/phoneUtils.ts b/src/app/utils/phoneUtils.ts
new file mode 100644
index 0000000..7342419
--- /dev/null
+++ b/src/app/utils/phoneUtils.ts
@@ -0,0 +1,179 @@
+'use client'
+
+/**
+ * Shared intl-tel-input utilities for the frontend (CDN-based).
+ * Handles:
+ * - CSS injection
+ * - JS script loading
+ * - utils.js loading via loadUtils
+ * - instance creation
+ */
+
+const ITI_CDN_CSS =
+ 'https://cdn.jsdelivr.net/npm/intl-tel-input@25.15.0/build/css/intlTelInput.css'
+const ITI_CDN_JS =
+ 'https://cdn.jsdelivr.net/npm/intl-tel-input@25.15.0/build/js/intlTelInput.min.js'
+const ITI_CDN_UTILS =
+ 'https://cdn.jsdelivr.net/npm/intl-tel-input@25.15.0/build/js/utils.js'
+
+export type IntlTelInputInstance = {
+ destroy: () => void
+ getNumber: () => string
+ isValidNumber: () => boolean
+ getValidationError?: () => number
+ getSelectedCountryData?: () => { name: string; iso2: string; dialCode: string }
+ promise?: Promise
+}
+
+declare global {
+ interface Window {
+ intlTelInput?: (input: HTMLInputElement, options: any) => IntlTelInputInstance
+ }
+}
+
+let intlLoaderPromise: Promise<(input: HTMLInputElement, options: any) => IntlTelInputInstance> | null =
+ null
+
+async function loadIntlTelInputFromCdn(): Promise<
+ (input: HTMLInputElement, options: any) => IntlTelInputInstance
+> {
+ if (typeof window === 'undefined') {
+ throw new Error('[phoneUtils] intl-tel-input can only be used in the browser')
+ }
+
+ // CSS once
+ if (!document.querySelector('link[data-intl-tel-input-css="true"]')) {
+ console.log('[phoneUtils] Injecting intl-tel-input CSS from CDN')
+ const link = document.createElement('link')
+ link.rel = 'stylesheet'
+ link.href = ITI_CDN_CSS
+ link.dataset.intlTelInputCss = 'true'
+ document.head.appendChild(link)
+ } else {
+ console.log('[phoneUtils] intl-tel-input CSS already present')
+ }
+
+ // Helper CSS to make .iti full-width
+ if (!document.querySelector('style[data-intl-tel-input-width="true"]')) {
+ console.log('[phoneUtils] Injecting full-width .iti CSS helper')
+ const style = document.createElement('style')
+ style.dataset.intlTelInputWidth = 'true'
+ style.innerHTML = `
+ .iti {
+ display: block;
+ width: 100%;
+ }
+ .iti input {
+ width: 100%;
+ }
+ `
+ document.head.appendChild(style)
+ }
+
+ // JS once
+ if (window.intlTelInput) {
+ console.log('[phoneUtils] intl-tel-input already loaded on window')
+ return window.intlTelInput
+ }
+
+ console.log('[phoneUtils] Loading intl-tel-input core (no utils) from CDN…')
+ await new Promise((resolve, reject) => {
+ const existing = document.querySelector(
+ 'script[data-intl-tel-input-js="true"]'
+ )
+ if (existing) {
+ console.log('[phoneUtils] Reusing existing intl-tel-input