Replaces all usages of clientFetch with the new apiRequest utility across dashboard pages, modal components, and the profit analytics service. This standardizes API interaction and improves consistency in request handling.
417 lines
15 KiB
TypeScript
417 lines
15 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 } 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 {
|
|
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">
|
|
<div className="flex items-center gap-6">
|
|
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white flex items-center">
|
|
<Globe className="mr-2 h-6 w-6" />
|
|
Storefront Settings
|
|
</h1>
|
|
<div className="flex items-center gap-2">
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
checked={storefront.isEnabled}
|
|
onCheckedChange={(checked) =>
|
|
setStorefront((prev) => ({
|
|
...prev,
|
|
isEnabled: checked,
|
|
}))
|
|
}
|
|
/>
|
|
<span className={`text-sm font-medium ${storefront.isEnabled ? 'text-emerald-400' : 'text-zinc-400'}`}>
|
|
{storefront.isEnabled ? 'Store Open' : 'Store Closed'}
|
|
</span>
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>{storefront.isEnabled ? 'Click to close store' : 'Click to open store'}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setBroadcastOpen(true)}
|
|
className="gap-2"
|
|
size="sm"
|
|
>
|
|
<Send className="h-4 w-4" />
|
|
Broadcast
|
|
</Button>
|
|
<Button
|
|
onClick={saveStorefront}
|
|
disabled={saving}
|
|
className="gap-2"
|
|
size="sm"
|
|
>
|
|
<Save className="h-4 w-4" />
|
|
{saving ? "Saving..." : "Save Changes"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4 lg:gap-6">
|
|
{/* Security Settings */}
|
|
<div className="space-y-3">
|
|
<div className="bg-[#0F0F12] rounded-lg p-4 border border-zinc-800">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Shield className="h-4 w-4 text-purple-400" />
|
|
<h2 className="text-base font-medium text-zinc-100">
|
|
Security
|
|
</h2>
|
|
</div>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<label className="text-xs font-medium mb-1 block text-zinc-400">PGP Public Key</label>
|
|
<Textarea
|
|
value={storefront.pgpKey}
|
|
onChange={(e) => setStorefront(prev => ({ ...prev, pgpKey: e.target.value }))}
|
|
placeholder="Enter your PGP public key"
|
|
className="font-mono text-sm h-24 bg-[#1C1C1C] border-zinc-800 resize-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-medium mb-1 block text-zinc-400">Telegram Bot Token</label>
|
|
<Input
|
|
type="password"
|
|
value={storefront.telegramToken}
|
|
onChange={(e) => setStorefront(prev => ({ ...prev, telegramToken: e.target.value }))}
|
|
placeholder="Enter your Telegram bot token"
|
|
className="bg-[#1C1C1C] border-zinc-800 h-8 text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Shipping Settings */}
|
|
<div className="bg-[#0F0F12] rounded-lg p-4 border border-zinc-800">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Globe className="h-4 w-4 text-blue-400" />
|
|
<h2 className="text-base font-medium text-zinc-100">
|
|
Shipping
|
|
</h2>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label className="text-xs font-medium mb-1 block text-zinc-400">Ships From</label>
|
|
<Select
|
|
value={storefront.shipsFrom}
|
|
onValueChange={(value) =>
|
|
setStorefront((prev) => ({ ...prev, shipsFrom: value as any }))
|
|
}
|
|
>
|
|
<SelectTrigger className="bg-[#1C1C1C] border-zinc-800 h-8 text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{SHIPPING_REGIONS.map((region) => (
|
|
<SelectItem key={region.value} value={region.value}>
|
|
{region.emoji} {region.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-medium mb-1 block text-zinc-400">Ships To</label>
|
|
<Select
|
|
value={storefront.shipsTo}
|
|
onValueChange={(value) =>
|
|
setStorefront((prev) => ({ ...prev, shipsTo: value as any }))
|
|
}
|
|
>
|
|
<SelectTrigger className="bg-[#1C1C1C] border-zinc-800 h-8 text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{SHIPPING_REGIONS.map((region) => (
|
|
<SelectItem key={region.value} value={region.value}>
|
|
{region.emoji} {region.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Messaging and Payments */}
|
|
<div className="space-y-3">
|
|
<div className="bg-[#0F0F12] rounded-lg p-4 border border-zinc-800">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<MessageSquare className="h-4 w-4 text-emerald-400" />
|
|
<h2 className="text-base font-medium text-zinc-100">
|
|
Welcome Message
|
|
</h2>
|
|
</div>
|
|
<Textarea
|
|
value={storefront.welcomeMessage}
|
|
onChange={(e) => setStorefront(prev => ({ ...prev, welcomeMessage: e.target.value }))}
|
|
placeholder="Enter the welcome message for new customers"
|
|
className="h-36 bg-[#1C1C1C] border-zinc-800 text-sm resize-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="bg-[#0F0F12] rounded-lg p-4 border border-zinc-800">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Shield className="h-4 w-4 text-orange-400" />
|
|
<h2 className="text-base font-medium text-zinc-100">
|
|
Store Policy
|
|
</h2>
|
|
</div>
|
|
<Textarea
|
|
value={storefront.storePolicy}
|
|
onChange={(e) => setStorefront(prev => ({ ...prev, storePolicy: e.target.value }))}
|
|
placeholder="Enter your store's policies, terms, and conditions"
|
|
className="h-48 bg-[#1C1C1C] border-zinc-800 text-sm resize-none"
|
|
/>
|
|
</div>
|
|
|
|
<div className="bg-[#0F0F12] rounded-lg p-4 border border-zinc-800">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Wallet className="h-4 w-4 text-yellow-400" />
|
|
<h2 className="text-base font-medium text-zinc-100">
|
|
Payment Methods
|
|
</h2>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{WALLET_OPTIONS.map((wallet) => (
|
|
<div key={wallet.id} className="space-y-1.5">
|
|
<div className="flex items-center justify-between">
|
|
<label className="text-xs font-medium flex items-center gap-2 text-zinc-400">
|
|
<span>{wallet.emoji}</span>
|
|
{wallet.name}
|
|
{wallet.comingSoon && (
|
|
<span className="text-[10px] bg-purple-900/50 text-purple-400 px-1.5 py-0.5 rounded">
|
|
Coming Soon
|
|
</span>
|
|
)}
|
|
</label>
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<div>
|
|
<Switch
|
|
checked={storefront.enabledWallets[wallet.id]}
|
|
onCheckedChange={(checked) =>
|
|
setStorefront((prev) => ({
|
|
...prev,
|
|
enabledWallets: {
|
|
...prev.enabledWallets,
|
|
[wallet.id]: checked,
|
|
},
|
|
}))
|
|
}
|
|
disabled={wallet.disabled}
|
|
/>
|
|
</div>
|
|
</TooltipTrigger>
|
|
{wallet.disabled && (
|
|
<TooltipContent>
|
|
<p>Coming soon</p>
|
|
</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</div>
|
|
{storefront.enabledWallets[wallet.id] && !wallet.disabled && (
|
|
<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-sm h-8 bg-[#1C1C1C] border-zinc-800"
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<BroadcastDialog open={broadcastOpen} setOpen={setBroadcastOpen} />
|
|
|
|
</Dashboard>
|
|
);
|
|
}
|