feat: добавить рабочий dashboard admin
- добавлен Mantine theme provider и AppShell layout\n- сгенерирован Backend API клиент и добавлены infra/business хуки\n- добавлены таблица assets, detail/presets panels и create asset modal
This commit is contained in:
@@ -5,14 +5,20 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
|
"codegen:backend-api": "npx @gromlab/api-codegen@latest -i http://localhost:3001/docs-json -o src/infra/backend-api/generated -n backend-api.generated",
|
||||||
"dev": "vite --host 0.0.0.0 --port 5173",
|
"dev": "vite --host 0.0.0.0 --port 5173",
|
||||||
"preview": "vite preview --host 0.0.0.0 --port 5173",
|
"preview": "vite preview --host 0.0.0.0 --port 5173",
|
||||||
"typecheck": "tsc -b"
|
"typecheck": "tsc -b"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mantine/core": "^9.1.1",
|
||||||
|
"@mantine/form": "^9.1.1",
|
||||||
|
"@mantine/hooks": "^9.1.1",
|
||||||
|
"@mantine/notifications": "^9.1.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5"
|
"react-dom": "^19.2.5",
|
||||||
|
"swr": "^2.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@csstools/postcss-global-data": "^4.0.0",
|
"@csstools/postcss-global-data": "^4.0.0",
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import { ThemeProvider } from "infra/theme"
|
||||||
import { MainLayout } from "layouts/main"
|
import { MainLayout } from "layouts/main"
|
||||||
import { DashboardScreen } from "screens/dashboard"
|
import { DashboardScreen } from "screens/dashboard"
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<DashboardScreen />
|
<DashboardScreen />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
15
apps/admin/src/business/assets/assets.factory.ts
Normal file
15
apps/admin/src/business/assets/assets.factory.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useAssetOverview } from "./hooks/use-asset-overview.hook"
|
||||||
|
import { useAssetsDashboard } from "./hooks/use-assets-dashboard.hook"
|
||||||
|
import { useCreateAsset } from "./hooks/use-create-asset.hook"
|
||||||
|
import type { AssetsFactory } from "./types/assets-factory.type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создаёт runtime API бизнес-модуля Assets.
|
||||||
|
*/
|
||||||
|
export const assetsFactory: AssetsFactory = () => {
|
||||||
|
return {
|
||||||
|
useAssetOverview,
|
||||||
|
useAssetsDashboard,
|
||||||
|
useCreateAsset,
|
||||||
|
}
|
||||||
|
}
|
||||||
6
apps/admin/src/business/assets/config/assets.config.ts
Normal file
6
apps/admin/src/business/assets/config/assets.config.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import type { ListAssetsParams } from "infra/backend-api"
|
||||||
|
|
||||||
|
export const ASSETS_DASHBOARD_LIST_PARAMS = {
|
||||||
|
limit: "20",
|
||||||
|
offset: "0",
|
||||||
|
} satisfies ListAssetsParams
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useGetAsset, useGetAssetVariants } from "infra/backend-api"
|
||||||
|
|
||||||
|
import type { AssetOverview } from "../types/assets-api.type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Данные выбранного asset и его variants.
|
||||||
|
*/
|
||||||
|
export const useAssetOverview = (publicId: string | null): AssetOverview => {
|
||||||
|
const assetQuery = useGetAsset(publicId)
|
||||||
|
const variantsQuery = useGetAssetVariants(
|
||||||
|
publicId,
|
||||||
|
assetQuery.data?.currentVersion ? String(assetQuery.data.currentVersion) : undefined,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
asset: assetQuery.data ?? null,
|
||||||
|
error: assetQuery.error ?? variantsQuery.error,
|
||||||
|
isLoading: assetQuery.isLoading || variantsQuery.isLoading,
|
||||||
|
variants: variantsQuery.data?.variants ?? [],
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useGetAssetsList, useGetPresets } from "infra/backend-api"
|
||||||
|
|
||||||
|
import { ASSETS_DASHBOARD_LIST_PARAMS } from "../config/assets.config"
|
||||||
|
import type { AssetsDashboard } from "../types/assets-api.type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Данные стартового dashboard по assets и presets.
|
||||||
|
*/
|
||||||
|
export const useAssetsDashboard = (): AssetsDashboard => {
|
||||||
|
const assetsQuery = useGetAssetsList(ASSETS_DASHBOARD_LIST_PARAMS)
|
||||||
|
const presetsQuery = useGetPresets()
|
||||||
|
|
||||||
|
const assets = assetsQuery.data?.assets ?? []
|
||||||
|
const presets = presetsQuery.data?.presets ?? []
|
||||||
|
const allowedSourceHosts = presetsQuery.data?.allowedSourceHosts ?? []
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowedSourceHosts,
|
||||||
|
assets,
|
||||||
|
custom: presetsQuery.data?.custom ?? null,
|
||||||
|
error: assetsQuery.error ?? presetsQuery.error,
|
||||||
|
isLoading: assetsQuery.isLoading || presetsQuery.isLoading,
|
||||||
|
presets,
|
||||||
|
summary: {
|
||||||
|
assets: assets.length,
|
||||||
|
hosts: allowedSourceHosts.length,
|
||||||
|
presets: presets.length,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
import { useSWRConfig } from "swr"
|
||||||
|
import { backendApi, getAssetsListKey } from "infra/backend-api"
|
||||||
|
|
||||||
|
import { ASSETS_DASHBOARD_LIST_PARAMS } from "../config/assets.config"
|
||||||
|
import type { CreateAssetAction, CreateAssetInput } from "../types/assets-api.type"
|
||||||
|
|
||||||
|
const toError = (error: unknown) => (error instanceof Error ? error : new Error("Неизвестная ошибка"))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сценарий создания asset с обновлением списка.
|
||||||
|
*/
|
||||||
|
export const useCreateAsset = (): CreateAssetAction => {
|
||||||
|
const { mutate } = useSWRConfig()
|
||||||
|
const [error, setError] = useState<Error | null>(null)
|
||||||
|
const [isCreating, setIsCreating] = useState(false)
|
||||||
|
|
||||||
|
const createAsset = async (input: CreateAssetInput) => {
|
||||||
|
setError(null)
|
||||||
|
setIsCreating(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createdAsset = await backendApi.assets.createAsset(input)
|
||||||
|
await mutate(getAssetsListKey(ASSETS_DASHBOARD_LIST_PARAMS))
|
||||||
|
|
||||||
|
return createdAsset
|
||||||
|
} catch (caughtError) {
|
||||||
|
const nextError = toError(caughtError)
|
||||||
|
setError(nextError)
|
||||||
|
throw nextError
|
||||||
|
} finally {
|
||||||
|
setIsCreating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createAsset,
|
||||||
|
error,
|
||||||
|
isCreating,
|
||||||
|
}
|
||||||
|
}
|
||||||
9
apps/admin/src/business/assets/index.ts
Normal file
9
apps/admin/src/business/assets/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export { assetsFactory } from "./assets.factory"
|
||||||
|
export type {
|
||||||
|
AssetOverview,
|
||||||
|
AssetsApi,
|
||||||
|
AssetsDashboard,
|
||||||
|
CreateAssetAction,
|
||||||
|
CreateAssetInput,
|
||||||
|
} from "./types/assets-api.type"
|
||||||
|
export type { AssetsFactory } from "./types/assets-factory.type"
|
||||||
46
apps/admin/src/business/assets/types/assets-api.type.ts
Normal file
46
apps/admin/src/business/assets/types/assets-api.type.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import type {
|
||||||
|
AssetResponseDto,
|
||||||
|
AssetVariantResponseDto,
|
||||||
|
CreateAssetRequestDto,
|
||||||
|
CreateAssetResponseDto,
|
||||||
|
PresetResponseDto,
|
||||||
|
PresetsResponseDto,
|
||||||
|
} from "infra/backend-api"
|
||||||
|
|
||||||
|
export type AssetOverview = {
|
||||||
|
asset: AssetResponseDto | null
|
||||||
|
error?: Error
|
||||||
|
isLoading: boolean
|
||||||
|
variants: AssetVariantResponseDto[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AssetsDashboard = {
|
||||||
|
allowedSourceHosts: string[]
|
||||||
|
assets: AssetResponseDto[]
|
||||||
|
custom: PresetsResponseDto["custom"] | null
|
||||||
|
error?: Error
|
||||||
|
isLoading: boolean
|
||||||
|
presets: PresetResponseDto[]
|
||||||
|
summary: {
|
||||||
|
assets: number
|
||||||
|
hosts: number
|
||||||
|
presets: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateAssetInput = CreateAssetRequestDto
|
||||||
|
|
||||||
|
export type CreateAssetAction = {
|
||||||
|
createAsset: (input: CreateAssetInput) => Promise<CreateAssetResponseDto>
|
||||||
|
error: Error | null
|
||||||
|
isCreating: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Публичный runtime API бизнес-модуля Assets.
|
||||||
|
*/
|
||||||
|
export type AssetsApi = {
|
||||||
|
useAssetOverview: (publicId: string | null) => AssetOverview
|
||||||
|
useAssetsDashboard: () => AssetsDashboard
|
||||||
|
useCreateAsset: () => CreateAssetAction
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { AssetsApi } from "./assets-api.type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Фабрика runtime API бизнес-модуля Assets.
|
||||||
|
*/
|
||||||
|
export type AssetsFactory = () => AssetsApi
|
||||||
11
apps/admin/src/infra/backend-api/client.ts
Normal file
11
apps/admin/src/infra/backend-api/client.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Api, HttpClient } from "./generated/backend-api.generated"
|
||||||
|
|
||||||
|
const httpClient = new HttpClient({
|
||||||
|
baseApiParams: {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const backendApi = new Api(httpClient)
|
||||||
1492
apps/admin/src/infra/backend-api/generated/backend-api.generated.ts
Normal file
1492
apps/admin/src/infra/backend-api/generated/backend-api.generated.ts
Normal file
File diff suppressed because it is too large
Load Diff
4
apps/admin/src/infra/backend-api/hooks/index.ts
Normal file
4
apps/admin/src/infra/backend-api/hooks/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { getAssetKey, useGetAsset } from "./use-get-asset.hook"
|
||||||
|
export { getAssetVariantsKey, useGetAssetVariants } from "./use-get-asset-variants.hook"
|
||||||
|
export { getAssetsListKey, useGetAssetsList } from "./use-get-assets-list.hook"
|
||||||
|
export { getPresetsKey, useGetPresets } from "./use-get-presets.hook"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import useSWR from "swr"
|
||||||
|
import type { SWRConfiguration } from "swr"
|
||||||
|
|
||||||
|
import { backendApi } from "../client"
|
||||||
|
import type { AssetVariantsResponseDto } from "../generated/backend-api.generated"
|
||||||
|
|
||||||
|
export const getAssetVariantsKey = (publicId: string, version?: string) =>
|
||||||
|
["backend-api", "assets", "variants", publicId, version ?? null] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение variants asset.
|
||||||
|
*/
|
||||||
|
export const useGetAssetVariants = (
|
||||||
|
publicId: string | null,
|
||||||
|
version?: string,
|
||||||
|
config?: SWRConfiguration,
|
||||||
|
) => {
|
||||||
|
const key = publicId !== null ? getAssetVariantsKey(publicId, version) : null
|
||||||
|
const fetcher = () => backendApi.assets.listAssetVariants({ publicId: publicId ?? "", version })
|
||||||
|
|
||||||
|
return useSWR<AssetVariantsResponseDto>(key, fetcher, config)
|
||||||
|
}
|
||||||
17
apps/admin/src/infra/backend-api/hooks/use-get-asset.hook.ts
Normal file
17
apps/admin/src/infra/backend-api/hooks/use-get-asset.hook.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import useSWR from "swr"
|
||||||
|
import type { SWRConfiguration } from "swr"
|
||||||
|
|
||||||
|
import { backendApi } from "../client"
|
||||||
|
import type { AssetResponseDto } from "../generated/backend-api.generated"
|
||||||
|
|
||||||
|
export const getAssetKey = (publicId: string) => ["backend-api", "assets", "detail", publicId] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение asset по publicId.
|
||||||
|
*/
|
||||||
|
export const useGetAsset = (publicId: string | null, config?: SWRConfiguration) => {
|
||||||
|
const key = publicId !== null ? getAssetKey(publicId) : null
|
||||||
|
const fetcher = () => backendApi.assets.getAsset({ publicId: publicId ?? "" })
|
||||||
|
|
||||||
|
return useSWR<AssetResponseDto>(key, fetcher, config)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import useSWR from "swr"
|
||||||
|
import type { SWRConfiguration } from "swr"
|
||||||
|
|
||||||
|
import { backendApi } from "../client"
|
||||||
|
import type { AssetsListResponseDto, ListAssetsParams } from "../generated/backend-api.generated"
|
||||||
|
|
||||||
|
export const getAssetsListKey = (params: ListAssetsParams = {}) =>
|
||||||
|
["backend-api", "assets", "list", params.limit ?? null, params.offset ?? null] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение списка assets.
|
||||||
|
*/
|
||||||
|
export const useGetAssetsList = (params: ListAssetsParams = {}, config?: SWRConfiguration) => {
|
||||||
|
const key = getAssetsListKey(params)
|
||||||
|
const fetcher = () => backendApi.assets.listAssets(params)
|
||||||
|
|
||||||
|
return useSWR<AssetsListResponseDto>(key, fetcher, config)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import useSWR from "swr"
|
||||||
|
import type { SWRConfiguration } from "swr"
|
||||||
|
|
||||||
|
import { backendApi } from "../client"
|
||||||
|
import type { PresetsResponseDto } from "../generated/backend-api.generated"
|
||||||
|
|
||||||
|
export const getPresetsKey = () => ["backend-api", "presets"] as const
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получение presets и custom transform config.
|
||||||
|
*/
|
||||||
|
export const useGetPresets = (config?: SWRConfiguration) => {
|
||||||
|
const fetcher = () => backendApi.presets.getPresets()
|
||||||
|
|
||||||
|
return useSWR<PresetsResponseDto>(getPresetsKey(), fetcher, config)
|
||||||
|
}
|
||||||
14
apps/admin/src/infra/backend-api/index.ts
Normal file
14
apps/admin/src/infra/backend-api/index.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export { backendApi } from "./client"
|
||||||
|
export * from "./hooks"
|
||||||
|
export type {
|
||||||
|
AssetResponseDto,
|
||||||
|
AssetVariantResponseDto,
|
||||||
|
AssetVariantsResponseDto,
|
||||||
|
AssetsListResponseDto,
|
||||||
|
CreateAssetRequestDto,
|
||||||
|
CreateAssetResponseDto,
|
||||||
|
CustomTransformConfigResponseDto,
|
||||||
|
ListAssetsParams,
|
||||||
|
PresetResponseDto,
|
||||||
|
PresetsResponseDto,
|
||||||
|
} from "./generated/backend-api.generated"
|
||||||
11
apps/admin/src/infra/theme/config/theme.config.ts
Normal file
11
apps/admin/src/infra/theme/config/theme.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createTheme } from "@mantine/core"
|
||||||
|
|
||||||
|
export const ADMIN_THEME = createTheme({
|
||||||
|
defaultRadius: "lg",
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
headings: {
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
fontWeight: "850",
|
||||||
|
},
|
||||||
|
primaryColor: "violet",
|
||||||
|
})
|
||||||
2
apps/admin/src/infra/theme/index.ts
Normal file
2
apps/admin/src/infra/theme/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { ThemeProvider } from "./theme-provider"
|
||||||
|
export type { ThemeProviderProps } from "./types/theme-provider-props.type"
|
||||||
23
apps/admin/src/infra/theme/theme-provider.tsx
Normal file
23
apps/admin/src/infra/theme/theme-provider.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { MantineProvider } from "@mantine/core"
|
||||||
|
import { Notifications } from "@mantine/notifications"
|
||||||
|
|
||||||
|
import { ADMIN_THEME } from "./config/theme.config"
|
||||||
|
import type { ThemeProviderProps } from "./types/theme-provider-props.type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Провайдер визуальной темы admin-приложения.
|
||||||
|
*
|
||||||
|
* Используется для:
|
||||||
|
* - подключения Mantine theme
|
||||||
|
* - подключения контейнера уведомлений
|
||||||
|
*/
|
||||||
|
export const ThemeProvider = (props: ThemeProviderProps) => {
|
||||||
|
const { children } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MantineProvider defaultColorScheme="light" theme={ADMIN_THEME}>
|
||||||
|
<Notifications position="top-right" />
|
||||||
|
{children}
|
||||||
|
</MantineProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { ReactNode } from "react"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры ThemeProvider.
|
||||||
|
*/
|
||||||
|
export type ThemeProviderProps = {
|
||||||
|
/** Содержимое приложения. */
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AppShell, Badge, Group, Text, ThemeIcon } from "@mantine/core"
|
||||||
import cl from "clsx"
|
import cl from "clsx"
|
||||||
|
|
||||||
import styles from "./styles/main.module.css"
|
import styles from "./styles/main.module.css"
|
||||||
@@ -14,17 +15,27 @@ export const MainLayout = (props: MainLayoutProps) => {
|
|||||||
const { children, className, ...rootAttrs } = props
|
const { children, className, ...rootAttrs } = props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...rootAttrs} className={cl(styles.root, className)}>
|
<AppShell {...rootAttrs} className={cl(styles.root, className)} header={{ height: 72 }} padding="md">
|
||||||
<header className={styles.header}>
|
<AppShell.Header className={styles.header}>
|
||||||
|
<Group h="100%" justify="space-between" px={{ base: "md", md: "xl" }}>
|
||||||
<a className={styles.brand} href="/" aria-label="Image Platform Admin">
|
<a className={styles.brand} href="/" aria-label="Image Platform Admin">
|
||||||
<span className={styles.brandMark}>IP</span>
|
<ThemeIcon className={styles.brandMark} radius="xl" size={42} variant="light">
|
||||||
<span className={styles.brandText}>Image Platform</span>
|
IP
|
||||||
|
</ThemeIcon>
|
||||||
|
<Text className={styles.brandText} fw={850}>
|
||||||
|
Image Platform
|
||||||
|
</Text>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p className={styles.status}>Admin MVP</p>
|
<Badge color="violet" radius="xl" size="lg" variant="light">
|
||||||
</header>
|
Admin MVP
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
</AppShell.Header>
|
||||||
|
|
||||||
<main className={styles.content}>{children}</main>
|
<AppShell.Main className={styles.main}>
|
||||||
</div>
|
<div className={styles.content}>{children}</div>
|
||||||
|
</AppShell.Main>
|
||||||
|
</AppShell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
.root {
|
.root {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: var(--space-4);
|
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 18% 18%, var(--color-accent-wash), transparent 30rem),
|
radial-gradient(circle at 16% 12%, var(--color-accent-wash), transparent 32rem),
|
||||||
|
radial-gradient(circle at 86% 4%, rgb(255 176 96 / 16%), transparent 28rem),
|
||||||
var(--color-page);
|
var(--color-page);
|
||||||
|
|
||||||
@media (--md) {
|
|
||||||
padding: var(--space-6);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
border-bottom: 1px solid var(--color-border);
|
||||||
align-items: center;
|
background: rgb(247 244 238 / 78%);
|
||||||
justify-content: space-between;
|
backdrop-filter: blur(18px);
|
||||||
max-width: var(--content-width);
|
|
||||||
margin: 0 auto var(--space-4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand {
|
.brand {
|
||||||
@@ -23,21 +17,16 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: -0.03em;
|
letter-spacing: -0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brandMark {
|
.brandMark {
|
||||||
display: grid;
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
place-items: center;
|
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: var(--radius-round);
|
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
box-shadow: var(--shadow-soft);
|
box-shadow: var(--shadow-soft);
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 850;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,22 +34,20 @@
|
|||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@media (--sm) {
|
@media (--sm) {
|
||||||
display: inline;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.main {
|
||||||
margin: 0;
|
background: transparent;
|
||||||
padding: var(--space-2) var(--space-3);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-round);
|
|
||||||
background: var(--color-surface-muted);
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
max-width: var(--content-width);
|
max-width: 82rem;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
padding: var(--space-4) 0 var(--space-8);
|
||||||
|
|
||||||
|
@media (--md) {
|
||||||
|
padding-top: var(--space-6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ComponentPropsWithoutRef, ReactNode } from "react"
|
import type { AppShellProps } from "@mantine/core"
|
||||||
|
import type { ReactNode } from "react"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Параметры MainLayout.
|
* Параметры MainLayout.
|
||||||
@@ -9,6 +10,6 @@ export type MainLayoutParams = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Атрибуты корневого элемента без children. */
|
/** Атрибуты корневого элемента без children. */
|
||||||
type RootAttrs = Omit<ComponentPropsWithoutRef<'div'>, 'children'>
|
type RootAttrs = Omit<AppShellProps, "children">
|
||||||
|
|
||||||
export type MainLayoutProps = RootAttrs & MainLayoutParams
|
export type MainLayoutProps = RootAttrs & MainLayoutParams
|
||||||
|
|||||||
@@ -1,16 +1,32 @@
|
|||||||
export const DASHBOARD_CARDS = [
|
export const DASHBOARD_CARDS = [
|
||||||
{
|
{
|
||||||
|
metric: "assets",
|
||||||
title: "Assets",
|
title: "Assets",
|
||||||
description: "Каталог исходных изображений, версий и публичных identifiers.",
|
description: "Каталог исходных изображений, версий и публичных identifiers.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
metric: "presets",
|
||||||
title: "Variants",
|
title: "Variants",
|
||||||
description: "Статусы генерации AVIF/WebP/JPEG под presets и custom transforms.",
|
description: "Статусы генерации AVIF/WebP/JPEG под presets и custom transforms.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
metric: "hosts",
|
||||||
title: "Storage",
|
title: "Storage",
|
||||||
description: "PostgreSQL как source of truth, S3/MinIO как хранилище готовых bytes.",
|
description: "PostgreSQL как source of truth, S3/MinIO как хранилище готовых bytes.",
|
||||||
},
|
},
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export const DASHBOARD_PIPELINE = ["Backend", "RabbitMQ", "Worker", "imgproxy", "S3"] as const
|
export const DASHBOARD_PIPELINE = ["Backend", "RabbitMQ", "Worker", "imgproxy", "S3"] as const
|
||||||
|
|
||||||
|
export const ASSET_STATUS_COLORS = {
|
||||||
|
active: "green",
|
||||||
|
deleted: "red",
|
||||||
|
disabled: "gray",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const VARIANT_STATUS_COLORS = {
|
||||||
|
failed: "red",
|
||||||
|
pending: "yellow",
|
||||||
|
processing: "blue",
|
||||||
|
ready: "green",
|
||||||
|
} as const
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
|
import { Alert, Button, Group, Paper, Stack, Text, Title } from "@mantine/core"
|
||||||
|
import { useDisclosure } from "@mantine/hooks"
|
||||||
import cl from "clsx"
|
import cl from "clsx"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { assetsFactory } from "business/assets"
|
||||||
|
|
||||||
import { DASHBOARD_CARDS, DASHBOARD_PIPELINE } from "./config/dashboard.config"
|
import { DASHBOARD_PIPELINE } from "./config/dashboard.config"
|
||||||
|
import { AssetDetailPanel } from "./parts/asset-detail-panel"
|
||||||
|
import { AssetsTable } from "./parts/assets-table"
|
||||||
|
import { CreateAssetModal } from "./parts/create-asset-modal"
|
||||||
|
import { PresetsPanel } from "./parts/presets-panel"
|
||||||
|
import { SummaryCards } from "./parts/summary-cards"
|
||||||
import styles from "./styles/dashboard.module.css"
|
import styles from "./styles/dashboard.module.css"
|
||||||
import type { DashboardScreenProps } from "./types/dashboard.type"
|
import type { DashboardScreenProps } from "./types/dashboard.type"
|
||||||
|
|
||||||
|
const assets = assetsFactory()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Стартовый dashboard admin-приложения.
|
* Стартовый dashboard admin-приложения.
|
||||||
*
|
*
|
||||||
@@ -13,34 +24,73 @@ import type { DashboardScreenProps } from "./types/dashboard.type"
|
|||||||
*/
|
*/
|
||||||
export const DashboardScreen = (props: DashboardScreenProps) => {
|
export const DashboardScreen = (props: DashboardScreenProps) => {
|
||||||
const { className, ...rootAttrs } = props
|
const { className, ...rootAttrs } = props
|
||||||
|
const [selectedPublicId, setSelectedPublicId] = useState<string | null>(null)
|
||||||
|
const [isCreateAssetOpen, createAssetModal] = useDisclosure(false)
|
||||||
|
const dashboard = assets.useAssetsDashboard()
|
||||||
|
const createAsset = assets.useCreateAsset()
|
||||||
|
const effectivePublicId = selectedPublicId ?? dashboard.assets[0]?.publicId ?? null
|
||||||
|
const overview = assets.useAssetOverview(effectivePublicId)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section {...rootAttrs} className={cl(styles.root, className)}>
|
<section {...rootAttrs} className={cl(styles.root, className)}>
|
||||||
<div className={styles.hero}>
|
<Stack gap="lg">
|
||||||
<p className={styles.eyebrow}>Image Platform Admin</p>
|
<Paper className={styles.hero} p={{ base: "xl", md: 42 }} radius="xl" shadow="xs" withBorder>
|
||||||
<h1 className={styles.title}>Control plane для image delivery</h1>
|
<Group align="flex-end" justify="space-between" gap="xl">
|
||||||
<p className={styles.lead}>
|
<div className={styles.heroContent}>
|
||||||
Админка будет управлять allowed hosts, assets, source versions, presets и variant
|
<Text className={styles.eyebrow}>Image Platform Admin</Text>
|
||||||
generation без прямого доступа к storage-слою.
|
<Title className={styles.title}>Control plane для image delivery</Title>
|
||||||
</p>
|
<Text className={styles.lead}>
|
||||||
|
Управление allowed hosts, assets, source versions, presets и variant generation без
|
||||||
|
прямого доступа к storage-слою.
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.grid} aria-label="Будущие разделы admin">
|
<Button className={styles.primaryAction} onClick={createAssetModal.open} radius="xl" size="md">
|
||||||
{DASHBOARD_CARDS.map((card) => (
|
Create asset
|
||||||
<article className={styles.card} key={card.title}>
|
</Button>
|
||||||
<h2 className={styles.cardTitle}>{card.title}</h2>
|
</Group>
|
||||||
<p className={styles.cardDescription}>{card.description}</p>
|
</Paper>
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.pipeline} aria-label="Пайплайн генерации изображений">
|
<SummaryCards isLoading={dashboard.isLoading} summary={dashboard.summary} />
|
||||||
|
|
||||||
|
<Group gap="xs" role="list" aria-label="Пайплайн генерации изображений">
|
||||||
{DASHBOARD_PIPELINE.map((step) => (
|
{DASHBOARD_PIPELINE.map((step) => (
|
||||||
<span className={styles.pipelineStep} key={step}>
|
<Text className={styles.pipelineStep} key={step} role="listitem">
|
||||||
{step}
|
{step}
|
||||||
</span>
|
</Text>
|
||||||
))}
|
))}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{dashboard.error ? (
|
||||||
|
<Alert color="red" radius="lg" title="Backend API недоступен">
|
||||||
|
Проверьте, что backend запущен на `localhost:3001`, а Vite proxy доступен по `/api`.
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className={styles.workbench}>
|
||||||
|
<AssetsTable
|
||||||
|
assets={dashboard.assets}
|
||||||
|
isLoading={dashboard.isLoading}
|
||||||
|
onSelect={setSelectedPublicId}
|
||||||
|
selectedPublicId={effectivePublicId}
|
||||||
|
/>
|
||||||
|
<AssetDetailPanel overview={overview} publicId={effectivePublicId} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<PresetsPanel
|
||||||
|
allowedSourceHosts={dashboard.allowedSourceHosts}
|
||||||
|
custom={dashboard.custom}
|
||||||
|
isLoading={dashboard.isLoading}
|
||||||
|
presets={dashboard.presets}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<CreateAssetModal
|
||||||
|
action={createAsset}
|
||||||
|
onClose={createAssetModal.close}
|
||||||
|
onCreated={setSelectedPublicId}
|
||||||
|
opened={isCreateAssetOpen}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
15
apps/admin/src/screens/dashboard/lib/format-date.ts
Normal file
15
apps/admin/src/screens/dashboard/lib/format-date.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export const formatDateTime = (value: string) => {
|
||||||
|
const date = new Date(value)
|
||||||
|
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat("ru-RU", {
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
}).format(date)
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { Anchor, Badge, Code, Group, Paper, ScrollArea, Skeleton, Stack, Table, Text, Title } from "@mantine/core"
|
||||||
|
|
||||||
|
import { ASSET_STATUS_COLORS, VARIANT_STATUS_COLORS } from "../../config/dashboard.config"
|
||||||
|
import { formatDateTime } from "../../lib/format-date"
|
||||||
|
import type { AssetDetailPanelProps } from "./types/asset-detail-panel-props.type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Детали выбранного asset и его variants.
|
||||||
|
*
|
||||||
|
* Используется для:
|
||||||
|
* - отображения source metadata
|
||||||
|
* - отображения статусов generated variants
|
||||||
|
*/
|
||||||
|
export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
||||||
|
const { overview, publicId } = props
|
||||||
|
const { asset, variants } = overview
|
||||||
|
|
||||||
|
if (!publicId) {
|
||||||
|
return (
|
||||||
|
<Paper bg="white" p="xl" radius="xl" shadow="xs" withBorder>
|
||||||
|
<Title order={2} size="h3">
|
||||||
|
Asset detail
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" mt="sm">
|
||||||
|
Выберите asset из таблицы, чтобы увидеть source URL и variants.
|
||||||
|
</Text>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper bg="white" p="xl" radius="xl" shadow="xs" withBorder>
|
||||||
|
<Group align="start" justify="space-between" mb="lg">
|
||||||
|
<div>
|
||||||
|
<Title order={2} size="h3">
|
||||||
|
Asset detail
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
{publicId}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{asset ? (
|
||||||
|
<Badge color={ASSET_STATUS_COLORS[asset.status] ?? "gray"} radius="xl" variant="light">
|
||||||
|
{asset.status}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{overview.isLoading ? (
|
||||||
|
<Skeleton height={260} radius="lg" />
|
||||||
|
) : asset ? (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Stack gap={6}>
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
Source URL
|
||||||
|
</Text>
|
||||||
|
<Anchor href={asset.sourceUrl} target="_blank">
|
||||||
|
{asset.sourceUrl}
|
||||||
|
</Anchor>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<Badge color="violet" radius="xl" variant="light">
|
||||||
|
v{asset.currentVersion}
|
||||||
|
</Badge>
|
||||||
|
<Badge color="gray" radius="xl" variant="light">
|
||||||
|
{asset.sourceHost}
|
||||||
|
</Badge>
|
||||||
|
<Badge color="gray" radius="xl" variant="light">
|
||||||
|
updated {formatDateTime(asset.updatedAt)}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Title order={3} size="h4">
|
||||||
|
Variants
|
||||||
|
</Title>
|
||||||
|
<Badge radius="xl" variant="light">
|
||||||
|
{variants.length}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{variants.length > 0 ? (
|
||||||
|
<ScrollArea>
|
||||||
|
<Table verticalSpacing="sm">
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Preset</Table.Th>
|
||||||
|
<Table.Th>Format</Table.Th>
|
||||||
|
<Table.Th>Size</Table.Th>
|
||||||
|
<Table.Th>Status</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{variants.map((variant) => (
|
||||||
|
<Table.Tr key={variant.id}>
|
||||||
|
<Table.Td>
|
||||||
|
<Code>{variant.preset}</Code>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>{variant.format}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{variant.width}x{variant.height || "auto"} q{variant.quality}
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge color={VARIANT_STATUS_COLORS[variant.status] ?? "gray"} radius="xl" variant="light">
|
||||||
|
{variant.status}
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</ScrollArea>
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed">Variants для текущей версии пока не созданы.</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed">Asset не найден или ещё загружается.</Text>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { AssetDetailPanel } from "./asset-detail-panel"
|
||||||
|
export type { AssetDetailPanelProps } from "./types/asset-detail-panel-props.type"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { AssetOverview } from "business/assets"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры AssetDetailPanel.
|
||||||
|
*/
|
||||||
|
export type AssetDetailPanelProps = {
|
||||||
|
/** Данные выбранного asset. */
|
||||||
|
overview: AssetOverview
|
||||||
|
/** Выбранный publicId. */
|
||||||
|
publicId: string | null
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Badge, Group, Paper, ScrollArea, Skeleton, Table, Text, Title } from "@mantine/core"
|
||||||
|
|
||||||
|
import { ASSET_STATUS_COLORS } from "../../config/dashboard.config"
|
||||||
|
import { formatDateTime } from "../../lib/format-date"
|
||||||
|
import type { AssetsTableProps } from "./types/assets-table-props.type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Таблица assets.
|
||||||
|
*
|
||||||
|
* Используется для:
|
||||||
|
* - отображения реального списка assets из Backend API
|
||||||
|
* - выбора asset для detail-панели
|
||||||
|
*/
|
||||||
|
export const AssetsTable = (props: AssetsTableProps) => {
|
||||||
|
const { assets, isLoading, onSelect, selectedPublicId } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper bg="white" p="xl" radius="xl" shadow="xs" withBorder>
|
||||||
|
<Group justify="space-between" mb="lg">
|
||||||
|
<div>
|
||||||
|
<Title order={2} size="h3">
|
||||||
|
Assets
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
Последние зарегистрированные исходные изображения.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
<Badge color="violet" radius="xl" variant="light">
|
||||||
|
{assets.length} loaded
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<Skeleton height={220} radius="lg" />
|
||||||
|
) : assets.length > 0 ? (
|
||||||
|
<ScrollArea>
|
||||||
|
<Table highlightOnHover verticalSpacing="sm">
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>publicId</Table.Th>
|
||||||
|
<Table.Th>Status</Table.Th>
|
||||||
|
<Table.Th>Version</Table.Th>
|
||||||
|
<Table.Th>Host</Table.Th>
|
||||||
|
<Table.Th>Updated</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{assets.map((asset) => (
|
||||||
|
<Table.Tr
|
||||||
|
bg={asset.publicId === selectedPublicId ? "violet.0" : undefined}
|
||||||
|
key={asset.id}
|
||||||
|
onClick={() => onSelect(asset.publicId)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fw={800}>{asset.publicId}</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Badge color={ASSET_STATUS_COLORS[asset.status] ?? "gray"} radius="xl" variant="light">
|
||||||
|
{asset.status}
|
||||||
|
</Badge>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>v{asset.currentVersion}</Table.Td>
|
||||||
|
<Table.Td>{asset.sourceHost}</Table.Td>
|
||||||
|
<Table.Td>{formatDateTime(asset.updatedAt)}</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</ScrollArea>
|
||||||
|
) : (
|
||||||
|
<Text c="dimmed">Assets пока не зарегистрированы.</Text>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { AssetsTable } from "./assets-table"
|
||||||
|
export type { AssetsTableProps } from "./types/assets-table-props.type"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { AssetsDashboard } from "business/assets"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры AssetsTable.
|
||||||
|
*/
|
||||||
|
export type AssetsTableProps = {
|
||||||
|
/** Список assets. */
|
||||||
|
assets: AssetsDashboard["assets"]
|
||||||
|
/** Признак загрузки списка. */
|
||||||
|
isLoading: boolean
|
||||||
|
/** Callback выбора asset. */
|
||||||
|
onSelect: (publicId: string) => void
|
||||||
|
/** Выбранный publicId. */
|
||||||
|
selectedPublicId: string | null
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { Button, Group, Modal, Stack, Text, TextInput } from "@mantine/core"
|
||||||
|
import { useForm } from "@mantine/form"
|
||||||
|
import { notifications } from "@mantine/notifications"
|
||||||
|
|
||||||
|
import type { CreateAssetInput } from "business/assets"
|
||||||
|
import type { CreateAssetModalProps } from "./types/create-asset-modal-props.type"
|
||||||
|
|
||||||
|
type CreateAssetFormValues = {
|
||||||
|
publicId: string
|
||||||
|
sourceUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SOURCE_URL_EXAMPLE = "https://storage.yandexcloud.net/shared1318/img/1.jpg"
|
||||||
|
|
||||||
|
const toErrorMessage = (error: unknown) => (error instanceof Error ? error.message : "Неизвестная ошибка")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal создания asset.
|
||||||
|
*
|
||||||
|
* Используется для:
|
||||||
|
* - регистрации source image
|
||||||
|
* - запуска первого write-сценария admin MVP
|
||||||
|
*/
|
||||||
|
export const CreateAssetModal = (props: CreateAssetModalProps) => {
|
||||||
|
const { action, onClose, onCreated, opened } = props
|
||||||
|
|
||||||
|
const form = useForm<CreateAssetFormValues>({
|
||||||
|
initialValues: {
|
||||||
|
publicId: "",
|
||||||
|
sourceUrl: SOURCE_URL_EXAMPLE,
|
||||||
|
},
|
||||||
|
validate: {
|
||||||
|
sourceUrl: (value) => {
|
||||||
|
if (!value.trim()) {
|
||||||
|
return "Укажите source URL"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(value)
|
||||||
|
return url.protocol === "http:" || url.protocol === "https:" ? null : "URL должен быть http/https"
|
||||||
|
} catch {
|
||||||
|
return "Некорректный URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!action.isCreating) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = form.onSubmit(async (values) => {
|
||||||
|
const publicId = values.publicId.trim()
|
||||||
|
const input: CreateAssetInput = {
|
||||||
|
sourceUrl: values.sourceUrl.trim(),
|
||||||
|
...(publicId ? { publicId } : {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createdAsset = await action.createAsset(input)
|
||||||
|
notifications.show({
|
||||||
|
color: "green",
|
||||||
|
message: `Asset ${createdAsset.publicId} зарегистрирован`,
|
||||||
|
title: "Asset created",
|
||||||
|
})
|
||||||
|
form.reset()
|
||||||
|
onCreated(createdAsset.publicId)
|
||||||
|
onClose()
|
||||||
|
} catch (error) {
|
||||||
|
notifications.show({
|
||||||
|
color: "red",
|
||||||
|
message: toErrorMessage(error),
|
||||||
|
title: "Не удалось создать asset",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal centered onClose={handleClose} opened={opened} radius="xl" title="Create asset">
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
Backend создаст asset и первую immutable source version. Public ID можно оставить пустым.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Public ID"
|
||||||
|
placeholder="asset_demo"
|
||||||
|
{...form.getInputProps("publicId")}
|
||||||
|
disabled={action.isCreating}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Source URL"
|
||||||
|
placeholder={SOURCE_URL_EXAMPLE}
|
||||||
|
required
|
||||||
|
{...form.getInputProps("sourceUrl")}
|
||||||
|
disabled={action.isCreating}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button color="gray" disabled={action.isCreating} onClick={handleClose} variant="subtle">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button loading={action.isCreating} type="submit">
|
||||||
|
Create asset
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { CreateAssetModal } from "./create-asset-modal"
|
||||||
|
export type { CreateAssetModalProps } from "./types/create-asset-modal-props.type"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { CreateAssetAction } from "business/assets"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры CreateAssetModal.
|
||||||
|
*/
|
||||||
|
export type CreateAssetModalProps = {
|
||||||
|
/** Сценарий создания asset. */
|
||||||
|
action: CreateAssetAction
|
||||||
|
/** Callback закрытия modal. */
|
||||||
|
onClose: () => void
|
||||||
|
/** Callback успешного создания asset. */
|
||||||
|
onCreated: (publicId: string) => void
|
||||||
|
/** Открыта ли modal. */
|
||||||
|
opened: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { PresetsPanel } from "./presets-panel"
|
||||||
|
export type { PresetsPanelProps } from "./types/presets-panel-props.type"
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { Badge, Code, Group, Paper, SimpleGrid, Skeleton, Stack, Text, Title } from "@mantine/core"
|
||||||
|
|
||||||
|
import type { PresetsPanelProps } from "./types/presets-panel-props.type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Панель presets и allowlist hosts.
|
||||||
|
*
|
||||||
|
* Используется для:
|
||||||
|
* - отображения static presets
|
||||||
|
* - отображения custom transform limits
|
||||||
|
*/
|
||||||
|
export const PresetsPanel = (props: PresetsPanelProps) => {
|
||||||
|
const { allowedSourceHosts, custom, isLoading, presets } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper bg="white" p="xl" radius="xl" shadow="xs" withBorder>
|
||||||
|
<Group justify="space-between" mb="lg">
|
||||||
|
<div>
|
||||||
|
<Title order={2} size="h3">
|
||||||
|
Presets
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
Static transform profiles, formats, qualities и source allowlist.
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{custom ? (
|
||||||
|
<Badge color={custom.enabled ? "green" : "gray"} radius="xl" variant="light">
|
||||||
|
custom {custom.enabled ? "enabled" : "disabled"}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<Skeleton height={180} radius="lg" />
|
||||||
|
) : (
|
||||||
|
<Stack gap="lg">
|
||||||
|
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
|
||||||
|
{presets.map((preset) => (
|
||||||
|
<Paper bg="gray.0" key={preset.name} p="md" radius="lg" withBorder>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Text fw={800}>{preset.name}</Text>
|
||||||
|
<Badge color="violet" radius="xl" variant="light">
|
||||||
|
{preset.mode}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
<Text c="dimmed" fz="sm">
|
||||||
|
{preset.resize}, q{preset.quality}
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm">formats: {preset.formats.join(", ")}</Text>
|
||||||
|
<Text fz="sm">
|
||||||
|
sizes: {preset.widths?.join(", ") ?? `${preset.width}x${preset.height}`}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{custom ? (
|
||||||
|
<Group gap="xs">
|
||||||
|
<Badge radius="xl" variant="light">
|
||||||
|
max {custom.maxWidth}x{custom.maxHeight}
|
||||||
|
</Badge>
|
||||||
|
<Badge radius="xl" variant="light">
|
||||||
|
q{custom.quality}
|
||||||
|
</Badge>
|
||||||
|
<Badge radius="xl" variant="light">
|
||||||
|
{custom.formats.join(", ")}
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Stack gap={6}>
|
||||||
|
<Text c="dimmed" fz="sm" fw={700}>
|
||||||
|
Allowed source hosts
|
||||||
|
</Text>
|
||||||
|
<Group gap="xs">
|
||||||
|
{allowedSourceHosts.map((host) => (
|
||||||
|
<Code key={host}>{host}</Code>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { AssetsDashboard } from "business/assets"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры PresetsPanel.
|
||||||
|
*/
|
||||||
|
export type PresetsPanelProps = {
|
||||||
|
/** Разрешённые source hosts. */
|
||||||
|
allowedSourceHosts: string[]
|
||||||
|
/** Custom transform config. */
|
||||||
|
custom: AssetsDashboard["custom"]
|
||||||
|
/** Признак загрузки presets. */
|
||||||
|
isLoading: boolean
|
||||||
|
/** Список presets. */
|
||||||
|
presets: AssetsDashboard["presets"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { SummaryCards } from "./summary-cards"
|
||||||
|
export type { SummaryCardsProps } from "./types/summary-cards-props.type"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Paper, SimpleGrid, Skeleton, Stack, Text } from "@mantine/core"
|
||||||
|
|
||||||
|
import { DASHBOARD_CARDS } from "../../config/dashboard.config"
|
||||||
|
import type { SummaryCardsProps } from "./types/summary-cards-props.type"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Карточки сводных метрик dashboard.
|
||||||
|
*
|
||||||
|
* Используется для:
|
||||||
|
* - отображения количества assets, presets и hosts
|
||||||
|
* - компактного статуса загрузки данных
|
||||||
|
*/
|
||||||
|
export const SummaryCards = (props: SummaryCardsProps) => {
|
||||||
|
const { isLoading, summary } = props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SimpleGrid cols={{ base: 1, md: 3 }} spacing="md">
|
||||||
|
{DASHBOARD_CARDS.map((card) => (
|
||||||
|
<Paper bg="white" key={card.title} p="xl" radius="xl" shadow="xs" withBorder>
|
||||||
|
<Stack gap="sm">
|
||||||
|
{isLoading ? (
|
||||||
|
<Skeleton height={42} width={86} />
|
||||||
|
) : (
|
||||||
|
<Text c="violet.7" fw={850} fz={42} lh={0.9}>
|
||||||
|
{summary[card.metric]}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<Text fw={800}>{card.title}</Text>
|
||||||
|
<Text c="dimmed" fz="sm" lh={1.55}>
|
||||||
|
{card.description}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { AssetsDashboard } from "business/assets"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Параметры SummaryCards.
|
||||||
|
*/
|
||||||
|
export type SummaryCardsProps = {
|
||||||
|
/** Признак загрузки данных. */
|
||||||
|
isLoading: boolean
|
||||||
|
/** Сводные метрики dashboard. */
|
||||||
|
summary: AssetsDashboard["summary"]
|
||||||
|
}
|
||||||
@@ -1,79 +1,44 @@
|
|||||||
.root {
|
.root {
|
||||||
display: grid;
|
min-width: 0;
|
||||||
gap: var(--space-4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
padding: var(--space-6);
|
overflow: hidden;
|
||||||
border: 1px solid var(--color-border);
|
background:
|
||||||
border-radius: var(--radius-5);
|
linear-gradient(135deg, rgb(255 255 255 / 92%), rgb(255 255 255 / 72%)),
|
||||||
background: var(--color-surface);
|
radial-gradient(circle at 92% 8%, rgb(123 76 255 / 18%), transparent 18rem);
|
||||||
box-shadow: var(--shadow-panel);
|
}
|
||||||
|
|
||||||
@media (--md) {
|
.heroContent {
|
||||||
padding: var(--space-8);
|
max-width: 54rem;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
margin: 0 0 var(--space-4);
|
margin-bottom: var(--space-4);
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
font-weight: 800;
|
font-weight: 850;
|
||||||
letter-spacing: 0.22em;
|
letter-spacing: 0.22em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
max-width: 48rem;
|
max-width: 50rem;
|
||||||
margin: 0;
|
font-size: clamp(2.75rem, 7vw, 5.75rem);
|
||||||
font-size: clamp(2.75rem, 7vw, 5.5rem);
|
|
||||||
line-height: 0.9;
|
line-height: 0.9;
|
||||||
letter-spacing: -0.07em;
|
letter-spacing: -0.075em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lead {
|
.lead {
|
||||||
max-width: 43rem;
|
max-width: 42rem;
|
||||||
margin: var(--space-5) 0 0;
|
margin-top: var(--space-5);
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
font-size: 1.0625rem;
|
font-size: 1.0625rem;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.primaryAction {
|
||||||
display: grid;
|
box-shadow: var(--shadow-soft);
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: var(--space-3);
|
|
||||||
|
|
||||||
@media (--md) {
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
min-height: 9.5rem;
|
|
||||||
padding: var(--space-5);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-4);
|
|
||||||
background: var(--color-surface-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardTitle {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cardDescription {
|
|
||||||
margin: var(--space-3) 0 0;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
line-height: 1.55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pipeline {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pipelineStep {
|
.pipelineStep {
|
||||||
@@ -85,3 +50,13 @@
|
|||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workbench {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-4);
|
||||||
|
|
||||||
|
@media (--md) {
|
||||||
|
grid-template-columns: minmax(0, 1.35fr) minmax(22rem, 0.65fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
|
@import "@mantine/core/styles.css";
|
||||||
|
@import "@mantine/notifications/styles.css";
|
||||||
@import "./variables.css";
|
@import "./variables.css";
|
||||||
@import "./reset.css";
|
@import "./reset.css";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import react from "@vitejs/plugin-react"
|
|||||||
import { defineConfig } from "vite"
|
import { defineConfig } from "vite"
|
||||||
|
|
||||||
const srcPath = (path: string) => fileURLToPath(new URL(`./src/${path}`, import.meta.url))
|
const srcPath = (path: string) => fileURLToPath(new URL(`./src/${path}`, import.meta.url))
|
||||||
|
const backendProxyTarget = process.env.ADMIN_BACKEND_PROXY_TARGET ?? "http://localhost:3001"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
@@ -19,4 +20,12 @@ export default defineConfig({
|
|||||||
shared: srcPath("shared"),
|
shared: srcPath("shared"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
changeOrigin: true,
|
||||||
|
target: backendProxyTarget,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"admin:build": "pnpm --filter @image-platform/admin build",
|
"admin:build": "pnpm --filter @image-platform/admin build",
|
||||||
|
"admin:codegen": "pnpm --filter @image-platform/admin codegen:backend-api",
|
||||||
"admin:dev": "pnpm --filter @image-platform/admin dev",
|
"admin:dev": "pnpm --filter @image-platform/admin dev",
|
||||||
"admin:preview": "pnpm --filter @image-platform/admin preview",
|
"admin:preview": "pnpm --filter @image-platform/admin preview",
|
||||||
"admin:typecheck": "pnpm --filter @image-platform/admin typecheck",
|
"admin:typecheck": "pnpm --filter @image-platform/admin typecheck",
|
||||||
|
|||||||
343
pnpm-lock.yaml
generated
343
pnpm-lock.yaml
generated
@@ -10,6 +10,18 @@ importers:
|
|||||||
|
|
||||||
apps/admin:
|
apps/admin:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@mantine/core':
|
||||||
|
specifier: ^9.1.1
|
||||||
|
version: 9.1.1(@mantine/hooks@9.1.1(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
|
'@mantine/form':
|
||||||
|
specifier: ^9.1.1
|
||||||
|
version: 9.1.1(react@19.2.5)
|
||||||
|
'@mantine/hooks':
|
||||||
|
specifier: ^9.1.1
|
||||||
|
version: 9.1.1(react@19.2.5)
|
||||||
|
'@mantine/notifications':
|
||||||
|
specifier: ^9.1.1
|
||||||
|
version: 9.1.1(@mantine/core@9.1.1(@mantine/hooks@9.1.1(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@mantine/hooks@9.1.1(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
@@ -19,6 +31,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.2.5
|
specifier: ^19.2.5
|
||||||
version: 19.2.5(react@19.2.5)
|
version: 19.2.5(react@19.2.5)
|
||||||
|
swr:
|
||||||
|
specifier: ^2.4.1
|
||||||
|
version: 2.4.1(react@19.2.5)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@csstools/postcss-global-data':
|
'@csstools/postcss-global-data':
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
@@ -415,6 +430,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/runtime@7.29.2':
|
||||||
|
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@borewit/text-codec@0.2.2':
|
'@borewit/text-codec@0.2.2':
|
||||||
resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==}
|
resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==}
|
||||||
|
|
||||||
@@ -946,6 +965,27 @@ packages:
|
|||||||
'@fastify/proxy-addr@5.1.0':
|
'@fastify/proxy-addr@5.1.0':
|
||||||
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
|
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
|
||||||
|
|
||||||
|
'@floating-ui/core@1.7.5':
|
||||||
|
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.7.6':
|
||||||
|
resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==}
|
||||||
|
|
||||||
|
'@floating-ui/react-dom@2.1.8':
|
||||||
|
resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
react-dom: '>=16.8.0'
|
||||||
|
|
||||||
|
'@floating-ui/react@0.27.19':
|
||||||
|
resolution: {integrity: sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=17.0.0'
|
||||||
|
react-dom: '>=17.0.0'
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.11':
|
||||||
|
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
||||||
|
|
||||||
'@inquirer/ansi@1.0.2':
|
'@inquirer/ansi@1.0.2':
|
||||||
resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==}
|
resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1109,6 +1149,36 @@ packages:
|
|||||||
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
'@mantine/core@9.1.1':
|
||||||
|
resolution: {integrity: sha512-vClOZdCeZ4oLYuA/3jAOgKGQ6dXbF6ZkzpYz09Gied9nZpB7HcQeb3dcMh8UPBE4f+EM7KlYWk6dch7GoASeaA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@mantine/hooks': 9.1.1
|
||||||
|
react: ^19.2.0
|
||||||
|
react-dom: ^19.2.0
|
||||||
|
|
||||||
|
'@mantine/form@9.1.1':
|
||||||
|
resolution: {integrity: sha512-xmebZ3s8GGMrCOPOaOwA+gQkdgNVfT2F9kBtkjAbRoZrMoY+vYFbiPWbIvWFl8pU1jBslYZrj+M0PIawJmFOdQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^19.2.0
|
||||||
|
|
||||||
|
'@mantine/hooks@9.1.1':
|
||||||
|
resolution: {integrity: sha512-tTJK73nGFyy1v214TLdvBq0be7QCoc6osfbXVuJgOH3YG85lWk9Mvvor6k+w6hC6HXSqKMqLKePyiGm83xGcMg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^19.2.0
|
||||||
|
|
||||||
|
'@mantine/notifications@9.1.1':
|
||||||
|
resolution: {integrity: sha512-ZfcEMMDp0BQ+yKmVp8ifPXLKej8pv9TcaRnmy2CZ07USD61E9LH5ClRAP/hxQuCyf/qLb5BPHsI7+f3K8uhj4Q==}
|
||||||
|
peerDependencies:
|
||||||
|
'@mantine/core': 9.1.1
|
||||||
|
'@mantine/hooks': 9.1.1
|
||||||
|
react: ^19.2.0
|
||||||
|
react-dom: ^19.2.0
|
||||||
|
|
||||||
|
'@mantine/store@9.1.1':
|
||||||
|
resolution: {integrity: sha512-kbxEU8wVGbobHlmQmk0lu9M+xCILKjuAPcMAshgzPznGLfXeE9zrB0gNT2cbk11Ik8dlV9J6Vsn9cuACyOSpfQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^19.2.0
|
||||||
|
|
||||||
'@microsoft/tsdoc@0.16.0':
|
'@microsoft/tsdoc@0.16.0':
|
||||||
resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==}
|
resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==}
|
||||||
|
|
||||||
@@ -1531,6 +1601,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==}
|
resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0':
|
||||||
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1969,6 +2042,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
detect-node-es@1.1.0:
|
||||||
|
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||||
|
|
||||||
|
dom-helpers@5.2.1:
|
||||||
|
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||||
|
|
||||||
drizzle-kit@0.31.10:
|
drizzle-kit@0.31.10:
|
||||||
resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==}
|
resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -2251,6 +2330,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
get-nonce@1.0.1:
|
||||||
|
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
get-proto@1.0.1:
|
get-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2366,6 +2449,10 @@ packages:
|
|||||||
jsonfile@6.2.1:
|
jsonfile@6.2.1:
|
||||||
resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==}
|
resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==}
|
||||||
|
|
||||||
|
klona@2.0.6:
|
||||||
|
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
|
||||||
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
light-my-request@6.6.0:
|
light-my-request@6.6.0:
|
||||||
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==}
|
||||||
|
|
||||||
@@ -2457,6 +2544,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
loose-envify@1.4.0:
|
||||||
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
lru-cache@11.3.6:
|
lru-cache@11.3.6:
|
||||||
resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==}
|
resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -2707,6 +2798,9 @@ packages:
|
|||||||
process-warning@5.0.0:
|
process-warning@5.0.0:
|
||||||
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==}
|
||||||
|
|
||||||
|
prop-types@15.8.1:
|
||||||
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -2735,6 +2829,51 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.2.5
|
react: ^19.2.5
|
||||||
|
|
||||||
|
react-is@16.13.1:
|
||||||
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
react-number-format@5.4.5:
|
||||||
|
resolution: {integrity: sha512-y8O2yHHj3w0aE9XO8d2BCcUOOdQTRSVq+WIuMlLVucAm5XNjJAy+BoOJiuQMldVYVOKTMyvVNfnbl2Oqp+YxGw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
react-remove-scroll-bar@2.3.8:
|
||||||
|
resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
react-remove-scroll@2.7.2:
|
||||||
|
resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
react-style-singleton@2.2.3:
|
||||||
|
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
react-transition-group@4.4.5:
|
||||||
|
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.6.0'
|
||||||
|
react-dom: '>=16.6.0'
|
||||||
|
|
||||||
react@19.2.5:
|
react@19.2.5:
|
||||||
resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
|
resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2937,10 +3076,22 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
express: '>=4.0.0 || >=5.0.0-beta'
|
express: '>=4.0.0 || >=5.0.0-beta'
|
||||||
|
|
||||||
|
swr@2.4.1:
|
||||||
|
resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
symbol-observable@4.0.0:
|
symbol-observable@4.0.0:
|
||||||
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
|
resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
|
|
||||||
|
tabbable@6.4.0:
|
||||||
|
resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
|
||||||
|
|
||||||
|
tagged-tag@1.0.0:
|
||||||
|
resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
tapable@2.3.3:
|
tapable@2.3.3:
|
||||||
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
|
resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3002,6 +3153,10 @@ packages:
|
|||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
type-fest@5.6.0:
|
||||||
|
resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
type-is@1.6.18:
|
type-is@1.6.18:
|
||||||
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -3054,6 +3209,31 @@ packages:
|
|||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
use-callback-ref@1.3.3:
|
||||||
|
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
use-sidecar@1.1.3:
|
||||||
|
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0:
|
||||||
|
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@@ -3638,6 +3818,8 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5': {}
|
'@babel/helper-validator-identifier@7.28.5': {}
|
||||||
|
|
||||||
|
'@babel/runtime@7.29.2': {}
|
||||||
|
|
||||||
'@borewit/text-codec@0.2.2': {}
|
'@borewit/text-codec@0.2.2': {}
|
||||||
|
|
||||||
'@colors/colors@1.5.0':
|
'@colors/colors@1.5.0':
|
||||||
@@ -3944,6 +4126,31 @@ snapshots:
|
|||||||
'@fastify/forwarded': 3.0.1
|
'@fastify/forwarded': 3.0.1
|
||||||
ipaddr.js: 2.4.0
|
ipaddr.js: 2.4.0
|
||||||
|
|
||||||
|
'@floating-ui/core@1.7.5':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/utils': 0.2.11
|
||||||
|
|
||||||
|
'@floating-ui/dom@1.7.6':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/core': 1.7.5
|
||||||
|
'@floating-ui/utils': 0.2.11
|
||||||
|
|
||||||
|
'@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/dom': 1.7.6
|
||||||
|
react: 19.2.5
|
||||||
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
|
|
||||||
|
'@floating-ui/react@0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
|
'@floating-ui/utils': 0.2.11
|
||||||
|
react: 19.2.5
|
||||||
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
|
tabbable: 6.4.0
|
||||||
|
|
||||||
|
'@floating-ui/utils@0.2.11': {}
|
||||||
|
|
||||||
'@inquirer/ansi@1.0.2': {}
|
'@inquirer/ansi@1.0.2': {}
|
||||||
|
|
||||||
'@inquirer/checkbox@4.3.2(@types/node@24.12.2)':
|
'@inquirer/checkbox@4.3.2(@types/node@24.12.2)':
|
||||||
@@ -4105,6 +4312,43 @@ snapshots:
|
|||||||
|
|
||||||
'@lukeed/csprng@1.1.0': {}
|
'@lukeed/csprng@1.1.0': {}
|
||||||
|
|
||||||
|
'@mantine/core@9.1.1(@mantine/hooks@9.1.1(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||||
|
dependencies:
|
||||||
|
'@floating-ui/react': 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
|
'@mantine/hooks': 9.1.1(react@19.2.5)
|
||||||
|
clsx: 2.1.1
|
||||||
|
react: 19.2.5
|
||||||
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
|
react-number-format: 5.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
|
react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5)
|
||||||
|
type-fest: 5.6.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@types/react'
|
||||||
|
|
||||||
|
'@mantine/form@9.1.1(react@19.2.5)':
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
klona: 2.0.6
|
||||||
|
react: 19.2.5
|
||||||
|
|
||||||
|
'@mantine/hooks@9.1.1(react@19.2.5)':
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.5
|
||||||
|
|
||||||
|
'@mantine/notifications@9.1.1(@mantine/core@9.1.1(@mantine/hooks@9.1.1(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@mantine/hooks@9.1.1(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||||
|
dependencies:
|
||||||
|
'@mantine/core': 9.1.1(@mantine/hooks@9.1.1(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
|
'@mantine/hooks': 9.1.1(react@19.2.5)
|
||||||
|
'@mantine/store': 9.1.1(react@19.2.5)
|
||||||
|
react: 19.2.5
|
||||||
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
|
react-transition-group: 4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||||
|
|
||||||
|
'@mantine/store@9.1.1(react@19.2.5)':
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.5
|
||||||
|
|
||||||
'@microsoft/tsdoc@0.16.0': {}
|
'@microsoft/tsdoc@0.16.0': {}
|
||||||
|
|
||||||
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
||||||
@@ -4605,6 +4849,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -5054,6 +5300,13 @@ snapshots:
|
|||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
|
detect-node-es@1.1.0: {}
|
||||||
|
|
||||||
|
dom-helpers@5.2.1:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.29.2
|
||||||
|
csstype: 3.2.3
|
||||||
|
|
||||||
drizzle-kit@0.31.10:
|
drizzle-kit@0.31.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@drizzle-team/brocli': 0.10.2
|
'@drizzle-team/brocli': 0.10.2
|
||||||
@@ -5373,6 +5626,8 @@ snapshots:
|
|||||||
hasown: 2.0.3
|
hasown: 2.0.3
|
||||||
math-intrinsics: 1.1.0
|
math-intrinsics: 1.1.0
|
||||||
|
|
||||||
|
get-nonce@1.0.1: {}
|
||||||
|
|
||||||
get-proto@1.0.1:
|
get-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
dunder-proto: 1.0.1
|
dunder-proto: 1.0.1
|
||||||
@@ -5471,6 +5726,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|
||||||
|
klona@2.0.6: {}
|
||||||
|
|
||||||
light-my-request@6.6.0:
|
light-my-request@6.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
cookie: 1.1.1
|
cookie: 1.1.1
|
||||||
@@ -5539,6 +5796,10 @@ snapshots:
|
|||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
is-unicode-supported: 0.1.0
|
is-unicode-supported: 0.1.0
|
||||||
|
|
||||||
|
loose-envify@1.4.0:
|
||||||
|
dependencies:
|
||||||
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
lru-cache@11.3.6: {}
|
lru-cache@11.3.6: {}
|
||||||
|
|
||||||
magic-string@0.30.17:
|
magic-string@0.30.17:
|
||||||
@@ -5767,6 +6028,12 @@ snapshots:
|
|||||||
|
|
||||||
process-warning@5.0.0: {}
|
process-warning@5.0.0: {}
|
||||||
|
|
||||||
|
prop-types@15.8.1:
|
||||||
|
dependencies:
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
object-assign: 4.1.1
|
||||||
|
react-is: 16.13.1
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
@@ -5794,6 +6061,49 @@ snapshots:
|
|||||||
react: 19.2.5
|
react: 19.2.5
|
||||||
scheduler: 0.27.0
|
scheduler: 0.27.0
|
||||||
|
|
||||||
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
|
react-number-format@5.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.5
|
||||||
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
|
|
||||||
|
react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.5):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.5
|
||||||
|
react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5)
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
|
||||||
|
react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.5):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.5
|
||||||
|
react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.5)
|
||||||
|
react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5)
|
||||||
|
tslib: 2.8.1
|
||||||
|
use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.5)
|
||||||
|
use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.5)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
|
||||||
|
react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.5):
|
||||||
|
dependencies:
|
||||||
|
get-nonce: 1.0.1
|
||||||
|
react: 19.2.5
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
|
||||||
|
react-transition-group@4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.29.2
|
||||||
|
dom-helpers: 5.2.1
|
||||||
|
loose-envify: 1.4.0
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 19.2.5
|
||||||
|
react-dom: 19.2.5(react@19.2.5)
|
||||||
|
|
||||||
react@19.2.5: {}
|
react@19.2.5: {}
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
readable-stream@3.6.2:
|
||||||
@@ -6018,8 +6328,18 @@ snapshots:
|
|||||||
express: 5.2.1
|
express: 5.2.1
|
||||||
swagger-ui-dist: 5.32.5
|
swagger-ui-dist: 5.32.5
|
||||||
|
|
||||||
|
swr@2.4.1(react@19.2.5):
|
||||||
|
dependencies:
|
||||||
|
dequal: 2.0.3
|
||||||
|
react: 19.2.5
|
||||||
|
use-sync-external-store: 1.6.0(react@19.2.5)
|
||||||
|
|
||||||
symbol-observable@4.0.0: {}
|
symbol-observable@4.0.0: {}
|
||||||
|
|
||||||
|
tabbable@6.4.0: {}
|
||||||
|
|
||||||
|
tagged-tag@1.0.0: {}
|
||||||
|
|
||||||
tapable@2.3.3: {}
|
tapable@2.3.3: {}
|
||||||
|
|
||||||
terser-webpack-plugin@5.5.0(webpack@5.106.0):
|
terser-webpack-plugin@5.5.0(webpack@5.106.0):
|
||||||
@@ -6078,6 +6398,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
type-fest@5.6.0:
|
||||||
|
dependencies:
|
||||||
|
tagged-tag: 1.0.0
|
||||||
|
|
||||||
type-is@1.6.18:
|
type-is@1.6.18:
|
||||||
dependencies:
|
dependencies:
|
||||||
media-typer: 0.3.0
|
media-typer: 0.3.0
|
||||||
@@ -6119,6 +6443,25 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.5
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
|
||||||
|
use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.5):
|
||||||
|
dependencies:
|
||||||
|
detect-node-es: 1.1.0
|
||||||
|
react: 19.2.5
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.2.14
|
||||||
|
|
||||||
|
use-sync-external-store@1.6.0(react@19.2.5):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.5
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user