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:
@@ -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 }
|
||||
}
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user