175 lines
6.3 KiB
TypeScript
175 lines
6.3 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { Card } from '../ui/card';
|
|
import { Button } from '../ui/button';
|
|
import { SearchInput } from '../ui/input';
|
|
import { Purchase } from '../../types';
|
|
import { Download, FileText, RefreshCcw, Loader2 } from 'lucide-react';
|
|
import { RefundRequestModal } from '../commerce/modals/RefundRequestModal';
|
|
import { useToast } from '../../components/feedback/ToastProvider';
|
|
import { commerceService } from '../../services/commerceService';
|
|
import { logger } from '@/utils/logger';
|
|
|
|
export const PurchasesView: React.FC = () => {
|
|
const { addToast } = useToast();
|
|
const [search, setSearch] = useState('');
|
|
const [refundOrderId, setRefundOrderId] = useState<string | null>(null);
|
|
const [activeDownloadId, setActiveDownloadId] = useState<string | null>(null);
|
|
const [purchases, setPurchases] = useState<Purchase[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const loadPurchases = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await commerceService.getPurchases();
|
|
setPurchases(data);
|
|
} catch (e) {
|
|
logger.error('Error loading purchases', {
|
|
error: e instanceof Error ? e.message : String(e),
|
|
stack: e instanceof Error ? e.stack : undefined,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
loadPurchases();
|
|
}, []);
|
|
|
|
const filteredPurchases = purchases.filter((p) =>
|
|
p.product.title.toLowerCase().includes(search.toLowerCase()),
|
|
);
|
|
|
|
const handleDownload = (format: string) => {
|
|
addToast(`Downloading ${format}...`, 'success');
|
|
setActiveDownloadId(null);
|
|
};
|
|
|
|
return (
|
|
<div className="animate-fadeIn max-w-5xl mx-auto pb-20">
|
|
<div className="flex flex-col md:flex-row justify-between items-end border-b border-kodo-steel/50 pb-6 gap-4 mb-8">
|
|
<div>
|
|
<h1 className="text-3xl font-display font-bold text-white mb-2">
|
|
MY PURCHASES
|
|
</h1>
|
|
<p className="text-kodo-content-dim font-mono text-sm">
|
|
Access your sounds, licenses, and receipts.
|
|
</p>
|
|
</div>
|
|
<div className="w-full md:w-64">
|
|
<SearchInput
|
|
placeholder="Search library..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
{loading && (
|
|
<div className="flex justify-center py-24">
|
|
<Loader2 className="w-8 h-8 text-kodo-steel animate-spin" />
|
|
</div>
|
|
)}
|
|
|
|
{!loading && filteredPurchases.length === 0 ? (
|
|
<div className="text-center py-24 text-kodo-content-dim">
|
|
<p>No purchases found.</p>
|
|
</div>
|
|
) : (
|
|
filteredPurchases.map((purchase) => (
|
|
<Card
|
|
key={purchase.id}
|
|
variant="default"
|
|
className="flex flex-col md:flex-row items-center gap-8 p-4"
|
|
>
|
|
{/* Image */}
|
|
<div className="w-20 h-20 rounded-lg overflow-hidden flex-shrink-0 bg-kodo-graphite">
|
|
<img
|
|
src={purchase.product.coverUrl}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
|
|
{/* Details */}
|
|
<div className="flex-1 w-full text-center md:text-left">
|
|
<h3 className="font-bold text-white text-lg mb-1">
|
|
{purchase.product.title}
|
|
</h3>
|
|
<div className="flex flex-wrap items-center justify-center md:justify-start gap-4 text-xs text-kodo-content-dim">
|
|
<span className="bg-kodo-ink px-2 py-1 rounded border border-kodo-steel text-kodo-steel">
|
|
{purchase.product.type}
|
|
</span>
|
|
<span>Order #{purchase.orderId}</span>
|
|
<span>•</span>
|
|
<span>{purchase.date}</span>
|
|
<span>•</span>
|
|
<span className="text-white">${purchase.price}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div className="flex flex-col sm:flex-row gap-4 w-full md:w-auto">
|
|
<div className="relative">
|
|
<Button
|
|
variant="primary"
|
|
size="sm"
|
|
icon={<Download className="w-4 h-4" />}
|
|
onClick={() =>
|
|
setActiveDownloadId(
|
|
activeDownloadId === purchase.id ? null : purchase.id,
|
|
)
|
|
}
|
|
>
|
|
Download
|
|
</Button>
|
|
{activeDownloadId === purchase.id && (
|
|
<div className="absolute top-full right-0 mt-2 w-40 bg-kodo-graphite border border-kodo-steel rounded-lg shadow-xl z-20 overflow-hidden animate-fadeIn">
|
|
{['WAV (24-bit)', 'MP3 (320kbps)', 'Stems (ZIP)'].map(
|
|
(fmt) => (
|
|
<button
|
|
key={fmt}
|
|
className="w-full text-left px-4 py-2 text-xs text-white hover:bg-white/10"
|
|
onClick={() => handleDownload(fmt)}
|
|
>
|
|
{fmt}
|
|
</button>
|
|
),
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="border border-kodo-steel"
|
|
icon={<FileText className="w-4 h-4" />}
|
|
onClick={() => addToast('License document opened')}
|
|
>
|
|
License
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="text-kodo-content-dim hover:text-white"
|
|
title="Request Refund"
|
|
onClick={() => setRefundOrderId(purchase.orderId)}
|
|
>
|
|
<RefreshCcw className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</Card>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{refundOrderId && (
|
|
<RefundRequestModal
|
|
orderId={refundOrderId}
|
|
onClose={() => setRefundOrderId(null)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|