From a248dc559b6eb30e2462b7b955871b63412d687b Mon Sep 17 00:00:00 2001 From: notshop Date: Sun, 26 Apr 2026 16:36:05 +0000 Subject: [PATCH] chore: add client/src/pages/admin/discounts.tsx --- client/src/pages/admin/discounts.tsx | 184 +++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 client/src/pages/admin/discounts.tsx diff --git a/client/src/pages/admin/discounts.tsx b/client/src/pages/admin/discounts.tsx new file mode 100644 index 0000000..3b9d364 --- /dev/null +++ b/client/src/pages/admin/discounts.tsx @@ -0,0 +1,184 @@ +import { useState } from "react"; +import { useQuery, useMutation } from "@tanstack/react-query"; +import { queryClient, apiRequest } from "../../lib/queryClient"; +import { AdminNav } from "./dashboard"; +import { toast } from "../../hooks/use-toast"; +import { Plus, Trash2, X } from "lucide-react"; + +interface DiscountCode { + id: number; + code: string; + type: string; + value: string; + minOrderAmount?: string; + maxUses?: number; + usedCount: number; + active: boolean; + expiresAt?: string; + createdAt: string; +} + +function DiscountModal({ onClose }: { onClose: () => void }) { + const [form, setForm] = useState({ code: "", type: "percentage", value: "", minOrderAmount: "", maxUses: "", expiresAt: "" }); + + const mutation = useMutation({ + mutationFn: (body: object) => apiRequest("POST", "/api/admin/discounts", body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["/api/admin/discounts"] }); + toast({ title: "Discount code created" }); + onClose(); + }, + onError: (err: any) => toast({ title: "Error", description: err.message, variant: "destructive" }), + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + mutation.mutate({ + ...form, + maxUses: form.maxUses ? parseInt(form.maxUses) : null, + expiresAt: form.expiresAt || null, + }); + }; + + return ( +
+
+
+

New discount code

+ +
+
+
+ + setForm({ ...form, code: e.target.value.toUpperCase() })} + placeholder="SUMMER20" required + className="w-full px-3 py-2 border border-border rounded-md text-sm bg-background font-mono uppercase focus:outline-none focus:ring-2 focus:ring-primary/30" + data-testid="input-discount-code" /> +
+
+
+ + +
+
+ + setForm({ ...form, value: e.target.value })} required placeholder={form.type === "percentage" ? "20" : "10.00"} + className="w-full px-3 py-2 border border-border rounded-md text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30" /> +
+
+
+
+ + setForm({ ...form, minOrderAmount: e.target.value })} placeholder="0.00" + className="w-full px-3 py-2 border border-border rounded-md text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30" /> +
+
+ + setForm({ ...form, maxUses: e.target.value })} placeholder="unlimited" + className="w-full px-3 py-2 border border-border rounded-md text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30" /> +
+
+
+ + setForm({ ...form, expiresAt: e.target.value })} + className="w-full px-3 py-2 border border-border rounded-md text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30" /> +
+
+ + +
+
+
+
+ ); +} + +export default function AdminDiscounts() { + const [showModal, setShowModal] = useState(false); + + const { data: codes, isLoading } = useQuery({ + queryKey: ["/api/admin/discounts"], + queryFn: async () => { + const r = await fetch("/api/admin/discounts", { credentials: "include" }); + if (r.status === 401) { window.location.href = "/admin/login"; return []; } + return r.json(); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: number) => apiRequest("DELETE", `/api/admin/discounts/${id}`), + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["/api/admin/discounts"] }); toast({ title: "Discount deleted" }); }, + }); + + const toggleMutation = useMutation({ + mutationFn: ({ id, active }: { id: number; active: boolean }) => apiRequest("PATCH", `/api/admin/discounts/${id}`, { active }), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["/api/admin/discounts"] }), + }); + + return ( +
+ +
+
+

Discount codes

+ +
+ +
+ + + + + + + + + + + + + {isLoading ? ( + Array.from({ length: 3 }).map((_, i) => ( + + )) + ) : codes?.map((c) => ( + + + + + + + + + ))} + {!isLoading && !codes?.length && ( + + )} + +
CodeDiscountUsesStatusExpiresActions
{c.code}{c.type === "percentage" ? `${c.value}% off` : `$${c.value} off`}{c.usedCount}{c.maxUses ? ` / ${c.maxUses}` : ""} + + {c.expiresAt ? new Date(c.expiresAt).toLocaleDateString() : "Never"} + +
No discount codes yet
+
+
+ {showModal && setShowModal(false)} />} +
+ ); +}