@@ -182,12 +182,12 @@ export function TelegramLink() {
className="w-12 h-12 rounded-full object-cover border-2 border-blue-500/50"
/>
) : (
-
+
)}
{/* Link indicator */}
-
@@ -205,7 +205,7 @@ export function TelegramLink() {
{/* Notifications Info */}
-
+
Уведомления включены:
@@ -254,7 +254,7 @@ export function TelegramLink() {
diff --git a/frontend/src/components/ui/GlassCard.tsx b/frontend/src/components/ui/GlassCard.tsx
index b2bb7dc..f576a3d 100644
--- a/frontend/src/components/ui/GlassCard.tsx
+++ b/frontend/src/components/ui/GlassCard.tsx
@@ -91,10 +91,14 @@ export function StatsCard({
className
)}
>
-
+
{label}
-
+
{value}
{trend && (
@@ -111,7 +115,7 @@ export function StatsCard({
{icon && (
diff --git a/frontend/src/components/ui/UserAvatar.tsx b/frontend/src/components/ui/UserAvatar.tsx
index f1e89ae..9c9afb0 100644
--- a/frontend/src/components/ui/UserAvatar.tsx
+++ b/frontend/src/components/ui/UserAvatar.tsx
@@ -3,6 +3,8 @@ import { usersApi } from '@/api'
// Глобальный кэш для blob URL аватарок
const avatarCache = new Map
()
+// Пользователи, для которых нужно сбросить HTTP-кэш при следующем запросе
+const needsCacheBust = new Set()
interface UserAvatarProps {
userId: number
@@ -10,6 +12,7 @@ interface UserAvatarProps {
nickname: string
size?: 'sm' | 'md' | 'lg'
className?: string
+ version?: number // Для принудительного обновления при смене аватара
}
const sizeClasses = {
@@ -18,7 +21,7 @@ const sizeClasses = {
lg: 'w-24 h-24 text-xl',
}
-export function UserAvatar({ userId, hasAvatar, nickname, size = 'md', className = '' }: UserAvatarProps) {
+export function UserAvatar({ userId, hasAvatar, nickname, size = 'md', className = '', version = 0 }: UserAvatarProps) {
const [blobUrl, setBlobUrl] = useState(null)
const [failed, setFailed] = useState(false)
@@ -28,16 +31,31 @@ export function UserAvatar({ userId, hasAvatar, nickname, size = 'md', className
return
}
- // Проверяем кэш
- const cached = avatarCache.get(userId)
- if (cached) {
- setBlobUrl(cached)
- return
+ // Если version > 0, значит аватар обновился - сбрасываем кэш
+ const shouldBustCache = version > 0 || needsCacheBust.has(userId)
+
+ // Проверяем кэш только если не нужен bust
+ if (!shouldBustCache) {
+ const cached = avatarCache.get(userId)
+ if (cached) {
+ setBlobUrl(cached)
+ return
+ }
+ }
+
+ // Очищаем старый кэш если bust
+ if (shouldBustCache) {
+ const cached = avatarCache.get(userId)
+ if (cached) {
+ URL.revokeObjectURL(cached)
+ avatarCache.delete(userId)
+ }
+ needsCacheBust.delete(userId)
}
// Загружаем аватарку
let cancelled = false
- usersApi.getAvatarUrl(userId)
+ usersApi.getAvatarUrl(userId, shouldBustCache)
.then(url => {
if (!cancelled) {
avatarCache.set(userId, url)
@@ -53,7 +71,7 @@ export function UserAvatar({ userId, hasAvatar, nickname, size = 'md', className
return () => {
cancelled = true
}
- }, [userId, hasAvatar])
+ }, [userId, hasAvatar, version])
const sizeClass = sizeClasses[size]
@@ -84,4 +102,6 @@ export function clearAvatarCache(userId: number) {
URL.revokeObjectURL(cached)
avatarCache.delete(userId)
}
+ // Помечаем, что при следующем запросе нужно сбросить HTTP-кэш браузера
+ needsCacheBust.add(userId)
}
diff --git a/frontend/src/pages/ProfilePage.tsx b/frontend/src/pages/ProfilePage.tsx
index fc8616b..3ea6148 100644
--- a/frontend/src/pages/ProfilePage.tsx
+++ b/frontend/src/pages/ProfilePage.tsx
@@ -33,7 +33,7 @@ type NicknameForm = z.infer
type PasswordForm = z.infer
export function ProfilePage() {
- const { user, updateUser } = useAuthStore()
+ const { user, updateUser, avatarVersion, bumpAvatarVersion } = useAuthStore()
const toast = useToast()
// State
@@ -72,31 +72,57 @@ export function ProfilePage() {
}
}, [])
+ // Ref для отслеживания текущего blob URL
+ const avatarBlobRef = useRef(null)
+
// Load avatar via API
useEffect(() => {
- if (user?.id && user?.avatar_url) {
- loadAvatar(user.id)
- } else {
+ if (!user?.id || !user?.avatar_url) {
setIsLoadingAvatar(false)
+ return
}
+
+ let cancelled = false
+ const bustCache = avatarVersion > 0
+
+ setIsLoadingAvatar(true)
+ usersApi.getAvatarUrl(user.id, bustCache)
+ .then(url => {
+ if (cancelled) {
+ URL.revokeObjectURL(url)
+ return
+ }
+ // Очищаем старый blob URL
+ if (avatarBlobRef.current) {
+ URL.revokeObjectURL(avatarBlobRef.current)
+ }
+ avatarBlobRef.current = url
+ setAvatarBlobUrl(url)
+ })
+ .catch(() => {
+ if (!cancelled) {
+ setAvatarBlobUrl(null)
+ }
+ })
+ .finally(() => {
+ if (!cancelled) {
+ setIsLoadingAvatar(false)
+ }
+ })
+
return () => {
- if (avatarBlobUrl) {
- URL.revokeObjectURL(avatarBlobUrl)
+ cancelled = true
+ }
+ }, [user?.id, user?.avatar_url, avatarVersion])
+
+ // Cleanup blob URL on unmount
+ useEffect(() => {
+ return () => {
+ if (avatarBlobRef.current) {
+ URL.revokeObjectURL(avatarBlobRef.current)
}
}
- }, [user?.id, user?.avatar_url])
-
- const loadAvatar = async (userId: number) => {
- setIsLoadingAvatar(true)
- try {
- const url = await usersApi.getAvatarUrl(userId)
- setAvatarBlobUrl(url)
- } catch {
- setAvatarBlobUrl(null)
- } finally {
- setIsLoadingAvatar(false)
- }
- }
+ }, [])
// Update nickname form when user changes
useEffect(() => {
@@ -150,12 +176,10 @@ export function ProfilePage() {
const updatedUser = await usersApi.uploadAvatar(file)
updateUser({ avatar_url: updatedUser.avatar_url })
if (user?.id) {
- if (avatarBlobUrl) {
- URL.revokeObjectURL(avatarBlobUrl)
- }
clearAvatarCache(user.id)
- await loadAvatar(user.id)
}
+ // Bump version - это вызовет перезагрузку через useEffect
+ bumpAvatarVersion()
toast.success('Аватар обновлен')
} catch {
toast.error('Не удалось загрузить аватар')
diff --git a/frontend/src/store/auth.ts b/frontend/src/store/auth.ts
index 5f2fe78..02a3a28 100644
--- a/frontend/src/store/auth.ts
+++ b/frontend/src/store/auth.ts
@@ -10,6 +10,7 @@ interface AuthState {
isLoading: boolean
error: string | null
pendingInviteCode: string | null
+ avatarVersion: number
login: (data: LoginData) => Promise
register: (data: RegisterData) => Promise
@@ -18,6 +19,7 @@ interface AuthState {
setPendingInviteCode: (code: string | null) => void
consumePendingInviteCode: () => string | null
updateUser: (updates: Partial) => void
+ bumpAvatarVersion: () => void
}
export const useAuthStore = create()(
@@ -29,6 +31,7 @@ export const useAuthStore = create()(
isLoading: false,
error: null,
pendingInviteCode: null,
+ avatarVersion: 0,
login: async (data) => {
set({ isLoading: true, error: null })
@@ -97,6 +100,10 @@ export const useAuthStore = create()(
set({ user: { ...currentUser, ...updates } })
}
},
+
+ bumpAvatarVersion: () => {
+ set({ avatarVersion: get().avatarVersion + 1 })
+ },
}),
{
name: 'auth-storage',