feat(admin-ui): minimal React+Vite+TanStack scaffolding

- Vite 7 + React 19 + TanStack Router (file-based) + TanStack Query
- Tailwind v4 (CSS-first config, OKLCH brand colors)
- i18next + react-i18next с RU/EN switcher в header
- Axios client с Accept-Language interceptor + JWT placeholder
- 2 pages: /dictionaries (list cards) + /dictionaries/$name (records table)
- Dockerfile (node build + nginx serving + API proxy)
- README + .gitignore

Backend:
- Новый GET /api/v1/dictionaries в read-api (DictionaryListController),
  фильтрует по доступному scope через ScopeContext
- DictionaryListItem DTO без heavy schema_json

routeTree.gen.ts — stub; реальное содержимое генерится Vite plugin'ом при
первом 'pnpm dev'.
This commit is contained in:
Александр Зимин
2026-05-04 00:55:04 +03:00
parent adc5315948
commit 1eba2ee59b
20 changed files with 609 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
import axios from 'axios'
/**
* Centralized axios instance. JWT injection вешается через interceptor:
* когда @nstart/auth интегрируется, добавляем Authorization: Bearer ...
*/
export const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE ?? '/api/v1',
timeout: 10_000,
})
apiClient.interceptors.request.use((config) => {
const lang = (typeof navigator !== 'undefined' && navigator.language) || 'ru-RU'
config.headers['Accept-Language'] = `${lang},ru-RU;q=0.9,en-US;q=0.8`
// JWT placeholder
const token = localStorage.getItem('ordinis.token')
if (token) config.headers['Authorization'] = `Bearer ${token}`
return config
})
apiClient.interceptors.response.use(
(resp) => resp,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('ordinis.token')
}
return Promise.reject(error)
},
)
export type DictionaryDefinition = {
id: string
name: string
displayName?: string
description?: string
scope: 'PUBLIC' | 'INTERNAL' | 'RESTRICTED'
schemaVersion: string
bundle: string
supportedLocales: string[]
defaultLocale: string
createdAt: string
updatedAt: string
}
export type FlattenedRecord = {
id: string
businessKey: string
data: Record<string, unknown>
dataScope: 'PUBLIC' | 'INTERNAL' | 'RESTRICTED'
validFrom: string
validTo: string
_meta?: { locale?: string }
}
+26
View File
@@ -0,0 +1,26 @@
import { useQuery, queryOptions } from '@tanstack/react-query'
import { apiClient, type DictionaryDefinition, type FlattenedRecord } from './client'
export const dictionariesQuery = queryOptions({
queryKey: ['dictionaries'],
queryFn: async (): Promise<DictionaryDefinition[]> => {
const { data } = await apiClient.get<DictionaryDefinition[]>('/dictionaries')
return data
},
})
export const recordsQuery = (dictionaryName: string, scopeCsv: string) =>
queryOptions({
queryKey: ['records', dictionaryName, scopeCsv],
queryFn: async (): Promise<FlattenedRecord[]> => {
const { data } = await apiClient.get<FlattenedRecord[]>(
`/${dictionaryName}/records`,
{ params: { as_scope: scopeCsv } },
)
return data
},
})
export const useDictionaries = () => useQuery(dictionariesQuery)
export const useRecords = (dictionaryName: string, scopeCsv: string) =>
useQuery(recordsQuery(dictionaryName, scopeCsv))