Merge pull request #41 from BEDOLAGA-DEV/dev

Dev
This commit is contained in:
Egor
2026-01-19 10:23:08 +03:00
committed by GitHub
4 changed files with 246 additions and 0 deletions

View File

@@ -7,6 +7,10 @@ export interface BrandingInfo {
has_custom_logo: boolean
}
export interface AnimationEnabled {
enabled: boolean
}
export const brandingApi = {
// Get current branding (public, no auth required)
getBranding: async (): Promise<BrandingInfo> => {
@@ -45,4 +49,16 @@ export const brandingApi = {
}
return `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}`
},
// Get animation enabled (public, no auth required)
getAnimationEnabled: async (): Promise<AnimationEnabled> => {
const response = await apiClient.get<AnimationEnabled>('/cabinet/branding/animation')
return response.data
},
// Update animation enabled (admin only)
updateAnimationEnabled: async (enabled: boolean): Promise<AnimationEnabled> => {
const response = await apiClient.patch<AnimationEnabled>('/cabinet/branding/animation', { enabled })
return response.data
},
}

View File

@@ -0,0 +1,190 @@
import { useQuery } from '@tanstack/react-query'
import { brandingApi } from '../api/branding'
export default function AnimatedBackground() {
const { data: animationSettings } = useQuery({
queryKey: ['animation-enabled'],
queryFn: brandingApi.getAnimationEnabled,
staleTime: 1000 * 60 * 5, // 5 minutes
retry: false,
})
// Don't render if animation is disabled
if (animationSettings && !animationSettings.enabled) {
return null
}
return (
<>
{/* SVG Filter for glow effect */}
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur" />
<feMerge>
<feMergeNode in="coloredBlur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</svg>
{/* Animated wave gradients */}
<div className="wave-bg-container">
<div className="wave-blob wave-blob-1" />
<div className="wave-blob wave-blob-2" />
<div className="wave-blob wave-blob-3" />
<div className="wave-blob wave-blob-4" />
</div>
<style>{`
.wave-bg-container {
position: fixed;
inset: 0;
z-index: -1;
overflow: hidden;
pointer-events: none;
background: transparent;
}
.wave-blob {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.6;
mix-blend-mode: screen;
}
.wave-blob-1 {
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(249, 115, 22, 0.8) 0%, transparent 70%);
top: -20%;
left: -10%;
animation: wave1 15s ease-in-out infinite;
}
.wave-blob-2 {
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(59, 130, 246, 0.8) 0%, transparent 70%);
bottom: -15%;
right: -10%;
animation: wave2 18s ease-in-out infinite;
}
.wave-blob-3 {
width: 400px;
height: 400px;
background: radial-gradient(circle, rgba(168, 85, 247, 0.6) 0%, transparent 70%);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: wave3 20s ease-in-out infinite;
}
.wave-blob-4 {
width: 350px;
height: 350px;
background: radial-gradient(circle, rgba(236, 72, 153, 0.5) 0%, transparent 70%);
bottom: 20%;
left: 20%;
animation: wave4 12s ease-in-out infinite;
}
@keyframes wave1 {
0%, 100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(10%, 15%) scale(1.1);
}
50% {
transform: translate(5%, 25%) scale(0.95);
}
75% {
transform: translate(-5%, 10%) scale(1.05);
}
}
@keyframes wave2 {
0%, 100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(-15%, -10%) scale(1.15);
}
50% {
transform: translate(-10%, -20%) scale(0.9);
}
75% {
transform: translate(5%, -5%) scale(1.1);
}
}
@keyframes wave3 {
0%, 100% {
transform: translate(-50%, -50%) scale(1);
}
33% {
transform: translate(-40%, -60%) scale(1.2);
}
66% {
transform: translate(-60%, -40%) scale(0.85);
}
}
@keyframes wave4 {
0%, 100% {
transform: translate(0, 0) scale(1);
}
50% {
transform: translate(20%, -15%) scale(1.25);
}
}
/* Dark theme - brighter */
:root.dark .wave-blob {
opacity: 0.5;
}
:root.dark .wave-blob-1 {
background: radial-gradient(circle, rgba(249, 115, 22, 0.7) 0%, transparent 70%);
}
:root.dark .wave-blob-2 {
background: radial-gradient(circle, rgba(59, 130, 246, 0.7) 0%, transparent 70%);
}
:root.dark .wave-blob-3 {
background: radial-gradient(circle, rgba(168, 85, 247, 0.5) 0%, transparent 70%);
}
:root.dark .wave-blob-4 {
background: radial-gradient(circle, rgba(236, 72, 153, 0.4) 0%, transparent 70%);
}
/* Light theme - visible */
:root:not(.dark) .wave-blob {
opacity: 0.7;
mix-blend-mode: multiply;
filter: blur(100px);
}
:root:not(.dark) .wave-blob-1 {
background: radial-gradient(circle, rgba(249, 115, 22, 0.9) 0%, transparent 70%);
}
:root:not(.dark) .wave-blob-2 {
background: radial-gradient(circle, rgba(59, 130, 246, 0.9) 0%, transparent 70%);
}
:root:not(.dark) .wave-blob-3 {
background: radial-gradient(circle, rgba(168, 85, 247, 0.7) 0%, transparent 70%);
}
:root:not(.dark) .wave-blob-4 {
background: radial-gradient(circle, rgba(236, 72, 153, 0.6) 0%, transparent 70%);
}
`}</style>
</>
)
}

