diff --git a/config/payment.ts b/config/payment.ts new file mode 100644 index 0000000..633196a --- /dev/null +++ b/config/payment.ts @@ -0,0 +1,165 @@ +/** + * PAYMENT CONFIGURATION + * + * Edit this file to configure all payment methods. + * This is the only file you need to modify for production. + */ + +export const CONFIG = { + // ============================================ + // STORE SETTINGS + // ============================================ + store: { + name: "Secure Market", + currency: "USD", + currencySymbol: "$", + supportEmail: "support@example.com", + pgpKey: "0x1234ABCD5678EFGH", + }, + + // ============================================ + // PRODUCTS + // ============================================ + products: [ + { + id: "prod_001", + name: "Digital Product #001", + description: "Secure digital delivery. Encrypted files with lifetime access.", + priceFiat: 25.00, + priceCrypto: { + btc: "0.001", + eth: "0.015", + ltc: "0.3", + xmr: "0.15", + }, + image: "📦", + downloadUrl: "/downloads/product-001.zip", // After payment verification + }, + { + id: "prod_002", + name: "Premium Bundle", + description: "Complete package with all features and priority support.", + priceFiat: 99.00, + priceCrypto: { + btc: "0.004", + eth: "0.06", + ltc: "1.2", + xmr: "0.6", + }, + image: "💎", + downloadUrl: "/downloads/premium-bundle.zip", + }, + ], + + // ============================================ + // PAYPAL / CARD PAYMENTS + // Get your Client ID: https://developer.paypal.com/dashboard/applications + // ============================================ + paypal: { + enabled: true, + clientId: "YOUR_PAYPAL_CLIENT_ID", // Replace with your PayPal Client ID + currency: "USD", + // PayPal supports: paypal, card, credit, venmo, paylater, bancontact, giropay, ideal, mybank, p24, sepa, sofort + enabledFunding: ["paypal", "card", "venmo", "paylater"], + }, + + // ============================================ + // STRIPE (for Apple Pay / Google Pay) + // Get your keys: https://dashboard.stripe.com/apikeys + // ============================================ + stripe: { + enabled: true, + publishableKey: "YOUR_STRIPE_PUBLISHABLE_KEY", // pk_live_... or pk_test_... + // For server-side (edge function), add STRIPE_SECRET_KEY to your secrets + }, + + // ============================================ + // CRYPTOCURRENCY + // ============================================ + crypto: { + enabled: true, + // Replace with YOUR wallet addresses + wallets: { + btc: { + address: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + enabled: true, + }, + eth: { + address: "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", + enabled: true, + }, + ltc: { + address: "ltc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", + enabled: true, + }, + xmr: { + address: "888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H", + enabled: true, + }, + }, + // Auto-detection requires backend (Supabase edge function) + autoDetection: { + enabled: false, + pollIntervalMs: 15000, + supabaseUrl: "https://your-project.supabase.co", + }, + }, + + // ============================================ + // DIRECT PAYMENTS (Manual Verification) + // ============================================ + direct: { + enabled: true, + venmo: { + enabled: true, + username: "@YourVenmoUsername", + link: "https://venmo.com/u/YourVenmoUsername", + }, + cashapp: { + enabled: true, + cashtag: "$YourCashTag", + link: "https://cash.app/$YourCashTag", + }, + zelle: { + enabled: true, + email: "payments@example.com", + phone: "+1234567890", + }, + bankTransfer: { + enabled: true, + bankName: "Your Bank", + accountName: "Your Business Name", + accountNumber: "1234567890", + routingNumber: "021000021", + swift: "YOURSWIFT", + iban: "US00BANK0000001234567890", + }, + giftCards: { + enabled: true, + accepted: ["Amazon", "Steam", "iTunes", "Google Play", "Visa", "Mastercard"], + }, + }, + + // ============================================ + // CAPTCHA SETTINGS + // ============================================ + captcha: { + enabled: true, + maxAttempts: 3, + challengeTypes: ["math", "text", "reverse", "count"], + }, + + // ============================================ + // NOTIFICATIONS (Webhooks) + // Called when payment is submitted for verification + // ============================================ + webhooks: { + onPaymentSubmitted: "", // Optional: https://your-server.com/api/payment-submitted + onPaymentVerified: "", // Optional: https://your-server.com/api/payment-verified + }, +}; + +// Type exports for TypeScript +export type Product = typeof CONFIG.products[0]; +export type CryptoType = keyof typeof CONFIG.crypto.wallets; + diff --git a/src/PayPalPayment.tsx b/src/PayPalPayment.tsx new file mode 100644 index 0000000..a777d3c --- /dev/null +++ b/src/PayPalPayment.tsx @@ -0,0 +1,310 @@ +import { useEffect, useState, useRef } from "react"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/hooks/use-toast"; +import { Loader2, CreditCard, Check } from "lucide-react"; + +// PayPal SDK types +declare global { + interface Window { + paypal?: any; + } +} + +interface PayPalPaymentProps { + amount: string; + currency?: string; + item: string; + onPaymentComplete?: (orderId: string) => void; +} + +type PaymentMethod = 'paypal' | 'card' | 'ideal'; + +const PayPalPayment = ({ + amount, + currency = "EUR", + item, + onPaymentComplete +}: PayPalPaymentProps) => { + const [sdkReady, setSdkReady] = useState(false); + const [loading, setLoading] = useState(true); + const [selectedMethod, setSelectedMethod] = useState('paypal'); + const [paymentComplete, setPaymentComplete] = useState(false); + const [orderId, setOrderId] = useState(null); + const paypalButtonRef = useRef(null); + const cardButtonRef = useRef(null); + const idealButtonRef = useRef(null); + const { toast } = useToast(); + + // Replace with your PayPal Client ID + // Get one at: https://developer.paypal.com/dashboard/applications + const PAYPAL_CLIENT_ID = "YOUR_PAYPAL_CLIENT_ID"; + + useEffect(() => { + const loadPayPalSDK = () => { + if (window.paypal) { + setSdkReady(true); + setLoading(false); + return; + } + + const script = document.createElement("script"); + script.src = `https://www.paypal.com/sdk/js?client-id=${PAYPAL_CLIENT_ID}¤cy=${currency}&components=buttons,funding-eligibility&enable-funding=ideal,card`; + script.async = true; + + script.onload = () => { + setSdkReady(true); + setLoading(false); + }; + + script.onerror = () => { + console.error("PayPal SDK failed to load"); + setLoading(false); + toast({ + title: "Error", + description: "Failed to load payment system. Check your Client ID.", + variant: "destructive", + }); + }; + + document.body.appendChild(script); + }; + + loadPayPalSDK(); + + return () => { + // Cleanup if needed + }; + }, [currency, toast]); + + useEffect(() => { + if (!sdkReady || !window.paypal || paymentComplete) return; + + const createOrder = (_data: any, actions: any) => { + return actions.order.create({ + purchase_units: [{ + description: item, + amount: { + currency_code: currency, + value: amount, + }, + }], + }); + }; + + const onApprove = async (_data: any, actions: any) => { + try { + const order = await actions.order.capture(); + console.log("Payment successful:", order); + + setPaymentComplete(true); + setOrderId(order.id); + + toast({ + title: "Payment Successful!", + description: `Order ${order.id} has been confirmed.`, + }); + + onPaymentComplete?.(order.id); + } catch (error) { + console.error("Payment capture error:", error); + toast({ + title: "Payment Failed", + description: "There was an error processing your payment.", + variant: "destructive", + }); + } + }; + + const onError = (err: any) => { + console.error("PayPal error:", err); + toast({ + title: "Payment Error", + description: "Something went wrong. Please try again.", + variant: "destructive", + }); + }; + + // Render PayPal button + if (paypalButtonRef.current && selectedMethod === 'paypal') { + paypalButtonRef.current.innerHTML = ''; + window.paypal.Buttons({ + fundingSource: window.paypal.FUNDING.PAYPAL, + style: { + layout: 'vertical', + color: 'black', + shape: 'rect', + label: 'paypal', + height: 40, + }, + createOrder, + onApprove, + onError, + }).render(paypalButtonRef.current); + } + + // Render Card button + if (cardButtonRef.current && selectedMethod === 'card') { + cardButtonRef.current.innerHTML = ''; + window.paypal.Buttons({ + fundingSource: window.paypal.FUNDING.CARD, + style: { + layout: 'vertical', + color: 'black', + shape: 'rect', + height: 40, + }, + createOrder, + onApprove, + onError, + }).render(cardButtonRef.current); + } + + // Render iDEAL button (available in Netherlands) + if (idealButtonRef.current && selectedMethod === 'ideal') { + idealButtonRef.current.innerHTML = ''; + if (window.paypal.FUNDING.IDEAL) { + window.paypal.Buttons({ + fundingSource: window.paypal.FUNDING.IDEAL, + style: { + layout: 'vertical', + shape: 'rect', + height: 40, + }, + createOrder, + onApprove, + onError, + }).render(idealButtonRef.current); + } else { + idealButtonRef.current.innerHTML = '

iDEAL not available in your region

'; + } + } + }, [sdkReady, selectedMethod, amount, currency, item, toast, onPaymentComplete, paymentComplete]); + + if (paymentComplete && orderId) { + return ( +
+
+ + [ PAYMENT CONFIRMED ] + +
+ +
+
+ +
+

+ Payment Successful! +

+

+ {amount} {currency} paid +

+
+ +
+

Order ID:

+ + {orderId} + +
+ +

+ Your purchase is complete. Check your email for confirmation. +

+
+ ); + } + + return ( +
+
+ + [ CARD / PAYPAL PAYMENT ] + +
+ +
+

{item}

+

+ Pay securely with PayPal, card, or iDEAL +

+
+ +
+ + + +
+ +
+
+ Amount: + + {amount} {currency} + +
+
+ + {loading ? ( +
+ +
+ ) : !sdkReady ? ( +
+

+ PayPal SDK not loaded +

+

+ Replace YOUR_PAYPAL_CLIENT_ID with your actual PayPal Client ID +

+
+ ) : ( +
+
+
+
+
+ )} + +

+ {selectedMethod === 'paypal' && "Pay with your PayPal balance or linked cards."} + {selectedMethod === 'card' && "Pay securely with Visa, Mastercard, or Amex."} + {selectedMethod === 'ideal' && "Pay directly from your Dutch bank account."} +

+
+ ); +}; + +export default PayPalPayment; + diff --git a/src/components/DesktopOnly.tsx b/src/components/DesktopOnly.tsx new file mode 100644 index 0000000..bdbce1b --- /dev/null +++ b/src/components/DesktopOnly.tsx @@ -0,0 +1,38 @@ +import { motion } from 'framer-motion'; +import { Monitor } from 'lucide-react'; + +interface DesktopOnlyProps { + children: React.ReactNode; +} + +const DesktopOnly = ({ children }: DesktopOnlyProps) => { + const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; + + if (isMobile) { + return ( +
+ + +

+ Desktop Only +

+

+ This game requires a keyboard and is designed for desktop play. +

+

+ Please visit this page on a desktop or laptop computer. +

+
+
+ ); + } + + return <>{children}; +}; + +export default DesktopOnly; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..05918e4 --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,235 @@ +import React, { Component, ReactNode } from 'react'; + +interface ErrorBoundaryState { + hasError: boolean; + error?: Error; + errorInfo?: React.ErrorInfo; + retryCount: number; +} + +interface ErrorBoundaryProps { + children: ReactNode; + fallback?: ReactNode; + onError?: (error: Error, errorInfo: React.ErrorInfo) => void; +} + +export class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + retryCount: 0 + }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { + hasError: true, + error + }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('🔥 ErrorBoundary caught an error:', error); + console.error('📍 Error details:', errorInfo); + + // Enhanced logging for debugging + console.group('🔍 Error Diagnostics'); + console.error('Error:', error); + console.error('Component Stack:', errorInfo.componentStack); + + // Log performance/memory context + if ((performance as any).memory) { + const memory = (performance as any).memory; + console.log('Memory Usage:', { + used: `${Math.round(memory.usedJSHeapSize / (1024 * 1024))}MB`, + total: `${Math.round(memory.totalJSHeapSize / (1024 * 1024))}MB`, + limit: `${Math.round(memory.jsHeapSizeLimit / (1024 * 1024))}MB` + }); + } + + console.log('Navigator Info:', { + userAgent: navigator.userAgent, + platform: navigator.platform, + language: navigator.language, + onLine: navigator.onLine + }); + console.groupEnd(); + + // Store error info for display + this.setState({ + error, + errorInfo + }); + + // Call custom error handler if provided + if (this.props.onError) { + this.props.onError(error, errorInfo); + } + } + + handleRetry = () => { + console.log(`🔄 Retry attempt ${this.state.retryCount + 1}`); + + // Clear any timeouts and intervals (safer approach) + const maxTimeouts = 1000; + for (let i = 1; i <= maxTimeouts; i++) { + clearTimeout(i); + clearInterval(i); + } + + // Reset error state and increment retry count + this.setState(prevState => ({ + hasError: false, + error: undefined, + errorInfo: undefined, + retryCount: prevState.retryCount + 1 + })); + }; + + handleReload = () => { + console.log('🔄 Reloading page due to error'); + window.location.reload(); + }; + + render() { + if (this.state.hasError) { + // Custom fallback UI if provided + if (this.props.fallback) { + return this.props.fallback; + } + + // Default error UI with detailed diagnostics + return ( +
+
+ {/* Header */} +
+

