Add news feature with CRUD operations and real-time notifications
- 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:
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user