diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..9d6d63f --- /dev/null +++ b/.env.production @@ -0,0 +1,3 @@ +#VITE_API_BASE_URL=https://admin-api.eventhub.local +VITE_WS_URL=wss://admin-ws.eventhub.local +VITE_APP_TITLE=EventHub Admin \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..700e660 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:alpine +COPY dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bc54591 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +.PHONY: install dev build docker-build docker-run docker-stop docker-restart clean + +IMAGE_NAME = eventhub-admin-ui +CONTAINER_NAME = admin-ui +NETWORK_NAME = eventhub_network + +# Установить зависимости +install: + npm install + +# Запустить dev‑сервер +dev: + npm run dev + +# Собрать production‑бандл +build: + npm run build + +# Собрать Docker‑образ +docker-build: + docker build -t $(IMAGE_NAME) . + +# Запустить контейнер (внутри сети eventhub_network) +docker-run: + docker run -d --name $(CONTAINER_NAME) --network $(NETWORK_NAME) --restart unless-stopped $(IMAGE_NAME) + +# Остановить и удалить контейнер +docker-stop: + docker stop $(CONTAINER_NAME) || true + docker rm $(CONTAINER_NAME) || true + +# Перезапустить: стоп → билд → ран +docker-restart: docker-stop docker-build docker-run + +# Удалить сгенерированные файлы +clean: + rm -rf node_modules dist .vite + npm cache clean --force \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..32afb87 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,29 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /v1/ { + proxy_pass http://eventhub:8445; # admin-api слушает HTTP на порту 8445 + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /admin/ws { + proxy_pass http://eventhub:8446; # admin-ws слушает HTTP на порту 8446 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/src/hooks/useAdminWebSocket.ts b/src/hooks/useAdminWebSocket.ts index b8930cb..54b0d01 100644 --- a/src/hooks/useAdminWebSocket.ts +++ b/src/hooks/useAdminWebSocket.ts @@ -20,8 +20,8 @@ export const useAdminWebSocket = () => { const queryClient = useQueryClient(); const accessToken = useAuthStore((s) => s.accessToken); const isAuthenticated = useAuthStore((s) => s.isAuthenticated); - const reconnectTimeoutRef = useRef>(); - const pingIntervalRef = useRef>(); + const reconnectTimeoutRef = useRef | null>(null); + const pingIntervalRef = useRef | null>(null); const lastMessageTimestamp = useRef(0); const wsRef = useRef(null); @@ -32,8 +32,7 @@ export const useAdminWebSocket = () => { const connect = () => { const wsUrl = import.meta.env.DEV ? `ws://localhost:5173/admin/ws?token=${accessToken}` - : `${import.meta.env.VITE_WS_URL || 'wss://admin-ws.eventhub.local'}/admin/ws?token=${accessToken}`; - + : `wss://${window.location.host}/admin/ws?token=${accessToken}`; const ws = new WebSocket(wsUrl); wsRef.current = ws; diff --git a/src/hooks/useAdmins.ts b/src/hooks/useAdmins.ts index b763052..f55206e 100644 --- a/src/hooks/useAdmins.ts +++ b/src/hooks/useAdmins.ts @@ -1,6 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { adminsApi } from '../api/adminsApi'; -import { AdminListParams } from '../types/api'; +import { AdminListParams, Admin } from '../types/api'; import { message } from 'antd'; import { normalizeData } from '../utils/normalize'; diff --git a/src/hooks/useEvents.ts b/src/hooks/useEvents.ts index c14b464..853bee8 100644 --- a/src/hooks/useEvents.ts +++ b/src/hooks/useEvents.ts @@ -1,6 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { eventsApi } from '../api/eventsApi'; -import { EventListParams } from '../types/api'; +import { Event, EventListParams } from '../types/api'; import { message } from 'antd'; import { normalizeData } from '../utils/normalize'; diff --git a/src/hooks/useReviews.ts b/src/hooks/useReviews.ts index 758bae7..a1ee6ba 100644 --- a/src/hooks/useReviews.ts +++ b/src/hooks/useReviews.ts @@ -1,6 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { reviewsApi } from '../api/reviewsApi'; -import { ReviewListParams } from '../types/api'; +import { ReviewListParams, Review } from '../types/api'; import { message } from 'antd'; import { normalizeData } from '../utils/normalize'; diff --git a/src/hooks/useSubscriptions.ts b/src/hooks/useSubscriptions.ts index 7224752..63e3452 100644 --- a/src/hooks/useSubscriptions.ts +++ b/src/hooks/useSubscriptions.ts @@ -1,6 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { subscriptionsApi } from '../api/subscriptionsApi'; -import { SubscriptionListParams } from '../types/api'; +import { SubscriptionListParams, Subscription as SubscriptionType } from '../types/api'; import { message } from 'antd'; import { normalizeData } from '../utils/normalize'; @@ -25,7 +25,7 @@ export const useSubscription = (id: string) => { export const useUpdateSubscription = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: ({ id, data }: { id: string; data: Partial }) => + mutationFn: ({ id, data }: { id: string; data: Partial }) => subscriptionsApi.updateSubscription(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['subscriptions'] }); diff --git a/src/hooks/useTickets.ts b/src/hooks/useTickets.ts index 97e1292..d13f12d 100644 --- a/src/hooks/useTickets.ts +++ b/src/hooks/useTickets.ts @@ -1,6 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { ticketsApi } from '../api/ticketsApi'; -import { TicketListParams } from '../types/api'; +import { TicketListParams, Ticket } from '../types/api'; import { message } from 'antd'; import { normalizeData } from '../utils/normalize'; diff --git a/src/hooks/useUsers.ts b/src/hooks/useUsers.ts index 764760c..0674fde 100644 --- a/src/hooks/useUsers.ts +++ b/src/hooks/useUsers.ts @@ -1,6 +1,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { usersApi } from '../api/usersApi'; -import { UserListParams } from '../types/api'; +import { UserListParams, User } from '../types/api'; import { message } from 'antd'; import { normalizeData } from '../utils/normalize'; diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index 7cff764..140d7d4 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -151,7 +151,7 @@ const AdminLayout: React.FC = () => { display: 'flex', flexDirection: 'column', height: '100vh', - position: 'sticky', +// position: 'sticky', top: 0, left: 0, }} diff --git a/src/pages/admins/AdminDetailPage.tsx b/src/pages/admins/AdminDetailPage.tsx index 5fc30f4..1e08ca6 100644 --- a/src/pages/admins/AdminDetailPage.tsx +++ b/src/pages/admins/AdminDetailPage.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Card, Descriptions, Spin, Button, Tag, Space, Modal, Form, Input, Select, message } from 'antd'; +import { Card, Descriptions, Spin, Button, Tag, Modal, Form, Input, Select, message } from 'antd'; import { EditOutlined } from '@ant-design/icons'; import { useAdmin, useUpdateAdmin } from '../../hooks/useAdmins'; import dayjs from 'dayjs'; diff --git a/src/pages/admins/AdminListPage.tsx b/src/pages/admins/AdminListPage.tsx index 8260c42..d87f0f8 100644 --- a/src/pages/admins/AdminListPage.tsx +++ b/src/pages/admins/AdminListPage.tsx @@ -87,7 +87,7 @@ const AdminListPage: React.FC = () => { const handleTableChange = ( pagination: any, - filters: any, + _filters: any, sorter: SorterResult | SorterResult[] ) => { const s = Array.isArray(sorter) ? sorter[0] : sorter; diff --git a/src/pages/audit/AuditPage.tsx b/src/pages/audit/AuditPage.tsx index 8eb0144..e423a69 100644 --- a/src/pages/audit/AuditPage.tsx +++ b/src/pages/audit/AuditPage.tsx @@ -5,7 +5,6 @@ import { useAudit } from '../../hooks/useAudit'; import { useAdmin, useAdmins } from '../../hooks/useAdmins'; import { useUser } from '../../hooks/useUsers'; import { useEvent } from '../../hooks/useEvents'; -import { useReview } from '../../hooks/useReviews'; import { AuditRecord, AuditListParams } from '../../types/api'; import type { ColumnsType, SorterResult } from 'antd/es/table/interface'; @@ -43,7 +42,6 @@ const AuditPage: React.FC = () => { const EntityNameCell: React.FC<{ entityType: string; entityId: string }> = ({ entityType, entityId }) => { const { data: user, isLoading: loadingUser } = useUser(entityType === 'user' ? entityId : ''); const { data: event, isLoading: loadingEvent } = useEvent(entityType === 'event' ? entityId : ''); - const { data: review, isLoading: loadingReview } = useReview(entityType === 'review' ? entityId : ''); if (entityType === 'user') { if (loadingUser) return ; @@ -60,18 +58,16 @@ const AuditPage: React.FC = () => { } if (entityType === 'review') { - if (loadingReview) return ; - // Для отзыва показываем ссылку на страницу отзыва + // для review показываем ссылку без загрузки, так как нет API для загрузки названия return {entityId}; } - // Для остальных типов (calendar, report, ticket, subscription, admin) пока просто ID return {entityId}; }; const handleTableChange = ( pagination: any, - filters: any, + _filters: any, sorter: SorterResult | SorterResult[] ) => { const s = Array.isArray(sorter) ? sorter[0] : sorter; diff --git a/src/pages/banned-words/BannedWordsPage.tsx b/src/pages/banned-words/BannedWordsPage.tsx index 60589e9..4131adb 100644 --- a/src/pages/banned-words/BannedWordsPage.tsx +++ b/src/pages/banned-words/BannedWordsPage.tsx @@ -1,11 +1,12 @@ import React, { useState } from 'react'; import { Table, Button, Input, Space, Popconfirm, Tooltip, Spin } from 'antd'; -import { SearchOutlined, DeleteOutlined } from '@ant-design/icons'; +import { DeleteOutlined } from '@ant-design/icons'; import { Link } from 'react-router-dom'; -import { useBannedWords, useAddBannedWord, useRemoveBannedWord, BannedWordListParams } from '../../hooks/useBannedWords'; +import { useBannedWords, useAddBannedWord, useRemoveBannedWord } from '../../hooks/useBannedWords'; +import { BannedWordListParams } from '../../api/bannedWordsApi'; import { useAdmin } from '../../hooks/useAdmins'; import { BannedWord } from '../../types/api'; -import type { ColumnsType, SorterResult } from 'antd/es/table'; +import type { ColumnsType, SorterResult } from 'antd/es/table/interface'; import dayjs from 'dayjs'; const BannedWordsPage: React.FC = () => { @@ -23,9 +24,10 @@ const BannedWordsPage: React.FC = () => { } }; + // Обработчик изменения таблицы (сортировка/пагинация) const handleTableChange = ( pagination: any, - filters: any, + _filters: any, sorter: SorterResult | SorterResult[] ) => { const s = Array.isArray(sorter) ? sorter[0] : sorter; @@ -38,6 +40,11 @@ const BannedWordsPage: React.FC = () => { })); }; + // Обработчик поиска + const handleSearch = (value: string) => { + setParams(prev => ({ ...prev, q: value || undefined, offset: 0 })); + }; + // Компонент для отображения "Кем добавлено" const AddedByCell: React.FC<{ adminId: string | null }> = ({ adminId }) => { const { data: admin, isLoading: loadingAdmin } = useAdmin(adminId || ''); @@ -108,7 +115,7 @@ const BannedWordsPage: React.FC = () => { setParams(prev => ({ ...prev, q: value || undefined, offset: 0 }))} + onSearch={handleSearch} style={{ width: 200 }} allowClear /> diff --git a/src/pages/events/EventDetailPage.tsx b/src/pages/events/EventDetailPage.tsx index 515e4b5..1fef1ed 100644 --- a/src/pages/events/EventDetailPage.tsx +++ b/src/pages/events/EventDetailPage.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Card, Descriptions, Spin, Button, Tag, Space } from 'antd'; +import { Card, Descriptions, Spin, Button, Tag } from 'antd'; import { Link } from 'react-router-dom'; import { useEvent } from '../../hooks/useEvents'; import dayjs from 'dayjs'; diff --git a/src/pages/events/EventListPage.tsx b/src/pages/events/EventListPage.tsx index 9810c54..40d7232 100644 --- a/src/pages/events/EventListPage.tsx +++ b/src/pages/events/EventListPage.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Table, Button, Tag, Space, Modal, Form, Select, message, Spin, Input, Tooltip, DatePicker, InputNumber } from 'antd'; +import { Table, Button, Tag, Space, Modal, Form, Select, Spin, Input, Tooltip, DatePicker, InputNumber } from 'antd'; import { InfoCircleOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; -import { Link, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { useEvents, useUpdateEvent, useDeleteEvent, useEvent } from '../../hooks/useEvents'; import { Event, EventListParams } from '../../types/api'; import type { ColumnsType, SorterResult } from 'antd/es/table/interface'; @@ -106,7 +106,7 @@ const EventListPage: React.FC = () => { const handleTableChange = ( pagination: any, - filters: any, + _filters: any, sorter: SorterResult | SorterResult[] ) => { const s = Array.isArray(sorter) ? sorter[0] : sorter; diff --git a/src/pages/profile/ProfilePage.tsx b/src/pages/profile/ProfilePage.tsx index 3d9b7eb..aeee4f0 100644 --- a/src/pages/profile/ProfilePage.tsx +++ b/src/pages/profile/ProfilePage.tsx @@ -38,7 +38,7 @@ const ProfilePage: React.FC = () => { ); if (payload.preferences) { try { - payload.preferences = JSON.parse(payload.preferences); + payload.preferences = JSON.parse(payload.preferences as string); } catch { message.error('Поле «Настройки» должно быть валидным JSON'); return; @@ -72,7 +72,7 @@ const ProfilePage: React.FC = () => { {!editing ? ( <>
- } src={isBadValue(user.avatar_url) ? undefined : user.avatar_url} /> + } src={isBadValue(user.avatar_url) ? '-' : {user.avatar_url}} />

