feat: purchase-plans global update
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Loader2, Filter, ArrowUpDown, X, Check, ArrowUp, ArrowDown } from "lucide-react";
|
||||
import { Filter, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -21,9 +21,8 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Placement, PlacementStatus } from "@/lib/types/api";
|
||||
import type { Placement, PlacementStatus, PlacementPostStatus, CostType, PlacementType } from "@/lib/types/api";
|
||||
|
||||
type FilterCondition = "contains" | "not_contains" | "equals";
|
||||
|
||||
@@ -60,11 +59,14 @@ export interface PlacementFilters {
|
||||
};
|
||||
placementFilters: {
|
||||
statuses: PlacementStatus[];
|
||||
postStatuses: PlacementPostStatus[];
|
||||
hasCreative: boolean | null;
|
||||
costMin: number | null;
|
||||
costMax: number | null;
|
||||
costFormat: CostType | null;
|
||||
placementType: PlacementType | null;
|
||||
dateFrom: string | null;
|
||||
dateTo: string | null;
|
||||
hasPlacement: boolean | null;
|
||||
};
|
||||
sort: SortOption | null;
|
||||
}
|
||||
@@ -74,13 +76,14 @@ const CHANNEL_FILTER_FIELDS: FilterField[] = [
|
||||
{ key: "username", label: "Username", type: "text" },
|
||||
];
|
||||
|
||||
const SORT_FIELDS: SortField[] = [
|
||||
{ key: "title", label: "Название канала" },
|
||||
{ key: "subscribers", label: "Подписчики" },
|
||||
const PLACEMENT_SORT_FIELDS: SortField[] = [
|
||||
{ key: "cost", label: "Стоимость" },
|
||||
{ key: "date", label: "Дата размещения" },
|
||||
{ key: "cpm", label: "CPM" },
|
||||
{ key: "cpf", label: "CPF" },
|
||||
{ key: "subscribers", label: "Подписчики" },
|
||||
{ key: "views", label: "Просмотры" },
|
||||
{ key: "created", label: "Дата создания" },
|
||||
];
|
||||
|
||||
const CONDITION_OPTIONS: { value: FilterCondition; label: string }[] = [
|
||||
@@ -102,21 +105,40 @@ const STATUS_OPTIONS: PlacementStatus[] = [
|
||||
"Не отвечает",
|
||||
];
|
||||
|
||||
const CONDITION_LABELS: Record<FilterCondition, string> = {
|
||||
contains: "содержит",
|
||||
not_contains: "не содержит",
|
||||
equals: "равно",
|
||||
};
|
||||
const POST_STATUS_OPTIONS: PlacementPostStatus[] = [
|
||||
"Без статуса",
|
||||
"Отправить пост",
|
||||
"Согласование поста",
|
||||
"Ожидание отложки",
|
||||
"Запланирован",
|
||||
"Пост вышел",
|
||||
"Размещение отработало - пост удалён",
|
||||
"Размещение отработало - пост не удалён",
|
||||
"Проверить - пост удалён раньше срока",
|
||||
"Проверить - пост не вышел",
|
||||
"Размещение отработало",
|
||||
];
|
||||
|
||||
const COST_FORMAT_OPTIONS: { value: CostType | "any"; label: string }[] = [
|
||||
{ value: "any", label: "Любой" },
|
||||
{ value: "fixed", label: "Фикс" },
|
||||
{ value: "cpm", label: "CPM" },
|
||||
];
|
||||
|
||||
const PLACEMENT_TYPE_OPTIONS: { value: PlacementType | "any"; label: string }[] = [
|
||||
{ value: "any", label: "Любой" },
|
||||
{ value: "standard", label: "Стандартный" },
|
||||
{ value: "self_promo", label: "Взаимный пиар" },
|
||||
];
|
||||
|
||||
export function FiltersModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onApply,
|
||||
existingPlacements,
|
||||
initialFilters,
|
||||
}: FiltersModalProps) {
|
||||
const [filters, setFilters] = useState<PlacementFilters>(initialFilters);
|
||||
const [activeTab, setActiveTab] = useState<"channels" | "placements" | "sort">("channels");
|
||||
const [activeTab, setActiveTab] = useState<"channels" | "placements">("channels");
|
||||
|
||||
const handleApply = () => {
|
||||
onApply(filters);
|
||||
@@ -133,11 +155,14 @@ export function FiltersModal({
|
||||
},
|
||||
placementFilters: {
|
||||
statuses: [],
|
||||
postStatuses: [],
|
||||
hasCreative: null,
|
||||
costMin: null,
|
||||
costMax: null,
|
||||
costFormat: null,
|
||||
placementType: null,
|
||||
dateFrom: null,
|
||||
dateTo: null,
|
||||
hasPlacement: null,
|
||||
},
|
||||
sort: null,
|
||||
};
|
||||
@@ -155,6 +180,17 @@ export function FiltersModal({
|
||||
});
|
||||
};
|
||||
|
||||
const togglePostStatus = (status: PlacementPostStatus) => {
|
||||
const currentStatuses = filters.placementFilters.postStatuses;
|
||||
const newStatuses = currentStatuses.includes(status)
|
||||
? currentStatuses.filter((s) => s !== status)
|
||||
: [...currentStatuses, status];
|
||||
setFilters({
|
||||
...filters,
|
||||
placementFilters: { ...filters.placementFilters, postStatuses: newStatuses },
|
||||
});
|
||||
};
|
||||
|
||||
const updateChannelFilter = (
|
||||
key: "name" | "username",
|
||||
updates: { value?: string; condition?: FilterCondition }
|
||||
@@ -174,8 +210,12 @@ export function FiltersModal({
|
||||
filters.channelFilters.subscribersMin !== null ||
|
||||
filters.channelFilters.subscribersMax !== null ||
|
||||
filters.placementFilters.statuses.length > 0 ||
|
||||
filters.placementFilters.postStatuses.length > 0 ||
|
||||
filters.placementFilters.hasCreative !== null ||
|
||||
filters.placementFilters.costMin !== null ||
|
||||
filters.placementFilters.costMax !== null ||
|
||||
filters.placementFilters.costFormat !== null ||
|
||||
filters.placementFilters.placementType !== null ||
|
||||
filters.placementFilters.dateFrom ||
|
||||
filters.placementFilters.dateTo ||
|
||||
filters.sort;
|
||||
@@ -186,7 +226,7 @@ export function FiltersModal({
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Filter className="h-5 w-5" />
|
||||
Фильтры и сортировка
|
||||
Фильтры
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Настройте фильтры для каналов и размещений
|
||||
@@ -208,42 +248,31 @@ export function FiltersModal({
|
||||
<button
|
||||
onClick={() => setActiveTab("placements")}
|
||||
className={cn(
|
||||
"px-4 py-2 text-sm font-medium border-b-2 transition-colors",
|
||||
"px-4 py-2 text-sm font-medium border-b-2 transition-colors flex items-center gap-1",
|
||||
activeTab === "placements"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
Размещения
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("sort")}
|
||||
className={cn(
|
||||
"px-4 py-2 text-sm font-medium border-b-2 transition-colors flex items-center gap-1",
|
||||
activeTab === "sort"
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<ArrowUpDown className="h-4 w-4" />
|
||||
Сортировка
|
||||
Размещения
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{activeTab === "channels" && (
|
||||
<div className="space-y-4">
|
||||
{CHANNEL_FILTER_FIELDS.map((field) => (
|
||||
<div key={field.key} className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
{field.label}
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Поиск канала</Label>
|
||||
{CHANNEL_FILTER_FIELDS.map((field) => (
|
||||
<div key={field.key} className="flex gap-2 items-center">
|
||||
<Select
|
||||
value={filters.channelFilters[field.key as "name" | "username"].condition}
|
||||
onValueChange={(value: FilterCondition) =>
|
||||
updateChannelFilter(field.key as "name" | "username", { condition: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-7 w-36 text-xs">
|
||||
<SelectTrigger className="h-8 w-32 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -254,27 +283,25 @@ export function FiltersModal({
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Label>
|
||||
<Input
|
||||
placeholder={
|
||||
filters.channelFilters[field.key as "name" | "username"].condition === "equals"
|
||||
? "Точное значение"
|
||||
: "Например: новости"
|
||||
}
|
||||
value={filters.channelFilters[field.key as "name" | "username"].value}
|
||||
onChange={(e) =>
|
||||
updateChannelFilter(field.key as "name" | "username", { value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
<Input
|
||||
placeholder={field.key === "name" ? "Название канала" : "@username"}
|
||||
className="h-8 text-sm"
|
||||
value={filters.channelFilters[field.key as "name" | "username"].value}
|
||||
onChange={(e) =>
|
||||
updateChannelFilter(field.key as "name" | "username", { value: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Мин. подписчиков</Label>
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Подписчики</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
placeholder="от"
|
||||
className="h-8 text-sm w-28"
|
||||
value={filters.channelFilters.subscribersMin || ""}
|
||||
onChange={(e) =>
|
||||
setFilters({
|
||||
@@ -286,12 +313,11 @@ export function FiltersModal({
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Макс. подписчиков</Label>
|
||||
<span className="text-muted-foreground">—</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="1000000"
|
||||
placeholder="до"
|
||||
className="h-8 text-sm w-28"
|
||||
value={filters.channelFilters.subscribersMax || ""}
|
||||
onChange={(e) =>
|
||||
setFilters({
|
||||
@@ -305,19 +331,25 @@ export function FiltersModal({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-muted/50 rounded-lg">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Каналы всегда сортируются по алфавиту
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "placements" && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Статусы размещений</Label>
|
||||
<div className="flex flex-wrap gap-2 max-h-40 overflow-y-auto">
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Статус сделки</Label>
|
||||
<div className="flex flex-wrap gap-1.5 max-h-28 overflow-y-auto">
|
||||
{STATUS_OPTIONS.map((status) => (
|
||||
<label
|
||||
key={status}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1.5 rounded-md border cursor-pointer transition-colors text-sm",
|
||||
"flex items-center gap-1.5 px-2 py-1 rounded-md border cursor-pointer transition-colors text-xs",
|
||||
filters.placementFilters.statuses.includes(status)
|
||||
? "bg-primary/10 border-primary text-primary"
|
||||
: "hover:bg-muted"
|
||||
@@ -326,92 +358,207 @@ export function FiltersModal({
|
||||
<Checkbox
|
||||
checked={filters.placementFilters.statuses.includes(status)}
|
||||
onCheckedChange={() => toggleStatus(status)}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
{status}
|
||||
<span className="truncate max-w-[150px]">{status}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Статус поста</Label>
|
||||
<div className="flex flex-wrap gap-1.5 max-h-28 overflow-y-auto">
|
||||
{POST_STATUS_OPTIONS.map((status) => (
|
||||
<label
|
||||
key={status}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-2 py-1 rounded-md border cursor-pointer transition-colors text-xs",
|
||||
filters.placementFilters.postStatuses.includes(status)
|
||||
? "bg-primary/10 border-primary text-primary"
|
||||
: "hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={filters.placementFilters.postStatuses.includes(status)}
|
||||
onCheckedChange={() => togglePostStatus(status)}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
<span className="truncate max-w-[150px]">{status}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Креатив</Label>
|
||||
<div className="flex gap-4">
|
||||
{[
|
||||
{ value: null, label: "Любой" },
|
||||
{ value: true, label: "С креативом" },
|
||||
{ value: false, label: "Без креатива" },
|
||||
].map((opt) => (
|
||||
<label
|
||||
key={String(opt.value)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-1.5 rounded-md border cursor-pointer transition-colors text-sm",
|
||||
filters.placementFilters.hasCreative === opt.value
|
||||
? "bg-primary/10 border-primary text-primary"
|
||||
: "hover:bg-muted"
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={filters.placementFilters.hasCreative === opt.value}
|
||||
onCheckedChange={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
placementFilters: { ...filters.placementFilters, hasCreative: opt.value },
|
||||
})
|
||||
}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
{opt.label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Мин. стоимость (₽)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
value={filters.placementFilters.costMin || ""}
|
||||
onChange={(e) =>
|
||||
setFilters({
|
||||
...filters,
|
||||
placementFilters: {
|
||||
...filters.placementFilters,
|
||||
costMin: e.target.value ? parseFloat(e.target.value) : null,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Стоимость (₽)</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="от"
|
||||
className="h-8 text-sm"
|
||||
value={filters.placementFilters.costMin || ""}
|
||||
onChange={(e) =>
|
||||
setFilters({
|
||||
...filters,
|
||||
placementFilters: {
|
||||
...filters.placementFilters,
|
||||
costMin: e.target.value ? parseFloat(e.target.value) : null,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className="text-muted-foreground">—</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="до"
|
||||
className="h-8 text-sm"
|
||||
value={filters.placementFilters.costMax || ""}
|
||||
onChange={(e) =>
|
||||
setFilters({
|
||||
...filters,
|
||||
placementFilters: {
|
||||
...filters.placementFilters,
|
||||
costMax: e.target.value ? parseFloat(e.target.value) : null,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Макс. стоимость (₽)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="100000"
|
||||
value={filters.placementFilters.costMax || ""}
|
||||
onChange={(e) =>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Формат оплаты</Label>
|
||||
<Select
|
||||
value={filters.placementFilters.costFormat || "any"}
|
||||
onValueChange={(value: CostType | "any") =>
|
||||
setFilters({
|
||||
...filters,
|
||||
placementFilters: {
|
||||
...filters.placementFilters,
|
||||
costMax: e.target.value ? parseFloat(e.target.value) : null,
|
||||
costFormat: value === "any" ? null : value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue placeholder="Любой" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{COST_FORMAT_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Дата размещения от</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={filters.placementFilters.dateFrom || ""}
|
||||
onChange={(e) =>
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Тип закупа</Label>
|
||||
<Select
|
||||
value={filters.placementFilters.placementType || "any"}
|
||||
onValueChange={(value: PlacementType | "any") =>
|
||||
setFilters({
|
||||
...filters,
|
||||
placementFilters: {
|
||||
...filters.placementFilters,
|
||||
dateFrom: e.target.value || null,
|
||||
placementType: value === "any" ? null : value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue placeholder="Любой" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PLACEMENT_TYPE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Дата размещения до</Label>
|
||||
<Input
|
||||
type="date"
|
||||
value={filters.placementFilters.dateTo || ""}
|
||||
onChange={(e) =>
|
||||
setFilters({
|
||||
...filters,
|
||||
placementFilters: {
|
||||
...filters.placementFilters,
|
||||
dateTo: e.target.value || null,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm font-medium">Дата размещения</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Input
|
||||
type="date"
|
||||
className="h-8 text-sm"
|
||||
value={filters.placementFilters.dateFrom || ""}
|
||||
onChange={(e) =>
|
||||
setFilters({
|
||||
...filters,
|
||||
placementFilters: {
|
||||
...filters.placementFilters,
|
||||
dateFrom: e.target.value || null,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span className="text-muted-foreground">—</span>
|
||||
<Input
|
||||
type="date"
|
||||
className="h-8 text-sm"
|
||||
value={filters.placementFilters.dateTo || ""}
|
||||
onChange={(e) =>
|
||||
setFilters({
|
||||
...filters,
|
||||
placementFilters: {
|
||||
...filters.placementFilters,
|
||||
dateTo: e.target.value || null,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "sort" && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="col-span-2 space-y-2">
|
||||
<Label>Сортировать по</Label>
|
||||
<div className="space-y-3 pt-2 border-t">
|
||||
<Label className="text-sm font-medium flex items-center gap-2">
|
||||
<ArrowUpDown className="h-4 w-4" />
|
||||
Сортировка внутри каналов
|
||||
</Label>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Select
|
||||
value={filters.sort?.field || ""}
|
||||
onValueChange={(field) =>
|
||||
@@ -419,82 +566,73 @@ export function FiltersModal({
|
||||
...filters,
|
||||
sort: filters.sort
|
||||
? { ...filters.sort, field }
|
||||
: { field, direction: "desc" },
|
||||
: { field, direction: "asc" },
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Выберите поле" />
|
||||
<SelectTrigger className="h-8 text-sm w-48">
|
||||
<SelectValue placeholder="Без сортировки" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{SORT_FIELDS.map((field) => (
|
||||
{PLACEMENT_SORT_FIELDS.map((field) => (
|
||||
<SelectItem key={field.key} value={field.key}>
|
||||
{field.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Направление</Label>
|
||||
<div className="flex gap-1 p-1 border rounded-md">
|
||||
{filters.sort && (
|
||||
<div className="flex gap-1 p-0.5 border rounded-md">
|
||||
<button
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
sort: { ...filters.sort!, direction: "asc" },
|
||||
})
|
||||
}
|
||||
className={cn(
|
||||
"flex items-center justify-center px-3 py-1 rounded-sm transition-colors text-xs",
|
||||
filters.sort.direction === "asc"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted"
|
||||
)}
|
||||
title="По возрастанию"
|
||||
>
|
||||
<ArrowUp className="h-3 w-3 mr-1" />
|
||||
А-Я
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
sort: { ...filters.sort!, direction: "desc" },
|
||||
})
|
||||
}
|
||||
className={cn(
|
||||
"flex items-center justify-center px-3 py-1 rounded-sm transition-colors text-xs",
|
||||
filters.sort.direction === "desc"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted"
|
||||
)}
|
||||
title="По убыванию"
|
||||
>
|
||||
<ArrowDown className="h-3 w-3 mr-1" />
|
||||
Я-А
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{filters.sort && (
|
||||
<button
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
sort: filters.sort
|
||||
? { ...filters.sort, direction: "asc" }
|
||||
: { field: "title", direction: "asc" },
|
||||
})
|
||||
setFilters({ ...filters, sort: null })
|
||||
}
|
||||
className={cn(
|
||||
"flex-1 flex items-center justify-center py-2 rounded-sm transition-colors",
|
||||
filters.sort?.direction === "asc"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted"
|
||||
)}
|
||||
title="По возрастанию"
|
||||
className="text-xs text-muted-foreground hover:text-foreground underline"
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
<span className="ml-1 text-xs">A-Z</span>
|
||||
Сбросить
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setFilters({
|
||||
...filters,
|
||||
sort: filters.sort
|
||||
? { ...filters.sort, direction: "desc" }
|
||||
: { field: "title", direction: "desc" },
|
||||
})
|
||||
}
|
||||
className={cn(
|
||||
"flex-1 flex items-center justify-center py-2 rounded-sm transition-colors",
|
||||
filters.sort?.direction === "desc"
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "hover:bg-muted"
|
||||
)}
|
||||
title="По убыванию"
|
||||
>
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
<span className="ml-1 text-xs">Z-A</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{filters.sort && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span>Текущая сортировка:</span>
|
||||
<span className="font-medium">
|
||||
{SORT_FIELDS.find((f) => f.key === filters.sort?.field)?.label ||
|
||||
filters.sort?.field}
|
||||
</span>
|
||||
<span>
|
||||
{filters.sort.direction === "asc" ? "↑" : "↓"} (
|
||||
{filters.sort.direction === "asc" ? "A-Z" : "Z-A"})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user