Files
bedolaga-cabinet/src/components/admin/BrandingTab.tsx
c0mrade 562ab7abf7 refactor: full codebase cleanup, dependency updates, and lint fixes
Phase 0: Remove ~920 lines of dead code (ThemeBentoPicker, PromoDiscountBadge,
AdminLayout, SettingsSidebar, MovingGradient, EmptyState, miniapp API, unused
types/functions/transitions/skeleton helpers). Fix API barrel file (add 20 missing exports).

Phase 1: Add ErrorBoundary (app/page/widget levels), centralize constants,
add axios timeout, fix staleTime:0 in React Query.

Phase 2: Consolidate hexToHsl, extract email validation, fix duplicate code.

Phase 3: Fix auth race condition (await checkAdminStatus), memoize useCurrency,
add WebSocket message validation.

Phase 4: Extract useBranding, useFeatureFlags, useScrollRestoration from AppShell.

Phase 5: Remove all eslint-disable react-hooks/exhaustive-deps (14 total),
simplify logger, remove deprecated hooks (useBackButton, useTelegramDnd,
useTelegramWebApp).

Dependencies: React 19, react-router 7, zustand 5, i18next 25, react-i18next 16,
eslint-plugin-react-refresh 0.5. Remove unused @lottiefiles/dotlottie-react.
Convert vite manualChunks to function-based approach for react-router v7 compat.

Lint: Fix logger.ts no-unused-expressions, fix react-refresh/only-export-components
in 6 Radix primitive files (const re-exports → direct re-exports), fix
WebSocketProvider exhaustive-deps. Result: 0 errors, 0 warnings.

Add CLAUDE.md to .gitignore.
2026-02-06 16:55:55 +03:00

255 lines
9.3 KiB
TypeScript

import { useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { brandingApi, setCachedBranding } from '../../api/branding';
import { setCachedAnimationEnabled } from '../AnimatedBackground';
import { setCachedFullscreenEnabled } from '../../hooks/useTelegramSDK';
import { UploadIcon, TrashIcon, PencilIcon, CheckIcon, CloseIcon } from './icons';
import { Toggle } from './Toggle';
interface BrandingTabProps {
accentColor?: string;
}
export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const fileInputRef = useRef<HTMLInputElement>(null);
const [editingName, setEditingName] = useState(false);
const [newName, setNewName] = useState('');
// Queries
const { data: branding } = useQuery({
queryKey: ['branding'],
queryFn: brandingApi.getBranding,
});
const { data: animationSettings } = useQuery({
queryKey: ['animation-enabled'],
queryFn: brandingApi.getAnimationEnabled,
});
const { data: fullscreenSettings } = useQuery({
queryKey: ['fullscreen-enabled'],
queryFn: brandingApi.getFullscreenEnabled,
});
const { data: emailAuthSettings } = useQuery({
queryKey: ['email-auth-enabled'],
queryFn: brandingApi.getEmailAuthEnabled,
});
// Mutations
const updateBrandingMutation = useMutation({
mutationFn: brandingApi.updateName,
onSuccess: (data) => {
setCachedBranding(data);
queryClient.invalidateQueries({ queryKey: ['branding'] });
setEditingName(false);
},
});
const uploadLogoMutation = useMutation({
mutationFn: brandingApi.uploadLogo,
onSuccess: (data) => {
setCachedBranding(data);
queryClient.invalidateQueries({ queryKey: ['branding'] });
},
});
const deleteLogoMutation = useMutation({
mutationFn: brandingApi.deleteLogo,
onSuccess: (data) => {
setCachedBranding(data);
queryClient.invalidateQueries({ queryKey: ['branding'] });
},
});
const updateAnimationMutation = useMutation({
mutationFn: (enabled: boolean) => brandingApi.updateAnimationEnabled(enabled),
onSuccess: (data) => {
setCachedAnimationEnabled(data.enabled);
queryClient.invalidateQueries({ queryKey: ['animation-enabled'] });
},
});
const updateFullscreenMutation = useMutation({
mutationFn: (enabled: boolean) => brandingApi.updateFullscreenEnabled(enabled),
onSuccess: (data) => {
setCachedFullscreenEnabled(data.enabled);
queryClient.invalidateQueries({ queryKey: ['fullscreen-enabled'] });
},
});
const updateEmailAuthMutation = useMutation({
mutationFn: (enabled: boolean) => brandingApi.updateEmailAuthEnabled(enabled),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['email-auth-enabled'] });
},
});
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
uploadLogoMutation.mutate(file);
}
};
return (
<div className="space-y-6">
{/* Logo & Name */}
<div className="rounded-2xl border border-dark-700/50 bg-dark-800/50 p-6">
<h3 className="mb-4 text-lg font-semibold text-dark-100">
{t('admin.settings.logoAndName')}
</h3>
<div className="flex items-start gap-6">
{/* Logo */}
<div className="flex-shrink-0">
<div
className="flex h-20 w-20 items-center justify-center overflow-hidden rounded-2xl text-3xl font-bold text-white"
style={{
background: `linear-gradient(135deg, ${accentColor}, ${accentColor}dd)`,
}}
>
{branding?.has_custom_logo ? (
<img
src={brandingApi.getLogoUrl(branding) ?? undefined}
alt="Logo"
className="h-full w-full object-cover"
/>
) : (
branding?.logo_letter || 'V'
)}
</div>
<div className="mt-3 flex gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleLogoUpload}
className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
disabled={uploadLogoMutation.isPending}
className="flex flex-1 items-center justify-center gap-1 rounded-xl bg-dark-700 px-3 py-2 text-sm text-dark-200 transition-colors hover:bg-dark-600 disabled:opacity-50"
>
<UploadIcon />
</button>
{branding?.has_custom_logo && (
<button
onClick={() => deleteLogoMutation.mutate()}
disabled={deleteLogoMutation.isPending}
className="rounded-xl bg-dark-700 px-3 py-2 text-dark-400 transition-colors hover:bg-error-500/20 hover:text-error-400 disabled:opacity-50"
>
<TrashIcon />
</button>
)}
</div>
</div>
{/* Name */}
<div className="flex-1">
<label className="mb-2 block text-sm font-medium text-dark-300">
{t('admin.settings.projectName')}
</label>
{editingName ? (
<div className="flex gap-2">
<input
type="text"
value={newName}
onChange={(e) => setNewName(e.target.value)}
className="flex-1 rounded-xl border border-dark-600 bg-dark-700 px-4 py-2 text-dark-100 focus:border-accent-500 focus:outline-none"
maxLength={50}
/>
<button
onClick={() => updateBrandingMutation.mutate(newName)}
disabled={updateBrandingMutation.isPending}
className="rounded-xl bg-accent-500 px-4 py-2 text-white transition-colors hover:bg-accent-600 disabled:opacity-50"
>
<CheckIcon />
</button>
<button
onClick={() => setEditingName(false)}
className="rounded-xl bg-dark-700 px-4 py-2 text-dark-300 transition-colors hover:bg-dark-600"
>
<CloseIcon />
</button>
</div>
) : (
<div className="flex items-center gap-2">
<span className="text-lg text-dark-100">
{branding?.name || t('admin.settings.notSpecified')}
</span>
<button
onClick={() => {
setNewName(branding?.name ?? '');
setEditingName(true);
}}
className="rounded-lg p-1.5 text-dark-400 transition-colors hover:bg-dark-700 hover:text-dark-200"
>
<PencilIcon />
</button>
</div>
)}
</div>
</div>
</div>
{/* Animation & Fullscreen toggles */}
<div className="rounded-2xl border border-dark-700/50 bg-dark-800/50 p-6">
<h3 className="mb-4 text-lg font-semibold text-dark-100">
{t('admin.settings.interfaceOptions')}
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between rounded-xl bg-dark-700/30 p-4">
<div>
<span className="font-medium text-dark-100">
{t('admin.settings.animatedBackground')}
</span>
<p className="text-sm text-dark-400">{t('admin.settings.animatedBackgroundDesc')}</p>
</div>
<Toggle
checked={animationSettings?.enabled ?? true}
onChange={() => updateAnimationMutation.mutate(!(animationSettings?.enabled ?? true))}
disabled={updateAnimationMutation.isPending}
/>
</div>
<div className="flex items-center justify-between rounded-xl bg-dark-700/30 p-4">
<div>
<span className="font-medium text-dark-100">
{t('admin.settings.autoFullscreen')}
</span>
<p className="text-sm text-dark-400">{t('admin.settings.autoFullscreenDesc')}</p>
</div>
<Toggle
checked={fullscreenSettings?.enabled ?? false}
onChange={() =>
updateFullscreenMutation.mutate(!(fullscreenSettings?.enabled ?? false))
}
disabled={updateFullscreenMutation.isPending}
/>
</div>
<div className="flex items-center justify-between rounded-xl bg-dark-700/30 p-4">
<div>
<span className="font-medium text-dark-100">{t('admin.settings.emailAuth')}</span>
<p className="text-sm text-dark-400">{t('admin.settings.emailAuthDesc')}</p>
</div>
<Toggle
checked={emailAuthSettings?.enabled ?? true}
onChange={() => updateEmailAuthMutation.mutate(!(emailAuthSettings?.enabled ?? true))}
disabled={updateEmailAuthMutation.isPending}
/>
</div>
</div>
</div>
</div>
);
}