{!isBadValue(user.nickname) ? user.nickname : user.email}

{ {isBadValue(user.language) ? '-' : user.language} {isBadValue(user.phone) ? '-' : user.phone} - {isBadValue(user.avatar_url) ? '-' : {user.avatar_url}} + {isBadValue(user.avatar_url) ? '-' : {user.avatar_url}} {isBadValue(user.preferences) ? '-' :
{JSON.stringify(user.preferences, null, 2)}
} diff --git a/src/pages/reports/ReportListPage.tsx b/src/pages/reports/ReportListPage.tsx index 7c498e2..4b96a1e 100644 --- a/src/pages/reports/ReportListPage.tsx +++ b/src/pages/reports/ReportListPage.tsx @@ -35,7 +35,7 @@ const ReportListPage: React.FC = () => { const handleTableChange = ( pagination: any, - filters: any, + _filters: any, sorter: SorterResult | SorterResult[] ) => { const s = Array.isArray(sorter) ? sorter[0] : sorter; @@ -60,14 +60,13 @@ const ReportListPage: React.FC = () => { const { data: user, isLoading: loading } = useUser(reporterId); if (loading) return ; if (!user) return {reporterId}; - // Используем ту же логику, что и в renderLink - const nick = user.nickname; - const email = user.email; + + const isGoodString = (val: any): val is string => !isBadValue(val); let name = ''; - if (!isBadValue(nick)) { - name = nick; - } else if (!isBadValue(email)) { - name = email; + if (isGoodString(user.nickname)) { + name = user.nickname; + } else if (isGoodString(user.email)) { + name = user.email; } else { name = user.id; } diff --git a/src/pages/reviews/ReviewDetailPage.tsx b/src/pages/reviews/ReviewDetailPage.tsx index 356f88d..c39a188 100644 --- a/src/pages/reviews/ReviewDetailPage.tsx +++ b/src/pages/reviews/ReviewDetailPage.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Card, Descriptions, Spin, Button, Tag, Space, Modal, Form, Select, Input, message } from 'antd'; +import { Card, Descriptions, Spin, Button, Tag, Space, Modal, Form, Input, message } from 'antd'; import { Link } from 'react-router-dom'; import { useReview, useUpdateReview } from '../../hooks/useReviews'; import { useUser } from '../../hooks/useUsers'; diff --git a/src/pages/reviews/ReviewListPage.tsx b/src/pages/reviews/ReviewListPage.tsx index a3d7686..026b7a0 100644 --- a/src/pages/reviews/ReviewListPage.tsx +++ b/src/pages/reviews/ReviewListPage.tsx @@ -92,7 +92,7 @@ const ReviewListPage: React.FC = () => { // ---- Таблица ---- const handleTableChange = ( pagination: any, - filters: any, + _filters: any, sorter: SorterResult | SorterResult[] ) => { const s = Array.isArray(sorter) ? sorter[0] : sorter; @@ -111,13 +111,13 @@ const ReviewListPage: React.FC = () => { const { data: user, isLoading: loading } = useUser(userId); if (loading) return ; if (!user) return {userId}; - const nick = user.nickname; - const email = user.email; + + const isGoodString = (val: any): val is string => !isBadValue(val); let name = ''; - if (!isBadValue(nick)) { - name = nick; - } else if (!isBadValue(email)) { - name = email; + if (isGoodString(user.nickname)) { + name = user.nickname; + } else if (isGoodString(user.email)) { + name = user.email; } else { name = user.id; } diff --git a/src/pages/subscriptions/SubscriptionListPage.tsx b/src/pages/subscriptions/SubscriptionListPage.tsx index 4723b5a..cdec1bb 100644 --- a/src/pages/subscriptions/SubscriptionListPage.tsx +++ b/src/pages/subscriptions/SubscriptionListPage.tsx @@ -72,7 +72,7 @@ const SubscriptionListPage: React.FC = () => { const handleTableChange = ( pagination: any, - filters: any, + _filters: any, sorter: SorterResult | SorterResult[] ) => { const s = Array.isArray(sorter) ? sorter[0] : sorter; diff --git a/src/pages/tickets/TicketDetailPage.tsx b/src/pages/tickets/TicketDetailPage.tsx index f8fd3ca..9cc121c 100644 --- a/src/pages/tickets/TicketDetailPage.tsx +++ b/src/pages/tickets/TicketDetailPage.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { Card, Descriptions, Spin, Button, Tag, Space, Form, Select, Input, message } from 'antd'; +import { Card, Descriptions, Spin, Button, Tag, Space, Form, Select, Input } from 'antd'; import { Link } from 'react-router-dom'; import { useTicket, useUpdateTicket } from '../../hooks/useTickets'; import { useUser } from '../../hooks/useUsers'; @@ -123,7 +123,7 @@ const TicketDetailPage: React.FC = () => { showSearch optionFilterProp="label" > - {(admins || []).map((admin) => { + {(admins?.data || []).map((admin) => { const label = `${admin.email}${admin.nickname && admin.nickname !== '-' ? ` – ${admin.nickname}` : ''}`; return ( diff --git a/src/pages/tickets/TicketListPage.tsx b/src/pages/tickets/TicketListPage.tsx index b742009..acd114c 100644 --- a/src/pages/tickets/TicketListPage.tsx +++ b/src/pages/tickets/TicketListPage.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Table, Button, Tag, Space, Popconfirm, Tooltip, Spin, Card, Col, Row, Statistic } from 'antd'; +import { Table, Button, Tag, Popconfirm, Tooltip, Spin, Card, Col, Row, Statistic } from 'antd'; import { InfoCircleOutlined, DeleteOutlined, BugOutlined, ClockCircleOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; import { Link, useNavigate } from 'react-router-dom'; import { useTickets, useDeleteTicket, useTicketStats } from '../../hooks/useTickets'; @@ -29,7 +29,7 @@ const TicketListPage: React.FC = () => { const handleTableChange = ( pagination: any, - filters: any, + _filters: any, sorter: SorterResult | SorterResult[] ) => { const s = Array.isArray(sorter) ? sorter[0] : sorter; diff --git a/src/pages/users/UserListPage.tsx b/src/pages/users/UserListPage.tsx index a7e5294..2977a92 100644 --- a/src/pages/users/UserListPage.tsx +++ b/src/pages/users/UserListPage.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Table, Button, Tag, Space, Modal, Form, Select, message, Spin, Input, Tooltip } from 'antd'; +import { Table, Button, Tag, Space, Modal, Form, Select, Spin, Input, Tooltip } from 'antd'; import { InfoCircleOutlined, EditOutlined, LockOutlined, UnlockOutlined, DeleteOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { useUsers, useUpdateUser, useDeleteUser, useUser } from '../../hooks/useUsers'; @@ -150,7 +150,7 @@ const UserListPage: React.FC = () => { // ==================== Таблица ==================== const handleTableChange = ( pagination: any, - filters: any, + _filters: any, sorter: SorterResult | SorterResult[] ) => { const s = Array.isArray(sorter) ? sorter[0] : sorter;