Add notification settings

This commit is contained in:
2026-01-04 02:47:38 +07:00
parent 7a3576aec0
commit 475e2cf4cd
14 changed files with 517 additions and 26 deletions

View File

@@ -1,5 +1,5 @@
import client from './client'
import type { User, UserProfilePublic, UserStats, PasswordChangeData } from '@/types'
import type { User, UserProfilePublic, UserStats, PasswordChangeData, NotificationSettings, NotificationSettingsUpdate } from '@/types'
export interface UpdateNicknameData {
nickname: string
@@ -48,4 +48,16 @@ export const usersApi = {
})
return URL.createObjectURL(response.data)
},
// Получить настройки уведомлений
getNotificationSettings: async (): Promise<NotificationSettings> => {
const response = await client.get<NotificationSettings>('/users/me/notifications')
return response.data
},
// Обновить настройки уведомлений
updateNotificationSettings: async (data: NotificationSettingsUpdate): Promise<NotificationSettings> => {
const response = await client.patch<NotificationSettings>('/users/me/notifications', data)
return response.data
},
}

View File

@@ -12,7 +12,8 @@ import {
import {
User, Camera, Trophy, Target, CheckCircle, Flame,
Loader2, MessageCircle, Link2, Link2Off, ExternalLink,
Eye, EyeOff, Save, KeyRound, Shield
Eye, EyeOff, Save, KeyRound, Shield, Bell, Sparkles,
AlertTriangle, FileCheck
} from 'lucide-react'
// Schemas
@@ -51,6 +52,12 @@ export function ProfilePage() {
const [isPolling, setIsPolling] = useState(false)
const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null)
// Notification settings state
const [notifyEvents, setNotifyEvents] = useState(user?.notify_events ?? true)
const [notifyDisputes, setNotifyDisputes] = useState(user?.notify_disputes ?? true)
const [notifyModeration, setNotifyModeration] = useState(user?.notify_moderation ?? true)
const [notificationUpdating, setNotificationUpdating] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
// Forms
@@ -265,6 +272,29 @@ export function ProfilePage() {
}
}
// Update notification setting
const handleNotificationToggle = async (
setting: 'notify_events' | 'notify_disputes' | 'notify_moderation',
currentValue: boolean,
setValue: (value: boolean) => void
) => {
setNotificationUpdating(setting)
const newValue = !currentValue
setValue(newValue)
try {
await usersApi.updateNotificationSettings({ [setting]: newValue })
updateUser({ [setting]: newValue })
toast.success('Настройки сохранены')
} catch {
// Revert on error
setValue(currentValue)
toast.error('Не удалось сохранить настройки')
} finally {
setNotificationUpdating(null)
}
}
const isLinked = !!user?.telegram_id
const displayAvatar = avatarBlobUrl || user?.telegram_avatar_url
@@ -544,6 +574,109 @@ export function ProfilePage() {
</form>
)}
</GlassCard>
{/* Notifications */}
{isLinked && (
<GlassCard>
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 rounded-xl bg-neon-500/20 flex items-center justify-center">
<Bell className="w-6 h-6 text-neon-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Уведомления</h2>
<p className="text-sm text-gray-400">Настройте типы уведомлений в Telegram</p>
</div>
</div>
<div className="space-y-4">
{/* Events toggle */}
<button
onClick={() => handleNotificationToggle('notify_events', notifyEvents, setNotifyEvents)}
disabled={notificationUpdating !== null}
className="w-full flex items-center justify-between p-4 bg-dark-700/50 rounded-xl border border-dark-600 hover:border-dark-500 transition-colors"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-yellow-500/20 flex items-center justify-center">
<Sparkles className="w-5 h-5 text-yellow-400" />
</div>
<div className="text-left">
<p className="text-white font-medium">События</p>
<p className="text-sm text-gray-400">Golden Hour, Jackpot, Double Risk и др.</p>
</div>
</div>
<div className={`
w-12 h-7 rounded-full transition-colors relative flex-shrink-0
${notifyEvents ? 'bg-neon-500' : 'bg-dark-600'}
${notificationUpdating === 'notify_events' ? 'opacity-50' : ''}
`}>
<div className={`
absolute top-1 w-5 h-5 rounded-full bg-white shadow transition-transform
${notifyEvents ? 'left-6' : 'left-1'}
`} />
</div>
</button>
{/* Disputes toggle */}
<button
onClick={() => handleNotificationToggle('notify_disputes', notifyDisputes, setNotifyDisputes)}
disabled={notificationUpdating !== null}
className="w-full flex items-center justify-between p-4 bg-dark-700/50 rounded-xl border border-dark-600 hover:border-dark-500 transition-colors"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-orange-500/20 flex items-center justify-center">
<AlertTriangle className="w-5 h-5 text-orange-400" />
</div>
<div className="text-left">
<p className="text-white font-medium">Споры</p>
<p className="text-sm text-gray-400">Оспаривания заданий и их решения</p>
</div>
</div>
<div className={`
w-12 h-7 rounded-full transition-colors relative flex-shrink-0
${notifyDisputes ? 'bg-neon-500' : 'bg-dark-600'}
${notificationUpdating === 'notify_disputes' ? 'opacity-50' : ''}
`}>
<div className={`
absolute top-1 w-5 h-5 rounded-full bg-white shadow transition-transform
${notifyDisputes ? 'left-6' : 'left-1'}
`} />
</div>
</button>
{/* Moderation toggle */}
<button
onClick={() => handleNotificationToggle('notify_moderation', notifyModeration, setNotifyModeration)}
disabled={notificationUpdating !== null}
className="w-full flex items-center justify-between p-4 bg-dark-700/50 rounded-xl border border-dark-600 hover:border-dark-500 transition-colors"
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-green-500/20 flex items-center justify-center">
<FileCheck className="w-5 h-5 text-green-400" />
</div>
<div className="text-left">
<p className="text-white font-medium">Модерация</p>
<p className="text-sm text-gray-400">Одобрение/отклонение игр и челленджей</p>
</div>
</div>
<div className={`
w-12 h-7 rounded-full transition-colors relative flex-shrink-0
${notifyModeration ? 'bg-neon-500' : 'bg-dark-600'}
${notificationUpdating === 'notify_moderation' ? 'opacity-50' : ''}
`}>
<div className={`
absolute top-1 w-5 h-5 rounded-full bg-white shadow transition-transform
${notifyModeration ? 'left-6' : 'left-1'}
`} />
</div>
</button>
{/* Info about mandatory notifications */}
<p className="text-xs text-gray-500 mt-4">
Уведомления о старте/финише марафонов и коды безопасности нельзя отключить.
</p>
</div>
</GlassCard>
)}
</div>
)
}

View File

@@ -18,6 +18,23 @@ export interface User extends UserPublic {
telegram_username?: string | null // Only visible to self
telegram_first_name?: string | null // Only visible to self
telegram_last_name?: string | null // Only visible to self
// Notification settings
notify_events?: boolean
notify_disputes?: boolean
notify_moderation?: boolean
}
// Notification settings
export interface NotificationSettings {
notify_events: boolean
notify_disputes: boolean
notify_moderation: boolean
}
export interface NotificationSettingsUpdate {
notify_events?: boolean
notify_disputes?: boolean
notify_moderation?: boolean
}
export interface TokenResponse {