Fixed an issue where media manager only shows files uploaded in specific sections.

Fixed an issue with icon picker names.

Removed jumbo rounding and a few other style tweaks.
This commit is contained in:
Stefan Lazic committed 2026-08-05 09:25:59 +01:00
1 parent 9e39908ea7
commit d3bbad09c6
74 files changed
+1622 -195

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ export function Skeleton({ className }: { className?: string }) {
export function CardSkeleton() {
return (
<div className="rounded-jumbo border border-line bg-white p-5">
<div className="rounded-md border border-line bg-white p-5">
<div className="mb-4 flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-full" />
<div className="flex-1 space-y-2">
+1 -1
View File
@@ -9,7 +9,7 @@ import { cn } from "@/lib/utils/cn";
export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("rounded-jumbo border border-line bg-white shadow-sm", className)}
className={cn("rounded-md border border-line bg-white shadow-sm", className)}
{...props}
/>
);
+3 -3
View File
@@ -27,7 +27,7 @@ export function ImageGallery({ images, className }: ImageGalleryProps) {
return (
<div className={cn("w-full", className)}>
<div className="bg-stone relative aspect-[16/9] w-full overflow-hidden rounded-jumbo">
<div className="bg-stone relative aspect-[16/9] w-full overflow-hidden rounded-md">
{/* eslint-disable-next-line @next/next/no-img-element -- local/user-supplied URL, same pattern used app-wide */}
<img src={active.url} alt={active.caption ?? ""} className="h-full w-full object-cover" />
{images.length > 1 && (
@@ -36,7 +36,7 @@ export function ImageGallery({ images, className }: ImageGalleryProps) {
type="button"
onClick={() => goTo(activeIndex - 1)}
aria-label="Previous image"
className="bg-ink/60 hover:bg-ink/80 absolute top-1/2 left-3 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full text-white"
className="bg-ink/60 hover:bg-ink/80 absolute top-1/2 left-3 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-md text-white"
>
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
</button>
@@ -44,7 +44,7 @@ export function ImageGallery({ images, className }: ImageGalleryProps) {
type="button"
onClick={() => goTo(activeIndex + 1)}
aria-label="Next image"
className="bg-ink/60 hover:bg-ink/80 absolute top-1/2 right-3 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-full text-white"
className="bg-ink/60 hover:bg-ink/80 absolute top-1/2 right-3 flex h-8 w-8 -translate-y-1/2 items-center justify-center rounded-md text-white"
>
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</button>
+74 -14
View File
@@ -1,8 +1,23 @@
"use client";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Gift } from "lucide-react";
import { LoyaltyCardFont, LoyaltyCardTextSize, RewardPositionMode } from "@/generated/prisma/enums";
import { findCatalogIcon } from "@/lib/icons/icon-catalog";
import { cn } from "@/lib/utils/cn";
import { resolveRewardThreshold } from "@/services/loyalty/reward-threshold";
const FONT_CLASSES: Record<LoyaltyCardFont, string> = {
[LoyaltyCardFont.SANS]: "font-sans",
[LoyaltyCardFont.SERIF]: "font-serif",
[LoyaltyCardFont.MONO]: "font-mono",
};
const TOP_RIGHT_TEXT_SIZE_CLASSES: Record<LoyaltyCardTextSize, string> = {
[LoyaltyCardTextSize.SM]: "text-[8px]",
[LoyaltyCardTextSize.MD]: "text-[10px]",
[LoyaltyCardTextSize.LG]: "text-[13px]",
};
export interface LoyaltyCardProps {
businessName: string;
@@ -15,6 +30,14 @@ export interface LoyaltyCardProps {
cardBackgroundColor?: string | null;
cardBackgroundImageUrl?: string | null;
stampIcon?: string | null;
/** See resolveRewardThreshold's doc comment — determines whether the final stamp position renders as a normal stamp or as the reward slot itself. */
rewardPosition?: RewardPositionMode;
cardTopRightText?: string | null;
cardFont?: LoyaltyCardFont;
cardTopRightTextSize?: LoyaltyCardTextSize;
cardTopRightTextColor?: string | null;
cardBorderColor?: string | null;
cardMemberNumberColor?: string | null;
}
/**
@@ -39,13 +62,34 @@ export function LoyaltyCard({
cardBackgroundColor,
cardBackgroundImageUrl,
stampIcon,
rewardPosition = RewardPositionMode.AFTER_ALL_STAMPS,
cardTopRightText,
cardFont = LoyaltyCardFont.SANS,
cardTopRightTextSize = LoyaltyCardTextSize.MD,
cardTopRightTextColor,
cardBorderColor,
cardMemberNumberColor,
}: LoyaltyCardProps) {
const icon = findCatalogIcon(stampIcon);
const threshold = resolveRewardThreshold({ stampsRequired, rewardPosition });
// Only FINAL_SLOT_IS_REWARD sets aside a dedicated slot — under
// AFTER_ALL_STAMPS every position is a plain stamp and the reward is
// shown separately (see the "Reward available" banner on the loyalty
// page), not by any one slot.
const rewardSlotIndex = rewardPosition === RewardPositionMode.FINAL_SLOT_IS_REWARD ? stampsRequired - 1 : null;
const borderColor = cardBorderColor || "var(--color-accent-loyalty)";
return (
<div
className="ring-accent-loyalty relative w-full overflow-hidden rounded-2xl shadow-[0_30px_60px_rgba(0,0,0,0.45)] ring-[3px]"
style={{ aspectRatio: "1.65 / 1", backgroundColor: cardBackgroundColor || "#141414" }}
className={cn(
"relative w-full overflow-hidden rounded-2xl",
FONT_CLASSES[cardFont],
)}
style={{
aspectRatio: "1.65 / 1",
backgroundColor: cardBackgroundColor || "#141414",
boxShadow: `0 30px 60px rgba(0,0,0,0.45), 0 0 0 3px ${borderColor}`,
}}
role="img"
aria-label={`${businessName} loyalty card, ${stampsEarned} of ${stampsRequired} stamps`}
>
@@ -66,33 +110,49 @@ export function LoyaltyCard({
/>
<div className="relative flex h-full flex-col justify-between p-4">
<div className="flex items-center justify-between">
<div className="flex items-start justify-between gap-2">
<h2 className="text-lg font-black italic tracking-tight text-white">Loyalty Card</h2>
<span className="border-accent-loyalty/30 text-accent-loyalty rounded border px-2 py-0.5 text-[8px] font-bold tracking-widest uppercase">
Member #{memberNumber}
</span>
<div className="flex flex-col items-end gap-1">
{cardTopRightText && (
<span
className={cn(TOP_RIGHT_TEXT_SIZE_CLASSES[cardTopRightTextSize], "text-right font-bold text-white")}
style={{ color: cardTopRightTextColor || undefined }}
>
{cardTopRightText}
</span>
)}
<span
className="border-accent-loyalty/30 text-accent-loyalty rounded border px-2 py-0.5 text-[8px] font-bold tracking-widest uppercase"
style={{ color: cardMemberNumberColor || undefined, borderColor: cardMemberNumberColor ? `${cardMemberNumberColor}4d` : undefined }}
>
Member #{memberNumber}
</span>
</div>
</div>
<div className="grid grid-cols-5 gap-2">
{Array.from({ length: stampsRequired }, (_, i) => {
const earned = i < stampsEarned;
// Only the final slot gets reward styling, and only once it's
// actually been earned — otherwise an in-progress card (e.g.
// 3/10 stamps) would show the reward label on slot 10 already.
const isRewardSlot = earned && i === stampsRequired - 1;
const isRewardSlot = i === rewardSlotIndex;
const earned = isRewardSlot ? stampsEarned >= threshold : i < stampsEarned;
return (
<div
key={i}
className={cn(
"flex aspect-square items-center justify-center rounded-full",
isRewardSlot
? "bg-accent-loyalty scale-[1.08] shadow-lg"
? earned
? "bg-accent-loyalty scale-[1.08] shadow-lg"
: "border-accent-loyalty/50 bg-accent-loyalty/10 border-2 border-dashed"
: earned
? "bg-white shadow-inner"
: "border-2 border-dashed border-white/20 bg-white/10",
)}
>
{isRewardSlot ? (
<span className="text-ink text-center text-[7px] leading-tight font-black">{rewardLabel}</span>
earned ? (
<span className="text-ink text-center text-[7px] leading-tight font-black">{rewardLabel}</span>
) : (
<Gift className="text-accent-loyalty h-3.5 w-3.5" aria-hidden="true" />
)
) : earned ? (
<FontAwesomeIcon icon={icon} className="text-ink h-3.5 w-3.5" />
) : null}
@@ -101,7 +161,7 @@ export function LoyaltyCard({
})}
</div>
{termsLine && (
<p className="text-center text-[7px] font-bold tracking-widest text-white/40 uppercase">{termsLine}</p>
<p className="text-center text-[7px] mt-2 font-bold tracking-widest text-white/40 uppercase">{termsLine}</p>
)}
</div>
</div>
+1 -1
View File
@@ -40,7 +40,7 @@ export function Modal({ open, onClose, title, children, className }: ModalProps)
}}
aria-labelledby="modal-title"
className={cn(
"m-auto w-[calc(100%-2rem)] max-w-md rounded-jumbo border border-line bg-white p-0 shadow-xl backdrop:bg-ink/40 backdrop:backdrop-blur-sm",
"m-auto w-[calc(100%-2rem)] max-w-md rounded-md border border-line bg-white p-0 shadow-xl backdrop:bg-ink/40 backdrop:backdrop-blur-sm",
className,
)}
>