fix(web): флаги стран, FormDatePicker и плоские таблицы без двойной рамки

SVG-флаги через flagcdn и resolveCountryCode; поле «Оплачено до» — компактный Popover+Calendar на русском; DataGridCard без вложенной обводки.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-27 01:59:50 +07:00
co-authored by Cursor
parent 4c7dfc45d6
commit f681f72e8c
8 changed files with 329 additions and 102 deletions
+22
View File
@@ -0,0 +1,22 @@
import { cn } from '@cfdm/ui/lib/utils'
import { getCountryFlagUrl, resolveCountryCode } from '@/lib/format'
interface CountryFlagProps {
code?: string
country?: string
className?: string
}
export function CountryFlag({ code, country, className }: CountryFlagProps) {
const resolvedCode = code ?? resolveCountryCode(country)
const url = getCountryFlagUrl(resolvedCode)
if (!url) return null
return (
<img
src={url}
alt=""
className={cn('size-4 shrink-0 rounded-full object-cover', className)}
/>
)
}
+110 -61
View File
@@ -10,6 +10,7 @@ import {
} from '@tanstack/react-table'
import { Card, CardContent, CardHeader, CardTitle } from '@cfdm/ui/components/card'
import { cn } from '@cfdm/ui/lib/utils'
import {
DataGrid,
DataGridContainer,
@@ -68,12 +69,73 @@ export interface DataGridCardProps<TData extends object> {
function DataGridPaginationBar() {
return (
<div className="border-t px-4 py-2.5">
<div className="px-4 py-2.5">
<DataGridPagination {...PAGINATION_LABELS} />
</div>
)
}
function DataGridCardBody<TData extends object>({
table,
data,
emptyTitle,
onRowClick,
dense,
virtualization,
height,
footerContent,
showPagination,
}: {
table: ReturnType<typeof useReactTable<TData>>
data: TData[]
emptyTitle: string
onRowClick?: (row: TData) => void
dense: boolean
virtualization: boolean
height: number
footerContent?: ReactNode
showPagination: boolean
}) {
return (
<DataGridContainer border={false}>
<DataGrid
table={table}
recordCount={data.length}
onRowClick={onRowClick}
emptyMessage={emptyTitle}
tableLayout={{
dense,
rowBorder: true,
headerSticky: true,
headerBackground: true,
headerBorder: true,
width: 'auto',
columnsVisibility: false,
columnsResizable: false,
columnsPinnable: false,
columnsMovable: false,
rowsDraggable: false,
rowsPinnable: false,
}}
>
{virtualization ? (
<>
<DataGridScrollArea orientation="vertical" style={{ height }}>
<DataGridTableVirtual height={height} footerContent={footerContent} />
</DataGridScrollArea>
{showPagination ? <DataGridPaginationBar /> : null}
</>
) : (
<>
<DataGridTable footerContent={footerContent} />
{showPagination ? <DataGridPaginationBar /> : null}
</>
)}
</DataGrid>
</DataGridContainer>
)
}
export function DataGridCard<TData extends object>({
title,
description,
@@ -119,28 +181,19 @@ export function DataGridCard<TData extends object>({
enableColumnPinning: pinLastColumn,
})
if (data.length === 0) {
return (
<Card className={className}>
{(title || actions) && (
<CardHeader className="flex flex-row items-center justify-between gap-2">
<div className="space-y-1">
{title ? <CardTitle>{title}</CardTitle> : null}
{description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
</div>
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
</CardHeader>
)}
<CardContent className="p-4">
<EmptyState title={emptyTitle} description={emptyDescription} action={emptyAction} />
</CardContent>
</Card>
)
}
const hasHeader = Boolean(title || description || actions)
return (
<Card className={className}>
{(title || actions) && (
if (data.length === 0) {
if (!hasHeader) {
return (
<div className={className}>
<EmptyState title={emptyTitle} description={emptyDescription} action={emptyAction} />
</div>
)
}
return (
<Card className={cn('ring-0 shadow-none', className)}>
<CardHeader className="flex flex-row items-center justify-between gap-2">
<div className="space-y-1">
{title ? <CardTitle>{title}</CardTitle> : null}
@@ -148,45 +201,41 @@ export function DataGridCard<TData extends object>({
</div>
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
</CardHeader>
)}
<CardContent className={dense ? 'p-3' : 'p-4'}>
<DataGridContainer>
<DataGrid
table={table}
recordCount={data.length}
onRowClick={onRowClick}
emptyMessage={emptyTitle}
tableLayout={{
dense,
rowBorder: true,
headerSticky: true,
headerBackground: true,
headerBorder: true,
width: 'auto',
columnsVisibility: false,
columnsResizable: false,
columnsPinnable: false,
columnsMovable: false,
rowsDraggable: false,
rowsPinnable: false,
}}
>
{virtualization ? (
<>
<DataGridScrollArea orientation="vertical" style={{ height }}>
<DataGridTableVirtual height={height} footerContent={footerContent} />
</DataGridScrollArea>
{showPagination ? <DataGridPaginationBar /> : null}
</>
) : (
<>
<DataGridTable footerContent={footerContent} />
{showPagination ? <DataGridPaginationBar /> : null}
</>
)}
</DataGrid>
</DataGridContainer>
</CardContent>
<CardContent>
<EmptyState title={emptyTitle} description={emptyDescription} action={emptyAction} />
</CardContent>
</Card>
)
}
const gridBody = (
<DataGridCardBody
table={table}
data={data}
emptyTitle={emptyTitle}
onRowClick={onRowClick}
dense={dense}
virtualization={virtualization}
height={height}
footerContent={footerContent}
showPagination={showPagination}
/>
)
if (!hasHeader) {
return <div className={className}>{gridBody}</div>
}
return (
<Card className={cn('ring-0 shadow-none', className)}>
<CardHeader className="flex flex-row items-center justify-between gap-2 border-b border-border/50 pb-4">
<div className="space-y-1">
{title ? <CardTitle>{title}</CardTitle> : null}
{description ? <p className="text-sm text-muted-foreground">{description}</p> : null}
</div>
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
</CardHeader>
<CardContent className="p-0 pt-2">{gridBody}</CardContent>
</Card>
)
}
+2 -2
View File
@@ -34,8 +34,8 @@ export function dataGridCellWithFlag(
secondary?: ReactNode,
) {
return (
<div className="flex items-center gap-2">
<span className="size-4 shrink-0 leading-none">{flag}</span>
<div className="flex items-center gap-2.5">
<span className="flex size-4 shrink-0 items-center justify-center">{flag}</span>
{secondary ? dataGridCellStack(primary, secondary) : <span className="font-medium">{primary}</span>}
</div>
)
@@ -0,0 +1,86 @@
import { useState } from 'react'
import { format, parseISO, isValid } from 'date-fns'
import { ru } from 'date-fns/locale'
import { CalendarIcon, XIcon } from 'lucide-react'
import { Button } from '@cfdm/ui/components/button'
import { Calendar } from '@cfdm/ui/components/calendar'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@cfdm/ui/components/popover'
import { cn } from '@cfdm/ui/lib/utils'
interface FormDatePickerProps {
id?: string
value?: string
onChange: (value: string) => void
placeholder?: string
className?: string
}
export function FormDatePicker({
id,
value = '',
onChange,
placeholder = 'Выберите дату',
className,
}: FormDatePickerProps) {
const [open, setOpen] = useState(false)
const parsed = value ? parseISO(value) : undefined
const selected = parsed && isValid(parsed) ? parsed : undefined
const label = selected
? format(selected, 'd MMMM yyyy', { locale: ru })
: placeholder
return (
<div className={cn('relative w-full', className)}>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
render={
<Button
id={id}
type="button"
variant="outline"
className={cn(
'w-full justify-start pe-9 font-normal',
!selected && 'text-muted-foreground',
)}
>
<CalendarIcon data-icon="inline-start" />
{label}
</Button>
}
/>
<PopoverContent align="start" className="w-auto p-0">
<Calendar
mode="single"
selected={selected}
onSelect={(date) => {
if (date) {
onChange(format(date, 'yyyy-MM-dd'))
setOpen(false)
}
}}
defaultMonth={selected}
locale={ru}
weekStartsOn={1}
/>
</PopoverContent>
</Popover>
{selected ? (
<Button
type="button"
variant="ghost"
size="icon-sm"
className="absolute end-1 top-1/2 -translate-y-1/2"
aria-label="Очистить дату"
onClick={() => onChange('')}
>
<XIcon />
</Button>
) : null}
</div>
)
}
@@ -30,7 +30,8 @@ import {
saveFilterPresets,
type VpsFilterPreset,
} from '@/components/vps-filters'
import { vpsStatusLabel, tariffTypeLabel, environmentLabel, getCountryFlagEmojiByCode } from '@/lib/format'
import { vpsStatusLabel, tariffTypeLabel, environmentLabel } from '@/lib/format'
import { CountryFlag } from '@/components/country-flag'
import type { Provider, ProviderAccount, Vps } from '@/types/entities'
interface VpsFiltersToolbarProps {
@@ -187,7 +188,7 @@ export function VpsFiltersToolbar({
const countryOpts: FilterOption<string>[] = countryOptions.map((c) => ({
value: c.value,
label: c.label,
icon: c.code ? <span className="size-4 leading-none">{getCountryFlagEmojiByCode(c.code)}</span> : undefined,
icon: c.code ? <CountryFlag code={c.code} /> : <CountryFlag country={c.value} />,
metadata: { count: count((v) => (v.country ?? '').trim() === c.value) },
}))