⚠️ SYSTEM ERROR

+

The oscilloscope encountered a critical error

+
+ + {/* Error Details */} +
+

🔍 Error Analysis

+ +
+
+ Error Message: +
+ {this.state.error?.message || 'Unknown error'} +
+
+ + {this.state.error?.stack && ( +
+ Stack Trace: +
+                      {this.state.error.stack}
+                    
+
+ )} + + {this.state.errorInfo?.componentStack && ( +
+ Component Stack: +
+                      {this.state.errorInfo.componentStack}
+                    
+
+ )} + +
+ Retry Count: {this.state.retryCount} +
+ + {this.state.retryCount > 2 && ( +
+

⚠️ Multiple Retry Attempts

+

+ This error may be persistent. Consider refreshing the page or using a smaller audio file. +

+
+ )} +
+
+ + {/* System Info */} +
+

💻 System Diagnostics

+ +
+
+ Browser: +
{navigator.userAgent.split(' ')[0]}
+
+ +
+ Platform: +
{(navigator as any).userAgentData?.platform || navigator.platform}
+
+ + {(performance as any).memory && ( +
+ Memory Usage: +
+ {Math.round((performance as any).memory.usedJSHeapSize / (1024 * 1024))}MB / + {Math.round((performance as any).memory.jsHeapSizeLimit / (1024 * 1024))}MB +
+
+ )} + +
+ Online Status: +
+ {navigator.onLine ? 'Online' : 'Offline'} +
+
+
+
+ + {/* Action Buttons */} +
+ + + + + +
+ + {/* Troubleshooting Tips */} +
+

