222 lines
8.5 KiB
TypeScript
222 lines
8.5 KiB
TypeScript
/**
|
||
* Публичная страница регистрации по ссылке-приглашению (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 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 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 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>
|
||
);
|
||
}
|