feat(ui): ürün sayfası, fiyat geçmişi grafiği ve birim fiyat karşılaştırması
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { Clock, Tag, Trophy } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { tr } from "@/i18n/tr";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { formatDate, formatMoney } from "@/shared/format";
|
||||
import { formatUnitPrice, type RankedOffer } from "@/shared/prices";
|
||||
import type { StoreOffer } from "../service";
|
||||
|
||||
const t = tr.productPrices;
|
||||
export const ymdLabel = (ymd: string) => formatDate(`${ymd}T12:00:00+03:00`);
|
||||
|
||||
/** Mağaza renk işareti (kimlik; metin her zaman yanında). */
|
||||
export function StoreDot({ slot, className }: { slot: number; className?: string }) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("mr-1.5 inline-block size-2.5 shrink-0 rounded-full", className)}
|
||||
style={{ backgroundColor: `var(--series-${slot})` }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Teklif rozetleri: en uygun, kampanya, kampanya bitti, eski fiyat. */
|
||||
function OfferBadges({ offer }: { offer: RankedOffer<StoreOffer> }) {
|
||||
const p = offer.price;
|
||||
return (
|
||||
<span className="flex flex-wrap items-center gap-1">
|
||||
{offer.cheapest ? (
|
||||
<Badge className="gap-1 bg-income text-income-foreground">
|
||||
<Trophy className="size-3" aria-hidden />
|
||||
{t.cheapest}
|
||||
</Badge>
|
||||
) : null}
|
||||
{p.isPromo && !offer.promoEnded ? (
|
||||
<Badge variant="outline" className="gap-1 border-budget/50 text-budget">
|
||||
<Tag className="size-3" aria-hidden />
|
||||
{t.promo}
|
||||
{p.promoUntil ? ` · ${t.promoUntil(ymdLabel(p.promoUntil))}` : ""}
|
||||
</Badge>
|
||||
) : null}
|
||||
{offer.fellBack ? <Badge variant="secondary">{t.fellBack}</Badge> : null}
|
||||
{offer.promoEnded ? <Badge variant="secondary">{t.promoEnded}</Badge> : null}
|
||||
{offer.stale ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="gap-1 border-amber-500/50 text-amber-700 dark:text-amber-400"
|
||||
title={t.staleHint}
|
||||
>
|
||||
<Clock className="size-3" aria-hidden />
|
||||
{t.stale(offer.ageDays)}
|
||||
</Badge>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Birim fiyata göre sıralı teklifler. `showProduct`: ürün tipi karşılaştırmasında marka/paket
|
||||
* ve ürün sayfası bağlantısı.
|
||||
*/
|
||||
export function OffersList({
|
||||
offers,
|
||||
typeUnit,
|
||||
showProduct = false,
|
||||
}: {
|
||||
offers: RankedOffer<StoreOffer>[];
|
||||
typeUnit: string;
|
||||
showProduct?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ol className="divide-y rounded-xl border bg-card">
|
||||
{offers.map((o) => (
|
||||
<li
|
||||
key={o.key}
|
||||
className={cn(
|
||||
"grid grid-cols-[1.5rem_minmax(0,1fr)_auto] items-start gap-x-3 gap-y-1 p-3",
|
||||
o.cheapest && "bg-income/5",
|
||||
)}
|
||||
>
|
||||
<span className="pt-0.5 text-sm text-muted-foreground tabular-nums">{o.rank ?? "–"}</span>
|
||||
<div className="min-w-0">
|
||||
{showProduct ? (
|
||||
<p className="truncate font-medium">
|
||||
<Link href={`/stok/urunler/${o.productId}`} className="hover:underline">
|
||||
{o.productLabel}
|
||||
</Link>{" "}
|
||||
<span className="text-xs font-normal text-muted-foreground">{o.packageLabel}</span>
|
||||
</p>
|
||||
) : null}
|
||||
<p
|
||||
className={cn(
|
||||
"flex flex-wrap items-center",
|
||||
showProduct ? "text-sm text-muted-foreground" : "font-medium",
|
||||
)}
|
||||
>
|
||||
<StoreDot slot={o.store.slot} />
|
||||
{o.store.label}
|
||||
<span className="ml-1.5 text-xs font-normal text-muted-foreground">
|
||||
· {ymdLabel(o.price.date)}
|
||||
</span>
|
||||
</p>
|
||||
<div className="mt-1">
|
||||
<OfferBadges offer={o} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right tabular-nums">
|
||||
<p className="font-semibold">
|
||||
{o.unitPriceMinor === null ? (
|
||||
<span className="text-sm font-normal text-muted-foreground">{t.noConversion}</span>
|
||||
) : (
|
||||
formatUnitPrice(o.unitPriceMinor, typeUnit)
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formatMoney(o.price.priceMinor)}
|
||||
{o.diffPct ? ` · ${t.diff(o.diffPct)}` : ""}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user