diff --git a/client/src/App.tsx b/client/src/App.tsx index e54f545..189baa6 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -17,7 +17,9 @@ import { useGpsTracking } from "@/hooks/useGpsTracking"; import { invalidateHomeQueriesThrottled } from "@/lib/throttledInvalidate"; import { GlobalErrorBoundary } from "@/components/GlobalErrorBoundary"; import { AppShell } from "@/components/AppShell"; -import { Loader2 } from "lucide-react"; +import { PageTransition } from "@/components/transitions/PageTransition"; +import { PageSkeleton } from "@/components/ui/skeleton"; +import { MotionConfig } from "framer-motion"; type LazyModule = { default: ComponentType }; @@ -108,12 +110,9 @@ const ShareTarget = safeLazy(() => import("@/pages/ShareTarget")); const QrHandler = safeLazy(() => import("@/pages/QrHandler")); const NotFound = safeLazy(() => import("@/pages/not-found")); +// Fallback подгрузки lazy-чанка: скелетон страницы вместо чистого экрана со спиннером function PageLoader() { - return ( -
- -
- ); + return ; } // Маппинг основных роутов на русские названия для document.title (порядок важен: более специфичные — раньше) @@ -164,6 +163,7 @@ function Router() { return ( // (Sidebar/навигация) остаётся смонтированным, fallback показывается внутри. }> + @@ -225,6 +225,7 @@ function Router() { return ( + ); @@ -342,9 +343,12 @@ function App() { {auth.isAuthenticated && } + {/* reducedMotion="user": при «уменьшить движение» в ОС transform-анимации отключаются */} + + diff --git a/client/src/components/transitions/PageTransition.tsx b/client/src/components/transitions/PageTransition.tsx new file mode 100644 index 0000000..8bca9d0 --- /dev/null +++ b/client/src/components/transitions/PageTransition.tsx @@ -0,0 +1,36 @@ +import { motion, useReducedMotion } from 'framer-motion'; +import { useLocation } from 'wouter'; +import { useSkinComponents } from '@/themes/ThemeProvider'; + +/** + * Переход между страницами: появление контента роута fade + лёгкий подъём + * (opacity 0→1, y 4→0) за 150 мс (шкала --duration-base / --ease-standard). + * key = location: при смене пути контент перемонтируется и проигрывает появление. + * Хром (Sidebar) снаружи — не анимируется. + * Отключается скином (components.motion.enabled === false) и при включённом + * «уменьшить движение» в ОС (useReducedMotion) — рендер без motion. + */ +export function PageTransition({ children }: { children: React.ReactNode }) { + const [location] = useLocation(); + const { motion: motionCfg } = useSkinComponents(); + const reducedMotion = useReducedMotion(); + + if (motionCfg.enabled === false || reducedMotion) { + return <>{children}; + } + + return ( + // flex-1/min-h-0 — чтобы страницы с h-full/flex-цепочками (Layout, чаты) не ломались + + {children} + + ); +} + +export default PageTransition; diff --git a/client/src/components/transitions/StaggerList.tsx b/client/src/components/transitions/StaggerList.tsx new file mode 100644 index 0000000..a6d70c0 --- /dev/null +++ b/client/src/components/transitions/StaggerList.tsx @@ -0,0 +1,51 @@ +import { motion, type Variants } from 'framer-motion'; +import { useSkinComponents } from '@/themes/ThemeProvider'; + +// Появление списка каскадом: каждый элемент — fade + подъём (opacity 0→1, y 4→0), +// задержка между элементами 25 мс. reducedMotion («уменьшить движение» в ОС) +// учитывается автоматически через MotionConfig reducedMotion="user" в App: +// transform-анимации отключаются, остаётся только мгновенный fade. +const listVariants: Variants = { + hidden: {}, + visible: { transition: { staggerChildren: 0.025 } }, +}; + +const itemVariants: Variants = { + hidden: { opacity: 0, y: 4 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.15, ease: [0.2, 0, 0, 1] } }, +}; + +// HTML-пропсы без drag/animation-хендлеров: у motion.div они имеют другую сигнатуру +interface StaggerProps extends Omit, + 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onDragEnter' | 'onDragLeave' | 'onDragOver' | 'onDrop' | + 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration' | 'onTransitionEnd'> { + children: React.ReactNode; +} + +/** Контейнер списка с каскадным появлением элементов (StaggerItem внутри). */ +export function StaggerList({ children, className, ...props }: StaggerProps) { + const { motion: motionCfg } = useSkinComponents(); + if (motionCfg.enabled === false) { + return
{children}
; + } + return ( + + {children} + + ); +} + +/** Элемент каскадного списка. Должен быть прямым потомком StaggerList. */ +export function StaggerItem({ children, className, ...props }: StaggerProps) { + const { motion: motionCfg } = useSkinComponents(); + if (motionCfg.enabled === false) { + return
{children}
; + } + return ( + + {children} + + ); +} + +export default StaggerList; diff --git a/client/src/components/ui/skeleton.tsx b/client/src/components/ui/skeleton.tsx index 01b8b6d..36a2068 100644 --- a/client/src/components/ui/skeleton.tsx +++ b/client/src/components/ui/skeleton.tsx @@ -12,4 +12,92 @@ function Skeleton({ ) } -export { Skeleton } +/** Скелетон списка: N строк «иконка + две строки текста» в рамке с разделителями. */ +function ListSkeleton({ + rows = 6, + withAvatar = false, + className, +}: { + rows?: number + withAvatar?: boolean + className?: string +}) { + return ( +
+ {Array.from({ length: rows }).map((_, i) => ( +
+ {withAvatar && } +
+ + +
+ +
+ ))} +
+ ) +} + +/** Скелетон страницы: шапка (заголовок + действие) и список строк под ней. */ +function PageSkeleton({ + rows = 6, + className, +}: { + rows?: number + className?: string +}) { + return ( +
+
+
+ + +
+ +
+
+ +
+
+ ) +} + +/** Скелетон карточки: заголовок и несколько строк контента. */ +function CardSkeleton({ + lines = 3, + className, +}: { + lines?: number + className?: string +}) { + return ( +
+ + {Array.from({ length: lines }).map((_, i) => ( + + ))} +
+ ) +} + +/** Скелетон формы: сетка «label + поле», как в форме создания/редактирования. */ +function FormSkeleton({ + fields = 6, + className, +}: { + fields?: number + className?: string +}) { + return ( +
+ {Array.from({ length: fields }).map((_, i) => ( +
+ + +
+ ))} +
+ ) +} + +export { Skeleton, PageSkeleton, ListSkeleton, CardSkeleton, FormSkeleton } diff --git a/client/src/pages/Automations.tsx b/client/src/pages/Automations.tsx index 203ce51..c6dd6cc 100644 --- a/client/src/pages/Automations.tsx +++ b/client/src/pages/Automations.tsx @@ -14,6 +14,7 @@ import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Zap, Plus, Pencil, Trash2, Play, ChevronRight, Circle, X, Terminal } from 'lucide-react'; +import { ListSkeleton } from '@/components/ui/skeleton'; type Automation = { @@ -364,7 +365,7 @@ export function AutomationsContent() { {isLoading ? ( -
Загрузка...
+ ) : items.length === 0 ? (
diff --git a/client/src/pages/Billing.tsx b/client/src/pages/Billing.tsx index 2922f89..bfd983b 100644 --- a/client/src/pages/Billing.tsx +++ b/client/src/pages/Billing.tsx @@ -21,6 +21,7 @@ import { ru } from "date-fns/locale"; import { apiRequest } from "@/lib/queryClient"; import { formatAmount } from "@/lib/format"; import { useToast } from "@/hooks/use-toast"; +import { ListSkeleton } from "@/components/ui/skeleton"; interface BillingSummary { balance: string; @@ -275,7 +276,7 @@ export default function Billing() { {loadingPayments ? ( -
Загрузка...
+
) : payments.length === 0 ? (
Платежей пока нет
) : ( @@ -320,7 +321,7 @@ export default function Billing() { {loadingTx ? ( -
Загрузка...
+
) : transactions.length === 0 ? (
Операций пока нет
) : ( diff --git a/client/src/pages/Chat.tsx b/client/src/pages/Chat.tsx index 5712fca..723aefc 100644 --- a/client/src/pages/Chat.tsx +++ b/client/src/pages/Chat.tsx @@ -21,6 +21,7 @@ import { HighlightMatch } from '@/components/chat/HighlightMatch'; import { TypingIndicator } from '@/components/chat/TypingIndicator'; import { formatDateHeader } from '@/components/chat/chat-utils'; import { useChatController } from '@/components/chat/useChatController'; +import { StaggerList, StaggerItem } from '@/components/transitions/StaggerList'; // ── Main Chat Page ──────────────────────────────────────────────────────────── @@ -237,7 +238,7 @@ export default function ChatPage() { Здесь появятся ваши диалоги. Начните новый чат кнопкой «+».
)} -
+ {conversations.map(conv => { const isActive = conv.id === activeConvId; const lastMsgText = conv.lastMessage?.isDeleted @@ -245,7 +246,7 @@ export default function ChatPage() { : (conv.lastMessage?.message?.trim() || (conv.lastMessage?.attachments?.length ? '📎 Вложение' : '')); return ( -
handleSelectConv(conv.id)} className={cn( @@ -313,10 +314,10 @@ export default function ChatPage() {

{lastMsgText}

)}
-
+ ); })} - + diff --git a/client/src/pages/CustomPageView.tsx b/client/src/pages/CustomPageView.tsx index 5680cd1..975cded 100644 --- a/client/src/pages/CustomPageView.tsx +++ b/client/src/pages/CustomPageView.tsx @@ -1,7 +1,8 @@ import { useParams, useLocation } from 'wouter'; import { useQuery } from '@tanstack/react-query'; import { JsComponentRenderer } from '@/components/tabs/JsComponentRenderer'; -import { Loader2, AlertTriangle, FileCode } from 'lucide-react'; +import { AlertTriangle, FileCode } from 'lucide-react'; +import { PageSkeleton } from '@/components/ui/skeleton'; import { useAuth } from '@/hooks/useAuth'; import { useEffect } from 'react'; import Layout from '@/components/Layout'; @@ -74,9 +75,7 @@ export default function CustomPageView() { if (pageLoading) { return ( -
- -
+
); } diff --git a/client/src/pages/FormEditor.tsx b/client/src/pages/FormEditor.tsx index bf77405..e8703ab 100644 --- a/client/src/pages/FormEditor.tsx +++ b/client/src/pages/FormEditor.tsx @@ -9,6 +9,7 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { ArrowLeft, Download, Pencil, Smartphone } from 'lucide-react'; +import { FormSkeleton, Skeleton } from '@/components/ui/skeleton'; import FormAccessTab from '@/components/FormAccessTab'; import { useFormEditorController } from '@/components/form-editor/useFormEditorController'; import { MainTab } from '@/components/form-editor/MainTab'; @@ -50,8 +51,13 @@ const FormEditor = () => { if (isLoading) { return ( -
-
Загрузка...
+
+
+ + +
+ +
); diff --git a/client/src/pages/Forms.tsx b/client/src/pages/Forms.tsx index 3982339..dbcc481 100644 --- a/client/src/pages/Forms.tsx +++ b/client/src/pages/Forms.tsx @@ -12,6 +12,7 @@ import { Plus, Search, Folder, X, FileText } from 'lucide-react'; import { BatchTaskQrDialog } from '@/components/BatchTaskQrDialog'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { FolderTreeWithItems } from '@/components/folders/FolderTreeWithItems'; +import { ListSkeleton } from '@/components/ui/skeleton'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -357,7 +358,7 @@ const Forms = () => {
{isLoading ? ( -
Загрузка форм...
+
) : mobileFilteredForms.length === 0 ? (
{searchQuery ? 'Нет результатов по заданным фильтрам' : 'Формы не найдены'} @@ -443,7 +444,7 @@ const Forms = () => { {/* Tree */}
{isLoading ? ( -
Загрузка форм...
+
) : forms.length === 0 && folders.length === 0 ? (
{searchQuery ? 'Нет результатов по заданным фильтрам' : 'Формы не найдены'} diff --git a/client/src/pages/MyProfilePage.tsx b/client/src/pages/MyProfilePage.tsx index 284c456..0c9500e 100644 --- a/client/src/pages/MyProfilePage.tsx +++ b/client/src/pages/MyProfilePage.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useLocation } from 'wouter'; -import { Loader2 } from 'lucide-react'; import Layout from '@/components/Layout'; +import { PageSkeleton } from '@/components/ui/skeleton'; import { useAuth } from '@/hooks/useAuth'; import UserProfilePage from '@/pages/UserProfilePage'; @@ -23,9 +23,7 @@ export default function MyProfilePage() { if (isLoading || !user) { return ( -
- -
+
); } diff --git a/client/src/pages/NotificationsPage.tsx b/client/src/pages/NotificationsPage.tsx index 772b159..873ffd1 100644 --- a/client/src/pages/NotificationsPage.tsx +++ b/client/src/pages/NotificationsPage.tsx @@ -11,7 +11,8 @@ import { useMarkAllNotificationsRead, } from '@/lib/notification-utils'; import { Button } from '@/components/ui/button'; -import { Bell, Check, CheckCheck, Loader2 } from 'lucide-react'; +import { Bell, Check, CheckCheck } from 'lucide-react'; +import { ListSkeleton } from '@/components/ui/skeleton'; import { cn } from '@/lib/utils'; // Сколько уведомлений показываем за раз (кнопка «Показать ещё») @@ -97,10 +98,7 @@ export default function NotificationsPage() {
{isLoading ? ( -
-
+ ) : filtered.length === 0 ? (
{isLoading ? ( -
+
+ + +
) : connections.length === 0 ? (
Нет подключений
) : ( @@ -552,7 +556,7 @@ function RagSettingsContent() { const isOllama = provider === 'ollama'; if (isLoading) { - return
; + return ; } return ( @@ -1404,7 +1408,7 @@ function LlmProvidersContent() { }; if (isLoading) { - return
; + return ; } const needsBaseUrl = form.providerType === 'openai_compatible' || form.providerType === 'ollama'; @@ -1701,8 +1705,8 @@ const Settings = () => { return (
-
- +
+
@@ -1844,8 +1848,9 @@ const Settings = () => { Эти ключи созданы ранее и продолжают работать с полным доступом.

{isLoadingApiKeys ? ( -
- +
+ +
) : apiKeys.length === 0 ? (
Нет ранее созданных ключей
diff --git a/client/src/pages/SuperAdminDashboard.tsx b/client/src/pages/SuperAdminDashboard.tsx index 24e6e69..a84e9d1 100644 --- a/client/src/pages/SuperAdminDashboard.tsx +++ b/client/src/pages/SuperAdminDashboard.tsx @@ -10,6 +10,7 @@ import { Label } from "@/components/ui/label"; import { Shield, LogOut, Building2, Users, FileText, Clock, ToggleLeft, ToggleRight, ChevronRight, RefreshCw, Wallet, AlertTriangle, UserPlus, Trash2 } from "lucide-react"; import { useState } from "react"; import { useToast } from "@/hooks/use-toast"; +import { ListSkeleton } from "@/components/ui/skeleton"; import { format } from "date-fns"; import { ru } from "date-fns/locale"; @@ -254,7 +255,7 @@ export default function SuperAdminDashboard() { {isLoading ? ( -
Загрузка...
+
) : isError ? (
Ошибка загрузки данных
) : orgs.length === 0 ? ( @@ -383,7 +384,7 @@ export default function SuperAdminDashboard() { {adminsLoading ? ( -
Загрузка...
+
) : adminsError ? (
Ошибка загрузки данных
) : admins.length === 0 ? ( diff --git a/client/src/pages/SuperAdminOrgDetail.tsx b/client/src/pages/SuperAdminOrgDetail.tsx index 8f422e6..47b5db6 100644 --- a/client/src/pages/SuperAdminOrgDetail.tsx +++ b/client/src/pages/SuperAdminOrgDetail.tsx @@ -12,6 +12,7 @@ import { } from "lucide-react"; import { useState } from "react"; import { useToast } from "@/hooks/use-toast"; +import { CardSkeleton } from "@/components/ui/skeleton"; import { format } from "date-fns"; import { ru } from "date-fns/locale"; import { formatAmount } from "@/lib/format"; @@ -203,7 +204,10 @@ export default function SuperAdminOrgDetail() {
{isLoading ? ( -
Загрузка...
+
+ + +
) : isError || !org ? (
Ошибка загрузки данных
) : ( diff --git a/client/src/pages/Tables.tsx b/client/src/pages/Tables.tsx index b1b0a01..486047f 100644 --- a/client/src/pages/Tables.tsx +++ b/client/src/pages/Tables.tsx @@ -35,6 +35,8 @@ import { import { Plus, Search, Table2, Trash2, MoreHorizontal, Folder, X, GripVertical, FileText } from 'lucide-react'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { FolderTreeWithItems } from '@/components/folders/FolderTreeWithItems'; +import { ListSkeleton } from '@/components/ui/skeleton'; +import { StaggerList, StaggerItem } from '@/components/transitions/StaggerList'; import { apiRequest, queryClient } from '@/lib/queryClient'; import { useToast } from '@/hooks/use-toast'; import type { DataTable } from '@shared/schema'; @@ -444,7 +446,7 @@ export default function Tables() {
{isLoading ? ( -
Загрузка справочников...
+
) : mobileFilteredTables.length === 0 ? (
@@ -460,10 +462,10 @@ export default function Tables() { )}
) : ( -
+ {mobileFilteredTables.map((table) => ( + + ))} -
+ )}
@@ -578,7 +581,7 @@ export default function Tables() {
{isLoading ? ( -
Загрузка справочников...
+
) : filteredTables.length === 0 ? (
diff --git a/client/src/pages/Tasks.tsx b/client/src/pages/Tasks.tsx index 1817506..a612b25 100644 --- a/client/src/pages/Tasks.tsx +++ b/client/src/pages/Tasks.tsx @@ -5,7 +5,9 @@ import Layout from '@/components/Layout'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; -import { Plus, ChevronRight, FileText, Loader2, CheckSquare, ArrowRight } from 'lucide-react'; +import { Plus, ChevronRight, FileText, CheckSquare, ArrowRight } from 'lucide-react'; +import { ListSkeleton, Skeleton } from '@/components/ui/skeleton'; +import { StaggerList, StaggerItem } from '@/components/transitions/StaggerList'; interface TaskItem { id: number; @@ -62,9 +64,9 @@ const Tasks = () => {

Задачи

-

- {isLoading ? 'Загрузка...' : `${tasks.length} задач в системе`} -

+
+ {isLoading ? : `${tasks.length} задач в системе`} +
{isLoading ? ( -
- -
+ ) : tasks.length === 0 ? ( setCreateDialogOpen(true)} /> ) : ( -
+ {tasks.map(task => { const form = formMap.get(task.formId); return ( -
setLocation(`/forms/${task.formId}/tasks/${task.id}`)} @@ -113,10 +113,10 @@ const Tasks = () => { {timeAgo(task.updatedAt)}
-
+ ); })} -
+ )}
diff --git a/client/src/pages/UserFieldsEditor.tsx b/client/src/pages/UserFieldsEditor.tsx index 4f3ac8c..69c872a 100644 --- a/client/src/pages/UserFieldsEditor.tsx +++ b/client/src/pages/UserFieldsEditor.tsx @@ -15,6 +15,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u import { useToast } from '@/hooks/use-toast'; import { FIELD_TYPE_DEFINITIONS } from '@shared/field-types'; import { ArrowLeft, Plus, Trash2, Save, Loader2, GripVertical } from 'lucide-react'; +import { FormSkeleton, Skeleton } from '@/components/ui/skeleton'; import { Link } from 'wouter'; import { DndContext, @@ -362,8 +363,12 @@ export default function UserFieldsEditor() { if (tabsLoading || fieldsLoading) { return ( -
- +
+
+ + +
+
); diff --git a/client/src/pages/UserProfilePage.tsx b/client/src/pages/UserProfilePage.tsx index 66221ee..f8c510f 100644 --- a/client/src/pages/UserProfilePage.tsx +++ b/client/src/pages/UserProfilePage.tsx @@ -14,6 +14,7 @@ import { isAdmin } from '@/lib/permissions'; import { apiRequest, queryClient } from '@/lib/queryClient'; import { cn } from '@/lib/utils'; import { ArrowLeft, Loader2, User as UserIcon, Mail, Phone, Shield, FileText, History, Calendar, Clock, CheckCircle2, Circle, UserCheck, Plus } from 'lucide-react'; +import { ListSkeleton, PageSkeleton } from '@/components/ui/skeleton'; import { Link } from 'wouter'; import { ProfileSystemTextRow, @@ -457,9 +458,7 @@ export default function UserProfilePage({ userId: userIdProp }: { userId?: numbe if (isLoading) { return ( -
- -
+
); } @@ -843,9 +842,7 @@ export default function UserProfilePage({ userId: userIdProp }: { userId?: numbe {/* Dossier tab */} {linkedTasksLoading ? ( -
- -
+ ) : ( )} @@ -854,9 +851,7 @@ export default function UserProfilePage({ userId: userIdProp }: { userId?: numbe {/* History tab */} {auditLogLoading ? ( -
- -
+ ) : ( )} diff --git a/client/src/pages/gps/AssetsTab.tsx b/client/src/pages/gps/AssetsTab.tsx index 8b3b20f..9b335a6 100644 --- a/client/src/pages/gps/AssetsTab.tsx +++ b/client/src/pages/gps/AssetsTab.tsx @@ -10,6 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Loader2, Pencil, Plus, Trash2, BellRing, Satellite } from 'lucide-react'; +import { ListSkeleton, Skeleton } from '@/components/ui/skeleton'; import { apiRequest } from '@/lib/queryClient'; import { useToast } from '@/hooks/use-toast'; import { invalidateGps, useGpsAssetSubscribers, useGpsAssets, useOrgUsers } from '@/hooks/useGps'; @@ -142,9 +143,7 @@ export function GpsAssetsTab() { {isLoading ? ( -
- -
+
) : assets.length === 0 ? (

Нет объектов. Добавьте машину или человека для отслеживания. @@ -416,8 +415,10 @@ function SubscribersDialog({ asset, onClose }: { asset: GpsAsset | null; onClose {isLoading ? ( -

- +
+ + +
) : (
diff --git a/client/src/pages/gps/GeozonesTab.tsx b/client/src/pages/gps/GeozonesTab.tsx index 23d69c9..be554bf 100644 --- a/client/src/pages/gps/GeozonesTab.tsx +++ b/client/src/pages/gps/GeozonesTab.tsx @@ -10,6 +10,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Loader2, Pencil, Trash2, Undo2, Check, Hexagon } from 'lucide-react'; +import { ListSkeleton } from '@/components/ui/skeleton'; import { apiRequest } from '@/lib/queryClient'; import { useToast } from '@/hooks/use-toast'; import { GpsMap, type GpsMapMarker, type GpsMapPolygon, type GpsMapPolyline } from '@/components/gps/GpsMap'; @@ -209,9 +210,7 @@ export function GpsGeozonesTab() { )}
{isLoading ? ( -
- -
+ ) : geozones.length === 0 ? (

Геозон пока нет. Нажмите «Нарисовать зону» и кликайте по карте. @@ -417,9 +416,7 @@ function GeozoneEventsJournal({ geozones }: { geozones: GpsGeozone[] }) {

{isLoading ? ( -
- -
+ ) : events.length === 0 ? (

Событий пока нет

) : ( diff --git a/client/src/pages/gps/GroupsTab.tsx b/client/src/pages/gps/GroupsTab.tsx index f33d1f2..d84457f 100644 --- a/client/src/pages/gps/GroupsTab.tsx +++ b/client/src/pages/gps/GroupsTab.tsx @@ -8,6 +8,7 @@ import { Badge } from '@/components/ui/badge'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Loader2, Pencil, Plus, Trash2 } from 'lucide-react'; +import { ListSkeleton } from '@/components/ui/skeleton'; import { apiRequest } from '@/lib/queryClient'; import { useToast } from '@/hooks/use-toast'; import { invalidateGps, useGpsAssets, useGpsGroups } from '@/hooks/useGps'; @@ -89,9 +90,7 @@ export function GpsGroupsTab() { {isLoading ? ( -
- -
+
) : groups.length === 0 ? (

Групп пока нет. Группы удобны для фильтрации объектов на карте. diff --git a/client/src/pages/gps/SettingsTab.tsx b/client/src/pages/gps/SettingsTab.tsx index 7e81baa..6372602 100644 --- a/client/src/pages/gps/SettingsTab.tsx +++ b/client/src/pages/gps/SettingsTab.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Loader2, MapPin, Save, SlidersHorizontal, X } from 'lucide-react'; +import { FormSkeleton } from '@/components/ui/skeleton'; import { apiRequest } from '@/lib/queryClient'; import { useToast } from '@/hooks/use-toast'; import { useAuth } from '@/hooks/useAuth'; @@ -103,9 +104,7 @@ export function GpsSettingsTab() { {isLoading ? ( -

- -
+ ) : ( <>
@@ -306,9 +305,7 @@ function DefaultCityCard({ {isLoading ? ( -
- -
+ ) : ( <> {hasCenter && ( diff --git a/client/src/pages/gps/index.tsx b/client/src/pages/gps/index.tsx index 89e5269..5db3cc5 100644 --- a/client/src/pages/gps/index.tsx +++ b/client/src/pages/gps/index.tsx @@ -1,8 +1,9 @@ import { useLocation } from 'wouter'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Card, CardContent } from '@/components/ui/card'; -import { Map, Truck, Hexagon, Route, Folder, BookOpen, Settings, ShieldAlert, Loader2 } from 'lucide-react'; +import { Map, Truck, Hexagon, Route, Folder, BookOpen, Settings, ShieldAlert } from 'lucide-react'; import Layout from '@/components/Layout'; +import { PageSkeleton } from '@/components/ui/skeleton'; import { useGpsConfig } from '@/hooks/useGps'; import { GpsMapTab } from './MapTab'; import { GpsAssetsTab } from './AssetsTab'; @@ -39,9 +40,7 @@ export default function GpsPage() { if (configLoading) { return ( -
- -
+
); }