💡 Troubleshooting Tips

+
    +
  • • Try using a smaller audio file (<500MB for testing)
  • +
  • • Convert large FLAC files to MP3/OGG format
  • +
  • • Close other browser tabs to free up memory
  • +
  • • Try Chrome browser with --max-old-space-size=8192 flag
  • +
  • • Check browser console for additional error details
  • +
  • • Restart browser if memory usage is high
  • +
+
+
+
+ ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/src/components/admin/AnalyticsTab.tsx b/src/components/admin/AnalyticsTab.tsx new file mode 100644 index 0000000..2effd8d --- /dev/null +++ b/src/components/admin/AnalyticsTab.tsx @@ -0,0 +1,367 @@ +import { useState, useEffect, useMemo } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + PieChart, + Pie, + Cell, + LineChart, + Line, + Legend, +} from "recharts"; +import { + TrendingUp, + TrendingDown, + DollarSign, + ShoppingCart, + Users, + CreditCard, + Calendar, + RefreshCw, +} from "lucide-react"; +import { CONFIG } from "@/config/payments"; + +interface Order { + id: string; + amount: number; + status: string; + payment_method: string; + created_at: string; + customer_email: string; +} + +const COLORS = ["hsl(142, 76%, 36%)", "hsl(38, 92%, 50%)", "hsl(0, 84%, 60%)", "hsl(240, 4%, 46%)"]; + +const AnalyticsTab = () => { + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [timeRange, setTimeRange] = useState("30"); + + useEffect(() => { + fetchOrders(); + }, [timeRange]); + + const fetchOrders = async () => { + setLoading(true); + try { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - parseInt(timeRange)); + + const { data, error } = await supabase + .from("orders") + .select("id, amount, status, payment_method, created_at, customer_email") + .gte("created_at", startDate.toISOString()) + .order("created_at", { ascending: true }); + + if (error) throw error; + setOrders(data || []); + } catch (error) { + console.error("Failed to fetch analytics data:", error); + } finally { + setLoading(false); + } + }; + + const stats = useMemo(() => { + const verifiedOrders = orders.filter(o => o.status === "verified" || o.status === "completed"); + const totalRevenue = verifiedOrders.reduce((sum, o) => sum + Number(o.amount), 0); + const avgOrderValue = verifiedOrders.length > 0 ? totalRevenue / verifiedOrders.length : 0; + const uniqueCustomers = new Set(orders.map(o => o.customer_email)).size; + const conversionRate = orders.length > 0 ? (verifiedOrders.length / orders.length) * 100 : 0; + + return { + totalRevenue, + totalOrders: orders.length, + verifiedOrders: verifiedOrders.length, + avgOrderValue, + uniqueCustomers, + conversionRate, + }; + }, [orders]); + + const revenueByDay = useMemo(() => { + const grouped: Record = {}; + + orders.forEach(order => { + if (order.status === "verified" || order.status === "completed") { + const date = new Date(order.created_at).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + if (!grouped[date]) { + grouped[date] = { date, revenue: 0, orders: 0 }; + } + grouped[date].revenue += Number(order.amount); + grouped[date].orders += 1; + } + }); + + return Object.values(grouped); + }, [orders]); + + const paymentMethodData = useMemo(() => { + const grouped: Record = {}; + + orders.forEach(order => { + const method = order.payment_method || "unknown"; + grouped[method] = (grouped[method] || 0) + 1; + }); + + return Object.entries(grouped).map(([name, value]) => ({ + name: name.charAt(0).toUpperCase() + name.slice(1), + value, + })); + }, [orders]); + + const statusData = useMemo(() => { + const grouped: Record = {}; + + orders.forEach(order => { + grouped[order.status] = (grouped[order.status] || 0) + 1; + }); + + return Object.entries(grouped).map(([name, value]) => ({ + name: name.charAt(0).toUpperCase() + name.slice(1), + value, + })); + }, [orders]); + + if (loading) { + return ( +
+ + Loading analytics... +
+ ); + } + + return ( +
+ {/* Time Range Selector */} +
+

+ + Analytics Overview +

+ +
+ + {/* Key Metrics */} +
+ + + + Total Revenue + + + + +
+ {CONFIG.store.currencySymbol}{stats.totalRevenue.toFixed(2)} +
+

+ From {stats.verifiedOrders} verified orders +

+
+
+ + + + + Total Orders + + + + +
{stats.totalOrders}
+

+ {stats.verifiedOrders} completed +

+
+
+ + + + + Avg. Order Value + + + + +
+ {CONFIG.store.currencySymbol}{stats.avgOrderValue.toFixed(2)} +
+

+ Per verified order +

+
+
+ + + + + Conversion Rate + + + + +
{stats.conversionRate.toFixed(1)}%
+

+ {stats.uniqueCustomers} unique customers +

+
+
+
+ + {/* Charts */} +
+ {/* Revenue Over Time */} + + + Revenue Over Time + + + {revenueByDay.length === 0 ? ( +
+ No revenue data for this period +
+ ) : ( + + + + + `$${value}`} + /> + [`$${value.toFixed(2)}`, "Revenue"]} + /> + + + + )} +
+
+ + {/* Orders by Status */} + + + Orders by Status + + + {statusData.length === 0 ? ( +
+ No order data for this period +
+ ) : ( + + + `${name}: ${value}`} + > + {statusData.map((_, index) => ( + + ))} + + + + + )} +
+
+ + {/* Payment Methods */} + + + Orders by Payment Method + + + {paymentMethodData.length === 0 ? ( +
+ No payment data for this period +
+ ) : ( + + + + + + [value, "Orders"]} + /> + + + + )} +
+
+
+
+ ); +}; + +export default AnalyticsTab; diff --git a/src/components/admin/DiscountCodesTab.tsx b/src/components/admin/DiscountCodesTab.tsx new file mode 100644 index 0000000..9969e89 --- /dev/null +++ b/src/components/admin/DiscountCodesTab.tsx @@ -0,0 +1,456 @@ +import { useState, useEffect } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useToast } from "@/hooks/use-toast"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Plus, Pencil, Trash2, Ticket, RefreshCw, Copy } from "lucide-react"; + +interface DiscountCode { + id: string; + code: string; + description: string | null; + discount_type: string; + discount_value: number; + min_order_amount: number; + max_uses: number | null; + current_uses: number; + valid_from: string; + valid_until: string | null; + is_active: boolean; + created_at: string; +} + +const DiscountCodesTab = () => { + const { toast } = useToast(); + const [codes, setCodes] = useState([]); + const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [editingCode, setEditingCode] = useState(null); + const [saving, setSaving] = useState(false); + + const [formData, setFormData] = useState({ + code: "", + description: "", + discount_type: "percentage", + discount_value: "", + min_order_amount: "0", + max_uses: "", + valid_until: "", + is_active: true, + }); + + useEffect(() => { + fetchCodes(); + }, []); + + const fetchCodes = async () => { + setLoading(true); + try { + const { data, error } = await supabase + .from("discount_codes") + .select("*") + .order("created_at", { ascending: false }); + + if (error) throw error; + setCodes(data || []); + } catch (error) { + console.error("Failed to fetch discount codes:", error); + toast({ + title: "Error", + description: "Failed to load discount codes.", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + const resetForm = () => { + setFormData({ + code: "", + description: "", + discount_type: "percentage", + discount_value: "", + min_order_amount: "0", + max_uses: "", + valid_until: "", + is_active: true, + }); + setEditingCode(null); + }; + + const generateCode = () => { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let code = ""; + for (let i = 0; i < 8; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + setFormData({ ...formData, code }); + }; + + const openEditDialog = (code: DiscountCode) => { + setEditingCode(code); + setFormData({ + code: code.code, + description: code.description || "", + discount_type: code.discount_type, + discount_value: code.discount_value.toString(), + min_order_amount: code.min_order_amount.toString(), + max_uses: code.max_uses?.toString() || "", + valid_until: code.valid_until ? code.valid_until.split("T")[0] : "", + is_active: code.is_active, + }); + setDialogOpen(true); + }; + + const handleSubmit = async () => { + if (!formData.code || !formData.discount_value) { + toast({ + title: "Validation Error", + description: "Code and discount value are required.", + variant: "destructive", + }); + return; + } + + setSaving(true); + try { + const codeData = { + code: formData.code.toUpperCase().trim(), + description: formData.description.trim() || null, + discount_type: formData.discount_type, + discount_value: parseFloat(formData.discount_value), + min_order_amount: parseFloat(formData.min_order_amount) || 0, + max_uses: formData.max_uses ? parseInt(formData.max_uses) : null, + valid_until: formData.valid_until ? new Date(formData.valid_until).toISOString() : null, + is_active: formData.is_active, + }; + + if (editingCode) { + const { error } = await supabase + .from("discount_codes") + .update(codeData) + .eq("id", editingCode.id); + + if (error) throw error; + toast({ title: "Success", description: "Discount code updated." }); + } else { + const { error } = await supabase.from("discount_codes").insert(codeData); + if (error) throw error; + toast({ title: "Success", description: "Discount code created." }); + } + + setDialogOpen(false); + resetForm(); + fetchCodes(); + } catch (error: any) { + console.error("Failed to save discount code:", error); + toast({ + title: "Error", + description: error.message || "Failed to save discount code.", + variant: "destructive", + }); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm("Are you sure you want to delete this discount code?")) return; + + try { + const { error } = await supabase.from("discount_codes").delete().eq("id", id); + if (error) throw error; + toast({ title: "Success", description: "Discount code deleted." }); + fetchCodes(); + } catch (error) { + console.error("Failed to delete discount code:", error); + toast({ + title: "Error", + description: "Failed to delete discount code.", + variant: "destructive", + }); + } + }; + + const copyCode = (code: string) => { + navigator.clipboard.writeText(code); + toast({ title: "Copied!", description: `Code "${code}" copied to clipboard.` }); + }; + + const toggleActive = async (code: DiscountCode) => { + try { + const { error } = await supabase + .from("discount_codes") + .update({ is_active: !code.is_active }) + .eq("id", code.id); + + if (error) throw error; + fetchCodes(); + } catch (error) { + console.error("Failed to toggle code:", error); + } + }; + + const isExpired = (validUntil: string | null) => { + if (!validUntil) return false; + return new Date(validUntil) < new Date(); + }; + + return ( + + + + + Discount Codes + +
+ + { setDialogOpen(open); if (!open) resetForm(); }}> + + + + + + {editingCode ? "Edit Discount Code" : "Create Discount Code"} + + {editingCode ? "Update discount code details." : "Create a new discount code for customers."} + + +
+
+ +
+ setFormData({ ...formData, code: e.target.value.toUpperCase() })} + placeholder="SAVE20" + className="font-mono" + /> + +
+
+
+ + setFormData({ ...formData, description: e.target.value })} + placeholder="Summer sale discount" + /> +
+
+
+ + +
+
+ + setFormData({ ...formData, discount_value: e.target.value })} + placeholder={formData.discount_type === "percentage" ? "20" : "10.00"} + /> +
+
+
+
+ + setFormData({ ...formData, min_order_amount: e.target.value })} + placeholder="0" + /> +
+
+ + setFormData({ ...formData, max_uses: e.target.value })} + placeholder="Unlimited" + /> +
+
+
+
+ + setFormData({ ...formData, valid_until: e.target.value })} + /> +
+
+ setFormData({ ...formData, is_active: checked })} + /> + +
+
+
+ + + + +
+
+
+
+ + {loading ? ( +
+ + Loading... +
+ ) : codes.length === 0 ? ( +
+ +

No discount codes yet

+
+ ) : ( + + + + Code + Discount + Uses + Expires + Status + Actions + + + + {codes.map((code) => ( + + +
+ {code.code} + +
+
+ + {code.discount_type === "percentage" + ? `${code.discount_value}%` + : `$${Number(code.discount_value).toFixed(2)}`} + + + {code.current_uses}{code.max_uses ? `/${code.max_uses}` : ""} + + + {code.valid_until + ? new Date(code.valid_until).toLocaleDateString() + : "Never"} + + + {isExpired(code.valid_until) ? ( + Expired + ) : ( + toggleActive(code)} + > + {code.is_active ? "Active" : "Inactive"} + + )} + + +
+ + +
+
+
+ ))} +
+
+ )} +
+
+ ); +}; + +export default DiscountCodesTab; diff --git a/src/components/admin/ProductsTab.tsx b/src/components/admin/ProductsTab.tsx new file mode 100644 index 0000000..747a8e6 --- /dev/null +++ b/src/components/admin/ProductsTab.tsx @@ -0,0 +1,378 @@ +import { useState, useEffect } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useToast } from "@/hooks/use-toast"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Plus, Pencil, Trash2, Package, RefreshCw } from "lucide-react"; + +interface Product { + id: string; + sku: string; + name: string; + description: string | null; + price: number; + currency: string; + download_url: string | null; + is_active: boolean; + stock_quantity: number | null; + created_at: string; +} + +const ProductsTab = () => { + const { toast } = useToast(); + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [editingProduct, setEditingProduct] = useState(null); + const [saving, setSaving] = useState(false); + + const [formData, setFormData] = useState({ + sku: "", + name: "", + description: "", + price: "", + currency: "USD", + download_url: "", + is_active: true, + stock_quantity: "", + }); + + useEffect(() => { + fetchProducts(); + }, []); + + const fetchProducts = async () => { + setLoading(true); + try { + const { data, error } = await supabase + .from("products") + .select("*") + .order("created_at", { ascending: false }); + + if (error) throw error; + setProducts(data || []); + } catch (error) { + console.error("Failed to fetch products:", error); + toast({ + title: "Error", + description: "Failed to load products.", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + const resetForm = () => { + setFormData({ + sku: "", + name: "", + description: "", + price: "", + currency: "USD", + download_url: "", + is_active: true, + stock_quantity: "", + }); + setEditingProduct(null); + }; + + const openEditDialog = (product: Product) => { + setEditingProduct(product); + setFormData({ + sku: product.sku, + name: product.name, + description: product.description || "", + price: product.price.toString(), + currency: product.currency, + download_url: product.download_url || "", + is_active: product.is_active, + stock_quantity: product.stock_quantity?.toString() || "", + }); + setDialogOpen(true); + }; + + const handleSubmit = async () => { + if (!formData.sku || !formData.name || !formData.price) { + toast({ + title: "Validation Error", + description: "SKU, name, and price are required.", + variant: "destructive", + }); + return; + } + + setSaving(true); + try { + const productData = { + sku: formData.sku.trim(), + name: formData.name.trim(), + description: formData.description.trim() || null, + price: parseFloat(formData.price), + currency: formData.currency, + download_url: formData.download_url.trim() || null, + is_active: formData.is_active, + stock_quantity: formData.stock_quantity ? parseInt(formData.stock_quantity) : null, + }; + + if (editingProduct) { + const { error } = await supabase + .from("products") + .update(productData) + .eq("id", editingProduct.id); + + if (error) throw error; + toast({ title: "Success", description: "Product updated." }); + } else { + const { error } = await supabase.from("products").insert(productData); + if (error) throw error; + toast({ title: "Success", description: "Product created." }); + } + + setDialogOpen(false); + resetForm(); + fetchProducts(); + } catch (error: any) { + console.error("Failed to save product:", error); + toast({ + title: "Error", + description: error.message || "Failed to save product.", + variant: "destructive", + }); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm("Are you sure you want to delete this product?")) return; + + try { + const { error } = await supabase.from("products").delete().eq("id", id); + if (error) throw error; + toast({ title: "Success", description: "Product deleted." }); + fetchProducts(); + } catch (error) { + console.error("Failed to delete product:", error); + toast({ + title: "Error", + description: "Failed to delete product.", + variant: "destructive", + }); + } + }; + + const toggleActive = async (product: Product) => { + try { + const { error } = await supabase + .from("products") + .update({ is_active: !product.is_active }) + .eq("id", product.id); + + if (error) throw error; + fetchProducts(); + } catch (error) { + console.error("Failed to toggle product:", error); + } + }; + + return ( + + + + + Products + +
+ + { setDialogOpen(open); if (!open) resetForm(); }}> + + + + + + {editingProduct ? "Edit Product" : "Add Product"} + + {editingProduct ? "Update product details." : "Create a new product."} + + +
+
+
+ + setFormData({ ...formData, sku: e.target.value })} + placeholder="prod_001" + /> +
+
+ + setFormData({ ...formData, price: e.target.value })} + placeholder="49.99" + /> +
+
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Product Name" + /> +
+
+ +