2025-07-29 15:19:48 -04:00
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { useRouter } from 'next/router';
|
2025-07-23 22:26:54 -04:00
|
|
|
import Layout from '../components/Layout';
|
2025-07-29 15:19:48 -04:00
|
|
|
import PermissionIndicator from '../components/PermissionIndicator';
|
|
|
|
|
import { useAuth } from '../lib/use-auth';
|
|
|
|
|
import Link from 'next/link';
|
2026-05-29 10:57:46 -04:00
|
|
|
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';
|
2025-07-23 22:26:54 -04:00
|
|
|
|
|
|
|
|
export default function Dashboard() {
|
2025-07-29 15:19:48 -04:00
|
|
|
const router = useRouter();
|
|
|
|
|
const { user, loading: authLoading } = useAuth();
|
|
|
|
|
|
|
|
|
|
const [collections, setCollections] = useState([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
// Redirect to login if not authenticated
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!authLoading && !user) {
|
|
|
|
|
router.push('/login');
|
|
|
|
|
}
|
|
|
|
|
}, [authLoading, user, router]);
|
2025-07-23 22:26:54 -04:00
|
|
|
|
2025-07-29 15:19:48 -04:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (user) {
|
|
|
|
|
fetchCollections();
|
|
|
|
|
}
|
|
|
|
|
}, [user]);
|
|
|
|
|
|
|
|
|
|
const fetchCollections = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const token = localStorage.getItem('auth_token');
|
|
|
|
|
const headers = {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (token) {
|
|
|
|
|
headers.Authorization = `Bearer ${token}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const response = await fetch('/api/collections', { headers });
|
|
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
// Fetch thumbnails for each collection
|
|
|
|
|
const collectionsWithThumbnails = await Promise.all(
|
|
|
|
|
data.map(async (collection) => {
|
|
|
|
|
try {
|
|
|
|
|
const identifier = collection.slug || collection.id;
|
|
|
|
|
const thumbnailResponse = await fetch(`/api/collections/${identifier}/thumbnails`, { headers });
|
|
|
|
|
if (thumbnailResponse.ok) {
|
|
|
|
|
const thumbnailData = await thumbnailResponse.json();
|
|
|
|
|
return { ...collection, thumbnails: thumbnailData.thumbnails };
|
|
|
|
|
}
|
|
|
|
|
return { ...collection, thumbnails: [] };
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Error fetching thumbnails for collection ${collection.id}:`, error);
|
|
|
|
|
return { ...collection, thumbnails: [] };
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
setCollections(collectionsWithThumbnails);
|
|
|
|
|
} else {
|
|
|
|
|
console.error('Failed to fetch collections');
|
|
|
|
|
setCollections([]);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error fetching collections:', error);
|
|
|
|
|
setCollections([]);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-07-23 22:26:54 -04:00
|
|
|
|
|
|
|
|
return (
|
2025-07-29 15:19:48 -04:00
|
|
|
<Layout user={user}>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="p-4 sm:p-6" style={{ backgroundColor: 'var(--bg-secondary)', borderBottom: '1px solid var(--border)' }}>
|
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
2025-07-23 22:26:54 -04:00
|
|
|
<div>
|
2025-07-29 15:19:48 -04:00
|
|
|
<h1 className="text-2xl sm:text-3xl font-bold mb-2" style={{ color: 'var(--text-primary)' }}>
|
2026-05-29 10:57:46 -04:00
|
|
|
{VOCAB.MY_COLLECTION}
|
2025-07-23 22:26:54 -04:00
|
|
|
</h1>
|
2025-07-29 15:19:48 -04:00
|
|
|
<p className="text-base sm:text-lg" style={{ color: 'var(--text-secondary)' }}>
|
2026-05-29 10:57:46 -04:00
|
|
|
Overview of your lists and owned cards
|
2025-07-23 22:26:54 -04:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2025-07-29 15:19:48 -04:00
|
|
|
<div className="flex space-x-2 sm:space-x-4">
|
|
|
|
|
<Link href="/collections">
|
|
|
|
|
<button className="px-4 py-2 text-sm font-medium rounded-xl transition-all duration-200 hover:opacity-90" style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}>
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
|
|
|
</svg>
|
2026-05-29 10:53:40 -04:00
|
|
|
<span>Create List</span>
|
2025-07-29 15:19:48 -04:00
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</Link>
|
2025-07-23 22:26:54 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-07-29 15:19:48 -04:00
|
|
|
</div>
|
2025-07-23 22:26:54 -04:00
|
|
|
|
2025-07-29 15:19:48 -04:00
|
|
|
{/* Content */}
|
|
|
|
|
<div className="p-4 sm:p-6">
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="flex items-center justify-center py-20">
|
|
|
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2" style={{ borderColor: 'var(--accent-ember)' }}></div>
|
2025-07-23 22:26:54 -04:00
|
|
|
</div>
|
2025-07-29 15:19:48 -04:00
|
|
|
) : (
|
|
|
|
|
<div className="max-w-7xl mx-auto">
|
|
|
|
|
{/* Stats Cards */}
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
|
|
|
|
<div className="rounded-2xl p-6" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<div className="p-3 rounded-2xl mr-4" style={{ backgroundColor: 'var(--accent-ember)' }}>
|
|
|
|
|
<svg className="h-8 w-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>{collections.length}</p>
|
2026-05-29 10:53:40 -04:00
|
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>Lists</p>
|
2025-07-29 15:19:48 -04:00
|
|
|
</div>
|
2025-07-23 22:26:54 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-07-29 15:19:48 -04:00
|
|
|
<div className="rounded-2xl p-6" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<div className="p-3 rounded-2xl mr-4" style={{ backgroundColor: 'var(--accent-gold)' }}>
|
|
|
|
|
<svg className="h-8 w-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
{collections.reduce((total, col) => total + (col.cardCount || 0), 0)}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>Total Cards</p>
|
|
|
|
|
</div>
|
2025-07-23 22:26:54 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-07-29 15:19:48 -04:00
|
|
|
<div className="rounded-2xl p-6" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
<div className="flex items-center">
|
|
|
|
|
<div className="p-3 rounded-2xl mr-4" style={{ backgroundColor: 'var(--accent-flame)' }}>
|
|
|
|
|
<svg className="h-8 w-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-2xl font-bold" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
${collections.reduce((total, col) => total + (col.value || 0), 0).toLocaleString()}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>Total Value</p>
|
|
|
|
|
</div>
|
2025-07-23 22:26:54 -04:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-07-29 15:19:48 -04:00
|
|
|
{/* Collections Grid */}
|
|
|
|
|
<div className="mb-6">
|
2026-05-29 10:53:40 -04:00
|
|
|
<h2 className="text-xl font-bold mb-4" style={{ color: 'var(--text-primary)' }}>Recent Lists</h2>
|
2025-07-29 15:19:48 -04:00
|
|
|
{collections.length === 0 ? (
|
|
|
|
|
<div className="text-center py-12">
|
|
|
|
|
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl flex items-center justify-center" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
2026-05-29 10:53:40 -04:00
|
|
|
<h3 className="text-lg font-semibold mb-2" style={{ color: 'var(--text-primary)' }}>No Lists Yet</h3>
|
2025-07-29 15:19:48 -04:00
|
|
|
<p className="mb-4" style={{ color: 'var(--text-secondary)' }}>
|
2026-05-29 10:57:46 -04:00
|
|
|
Create your first list to start organizing your cards
|
2025-07-29 15:19:48 -04:00
|
|
|
</p>
|
|
|
|
|
<Link href="/collections">
|
|
|
|
|
<button className="px-6 py-3 font-medium rounded-xl transition-all duration-200 hover:opacity-90" style={{ backgroundColor: 'var(--accent-ember)', color: 'white' }}>
|
2026-05-29 10:53:40 -04:00
|
|
|
Create Your First List
|
2025-07-29 15:19:48 -04:00
|
|
|
</button>
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
|
|
|
{collections.slice(0, 6).map((collection) => (
|
|
|
|
|
<Link key={collection.id} href={`/collection/${collection.slug || collection.id}`}>
|
|
|
|
|
<div className="rounded-2xl p-6 transition-all duration-200 hover:scale-105 cursor-pointer" style={{ backgroundColor: 'var(--bg-secondary)' }}>
|
|
|
|
|
<div className="flex items-center mb-4">
|
|
|
|
|
<div className="w-12 h-12 rounded-xl mr-4 flex items-center justify-center gradient-bg-ember">
|
|
|
|
|
<span className="text-white font-bold">
|
|
|
|
|
{collection.name?.charAt(0)?.toUpperCase() || 'C'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<h3 className="font-semibold truncate" style={{ color: 'var(--text-primary)' }}>
|
2026-05-29 10:57:46 -04:00
|
|
|
{collectionDisplayName(collection)}
|
2025-07-29 15:19:48 -04:00
|
|
|
</h3>
|
|
|
|
|
<p className="text-sm truncate" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
{collection.cardCount || 0} cards
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{collection.description && (
|
|
|
|
|
<p className="text-sm mb-3 line-clamp-2" style={{ color: 'var(--text-secondary)' }}>
|
|
|
|
|
{collection.description}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<span className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
|
|
|
|
|
${(collection.value || 0).toLocaleString()}
|
|
|
|
|
</span>
|
|
|
|
|
<PermissionIndicator isPublic={collection.isPublic} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-07-23 22:26:54 -04:00
|
|
|
</div>
|
2025-07-29 15:19:48 -04:00
|
|
|
|
|
|
|
|
{collections.length > 6 && (
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<Link href="/collections">
|
|
|
|
|
<button className="px-6 py-3 font-medium rounded-xl border-2 transition-all duration-200 hover:opacity-80" style={{ color: 'var(--text-primary)', borderColor: 'var(--accent-ember)' }}>
|
2026-05-29 10:53:40 -04:00
|
|
|
View All Lists
|
2025-07-29 15:19:48 -04:00
|
|
|
</button>
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-07-23 22:26:54 -04:00
|
|
|
</div>
|
2025-07-29 15:19:48 -04:00
|
|
|
)}
|
2025-07-23 22:26:54 -04:00
|
|
|
</div>
|
|
|
|
|
</Layout>
|
|
|
|
|
);
|
|
|
|
|
}
|