2025-11-03 20:35:01 +00:00
|
|
|
|
import { useState } from 'react'
|
2025-11-10 20:13:22 +00:00
|
|
|
|
import { Settings, Heart, Edit2, Shield } from 'lucide-react'
|
2025-11-03 20:35:01 +00:00
|
|
|
|
import { updateProfile } from '../utils/api'
|
2025-11-10 20:13:22 +00:00
|
|
|
|
import { hapticFeedback } from '../utils/telegram'
|
2025-11-03 20:35:01 +00:00
|
|
|
|
import ThemeToggle from '../components/ThemeToggle'
|
|
|
|
|
|
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-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">
|
|
|
|
|
|
<p>{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">
|
|
|
|
|
|
<span className="stat-value">{user.followersCount || 0}</span>
|
|
|
|
|
|
<span className="stat-label">Подписчики</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="stat-divider" />
|
|
|
|
|
|
<div className="stat-item">
|
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
|
|
<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>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|