veza/apps/web/src/components/views/PurchasesView.tsx

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>
);
};