Add news feature with CRUD operations and real-time notifications
CI / Backend (build + test) (push) Successful in 1m18s
CI / Frontend (lint + typecheck + build) (push) Successful in 32s

- Implemented news management functionality, allowing admins to create, read, update, and delete news posts.
- Introduced a new SignalR event for broadcasting news updates to all connected clients.
- Updated API documentation to include new endpoints for news management.
- Enhanced frontend with a dedicated news page and admin interface for managing news posts.
- Added necessary localization for news-related terms in both Russian and English.
This commit is contained in:
Leonid Pershin
2026-07-03 15:28:33 +03:00
parent bea2b5fcf7
commit b6637a1c03
47 changed files with 2523 additions and 9 deletions
+9
View File
@@ -89,6 +89,15 @@ export type ClientAppDto = {
/** GET /api/apps — сгруппировано по ОС на бэкенде; отсутствующая ОС значит "нет приложений". */
export type AppsByOs = Partial<Record<OsPlatform, ClientAppDto[]>>
/** Один DTO на пользовательскую ленту и админку — у новости нет полей, скрытых от юзера. */
export type NewsPostDto = {
id: string
title: string
body: string
createdAt: string
updatedAt: string | null
}
export type LinkTokenResponse = {
deepLink: string | null
expiresAt: string
+36
View File
@@ -48,6 +48,7 @@ const resources = {
nav: {
dashboard: 'Мои конфиги',
instructions: 'Инструкции',
news: 'Новости',
settings: 'Настройки',
admin: 'Админка',
logout: 'Выйти',
@@ -114,6 +115,11 @@ const resources = {
},
},
news: {
title: 'Новости',
empty: 'Пока нет новостей.',
},
settings: {
changePassword: 'Сменить пароль',
currentPassword: 'Текущий пароль',
@@ -150,6 +156,7 @@ const resources = {
roles: 'Роли',
nodes: 'Ноды',
apps: 'Приложения',
news: 'Новости',
audit: 'Аудит',
},
users: {
@@ -251,6 +258,17 @@ const resources = {
deleted: 'Приложение удалено.',
confirmDelete: 'Удалить приложение из каталога?',
},
news: {
create: 'Добавить новость',
title: 'Заголовок',
body: 'Текст (Markdown)',
preview: 'Предпросмотр',
empty: 'Новостей пока нет.',
created: 'Новость опубликована.',
updated: 'Новость обновлена.',
deleted: 'Новость удалена.',
confirmDelete: 'Удалить новость?',
},
audit: {
time: 'Время',
action: 'Действие',
@@ -317,6 +335,7 @@ const resources = {
nav: {
dashboard: 'My configs',
instructions: 'Instructions',
news: 'News',
settings: 'Settings',
admin: 'Admin',
logout: 'Log out',
@@ -383,6 +402,11 @@ const resources = {
},
},
news: {
title: 'News',
empty: 'No news yet.',
},
settings: {
changePassword: 'Change password',
currentPassword: 'Current password',
@@ -419,6 +443,7 @@ const resources = {
roles: 'Roles',
nodes: 'Nodes',
apps: 'Apps',
news: 'News',
audit: 'Audit',
},
users: {
@@ -520,6 +545,17 @@ const resources = {
deleted: 'App deleted.',
confirmDelete: 'Remove this app from the catalog?',
},
news: {
create: 'Add post',
title: 'Title',
body: 'Body (Markdown)',
preview: 'Preview',
empty: 'No news yet.',
created: 'Post published.',
updated: 'Post updated.',
deleted: 'Post deleted.',
confirmDelete: 'Delete this post?',
},
audit: {
time: 'Time',
action: 'Action',
@@ -7,6 +7,7 @@ import { getConnection, startConnection, stopConnection } from './connection'
type ConfigTrafficUpdated = { configId: string; usedUpBytes: number; usedDownBytes: number }
type ConfigStatusChanged = { configId: string; status: ConfigStatus }
type UserActivated = { userId: string }
type NewsPublished = { id: string; title: string; createdAt: string }
/** Живые обновления по SignalR: точечно патчит кэш TanStack Query вместо инвалидации всего списка. */
export function RealtimeProvider({ children }: { children: React.ReactNode }) {
@@ -49,9 +50,14 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) {
void queryClient.invalidateQueries({ queryKey: ['me-poll'] })
}
const onNewsPublished = (_payload: NewsPublished) => {
void queryClient.invalidateQueries({ queryKey: ['news'] })
}
connection.on('configTrafficUpdated', onTrafficUpdated)
connection.on('configStatusChanged', onStatusChanged)
connection.on('userActivated', onUserActivated)
connection.on('newsPublished', onNewsPublished)
void startConnection()
@@ -59,6 +65,7 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) {
connection.off('configTrafficUpdated', onTrafficUpdated)
connection.off('configStatusChanged', onStatusChanged)
connection.off('userActivated', onUserActivated)
connection.off('newsPublished', onNewsPublished)
}
}, [user, queryClient])
+16
View File
@@ -0,0 +1,16 @@
import { type TextareaHTMLAttributes, forwardRef } from 'react'
import { cn } from '@/shared/lib/cn'
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaHTMLAttributes<HTMLTextAreaElement>>(
({ className, ...props }, ref) => (
<textarea
className={cn(
'flex min-h-32 w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none transition-colors placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
ref={ref}
{...props}
/>
),
)
Textarea.displayName = 'Textarea'