feat(sase): VIN business-impact dashboard — Faz 3 (no-quota model)
Sase.tr has no monthly VIN quota — access is gated by plan.brandCount + user_brands assignments. The PRD's Faz 3 quota-centric metrics don't apply; this commit ships the 8 business signals that DO apply to that model. All queries are RO joins of query_logs to subscription/plan/brand. 1. Tier × decode volume (30d) — unique users, decode count, decodes/user, and success rate per plan. Plan derived from each user's most-recent active/trial subscription. 2. Brand-tier alignment — for plans with brandCount > 0, the fraction of decodes that landed on a brand the user actually has access to via user_brands. Low alignment = wrong tier or upsell signal. 3. Dormant payers — active subscription + no decode in the last 14 days. Sorted by lastDecodeAt ASC (most dormant first), 30 rows. 4. Empty-handed payers — active subscription + zero lifetime decodes. Onboarding-broken signal, sorted oldest start_date first. 5. Trial → paid funnel — bucket trial users by decodes-during-trial (0, 1–2, 3–5, 6–10, 11–25, 26+) and show the conversion rate (any subsequent active/cancelled subscription) per bucket. Answers "how many decodes is the aha moment". 6. Retention cohort — weekly signup cohorts (mature ≥30d), split by "had a successful decode in first 7 days" vs not; columns show 30d active rate per bucket and the retention lift (pp difference). 7. Power users — top 20 by 30d decode count: plan, decode count, brand diversity, top brand + occurrences, success rate. 8. Tier mismatch — brand-spesifik plan kullanıcıları whose 30d decodes include ≥3 hits on brands they don't have access to. Upsell candidates (or refund/downgrade conversation). All tables link emails to /projects/sase/users/[id] (existing detail page), so the dashboard is a "find users to talk to" tool, not just numbers. VIN dashboard header now has a "Business →" pill next to the time range picker. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
517
apps/web/src/app/projects/sase/vin-decode/business/page.tsx
Normal file
517
apps/web/src/app/projects/sase/vin-decode/business/page.tsx
Normal file
@@ -0,0 +1,517 @@
|
||||
import Link from "next/link";
|
||||
import { PanelShell } from "@/components/panel-shell";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
getTierUsage,
|
||||
getBrandTierAlignment,
|
||||
getDormantPayers,
|
||||
getEmptyHandedPayers,
|
||||
getTrialFunnel,
|
||||
getRetentionCohorts,
|
||||
getPowerUsers,
|
||||
getTierMismatchUsers,
|
||||
} from "@/lib/sase/business-impact";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function BusinessImpactPage() {
|
||||
const [tiers, alignment, dormant, empty, funnel, cohorts, powerUsers, mismatch] =
|
||||
await Promise.all([
|
||||
getTierUsage(30),
|
||||
getBrandTierAlignment(30),
|
||||
getDormantPayers(14, 30),
|
||||
getEmptyHandedPayers(30),
|
||||
getTrialFunnel(90),
|
||||
getRetentionCohorts(8),
|
||||
getPowerUsers(20, 30),
|
||||
getTierMismatchUsers(30, 3, 20),
|
||||
]);
|
||||
|
||||
return (
|
||||
<PanelShell title="Sase · VIN Business Impact">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Link href="/projects/sase/vin-decode" className="hover:underline">
|
||||
← VIN Decode dashboard
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Business Impact</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Decode davranışı ile subscription/plan/brand verisinin kesişimi.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ── 1. Tier × decode ───────────────────────────────────────────── */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>1. Tier × decode hacmi (son 30g)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{tiers.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Veri yok.</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Plan</TableHead>
|
||||
<TableHead className="text-right">Brand sayısı</TableHead>
|
||||
<TableHead className="text-right">Kullanıcı</TableHead>
|
||||
<TableHead className="text-right">Decode</TableHead>
|
||||
<TableHead className="text-right">User başına</TableHead>
|
||||
<TableHead className="text-right">Başarı</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tiers.map((t) => (
|
||||
<TableRow key={t.planId ?? "none"}>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{t.planName ?? "—"}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{t.planBrandCount === 0
|
||||
? "tüm"
|
||||
: t.planBrandCount ?? "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{t.uniqueUsers.toLocaleString("tr-TR")}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{t.totalDecodes.toLocaleString("tr-TR")}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{t.decodesPerUser.toFixed(1)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{pct(t.successRate)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── 2. Brand-tier alignment ─────────────────────────────────────── */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>
|
||||
2. Brand-tier uyumu (brand-spesifik plan kullanıcıları, son 30g)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{alignment.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Brand-spesifik plan kullanımı yok.</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Plan</TableHead>
|
||||
<TableHead className="text-right">Brand sayısı</TableHead>
|
||||
<TableHead className="text-right">Toplam</TableHead>
|
||||
<TableHead className="text-right">Atanmış brand</TableHead>
|
||||
<TableHead className="text-right">Off-brand</TableHead>
|
||||
<TableHead className="text-right">Uyum oranı</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{alignment.map((a) => (
|
||||
<TableRow key={a.planName}>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{a.planName}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{a.planBrandCount}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{a.totalDecodes.toLocaleString("tr-TR")}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums text-emerald-600">
|
||||
{a.onAssignedBrand.toLocaleString("tr-TR")}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums text-destructive">
|
||||
{a.offAssignedBrand.toLocaleString("tr-TR")}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={`text-right tabular-nums ${
|
||||
a.alignmentRate >= 0.9
|
||||
? "text-emerald-600"
|
||||
: a.alignmentRate >= 0.75
|
||||
? ""
|
||||
: "text-destructive"
|
||||
}`}
|
||||
>
|
||||
{pct(a.alignmentRate)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
Düşük uyum = ya yanlış tier'da kullanıcı, ya da farklı brand'leri görmek isteyen
|
||||
upsell adayı. Tier-mismatch tablosunda kişi başına ayrıntı var.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── 3. Dormant payers ───────────────────────────────────────────── */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>
|
||||
3. Dormant payers — active sub var, son 14+ gün decode yok
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{dormant.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Yok — herkes aktif.</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>İsim</TableHead>
|
||||
<TableHead>Plan</TableHead>
|
||||
<TableHead className="text-right">Son decode</TableHead>
|
||||
<TableHead className="text-right">Lifetime</TableHead>
|
||||
<TableHead className="text-right">Sub. bitiş</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{dormant.map((u) => (
|
||||
<TableRow key={u.userId}>
|
||||
<TableCell className="font-mono text-xs">
|
||||
<Link
|
||||
href={`/projects/sase/users/${u.userId}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{u.email}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{u.name ?? "—"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{u.planName ?? "—"}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs">
|
||||
{u.daysSinceLastDecode}g önce
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums text-xs">
|
||||
{u.lifetimeDecodes}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs text-muted-foreground">
|
||||
{u.subscriptionEndsAt
|
||||
? u.subscriptionEndsAt.toISOString().slice(0, 10)
|
||||
: "—"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── 4. Empty-handed payers ──────────────────────────────────────── */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>
|
||||
4. Empty-handed payers — active sub, 0 lifetime decode (onboarding kırık)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{empty.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Yok — paying herkes en az bir sorgu yapmış.</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>İsim</TableHead>
|
||||
<TableHead>Plan</TableHead>
|
||||
<TableHead className="text-right">Sub. başlangıç</TableHead>
|
||||
<TableHead className="text-right">Süre</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{empty.map((u) => (
|
||||
<TableRow key={u.userId}>
|
||||
<TableCell className="font-mono text-xs">
|
||||
<Link
|
||||
href={`/projects/sase/users/${u.userId}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{u.email}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">{u.name ?? "—"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{u.planName ?? "—"}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs">
|
||||
{u.subscriptionStartedAt
|
||||
? u.subscriptionStartedAt.toISOString().slice(0, 10)
|
||||
: "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs">
|
||||
{u.daysSinceSubscriptionStart != null
|
||||
? `${u.daysSinceSubscriptionStart}g`
|
||||
: "—"}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── 5. Trial → paid funnel ──────────────────────────────────────── */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>
|
||||
5. Trial → paid funnel — trial dönemi decode sayısı vs conversion (son 90g)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{funnel.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Trial veri yok.</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Decode bucket</TableHead>
|
||||
<TableHead className="text-right">Trial user</TableHead>
|
||||
<TableHead className="text-right">Converted</TableHead>
|
||||
<TableHead className="text-right">Conversion oranı</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{funnel.map((b) => (
|
||||
<TableRow key={b.decodeBucket}>
|
||||
<TableCell className="font-mono text-xs">{b.decodeBucket}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{b.trialUsers.toLocaleString("tr-TR")}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{b.converted.toLocaleString("tr-TR")}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={`text-right tabular-nums ${
|
||||
b.conversionRate >= 0.3
|
||||
? "text-emerald-600"
|
||||
: b.conversionRate >= 0.1
|
||||
? ""
|
||||
: "text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{pct(b.conversionRate)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
<p className="mt-2 text-xs text-muted-foreground">
|
||||
"X decode yapan %Y dönüyor" — activation metric. Trial'da hangi sayı "aha moment".
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── 6. Retention cohort ─────────────────────────────────────────── */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>
|
||||
6. Decode başarısı → retention (ilk hafta success var/yok vs 30g sonrası aktiflik)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{cohorts.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Mature olmuş cohort yok.</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Cohort</TableHead>
|
||||
<TableHead className="text-right">Toplam</TableHead>
|
||||
<TableHead className="text-right">İlk hafta success</TableHead>
|
||||
<TableHead className="text-right">İlk hafta success'siz</TableHead>
|
||||
<TableHead className="text-right">Retention lift</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{cohorts.map((c) => {
|
||||
const hadRate =
|
||||
c.hadFirstWeekSuccess > 0
|
||||
? c.hadFirstWeekSuccessActive30d / c.hadFirstWeekSuccess
|
||||
: 0;
|
||||
const nohRate =
|
||||
c.noFirstWeekSuccess > 0
|
||||
? c.noFirstWeekSuccessActive30d / c.noFirstWeekSuccess
|
||||
: 0;
|
||||
return (
|
||||
<TableRow key={c.cohort}>
|
||||
<TableCell className="font-mono text-xs">{c.cohort}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{c.signupCount}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs">
|
||||
{c.hadFirstWeekSuccessActive30d}/{c.hadFirstWeekSuccess}{" "}
|
||||
<span className="text-emerald-600">({pct(hadRate)})</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs">
|
||||
{c.noFirstWeekSuccessActive30d}/{c.noFirstWeekSuccess}{" "}
|
||||
<span className="text-muted-foreground">({pct(nohRate)})</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={`text-right tabular-nums ${
|
||||
c.retentionLift > 0.1 ? "text-emerald-600" : ""
|
||||
}`}
|
||||
>
|
||||
{c.retentionLift > 0 ? "+" : ""}
|
||||
{(c.retentionLift * 100).toFixed(1)}pp
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── 7. Power users ──────────────────────────────────────────────── */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>7. Top 20 power user (son 30g decode hacmi)</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{powerUsers.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Veri yok.</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Plan</TableHead>
|
||||
<TableHead className="text-right">Decode</TableHead>
|
||||
<TableHead className="text-right">Brand çeşitliliği</TableHead>
|
||||
<TableHead>Top brand</TableHead>
|
||||
<TableHead className="text-right">Başarı</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{powerUsers.map((u) => (
|
||||
<TableRow key={u.userId}>
|
||||
<TableCell className="font-mono text-xs">
|
||||
<Link
|
||||
href={`/projects/sase/users/${u.userId}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{u.email}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{u.planName ?? "—"}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{u.decodeCount.toLocaleString("tr-TR")}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{u.uniqueBrands}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-xs">
|
||||
{u.topBrand ?? "—"}{" "}
|
||||
<span className="text-muted-foreground">
|
||||
({u.topBrandCount})
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{pct(u.successRate)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ── 8. Tier mismatch ────────────────────────────────────────────── */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardDescription>
|
||||
8. Tier mismatch — brand-spesifik tier ama atanmış olmayan brand'lerde 3+ sorgu (upsell adayı)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{mismatch.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">Mismatch yok.</p>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Plan</TableHead>
|
||||
<TableHead>Atanmış brand'ler</TableHead>
|
||||
<TableHead className="text-right">Off-brand</TableHead>
|
||||
<TableHead className="text-right">Off oranı</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{mismatch.map((u) => (
|
||||
<TableRow key={u.userId}>
|
||||
<TableCell className="font-mono text-xs">
|
||||
<Link
|
||||
href={`/projects/sase/users/${u.userId}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{u.email}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{u.planName}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{u.assignedBrandSlugs.map((b) => (
|
||||
<Badge key={b} variant="secondary" className="font-mono">
|
||||
{b}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums text-destructive">
|
||||
{u.offBrandDecodes}/{u.totalDecodes}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{pct(u.offBrandRate)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PanelShell>
|
||||
);
|
||||
}
|
||||
|
||||
function pct(v: number): string {
|
||||
return `${(v * 100).toFixed(1)}%`;
|
||||
}
|
||||
@@ -87,20 +87,28 @@ export default async function VinDecodePage({
|
||||
{RANGE_LABEL[range]} · {health.totalCount.toLocaleString("tr-TR")} sorgu
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{ALL_RANGES.map((r) => (
|
||||
<Link
|
||||
key={r}
|
||||
href={`?range=${r}`}
|
||||
className={buttonVariants({
|
||||
variant: r === range ? "default" : "outline",
|
||||
size: "sm",
|
||||
})}
|
||||
scroll={false}
|
||||
>
|
||||
{r}
|
||||
</Link>
|
||||
))}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-1">
|
||||
{ALL_RANGES.map((r) => (
|
||||
<Link
|
||||
key={r}
|
||||
href={`?range=${r}`}
|
||||
className={buttonVariants({
|
||||
variant: r === range ? "default" : "outline",
|
||||
size: "sm",
|
||||
})}
|
||||
scroll={false}
|
||||
>
|
||||
{r}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
href="/projects/sase/vin-decode/business"
|
||||
className={buttonVariants({ variant: "outline", size: "sm" })}
|
||||
>
|
||||
Business →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user