diff --git a/components/Layout.js b/components/Layout.js
index 507c576..ecfe5dc 100644
--- a/components/Layout.js
+++ b/components/Layout.js
@@ -135,6 +135,7 @@ export default function Layout({ children, user = { email: 'me@randallstillwell.
{ name: 'Decks', href: '/decks', icon: 'deck', active: router.pathname === '/decks', badge: '12' },
{ name: 'Analytics', href: '/analytics', icon: 'analytics', active: router.pathname === '/analytics' },
{ name: 'Community', href: '/community', icon: 'community', active: router.pathname === '/community' },
+ { name: 'Community Collections', href: '/community/collections', icon: 'community', active: router.pathname === '/community/collections' },
{ name: 'Settings', href: '/settings', icon: 'settings', active: router.pathname === '/settings' },
...(user?.role === 'admin' ? [
{ name: 'Admin Tools', href: '/admin/card-editor', icon: 'admin', active: router.pathname.startsWith('/admin'), badge: 'ADMIN' }
diff --git a/pages/api/collections.js b/pages/api/collections.js
index c062e11..17f026d 100644
--- a/pages/api/collections.js
+++ b/pages/api/collections.js
@@ -24,7 +24,7 @@ export default async function handler(req, res) {
const currentUserId = user.userId;
- // Get collections based on ownership, collaboration, or public visibility
+ // Get collections based on ownership, collaboration, or shared access (no public discovery)
const result = await sql`
SELECT DISTINCT
c.*,
@@ -44,8 +44,7 @@ export default async function handler(req, res) {
LEFT JOIN collection_permissions cp ON c.id = cp.collection_id AND cp.user_id = ${currentUserId} AND cp.status = 'active'
WHERE
c.user_id = ${currentUserId} OR
- cp.id IS NOT NULL OR
- (c.is_public = true)
+ cp.id IS NOT NULL
GROUP BY c.id, u.email, cp.role
ORDER BY c.updated_at DESC
`;
diff --git a/pages/api/community/collections.js b/pages/api/community/collections.js
new file mode 100644
index 0000000..d3869f4
--- /dev/null
+++ b/pages/api/community/collections.js
@@ -0,0 +1,74 @@
+import { sql } from '@vercel/postgres';
+import { getUserFromRequest } from '../../../lib/permission-middleware';
+
+export default async function handler(req, res) {
+ // Set CORS headers
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+
+ // Handle preflight requests
+ if (req.method === 'OPTIONS') {
+ res.status(200).end();
+ return;
+ }
+
+ if (req.method !== 'GET') {
+ return res.status(405).json({ error: 'Method not allowed' });
+ }
+
+ try {
+ // Get authenticated user
+ const user = await getUserFromRequest(req);
+ if (!user) {
+ return res.status(401).json({ error: 'Authentication required' });
+ }
+
+ const currentUserId = user.userId;
+
+ // Get all public collections for community discovery
+ const result = await sql`
+ SELECT DISTINCT
+ c.*,
+ u.email as creator_email,
+ COUNT(cc.card_id) as card_count,
+ COALESCE(SUM(cards.market_price * cc.quantity), 0) as total_value,
+ cp.role as user_role,
+ CASE
+ WHEN c.user_id = ${currentUserId} THEN 'owner'
+ WHEN cp.role IS NOT NULL THEN cp.role
+ ELSE NULL
+ END as effective_role
+ FROM collections c
+ LEFT JOIN users u ON c.user_id = u.id
+ LEFT JOIN collection_cards cc ON c.id = cc.collection_id
+ LEFT JOIN cards ON cc.card_id = cards.id
+ LEFT JOIN collection_permissions cp ON c.id = cp.collection_id AND cp.user_id = ${currentUserId} AND cp.status = 'active'
+ WHERE c.is_public = true
+ GROUP BY c.id, u.email, cp.role
+ ORDER BY c.updated_at DESC
+ `;
+
+ const collections = result.rows.map(collection => ({
+ id: collection.id,
+ slug: collection.slug,
+ name: collection.name,
+ description: collection.description,
+ tcg: collection.tcg || 'MTG',
+ cardCount: parseInt(collection.card_count) || 0,
+ value: parseFloat(collection.total_value) || 0,
+ lastViewed: collection.updated_at,
+ createdAt: collection.created_at,
+ isPublic: collection.is_public || false,
+ tags: collection.tags ? collection.tags.split(',') : [],
+ creator: collection.creator_email,
+ userRole: collection.effective_role
+ }));
+
+ res.status(200).json(collections);
+
+ } catch (error) {
+ console.error('Error fetching community collections:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+}
\ No newline at end of file
diff --git a/pages/collections.js b/pages/collections.js
index 7d170c7..de93bdd 100644
--- a/pages/collections.js
+++ b/pages/collections.js
@@ -3,6 +3,7 @@ import { useRouter } from 'next/router';
import Layout from '../components/Layout';
import PermissionIndicator from '../components/PermissionIndicator';
import { useAuth } from '../lib/use-auth';
+import Link from 'next/link';
export default function Collections() {
const router = useRouter();
@@ -359,26 +360,36 @@ export default function Collections() {
-
+
My Collections
-
- Organize and manage your card collections
+
+ Collections you own, collaborate on, or have been shared with you
-
setShowCreateModal(true)}
- className="px-6 py-3 rounded-xl font-medium transition-all duration-200 hover:shadow-md flex items-center space-x-2"
- style={{
- backgroundColor: 'var(--accent-ember)',
- color: 'white'
- }}
- >
-
-
-
- Create Collection
-
+
+
+
+ 🌍 Discover Community
+
+
+ setShowCreateModal(true)}
+ className="px-6 py-2 rounded-xl font-medium transition-all duration-200 hover:shadow-md"
+ style={{
+ backgroundColor: 'var(--accent-ember)',
+ color: 'white'
+ }}
+ >
+ + Create Collection
+
+
diff --git a/pages/community/collections.js b/pages/community/collections.js
new file mode 100644
index 0000000..5d68878
--- /dev/null
+++ b/pages/community/collections.js
@@ -0,0 +1,375 @@
+import { useState, useEffect } from 'react';
+import { useRouter } from 'next/router';
+import Link from 'next/link';
+import Layout from '../../components/Layout';
+import PermissionIndicator from '../../components/PermissionIndicator';
+import { useAuth } from '../../lib/use-auth';
+
+export default function CommunityCollections() {
+ const router = useRouter();
+ const { user, loading: authLoading } = useAuth();
+
+ const [collections, setCollections] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [sortBy, setSortBy] = useState('name');
+
+ // Redirect to login if not authenticated
+ useEffect(() => {
+ if (!authLoading && !user) {
+ router.push('/login');
+ }
+ }, [authLoading, user, router]);
+
+ useEffect(() => {
+ if (user) {
+ fetchPublicCollections();
+ }
+ }, [user]);
+
+ const fetchPublicCollections = 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/community/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 public collections');
+ }
+ } catch (error) {
+ console.error('Error fetching public collections:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const sortOptions = [
+ { value: 'name', label: 'Name (A-Z)' },
+ { value: 'value', label: 'Value (High to Low)' },
+ { value: 'cardCount', label: 'Card Count (High to Low)' },
+ { value: 'createdAt', label: 'Recently Created' }
+ ];
+
+ const sortCollections = (collections, sortBy) => {
+ return [...collections].sort((a, b) => {
+ switch (sortBy) {
+ case 'name':
+ return a.name.localeCompare(b.name);
+ case 'value':
+ return (b.value || 0) - (a.value || 0);
+ case 'cardCount':
+ return (b.cardCount || 0) - (a.cardCount || 0);
+ case 'createdAt':
+ return new Date(b.createdAt) - new Date(a.createdAt);
+ default:
+ return 0;
+ }
+ });
+ };
+
+ const filteredCollections = collections.filter(collection => {
+ const matchesSearch = collection.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ collection.description.toLowerCase().includes(searchQuery.toLowerCase());
+ return matchesSearch;
+ });
+
+ const sortedCollections = sortCollections(filteredCollections, sortBy);
+
+ const formatCurrency = (amount) => {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD'
+ }).format(amount);
+ };
+
+ const formatDate = (dateString) => {
+ return new Date(dateString).toLocaleDateString();
+ };
+
+ // Collection thumbnail component (same as in regular collections)
+ const CollectionThumbnail = ({ collection }) => {
+ const { thumbnails = [], image } = collection;
+
+ // If collection has a custom hero image, use it
+ if (image) {
+ return (
+
+
+
+ );
+ }
+
+ // If no thumbnails available, show placeholder
+ if (!thumbnails || thumbnails.length === 0) {
+ return (
+
+ );
+ }
+
+ // Show main card (rarest) and grid of 4 others
+ const mainCard = thumbnails[0]; // Rarest card
+ const gridCards = thumbnails.slice(1, 5); // Next 4 cards
+
+ return (
+
+ {/* Main card (rarest) - takes up 2/3 of the space */}
+
+ {mainCard ? (
+
+
+ {/* Rarity glow effect */}
+
+ {/* Card name overlay */}
+
+
+ ) : (
+
+ )}
+
+
+ {/* Grid of 4 other cards - takes up 1/3 of the space */}
+
+
+ {Array.from({ length: 4 }).map((_, index) => {
+ const card = gridCards[index];
+ return (
+
+ {card ? (
+
+
+ {/* Subtle rarity glow */}
+
+
+ ) : (
+
+ )}
+
+ );
+ })}
+
+
+
+ );
+ };
+
+ // Show loading spinner while auth is loading or data is loading
+ if (authLoading || loading) {
+ return (
+
+
+
+ );
+ }
+
+ // Redirect to login if not authenticated (handled by useEffect, but this is a fallback)
+ if (!user) {
+ return null;
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+ Community Collections
+
+
+ Discover public collections shared by the community
+
+
+
+
+ {/* Search and Sort */}
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+ setSortBy(e.target.value)}
+ className="input-field w-48"
+ >
+ {sortOptions.map(option => (
+
+ {option.label}
+
+ ))}
+
+
+
+
+
+ {/* Collections Grid */}
+
+ {sortedCollections.length === 0 ? (
+
+
🌍
+
+ {searchQuery ? 'No collections found' : 'No public collections yet'}
+
+
+ {searchQuery
+ ? 'Try adjusting your search terms'
+ : 'Be the first to share a public collection with the community!'
+ }
+
+ {!searchQuery && (
+
+
+ Go to My Collections
+
+
+ )}
+
+ ) : (
+
+ {sortedCollections.map(collection => (
+
+
+
+
+
+
+
+
+ {collection.name}
+
+ {collection.description && (
+
+ {collection.description}
+
+ )}
+
+ by {collection.creator}
+
+
+
+
+
+ {/* Compact Stats */}
+
+
+
+ {collection.cardCount} cards
+
+
+ {formatCurrency(collection.value)}
+
+
+
+ {formatDate(collection.createdAt)}
+
+
+
+ {/* Tags */}
+ {collection.tags && collection.tags.length > 0 && (
+
+ {collection.tags.slice(0, 2).map((tag, index) => (
+
+ {tag}
+
+ ))}
+ {collection.tags.length > 2 && (
+
+ +{collection.tags.length - 2}
+
+ )}
+
+ )}
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
\ No newline at end of file