All checks were successful
Build Frontend / build (push) Successful in 1m12s
Refactored dashboard pages for improved layout and visual consistency using Card components, motion animations, and updated color schemes. Added an OrderTimeline component to the order details page to visualize order lifecycle. Improved customer management page with better sorting, searching, and a detailed customer dialog. Updated storefront settings page with a modernized layout and clearer sectioning.
468 lines
19 KiB
TypeScript
468 lines
19 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, ChangeEvent } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Layout from "@/components/layout/layout";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Save, Send, Key, MessageSquare, Shield, Globe, Wallet, RefreshCw } from "lucide-react";
|
|
import { apiRequest } from "@/lib/api";
|
|
import { toast } from "sonner";
|
|
import BroadcastDialog from "@/components/modals/broadcast-dialog";
|
|
import Dashboard from "@/components/dashboard/dashboard";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
|
|
const SHIPPING_REGIONS = [
|
|
{ value: "UK", label: "United Kingdom", emoji: "🇬🇧" },
|
|
{ value: "EU", label: "European Union", emoji: "🇪🇺" },
|
|
{ value: "USA", label: "United States", emoji: "🇺🇸" },
|
|
{ value: "WW", label: "Worldwide", emoji: "🌍" },
|
|
] as const;
|
|
|
|
interface Storefront {
|
|
pgpKey: string;
|
|
welcomeMessage: string;
|
|
telegramToken: string;
|
|
shipsFrom: typeof SHIPPING_REGIONS[number]["value"];
|
|
shipsTo: typeof SHIPPING_REGIONS[number]["value"];
|
|
storePolicy: string;
|
|
isEnabled: boolean;
|
|
wallets: {
|
|
bitcoin?: string;
|
|
litecoin: string;
|
|
monero?: string;
|
|
};
|
|
enabledWallets: {
|
|
bitcoin: boolean;
|
|
litecoin: boolean;
|
|
monero: boolean;
|
|
};
|
|
}
|
|
|
|
const WALLET_OPTIONS = [
|
|
{
|
|
id: 'bitcoin',
|
|
name: 'Bitcoin',
|
|
emoji: '₿',
|
|
placeholder: 'Your BTC address',
|
|
disabled: true,
|
|
comingSoon: true
|
|
},
|
|
{
|
|
id: 'litecoin',
|
|
name: 'Litecoin',
|
|
emoji: 'Ł',
|
|
placeholder: 'Your LTC address',
|
|
disabled: false,
|
|
comingSoon: false
|
|
},
|
|
{
|
|
id: 'monero',
|
|
name: 'Monero',
|
|
emoji: 'M',
|
|
placeholder: 'Your XMR address',
|
|
disabled: true,
|
|
comingSoon: true
|
|
},
|
|
] as const;
|
|
|
|
export default function StorefrontPage() {
|
|
const router = useRouter();
|
|
const [storefront, setStorefront] = useState<Storefront>({
|
|
pgpKey: "",
|
|
welcomeMessage: "",
|
|
telegramToken: "",
|
|
shipsFrom: "UK",
|
|
shipsTo: "WW",
|
|
storePolicy: "",
|
|
isEnabled: false,
|
|
wallets: {
|
|
bitcoin: '',
|
|
litecoin: '',
|
|
monero: ''
|
|
},
|
|
enabledWallets: {
|
|
bitcoin: false,
|
|
litecoin: false,
|
|
monero: false
|
|
}
|
|
});
|
|
|
|
const [broadcastOpen, setBroadcastOpen] = useState<boolean>(false);
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
const [saving, setSaving] = useState<boolean>(false);
|
|
|
|
useEffect(() => {
|
|
const authToken = document.cookie
|
|
.split("; ")
|
|
.find((row) => row.startsWith("Authorization="))
|
|
?.split("=")[1];
|
|
|
|
if (!authToken) {
|
|
router.push("/login");
|
|
return;
|
|
}
|
|
|
|
const fetchStorefront = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await apiRequest("/storefront");
|
|
setStorefront({
|
|
pgpKey: data.pgpKey || "",
|
|
welcomeMessage: data.welcomeMessage || "",
|
|
telegramToken: data.telegramToken || "",
|
|
shipsFrom: data.shipsFrom || "UK",
|
|
shipsTo: data.shipsTo || "WW",
|
|
storePolicy: data.storePolicy || "",
|
|
isEnabled: data.isEnabled || false,
|
|
wallets: {
|
|
bitcoin: data.wallets?.bitcoin || '',
|
|
litecoin: data.wallets?.litecoin || '',
|
|
monero: data.wallets?.monero || ''
|
|
},
|
|
enabledWallets: {
|
|
bitcoin: data.enabledWallets?.bitcoin || false,
|
|
litecoin: data.enabledWallets?.litecoin || true,
|
|
monero: data.enabledWallets?.monero || false
|
|
}
|
|
});
|
|
} catch (error) {
|
|
toast.error("Failed to load storefront data.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchStorefront();
|
|
}, []);
|
|
|
|
const handleInputChange = (
|
|
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
|
) => {
|
|
setStorefront(prev => ({ ...prev, [e.target.name]: e.target.value }));
|
|
};
|
|
|
|
const saveStorefront = async () => {
|
|
try {
|
|
setSaving(true);
|
|
await apiRequest("/storefront", "PUT", storefront);
|
|
toast.success("Storefront updated successfully!");
|
|
} catch (error) {
|
|
toast.error("Failed to update storefront.");
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dashboard>
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between mb-8">
|
|
<div className="flex items-center gap-4">
|
|
<div className="p-3 rounded-xl bg-primary/10 text-primary">
|
|
<Globe className="h-8 w-8" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight text-foreground">
|
|
Storefront Settings
|
|
</h1>
|
|
<p className="text-muted-foreground">
|
|
Manage your shop's appearance, policies, and configuration
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setBroadcastOpen(true)}
|
|
className="gap-2 h-10"
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
Broadcast
|
|
</Button>
|
|
<Button
|
|
onClick={saveStorefront}
|
|
disabled={saving}
|
|
className="gap-2 h-10 min-w-[120px]"
|
|
>
|
|
{saving ? <RefreshCw className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
|
{saving ? "Saving..." : "Save Changes"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Main Column */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
|
|
{/* Store Status Card */}
|
|
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }}>
|
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden relative">
|
|
<div className={`absolute top-0 left-0 w-1 h-full ${storefront.isEnabled ? 'bg-emerald-500' : 'bg-destructive'}`} />
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<CardTitle>Store Status</CardTitle>
|
|
<CardDescription>Control your store's visibility to customers</CardDescription>
|
|
</div>
|
|
<Badge variant={storefront.isEnabled ? "default" : "destructive"} className="h-6">
|
|
{storefront.isEnabled ? "Open for Business" : "Store Closed"}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="pb-6">
|
|
<div className="flex items-center gap-4 p-4 rounded-lg bg-card border border-border/50">
|
|
<Switch
|
|
checked={storefront.isEnabled}
|
|
onCheckedChange={(checked) =>
|
|
setStorefront((prev) => ({
|
|
...prev,
|
|
isEnabled: checked,
|
|
}))
|
|
}
|
|
className="data-[state=checked]:bg-emerald-500"
|
|
/>
|
|
<div>
|
|
<h4 className="font-medium text-sm">
|
|
{storefront.isEnabled ? 'Your store is currently online' : 'Your store is currently offline'}
|
|
</h4>
|
|
<p className="text-xs text-muted-foreground">
|
|
{storefront.isEnabled
|
|
? 'Customers can browse listings and place orders normally.'
|
|
: 'Customers will see a maintenance page. No new orders can be placed.'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
{/* Welcome & Policy */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }}>
|
|
<Card className="h-full border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
<MessageSquare className="h-4 w-4 text-primary" />
|
|
Welcome Message
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Textarea
|
|
value={storefront.welcomeMessage}
|
|
onChange={(e) => setStorefront(prev => ({ ...prev, welcomeMessage: e.target.value }))}
|
|
placeholder="Enter the welcome message for new customers..."
|
|
className="min-h-[180px] bg-background/50 border-border/50 resize-none focus:ring-primary/20"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }}>
|
|
<Card className="h-full border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
<Shield className="h-4 w-4 text-orange-400" />
|
|
Store Policy
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Textarea
|
|
value={storefront.storePolicy}
|
|
onChange={(e) => setStorefront(prev => ({ ...prev, storePolicy: e.target.value }))}
|
|
placeholder="Enter your store's policies, terms, and conditions..."
|
|
className="min-h-[180px] bg-background/50 border-border/50 resize-none focus:ring-primary/20"
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Security Settings */}
|
|
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.3 }}>
|
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Key className="h-5 w-5 text-purple-400" />
|
|
Security Configuration
|
|
</CardTitle>
|
|
<CardDescription>Manage keys and access tokens for your store security</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">PGP Public Key</Label>
|
|
<Textarea
|
|
value={storefront.pgpKey}
|
|
onChange={(e) => setStorefront(prev => ({ ...prev, pgpKey: e.target.value }))}
|
|
placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----..."
|
|
className="font-mono text-xs h-32 bg-zinc-950/50 border-zinc-800/50 resize-none"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Telegram Bot Token</Label>
|
|
<div className="relative">
|
|
<Input
|
|
type="password"
|
|
value={storefront.telegramToken}
|
|
onChange={(e) => setStorefront(prev => ({ ...prev, telegramToken: e.target.value }))}
|
|
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
|
className="bg-background/50 border-border/50 font-mono text-sm pl-10"
|
|
/>
|
|
<div className="absolute left-3 top-2.5 text-muted-foreground">
|
|
<Shield className="h-4 w-4" />
|
|
</div>
|
|
</div>
|
|
<p className="text-[10px] text-muted-foreground">Used for notifications and bot integration.</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Sidebar Column */}
|
|
<div className="space-y-6">
|
|
{/* Shipping Settings */}
|
|
<motion.div initial={{ opacity: 0, x: 10 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.4 }}>
|
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
<Globe className="h-4 w-4 text-blue-400" />
|
|
Shipping & Logistics
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Ships From</Label>
|
|
<Select
|
|
value={storefront.shipsFrom}
|
|
onValueChange={(value) =>
|
|
setStorefront((prev) => ({ ...prev, shipsFrom: value as any }))
|
|
}
|
|
>
|
|
<SelectTrigger className="bg-background/50 border-border/50">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{SHIPPING_REGIONS.map((region) => (
|
|
<SelectItem key={region.value} value={region.value}>
|
|
<span className="flex items-center gap-2">
|
|
<span className="text-lg">{region.emoji}</span>
|
|
{region.label}
|
|
</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Ships To</Label>
|
|
<Select
|
|
value={storefront.shipsTo}
|
|
onValueChange={(value) =>
|
|
setStorefront((prev) => ({ ...prev, shipsTo: value as any }))
|
|
}
|
|
>
|
|
<SelectTrigger className="bg-background/50 border-border/50">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{SHIPPING_REGIONS.map((region) => (
|
|
<SelectItem key={region.value} value={region.value}>
|
|
<span className="flex items-center gap-2">
|
|
<span className="text-lg">{region.emoji}</span>
|
|
{region.label}
|
|
</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
|
|
{/* Payment Methods */}
|
|
<motion.div initial={{ opacity: 0, x: 10 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 0.5 }}>
|
|
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2 text-lg">
|
|
<Wallet className="h-4 w-4 text-yellow-500" />
|
|
Crypto Wallets
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{WALLET_OPTIONS.map((wallet) => (
|
|
<div key={wallet.id} className="p-3 rounded-lg border border-border/50 bg-card/30 hover:bg-card/50 transition-colors">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<label className="text-sm font-medium flex items-center gap-2">
|
|
<span className="text-lg">{wallet.emoji}</span>
|
|
{wallet.name}
|
|
</label>
|
|
<div className="flex items-center gap-2">
|
|
{wallet.comingSoon && (
|
|
<Badge variant="secondary" className="text-[10px] h-5">Soon</Badge>
|
|
)}
|
|
<Switch
|
|
checked={storefront.enabledWallets[wallet.id]}
|
|
onCheckedChange={(checked) =>
|
|
setStorefront((prev) => ({
|
|
...prev,
|
|
enabledWallets: {
|
|
...prev.enabledWallets,
|
|
[wallet.id]: checked,
|
|
},
|
|
}))
|
|
}
|
|
disabled={wallet.disabled}
|
|
className="scale-90"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{storefront.enabledWallets[wallet.id] && !wallet.disabled && (
|
|
<motion.div initial={{ height: 0, opacity: 0 }} animate={{ height: 'auto', opacity: 1 }}>
|
|
<Input
|
|
value={storefront.wallets[wallet.id]}
|
|
onChange={(e) =>
|
|
setStorefront((prev) => ({
|
|
...prev,
|
|
wallets: {
|
|
...prev.wallets,
|
|
[wallet.id]: e.target.value,
|
|
},
|
|
}))
|
|
}
|
|
placeholder={wallet.placeholder}
|
|
className="font-mono text-xs h-9 bg-background/50"
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<BroadcastDialog open={broadcastOpen} setOpen={setBroadcastOpen} />
|
|
|
|
</Dashboard >
|
|
);
|
|
}
|