feat: Linear-style UI redesign with improved mobile experience

Major changes:
- Redesign cabinet with Linear-style components and top navigation
- Replace detail modals with dedicated pages (users, broadcasts, email templates)
- Add email channel support for broadcasts
- Remove pull-to-refresh, improve drag-and-drop on touch devices
- Fix Telegram Mini App: fullscreen, back button, scroll restoration
- Unify admin pages color scheme to design system
- Mobile-first improvements: horizontal tabs for settings, better touch targets
This commit is contained in:
c0mrade
2026-01-31 22:06:36 +03:00
parent 929634aac4
commit b953ee0b8c
108 changed files with 11711 additions and 4542 deletions

View File

@@ -0,0 +1,33 @@
import { Link } from 'react-router-dom';
import { usePlatform } from '@/platform';
import { BackIcon } from './icons';
interface AdminBackButtonProps {
to?: string;
className?: string;
}
/**
* Back button for admin pages.
* Hidden in Telegram Mini App since native back button is used instead.
*/
export function AdminBackButton({ to = '/admin', className }: AdminBackButtonProps) {
const { platform } = usePlatform();
// In Telegram Mini App, we use native back button
if (platform === 'telegram') {
return null;
}
return (
<Link
to={to}
className={
className ||
'flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600'
}
>
<BackIcon />
</Link>
);
}

View File

@@ -0,0 +1,61 @@
import { motion } from 'framer-motion';
import type { Transition, Variants } from 'framer-motion';
import type { ReactNode } from 'react';
/**
* Centralized animation config for all admin pages.
* Change animations here to affect all admin pages at once.
*/
export const adminPageTransition: Transition = {
duration: 0.15,
ease: [0.16, 1, 0.3, 1] as const, // easeOutExpo
};
export const adminPageVariants: Variants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};
interface AdminLayoutProps {
children: ReactNode;
className?: string;
}
/**
* AdminLayout - wrapper for all admin pages with consistent animations.
* Use this to wrap admin page content for unified transitions.
*/
export function AdminLayout({ children, className }: AdminLayoutProps) {
return (
<motion.div
variants={adminPageVariants}
initial="initial"
animate="animate"
exit="exit"
transition={adminPageTransition}
className={className}
>
{children}
</motion.div>
);
}
/**
* Admin content animations for lists and cards.
* Use with staggered children.
*/
export const adminStaggerContainer: Variants = {
initial: {},
animate: {
transition: {
staggerChildren: 0.03,
},
},
};
export const adminStaggerItem: Variants = {
initial: { opacity: 0 },
animate: { opacity: 1 },
exit: { opacity: 0 },
};

View File

@@ -0,0 +1,78 @@
import { useTranslation } from 'react-i18next';
import { useRef, useEffect } from 'react';
import { MENU_SECTIONS } from './constants';
import { StarIcon } from './icons';
interface SettingsMobileTabsProps {
activeSection: string;
setActiveSection: (section: string) => void;
favoritesCount: number;
}
export function SettingsMobileTabs({
activeSection,
setActiveSection,
favoritesCount,
}: SettingsMobileTabsProps) {
const { t } = useTranslation();
const scrollRef = useRef<HTMLDivElement>(null);
const activeRef = useRef<HTMLButtonElement>(null);
// Scroll active tab into view
useEffect(() => {
if (activeRef.current && scrollRef.current) {
const container = scrollRef.current;
const activeEl = activeRef.current;
const containerRect = container.getBoundingClientRect();
const activeRect = activeEl.getBoundingClientRect();
// Check if active element is not fully visible
if (activeRect.left < containerRect.left || activeRect.right > containerRect.right) {
activeEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
}
}, [activeSection]);
// Flatten all items from all sections
const allItems = MENU_SECTIONS.flatMap((section) => section.items);
return (
<div
ref={scrollRef}
className="scrollbar-hide flex gap-2 overflow-x-auto px-3 py-3"
style={{ WebkitOverflowScrolling: 'touch' }}
>
{allItems.map((item) => {
const isActive = activeSection === item.id;
const hasIcon = item.iconType === 'star';
return (
<button
key={item.id}
ref={isActive ? activeRef : null}
onClick={() => setActiveSection(item.id)}
className={`flex shrink-0 items-center gap-2 rounded-xl px-4 py-2.5 text-sm font-medium transition-all ${
isActive
? 'bg-accent-500/15 text-accent-400 ring-1 ring-accent-500/30'
: 'bg-dark-800/50 text-dark-400 active:bg-dark-700'
}`}
>
{hasIcon && <StarIcon filled={isActive && item.id === 'favorites'} />}
<span className="whitespace-nowrap">{t(`admin.settings.${item.id}`)}</span>
{item.id === 'favorites' && favoritesCount > 0 && (
<span
className={`rounded-full px-1.5 py-0.5 text-xs ${
isActive
? 'bg-accent-500/20 text-accent-400'
: 'bg-warning-500/20 text-warning-400'
}`}
>
{favoritesCount}
</span>
)}
</button>
);
})}
</div>
);
}

View File

@@ -1,6 +1,5 @@
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { BackIcon, StarIcon, CloseIcon, MENU_SECTIONS } from './index';
import { AdminBackButton, StarIcon, CloseIcon, MENU_SECTIONS } from './index';
interface SettingsSidebarProps {
activeSection: string;
@@ -26,12 +25,7 @@ export function SettingsSidebar({
{/* Header */}
<div className="border-b border-dark-700/50 p-4">
<div className="flex items-center gap-3">
<Link
to="/admin"
className="rounded-xl bg-dark-800 p-2 transition-colors hover:bg-dark-700"
>
<BackIcon />
</Link>
<AdminBackButton className="rounded-xl bg-dark-800 p-2 transition-colors hover:bg-dark-700" />
<h1 className="text-lg font-bold text-dark-100">{t('admin.settings.title')}</h1>
<button
onClick={() => setMobileMenuOpen(false)}

View File

@@ -1,4 +1,6 @@
// Components
export * from './AdminBackButton';
export * from './AdminLayout';
export * from './icons';
export * from './Toggle';
export * from './SettingInput';
@@ -9,6 +11,7 @@ export * from './ThemeTab';
export * from './FavoritesTab';
export * from './SettingsTab';
export * from './SettingsSidebar';
export * from './SettingsMobileTabs';
export * from './SettingsSearch';
// Constants and utils