nakama/frontend/src/pages/Profile.jsx

421 lines
15 KiB
JavaScript
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.

import { useState } from 'react'
import { Settings, Heart, Edit2, Shield, Copy, Users } from 'lucide-react'
import { updateProfile } from '../utils/api'
import { hapticFeedback, showAlert } from '../utils/telegram'
import { decodeHtmlEntities } from '../utils/htmlEntities'
import ThemeToggle from '../components/ThemeToggle'
import FollowListModal from '../components/FollowListModal'
import './Profile.css'
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: {
noNSFW: true,
// Скрыть гомосексуальный контент
noHomo: true
},
searchPreference: 'furry'
}
const normalizeSettings = (rawSettings = {}) => {
const mergedWhitelist = {
...DEFAULT_SETTINGS.whitelist,
...(rawSettings.whitelist || {})
}
return {
...DEFAULT_SETTINGS,
...rawSettings,
whitelist: mergedWhitelist,
searchPreference: normalizeSearchPreference(rawSettings.searchPreference)
}
}
export default function Profile({ user, setUser }) {
const [showSettings, setShowSettings] = useState(false)
const [showEditBio, setShowEditBio] = useState(false)
const [bio, setBio] = useState(user.bio || '')
const [showFollowers, setShowFollowers] = useState(false)
const [showFollowing, setShowFollowing] = useState(false)
const [settings, setSettings] = useState(normalizeSettings(user.settings))
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')
const normalizedSettings = normalizeSettings(settings)
await updateProfile({ settings: normalizedSettings })
setUser({ ...user, settings: normalizedSettings })
setSettings(normalizedSettings)
setShowSettings(false)
hapticFeedback('success')
} catch (error) {
console.error('Ошибка сохранения:', error)
hapticFeedback('error')
} finally {
setSaving(false)
}
}
const handleDonate = () => {
hapticFeedback('light')
window.open(DONATION_URL, '_blank', 'noopener,noreferrer')
}
const updateWhitelistSetting = async (key, value) => {
const updatedSettings = normalizeSettings({
...settings,
whitelist: {
...settings.whitelist,
[key]: value
}
})
setSettings(updatedSettings)
// Сохранить сразу на сервер
try {
await updateProfile({ settings: updatedSettings })
hapticFeedback('success')
} catch (error) {
console.error('Ошибка сохранения настроек:', error)
hapticFeedback('error')
}
}
const updateSearchPreference = (value) => {
const updatedSettings = normalizeSettings({
...settings,
searchPreference: value
})
setSettings(updatedSettings)
}
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'}
alt={user.username || user.firstName || 'User'}
className="profile-avatar"
/>
<div className="profile-details">
<h2 className="profile-name">
{user.firstName || ''} {user.lastName || ''}
{!user.firstName && !user.lastName && 'Пользователь'}
{(user.role === 'moderator' || user.role === 'admin') && (
<Shield size={20} color="var(--button-accent)" />
)}
</h2>
<p className="profile-username">@{user.username || user.firstName || 'user'}</p>
{user.bio ? (
<div className="profile-bio">
<p>{decodeHtmlEntities(user.bio)}</p>
<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">
<div className="stat-item" onClick={() => setShowFollowers(true)} style={{ cursor: 'pointer' }}>
<span className="stat-value">{user.followersCount || 0}</span>
<span className="stat-label">Подписчики</span>
</div>
<div className="stat-divider" />
<div className="stat-item" onClick={() => setShowFollowing(true)} style={{ cursor: 'pointer' }}>
<span className="stat-value">{user.followingCount || 0}</span>
<span className="stat-label">Подписки</span>
</div>
</div>
</div>
<div className="donation-card card">
<div className="donation-content">
<div className="donation-icon">
<Heart size={20} />
</div>
<div className="donation-text">
<h3>Поддержите проект</h3>
<p>Каждый взнос помогает развивать Nakama и запускать новые функции.</p>
</div>
</div>
<button className="donation-button" onClick={handleDonate}>
Перейти к донату
</button>
</div>
{/* Реферальная ссылка */}
{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>
)}
<div className="profile-powered">
Powered by glpshcn \\ RBach \\ E621 \\ GelBooru
</div>
{/* Быстрые настройки */}
<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>
<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>
</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>
</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>
</div>
</div>
<div className="settings-section">
<h3>Настройки поиска</h3>
<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>
</div>
</div>
</div>
</div>
</div>
)}
{/* Модалка подписчиков */}
{showFollowers && user && (
<FollowListModal
users={user.followers || []}
title="Подписчики"
currentUser={user}
onClose={() => setShowFollowers(false)}
/>
)}
{/* Модалка подписок */}
{showFollowing && user && (
<FollowListModal
users={user.following || []}
title="Подписки"
currentUser={user}
onClose={() => setShowFollowing(false)}
/>
)}
</div>
)
}