feat(shop): show sale badge and strikethrough price on discounted products
Products with compareAtPrice set now show a "-XX%" badge over the image plus a struck-through original price, wherever price is displayed (shop grid, homepage spotlight, cart line items). Also switched the "inkl. MwSt. zzgl. Versand" rows on Pricing.tsx and ProductSpotlight from items-center to items-baseline — with a large price next to small disclosure text, center alignment left the small text visibly floating above the price's bottom edge.
This commit is contained in:
@@ -5,7 +5,7 @@ import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useCart, removeFromCart, setQuantity } from "../../lib/cart";
|
||||
import { useProducts } from "../../lib/products";
|
||||
import { formatPrice } from "../../lib/format";
|
||||
import { formatPrice, discountPercent } from "../../lib/format";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
import { VersandModal } from "../../components/VersandModal";
|
||||
import { FreeShippingBanner } from "./FreeShippingBanner";
|
||||
@@ -96,7 +96,9 @@ export function CartContent({
|
||||
have had room for a real 2-column layout at Tablet widths
|
||||
anyway). */}
|
||||
<Reveal className="w-full lg:flex-1 flex flex-col gap-6 items-start bg-bg-base border border-border rounded-md p-6 md:p-8">
|
||||
{items.map(({ entry, product }, i) => (
|
||||
{items.map(({ entry, product }, i) => {
|
||||
const discount = discountPercent(product.price, product.compareAtPrice);
|
||||
return (
|
||||
<div key={product.id} className="w-full">
|
||||
{i > 0 && <div className="h-px bg-border w-full mb-6" />}
|
||||
<div className="flex flex-col sm:flex-row gap-4 sm:gap-6 items-start sm:items-center w-full">
|
||||
@@ -114,6 +116,9 @@ export function CartContent({
|
||||
<div className="flex flex-col gap-0.5 items-start">
|
||||
<p className="text-label text-text-muted">Einzelpreis</p>
|
||||
<p className="flex items-baseline gap-1.5">
|
||||
{discount !== null && (
|
||||
<span className="text-label text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</span>
|
||||
)}
|
||||
<span className="font-bold text-body-sm text-text-primary">{formatPrice(product.price)}</span>
|
||||
<span className="text-label text-text-muted">inkl. MwSt.</span>
|
||||
</p>
|
||||
@@ -147,7 +152,8 @@ export function CartContent({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="h-px bg-border w-full" />
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import Link from "next/link";
|
||||
import { AddToCartButton } from "./AddToCartButton";
|
||||
import { Reveal } from "./Reveal";
|
||||
import { getSpotlightProduct } from "../lib/payload";
|
||||
import { formatPrice } from "../lib/format";
|
||||
import { formatPrice, discountPercent } from "../lib/format";
|
||||
|
||||
/**
|
||||
* Product teaser for whichever product is marked `spotlight` in Payload
|
||||
@@ -26,6 +26,7 @@ export async function ProductSpotlight() {
|
||||
if (!product) return null;
|
||||
|
||||
const image = product.spotlightImage || product.image;
|
||||
const discount = discountPercent(product.price, product.compareAtPrice);
|
||||
|
||||
return (
|
||||
<section className="w-full bg-bg-base py-12 md:py-16 px-[var(--layout-padding-x)]">
|
||||
@@ -38,6 +39,11 @@ export async function ProductSpotlight() {
|
||||
sizes="(min-width: 768px) 380px, 100vw"
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
{discount !== null && (
|
||||
<span className="absolute top-3 left-3 rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
|
||||
-{discount}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 items-start flex-1 min-w-0 w-full">
|
||||
@@ -51,9 +57,9 @@ export async function ProductSpotlight() {
|
||||
<p className="text-body text-text-body">
|
||||
{product.spotlightText || product.description}
|
||||
</p>
|
||||
<div className="flex gap-2 items-center">
|
||||
{product.compareAtPrice && product.compareAtPrice > product.price && (
|
||||
<p className="text-body text-text-muted line-through">{formatPrice(product.compareAtPrice)}</p>
|
||||
<div className="flex gap-2 items-baseline">
|
||||
{discount !== null && (
|
||||
<p className="text-body text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</p>
|
||||
)}
|
||||
<p className="font-bold text-h3 text-text-primary">{formatPrice(product.price)}</p>
|
||||
<p className="text-label text-text-muted">inkl. MwSt. zzgl. Versand</p>
|
||||
|
||||
@@ -10,6 +10,15 @@ export function formatPrice(value: number): string {
|
||||
return `${value.toFixed(2).replace(".", ",")} €`;
|
||||
}
|
||||
|
||||
// null when there's nothing to advertise — compareAtPrice unset, or set
|
||||
// but not actually higher than price (e.g. cleared back down without
|
||||
// nulling the field). Callers use this both to gate whether to render
|
||||
// strikethrough/badge UI at all and to fill in the "-XX%" label.
|
||||
export function discountPercent(price: number, compareAtPrice: number | null): number | null {
|
||||
if (compareAtPrice === null || compareAtPrice <= price) return null;
|
||||
return Math.round((1 - price / compareAtPrice) * 100);
|
||||
}
|
||||
|
||||
const MONTHS_DE = [
|
||||
"Januar", "Februar", "März", "April", "Mai", "Juni",
|
||||
"Juli", "August", "September", "Oktober", "November", "Dezember",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { getProducts } from "../../lib/payload";
|
||||
import { formatPrice } from "../../lib/format";
|
||||
import { formatPrice, discountPercent } from "../../lib/format";
|
||||
import { RevealGroup, RevealItem } from "../../components/Reveal";
|
||||
import { AddToCartInlineButton } from "../../components/AddToCartInlineButton";
|
||||
|
||||
@@ -22,7 +22,9 @@ export async function ProductGrid() {
|
||||
return (
|
||||
<section className="w-full bg-bg-base flex flex-col pb-16 md:pb-20 px-[var(--layout-padding-x)]">
|
||||
<RevealGroup className="grid grid-cols-1 md:grid-cols-12 gap-6 md:gap-[var(--layout-grid-gap)] w-full">
|
||||
{products.map((product) => (
|
||||
{products.map((product) => {
|
||||
const discount = discountPercent(product.price, product.compareAtPrice);
|
||||
return (
|
||||
<RevealItem
|
||||
key={product.id}
|
||||
className="group md:col-span-3 bg-bg-base border border-border rounded-md overflow-hidden flex flex-col h-full transition-transform duration-300 hover:-translate-y-1"
|
||||
@@ -35,6 +37,11 @@ export async function ProductGrid() {
|
||||
sizes="(min-width: 768px) 25vw, 100vw"
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
{discount !== null && (
|
||||
<span className="absolute top-3 left-3 rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
|
||||
-{discount}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 items-start px-5 pb-5 pt-4 w-full flex-1">
|
||||
<p
|
||||
@@ -44,6 +51,9 @@ export async function ProductGrid() {
|
||||
{product.name}
|
||||
</p>
|
||||
<p className="flex items-baseline gap-1.5">
|
||||
{discount !== null && (
|
||||
<span className="text-label text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</span>
|
||||
)}
|
||||
<span className="font-bold text-h4 text-text-primary">{formatPrice(product.price)}</span>
|
||||
<span className="text-label text-text-muted">inkl. MwSt.</span>
|
||||
</p>
|
||||
@@ -66,7 +76,8 @@ export async function ProductGrid() {
|
||||
<AddToCartInlineButton id={product.id} />
|
||||
</div>
|
||||
</RevealItem>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</RevealGroup>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -55,7 +55,7 @@ export async function Pricing() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3 items-start w-full lg:w-[18.75rem] lg:shrink-0">
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<p className="font-bold text-h3 text-text-primary">{formatPrice(product.price)}</p>
|
||||
<p className="text-label text-text-muted">inkl. MwSt. zzgl. Versand</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user