Improve admin UI and vendor invite experience
All checks were successful
Build Frontend / build (push) Successful in 1m7s

Enhanced the admin dashboard tab styling for better clarity. Refactored InviteVendorCard with improved UI, feedback, and clipboard copy functionality. Fixed vendor store ID update to send raw object instead of JSON string. Ensured product price formatting is robust against non-numeric values.
This commit is contained in:
g
2026-01-12 07:33:16 +00:00
parent 1186952ed8
commit 244014f33a
4 changed files with 84 additions and 25 deletions

View File

@@ -403,11 +403,12 @@ export default function AdminPage() {
</div> </div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6"> <Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList> <TabsList className="bg-muted/20 p-1 border border-border/40 backdrop-blur-sm h-auto">
<TabsTrigger <TabsTrigger
value="analytics" value="analytics"
onMouseEnter={() => handleTabHover("analytics")} onMouseEnter={() => handleTabHover("analytics")}
onFocus={() => handleTabFocus("analytics")} onFocus={() => handleTabFocus("analytics")}
className="data-[state=active]:bg-background/80 data-[state=active]:backdrop-blur-sm data-[state=active]:text-foreground data-[state=active]:shadow-sm px-6 py-2 transition-all"
> >
Analytics Analytics
</TabsTrigger> </TabsTrigger>
@@ -415,6 +416,7 @@ export default function AdminPage() {
value="management" value="management"
onMouseEnter={() => handleTabHover("management")} onMouseEnter={() => handleTabHover("management")}
onFocus={() => handleTabFocus("management")} onFocus={() => handleTabFocus("management")}
className="data-[state=active]:bg-background/80 data-[state=active]:backdrop-blur-sm data-[state=active]:text-foreground data-[state=active]:shadow-sm px-6 py-2 transition-all"
> >
Management Management
</TabsTrigger> </TabsTrigger>

View File

@@ -61,7 +61,7 @@ export default function AdminVendorsPage() {
setUpdating(true); setUpdating(true);
await fetchClient(`/admin/vendors/${editingVendor._id}/store-id`, { await fetchClient(`/admin/vendors/${editingVendor._id}/store-id`, {
method: 'PUT', method: 'PUT',
body: JSON.stringify({ storeId: newStoreId }) body: { storeId: newStoreId }
}); });
toast({ toast({

View File

@@ -1,47 +1,104 @@
"use client";
import { useState } from "react"; import { useState } from "react";
import { fetchClient } from "@/lib/api-client"; import { fetchClient } from "@/lib/api-client";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Copy, Check, Ticket, Loader2, RefreshCw } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
export default function InviteVendorCard() { export default function InviteVendorCard() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [code, setCode] = useState<string | null>(null); const [code, setCode] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const { toast } = useToast();
async function handleInvite() { async function handleInvite() {
setLoading(true); setLoading(true);
setMessage(null);
setCode(null); setCode(null);
setCopied(false);
try { try {
const res = await fetchClient<{ code: string }>("/admin/invitations", { method: "POST" }); const res = await fetchClient<{ code: string }>("/admin/invitations", { method: "POST" });
setMessage("Invitation created");
setCode(res.code); setCode(res.code);
toast({
title: "Invitation Created",
description: "New vendor invitation code generated successfully.",
});
} catch (e: any) { } catch (e: any) {
setMessage(e?.message || "Failed to send invitation"); toast({
title: "Error",
description: e?.message || "Failed to generate invitation",
variant: "destructive",
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
} }
const copyToClipboard = () => {
if (!code) return;
navigator.clipboard.writeText(code);
setCopied(true);
toast({
title: "Copied",
description: "Invitation code copied to clipboard",
});
setTimeout(() => setCopied(false), 2000);
};
return ( return (
<div className="rounded-lg border border-border/60 bg-background p-4 h-full min-h-[200px]"> <Card className="h-full border-border/40 bg-background/50 backdrop-blur-sm shadow-sm flex flex-col">
<h2 className="font-medium">Invite Vendor</h2> <CardHeader className="pb-3">
<p className="text-sm text-muted-foreground mt-1">Generate a new invitation code</p> <div className="flex items-center justify-between">
<div className="mt-4 space-y-3"> <CardTitle className="text-base font-medium flex items-center gap-2">
<button <Ticket className="h-4 w-4 text-primary" />
onClick={handleInvite} Invite Vendor
className="inline-flex items-center rounded-md bg-primary px-3 py-2 text-sm text-primary-foreground disabled:opacity-60" </CardTitle>
disabled={loading} </div>
> <CardDescription>Generate a one-time invitation code.</CardDescription>
{loading ? "Generating..." : "Generate Invite Code"} </CardHeader>
</button> <CardContent className="flex-1 flex flex-col justify-center gap-4">
{message && <p className="text-xs text-muted-foreground">{message}</p>} {code ? (
{code && ( <div className="space-y-3 animate-in fade-in zoom-in-95 duration-300">
<div className="text-sm"> <div className="p-3 rounded-md bg-muted/50 border border-border/50 text-center relative group">
Code: <span className="font-mono px-1.5 py-0.5 rounded bg-muted">{code}</span> <span className="font-mono text-xl font-bold tracking-widest text-primary">{code}</span>
<Button
variant="ghost"
size="icon"
className="absolute right-1 top-1 h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={copyToClipboard}
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
<p className="text-xs text-center text-muted-foreground">
Share this code with the new vendor. It expires in 7 days.
</p>
</div>
) : (
<div className="text-center py-2 text-sm text-muted-foreground/80">
Click generate to create a new code.
</div> </div>
)} )}
</div> </CardContent>
</div> <CardFooter className="pt-0">
<Button
onClick={handleInvite}
disabled={loading}
className="w-full bg-primary/90 hover:bg-primary shadow-sm"
>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating...
</>
) : (
<>
{code ? <RefreshCw className="mr-2 h-4 w-4" /> : <Ticket className="mr-2 h-4 w-4" />}
{code ? "Generate Another" : "Generate Code"}
</>
)}
</Button>
</CardFooter>
</Card>
); );
} }

View File

@@ -181,7 +181,7 @@ export default function Content({ username, orderStats }: ContentProps) {
<div className="flex-grow min-w-0"> <div className="flex-grow min-w-0">
<h4 className="font-semibold text-lg truncate group-hover:text-primary transition-colors">{product.name}</h4> <h4 className="font-semibold text-lg truncate group-hover:text-primary transition-colors">{product.name}</h4>
<div className="flex items-center gap-3 mt-0.5"> <div className="flex items-center gap-3 mt-0.5">
<span className="text-sm text-muted-foreground font-medium">£{product.price.toFixed(2)}</span> <span className="text-sm text-muted-foreground font-medium">£{(Number(product.price) || 0).toFixed(2)}</span>
<span className="h-1 w-1 rounded-full bg-muted-foreground/30" /> <span className="h-1 w-1 rounded-full bg-muted-foreground/30" />
<span className="text-xs text-muted-foreground">ID: {product.id.slice(-6)}</span> <span className="text-xs text-muted-foreground">ID: {product.id.slice(-6)}</span>
</div> </div>