From 6287449521b94696c717f6c6f5cc023545faf1fc Mon Sep 17 00:00:00 2001 From: Bivekich Date: Sat, 19 Jul 2025 14:53:45 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20=D0=B8=20?= =?UTF-8?q?=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=B8=D1=81=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B0=D0=BC=D0=B8,=20=D0=B2=D0=BA=D0=BB?= =?UTF-8?q?=D1=8E=D1=87=D0=B0=D1=8F=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8E=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=20JWT,=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81=D1=8B=20=D0=B8?= =?UTF-8?q?=20=D0=BC=D1=83=D1=82=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D0=B8?= =?UTF-8?q?=20=D0=BE=D0=B1=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=B8=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B0=D1=82=D0=BE=D1=80=D0=B0=D1=85=20=D0=B8=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D1=8F=D0=BC=D0=B8.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D1=82=D0=BE=D0=BA=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=D0=BC=D0=B8=20=D0=B2=20Apollo=20Client.=20=D0=A3?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD=20=D0=B8=D0=BD=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D1=84=D0=B5=D0=B9=D1=81=20=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=D1=81?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 17 + package.json | 2 + prisma/schema.prisma | 13 + scripts/create-admin.mjs | 43 ++ src/app/admin/dashboard/page.tsx | 10 + src/app/admin/page.tsx | 14 + src/app/api/graphql/route.ts | 39 +- src/app/globals.css | 9 + src/components/admin/admin-dashboard.tsx | 44 ++ src/components/admin/admin-guard.tsx | 65 +++ src/components/admin/admin-login.tsx | 117 ++++ src/components/admin/admin-sidebar.tsx | 118 ++++ src/components/admin/ui-kit-section.tsx | 86 +++ src/components/admin/ui-kit/buttons-demo.tsx | 288 ++++++++++ src/components/admin/ui-kit/cards-demo.tsx | 413 +++++++++++++ src/components/admin/ui-kit/colors-demo.tsx | 367 ++++++++++++ src/components/admin/ui-kit/forms-demo.tsx | 527 +++++++++++++++++ src/components/admin/ui-kit/icons-demo.tsx | 543 ++++++++++++++++++ .../admin/ui-kit/typography-demo.tsx | 390 +++++++++++++ src/components/admin/users-section.tsx | 286 +++++++++ src/graphql/mutations.ts | 26 + src/graphql/queries.ts | 41 ++ src/graphql/resolvers.ts | 168 ++++++ src/graphql/typedefs.ts | 32 ++ src/hooks/useAdminAuth.ts | 255 ++++++++ src/lib/apollo-client.ts | 37 +- 26 files changed, 3931 insertions(+), 19 deletions(-) create mode 100644 scripts/create-admin.mjs create mode 100644 src/app/admin/dashboard/page.tsx create mode 100644 src/app/admin/page.tsx create mode 100644 src/components/admin/admin-dashboard.tsx create mode 100644 src/components/admin/admin-guard.tsx create mode 100644 src/components/admin/admin-login.tsx create mode 100644 src/components/admin/admin-sidebar.tsx create mode 100644 src/components/admin/ui-kit-section.tsx create mode 100644 src/components/admin/ui-kit/buttons-demo.tsx create mode 100644 src/components/admin/ui-kit/cards-demo.tsx create mode 100644 src/components/admin/ui-kit/colors-demo.tsx create mode 100644 src/components/admin/ui-kit/forms-demo.tsx create mode 100644 src/components/admin/ui-kit/icons-demo.tsx create mode 100644 src/components/admin/ui-kit/typography-demo.tsx create mode 100644 src/components/admin/users-section.tsx create mode 100644 src/hooks/useAdminAuth.ts diff --git a/package-lock.json b/package-lock.json index d9435dc..389ce34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,10 +26,12 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", + "@types/bcryptjs": "^2.4.6", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", "axios": "^1.10.0", + "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cors": "^2.8.5", @@ -4622,6 +4624,12 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -5781,6 +5789,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/body-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", diff --git a/package.json b/package.json index 726cc72..01a5d5b 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,12 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.12", + "@types/bcryptjs": "^2.4.6", "@types/cors": "^2.8.19", "@types/express": "^5.0.3", "@types/jsonwebtoken": "^9.0.10", "axios": "^1.10.0", + "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cors": "^2.8.5", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0db3750..4cae4fa 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,6 +22,19 @@ model User { @@map("users") } +model Admin { + id String @id @default(cuid()) + username String @unique + password String // Хеш пароля + email String? @unique + isActive Boolean @default(true) + lastLogin DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("admins") +} + model SmsCode { id String @id @default(cuid()) code String diff --git a/scripts/create-admin.mjs b/scripts/create-admin.mjs new file mode 100644 index 0000000..7065092 --- /dev/null +++ b/scripts/create-admin.mjs @@ -0,0 +1,43 @@ +import { PrismaClient } from '@prisma/client' +import bcrypt from 'bcryptjs' + +const prisma = new PrismaClient() + +async function createAdmin() { + try { + console.log('🔐 Создание администратора...') + + // Генерируем хеш пароля + const password = 'admin123' // Временный пароль + const hashedPassword = await bcrypt.hash(password, 12) + + // Создаем администратора + const admin = await prisma.admin.create({ + data: { + username: 'admin', + password: hashedPassword, + email: 'admin@sferav.com', + isActive: true + } + }) + + console.log('✅ Администратор создан:') + console.log(` Логин: ${admin.username}`) + console.log(` Пароль: ${password}`) + console.log(` Email: ${admin.email}`) + console.log(` ID: ${admin.id}`) + + console.log('\n⚠️ Обязательно смените пароль после первого входа!') + + } catch (error) { + if (error.code === 'P2002') { + console.log('❌ Администратор с таким логином уже существует') + } else { + console.error('❌ Ошибка создания администратора:', error) + } + } finally { + await prisma.$disconnect() + } +} + +createAdmin() \ No newline at end of file diff --git a/src/app/admin/dashboard/page.tsx b/src/app/admin/dashboard/page.tsx new file mode 100644 index 0000000..f0dd0ab --- /dev/null +++ b/src/app/admin/dashboard/page.tsx @@ -0,0 +1,10 @@ +import { AdminGuard } from "@/components/admin/admin-guard" +import { AdminDashboard } from "@/components/admin/admin-dashboard" + +export default function AdminDashboardPage() { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..9c69484 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,14 @@ +"use client" + +import { AdminLogin } from "@/components/admin/admin-login" +import { AdminGuard } from "@/components/admin/admin-guard" +import { redirect } from "next/navigation" + +export default function AdminPage() { + return ( + }> + {/* Если администратор авторизован, перенаправляем в админ-дашборд */} + {redirect('/admin/dashboard')} + + ) +} \ No newline at end of file diff --git a/src/app/api/graphql/route.ts b/src/app/api/graphql/route.ts index 7b2a2de..d05d64f 100644 --- a/src/app/api/graphql/route.ts +++ b/src/app/api/graphql/route.ts @@ -11,6 +11,10 @@ interface Context { id: string phone: string } + admin?: { + id: string + username: string + } } // Создаем Apollo Server @@ -31,27 +35,42 @@ const handler = startServerAndCreateNextHandler(server, { if (!token) { console.log('GraphQL Context - No token provided') - return { user: undefined } + return { user: undefined, admin: undefined } } try { // Верифицируем JWT токен const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { - userId: string - phone: string + userId?: string + phone?: string + adminId?: string + username?: string + type?: string } - console.log('GraphQL Context - Decoded user:', { id: decoded.userId, phone: decoded.phone }) - - return { - user: { - id: decoded.userId, - phone: decoded.phone + // Проверяем тип токена + if (decoded.type === 'admin' && decoded.adminId && decoded.username) { + console.log('GraphQL Context - Decoded admin:', { id: decoded.adminId, username: decoded.username }) + return { + admin: { + id: decoded.adminId, + username: decoded.username + } + } + } else if (decoded.userId && decoded.phone) { + console.log('GraphQL Context - Decoded user:', { id: decoded.userId, phone: decoded.phone }) + return { + user: { + id: decoded.userId, + phone: decoded.phone + } } } + + return { user: undefined, admin: undefined } } catch (error) { console.error('GraphQL Context - Invalid token:', error) - return { user: undefined } + return { user: undefined, admin: undefined } } } }) diff --git a/src/app/globals.css b/src/app/globals.css index 297d0a5..86bbb38 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -270,6 +270,15 @@ box-shadow: 0 8px 24px rgba(139, 69, 199, 0.15); } + .glass-sidebar { + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 8px 32px rgba(168, 85, 247, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.2); + } + /* Обеспечиваем курсор pointer для всех кликабельных элементов */ button, [role="button"], [data-state] { cursor: pointer; diff --git a/src/components/admin/admin-dashboard.tsx b/src/components/admin/admin-dashboard.tsx new file mode 100644 index 0000000..edcddfc --- /dev/null +++ b/src/components/admin/admin-dashboard.tsx @@ -0,0 +1,44 @@ +"use client" + +import { useState } from 'react' +import { AdminSidebar } from './admin-sidebar' +import { UsersSection } from './users-section' +import { UIKitSection } from './ui-kit-section' + +type AdminSection = 'users' | 'ui-kit' | 'settings' + +export function AdminDashboard() { + const [activeSection, setActiveSection] = useState('users') + + const renderContent = () => { + switch (activeSection) { + case 'users': + return + case 'ui-kit': + return + case 'settings': + return ( +
+

Настройки

+
+

Раздел настроек в разработке

+
+
+ ) + default: + return + } + } + + return ( +
+ +
+ {renderContent()} +
+
+ ) +} \ No newline at end of file diff --git a/src/components/admin/admin-guard.tsx b/src/components/admin/admin-guard.tsx new file mode 100644 index 0000000..0c9aa3d --- /dev/null +++ b/src/components/admin/admin-guard.tsx @@ -0,0 +1,65 @@ +"use client" + +import { useState, useEffect, useRef } from 'react' +import { useAdminAuth } from '@/hooks/useAdminAuth' +import { AdminLogin } from './admin-login' + +interface AdminGuardProps { + children: React.ReactNode + fallback?: React.ReactNode +} + +export function AdminGuard({ children, fallback }: AdminGuardProps) { + const { isAuthenticated, isLoading, checkAuth, admin } = useAdminAuth() + const [isChecking, setIsChecking] = useState(true) + const initRef = useRef(false) // Защита от повторных инициализаций + + useEffect(() => { + const initAuth = async () => { + if (initRef.current) { + console.log('AdminGuard - Already initialized, skipping') + return + } + + initRef.current = true + console.log('AdminGuard - Initializing admin auth check') + await checkAuth() + setIsChecking(false) + console.log('AdminGuard - Admin auth check completed, authenticated:', isAuthenticated, 'admin:', !!admin) + } + + initAuth() + }, [checkAuth, isAuthenticated, admin]) + + // Дополнительное логирование состояний + useEffect(() => { + console.log('AdminGuard - State update:', { + isChecking, + isLoading, + isAuthenticated, + hasAdmin: !!admin + }) + }, [isChecking, isLoading, isAuthenticated, admin]) + + // Показываем лоадер пока проверяем авторизацию + if (isChecking || isLoading) { + return ( +
+
+
+

Проверяем авторизацию администратора...

+
+
+ ) + } + + // Если не авторизован, показываем форму авторизации + if (!isAuthenticated) { + console.log('AdminGuard - Admin not authenticated, showing admin login') + return fallback || + } + + // Если авторизован, показываем защищенный контент + console.log('AdminGuard - Admin authenticated, showing admin panel') + return <>{children} +} \ No newline at end of file diff --git a/src/components/admin/admin-login.tsx b/src/components/admin/admin-login.tsx new file mode 100644 index 0000000..4cd69bd --- /dev/null +++ b/src/components/admin/admin-login.tsx @@ -0,0 +1,117 @@ +"use client" + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { useAdminAuth } from '@/hooks/useAdminAuth' +import { Eye, EyeOff, Shield, Loader2 } from 'lucide-react' +import { toast } from 'sonner' + +export function AdminLogin() { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + const { login, isLoading } = useAdminAuth() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!username.trim() || !password.trim()) { + toast.error('Заполните все поля') + return + } + + const result = await login(username.trim(), password) + + if (!result.success) { + toast.error(result.message) + } + } + + return ( +
+ + +
+ +
+ + Админ-панель + + + Вход в систему администрирования + +
+ + +
+
+ + setUsername(e.target.value)} + placeholder="Введите логин" + className="glass-input text-white placeholder:text-white/50" + disabled={isLoading} + autoComplete="username" + /> +
+ +
+ +
+ setPassword(e.target.value)} + placeholder="Введите пароль" + className="glass-input text-white placeholder:text-white/50 pr-10" + disabled={isLoading} + autoComplete="current-password" + /> + +
+
+ + +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/admin/admin-sidebar.tsx b/src/components/admin/admin-sidebar.tsx new file mode 100644 index 0000000..f6854e3 --- /dev/null +++ b/src/components/admin/admin-sidebar.tsx @@ -0,0 +1,118 @@ +"use client" + +import { useAdminAuth } from '@/hooks/useAdminAuth' +import { Button } from '@/components/ui/button' +import { Card } from '@/components/ui/card' +import { Avatar, AvatarFallback } from '@/components/ui/avatar' +import { + Settings, + LogOut, + Users, + Shield, + Palette +} from 'lucide-react' + +interface AdminSidebarProps { + activeSection: string + onSectionChange: (section: 'users' | 'ui-kit' | 'settings') => void +} + +export function AdminSidebar({ activeSection, onSectionChange }: AdminSidebarProps) { + const { admin, logout } = useAdminAuth() + + const getInitials = () => { + if (admin?.username) { + return admin.username.charAt(0).toUpperCase() + } + return 'A' + } + + const handleLogout = () => { + logout() + } + + return ( +
+ {/* Профиль администратора */} + +
+ + + {getInitials()} + + +
+

+ {admin?.username || 'Администратор'} +

+

+ Админ-панель +

+
+
+
+ + {/* Навигация */} + + + {/* Управление */} +
+ +
+ + {/* Логотип внизу */} +
+
+ + SferaV Admin +
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/admin/ui-kit-section.tsx b/src/components/admin/ui-kit-section.tsx new file mode 100644 index 0000000..6022520 --- /dev/null +++ b/src/components/admin/ui-kit-section.tsx @@ -0,0 +1,86 @@ +"use client" + +import { useState } from 'react' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { ButtonsDemo } from './ui-kit/buttons-demo' +import { FormsDemo } from './ui-kit/forms-demo' +import { CardsDemo } from './ui-kit/cards-demo' +import { TypographyDemo } from './ui-kit/typography-demo' +import { ColorsDemo } from './ui-kit/colors-demo' +import { IconsDemo } from './ui-kit/icons-demo' + +export function UIKitSection() { + return ( +
+
+

UI Kit

+

Полная коллекция компонентов дизайн-системы SferaV

+
+ + + + + Кнопки + + + Формы + + + Карточки + + + Типографика + + + Цвета + + + Иконки + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/src/components/admin/ui-kit/buttons-demo.tsx b/src/components/admin/ui-kit/buttons-demo.tsx new file mode 100644 index 0000000..f5e2828 --- /dev/null +++ b/src/components/admin/ui-kit/buttons-demo.tsx @@ -0,0 +1,288 @@ +"use client" + +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { + Play, + Download, + Heart, + Settings, + Trash2, + Plus, + Search, + Filter, + Loader2 +} from 'lucide-react' + +export function ButtonsDemo() { + return ( +
+ {/* Основные варианты */} + + + Основные варианты + + + {/* Default */} +
+

Default

+
+ + + + +
+
+ variant="default" +
+
+ + {/* Glass */} +
+

Glass (Основная)

+
+ + + + +
+
+ variant="glass" +
+
+ + {/* Glass Secondary */} +
+

Glass Secondary

+
+ + + + +
+
+ variant="glass-secondary" +
+
+ + {/* Secondary */} +
+

Secondary

+
+ + + + +
+
+ variant="secondary" +
+
+ + {/* Outline */} +
+

Outline

+
+ + + + +
+
+ variant="outline" +
+
+ + {/* Ghost */} +
+

Ghost

+
+ + + + +
+
+ variant="ghost" +
+
+ + {/* Destructive */} +
+

Destructive

+
+ + + + +
+
+ variant="destructive" +
+
+ + {/* Link */} +
+

Link

+
+ + + + +
+
+ variant="link" +
+
+
+
+ + {/* Кнопки с иконками */} + + + Кнопки с иконками + + + {/* Иконка слева */} +
+

Иконка слева

+
+ + + + +
+
+ + {/* Иконка справа */} +
+

Иконка справа

+
+ + +
+
+ + {/* Только иконка */} +
+

Только иконка

+
+ + + + +
+
+ + {/* Загрузка */} +
+

Состояние загрузки

+
+ + +
+
+
+
+ + {/* Размеры */} + + + Размеры + + +
+ + + +
+
+ size="sm" | "default" | "lg" | "icon" +
+
+
+ + {/* Примеры использования */} + + + Примеры использования + + + {/* Группа действий */} +
+

Группа действий

+
+ + + +
+
+ + {/* Навигация */} +
+

Навигация

+
+ + +
+
+ + {/* Формы */} +
+

Формы

+
+ + +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/admin/ui-kit/cards-demo.tsx b/src/components/admin/ui-kit/cards-demo.tsx new file mode 100644 index 0000000..beb63c7 --- /dev/null +++ b/src/components/admin/ui-kit/cards-demo.tsx @@ -0,0 +1,413 @@ +"use client" + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Progress } from '@/components/ui/progress' +import { + Heart, + Share2, + MoreHorizontal, + Star, + Clock, + MapPin, + Users, + TrendingUp, + Package, + Building, + Phone, + Calendar +} from 'lucide-react' + +export function CardsDemo() { + return ( +
+ {/* Базовые карточки */} + + + Базовые карточки + + Основные варианты карточек с Glass Morphism эффектом + + + + {/* Простая карточка */} +
+

Простая карточка

+ + + Заголовок карточки + + Описание содержимого карточки + + + +

+ Основное содержимое карточки. Здесь может быть любая информация. +

+
+
+
+ + {/* Карточка с действиями */} +
+

Карточка с действиями

+ + + Карточка с кнопками + + Пример карточки с различными действиями + + + +

+ Содержимое карточки с возможностями взаимодействия. +

+
+ + +
+
+
+
+ + {/* Интерактивная карточка */} +
+

Интерактивная карточка

+ + +
+ + Кликабельная карточка + + +
+ + Карточка с hover эффектами + +
+ +

+ При наведении карточка меняет внешний вид и становится более яркой. +

+
+
+
+
+
+ + {/* Карточки продуктов */} + + + Карточки продуктов + + +
+ {/* Карточка товара */} + +
+
+
+ + +
+
+ + Новинка + +
+
+ + + Название товара + +
+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+ (24 отзыва) +
+
+ +
+ 1 299 ₽ + 1 599 ₽ +
+ +
+
+ + {/* Карточка услуги */} + + +
+
+ + Фулфилмент услуги + + + Полный цикл обработки заказов + +
+ + Активно + +
+
+ +
+
+ + Москва, складской комплекс +
+
+ + 24/7 обработка заказов +
+
+ + до 1000 заказов/день +
+
+
+
+ Загрузка склада + 75% +
+ +
+ +
+
+ + {/* Карточка статистики */} + + +
+ + Продажи за месяц + + +
+
+ +
+
+ ₽2,847,532 +
+
+ + +12.5% + от прошлого месяца +
+
+
+
+ Заказов + 1,247 +
+
+ Средний чек + ₽2,284 +
+
+ Конверсия + 3.2% +
+
+
+
+
+
+
+ + {/* Карточки пользователей */} + + + Карточки пользователей + + +
+ {/* Профиль пользователя */} + + +
+ + + ИП + + +
+ + Иван Петров + + + Менеджер фулфилмент центра + +
+
+
+ +
+
+ + +7 (999) 123-45-67 +
+
+ + ООО "Логистик Центр" +
+
+ + Регистрация: 15.03.2024 +
+
+
+ + +
+
+
+ + {/* Компактная карточка организации */} + + +
+ + + ООО + + +
+
+

+ Торговая компания "Альфа" +

+ + Селлер + +
+

+ ИНН: 7708123456789 +

+
+ + Последний вход: 2 часа назад + +
+
+
+
+
+
+
+
+
+ + {/* Специальные контейнеры */} + + + Специальные контейнеры + + + {/* Alert карточка */} +
+

Уведомления

+
+ + +
+
+
+

Успешно

+

Данные сохранены успешно

+
+
+
+
+ + + +
+
+
+

Предупреждение

+

Проверьте корректность введенных данных

+
+
+
+
+ + + +
+
+
+

Ошибка

+

Не удалось выполнить операцию

+
+
+
+
+
+
+ + {/* Карточка с навигацией */} +
+

Навигационная карточка

+ + + Быстрые действия + + +
+ + + + +
+
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/admin/ui-kit/colors-demo.tsx b/src/components/admin/ui-kit/colors-demo.tsx new file mode 100644 index 0000000..f4ea222 --- /dev/null +++ b/src/components/admin/ui-kit/colors-demo.tsx @@ -0,0 +1,367 @@ +"use client" + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' + +export function ColorsDemo() { + return ( +
+ {/* Основная палитра */} + + + Основная цветовая палитра + + + {/* Primary цвета */} +
+

Primary (Основной)

+
+
+
+
primary
+
oklch(0.65 0.28 315)
+
+
+
+
primary-foreground
+
oklch(0.985 0 0)
+
+
+
+ + {/* Secondary цвета */} +
+

Secondary (Вторичный)

+
+
+
+
secondary
+
oklch(0.94 0.08 315)
+
+
+
+
secondary-foreground
+
oklch(0.205 0 0)
+
+
+
+ + {/* Accent цвета */} +
+

Accent (Акцентный)

+
+
+
+
accent
+
oklch(0.90 0.12 315)
+
+
+
+
accent-foreground
+
oklch(0.205 0 0)
+
+
+
+ + {/* Destructive цвета */} +
+

Destructive (Деструктивный)

+
+
+
+
destructive
+
oklch(0.577 0.245 27.325)
+
+
+
+
+
+ + {/* Нейтральные цвета */} + + + Нейтральные цвета + + +
+ {/* Background цвета */} +
+

Background

+
+
+
+
+
background
+
oklch(0.98 0.02 320)
+
+
+
+
+
+
foreground
+
oklch(0.145 0 0)
+
+
+
+
+ + {/* Muted цвета */} +
+

Muted

+
+
+
+
+
muted
+
oklch(0.94 0.05 315)
+
+
+
+
+
+
muted-foreground
+
oklch(0.556 0 0)
+
+
+
+
+
+ + {/* Border и input цвета */} +
+
+

Border & Input

+
+
+
+
+
border
+
oklch(0.90 0.08 315)
+
+
+
+
+
+
input
+
oklch(0.96 0.05 315)
+
+
+
+
+
+
ring
+
oklch(0.65 0.28 315)
+
+
+
+
+ +
+

Card & Popover

+
+
+
+
+
card
+
oklch(1 0 0)
+
+
+
+
+
+
popover
+
oklch(1 0 0)
+
+
+
+
+
+
+
+ + {/* Градиенты */} + + + Градиенты + + +
+ {/* Основные градиенты */} +
+

Основные градиенты

+
+
+
+
gradient-purple
+
Основной фиолетовый градиент
+
+ +
+
+
gradient-purple-light
+
Светлый фиолетовый градиент
+
+ +
+
+
bg-gradient-smooth
+
Темный градиент для фона
+
+
+
+ + {/* Текстовые градиенты */} +
+

Текстовые градиенты

+
+
+
+ Текст с ярким градиентом +
+
text-gradient-bright
+
+ +
+
+ Текст с обычным градиентом +
+
text-gradient
+
+ +
+
+ Текст с свечением +
+
glow-text
+
+
+
+
+
+
+ + {/* Прозрачности белого */} + + + Прозрачности белого цвета + + +
+ {[ + { opacity: '100', class: 'text-white', bg: 'bg-white' }, + { opacity: '90', class: 'text-white/90', bg: 'bg-white/90' }, + { opacity: '80', class: 'text-white/80', bg: 'bg-white/80' }, + { opacity: '70', class: 'text-white/70', bg: 'bg-white/70' }, + { opacity: '60', class: 'text-white/60', bg: 'bg-white/60' }, + { opacity: '50', class: 'text-white/50', bg: 'bg-white/50' }, + { opacity: '40', class: 'text-white/40', bg: 'bg-white/40' }, + { opacity: '30', class: 'text-white/30', bg: 'bg-white/30' }, + { opacity: '20', class: 'text-white/20', bg: 'bg-white/20' }, + { opacity: '10', class: 'text-white/10', bg: 'bg-white/10' }, + ].map((item) => ( +
+
+
+ {item.opacity}% +
+
+ {item.class} +
+
+ ))} +
+
+
+ + {/* Статусные цвета */} + + + Статусные цвета + + +
+
+

Успех

+
+
+
green-500
+
Успешная операция
+
+
+ +
+

Предупреждение

+
+
+
yellow-500
+
Требует внимания
+
+
+ +
+

Ошибка

+
+
+
red-500
+
Ошибка или проблема
+
+
+ +
+

Информация

+
+
+
blue-500
+
Информационное сообщение
+
+
+
+
+
+ + {/* Glass эффекты */} + + + Glass Morphism эффекты + + +
+
+

Основные Glass эффекты

+
+
+
glass-card
+
Основная стеклянная карточка
+
+ +
+
glass-input
+
Стеклянное поле ввода
+
+ +
+
glass-sidebar
+
Стеклянный сайдбар
+
+
+
+ +
+

Glass кнопки

+
+
+
glass-button
+
Основная стеклянная кнопка
+
+ +
+
glass-secondary
+
Вторичная стеклянная кнопка
+
+
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/admin/ui-kit/forms-demo.tsx b/src/components/admin/ui-kit/forms-demo.tsx new file mode 100644 index 0000000..ea6b067 --- /dev/null +++ b/src/components/admin/ui-kit/forms-demo.tsx @@ -0,0 +1,527 @@ +"use client" + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Checkbox } from '@/components/ui/checkbox' +import { Switch } from '@/components/ui/switch' +import { Slider } from '@/components/ui/slider' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { PhoneInput } from '@/components/ui/phone-input' +import { + Eye, + EyeOff, + Search, + Calendar, + Mail, + User, + Lock +} from 'lucide-react' + +export function FormsDemo() { + const [showPassword, setShowPassword] = useState(false) + const [switchValue, setSwitchValue] = useState(false) + const [sliderValue, setSliderValue] = useState([50]) + const [checkboxValue, setCheckboxValue] = useState(false) + + return ( +
+ {/* Базовые инпуты */} + + + Базовые поля ввода + + + {/* Обычный инпут */} +
+ + +
+ + {/* Инпут с иконкой */} +
+ +
+ + +
+
+ + {/* Пароль */} +
+ +
+ + + +
+
+ + {/* Email */} +
+ +
+ + +
+
+ + {/* Телефон */} +
+ + +
+ + {/* Дата */} +
+ +
+ + +
+
+ + {/* Число */} +
+ + +
+ + {/* Отключенное поле */} +
+ + +
+
+
+ + {/* Селекты и выпадающие списки */} + + + Селекты и выпадающие списки + + +
+ + +
+ +
+ + +
+
+
+ + {/* Чекбоксы и переключатели */} + + + Чекбоксы и переключатели + + + {/* Чекбоксы */} +
+

Чекбоксы

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Переключатели */} +
+

Переключатели (Switch)

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ + {/* Слайдеры */} + + + Слайдеры + + +
+ + +
+ +
+ + +
+
+
+ + {/* Состояния полей */} + + + Состояния полей + + + {/* Обычное состояние */} +
+ + +
+ + {/* Фокус */} +
+ + +
+ + {/* Ошибка */} +
+ + +

Это поле обязательно для заполнения

+
+ + {/* Успех */} +
+ + +

Данные сохранены успешно

+
+
+
+ + {/* Примеры форм */} + + + Примеры форм + + + {/* Форма входа */} +
+

Форма входа

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+ +
+
+ + {/* Форма регистрации */} +
+

Форма регистрации

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/admin/ui-kit/icons-demo.tsx b/src/components/admin/ui-kit/icons-demo.tsx new file mode 100644 index 0000000..0b82c0f --- /dev/null +++ b/src/components/admin/ui-kit/icons-demo.tsx @@ -0,0 +1,543 @@ +"use client" + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { + // Основные иконки + Home, Settings, User, Search, Bell, Heart, Share2, Download, Upload, + Edit, Trash2, Plus, Minus, X, Check, ChevronDown, ChevronUp, ChevronLeft, + ChevronRight, ArrowLeft, ArrowRight, ArrowUp, ArrowDown, + + // Бизнес иконки + Building, Package, Truck, Users, Phone, Mail, Calendar, Clock, MapPin, + TrendingUp, TrendingDown, BarChart, PieChart, DollarSign, CreditCard, + + // Интерфейс + Menu, MoreHorizontal, MoreVertical, Eye, EyeOff, Lock, Unlock, Key, + Shield, AlertCircle, AlertTriangle, Info, CheckCircle, XCircle, + + // Файлы и медиа + File, FileText, Image, Video, Music, Camera, Paperclip, Link2, + + // Действия + Play, Pause, Square, SkipForward, SkipBack, Volume2, VolumeX, + + // Социальные + MessageCircle, Send, ThumbsUp, ThumbsDown, Star, Bookmark, + + // Статус + Wifi, WifiOff, Battery, Zap, Globe, Monitor, Smartphone, Tablet +} from 'lucide-react' + +const iconSections = [ + { + title: 'Основные иконки', + icons: [ + { icon: Home, name: 'Home' }, + { icon: Settings, name: 'Settings' }, + { icon: User, name: 'User' }, + { icon: Search, name: 'Search' }, + { icon: Bell, name: 'Bell' }, + { icon: Heart, name: 'Heart' }, + { icon: Share2, name: 'Share2' }, + { icon: Download, name: 'Download' }, + { icon: Upload, name: 'Upload' }, + { icon: Edit, name: 'Edit' }, + { icon: Trash2, name: 'Trash2' }, + { icon: Plus, name: 'Plus' }, + { icon: Minus, name: 'Minus' }, + { icon: X, name: 'X' }, + { icon: Check, name: 'Check' }, + ] + }, + { + title: 'Навигация', + icons: [ + { icon: ChevronDown, name: 'ChevronDown' }, + { icon: ChevronUp, name: 'ChevronUp' }, + { icon: ChevronLeft, name: 'ChevronLeft' }, + { icon: ChevronRight, name: 'ChevronRight' }, + { icon: ArrowLeft, name: 'ArrowLeft' }, + { icon: ArrowRight, name: 'ArrowRight' }, + { icon: ArrowUp, name: 'ArrowUp' }, + { icon: ArrowDown, name: 'ArrowDown' }, + { icon: Menu, name: 'Menu' }, + { icon: MoreHorizontal, name: 'MoreHorizontal' }, + { icon: MoreVertical, name: 'MoreVertical' }, + ] + }, + { + title: 'Бизнес и коммерция', + icons: [ + { icon: Building, name: 'Building' }, + { icon: Package, name: 'Package' }, + { icon: Truck, name: 'Truck' }, + { icon: Users, name: 'Users' }, + { icon: Phone, name: 'Phone' }, + { icon: Mail, name: 'Mail' }, + { icon: Calendar, name: 'Calendar' }, + { icon: Clock, name: 'Clock' }, + { icon: MapPin, name: 'MapPin' }, + { icon: TrendingUp, name: 'TrendingUp' }, + { icon: TrendingDown, name: 'TrendingDown' }, + { icon: BarChart, name: 'BarChart' }, + { icon: PieChart, name: 'PieChart' }, + { icon: DollarSign, name: 'DollarSign' }, + { icon: CreditCard, name: 'CreditCard' }, + ] + }, + { + title: 'Безопасность и статус', + icons: [ + { icon: Eye, name: 'Eye' }, + { icon: EyeOff, name: 'EyeOff' }, + { icon: Lock, name: 'Lock' }, + { icon: Unlock, name: 'Unlock' }, + { icon: Key, name: 'Key' }, + { icon: Shield, name: 'Shield' }, + { icon: AlertCircle, name: 'AlertCircle' }, + { icon: AlertTriangle, name: 'AlertTriangle' }, + { icon: Info, name: 'Info' }, + { icon: CheckCircle, name: 'CheckCircle' }, + { icon: XCircle, name: 'XCircle' }, + ] + }, + { + title: 'Файлы и медиа', + icons: [ + { icon: File, name: 'File' }, + { icon: FileText, name: 'FileText' }, + { icon: Image, name: 'Image' }, + { icon: Video, name: 'Video' }, + { icon: Music, name: 'Music' }, + { icon: Camera, name: 'Camera' }, + { icon: Paperclip, name: 'Paperclip' }, + { icon: Link2, name: 'Link2' }, + ] + }, + { + title: 'Медиа контролы', + icons: [ + { icon: Play, name: 'Play' }, + { icon: Pause, name: 'Pause' }, + { icon: Square, name: 'Square' }, + { icon: SkipForward, name: 'SkipForward' }, + { icon: SkipBack, name: 'SkipBack' }, + { icon: Volume2, name: 'Volume2' }, + { icon: VolumeX, name: 'VolumeX' }, + ] + }, + { + title: 'Коммуникации', + icons: [ + { icon: MessageCircle, name: 'MessageCircle' }, + { icon: Send, name: 'Send' }, + { icon: ThumbsUp, name: 'ThumbsUp' }, + { icon: ThumbsDown, name: 'ThumbsDown' }, + { icon: Star, name: 'Star' }, + { icon: Bookmark, name: 'Bookmark' }, + ] + }, + { + title: 'Устройства и подключения', + icons: [ + { icon: Wifi, name: 'Wifi' }, + { icon: WifiOff, name: 'WifiOff' }, + { icon: Battery, name: 'Battery' }, + { icon: Zap, name: 'Zap' }, + { icon: Globe, name: 'Globe' }, + { icon: Monitor, name: 'Monitor' }, + { icon: Smartphone, name: 'Smartphone' }, + { icon: Tablet, name: 'Tablet' }, + ] + } +] + +export function IconsDemo() { + return ( +
+ {/* Размеры иконок */} + + + Размеры иконок + + +
+
+ +
h-3 w-3
+
+
+ +
h-4 w-4
+
+
+ +
h-5 w-5
+
+
+ +
h-6 w-6
+
+
+ +
h-8 w-8
+
+
+ +
h-10 w-10
+
+
+ +
h-12 w-12
+
+
+
+
+ + {/* Цвета иконок */} + + + Цвета иконок + + +
+
+ +
text-white
+
+
+ +
text-white/80
+
+
+ +
text-white/60
+
+
+ +
text-primary
+
+
+ +
text-red-400
+
+
+ +
text-green-400
+
+
+ +
text-blue-400
+
+
+ +
text-yellow-400
+
+
+ +
text-purple-400
+
+
+ +
text-pink-400
+
+
+
+
+ + {/* Заполненные иконки */} + + + Заполненные иконки + + +
+
+ +
Обычная
+
+
+ +
Заполненная
+
+
+ +
Обычная
+
+
+ +
Заполненная
+
+
+
+ Добавьте класс fill-[color] для заливки иконки +
+
+
+ + {/* Аватары */} + + + Аватары + + + {/* Размеры аватаров */} +
+

Размеры

+
+
+ + XS + +
h-6 w-6
+
+
+ + SM + +
h-8 w-8
+
+
+ + MD + +
h-10 w-10
+
+
+ + LG + +
h-12 w-12
+
+
+ + XL + +
h-16 w-16
+
+
+ + 2XL + +
h-20 w-20
+
+
+
+ + {/* Типы аватаров */} +
+

Типы

+
+
+ + ИП + +
Инициалы
+
+
+ + AB + +
Цветной фон
+
+
+ + + +
С иконкой
+
+
+ +
+ GR +
+
+
Градиент
+
+
+
+ + {/* Статусы */} +
+

Со статусом

+
+
+
+ + ON + +
+
+
Онлайн
+
+
+
+ + AW + +
+
+
Отошел
+
+
+
+ + OF + +
+
+
Не в сети
+
+
+
+
+
+ + {/* Бейджи */} + + + Бейджи + + +
+

Варианты

+
+ Default + Secondary + Outline + Destructive +
+
+ +
+

С иконками

+
+ + + Активно + + + + Ожидание + + + + Предупреждение + + + + Ошибка + +
+
+ +
+

Статусы организаций

+
+ + Фулфилмент + + + Селлер + + + Логистика + + + Оптовик + +
+
+
+
+ + {/* Коллекция иконок */} + {iconSections.map((section) => ( + + + {section.title} + + +
+ {section.icons.map((iconData) => { + const IconComponent = iconData.icon + return ( +
+ +
{iconData.name}
+
+ ) + })} +
+
+
+ ))} + + {/* Примеры использования */} + + + Примеры использования в интерфейсе + + + {/* Кнопки с иконками */} +
+

Кнопки с иконками

+
+ + + + + +
+
+ + {/* Элементы интерфейса */} +
+

Элементы интерфейса

+
+
+
+ + ИП + +
+
Иван Петров
+
+ + ООО "Логистик" +
+
+
+
+ + + Активен + + +
+
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/admin/ui-kit/typography-demo.tsx b/src/components/admin/ui-kit/typography-demo.tsx new file mode 100644 index 0000000..3da6d4c --- /dev/null +++ b/src/components/admin/ui-kit/typography-demo.tsx @@ -0,0 +1,390 @@ +"use client" + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' + +export function TypographyDemo() { + return ( +
+ {/* Заголовки */} + + + Заголовки + + +
+
+

+ Заголовок H1 +

+

+ text-4xl font-bold +

+
+ +
+

+ Заголовок H2 +

+

+ text-3xl font-bold +

+
+ +
+

+ Заголовок H3 +

+

+ text-2xl font-semibold +

+
+ +
+

+ Заголовок H4 +

+

+ text-xl font-semibold +

+
+ +
+
+ Заголовок H5 +
+

+ text-lg font-medium +

+
+ +
+
+ Заголовок H6 +
+

+ text-base font-medium +

+
+
+
+
+ + {/* Градиентные заголовки */} + + + Градиентные заголовки + + +
+

+ Яркий градиентный заголовок +

+

+ text-gradient-bright +

+
+ +
+

+ Обычный градиентный заголовок +

+

+ text-gradient +

+
+ +
+

+ Заголовок с свечением +

+

+ glow-text +

+
+
+
+ + {/* Основной текст */} + + + Основной текст + + +
+

+ Обычный текст. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + Ut enim ad minim veniam, quis nostrud exercitation. +

+

+ text-white text-base +

+
+ +
+

+ Текст с прозрачностью 90%. Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +

+

+ text-white/90 +

+
+ +
+

+ Текст с прозрачностью 70%. Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +

+

+ text-white/70 +

+
+ +
+

+ Вторичный текст с прозрачностью 60%. Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +

+

+ text-white/60 +

+
+
+
+ + {/* Размеры текста */} + + + Размеры текста + + +
+

Очень мелкий текст

+

text-xs

+
+ +
+

Мелкий текст

+

text-sm

+
+ +
+

Базовый текст

+

text-base

+
+ +
+

Большой текст

+

text-lg

+
+ +
+

Очень большой текст

+

text-xl

+
+ +
+

Огромный текст

+

text-2xl

+
+
+
+ + {/* Насыщенность шрифта */} + + + Насыщенность шрифта + + +
+

Легкий шрифт

+

font-light

+
+ +
+

Обычный шрифт

+

font-normal

+
+ +
+

Средний шрифт

+

font-medium

+
+ +
+

Полужирный шрифт

+

font-semibold

+
+ +
+

Жирный шрифт

+

font-bold

+
+
+
+ + {/* Специальные стили */} + + + Специальные стили текста + + +
+

+ Моноширинный шрифт для кода +

+

font-mono

+
+ +
+

+ Курсивный текст +

+

italic

+
+ +
+

+ Подчеркнутый текст +

+

underline

+
+ +
+

+ Зачеркнутый текст +

+

line-through

+
+ +
+

+ Заглавные буквы +

+

uppercase

+
+ +
+

+ СТРОЧНЫЕ БУКВЫ +

+

lowercase

+
+ +
+

+ первая буква заглавная +

+

capitalize

+
+
+
+ + {/* Выравнивание текста */} + + + Выравнивание текста + + +
+

+ Выравнивание по левому краю. Lorem ipsum dolor sit amet consectetur. +

+

text-left

+
+ +
+

+ Выравнивание по центру. Lorem ipsum dolor sit amet consectetur. +

+

text-center

+
+ +
+

+ Выравнивание по правому краю. Lorem ipsum dolor sit amet consectetur. +

+

text-right

+
+ +
+

+ Выравнивание по ширине. Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation. +

+

text-justify

+
+
+
+ + {/* Списки */} + + + Списки + + +
+

Маркированный список

+
    +
  • Первый пункт списка
  • +
  • Второй пункт списка
  • +
  • Третий пункт списка с длинным текстом, который может переноситься на несколько строк
  • +
  • Четвертый пункт
  • +
+
+ +
+

Нумерованный список

+
    +
  1. Первый шаг процесса
  2. +
  3. Второй шаг процесса
  4. +
  5. Третий шаг с подробным описанием того, что нужно сделать
  6. +
  7. Завершающий шаг
  8. +
+
+
+
+ + {/* Цитаты и код */} + + + Цитаты и код + + +
+

Цитата

+
+ "Дизайн - это не то, как вещь выглядит. Дизайн - это то, как вещь работает." +
+ — Стив Джобс +
+
+
+ +
+

Инлайн код

+

+ Используйте класс glass-card для создания карточек с эффектом стекла. +

+
+ +
+

Блок кода

+
+{`
+  
+    
+      Заголовок карточки
+    
+  
+  
+    Содержимое карточки
+  
+`}
+            
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/admin/users-section.tsx b/src/components/admin/users-section.tsx new file mode 100644 index 0000000..b061c83 --- /dev/null +++ b/src/components/admin/users-section.tsx @@ -0,0 +1,286 @@ +"use client" + +import { useState, useEffect } from 'react' +import { useQuery } from '@apollo/client' +import { gql } from '@apollo/client' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Badge } from '@/components/ui/badge' +import { Search, Phone, Building, Calendar, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react' + +// GraphQL запрос для получения пользователей +const ALL_USERS = gql` + query AllUsers($search: String, $limit: Int, $offset: Int) { + allUsers(search: $search, limit: $limit, offset: $offset) { + users { + id + phone + managerName + avatar + createdAt + updatedAt + organization { + id + inn + name + fullName + type + status + createdAt + } + } + total + hasMore + } + } +` + +interface User { + id: string + phone: string + managerName?: string + avatar?: string + createdAt: string + updatedAt: string + organization?: { + id: string + inn: string + name?: string + fullName?: string + type: string + status?: string + createdAt: string + } +} + +export function UsersSection() { + const [search, setSearch] = useState('') + const [currentPage, setCurrentPage] = useState(1) + const [searchQuery, setSearchQuery] = useState('') + const limit = 20 + + const { data, loading, error, refetch } = useQuery(ALL_USERS, { + variables: { + search: searchQuery || undefined, + limit, + offset: (currentPage - 1) * limit + }, + fetchPolicy: 'cache-and-network' + }) + + // Обновляем запрос при изменении поиска с дебаунсом + useEffect(() => { + const timer = setTimeout(() => { + setSearchQuery(search) + setCurrentPage(1) // Сбрасываем на первую страницу при поиске + }, 500) + + return () => clearTimeout(timer) + }, [search]) + + const users = data?.allUsers?.users || [] + const total = data?.allUsers?.total || 0 + const hasMore = data?.allUsers?.hasMore || false + const totalPages = Math.ceil(total / limit) + + const getOrganizationTypeBadge = (type: string) => { + const typeMap = { + FULFILLMENT: { label: 'Фулфилмент', variant: 'default' as const }, + SELLER: { label: 'Селлер', variant: 'secondary' as const }, + LOGIST: { label: 'Логистика', variant: 'outline' as const }, + WHOLESALE: { label: 'Оптовик', variant: 'destructive' as const } + } + return typeMap[type as keyof typeof typeMap] || { label: type, variant: 'outline' as const } + } + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('ru-RU', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }) + } + + const getInitials = (name?: string, phone?: string) => { + if (name) { + return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2) + } + if (phone) { + return phone.slice(-2) + } + return 'У' + } + + const handlePrevPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1) + } + } + + const handleNextPage = () => { + if (currentPage < totalPages) { + setCurrentPage(currentPage + 1) + } + } + + if (error) { + return ( +
+
+

Ошибка загрузки пользователей: {error.message}

+ +
+
+ ) + } + + return ( +
+
+

Пользователи

+

Управление пользователями системы

+
+ + {/* Поиск и статистика */} +
+
+ + setSearch(e.target.value)} + className="pl-10 glass-input text-white placeholder:text-white/50" + /> +
+
+ Всего: {total} + {searchQuery && Найдено: {users.length}} +
+
+ + {/* Список пользователей */} + {loading ? ( +
+ +

Загрузка пользователей...

+
+ ) : users.length === 0 ? ( +
+

+ {searchQuery ? 'Пользователи не найдены' : 'Пользователи отсутствуют'} +

+
+ ) : ( +
+ {users.map((user: User) => ( + + +
+ {/* Аватар */} + + + + {getInitials(user.managerName, user.phone)} + + + + {/* Основная информация */} +
+
+
+

+ {user.managerName || 'Без имени'} +

+
+ + {user.phone} +
+
+
+
+ + {formatDate(user.createdAt)} +
+
+
+ + {/* Организация */} + {user.organization && ( +
+
+
+
+ + + {user.organization.name || user.organization.fullName || 'Без названия'} + + + {getOrganizationTypeBadge(user.organization.type).label} + +
+

+ ИНН: {user.organization.inn} +

+ {user.organization.status && ( +

+ Статус: {user.organization.status} +

+ )} +
+
+

+ Зарегистрировано: {formatDate(user.organization.createdAt)} +

+
+
+
+ )} +
+
+
+
+ ))} +
+ )} + + {/* Пагинация */} + {totalPages > 1 && ( +
+
+ Страница {currentPage} из {totalPages} +
+
+ + +
+
+ )} +
+ ) +} \ No newline at end of file diff --git a/src/graphql/mutations.ts b/src/graphql/mutations.ts index bded43f..cabd456 100644 --- a/src/graphql/mutations.ts +++ b/src/graphql/mutations.ts @@ -991,4 +991,30 @@ export const UPDATE_EMPLOYEE_SCHEDULE = gql` mutation UpdateEmployeeSchedule($input: UpdateScheduleInput!) { updateEmployeeSchedule(input: $input) } +` + +// Админ мутации +export const ADMIN_LOGIN = gql` + mutation AdminLogin($username: String!, $password: String!) { + adminLogin(username: $username, password: $password) { + success + message + token + admin { + id + username + email + isActive + lastLogin + createdAt + updatedAt + } + } + } +` + +export const ADMIN_LOGOUT = gql` + mutation AdminLogout { + adminLogout + } ` \ No newline at end of file diff --git a/src/graphql/queries.ts b/src/graphql/queries.ts index 061f6bc..3fdcf49 100644 --- a/src/graphql/queries.ts +++ b/src/graphql/queries.ts @@ -567,4 +567,45 @@ export const GET_EMPLOYEE_SCHEDULE = gql` } } } +` + +// Админ запросы +export const ADMIN_ME = gql` + query AdminMe { + adminMe { + id + username + email + isActive + lastLogin + createdAt + updatedAt + } + } +` + +export const ALL_USERS = gql` + query AllUsers($search: String, $limit: Int, $offset: Int) { + allUsers(search: $search, limit: $limit, offset: $offset) { + users { + id + phone + managerName + avatar + createdAt + updatedAt + organization { + id + inn + name + fullName + type + status + createdAt + } + } + total + hasMore + } + } ` \ No newline at end of file diff --git a/src/graphql/resolvers.ts b/src/graphql/resolvers.ts index f95eb27..a44b54f 100644 --- a/src/graphql/resolvers.ts +++ b/src/graphql/resolvers.ts @@ -1,4 +1,5 @@ import jwt from 'jsonwebtoken' +import bcrypt from 'bcryptjs' import { GraphQLError } from 'graphql' import { GraphQLScalarType, Kind } from 'graphql' import { prisma } from '@/lib/prisma' @@ -18,6 +19,10 @@ interface Context { id: string phone: string } + admin?: { + id: string + username: string + } } interface CreateEmployeeInput { @@ -3736,4 +3741,167 @@ const logisticsMutations = { resolvers.Mutation = { ...resolvers.Mutation, ...logisticsMutations +} + +// Админ резолверы +const adminQueries = { + adminMe: async (_: unknown, __: unknown, context: Context) => { + if (!context.admin) { + throw new GraphQLError('Требуется авторизация администратора', { + extensions: { code: 'UNAUTHENTICATED' } + }) + } + + const admin = await prisma.admin.findUnique({ + where: { id: context.admin.id } + }) + + if (!admin) { + throw new GraphQLError('Администратор не найден') + } + + return admin + }, + + allUsers: async (_: unknown, args: { search?: string; limit?: number; offset?: number }, context: Context) => { + if (!context.admin) { + throw new GraphQLError('Требуется авторизация администратора', { + extensions: { code: 'UNAUTHENTICATED' } + }) + } + + const limit = args.limit || 50 + const offset = args.offset || 0 + + // Строим условие поиска + const whereCondition: Prisma.UserWhereInput = args.search + ? { + OR: [ + { phone: { contains: args.search, mode: 'insensitive' } }, + { managerName: { contains: args.search, mode: 'insensitive' } }, + { + organization: { + OR: [ + { name: { contains: args.search, mode: 'insensitive' } }, + { fullName: { contains: args.search, mode: 'insensitive' } }, + { inn: { contains: args.search, mode: 'insensitive' } } + ] + } + } + ] + } + : {} + + // Получаем пользователей с пагинацией + const [users, total] = await Promise.all([ + prisma.user.findMany({ + where: whereCondition, + include: { + organization: true + }, + take: limit, + skip: offset, + orderBy: { createdAt: 'desc' } + }), + prisma.user.count({ + where: whereCondition + }) + ]) + + return { + users, + total, + hasMore: offset + limit < total + } + } +} + +const adminMutations = { + adminLogin: async (_: unknown, args: { username: string; password: string }) => { + try { + // Найти администратора + const admin = await prisma.admin.findUnique({ + where: { username: args.username } + }) + + if (!admin) { + return { + success: false, + message: 'Неверные учетные данные' + } + } + + // Проверить активность + if (!admin.isActive) { + return { + success: false, + message: 'Аккаунт заблокирован' + } + } + + // Проверить пароль + const isPasswordValid = await bcrypt.compare(args.password, admin.password) + + if (!isPasswordValid) { + return { + success: false, + message: 'Неверные учетные данные' + } + } + + // Обновить время последнего входа + await prisma.admin.update({ + where: { id: admin.id }, + data: { lastLogin: new Date() } + }) + + // Создать токен + const token = jwt.sign( + { + adminId: admin.id, + username: admin.username, + type: 'admin' + }, + process.env.JWT_SECRET!, + { expiresIn: '24h' } + ) + + return { + success: true, + message: 'Успешная авторизация', + token, + admin: { + ...admin, + password: undefined // Не возвращаем пароль + } + } + } catch (error) { + console.error('Admin login error:', error) + return { + success: false, + message: 'Ошибка авторизации' + } + } + }, + + adminLogout: async (_: unknown, __: unknown, context: Context) => { + if (!context.admin) { + throw new GraphQLError('Требуется авторизация администратора', { + extensions: { code: 'UNAUTHENTICATED' } + }) + } + + return true + } +} + +// Добавляем админ запросы и мутации к основным резолверам +resolvers.Query = { + ...resolvers.Query, + ...adminQueries +} + +resolvers.Mutation = { + ...resolvers.Mutation, + ...adminMutations } \ No newline at end of file diff --git a/src/graphql/typedefs.ts b/src/graphql/typedefs.ts index 71be7d2..2deaa16 100644 --- a/src/graphql/typedefs.ts +++ b/src/graphql/typedefs.ts @@ -53,6 +53,10 @@ export const typeDefs = gql` # Табель сотрудника за месяц employeeSchedule(employeeId: ID!, year: Int!, month: Int!): [EmployeeSchedule!]! + + # Админ запросы + adminMe: Admin + allUsers(search: String, limit: Int, offset: Int): UsersResponse! } type Mutation { @@ -128,6 +132,10 @@ export const typeDefs = gql` updateEmployee(id: ID!, input: UpdateEmployeeInput!): EmployeeResponse! deleteEmployee(id: ID!): Boolean! updateEmployeeSchedule(input: UpdateScheduleInput!): Boolean! + + # Админ мутации + adminLogin(username: String!, password: String!): AdminAuthResponse! + adminLogout: Boolean! } # Типы данных @@ -644,4 +652,28 @@ export const typeDefs = gql` # JSON скаляр scalar JSON + + # Админ типы + type Admin { + id: ID! + username: String! + email: String + isActive: Boolean! + lastLogin: String + createdAt: String! + updatedAt: String! + } + + type AdminAuthResponse { + success: Boolean! + message: String! + token: String + admin: Admin + } + + type UsersResponse { + users: [User!]! + total: Int! + hasMore: Boolean! + } ` \ No newline at end of file diff --git a/src/hooks/useAdminAuth.ts b/src/hooks/useAdminAuth.ts new file mode 100644 index 0000000..5fb2365 --- /dev/null +++ b/src/hooks/useAdminAuth.ts @@ -0,0 +1,255 @@ +import { useMutation, useQuery } from '@apollo/client' +import { useState, useEffect } from 'react' +import { gql } from '@apollo/client' +import { apolloClient } from '@/lib/apollo-client' + +// GraphQL мутации и запросы +const ADMIN_LOGIN = gql` + mutation AdminLogin($username: String!, $password: String!) { + adminLogin(username: $username, password: $password) { + success + message + token + admin { + id + username + email + isActive + lastLogin + createdAt + updatedAt + } + } + } +` + +const ADMIN_ME = gql` + query AdminMe { + adminMe { + id + username + email + isActive + lastLogin + createdAt + updatedAt + } + } +` + +const ADMIN_LOGOUT = gql` + mutation AdminLogout { + adminLogout + } +` + +interface Admin { + id: string + username: string + email?: string + isActive: boolean + lastLogin?: string + createdAt: string + updatedAt: string +} + +interface UseAdminAuthReturn { + login: (username: string, password: string) => Promise<{ success: boolean; message: string; admin?: Admin }> + logout: () => void + admin: Admin | null + isAuthenticated: boolean + isLoading: boolean + checkAuth: () => Promise +} + +// Утилиты для работы с токенами администратора +const ADMIN_TOKEN_KEY = 'adminAuthToken' +const ADMIN_DATA_KEY = 'adminData' + +const setAdminToken = (token: string) => { + if (typeof window !== 'undefined') { + localStorage.setItem(ADMIN_TOKEN_KEY, token) + } +} + +const getAdminToken = (): string | null => { + if (typeof window !== 'undefined') { + return localStorage.getItem(ADMIN_TOKEN_KEY) + } + return null +} + +const removeAdminToken = () => { + if (typeof window !== 'undefined') { + localStorage.removeItem(ADMIN_TOKEN_KEY) + localStorage.removeItem(ADMIN_DATA_KEY) + } +} + +const setAdminData = (admin: Admin) => { + if (typeof window !== 'undefined') { + localStorage.setItem(ADMIN_DATA_KEY, JSON.stringify(admin)) + } +} + +export const useAdminAuth = (): UseAdminAuthReturn => { + const [isLoading, setIsLoading] = useState(false) + const [admin, setAdmin] = useState(null) + const [isAuthenticated, setIsAuthenticated] = useState(() => { + return !!getAdminToken() + }) + const [isCheckingAuth, setIsCheckingAuth] = useState(false) + + const [adminLoginMutation] = useMutation(ADMIN_LOGIN) + const [adminLogoutMutation] = useMutation(ADMIN_LOGOUT) + + // Проверка авторизации администратора + const checkAuth = async () => { + if (isCheckingAuth) { + return + } + + const token = getAdminToken() + console.log('useAdminAuth - checkAuth called, token exists:', !!token) + + if (!token) { + setIsAuthenticated(false) + setAdmin(null) + setIsCheckingAuth(false) + return + } + + setIsCheckingAuth(true) + + try { + console.log('useAdminAuth - Making ADMIN_ME query') + const { data } = await apolloClient.query({ + query: ADMIN_ME, + errorPolicy: 'all', + fetchPolicy: 'network-only', + context: { + headers: { + authorization: `Bearer ${token}` + } + } + }) + + console.log('useAdminAuth - ADMIN_ME response:', !!data?.adminMe) + if (data?.adminMe) { + setAdmin(data.adminMe) + setIsAuthenticated(true) + setAdminData(data.adminMe) + console.log('useAdminAuth - Admin authenticated:', data.adminMe.username) + } else { + setIsAuthenticated(false) + setAdmin(null) + } + } catch (error: unknown) { + console.log('useAdminAuth - ADMIN_ME error:', error) + if ((error as { graphQLErrors?: Array<{ extensions?: { code?: string } }> })?.graphQLErrors?.some((e) => e.extensions?.code === 'UNAUTHENTICATED')) { + logout() + } else { + setIsAuthenticated(false) + setAdmin(null) + } + } finally { + setIsCheckingAuth(false) + } + } + + // Проверяем авторизацию при загрузке компонента + useEffect(() => { + const token = getAdminToken() + console.log('useAdminAuth - useEffect init, token exists:', !!token, 'admin exists:', !!admin, 'isChecking:', isCheckingAuth) + + if (token && !admin && !isCheckingAuth) { + console.log('useAdminAuth - Running checkAuth because token exists but no admin data') + checkAuth() + } else if (!token) { + console.log('useAdminAuth - No token, setting unauthenticated state') + setIsAuthenticated(false) + setAdmin(null) + } + }, []) // eslint-disable-line react-hooks/exhaustive-deps + + const login = async (username: string, password: string) => { + try { + setIsLoading(true) + console.log('useAdminAuth - Starting adminLogin mutation') + + const { data } = await adminLoginMutation({ + variables: { username, password } + }) + + console.log('useAdminAuth - GraphQL response data:', data) + const result = data.adminLogin + console.log('useAdminAuth - Admin login result:', { + success: result.success, + hasToken: !!result.token, + hasAdmin: !!result.admin, + message: result.message + }) + + if (result.success && result.token && result.admin) { + // Сохраняем токен и данные администратора + console.log('useAdminAuth - Saving admin token') + setAdminToken(result.token) + setAdminData(result.admin) + + // Обновляем состояние хука + setAdmin(result.admin) + setIsAuthenticated(true) + console.log('useAdminAuth - State updated: admin set, isAuthenticated=true') + + // Принудительно обновляем Apollo Client + apolloClient.resetStore() + + // Перенаправляем в админ-дашборд + if (typeof window !== 'undefined') { + window.location.href = '/admin/dashboard' + } + + return { + success: true, + message: result.message, + admin: result.admin + } + } + + return { + success: false, + message: result.message + } + } catch (error) { + console.error('Error during admin login:', error) + return { + success: false, + message: 'Ошибка при авторизации' + } + } finally { + setIsLoading(false) + } + } + + const logout = () => { + console.log('useAdminAuth - Logging out') + removeAdminToken() + setAdmin(null) + setIsAuthenticated(false) + apolloClient.resetStore() + + // Перенаправляем на страницу входа администратора + if (typeof window !== 'undefined') { + window.location.href = '/admin' + } + } + + return { + login, + logout, + admin, + isAuthenticated, + isLoading, + checkAuth + } +} \ No newline at end of file diff --git a/src/lib/apollo-client.ts b/src/lib/apollo-client.ts index db91026..31677bb 100644 --- a/src/lib/apollo-client.ts +++ b/src/lib/apollo-client.ts @@ -9,10 +9,19 @@ const httpLink = createHttpLink({ // Auth Link для добавления JWT токена в заголовки const authLink = setContext((operation, { headers }) => { - // Получаем токен из localStorage каждый раз - const token = typeof window !== 'undefined' ? localStorage.getItem('authToken') : null + if (typeof window === 'undefined') { + return { headers } + } - console.log(`Apollo Client - Operation: ${operation.operationName}, Token:`, token ? `${token.substring(0, 20)}...` : 'No token') + // Проверяем токены администратора и пользователя + const adminToken = localStorage.getItem('adminAuthToken') + const userToken = localStorage.getItem('authToken') + + // Приоритет у админского токена + const token = adminToken || userToken + const tokenType = adminToken ? 'admin' : 'user' + + console.log(`Apollo Client - Operation: ${operation.operationName}, Token type: ${tokenType}, Token:`, token ? `${token.substring(0, 20)}...` : 'No token') const authHeaders = { ...headers, @@ -35,14 +44,24 @@ const errorLink = onError(({ graphQLErrors, networkError }) => { ) // Если токен недействителен, очищаем localStorage и перенаправляем на авторизацию - // Но не делаем редирект если пользователь уже на главной странице (в процессе авторизации) if (extensions?.code === 'UNAUTHENTICATED') { if (typeof window !== 'undefined') { - localStorage.removeItem('authToken') - localStorage.removeItem('userData') - // Перенаправляем на страницу авторизации только если не находимся на ней - if (window.location.pathname !== '/') { - window.location.href = '/' + const isAdminPath = window.location.pathname.startsWith('/admin') + + if (isAdminPath) { + // Для админских страниц очищаем админские токены + localStorage.removeItem('adminAuthToken') + localStorage.removeItem('adminData') + if (window.location.pathname !== '/admin') { + window.location.href = '/admin' + } + } else { + // Для пользовательских страниц очищаем пользовательские токены + localStorage.removeItem('authToken') + localStorage.removeItem('userData') + if (window.location.pathname !== '/') { + window.location.href = '/' + } } } }