feat: добавить preview image pipeline в admin
- добавлен polling variants и ручной refresh выбранного asset\n- добавлен picture/srcset preview с выбором preset\n- добавлен URL-state и copy actions для рабочих ссылок
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useAssetPicture } from "./hooks/use-asset-picture.hook"
|
||||
import { useAssetOverview } from "./hooks/use-asset-overview.hook"
|
||||
import { useAssetsDashboard } from "./hooks/use-assets-dashboard.hook"
|
||||
import { useCreateAsset } from "./hooks/use-create-asset.hook"
|
||||
@@ -11,6 +12,7 @@ import type { AssetsFactory } from "./types/assets-factory.type"
|
||||
export const assetsFactory: AssetsFactory = () => {
|
||||
return {
|
||||
useAssetOverview,
|
||||
useAssetPicture,
|
||||
useAssetsDashboard,
|
||||
useCreateAsset,
|
||||
useCreateAssetVersion,
|
||||
|
||||
@@ -4,3 +4,7 @@ export const ASSETS_DASHBOARD_LIST_PARAMS = {
|
||||
limit: "20",
|
||||
offset: "0",
|
||||
} satisfies ListAssetsParams
|
||||
|
||||
export const ASSET_PICTURE_SIZES = "(min-width: 992px) 34vw, 100vw"
|
||||
|
||||
export const ASSET_VARIANTS_POLLING_MS = 2000
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { useGetAsset, useGetAssetVariants } from "infra/backend-api"
|
||||
import { useGetAsset, useGetAssetVariants, type AssetVariantsResponseDto } from "infra/backend-api"
|
||||
|
||||
import { ASSET_VARIANTS_POLLING_MS } from "../config/assets.config"
|
||||
import type { AssetOverview } from "../types/assets-api.type"
|
||||
|
||||
const isRunningVariantStatus = (status: string) => status === "pending" || status === "processing"
|
||||
|
||||
const hasRunningVariants = (variants: AssetVariantsResponseDto["variants"] = []) =>
|
||||
variants.some((variant) => isRunningVariantStatus(variant.status))
|
||||
|
||||
/**
|
||||
* Данные выбранного asset и его variants.
|
||||
*/
|
||||
@@ -10,12 +16,23 @@ export const useAssetOverview = (publicId: string | null): AssetOverview => {
|
||||
const variantsQuery = useGetAssetVariants(
|
||||
publicId,
|
||||
assetQuery.data?.currentVersion ? String(assetQuery.data.currentVersion) : undefined,
|
||||
{
|
||||
refreshInterval: (data) => (hasRunningVariants(data?.variants) ? ASSET_VARIANTS_POLLING_MS : 0),
|
||||
},
|
||||
)
|
||||
const variants = variantsQuery.data?.variants ?? []
|
||||
|
||||
const refresh = async () => {
|
||||
await Promise.all([assetQuery.mutate(), variantsQuery.mutate()])
|
||||
}
|
||||
|
||||
return {
|
||||
asset: assetQuery.data ?? null,
|
||||
error: assetQuery.error ?? variantsQuery.error,
|
||||
hasRunningVariants: hasRunningVariants(variants),
|
||||
isLoading: assetQuery.isLoading || variantsQuery.isLoading,
|
||||
variants: variantsQuery.data?.variants ?? [],
|
||||
isRefreshing: assetQuery.isValidating || variantsQuery.isValidating,
|
||||
refresh,
|
||||
variants,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useGetAssetPicture } from "infra/backend-api"
|
||||
|
||||
import { ASSET_PICTURE_SIZES } from "../config/assets.config"
|
||||
import type { AssetPicturePreview } from "../types/assets-api.type"
|
||||
|
||||
/**
|
||||
* Picture/srcset preview contract выбранного asset.
|
||||
*/
|
||||
export const useAssetPicture = (publicId: string | null, preset: string | null): AssetPicturePreview => {
|
||||
const pictureQuery = preset ? { preset, sizes: ASSET_PICTURE_SIZES } : null
|
||||
const picture = useGetAssetPicture(publicId, pictureQuery)
|
||||
|
||||
const refresh = async () => {
|
||||
await picture.mutate()
|
||||
}
|
||||
|
||||
return {
|
||||
error: picture.error,
|
||||
isLoading: picture.isLoading,
|
||||
isRefreshing: picture.isValidating,
|
||||
picture: picture.data ?? null,
|
||||
refresh,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
export { assetsFactory } from "./assets.factory"
|
||||
export type {
|
||||
AssetOverview,
|
||||
AssetPicturePreview,
|
||||
AssetsApi,
|
||||
AssetsDashboard,
|
||||
AssetVariantFormat,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
AssetResponseDto,
|
||||
AssetPictureResponseDto,
|
||||
AssetVariantResponseDto,
|
||||
CreateAssetRequestDto,
|
||||
CreateAssetResponseDto,
|
||||
@@ -16,10 +17,21 @@ export type AssetVariantResize = "fill" | "fit"
|
||||
export type AssetOverview = {
|
||||
asset: AssetResponseDto | null
|
||||
error?: Error
|
||||
hasRunningVariants: boolean
|
||||
isLoading: boolean
|
||||
isRefreshing: boolean
|
||||
refresh: () => Promise<void>
|
||||
variants: AssetVariantResponseDto[]
|
||||
}
|
||||
|
||||
export type AssetPicturePreview = {
|
||||
error?: Error
|
||||
isLoading: boolean
|
||||
isRefreshing: boolean
|
||||
picture: AssetPictureResponseDto | null
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
export type AssetsDashboard = {
|
||||
allowedSourceHosts: string[]
|
||||
assets: AssetResponseDto[]
|
||||
@@ -77,6 +89,7 @@ export type GenerateAssetVariantsAction = {
|
||||
*/
|
||||
export type AssetsApi = {
|
||||
useAssetOverview: (publicId: string | null) => AssetOverview
|
||||
useAssetPicture: (publicId: string | null, preset: string | null) => AssetPicturePreview
|
||||
useAssetsDashboard: () => AssetsDashboard
|
||||
useCreateAsset: () => CreateAssetAction
|
||||
useCreateAssetVersion: () => CreateAssetVersionAction
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export { getAssetPictureKey, useGetAssetPicture } from "./use-get-asset-picture.hook"
|
||||
export type { AssetPictureQuery } from "./use-get-asset-picture.hook"
|
||||
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"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import useSWR from "swr"
|
||||
import type { SWRConfiguration } from "swr"
|
||||
|
||||
import { backendApi } from "../client"
|
||||
import type { AssetPictureResponseDto, GetAssetPictureParams } from "../generated/backend-api.generated"
|
||||
|
||||
export type AssetPictureQuery = Omit<GetAssetPictureParams, "publicId">
|
||||
|
||||
export const getAssetPictureKey = (publicId: string, query: AssetPictureQuery) =>
|
||||
[
|
||||
"backend-api",
|
||||
"assets",
|
||||
"picture",
|
||||
publicId,
|
||||
query.preset,
|
||||
query.version ?? null,
|
||||
query.quality ?? null,
|
||||
query.sizes ?? null,
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Получение picture/srcset contract asset.
|
||||
*/
|
||||
export const useGetAssetPicture = (
|
||||
publicId: string | null,
|
||||
query: AssetPictureQuery | null,
|
||||
config?: SWRConfiguration<AssetPictureResponseDto>,
|
||||
) => {
|
||||
const key = publicId !== null && query !== null ? getAssetPictureKey(publicId, query) : null
|
||||
const fetcher = () => backendApi.assets.getAssetPicture({ publicId: publicId ?? "", ...(query ?? { preset: "" }) })
|
||||
|
||||
return useSWR<AssetPictureResponseDto>(key, fetcher, config)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export const getAssetVariantsKey = (publicId: string, version?: string) =>
|
||||
export const useGetAssetVariants = (
|
||||
publicId: string | null,
|
||||
version?: string,
|
||||
config?: SWRConfiguration,
|
||||
config?: SWRConfiguration<AssetVariantsResponseDto>,
|
||||
) => {
|
||||
const key = publicId !== null ? getAssetVariantsKey(publicId, version) : null
|
||||
const fetcher = () => backendApi.assets.listAssetVariants({ publicId: publicId ?? "", version })
|
||||
|
||||
@@ -2,6 +2,7 @@ export { backendApi } from "./client"
|
||||
export * from "./hooks"
|
||||
export type {
|
||||
AssetResponseDto,
|
||||
AssetPictureResponseDto,
|
||||
AssetVariantResponseDto,
|
||||
AssetVariantsResponseDto,
|
||||
AssetsListResponseDto,
|
||||
@@ -12,6 +13,7 @@ export type {
|
||||
CreateAssetVariantsRequestDto,
|
||||
CreateAssetVariantsResponseDto,
|
||||
CustomTransformConfigResponseDto,
|
||||
GetAssetPictureParams,
|
||||
ListAssetsParams,
|
||||
PresetResponseDto,
|
||||
PresetsResponseDto,
|
||||
|
||||
@@ -18,6 +18,10 @@ export const DASHBOARD_CARDS = [
|
||||
|
||||
export const DASHBOARD_PIPELINE = ["Backend", "RabbitMQ", "Worker", "imgproxy", "S3"] as const
|
||||
|
||||
export const DASHBOARD_ASSET_SEARCH_PARAM = "asset"
|
||||
|
||||
export const DASHBOARD_PRESET_SEARCH_PARAM = "preset"
|
||||
|
||||
export const ASSET_STATUS_COLORS = {
|
||||
active: "green",
|
||||
deleted: "red",
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Alert, Button, Group, Paper, Stack, Text, Title } from "@mantine/core"
|
||||
import { useDisclosure } from "@mantine/hooks"
|
||||
import cl from "clsx"
|
||||
import { useState } from "react"
|
||||
import { assetsFactory } from "business/assets"
|
||||
|
||||
import { DASHBOARD_PIPELINE } from "./config/dashboard.config"
|
||||
import { useDashboardUrlState } from "./hooks/use-dashboard-url-state.hook"
|
||||
import { AssetDetailPanel } from "./parts/asset-detail-panel"
|
||||
import { AssetsTable } from "./parts/assets-table"
|
||||
import { CreateAssetModal } from "./parts/create-asset-modal"
|
||||
import { CreateSourceVersionModal } from "./parts/create-source-version-modal"
|
||||
import { GenerateVariantsModal } from "./parts/generate-variants-modal"
|
||||
import { PicturePreviewPanel } from "./parts/picture-preview-panel"
|
||||
import { PresetsPanel } from "./parts/presets-panel"
|
||||
import { SummaryCards } from "./parts/summary-cards"
|
||||
import styles from "./styles/dashboard.module.css"
|
||||
@@ -26,7 +27,8 @@ const assets = assetsFactory()
|
||||
*/
|
||||
export const DashboardScreen = (props: DashboardScreenProps) => {
|
||||
const { className, ...rootAttrs } = props
|
||||
const [selectedPublicId, setSelectedPublicId] = useState<string | null>(null)
|
||||
const { selectedPicturePreset, selectedPublicId, setSelectedPicturePreset, setSelectedPublicId } =
|
||||
useDashboardUrlState()
|
||||
const [isCreateAssetOpen, createAssetModal] = useDisclosure(false)
|
||||
const [isCreateVersionOpen, createVersionModal] = useDisclosure(false)
|
||||
const [isGenerateVariantsOpen, generateVariantsModal] = useDisclosure(false)
|
||||
@@ -35,7 +37,9 @@ export const DashboardScreen = (props: DashboardScreenProps) => {
|
||||
const createAssetVersion = assets.useCreateAssetVersion()
|
||||
const generateAssetVariants = assets.useGenerateAssetVariants()
|
||||
const effectivePublicId = selectedPublicId ?? dashboard.assets[0]?.publicId ?? null
|
||||
const effectivePicturePreset = selectedPicturePreset ?? dashboard.presets[0]?.name ?? null
|
||||
const overview = assets.useAssetOverview(effectivePublicId)
|
||||
const picturePreview = assets.useAssetPicture(effectivePublicId, effectivePicturePreset)
|
||||
|
||||
return (
|
||||
<section {...rootAttrs} className={cl(styles.root, className)}>
|
||||
@@ -88,6 +92,14 @@ export const DashboardScreen = (props: DashboardScreenProps) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PicturePreviewPanel
|
||||
onPresetChange={setSelectedPicturePreset}
|
||||
picturePreview={picturePreview}
|
||||
presets={dashboard.presets}
|
||||
publicId={effectivePublicId}
|
||||
selectedPreset={effectivePicturePreset}
|
||||
/>
|
||||
|
||||
<PresetsPanel
|
||||
allowedSourceHosts={dashboard.allowedSourceHosts}
|
||||
custom={dashboard.custom}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { DASHBOARD_ASSET_SEARCH_PARAM, DASHBOARD_PRESET_SEARCH_PARAM } from "../config/dashboard.config"
|
||||
|
||||
const readSearchParam = (name: string) => {
|
||||
if (typeof window === "undefined") {
|
||||
return null
|
||||
}
|
||||
|
||||
return new URLSearchParams(window.location.search).get(name)
|
||||
}
|
||||
|
||||
const replaceSearchParam = (name: string, value: string | null) => {
|
||||
const url = new URL(window.location.href)
|
||||
|
||||
if (value) {
|
||||
url.searchParams.set(name, value)
|
||||
} else {
|
||||
url.searchParams.delete(name)
|
||||
}
|
||||
|
||||
window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* URL-state dashboard для выбранного asset и picture preset.
|
||||
*/
|
||||
export const useDashboardUrlState = () => {
|
||||
const [selectedPublicId, setSelectedPublicIdState] = useState(() => readSearchParam(DASHBOARD_ASSET_SEARCH_PARAM))
|
||||
const [selectedPicturePreset, setSelectedPicturePresetState] = useState(() =>
|
||||
readSearchParam(DASHBOARD_PRESET_SEARCH_PARAM),
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const handlePopState = () => {
|
||||
setSelectedPublicIdState(readSearchParam(DASHBOARD_ASSET_SEARCH_PARAM))
|
||||
setSelectedPicturePresetState(readSearchParam(DASHBOARD_PRESET_SEARCH_PARAM))
|
||||
}
|
||||
|
||||
window.addEventListener("popstate", handlePopState)
|
||||
|
||||
return () => window.removeEventListener("popstate", handlePopState)
|
||||
}, [])
|
||||
|
||||
const setSelectedPublicId = (publicId: string | null) => {
|
||||
setSelectedPublicIdState(publicId)
|
||||
replaceSearchParam(DASHBOARD_ASSET_SEARCH_PARAM, publicId)
|
||||
}
|
||||
|
||||
const setSelectedPicturePreset = (preset: string | null) => {
|
||||
setSelectedPicturePresetState(preset)
|
||||
replaceSearchParam(DASHBOARD_PRESET_SEARCH_PARAM, preset)
|
||||
}
|
||||
|
||||
return {
|
||||
selectedPicturePreset,
|
||||
selectedPublicId,
|
||||
setSelectedPicturePreset,
|
||||
setSelectedPublicId,
|
||||
}
|
||||
}
|
||||
18
apps/admin/src/screens/dashboard/lib/copy-text.ts
Normal file
18
apps/admin/src/screens/dashboard/lib/copy-text.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { notifications } from "@mantine/notifications"
|
||||
|
||||
export const copyText = async (value: string, label: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value)
|
||||
notifications.show({
|
||||
color: "green",
|
||||
message: `${label} скопирован в clipboard`,
|
||||
title: "Copied",
|
||||
})
|
||||
} catch {
|
||||
notifications.show({
|
||||
color: "red",
|
||||
message: `Не удалось скопировать ${label}`,
|
||||
title: "Copy failed",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,21 @@
|
||||
import { Anchor, Badge, Button, Code, Group, Paper, ScrollArea, Skeleton, Stack, Table, Text, Title } from "@mantine/core"
|
||||
import {
|
||||
Anchor,
|
||||
Badge,
|
||||
Button,
|
||||
Code,
|
||||
Group,
|
||||
Image,
|
||||
Paper,
|
||||
ScrollArea,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
Title,
|
||||
} from "@mantine/core"
|
||||
|
||||
import { ASSET_STATUS_COLORS, VARIANT_STATUS_COLORS } from "../../config/dashboard.config"
|
||||
import { copyText } from "../../lib/copy-text"
|
||||
import { formatDateTime } from "../../lib/format-date"
|
||||
import type { AssetDetailPanelProps } from "./types/asset-detail-panel-props.type"
|
||||
|
||||
@@ -48,9 +63,20 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
||||
<Button onClick={onGenerateVariants} radius="xl" size="xs">
|
||||
Generate variants
|
||||
</Button>
|
||||
<Button onClick={() => void copyText(asset.publicId, "publicId")} radius="xl" size="xs" variant="subtle">
|
||||
Copy ID
|
||||
</Button>
|
||||
<Button loading={overview.isRefreshing} onClick={overview.refresh} radius="xl" size="xs" variant="subtle">
|
||||
Refresh
|
||||
</Button>
|
||||
<Badge color={ASSET_STATUS_COLORS[asset.status] ?? "gray"} radius="xl" variant="light">
|
||||
{asset.status}
|
||||
</Badge>
|
||||
{overview.hasRunningVariants ? (
|
||||
<Badge color="yellow" radius="xl" variant="light">
|
||||
polling variants
|
||||
</Badge>
|
||||
) : null}
|
||||
</Group>
|
||||
) : null}
|
||||
</Group>
|
||||
@@ -63,9 +89,14 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
||||
<Text c="dimmed" fz="sm">
|
||||
Source URL
|
||||
</Text>
|
||||
<Anchor href={asset.sourceUrl} target="_blank">
|
||||
{asset.sourceUrl}
|
||||
</Anchor>
|
||||
<Group align="center" gap="xs">
|
||||
<Anchor href={asset.sourceUrl} target="_blank">
|
||||
{asset.sourceUrl}
|
||||
</Anchor>
|
||||
<Button onClick={() => void copyText(asset.sourceUrl, "source URL")} radius="xl" size="compact-xs" variant="subtle">
|
||||
Copy
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Group gap="xs">
|
||||
@@ -95,16 +126,26 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Preview</Table.Th>
|
||||
<Table.Th>Preset</Table.Th>
|
||||
<Table.Th>Format</Table.Th>
|
||||
<Table.Th>Size</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>URL</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
<Table.Th>URL</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{variants.map((variant) => (
|
||||
<Table.Tr key={variant.id}>
|
||||
<Table.Td>
|
||||
{String(variant.status) === "ready" ? (
|
||||
<Image alt={`${variant.preset} ${variant.format}`} fit="cover" h={46} radius="md" src={variant.url} w={72} />
|
||||
) : (
|
||||
<Text c="dimmed" fz="xs">
|
||||
not ready
|
||||
</Text>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Code>{variant.preset}</Code>
|
||||
</Table.Td>
|
||||
@@ -118,9 +159,14 @@ export const AssetDetailPanel = (props: AssetDetailPanelProps) => {
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Anchor href={variant.url} target="_blank">
|
||||
open
|
||||
</Anchor>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Anchor href={variant.url} target="_blank">
|
||||
open
|
||||
</Anchor>
|
||||
<Button onClick={() => void copyText(variant.url, "variant URL")} size="compact-xs" variant="subtle">
|
||||
copy
|
||||
</Button>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { PicturePreviewPanel } from "./picture-preview-panel"
|
||||
export type { PicturePreviewPanelProps } from "./types/picture-preview-panel-props.type"
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Anchor, Badge, Button, Code, Group, Image, Paper, Select, Skeleton, Stack, Table, Text, Title } from "@mantine/core"
|
||||
|
||||
import { copyText } from "../../lib/copy-text"
|
||||
import type { PicturePreviewPanelProps } from "./types/picture-preview-panel-props.type"
|
||||
|
||||
/**
|
||||
* Preview Backend picture/srcset contract.
|
||||
*
|
||||
* Используется для:
|
||||
* - проверки consumer-facing picture contract
|
||||
* - просмотра fallback image и generated srcset URLs
|
||||
*/
|
||||
export const PicturePreviewPanel = (props: PicturePreviewPanelProps) => {
|
||||
const { onPresetChange, picturePreview, presets, publicId, selectedPreset } = props
|
||||
const picture = picturePreview.picture
|
||||
const presetOptions = presets.map((preset) => ({
|
||||
label: `${preset.name} (${preset.mode})`,
|
||||
value: preset.name,
|
||||
}))
|
||||
|
||||
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">
|
||||
Picture contract
|
||||
</Title>
|
||||
<Text c="dimmed" fz="sm">
|
||||
Preview для `GET /api/assets/:publicId/picture`.
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
loading={picturePreview.isRefreshing}
|
||||
onClick={() => void picturePreview.refresh()}
|
||||
radius="xl"
|
||||
size="xs"
|
||||
variant="light"
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<Stack gap="lg">
|
||||
<Select
|
||||
data={presetOptions}
|
||||
disabled={!publicId || presetOptions.length === 0}
|
||||
label="Preset"
|
||||
onChange={onPresetChange}
|
||||
placeholder="Select preset"
|
||||
value={selectedPreset}
|
||||
/>
|
||||
|
||||
{!publicId ? (
|
||||
<Text c="dimmed">Выберите asset для preview.</Text>
|
||||
) : picturePreview.isLoading ? (
|
||||
<Skeleton height={280} radius="lg" />
|
||||
) : picture ? (
|
||||
<Stack gap="lg">
|
||||
<Image alt={`${picture.publicId} ${picture.preset}`} fit="cover" mah={360} radius="lg" src={picture.image.src} />
|
||||
|
||||
<Group gap="xs">
|
||||
<Badge color="violet" radius="xl" variant="light">
|
||||
{picture.preset}
|
||||
</Badge>
|
||||
<Badge radius="xl" variant="light">
|
||||
v{picture.version}
|
||||
</Badge>
|
||||
<Badge radius="xl" variant="light">
|
||||
q{picture.quality}
|
||||
</Badge>
|
||||
<Badge radius="xl" variant="light">
|
||||
widths {picture.widths.join(", ")}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Stack gap={6}>
|
||||
<Text c="dimmed" fz="sm" fw={700}>
|
||||
Fallback URL
|
||||
</Text>
|
||||
<Group gap="xs">
|
||||
<Anchor href={picture.image.src} target="_blank">
|
||||
{picture.image.src}
|
||||
</Anchor>
|
||||
<Button onClick={() => void copyText(picture.image.src, "fallback URL")} radius="xl" size="compact-xs" variant="subtle">
|
||||
Copy
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<Table verticalSpacing="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Format</Table.Th>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>srcset</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{picture.sources.map((source) => (
|
||||
<Table.Tr key={source.format}>
|
||||
<Table.Td>
|
||||
<Code>{source.format}</Code>
|
||||
</Table.Td>
|
||||
<Table.Td>{source.type}</Table.Td>
|
||||
<Table.Td>
|
||||
<Text lineClamp={2}>{source.srcSet}</Text>
|
||||
<Button onClick={() => void copyText(source.srcSet, `${source.format} srcset`)} size="compact-xs" variant="subtle">
|
||||
copy srcset
|
||||
</Button>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
) : (
|
||||
<Text c="dimmed">Picture contract пока недоступен.</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { AssetPicturePreview, AssetsDashboard } from "business/assets"
|
||||
|
||||
/**
|
||||
* Параметры PicturePreviewPanel.
|
||||
*/
|
||||
export type PicturePreviewPanelProps = {
|
||||
/** Preview contract `<picture>`. */
|
||||
picturePreview: AssetPicturePreview
|
||||
/** Public ID выбранного asset. */
|
||||
publicId: string | null
|
||||
/** Callback смены preset. */
|
||||
onPresetChange: (preset: string | null) => void
|
||||
/** Static presets. */
|
||||
presets: AssetsDashboard["presets"]
|
||||
/** Выбранный preset. */
|
||||
selectedPreset: string | null
|
||||
}
|
||||
Reference in New Issue
Block a user