View File

@@ -6,6 +6,7 @@ import { useAuthStore } from '../../store/auth'
import LanguageSwitcher from '../LanguageSwitcher'
import PromoDiscountBadge from '../PromoDiscountBadge'
import TicketNotificationBell from '../TicketNotificationBell'
import AnimatedBackground from '../AnimatedBackground'
import { contestsApi } from '../../api/contests'
import { pollsApi } from '../../api/polls'
import { brandingApi } from '../../api/branding'
@@ -270,6 +271,9 @@ export default function Layout({ children }: LayoutProps) {
return (
<div className="min-h-screen flex flex-col">
{/* Animated Background */}
<AnimatedBackground />
{/* Header */}
<header className="sticky top-0 z-50 glass border-b border-dark-800/50">
<div className="w-full mx-auto px-4 sm:px-6">

View File

@@ -864,6 +864,19 @@ export default function AdminSettings() {
queryFn: brandingApi.getBranding,
})
// Animation toggle query and mutation
const { data: animationSettings } = useQuery({
queryKey: ['animation-enabled'],
queryFn: brandingApi.getAnimationEnabled,
})
const updateAnimationMutation = useMutation({
mutationFn: (enabled: boolean) => brandingApi.updateAnimationEnabled(enabled),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['animation-enabled'] })
},
})
const updateNameMutation = useMutation({
mutationFn: (name: string) => brandingApi.updateName(name),
onSuccess: () => {
@@ -1187,6 +1200,29 @@ export default function AdminSettings() {
</p>
</div>
</div>
{/* Animation Toggle */}
<div className="mt-6 pt-6 border-t border-dark-700/50">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-dark-200">Анимированный фон</h3>
<p className="text-xs text-dark-500 mt-0.5">
Волновая анимация на фоне для всех пользователей
</p>
</div>
<button
onClick={() => updateAnimationMutation.mutate(!(animationSettings?.enabled ?? true))}
disabled={updateAnimationMutation.isPending}
className={`relative w-12 h-6 rounded-full transition-colors ${
(animationSettings?.enabled ?? true) ? 'bg-accent-500' : 'bg-dark-600'
} ${updateAnimationMutation.isPending ? 'opacity-50' : ''}`}
>
<div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${
(animationSettings?.enabled ?? true) ? 'left-7' : 'left-1'
}`} />
</button>
</div>
</div>
</div>
{/* Theme Colors Card */}