feat(web): /catalog main page — search, in-plan-first, hide-locked, PostHog, primitives
Migrates the brand index off its inline view toggle to the shared
ViewModeToggle/CatalogHeader primitives from 10A, then adds the toolbox the
audit called out:
- Brand search: client-side, Turkish-locale fold over brandName; visible
X-clear button; the no-match panel exposes the original query
- In-plan brands rendered above locked ones with two sub-section headers
("Planındaki markalar" / "Plan dışı markalar"); locked section gets the
"upgrade to access" microcopy
- Hide-locked toggle with aria-pressed (also persisted as a click event so
we can size the cohort that prefers a clean view)
- Searching collapses the section split into one ranked list — matches
the "show me what I typed" mental model
- Locked brand cards/rows are now real `<Link to="/dashboard/subscription">`
buttons so the upgrade target is one tap; firing
`catalog_locked_brand_upgrade_clicked` with brand_name + surface
- `hover:-translate-y-0.5` (jittery on grid scroll) → `hover:scale-[1.02]`
- ARIA tablist replaced by the role="group" + aria-pressed pattern from
the shared ViewModeToggle — keyboard story actually works now
- Drops manual `encodeURIComponent(brand.brandName)` in `params`; TanStack
Router already encodes — caught the audit's double-encoding risk
New PostHog events: catalog_brands_viewed, catalog_brand_search_used,
catalog_hide_locked_toggled, catalog_view_mode_changed,
catalog_brand_clicked, catalog_locked_brand_upgrade_clicked.
This commit is contained in:
@@ -1,20 +1,17 @@
|
||||
import {
|
||||
CardsViewIcon,
|
||||
ListViewIcon,
|
||||
TreeViewIcon,
|
||||
} from "@/components/categories/view-toggle-icons";
|
||||
import { CatalogHeader } from "@/components/catalog/catalog-header";
|
||||
import { ViewModeToggle } from "@/components/catalog/view-mode-toggle";
|
||||
import { CarBrandLogo } from "@/components/ui/car-brand-logo";
|
||||
import { api } from "@/lib/api-client";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { getUserSettings, setUserSetting } from "@/lib/user-settings";
|
||||
import { Skeleton, cn } from "@sase/ui";
|
||||
import { Button } from "@sase/ui";
|
||||
import { KEYS_10 } from "@/lib/keys";
|
||||
import { capture } from "@/lib/posthog";
|
||||
import { useViewMode } from "@/lib/use-view-mode";
|
||||
import { Button, Input, Skeleton, cn } from "@sase/ui";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { ChevronRight, Library, Lock } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ChevronRight, Library, Lock, Search, X } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { KEYS_10 } from "@/lib/keys";
|
||||
export const Route = createFileRoute("/dashboard/catalog/")({
|
||||
component: CatalogBrandsPage,
|
||||
});
|
||||
@@ -29,93 +26,226 @@ interface CatalogBrand {
|
||||
|
||||
function CatalogBrandsPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [viewMode, setViewMode] = useState<"grid" | "tree" | "columns">(
|
||||
() => getUserSettings().brandViewMode ?? "grid",
|
||||
);
|
||||
|
||||
const changeViewMode = (mode: "grid" | "tree" | "columns") => {
|
||||
setViewMode(mode);
|
||||
setUserSetting("brandViewMode", mode);
|
||||
};
|
||||
const [viewMode, setViewMode] = useViewMode("brandViewMode");
|
||||
const [query, setQuery] = useState("");
|
||||
const [hideLocked, setHideLocked] = useState(false);
|
||||
const viewedRef = useRef(false);
|
||||
|
||||
const { data: brands, isLoading } = useQuery({
|
||||
queryKey: ["catalog-brands"],
|
||||
queryFn: () => api.get<CatalogBrand[]>("/catalog/brands"),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (brands && !viewedRef.current) {
|
||||
viewedRef.current = true;
|
||||
const inPlan = brands.filter((b) => b.hasAccess).length;
|
||||
capture("catalog_brands_viewed", {
|
||||
total: brands.length,
|
||||
in_plan: inPlan,
|
||||
locked: brands.length - inPlan,
|
||||
});
|
||||
}
|
||||
}, [brands]);
|
||||
|
||||
// Filter + sort: in-plan brands first (alphabetical), locked second
|
||||
// (alphabetical). Search wipes the section split; matches all in one list.
|
||||
const { inPlan, locked, hasQuery, filteredTotal } = useMemo(() => {
|
||||
const empty = {
|
||||
inPlan: [] as CatalogBrand[],
|
||||
locked: [] as CatalogBrand[],
|
||||
hasQuery: false,
|
||||
filteredTotal: 0,
|
||||
};
|
||||
if (!brands) return empty;
|
||||
const q = query.trim().toLocaleLowerCase("tr");
|
||||
const matches = q
|
||||
? brands.filter((b) => b.brandName.toLocaleLowerCase("tr").includes(q))
|
||||
: brands;
|
||||
const ip = matches.filter((b) => b.hasAccess).sort(byBrandName);
|
||||
const lk = matches.filter((b) => !b.hasAccess).sort(byBrandName);
|
||||
return { inPlan: ip, locked: lk, hasQuery: q.length > 0, filteredTotal: matches.length };
|
||||
}, [brands, query]);
|
||||
|
||||
function changeViewMode(next: typeof viewMode) {
|
||||
setViewMode(next);
|
||||
capture("catalog_view_mode_changed", { mode: next, surface: "brands" });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{t("catalog.title")}</h1>
|
||||
<p className="text-sm text-muted-foreground">{t("catalog.brands")}</p>
|
||||
<CatalogHeader
|
||||
title={t("catalog.title")}
|
||||
subtitle={t("catalog.subtitle")}
|
||||
actions={
|
||||
<ViewModeToggle
|
||||
value={viewMode}
|
||||
onChange={changeViewMode}
|
||||
groupLabelKey="catalog.view.groupLabel"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Toolbar — search + hide-locked toggle */}
|
||||
{!isLoading && brands && brands.length > 0 && (
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="relative flex-1 sm:max-w-md">
|
||||
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
value={query}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
if (e.target.value.trim()) {
|
||||
capture("catalog_brand_search_used", { length: e.target.value.length });
|
||||
}
|
||||
}}
|
||||
placeholder={t("catalog.brandSearchPlaceholder")}
|
||||
aria-label={t("catalog.brandSearchPlaceholder")}
|
||||
className="pl-9"
|
||||
/>
|
||||
{query && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setQuery("")}
|
||||
aria-label={t("common.cancel")}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-1 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const next = !hideLocked;
|
||||
setHideLocked(next);
|
||||
capture("catalog_hide_locked_toggled", { hidden: next });
|
||||
}}
|
||||
aria-pressed={hideLocked}
|
||||
className="inline-flex items-center gap-2 self-start rounded-md border border-border bg-background px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
<Lock className="size-3.5" />
|
||||
{hideLocked ? t("catalog.showLocked") : t("catalog.hideLocked")}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label="Görünüm modu"
|
||||
className="inline-flex items-center gap-0.5 rounded-lg border border-border bg-muted/40 p-0.5"
|
||||
>
|
||||
{[
|
||||
{ mode: "grid" as const, Icon: CardsViewIcon, label: "Izgara" },
|
||||
{ mode: "tree" as const, Icon: TreeViewIcon, label: "Liste" },
|
||||
{ mode: "columns" as const, Icon: ListViewIcon, label: "Sütun" },
|
||||
].map(({ mode, Icon, label }) => (
|
||||
<button
|
||||
key={mode}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={viewMode === mode}
|
||||
onClick={() => changeViewMode(mode)}
|
||||
className={cn(
|
||||
"inline-flex size-7 items-center justify-center rounded-md transition-all duration-200",
|
||||
viewMode === mode
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
title={label}
|
||||
>
|
||||
<Icon isActive={viewMode === mode} className="size-3.5" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{KEYS_10.map((__k) => (
|
||||
<Skeleton key={__k} className="h-28 w-full rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
<BrandGridSkeleton />
|
||||
) : !brands || brands.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||
<Library className="mb-4 size-12 text-muted-foreground/40" />
|
||||
<p className="text-muted-foreground">{t("catalog.noBrands")}</p>
|
||||
</div>
|
||||
) : viewMode === "grid" ? (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{brands.map((brand) => (
|
||||
<BrandCard key={brand.brandName} brand={brand} />
|
||||
))}
|
||||
) : filteredTotal === 0 ? (
|
||||
<div className="rounded-2xl border border-border bg-muted/10 p-8 text-center">
|
||||
<p className="text-sm font-medium">{t("catalog.brandSearchNoMatch", { query })}</p>
|
||||
</div>
|
||||
) : viewMode === "tree" ? (
|
||||
<BrandListTree brands={brands} />
|
||||
<BrandListTree inPlan={inPlan} locked={hideLocked ? [] : locked} hasQuery={hasQuery} />
|
||||
) : viewMode === "columns" ? (
|
||||
<BrandListColumns inPlan={inPlan} locked={hideLocked ? [] : locked} />
|
||||
) : (
|
||||
<BrandListColumns brands={brands} />
|
||||
<BrandGrid inPlan={inPlan} locked={hideLocked ? [] : locked} hasQuery={hasQuery} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Grid card (existing) ── */
|
||||
function byBrandName(a: CatalogBrand, b: CatalogBrand) {
|
||||
return a.brandName.localeCompare(b.brandName, "tr");
|
||||
}
|
||||
|
||||
function BrandGridSkeleton() {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{KEYS_10.map((__k) => (
|
||||
<Skeleton key={__k} className="h-28 w-full rounded-xl" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Grid ── */
|
||||
|
||||
function BrandGrid({
|
||||
inPlan,
|
||||
locked,
|
||||
hasQuery,
|
||||
}: {
|
||||
inPlan: CatalogBrand[];
|
||||
locked: CatalogBrand[];
|
||||
hasQuery: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
// When the user searched, drop the section headings — a single visual list
|
||||
// matches the user's mental model ("show me matches").
|
||||
if (hasQuery) {
|
||||
const all = [...inPlan, ...locked];
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{all.map((brand) => (
|
||||
<BrandCard key={brand.brandName} brand={brand} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{inPlan.length > 0 && (
|
||||
<section>
|
||||
<SectionHeader label={t("catalog.inPlanSection")} count={inPlan.length} />
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{inPlan.map((brand) => (
|
||||
<BrandCard key={brand.brandName} brand={brand} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{locked.length > 0 && (
|
||||
<section>
|
||||
<SectionHeader
|
||||
label={t("catalog.lockedSection")}
|
||||
count={locked.length}
|
||||
hint={t("catalog.lockedSectionHint")}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{locked.map((brand) => (
|
||||
<BrandCard key={brand.brandName} brand={brand} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionHeader({ label, count, hint }: { label: string; count: number; hint?: string }) {
|
||||
return (
|
||||
<div className="mb-3 flex flex-wrap items-baseline gap-3">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{label}
|
||||
</h2>
|
||||
<span className="text-xs tabular-nums text-muted-foreground/70">({count})</span>
|
||||
{hint && <p className="text-xs text-muted-foreground/80">{hint}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BrandCard({ brand }: { brand: CatalogBrand }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!brand.hasAccess) {
|
||||
return (
|
||||
<div className="group relative flex flex-col items-center justify-center overflow-hidden rounded-xl border border-border/50 bg-muted/30 p-4 text-center select-none">
|
||||
{/* Diagonal stripe overlay for locked feel */}
|
||||
<Link
|
||||
to="/dashboard/subscription"
|
||||
onClick={() =>
|
||||
capture("catalog_locked_brand_upgrade_clicked", { brand_name: brand.brandName })
|
||||
}
|
||||
className="group relative flex flex-col items-center justify-center overflow-hidden rounded-xl border border-border/50 bg-muted/30 p-4 text-center transition-colors hover:border-primary/30 hover:bg-muted/50"
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-[0.06]"
|
||||
style={{
|
||||
@@ -131,7 +261,7 @@ function BrandCard({ brand }: { brand: CatalogBrand }) {
|
||||
size={40}
|
||||
className="grayscale opacity-70"
|
||||
/>
|
||||
<div className="absolute -right-1 -bottom-1 flex size-4 items-center justify-center rounded-full bg-foreground">
|
||||
<div className="absolute -bottom-1 -right-1 flex size-4 items-center justify-center rounded-full bg-foreground">
|
||||
<Lock className="size-2.5 text-background" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,23 +269,26 @@ function BrandCard({ brand }: { brand: CatalogBrand }) {
|
||||
<p className="relative mt-1 text-[11px] uppercase tracking-wider text-muted-foreground/70">
|
||||
{t("catalog.locked")}
|
||||
</p>
|
||||
<Link
|
||||
to="/dashboard/subscription"
|
||||
className="relative mt-2 inline-flex items-center gap-1 rounded-full text-xs font-medium text-brand transition-colors hover:text-brand/80"
|
||||
>
|
||||
<span className="relative mt-2 inline-flex items-center gap-1 text-xs font-medium text-brand transition-colors group-hover:text-brand/80">
|
||||
{t("catalog.upgradeCta")}
|
||||
<ChevronRight className="size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
to="/dashboard/catalog/$brandName"
|
||||
params={{ brandName: encodeURIComponent(brand.brandName) }}
|
||||
params={{ brandName: brand.brandName }}
|
||||
search={{ catalog: undefined }}
|
||||
className="group flex flex-col items-center justify-center rounded-xl border border-border bg-card p-4 text-center transition-all duration-200 hover:-translate-y-0.5 hover:border-foreground/20 hover:shadow-[var(--shadow-md)]"
|
||||
onClick={() =>
|
||||
capture("catalog_brand_clicked", {
|
||||
brand_name: brand.brandName,
|
||||
service_names: brand.serviceNames,
|
||||
})
|
||||
}
|
||||
className="group flex flex-col items-center justify-center rounded-xl border border-border bg-card p-4 text-center transition-all duration-200 hover:border-primary/30 hover:bg-accent/40 hover:scale-[1.02]"
|
||||
>
|
||||
<CarBrandLogo
|
||||
brandName={brand.brandName}
|
||||
@@ -170,62 +303,111 @@ function BrandCard({ brand }: { brand: CatalogBrand }) {
|
||||
|
||||
/* ── Tree (flat list) ── */
|
||||
|
||||
function BrandListTree({ brands }: { brands: CatalogBrand[] }) {
|
||||
function BrandListTree({
|
||||
inPlan,
|
||||
locked,
|
||||
hasQuery,
|
||||
}: {
|
||||
inPlan: CatalogBrand[];
|
||||
locked: CatalogBrand[];
|
||||
hasQuery: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (hasQuery) {
|
||||
return <TreeBlock brands={[...inPlan, ...locked]} />;
|
||||
}
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{inPlan.length > 0 && (
|
||||
<section>
|
||||
<SectionHeader label={t("catalog.inPlanSection")} count={inPlan.length} />
|
||||
<TreeBlock brands={inPlan} />
|
||||
</section>
|
||||
)}
|
||||
{locked.length > 0 && (
|
||||
<section>
|
||||
<SectionHeader
|
||||
label={t("catalog.lockedSection")}
|
||||
count={locked.length}
|
||||
hint={t("catalog.lockedSectionHint")}
|
||||
/>
|
||||
<TreeBlock brands={locked} />
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TreeBlock({ brands }: { brands: CatalogBrand[] }) {
|
||||
return (
|
||||
<div className="divide-y rounded-lg border">
|
||||
{brands.map((brand) => {
|
||||
if (!brand.hasAccess) {
|
||||
return (
|
||||
<div key={brand.brandName} className="flex items-center gap-3 px-4 py-3 opacity-50">
|
||||
<CarBrandLogo brandName={brand.brandName} logoUrl={brand.logoUrl} size={24} />
|
||||
<span className="flex-1 truncate text-sm font-medium">{brand.brandName}</span>
|
||||
<Lock className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
{brands.map((brand) =>
|
||||
brand.hasAccess ? (
|
||||
<Link
|
||||
key={brand.brandName}
|
||||
to="/dashboard/catalog/$brandName"
|
||||
params={{ brandName: encodeURIComponent(brand.brandName) }}
|
||||
params={{ brandName: brand.brandName }}
|
||||
search={{ catalog: undefined }}
|
||||
onClick={() =>
|
||||
capture("catalog_brand_clicked", {
|
||||
brand_name: brand.brandName,
|
||||
service_names: brand.serviceNames,
|
||||
})
|
||||
}
|
||||
className="flex items-center gap-3 px-4 py-3 transition-colors hover:bg-accent"
|
||||
>
|
||||
<CarBrandLogo brandName={brand.brandName} logoUrl={brand.logoUrl} size={24} />
|
||||
<span className="flex-1 truncate text-sm font-medium">{brand.brandName}</span>
|
||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground" />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
) : (
|
||||
<Link
|
||||
key={brand.brandName}
|
||||
to="/dashboard/subscription"
|
||||
onClick={() =>
|
||||
capture("catalog_locked_brand_upgrade_clicked", { brand_name: brand.brandName })
|
||||
}
|
||||
className="flex items-center gap-3 px-4 py-3 opacity-70 transition-opacity hover:opacity-100"
|
||||
>
|
||||
<CarBrandLogo brandName={brand.brandName} logoUrl={brand.logoUrl} size={24} />
|
||||
<span className="flex-1 truncate text-sm font-medium">{brand.brandName}</span>
|
||||
<Lock className="size-3.5 shrink-0 text-muted-foreground" />
|
||||
</Link>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Columns (left: brand list, right: detail + CTA) ── */
|
||||
/* ── Columns ── */
|
||||
|
||||
function BrandListColumns({ brands }: { brands: CatalogBrand[] }) {
|
||||
function BrandListColumns({
|
||||
inPlan,
|
||||
locked,
|
||||
}: {
|
||||
inPlan: CatalogBrand[];
|
||||
locked: CatalogBrand[];
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [selectedName, setSelectedName] = useState<string | null>(null);
|
||||
const ordered = [...inPlan, ...locked];
|
||||
const [selectedName, setSelectedName] = useState<string | null>(
|
||||
() => ordered[0]?.brandName ?? null,
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const selected = brands.find((b) => b.brandName === selectedName) ?? null;
|
||||
const selected = ordered.find((b) => b.brandName === selectedName) ?? null;
|
||||
|
||||
return (
|
||||
<div className="flex border rounded-lg overflow-hidden" style={{ minHeight: 320 }}>
|
||||
{/* Left panel */}
|
||||
<div className="w-[240px] shrink-0 border-r overflow-y-auto" style={{ maxHeight: 480 }}>
|
||||
{brands.map((brand) => (
|
||||
<div className="flex min-h-80 max-h-[480px] overflow-hidden rounded-lg border">
|
||||
<div className="w-[240px] shrink-0 overflow-y-auto border-r">
|
||||
{ordered.map((brand) => (
|
||||
<button
|
||||
key={brand.brandName}
|
||||
type="button"
|
||||
onClick={() => setSelectedName(brand.brandName)}
|
||||
disabled={!brand.hasAccess}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-2 px-3 py-2.5 text-left text-sm transition-colors",
|
||||
brand.hasAccess ? "hover:bg-accent" : "opacity-50 cursor-not-allowed",
|
||||
"focus-visible:outline-none focus-visible:bg-accent",
|
||||
brand.hasAccess ? "hover:bg-accent" : "opacity-60 hover:opacity-100",
|
||||
selectedName === brand.brandName && "bg-accent font-medium",
|
||||
)}
|
||||
>
|
||||
@@ -239,36 +421,49 @@ function BrandListColumns({ brands }: { brands: CatalogBrand[] }) {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right panel */}
|
||||
<div className="flex-1 flex flex-col items-center justify-center p-6 text-center">
|
||||
<div className="flex flex-1 flex-col items-center justify-center p-6 text-center">
|
||||
{selected ? (
|
||||
<div className="space-y-4">
|
||||
<CarBrandLogo brandName={selected.brandName} logoUrl={selected.logoUrl} size={56} />
|
||||
<p className="text-lg font-semibold">{selected.brandName}</p>
|
||||
{selected.hasAccess ? (
|
||||
<Button
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
capture("catalog_brand_clicked", {
|
||||
brand_name: selected.brandName,
|
||||
service_names: selected.serviceNames,
|
||||
surface: "columns",
|
||||
});
|
||||
navigate({
|
||||
to: "/dashboard/catalog/$brandName",
|
||||
params: { brandName: encodeURIComponent(selected.brandName) },
|
||||
params: { brandName: selected.brandName },
|
||||
search: { catalog: undefined },
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Modellere Git
|
||||
{t("catalog.goToModels")}
|
||||
<ChevronRight className="ml-1 size-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">{t("catalog.locked")}</p>
|
||||
<Button variant="outline" asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
onClick={() =>
|
||||
capture("catalog_locked_brand_upgrade_clicked", {
|
||||
brand_name: selected.brandName,
|
||||
surface: "columns",
|
||||
})
|
||||
}
|
||||
>
|
||||
<Link to="/dashboard/subscription">{t("catalog.upgradeCta")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">Soldan bir marka seçin</p>
|
||||
<p className="text-sm text-muted-foreground">{t("catalog.columnsBrowseHint")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user