Run 'biome check --fix' on apps/api/src and apps/web/src to clear the safe-fixable lint backlog (151 files: parseInt → Number.parseInt, isNaN → Number.isNaN, organize imports, etc.). 769 errors remain that require manual changes (mostly noExplicitAny). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
|
|
import { api } from "@/lib/api-client";
|
|
import { useTranslation } from "@/lib/i18n";
|
|
import { Card, CardContent } from "@sase/ui";
|
|
import { Badge } from "@sase/ui";
|
|
import { Skeleton } from "@sase/ui";
|
|
import { cn } from "@sase/ui";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { Check } from "lucide-react";
|
|
import { useEffect, useState } from "react";
|
|
|
|
interface Brand {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
logoUrl?: string;
|
|
}
|
|
|
|
interface BrandSelectorProps {
|
|
maxBrands: number;
|
|
selectedBrandIds: string[];
|
|
onSelectionChange: (brandIds: string[]) => void;
|
|
isFullPlan?: boolean;
|
|
}
|
|
|
|
export function BrandSelector({
|
|
maxBrands,
|
|
selectedBrandIds,
|
|
onSelectionChange,
|
|
isFullPlan = false,
|
|
}: BrandSelectorProps) {
|
|
const { t } = useTranslation();
|
|
const [selected, setSelected] = useState<string[]>(selectedBrandIds);
|
|
|
|
const { data: brands, isLoading } = useQuery({
|
|
queryKey: ["brands"],
|
|
queryFn: () => api.get<Brand[]>("/brands"),
|
|
});
|
|
|
|
useEffect(() => {
|
|
setSelected(selectedBrandIds);
|
|
}, [selectedBrandIds]);
|
|
|
|
useEffect(() => {
|
|
if (isFullPlan && brands) {
|
|
const allIds = brands.map((b) => b.id);
|
|
setSelected(allIds);
|
|
onSelectionChange(allIds);
|
|
}
|
|
}, [isFullPlan, brands, onSelectionChange]);
|
|
|
|
function toggleBrand(brandId: string) {
|
|
if (isFullPlan) return;
|
|
|
|
let next: string[];
|
|
if (selected.includes(brandId)) {
|
|
next = selected.filter((id) => id !== brandId);
|
|
} else {
|
|
if (selected.length >= maxBrands) return;
|
|
next = [...selected, brandId];
|
|
}
|
|
setSelected(next);
|
|
onSelectionChange(next);
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4">
|
|
{["s1", "s2", "s3", "s4", "s5", "s6", "s7", "s8"].map((id) => (
|
|
<Skeleton key={id} className="h-24 w-full rounded-lg" />
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!brands || brands.length === 0) {
|
|
return <p className="text-sm text-muted-foreground">{t("common.noData")}</p>;
|
|
}
|
|
|
|
const isMaxReached = !isFullPlan && selected.length >= maxBrands;
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-sm font-medium">{t("subscription.selectBrands")}</p>
|
|
<Badge variant={isFullPlan ? "default" : "secondary"}>
|
|
{isFullPlan
|
|
? t("subscription.allBrandsSelected")
|
|
: `${selected.length}/${maxBrands} ${t("subscription.brandsSelected")}`}
|
|
</Badge>
|
|
</div>
|
|
|
|
{isMaxReached && (
|
|
<p className="text-xs text-amber-600">{t("subscription.maxBrandsReached")}</p>
|
|
)}
|
|
|
|
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4">
|
|
{brands.map((brand) => {
|
|
const isSelected = selected.includes(brand.id) || isFullPlan;
|
|
const isDisabled = !isSelected && isMaxReached && !isFullPlan;
|
|
|
|
return (
|
|
<Card
|
|
key={brand.id}
|
|
className={cn(
|
|
"cursor-pointer transition-all hover:shadow-md",
|
|
isSelected && "border-primary ring-2 ring-primary/20",
|
|
isDisabled && "cursor-not-allowed opacity-50",
|
|
isFullPlan && "cursor-default",
|
|
)}
|
|
onClick={() => !isDisabled && toggleBrand(brand.id)}
|
|
>
|
|
<CardContent className="flex flex-col items-center justify-center p-4">
|
|
<div className="relative">
|
|
<CarBrandLogo
|
|
brandName={brand.name}
|
|
logoUrl={brand.logoUrl}
|
|
size={48}
|
|
className="mb-2"
|
|
/>
|
|
{isSelected && (
|
|
<div className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
|
<Check className="h-3 w-3" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<span className="text-center text-sm font-medium">{brand.name}</span>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|