Files
links/frontend/linktree-frontend/src/app/profile/page.tsx
Andrey K. Choi a963281be0
Some checks failed
continuous-integration/drone/pr Build is failing
feat: улучшена навигационная панель с полной интеграцией темы и локализации
- Обновлен LayoutWrapper с улучшенным UI навигации
- Добавлен dropdown меню пользователя с аватаром
- Интегрированы ThemeToggle и LanguageSelector в навигацию
- Переключатели темы и языка теперь всегда видны
- Добавлены флаги стран в селектор языков
- Создана страница редактирования профиля /profile
- Улучшены стили для темной темы в navbar
- Добавлены CSS стили для навигации и профиля
2025-11-09 15:45:01 +09:00

311 lines
10 KiB
TypeScript

'use client'
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useLocale } from '../contexts/LocaleContext'
interface UserProfile {
id: number
username: string
email: string
full_name: string
bio: string
avatar: string | null
cover: string | null
}
export default function ProfilePage() {
const { t } = useLocale()
const router = useRouter()
const [profile, setProfile] = useState<UserProfile | null>(null)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [avatarFile, setAvatarFile] = useState<File | null>(null)
const [coverFile, setCoverFile] = useState<File | null>(null)
useEffect(() => {
loadProfile()
}, [])
const loadProfile = async () => {
try {
const token = localStorage.getItem('token')
if (!token) {
router.push('/auth/login')
return
}
const response = await fetch('/api/auth/user', {
headers: { 'Authorization': `Bearer ${token}` }
})
if (!response.ok) {
throw new Error('Failed to load profile')
}
const data = await response.json()
setProfile(data)
} catch (error) {
console.error('Error loading profile:', error)
} finally {
setLoading(false)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!profile) return
setSaving(true)
try {
const token = localStorage.getItem('token')
const formData = new FormData()
formData.append('username', profile.username)
formData.append('email', profile.email)
formData.append('full_name', profile.full_name)
formData.append('bio', profile.bio)
if (avatarFile) {
formData.append('avatar', avatarFile)
}
if (coverFile) {
formData.append('cover', coverFile)
}
const response = await fetch('/api/auth/user', {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
})
if (response.ok) {
const updatedProfile = await response.json()
setProfile(updatedProfile)
// Очистить выбранные файлы
setAvatarFile(null)
setCoverFile(null)
}
} catch (error) {
console.error('Error saving profile:', error)
} finally {
setSaving(false)
}
}
const removeAvatar = () => {
if (profile) {
setProfile({ ...profile, avatar: null })
}
}
const removeCover = () => {
if (profile) {
setProfile({ ...profile, cover: null })
}
}
if (loading) {
return (
<div className="container mt-4">
<div className="d-flex justify-content-center">
<div className="spinner-border" role="status">
<span className="visually-hidden">{t('common.loading')}</span>
</div>
</div>
</div>
)
}
if (!profile) {
return (
<div className="container mt-4">
<div className="alert alert-danger">
{t('common.error')}: Profile not found
</div>
</div>
)
}
return (
<div className="container mt-4">
<div className="row justify-content-center">
<div className="col-md-8">
<div className="card">
<div className="card-header">
<h4 className="mb-0">
<i className="fas fa-user me-2"></i>
{t('profile.edit')}
</h4>
</div>
<div className="card-body">
<form onSubmit={handleSubmit}>
{/* Avatar */}
<div className="mb-4 text-center">
<div className="mb-3">
<img
src={
avatarFile
? URL.createObjectURL(avatarFile)
: profile.avatar
? `${process.env.NEXT_PUBLIC_API_URL || 'https://links.shareon.kr'}${profile.avatar}`
: '/assets/img/avatar-dhg.png'
}
alt="Avatar"
className="rounded-circle profile-avatar"
width={120}
height={120}
/>
</div>
<div className="d-flex gap-2 justify-content-center">
<label className="btn btn-outline-primary btn-sm">
<i className="fas fa-camera me-1"></i>
{t('profile.avatar')}
<input
type="file"
className="d-none"
accept="image/*"
onChange={(e) => setAvatarFile(e.target.files?.[0] || null)}
/>
</label>
{(profile.avatar || avatarFile) && (
<button
type="button"
className="btn btn-outline-danger btn-sm"
onClick={removeAvatar}
>
<i className="fas fa-times me-1"></i>
{t('profile.removeAvatar')}
</button>
)}
</div>
</div>
{/* Cover Image */}
<div className="mb-4">
<label className="form-label">{t('profile.cover')}</label>
{(profile.cover || coverFile) && (
<div className="mb-2">
<img
src={
coverFile
? URL.createObjectURL(coverFile)
: `${process.env.NEXT_PUBLIC_API_URL || 'https://links.shareon.kr'}${profile.cover}`
}
alt="Cover"
className="img-fluid rounded profile-cover"
/>
</div>
)}
<div className="d-flex gap-2">
<label className="btn btn-outline-primary btn-sm">
<i className="fas fa-image me-1"></i>
{profile.cover ? t('common.update') : t('common.add')}
<input
type="file"
className="d-none"
accept="image/*"
onChange={(e) => setCoverFile(e.target.files?.[0] || null)}
/>
</label>
{(profile.cover || coverFile) && (
<button
type="button"
className="btn btn-outline-danger btn-sm"
onClick={removeCover}
>
<i className="fas fa-times me-1"></i>
{t('profile.removeCover')}
</button>
)}
</div>
</div>
{/* Basic Info */}
<div className="row">
<div className="col-md-6">
<div className="mb-3">
<label className="form-label">{t('profile.username')}</label>
<input
type="text"
className="form-control"
value={profile.username}
onChange={(e) => setProfile({ ...profile, username: e.target.value })}
required
/>
</div>
</div>
<div className="col-md-6">
<div className="mb-3">
<label className="form-label">{t('profile.email')}</label>
<input
type="email"
className="form-control"
value={profile.email}
onChange={(e) => setProfile({ ...profile, email: e.target.value })}
required
/>
</div>
</div>
</div>
<div className="mb-3">
<label className="form-label">{t('profile.fullName')}</label>
<input
type="text"
className="form-control"
value={profile.full_name}
onChange={(e) => setProfile({ ...profile, full_name: e.target.value })}
/>
</div>
<div className="mb-4">
<label className="form-label">{t('profile.bio')}</label>
<textarea
className="form-control"
rows={4}
value={profile.bio}
onChange={(e) => setProfile({ ...profile, bio: e.target.value })}
placeholder={t('profile.bio')}
/>
</div>
{/* Actions */}
<div className="d-flex justify-content-between">
<button
type="button"
className="btn btn-secondary"
onClick={() => router.back()}
>
<i className="fas fa-arrow-left me-2"></i>
{t('common.back')}
</button>
<button
type="submit"
className="btn btn-primary"
disabled={saving}
>
{saving ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
{t('common.saving')}
</>
) : (
<>
<i className="fas fa-save me-2"></i>
{t('common.save')}
</>
)}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
)
}