uchill/front_material/app/invite/[token]/page.tsx

222 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Публичная страница регистрации по ссылке-приглашению (Material UI версия)
*/
'use client';
import React, { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { getMentorInfoByToken, registerByLink } from '@/api/students';
import { useAuth } from '@/contexts/AuthContext';
import { getErrorMessage } from '@/lib/error-utils';
const loadMaterialComponents = async () => {
await Promise.all([
import('@material/web/textfield/filled-text-field.js'),
import('@material/web/button/filled-button.js'),
import('@material/web/button/text-button.js'),
]);
};
export default function InvitationPage() {
const { token } = useParams();
const router = useRouter();
const { login: authLogin } = useAuth();
const [mounted, setMounted] = useState(false);
const [mentor, setMentor] = useState<{ mentor_name: string; avatar_url: string | null } | null>(null);
const [loading, setLoading] = useState(true);
const [registering, setRegistering] = useState(false);
const [error, setError] = useState<string | null>(null);
const [componentsLoaded, setComponentsLoaded] = useState(false);
const [formData, setFormData] = useState({
first_name: '',
last_name: '',
email: '',
password: '',
});
useEffect(() => {
setMounted(true);
loadMaterialComponents()
.then(() => setComponentsLoaded(true))
.catch((err) => {
console.error('Error loading Material components:', err);
setComponentsLoaded(true);
});
}, []);
useEffect(() => {
const fetchMentor = async () => {
try {
const data = await getMentorInfoByToken(token as string);
setMentor(data);
} catch (err: any) {
setError('Недействительная или просроченная ссылка');
} finally {
setLoading(false);
}
};
if (token) {
fetchMentor();
}
}, [token]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
setRegistering(true);
try {
const response = await registerByLink({
token: token as string,
...formData,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
// Автоматический вход после регистрации
if (response.access) {
localStorage.setItem('access_token', response.access);
if (response.refresh) {
localStorage.setItem('refresh_token', response.refresh);
}
if (response.user) {
await authLogin(response.access, response.user);
} else {
await authLogin(response.access);
}
window.location.href = '/dashboard';
}
} catch (err: any) {
setError(getErrorMessage(err, 'Ошибка при регистрации'));
setRegistering(false);
}
};
if (!mounted || loading || !componentsLoaded) {
return (
<div style={{ display: 'flex', justifyContent: 'center', padding: '48px', height: '100vh', alignItems: 'center', background: '#f8f9fa' }}>
<div
style={{
width: '40px',
height: '40px',
border: '3px solid #e0e0e0',
borderTopColor: 'var(--md-sys-color-primary, #6750a4)',
borderRadius: '50%',
animation: 'spin 0.8s linear infinite',
}}
/>
</div>
);
}
if (error && !mentor) {
return (
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20 }}>
<div style={{ maxWidth: 400, width: '100%', padding: 32, textAlign: 'center', borderRadius: 24, boxShadow: '0 4px 12px rgba(0,0,0,0.1)', background: '#fff' }}>
<div style={{ color: '#c62828', marginBottom: 16 }}>
<svg style={{ width: 64, height: 64 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 8 }}>Упс!</h1>
<p style={{ color: '#666', marginBottom: 24 }}>{error}</p>
<md-filled-button onClick={() => router.push('/')} style={{ width: '100%' }}>
На главную
</md-filled-button>
</div>
</div>
);
}
return (
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 20, background: 'var(--md-sys-color-surface-container-lowest)' }}>
<div style={{ maxWidth: '400px', width: '100%' }}>
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
<div style={{ marginBottom: '24px' }}>
<img
src="/logo/logo.svg"
alt="Uchill Logo"
style={{ width: '120px', height: 'auto' }}
/>
</div>
<h1 className="invite-title" style={{ fontSize: '32px', fontWeight: '700', color: 'var(--md-sys-color-on-surface)', marginBottom: '8px' }}>Присоединяйтесь!</h1>
<p style={{ fontSize: '16px', color: 'var(--md-sys-color-on-surface-variant)' }}>
Вас пригласил ментор <span style={{ fontWeight: '600', color: 'var(--md-sys-color-primary)' }}>{mentor?.mentor_name}</span>
</p>
{mentor?.avatar_url && (
<div style={{ marginTop: '20px', display: 'flex', justifyContent: 'center' }}>
<img
src={mentor.avatar_url}
alt={mentor.mentor_name}
style={{ height: '100px', width: '100px', borderRadius: '50%', objectFit: 'cover', border: '4px solid var(--md-sys-color-surface)', boxShadow: '0 8px 24px rgba(0,0,0,0.12)' }}
/>
</div>
)}
</div>
<div className="invite-form-card" style={{ padding: '32px', borderRadius: '24px', background: 'var(--md-sys-color-surface)', boxShadow: '0 4px 20px rgba(0,0,0,0.08)' }}>
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div className="auth-name-grid" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<md-filled-text-field
label="Имя"
value={formData.first_name}
onInput={(e: any) => setFormData({ ...formData, first_name: e.target.value })}
required
style={{ width: '100%' }}
/>
<md-filled-text-field
label="Фамилия"
value={formData.last_name}
onInput={(e: any) => setFormData({ ...formData, last_name: e.target.value })}
required
style={{ width: '100%' }}
/>
</div>
<md-filled-text-field
label="Email"
type="email"
value={formData.email}
onInput={(e: any) => setFormData({ ...formData, email: e.target.value })}
required
style={{ width: '100%' }}
/>
<md-filled-text-field
label="Пароль"
type="password"
value={formData.password}
onInput={(e: any) => setFormData({ ...formData, password: e.target.value })}
required
style={{ width: '100%' }}
/>
{error && (
<div style={{ padding: '12px 16px', borderRadius: '12px', background: 'var(--md-sys-color-error-container)', color: 'var(--md-sys-color-on-error-container)', fontSize: '14px', lineHeight: '1.5' }}>
{error}
</div>
)}
<md-filled-button
type="submit"
disabled={registering}
style={{ height: '56px', fontSize: '16px', fontWeight: '600', borderRadius: '16px' }}
>
{registering ? 'Регистрация...' : 'Начать обучение'}
</md-filled-button>
</form>
</div>
<div style={{ textAlign: 'center', marginTop: '24px' }}>
<md-text-button onClick={() => router.push('/login')} style={{ fontSize: '14px' }}>
Уже есть аккаунт? Войти
</md-text-button>
</div>
</div>
</div>
);
}