refactor: voucher cards and voucher status

This commit is contained in:
etiennecollin
2025-09-07 15:54:24 -04:00
parent b81efc3c87
commit dacd6a9a5c
3 changed files with 85 additions and 57 deletions

View File

@@ -1,5 +1,10 @@
import { Voucher } from "@/types/voucher";
import { formatCode, formatDuration, formatGuestUsage } from "@/utils/format";
import {
formatCode,
formatDuration,
formatGuestUsage,
formatStatus,
} from "@/utils/format";
import { memo, useCallback } from "react";
type Props = {
@@ -12,6 +17,8 @@ type Props = {
const VoucherCard = ({ voucher, selected, editMode, onClick }: Props) => {
const statusClass = voucher.expired
? "bg-status-danger text-status-danger"
: voucher.activatedAt
? "bg-status-warning text-status-warning"
: "bg-status-success text-status-success";
const onClickHandler = useCallback(
() => onClick?.(voucher),
@@ -69,7 +76,7 @@ const VoucherCard = ({ voucher, selected, editMode, onClick }: Props) => {
<span
className={`px-2 py-1 rounded-lg text-xs font-semibold uppercase ${statusClass}`}
>
{voucher.expired ? "Expired" : "Active"}
{formatStatus(voucher.expired, voucher.activatedAt)}
</span>
{voucher.expiresAt && (
<span className="text-xs">Expires: {voucher.expiresAt}</span>

View File

@@ -3,15 +3,17 @@
import Modal from "@/components/modals/Modal";
import Spinner from "@/components/utils/Spinner";
import { api } from "@/utils/api";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
formatBytes,
formatDuration,
formatGuestUsage,
formatSpeed,
formatStatus,
} from "@/utils/format";
import VoucherCode from "@/components/utils/VoucherCode";
import { Voucher } from "@/types/voucher";
import { TriState } from "@/types/state";
type Props = {
voucher: Voucher;
@@ -20,8 +22,7 @@ type Props = {
export default function VoucherModal({ voucher, onClose }: Props) {
const [details, setDetails] = useState<Voucher | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [state, setState] = useState<TriState | null>(null);
const lastFetchedId = useRef<string | null>(null);
useEffect(() => {
@@ -31,35 +32,39 @@ export default function VoucherModal({ voucher, onClose }: Props) {
}
(async () => {
setLoading(true);
setError(false);
setState("loading");
lastFetchedId.current = voucher.id;
try {
const res = await api.getVoucherDetails(voucher.id);
setDetails(res);
setState("ok");
} catch {
setError(true);
} finally {
setLoading(false);
setState("error");
}
})();
}, [voucher.id]);
const renderContent = useCallback(() => {
switch (state) {
case null:
case "loading":
return <Spinner />;
case "error":
return (
<Modal onClose={onClose}>
<VoucherCode voucher={voucher} contentClassName="mb-8" />
{loading ? (
<Spinner />
) : error || details == null ? (
<div className="card text-status-danger text-center">
Failed to load detailed information
</div>
) : (
);
case "ok":
if (details == null) {
return;
}
return (
<div className="space-y-4">
{(
[
["Status", details.expired ? "Expired" : "Active"],
["Status", formatStatus(details.expired, details.activatedAt)],
["Name", details.name || "No note"],
["Created", details.createdAt],
...(details.activatedAt
@@ -94,7 +99,14 @@ export default function VoucherModal({ voucher, onClose }: Props) {
</div>
))}
</div>
)}
);
}
}, [state, details]);
return (
<Modal onClose={onClose}>
<VoucherCode voucher={voucher} contentClassName="mb-8" />
{renderContent()}
</Modal>
);
}

View File

@@ -6,6 +6,15 @@ export function formatMaxGuests(maxGuests: number | null | undefined) {
return !maxGuests ? "Unlimited" : Math.max(maxGuests, 0);
}
export function formatStatus(
expired: boolean,
activatedAt: string | null | undefined,
) {
if (expired) return "Expired";
if (activatedAt) return "Active";
return "Available";
}
export function formatDuration(m: number | null | undefined) {
if (!m) return "Unlimited";
const days = Math.floor(m / 1440),