Tech4biz-channel/Channel-Frontend/src/pages/InvitePage.tsx
2026-07-15 14:28:24 +05:30

213 lines
8.8 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { Shield, CheckCircle, AlertCircle, ChevronRight, KeyRound, Eye, EyeOff } from 'lucide-react';
import { axiosInstance } from '../services/axios';
import { useAuthStore } from '../hooks/use-auth';
import { useToast } from '../hooks/use-toast';
export const InvitePage: React.FC = () => {
const { success } = useToast();
const [searchParams] = useSearchParams();
const token = searchParams.get('token');
const navigate = useNavigate();
const { setAuth } = useAuthStore();
const [status, setStatus] = useState<'loading' | 'valid' | 'invalid'>('loading');
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!token) {
setStatus('invalid');
return;
}
// verify token on mount
const verifyToken = async () => {
try {
const response = await axiosInstance.get(`/auth/invite/${token}`);
setEmail(response.data.email);
setStatus('valid');
} catch (err) {
setStatus('invalid');
}
};
verifyToken();
}, [token]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
if (password.length < 8) {
setError('Password must be at least 8 characters long');
return;
}
setIsSubmitting(true);
try {
const response = await axiosInstance.post('/auth/invite/accept', {
token,
password
});
// Show success toast
success('Account activated successfully!');
// Set auth store
setAuth({ user: response.data.user, accessToken: response.data.token });
// Redirect based on role / status
setTimeout(() => {
if (response.data.user.role === 'ADMIN') {
navigate('/admin');
} else if (response.data.user.onboardingStatus === 'APPROVED') {
navigate('/client');
} else {
navigate('/onboarding');
}
}, 1000);
} catch (err: any) {
setError(err.response?.data?.detail || err.response?.data?.error || 'Activation failed');
setIsSubmitting(false);
}
};
if (status === 'loading') {
return (
<div className="min-h-screen bg-ink-50 flex items-center justify-center">
<div className="w-8 h-8 border-4 border-ink-900/35 border-t-ink-900 rounded-full animate-spin" />
</div>
);
}
if (status === 'invalid') {
return (
<div className="min-h-screen bg-ink-50 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-ink-0 rounded-[2rem] p-8 border border-ink-200 shadow-2xl text-center">
<div className="w-16 h-16 bg-red-500/10 rounded-full flex items-center justify-center mx-auto mb-6">
<AlertCircle className="w-8 h-8 text-red-600 dark:text-red-500" />
</div>
<h2 className="text-2xl login-form-title font-bold text-ink-900 mb-2">Invalid or Expired Link</h2>
<p className="text-sm login-form-subtitle text-ink-500 max-w-md mx-auto mb-6">
The onboarding invitation link is invalid, expired, or has already been used. Please request a new invitation from your administrator.
</p>
<button onClick={() => navigate('/login')} className="text-ink-900 font-extrabold hover:underline">
Return to Login
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-ink-50 text-ink-900 font-sans flex items-center justify-center p-4">
{/* Monochromatic background accent */}
<div className="fixed top-0 right-0 w-[500px] h-[500px] bg-ink-900/5 rounded-full blur-[120px] pointer-events-none" />
<div className="w-full max-w-md z-10">
<div className="text-center mb-6">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-ink-900 to-ink-800 shadow-xl flex items-center justify-center mx-auto mb-6">
<Shield className="w-6 h-6 text-ink-0" />
</div>
<h1 className="text-3xl login-title font-extrabold tracking-tight mb-2">Welcome to Tech4Biz</h1>
<p className="text-sm login-desc text-ink-500">
Set up your partner account for <span className="text-ink-900 font-bold">{email}</span>
</p>
</div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-ink-0 rounded-[2rem] p-8 shadow-2xl border border-ink-200"
>
<form onSubmit={handleSubmit} className="space-y-5">
{error && (
<div className="p-4 bg-red-500/10 border border-red-500/20 rounded-xl flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-red-600 dark:text-red-400 shrink-0" />
<p className="text-xs font-bold text-red-600 dark:text-red-400">{error}</p>
</div>
)}
<div className="space-y-1.5">
<label className="text-xs font-bold uppercase tracking-wider text-ink-400">Set Password</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<KeyRound className="w-4 h-4 text-ink-400" />
</div>
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-11 pr-10 py-3.5 bg-ink-50 border border-ink-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-ink-900/10 outline-none transition-all placeholder-ink-400 text-ink-900"
placeholder="Enter a secure password"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-ink-400 hover:text-ink-900 cursor-pointer"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="space-y-1.5">
<label className="text-xs font-bold uppercase tracking-wider text-ink-400">Confirm Password</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<CheckCircle className="w-4 h-4 text-ink-400" />
</div>
<input
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full pl-11 pr-10 py-3.5 bg-ink-50 border border-ink-200 rounded-xl text-sm font-medium focus:ring-4 focus:ring-ink-900/10 outline-none transition-all placeholder-ink-400 text-ink-900"
placeholder="Confirm your password"
required
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-ink-400 hover:text-ink-900 cursor-pointer"
>
{showConfirmPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<button
type="submit"
disabled={isSubmitting || !password || !confirmPassword}
className="w-full flex items-center justify-center gap-2 py-4 rounded-xl bg-ink-900 text-ink-0 font-bold hover:bg-ink-800 hover:shadow-lg hover:-translate-y-0.5 transition-all disabled:opacity-50 disabled:hover:translate-y-0 mt-6"
>
{isSubmitting ? (
<div className="w-5 h-5 border-2 border-ink-0/30 border-t-ink-0 rounded-full animate-spin" />
) : (
<>
Create Account & Continue
<ChevronRight className="w-4 h-4" />
</>
)}
</button>
</form>
</motion.div>
</div>
</div>
);
};
export default InvitePage;