feat: add company settings management and guest registration functionality
This commit is contained in:
parent
de290cd9ef
commit
48c63a896f
@ -0,0 +1,140 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import useContractManagement from '../hooks/useContractManagement'
|
||||||
|
|
||||||
|
export default function CompanySettingsPanel() {
|
||||||
|
const { getCompanySettings, updateCompanySettings } = useContractManagement()
|
||||||
|
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
company_name: '',
|
||||||
|
company_street: '',
|
||||||
|
company_postal_city: '',
|
||||||
|
company_country: '',
|
||||||
|
})
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [saved, setSaved] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCompanySettings()
|
||||||
|
.then(data => {
|
||||||
|
setForm({
|
||||||
|
company_name: data.company_name || '',
|
||||||
|
company_street: data.company_street || '',
|
||||||
|
company_postal_city: data.company_postal_city || '',
|
||||||
|
company_country: data.company_country || '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => setLoading(false))
|
||||||
|
}, [getCompanySettings])
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target
|
||||||
|
setForm(prev => ({ ...prev, [name]: value }))
|
||||||
|
setSaved(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setSaving(true)
|
||||||
|
setSaved(false)
|
||||||
|
try {
|
||||||
|
await updateCompanySettings(form)
|
||||||
|
setSaved(true)
|
||||||
|
setTimeout(() => setSaved(false), 3000)
|
||||||
|
} catch {
|
||||||
|
// silent
|
||||||
|
} finally {
|
||||||
|
setSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-500 py-4">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-900" />
|
||||||
|
Loading settings…
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="company_name" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Company Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="company_name"
|
||||||
|
name="company_name"
|
||||||
|
value={form.company_name}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="ProfitPlanet GmbH"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="company_street" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Street
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="company_street"
|
||||||
|
name="company_street"
|
||||||
|
value={form.company_street}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="Musterstraße 1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="company_postal_city" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Postal Code & City
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="company_postal_city"
|
||||||
|
name="company_postal_city"
|
||||||
|
value={form.company_postal_city}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="12345 Berlin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="company_country" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Country
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="company_country"
|
||||||
|
name="company_country"
|
||||||
|
value={form.company_country}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
placeholder="Germany"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
className={`px-5 py-2 rounded-lg text-sm font-semibold text-white transition-colors ${
|
||||||
|
saving ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-900 hover:bg-blue-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{saving ? 'Saving…' : 'Save'}
|
||||||
|
</button>
|
||||||
|
{saved && (
|
||||||
|
<span className="text-sm text-green-600 font-medium">Saved successfully</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -444,6 +444,21 @@ export default function useContractManagement() {
|
|||||||
return Array.isArray(data) ? data : [];
|
return Array.isArray(data) ? data : [];
|
||||||
}, [authorizedFetch]);
|
}, [authorizedFetch]);
|
||||||
|
|
||||||
|
// Company settings (invoice address info)
|
||||||
|
const getCompanySettings = useCallback(async () => {
|
||||||
|
return authorizedFetch<{ company_name: string; company_street: string; company_postal_city: string; company_country: string }>(
|
||||||
|
'/api/admin/company-settings', { method: 'GET' }
|
||||||
|
);
|
||||||
|
}, [authorizedFetch]);
|
||||||
|
|
||||||
|
const updateCompanySettings = useCallback(async (data: {
|
||||||
|
company_name: string; company_street: string; company_postal_city: string; company_country: string;
|
||||||
|
}) => {
|
||||||
|
return authorizedFetch<{ company_name: string; company_street: string; company_postal_city: string; company_country: string }>(
|
||||||
|
'/api/admin/company-settings', { method: 'PUT', body: JSON.stringify(data) }
|
||||||
|
);
|
||||||
|
}, [authorizedFetch]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// templates
|
// templates
|
||||||
listTemplates,
|
listTemplates,
|
||||||
@ -466,6 +481,9 @@ export default function useContractManagement() {
|
|||||||
activateCompanyStamp,
|
activateCompanyStamp,
|
||||||
deactivateCompanyStamp,
|
deactivateCompanyStamp,
|
||||||
deleteCompanyStamp,
|
deleteCompanyStamp,
|
||||||
|
// company settings
|
||||||
|
getCompanySettings,
|
||||||
|
updateCompanySettings,
|
||||||
// utils
|
// utils
|
||||||
downloadBlobFile,
|
downloadBlobFile,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import PageLayout from '../../components/PageLayout';
|
import PageLayout from '../../components/PageLayout';
|
||||||
import ContractEditor from './components/contractEditor';
|
import ContractEditor from './components/contractEditor';
|
||||||
import ContractUploadCompanyStamp from './components/contractUploadCompanyStamp';
|
import ContractUploadCompanyStamp from './components/contractUploadCompanyStamp';
|
||||||
|
import CompanySettingsPanel from './components/companySettingsPanel';
|
||||||
import ContractTemplateList from './components/contractTemplateList';
|
import ContractTemplateList from './components/contractTemplateList';
|
||||||
import useAuthStore from '../../store/authStore';
|
import useAuthStore from '../../store/authStore';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@ -98,6 +99,15 @@ export default function ContractManagementPage() {
|
|||||||
Company Stamp
|
Company Stamp
|
||||||
</h2>
|
</h2>
|
||||||
<ContractUploadCompanyStamp onUploaded={bumpRefresh} />
|
<ContractUploadCompanyStamp onUploaded={bumpRefresh} />
|
||||||
|
|
||||||
|
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||||
|
<h3 className="text-lg font-semibold text-blue-900 mb-3 flex items-center gap-2">
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>
|
||||||
|
Company Information
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-500 mb-4">Address details used on invoices.</p>
|
||||||
|
<CompanySettingsPanel />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
{section === 'templates' && (
|
{section === 'templates' && (
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import { useToast } from '../../components/toast/toastComponent'
|
|||||||
import TelephoneInput, { TelephoneInputHandle } from '../../components/phone/telephoneInput'
|
import TelephoneInput, { TelephoneInputHandle } from '../../components/phone/telephoneInput'
|
||||||
|
|
||||||
interface RegisterFormProps {
|
interface RegisterFormProps {
|
||||||
mode: 'personal' | 'company'
|
mode: 'personal' | 'company' | 'guest'
|
||||||
setMode: (mode: 'personal' | 'company') => void
|
setMode: (mode: 'personal' | 'company' | 'guest') => void
|
||||||
refToken: string | null
|
refToken: string | null
|
||||||
onRegistered: () => void
|
onRegistered: () => void
|
||||||
referrerEmail?: string
|
referrerEmail?: string
|
||||||
@ -78,9 +78,19 @@ export default function RegisterForm({
|
|||||||
const contactPhoneRef = useRef<TelephoneInputHandle | null>(null)
|
const contactPhoneRef = useRef<TelephoneInputHandle | null>(null)
|
||||||
|
|
||||||
// Hook for backend calls
|
// Hook for backend calls
|
||||||
const { registerPersonalReferral, registerCompanyReferral, error: regError } = useRegister()
|
const { registerPersonalReferral, registerCompanyReferral, registerGuest, error: regError } = useRegister()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
|
// Guest form state
|
||||||
|
const [guestForm, setGuestForm] = useState({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
confirmEmail: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
})
|
||||||
|
|
||||||
// Animate form when mode changes
|
// Animate form when mode changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFormFade('fade-out')
|
setFormFade('fade-out')
|
||||||
@ -351,6 +361,71 @@ export default function RegisterForm({
|
|||||||
setCompanyForm(prev => ({ ...prev, [name]: value }))
|
setCompanyForm(prev => ({ ...prev, [name]: value }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGuestChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target
|
||||||
|
setGuestForm(prev => ({ ...prev, [name]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateGuestForm = (): boolean => {
|
||||||
|
if (!guestForm.firstName.trim() || !guestForm.lastName.trim() ||
|
||||||
|
!guestForm.email.trim() || !guestForm.confirmEmail.trim() ||
|
||||||
|
!guestForm.password.trim() || !guestForm.confirmPassword.trim()
|
||||||
|
) {
|
||||||
|
setError('All fields are required')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (guestForm.email !== guestForm.confirmEmail) {
|
||||||
|
setError('Email addresses do not match')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (guestForm.password !== guestForm.confirmPassword) {
|
||||||
|
setError('Passwords do not match')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/.test(guestForm.password)) {
|
||||||
|
setError('Password must be at least 8 characters long and contain uppercase and lowercase letters, numbers and special characters')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
setError('')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleGuestSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (loading) return
|
||||||
|
if (!validateGuestForm()) return
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
const res = await registerGuest({
|
||||||
|
refToken: refToken || '',
|
||||||
|
firstName: guestForm.firstName,
|
||||||
|
lastName: guestForm.lastName,
|
||||||
|
email: guestForm.email,
|
||||||
|
password: guestForm.password,
|
||||||
|
})
|
||||||
|
if (res.ok) {
|
||||||
|
showToast({
|
||||||
|
variant: 'success',
|
||||||
|
title: 'Registration successful',
|
||||||
|
message: 'You can now log in to view your coffee abonnement.'
|
||||||
|
})
|
||||||
|
onRegistered()
|
||||||
|
} else {
|
||||||
|
const msg = res.message || 'Registration failed. Please try again.'
|
||||||
|
setError(msg)
|
||||||
|
showToast({ variant: 'error', title: 'Registration failed', message: msg })
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
const msg = 'Registration failed. Please try again.'
|
||||||
|
setError(msg)
|
||||||
|
showToast({ variant: 'error', title: 'Registration failed', message: msg })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Password strength indicator
|
// Password strength indicator
|
||||||
const getPasswordStrength = (password: string) => {
|
const getPasswordStrength = (password: string) => {
|
||||||
let strength = 0
|
let strength = 0
|
||||||
@ -408,6 +483,15 @@ export default function RegisterForm({
|
|||||||
{/* Mode Toggle */}
|
{/* Mode Toggle */}
|
||||||
<div className="flex justify-center mb-8">
|
<div className="flex justify-center mb-8">
|
||||||
<div className="bg-white/40 backdrop-blur-[18px] border border-white/35 shadow-sm p-1 rounded-lg">
|
<div className="bg-white/40 backdrop-blur-[18px] border border-white/35 shadow-sm p-1 rounded-lg">
|
||||||
|
{mode === 'guest' ? (
|
||||||
|
<button
|
||||||
|
className="px-6 py-2 rounded-md font-semibold text-sm bg-[#8D6B1D] text-white shadow-sm cursor-default"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Guest
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
className={`px-6 py-2 rounded-md font-semibold text-sm transition-all duration-200 ${
|
className={`px-6 py-2 rounded-md font-semibold text-sm transition-all duration-200 ${
|
||||||
mode === 'personal'
|
mode === 'personal'
|
||||||
@ -430,6 +514,8 @@ export default function RegisterForm({
|
|||||||
>
|
>
|
||||||
Company
|
Company
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -592,7 +678,7 @@ export default function RegisterForm({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : mode === 'company' ? (
|
||||||
<form onSubmit={handleCompanySubmit} className="space-y-6">
|
<form onSubmit={handleCompanySubmit} className="space-y-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
@ -764,6 +850,140 @@ export default function RegisterForm({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleGuestSubmit} className="space-y-6">
|
||||||
|
<div className="p-4 bg-amber-50/70 backdrop-blur-[18px] border border-amber-200/70 rounded-lg mb-2">
|
||||||
|
<p className="text-amber-800 text-sm font-medium">
|
||||||
|
You are registering as a guest. You will have access to your coffee abonnements only.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="guestFirstName" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||||
|
First name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="guestFirstName"
|
||||||
|
name="firstName"
|
||||||
|
value={guestForm.firstName}
|
||||||
|
onChange={handleGuestChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="guestLastName" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||||
|
Last name *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="guestLastName"
|
||||||
|
name="lastName"
|
||||||
|
value={guestForm.lastName}
|
||||||
|
onChange={handleGuestChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="guestEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||||
|
Email *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="guestEmail"
|
||||||
|
name="email"
|
||||||
|
value={guestForm.email}
|
||||||
|
onChange={handleGuestChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="guestConfirmEmail" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||||
|
Confirm email *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="guestConfirmEmail"
|
||||||
|
name="confirmEmail"
|
||||||
|
value={guestForm.confirmEmail}
|
||||||
|
onChange={handleGuestChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="guestPassword" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||||
|
Password *
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type={showPersonalPassword ? 'text' : 'password'}
|
||||||
|
id="guestPassword"
|
||||||
|
name="password"
|
||||||
|
value={guestForm.password}
|
||||||
|
onChange={handleGuestChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary pr-10"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPersonalPassword(!showPersonalPassword)}
|
||||||
|
className="absolute inset-y-0 right-0 px-3 flex items-center text-slate-500 hover:text-[#8D6B1D]"
|
||||||
|
>
|
||||||
|
{showPersonalPassword ? (
|
||||||
|
<EyeSlashIcon className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<EyeIcon className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{guestForm.password && renderPasswordStrength(guestForm.password)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="guestConfirmPassword" className="block text-sm font-medium text-[#0F172A] mb-2">
|
||||||
|
Confirm password *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="guestConfirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
value={guestForm.confirmPassword}
|
||||||
|
onChange={handleGuestChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-white/60 focus:ring-2 focus:ring-[#8D6B1D] focus:border-transparent transition-colors text-primary"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className={`w-full flex items-center justify-center py-3 px-4 rounded-lg text-white font-semibold transition-colors ${
|
||||||
|
loading
|
||||||
|
? 'bg-gray-400 cursor-not-allowed'
|
||||||
|
: 'bg-[#8D6B1D] hover:bg-[#7A5E1A] focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
Registration in progress...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Register as Guest'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,15 @@ export type CompanyReferralPayload = {
|
|||||||
lang?: 'de' | 'en'
|
lang?: 'de' | 'en'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GuestReferralPayload = {
|
||||||
|
refToken: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
lang?: 'de' | 'en'
|
||||||
|
}
|
||||||
|
|
||||||
type RegisterResult<T = any> = {
|
type RegisterResult<T = any> = {
|
||||||
ok: boolean
|
ok: boolean
|
||||||
status: number
|
status: number
|
||||||
@ -144,11 +153,69 @@ export function useRegister() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registerGuest = async (payload: GuestReferralPayload): Promise<RegisterResult> => {
|
||||||
|
setError('')
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
firstName: payload.firstName,
|
||||||
|
lastName: payload.lastName,
|
||||||
|
email: payload.email,
|
||||||
|
confirmEmail: payload.email,
|
||||||
|
password: payload.password,
|
||||||
|
confirmPassword: payload.password,
|
||||||
|
referralEmail: undefined as string | undefined,
|
||||||
|
lang: payload.lang || detectLang(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to resolve referral email from token
|
||||||
|
if (payload.refToken) {
|
||||||
|
try {
|
||||||
|
const infoRes = await fetch(`${base}/api/referral/info/${encodeURIComponent(payload.refToken)}`, {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
const infoJson = await infoRes.json().catch(() => null)
|
||||||
|
if (infoJson?.referrerEmail) {
|
||||||
|
body.referralEmail = infoJson.referrerEmail
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${base}/api/register/guest`
|
||||||
|
console.log('🌐 useRegister: POST guest', { url })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
const json = await res.json().catch(() => null)
|
||||||
|
console.log('📡 useRegister: guest status:', res.status, 'body:', json)
|
||||||
|
if (!res.ok) {
|
||||||
|
const msg = json?.message || mapError(res.status)
|
||||||
|
setError(msg)
|
||||||
|
return { ok: false, status: res.status, data: json, message: msg }
|
||||||
|
}
|
||||||
|
return { ok: true, status: res.status, data: json, message: json?.message }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ useRegister: guest error:', e)
|
||||||
|
const msg = 'Network error. Please try again later.'
|
||||||
|
setError(msg)
|
||||||
|
return { ok: false, status: 0, data: null, message: msg }
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
setError,
|
setError,
|
||||||
registerPersonalReferral,
|
registerPersonalReferral,
|
||||||
registerCompanyReferral,
|
registerCompanyReferral,
|
||||||
|
registerGuest,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,8 +15,9 @@ import BlueBlurryBackground from '../components/background/blueblurry' // NEW
|
|||||||
function RegisterPageInner() {
|
function RegisterPageInner() {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const refToken = searchParams.get('ref')
|
const refToken = searchParams.get('ref')
|
||||||
|
const isGuestInvite = searchParams.get('guest') === 'true'
|
||||||
const [registered, setRegistered] = useState(false)
|
const [registered, setRegistered] = useState(false)
|
||||||
const [mode, setMode] = useState<'personal' | 'company'>('personal')
|
const [mode, setMode] = useState<'personal' | 'company' | 'guest'>(isGuestInvite ? 'guest' : 'personal')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
|
|
||||||
@ -71,6 +72,8 @@ function RegisterPageInner() {
|
|||||||
const res = await fetch(url, { method: 'GET', credentials: 'include' })
|
const res = await fetch(url, { method: 'GET', credentials: 'include' })
|
||||||
const body = await res.json().catch(() => null)
|
const body = await res.json().catch(() => null)
|
||||||
|
|
||||||
|
console.log('[REGISTER] Token validation response:', { status: res.status, body })
|
||||||
|
|
||||||
const success = !!body?.success
|
const success = !!body?.success
|
||||||
const isUnlimited = !!body?.isUnlimited
|
const isUnlimited = !!body?.isUnlimited
|
||||||
const usesRemaining =
|
const usesRemaining =
|
||||||
@ -92,16 +95,18 @@ function RegisterPageInner() {
|
|||||||
message: 'Your invitation link is valid. You can register now.'
|
message: 'Your invitation link is valid. You can register now.'
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
const reason = body?.reason || `HTTP ${res.status}`
|
||||||
setInvalidRef(true)
|
setInvalidRef(true)
|
||||||
showToast({
|
showToast({
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
title: 'Invalid invitation',
|
title: 'Invalid invitation',
|
||||||
message: 'This invitation link is invalid or no longer active.'
|
message: `Reason: ${reason}. This invitation link is invalid or no longer active.`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
setIsRefChecked(true)
|
setIsRefChecked(true)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
console.error('[REGISTER] Token validation network error:', err)
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setInvalidRef(true)
|
setInvalidRef(true)
|
||||||
setIsRefChecked(true)
|
setIsRefChecked(true)
|
||||||
@ -109,7 +114,7 @@ function RegisterPageInner() {
|
|||||||
showToast({
|
showToast({
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
title: 'Network error',
|
title: 'Network error',
|
||||||
message: 'Could not validate the invitation link. Please try again.'
|
message: 'Could not reach the server. Is the backend running?'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -277,10 +282,12 @@ function RegisterPageInner() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="mx-auto max-w-2xl text-center mb-8">
|
<div className="mx-auto max-w-2xl text-center mb-8">
|
||||||
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-[#0F172A]">
|
<h1 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-[#0F172A]">
|
||||||
Register now
|
{mode === 'guest' ? 'Guest Registration' : 'Register now'}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-3 text-slate-700 text-base sm:text-lg">
|
<p className="mt-3 text-slate-700 text-base sm:text-lg">
|
||||||
Create your personal or company account with Profit Planet.
|
{mode === 'guest'
|
||||||
|
? 'Register as a guest to access your coffee abonnement.'
|
||||||
|
: 'Create your personal or company account with Profit Planet.'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user