mirror of
https://github.com/etiennecollin/unifi-voucher-manager.git
synced 2025-10-23 08:12:15 +00:00
refactor: voucher cards and voucher status
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
import { Voucher } from "@/types/voucher";
|
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";
|
import { memo, useCallback } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -12,7 +17,9 @@ type Props = {
|
|||||||
const VoucherCard = ({ voucher, selected, editMode, onClick }: Props) => {
|
const VoucherCard = ({ voucher, selected, editMode, onClick }: Props) => {
|
||||||
const statusClass = voucher.expired
|
const statusClass = voucher.expired
|
||||||
? "bg-status-danger text-status-danger"
|
? "bg-status-danger text-status-danger"
|
||||||
: "bg-status-success text-status-success";
|
: voucher.activatedAt
|
||||||
|
? "bg-status-warning text-status-warning"
|
||||||
|
: "bg-status-success text-status-success";
|
||||||
const onClickHandler = useCallback(
|
const onClickHandler = useCallback(
|
||||||
() => onClick?.(voucher),
|
() => onClick?.(voucher),
|
||||||
[voucher, onClick],
|
[voucher, onClick],
|
||||||
@@ -69,7 +76,7 @@ const VoucherCard = ({ voucher, selected, editMode, onClick }: Props) => {
|
|||||||
<span
|
<span
|
||||||
className={`px-2 py-1 rounded-lg text-xs font-semibold uppercase ${statusClass}`}
|
className={`px-2 py-1 rounded-lg text-xs font-semibold uppercase ${statusClass}`}
|
||||||
>
|
>
|
||||||
{voucher.expired ? "Expired" : "Active"}
|
{formatStatus(voucher.expired, voucher.activatedAt)}
|
||||||
</span>
|
</span>
|
||||||
{voucher.expiresAt && (
|
{voucher.expiresAt && (
|
||||||
<span className="text-xs">Expires: {voucher.expiresAt}</span>
|
<span className="text-xs">Expires: {voucher.expiresAt}</span>
|
||||||
|
@@ -3,15 +3,17 @@
|
|||||||
import Modal from "@/components/modals/Modal";
|
import Modal from "@/components/modals/Modal";
|
||||||
import Spinner from "@/components/utils/Spinner";
|
import Spinner from "@/components/utils/Spinner";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
formatBytes,
|
formatBytes,
|
||||||
formatDuration,
|
formatDuration,
|
||||||
formatGuestUsage,
|
formatGuestUsage,
|
||||||
formatSpeed,
|
formatSpeed,
|
||||||
|
formatStatus,
|
||||||
} from "@/utils/format";
|
} from "@/utils/format";
|
||||||
import VoucherCode from "@/components/utils/VoucherCode";
|
import VoucherCode from "@/components/utils/VoucherCode";
|
||||||
import { Voucher } from "@/types/voucher";
|
import { Voucher } from "@/types/voucher";
|
||||||
|
import { TriState } from "@/types/state";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
voucher: Voucher;
|
voucher: Voucher;
|
||||||
@@ -20,8 +22,7 @@ type Props = {
|
|||||||
|
|
||||||
export default function VoucherModal({ voucher, onClose }: Props) {
|
export default function VoucherModal({ voucher, onClose }: Props) {
|
||||||
const [details, setDetails] = useState<Voucher | null>(null);
|
const [details, setDetails] = useState<Voucher | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [state, setState] = useState<TriState | null>(null);
|
||||||
const [error, setError] = useState(false);
|
|
||||||
const lastFetchedId = useRef<string | null>(null);
|
const lastFetchedId = useRef<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,70 +32,81 @@ export default function VoucherModal({ voucher, onClose }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
setLoading(true);
|
setState("loading");
|
||||||
setError(false);
|
|
||||||
lastFetchedId.current = voucher.id;
|
lastFetchedId.current = voucher.id;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await api.getVoucherDetails(voucher.id);
|
const res = await api.getVoucherDetails(voucher.id);
|
||||||
setDetails(res);
|
setDetails(res);
|
||||||
|
setState("ok");
|
||||||
} catch {
|
} catch {
|
||||||
setError(true);
|
setState("error");
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [voucher.id]);
|
}, [voucher.id]);
|
||||||
|
|
||||||
|
const renderContent = useCallback(() => {
|
||||||
|
switch (state) {
|
||||||
|
case null:
|
||||||
|
case "loading":
|
||||||
|
return <Spinner />;
|
||||||
|
case "error":
|
||||||
|
return (
|
||||||
|
<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", formatStatus(details.expired, details.activatedAt)],
|
||||||
|
["Name", details.name || "No note"],
|
||||||
|
["Created", details.createdAt],
|
||||||
|
...(details.activatedAt
|
||||||
|
? [["Activated", details.activatedAt]]
|
||||||
|
: []),
|
||||||
|
...(details.expiresAt ? [["Expires", details.expiresAt]] : []),
|
||||||
|
["Duration", formatDuration(details.timeLimitMinutes)],
|
||||||
|
[
|
||||||
|
"Guest Usage",
|
||||||
|
formatGuestUsage(
|
||||||
|
details.authorizedGuestCount,
|
||||||
|
details.authorizedGuestLimit,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Data Limit",
|
||||||
|
details.dataUsageLimitMBytes
|
||||||
|
? formatBytes(details.dataUsageLimitMBytes * 1024 * 1024)
|
||||||
|
: "Unlimited",
|
||||||
|
],
|
||||||
|
["Download Speed", formatSpeed(details.rxRateLimitKbps)],
|
||||||
|
["Upload Speed", formatSpeed(details.txRateLimitKbps)],
|
||||||
|
["ID", details.id],
|
||||||
|
] as [string, any][]
|
||||||
|
).map(([label, value]) => (
|
||||||
|
<div
|
||||||
|
key={label}
|
||||||
|
className="flex-center-between p-4 bg-interactive border border-subtle rounded-xl space-x-4"
|
||||||
|
>
|
||||||
|
<span className="font-semibold text-primary">{label}:</span>
|
||||||
|
<span className="text-secondary">{value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [state, details]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal onClose={onClose}>
|
<Modal onClose={onClose}>
|
||||||
<VoucherCode voucher={voucher} contentClassName="mb-8" />
|
<VoucherCode voucher={voucher} contentClassName="mb-8" />
|
||||||
{loading ? (
|
{renderContent()}
|
||||||
<Spinner />
|
|
||||||
) : error || details == null ? (
|
|
||||||
<div className="card text-status-danger text-center">
|
|
||||||
Failed to load detailed information
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
["Status", details.expired ? "Expired" : "Active"],
|
|
||||||
["Name", details.name || "No note"],
|
|
||||||
["Created", details.createdAt],
|
|
||||||
...(details.activatedAt
|
|
||||||
? [["Activated", details.activatedAt]]
|
|
||||||
: []),
|
|
||||||
...(details.expiresAt ? [["Expires", details.expiresAt]] : []),
|
|
||||||
["Duration", formatDuration(details.timeLimitMinutes)],
|
|
||||||
[
|
|
||||||
"Guest Usage",
|
|
||||||
formatGuestUsage(
|
|
||||||
details.authorizedGuestCount,
|
|
||||||
details.authorizedGuestLimit,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"Data Limit",
|
|
||||||
details.dataUsageLimitMBytes
|
|
||||||
? formatBytes(details.dataUsageLimitMBytes * 1024 * 1024)
|
|
||||||
: "Unlimited",
|
|
||||||
],
|
|
||||||
["Download Speed", formatSpeed(details.rxRateLimitKbps)],
|
|
||||||
["Upload Speed", formatSpeed(details.txRateLimitKbps)],
|
|
||||||
["ID", details.id],
|
|
||||||
] as [string, any][]
|
|
||||||
).map(([label, value]) => (
|
|
||||||
<div
|
|
||||||
key={label}
|
|
||||||
className="flex-center-between p-4 bg-interactive border border-subtle rounded-xl space-x-4"
|
|
||||||
>
|
|
||||||
<span className="font-semibold text-primary">{label}:</span>
|
|
||||||
<span className="text-secondary">{value}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -6,6 +6,15 @@ export function formatMaxGuests(maxGuests: number | null | undefined) {
|
|||||||
return !maxGuests ? "Unlimited" : Math.max(maxGuests, 0);
|
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) {
|
export function formatDuration(m: number | null | undefined) {
|
||||||
if (!m) return "Unlimited";
|
if (!m) return "Unlimited";
|
||||||
const days = Math.floor(m / 1440),
|
const days = Math.floor(m / 1440),
|
||||||
|
Reference in New Issue
Block a user