nakama/frontend/src/pages/Profile.jsx

421 lines
15 KiB
React
Raw Normal View History

2025-11-03 20:35:01 +00:00
import { useState } from 'react'
2025-12-04 17:44:05 +00:00
import { Settings, Heart, Edit2, Shield, Copy, Users } from 'lucide-react'
2025-11-03 20:35:01 +00:00
import { updateProfile } from '../utils/api'
2025-12-04 17:44:05 +00:00
import { hapticFeedback, showAlert } from '../utils/telegram'
2025-12-04 20:00:39 +00:00
import { decodeHtmlEntities } from '../utils/htmlEntities'
2025-11-03 20:35:01 +00:00
import ThemeToggle from '../components/ThemeToggle'
2025-12-04 20:00:39 +00:00
import FollowListModal from '../components/FollowListModal'
2025-11-03 20:35:01 +00:00
import './Profile.css'
2025-11-10 20:13:22 +00:00
const DONATION_URL = 'https://donatepay.ru/don/1435720'
const ALLOWED_SEARCH_PREFERENCES = ['furry', 'anime']
const normalizeSearchPreference = (value) =>
ALLOWED_SEARCH_PREFERENCES.includes(value) ? value : 'furry'
const DEFAULT_SETTINGS = {
whitelist: {
2025-12-01 14:26:18 +00:00
noNSFW: true,
// Скрыть гомосексуальный контент
noHomo: true
2025-11-10 20:13:22 +00:00
},
searchPreference: 'furry'
}
const normalizeSettings = (rawSettings = {}) => {
const mergedWhitelist = {
...DEFAULT_SETTINGS.whitelist,
...(rawSettings.whitelist || {})
}
return {
...DEFAULT_SETTINGS,
...rawSettings,
whitelist: mergedWhitelist,
searchPreference: normalizeSearchPreference(rawSettings.searchPreference)
}
}
2025-11-03 20:35:01 +00:00
export default function Profile({ user, setUser }) {
const [showSettings, setShowSettings] = useState(false)
const [showEditBio, setShowEditBio] = useState(false)
const [bio, setBio] = useState(user.bio || '')
2025-12-04 20:00:39 +00:00
const [showFollowers, setShowFollowers] = useState(false)
const [showFollowing, setShowFollowing] = useState(false)
2025-11-10 20:13:22 +00:00
const [settings, setSettings] = useState(normalizeSettings(user.settings))
2025-11-03 20:35:01 +00:00
const [saving, setSaving] = useState(false)
const handleSaveBio = async () => {
try {
setSaving(true)
hapticFeedback('light')
const updatedUser = await updateProfile({ bio })
setUser({ ...user, bio })
setShowEditBio(false)
hapticFeedback('success')
} catch (error) {
console.error('Ошибка сохранения:', error)
hapticFeedback('error')
} finally {
setSaving(false)
}
}
const handleSaveSettings = async () => {
try {
setSaving(true)
hapticFeedback('light')
2025-11-10 20:13:22 +00:00
const normalizedSettings = normalizeSettings(settings)
await updateProfile({ settings: normalizedSettings })
setUser({ ...user, settings: normalizedSettings })
setSettings(normalizedSettings)
2025-11-03 20:35:01 +00:00
setShowSettings(false)
hapticFeedback('success')
} catch (error) {
console.error('Ошибка сохранения:', error)
hapticFeedback('error')
} finally {
setSaving(false)
}
}
const handleDonate = () => {
hapticFeedback('light')
2025-11-10 20:13:22 +00:00
window.open(DONATION_URL, '_blank', 'noopener,noreferrer')
2025-11-03 20:35:01 +00:00
}
const updateWhitelistSetting = async (key, value) => {
2025-11-10 20:13:22 +00:00
const updatedSettings = normalizeSettings({
2025-11-03 20:35:01 +00:00
...settings,
whitelist: {
...settings.whitelist,
[key]: value
}
2025-11-10 20:13:22 +00:00
})
setSettings(updatedSettings)
2025-11-03 20:35:01 +00:00
// Сохранить сразу на сервер
try {
2025-11-10 20:13:22 +00:00
await updateProfile({ settings: updatedSettings })
2025-11-03 20:35:01 +00:00
hapticFeedback('success')
} catch (error) {
console.error('Ошибка сохранения настроек:', error)
hapticFeedback('error')
}
}
const updateSearchPreference = (value) => {
2025-11-10 20:13:22 +00:00
const updatedSettings = normalizeSettings({
2025-11-03 20:35:01 +00:00
...settings,
searchPreference: value
})
2025-11-10 20:13:22 +00:00
setSettings(updatedSettings)
2025-11-03 20:35:01 +00:00
}
return (
<div className="profile-page">
{/* Хедер */}
<div className="profile-header">
<h1>Профиль</h1>
<button className="settings-btn" onClick={() => setShowSettings(true)}>
<Settings size={24} />
</button>
</div>
{/* Информация о пользователе */}
<div className="profile-info card">
<img
src={user.photoUrl || '/default-avatar.png'}
2025-12-01 00:51:23 +00:00
alt={user.username || user.firstName || 'User'}
2025-11-03 20:35:01 +00:00
className="profile-avatar"
/>
<div className="profile-details">
<h2 className="profile-name">
2025-12-01 00:51:23 +00:00
{user.firstName || ''} {user.lastName || ''}
{!user.firstName && !user.lastName && 'Пользователь'}
2025-11-03 20:35:01 +00:00
{(user.role === 'moderator' || user.role === 'admin') && (
<Shield size={20} color="var(--button-accent)" />
)}
</h2>
2025-12-01 00:51:23 +00:00
<p className="profile-username">@{user.username || user.firstName || 'user'}</p>
2025-11-03 20:35:01 +00:00
{user.bio ? (
<div className="profile-bio">
2025-12-04 20:00:39 +00:00
<p>{decodeHtmlEntities(user.bio)}</p>
2025-11-03 20:35:01 +00:00
<button className="edit-bio-btn" onClick={() => setShowEditBio(true)}>
<Edit2 size={16} />
</button>
</div>
) : (
<button className="add-bio-btn" onClick={() => setShowEditBio(true)}>
<Edit2 size={16} />
<span>Добавить описание</span>
</button>
)}
</div>
<div className="profile-stats">
2025-12-04 20:00:39 +00:00
<div className="stat-item" onClick={() => setShowFollowers(true)} style={{ cursor: 'pointer' }}>
2025-11-03 20:35:01 +00:00
<span className="stat-value">{user.followersCount || 0}</span>
<span className="stat-label">Подписчики</span>
</div>
<div className="stat-divider" />
2025-12-04 20:00:39 +00:00
<div className="stat-item" onClick={() => setShowFollowing(true)} style={{ cursor: 'pointer' }}>
2025-11-03 20:35:01 +00:00
<span className="stat-value">{user.followingCount || 0}</span>
<span className="stat-label">Подписки</span>
</div>
</div>
</div>
2025-11-10 20:13:22 +00:00
<div className="donation-card card">
<div className="donation-content">
<div className="donation-icon">
<Heart size={20} />
</div>
<div className="donation-text">
<h3>Поддержите проект</h3>
2025-11-20 21:32:48 +00:00
<p>Каждый взнос помогает развивать Nakama и запускать новые функции.</p>
2025-11-10 20:13:22 +00:00
</div>
</div>
<button className="donation-button" onClick={handleDonate}>
Перейти к донату
</button>
</div>
2025-12-04 17:44:05 +00:00
{/* Реферальная ссылка */}
{user.referralCode && (
<div className="referral-card card">
<div className="referral-content">
<div className="referral-icon">
<Users size={20} />
</div>
<div className="referral-text">
<h3>Пригласи друзей</h3>
<p>Получи +1 к счетчику, когда приглашенный создаст первый пост</p>
<div className="referral-stats">
Приглашено: <strong>{user.referralsCount || 0}</strong>
</div>
</div>
</div>
<div className="referral-link-section">
<div className="referral-link">
<code>{`https://t.me/${import.meta.env.VITE_TELEGRAM_BOT_NAME || 'NakamaSpaceBot'}?startapp=${user.referralCode}`}</code>
</div>
<button
className="referral-copy-btn"
onClick={async () => {
try {
hapticFeedback('light')
const botName = import.meta.env.VITE_TELEGRAM_BOT_NAME || 'NakamaSpaceBot'
const referralLink = `https://t.me/${botName}?startapp=${user.referralCode}`
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(referralLink)
hapticFeedback('success')
showAlert('✅ Ссылка скопирована!')
} else {
const textArea = document.createElement('textarea')
textArea.value = referralLink
textArea.style.position = 'fixed'
textArea.style.opacity = '0'
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
hapticFeedback('success')
showAlert('✅ Ссылка скопирована!')
}
} catch (error) {
console.error('Ошибка копирования:', error)
hapticFeedback('error')
}
}}
>
<Copy size={18} />
<span>Копировать</span>
</button>
</div>
</div>
)}
2025-11-10 20:13:22 +00:00
<div className="profile-powered">
Powered by glpshcn \\ RBach \\ E621 \\ GelBooru
</div>
2025-11-03 20:35:01 +00:00
{/* Быстрые настройки */}
<div className="quick-settings">
<h3>Быстрые настройки</h3>
<div className="setting-item card">
<div>
<div className="setting-name">Тема оформления</div>
<div className="setting-desc">Светлая / Тёмная / Авто</div>
</div>
<ThemeToggle showLabel />
</div>
<div className="setting-item card">
<div>
<div className="setting-name">Скрыть контент 18+</div>
<div className="setting-desc">Не показывать посты с пометкой NSFW</div>
</div>
<label className="toggle">
<input
type="checkbox"
checked={settings.whitelist.noNSFW}
onChange={(e) => updateWhitelistSetting('noNSFW', e.target.checked)}
/>
<span className="toggle-slider" />
</label>
</div>
2025-12-01 14:26:18 +00:00
<div className="setting-item card">
<div>
<div className="setting-name">Скрыть Homo</div>
<div className="setting-desc">Не показывать посты с гомосексуальным контентом</div>
</div>
<label className="toggle">
<input
type="checkbox"
checked={settings.whitelist.noHomo}
onChange={(e) => updateWhitelistSetting('noHomo', e.target.checked)}
/>
<span className="toggle-slider" />
</label>
</div>
2025-11-03 20:35:01 +00:00
</div>
{/* Модальное окно редактирования bio */}
{showEditBio && (
<div className="modal-overlay" onClick={() => setShowEditBio(false)}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2>Описание профиля</h2>
<button
className="submit-btn"
onClick={handleSaveBio}
disabled={saving}
>
{saving ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
<div className="modal-body">
<textarea
placeholder="Расскажите о себе..."
value={bio}
onChange={e => setBio(e.target.value)}
maxLength={300}
rows={6}
autoFocus
/>
<div className="char-count">
{bio.length} / 300
</div>
</div>
</div>
</div>
)}
{/* Модальное окно настроек */}
{showSettings && (
<div className="modal-overlay" onClick={() => setShowSettings(false)}>
<div className="modal-content settings-modal" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2>Настройки</h2>
<button
className="submit-btn"
onClick={handleSaveSettings}
disabled={saving}
>
{saving ? 'Сохранение...' : 'Сохранить'}
</button>
</div>
<div className="modal-body">
<div className="settings-section">
<h3>Фильтры контента</h3>
<div className="setting-row">
<div>
<div className="setting-name">Без NSFW</div>
<div className="setting-desc">Скрыть контент 18+</div>
</div>
<label className="toggle">
<input
type="checkbox"
checked={settings.whitelist.noNSFW}
onChange={(e) => updateWhitelistSetting('noNSFW', e.target.checked)}
/>
<span className="toggle-slider" />
</label>
2025-12-01 14:26:18 +00:00
</div>
<div className="setting-row">
<div>
<div className="setting-name">Скрыть Homo</div>
<div className="setting-desc">Убрать гомосексуальный контент из ленты и поиска</div>
</div>
<label className="toggle">
<input
type="checkbox"
checked={settings.whitelist.noHomo}
onChange={(e) => updateWhitelistSetting('noHomo', e.target.checked)}
/>
<span className="toggle-slider" />
</label>
2025-11-03 20:35:01 +00:00
</div>
</div>
<div className="settings-section">
<h3>Настройки поиска</h3>
2025-11-10 20:13:22 +00:00
<div className="search-switch">
<button
type="button"
className={`search-switch-btn ${settings.searchPreference === 'furry' ? 'active' : ''}`}
onClick={() => updateSearchPreference('furry')}
>
Только Furry (e621)
</button>
<button
type="button"
className={`search-switch-btn ${settings.searchPreference === 'anime' ? 'active' : ''}`}
onClick={() => updateSearchPreference('anime')}
>
Только Anime (gelbooru)
</button>
2025-11-03 20:35:01 +00:00
</div>
</div>
</div>
</div>
</div>
)}
2025-12-04 20:00:39 +00:00
{/* Модалка подписчиков */}
2025-12-04 20:27:45 +00:00
{showFollowers && user && (
2025-12-04 20:00:39 +00:00
<FollowListModal
users={user.followers || []}
title="Подписчики"
currentUser={user}
onClose={() => setShowFollowers(false)}
/>
)}
{/* Модалка подписок */}
2025-12-04 20:27:45 +00:00
{showFollowing && user && (
2025-12-04 20:00:39 +00:00
<FollowListModal
users={user.following || []}
title="Подписки"
currentUser={user}
onClose={() => setShowFollowing(false)}
/>
)}
2025-11-03 20:35:01 +00:00
</div>
)
}