From 603bf5bc89cafce1fe764ad866ebbdc092af23d4 Mon Sep 17 00:00:00 2001 From: Randall Stillwell Date: Sun, 27 Jul 2025 15:17:51 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20Implement=20'All=20My=20Cards'?= =?UTF-8?q?=20System=20Collection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✨ New Feature - Automatic System Collection: - Every user gets an undeletable 'All My Cards' collection on registration - Contains all cards marked as owned by the user - Cannot be deleted, renamed, or made public - Special šŸ”’ System indicator in the UI šŸ—ƒļø Database Changes: - Added is_system_collection column to collections table - Migration script created 'All My Cards' for all existing users (5 users) - Automatic creation in registration API for new users šŸ›”ļø API Protections: - DELETE: System collections cannot be deleted - PUT: System collections cannot be renamed or made public - Added isSystemCollection field to API responses šŸŽØ Frontend Updates: - System collections show šŸ”’ System badge - Edit/Delete buttons hidden for system collections - Special visual indicator for protected collections šŸŽÆ Implementation Details: - Unique slug generation (all-my-cards, all-my-cards-2, etc.) - Proper permissions setup for each collection - Error handling for edge cases - Non-blocking registration if collection creation fails Ready for users to have their automatic 'All My Cards' collection! šŸš€ --- pages/api/auth/register.js | 51 +++++++++++ pages/api/collections/[identifier].js | 24 ++++- pages/collections.js | 80 +++++++++-------- scripts/add-system-collection-column.js | 111 ++++++++++++++++++++++++ 4 files changed, 227 insertions(+), 39 deletions(-) create mode 100644 scripts/add-system-collection-column.js diff --git a/pages/api/auth/register.js b/pages/api/auth/register.js index dcf6c47..708416d 100644 --- a/pages/api/auth/register.js +++ b/pages/api/auth/register.js @@ -1,6 +1,7 @@ import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import { sql } from '@vercel/postgres'; +import { generateUniqueSlug } from '../../../lib/slug-utils.js'; const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; @@ -52,6 +53,56 @@ export default async function handler(req, res) { const user = result.rows[0]; + // Create the automatic "All My Cards" collection for the new user + try { + // Get existing slugs to ensure uniqueness + const existingSlugsData = await sql`SELECT slug FROM collections WHERE slug IS NOT NULL`; + const existingSlugs = (existingSlugsData.rows || []).map(row => row.slug); + + // Generate unique slug for "All My Cards" + const uniqueSlug = await generateUniqueSlug("All My Cards", existingSlugs); + + // Create the special collection + const collectionResult = await sql` + INSERT INTO collections ( + name, + description, + tcg, + is_public, + user_id, + slug, + is_system_collection, + created_at, + updated_at + ) + VALUES ( + 'All My Cards', + 'Automatically contains all cards you mark as owned. This collection cannot be deleted or made public.', + 'All', + false, + ${user.id}, + ${uniqueSlug}, + true, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) + RETURNING id + `; + + const collection = collectionResult.rows[0]; + + // Create owner permission for the collection + await sql` + INSERT INTO collection_permissions (collection_id, user_id, role, status, created_at) + VALUES (${collection.id}, ${user.id}, 'owner', 'active', CURRENT_TIMESTAMP) + `; + + console.log(`āœ… Created "All My Cards" collection for user ${user.email} (ID: ${collection.id})`); + } catch (collectionError) { + console.error('Error creating "All My Cards" collection:', collectionError); + // Don't fail the registration if collection creation fails + } + // Generate JWT token const token = jwt.sign( { userId: user.id, email: user.email, role: user.role }, diff --git a/pages/api/collections/[identifier].js b/pages/api/collections/[identifier].js index df59031..0c9a015 100644 --- a/pages/api/collections/[identifier].js +++ b/pages/api/collections/[identifier].js @@ -59,7 +59,6 @@ export default async function handler(req, res) { GROUP BY c.id, u.email, cp.role `; } else { - // Numeric ID lookup const numericId = parseInt(identifier); collectionQuery = sql` SELECT DISTINCT @@ -88,13 +87,13 @@ export default async function handler(req, res) { `; } - const result = await collectionQuery; + const collectionResult = await collectionQuery; - if (result.rows.length === 0) { + if (collectionResult.rows.length === 0) { return res.status(404).json({ error: 'Collection not found or access denied' }); } - const collection = result.rows[0]; + const collection = collectionResult.rows[0]; if (req.method === 'GET') { const formattedCollection = { @@ -108,6 +107,7 @@ export default async function handler(req, res) { lastViewed: collection.updated_at, createdAt: collection.created_at, isPublic: collection.is_public || false, + isSystemCollection: collection.is_system_collection || false, image: collection.image, tags: collection.tags ? collection.tags.split(',') : [], creator: collection.creator_email, @@ -124,6 +124,16 @@ export default async function handler(req, res) { const { name, description, isPublic, image, tags } = req.body; + // Prevent system collections from being made public + if (collection.is_system_collection && isPublic === true) { + return res.status(403).json({ error: 'System collections cannot be made public' }); + } + + // Prevent renaming system collections + if (collection.is_system_collection && name !== undefined && name !== collection.name) { + return res.status(403).json({ error: 'System collections cannot be renamed' }); + } + // If name is being changed, generate new slug let updateFields = []; let updateValues = []; @@ -187,6 +197,7 @@ export default async function handler(req, res) { name: updateResult.rows[0].name, description: updateResult.rows[0].description, isPublic: updateResult.rows[0].is_public, + isSystemCollection: updateResult.rows[0].is_system_collection, image: updateResult.rows[0].image, tags: updateResult.rows[0].tags ? updateResult.rows[0].tags.split(',') : [] }; @@ -199,6 +210,11 @@ export default async function handler(req, res) { return res.status(403).json({ error: 'Only collection owners can delete collections' }); } + // Prevent deletion of system collections + if (collection.is_system_collection) { + return res.status(403).json({ error: 'System collections cannot be deleted' }); + } + // Delete collection and all related data await sql`DELETE FROM collection_cards WHERE collection_id = ${collection.id}`; await sql`DELETE FROM collection_permissions WHERE collection_id = ${collection.id}`; diff --git a/pages/collections.js b/pages/collections.js index 0748e7b..23b814e 100644 --- a/pages/collections.js +++ b/pages/collections.js @@ -491,42 +491,52 @@ export default function Collections() { {/* Header with name and description - more space */}
-

- {collection.name} -

- {/* Edit/Delete buttons moved to hover only */} -
- - +
+

+ {collection.name} +

+ {/* System collection indicator */} + {collection.isSystemCollection && ( + + šŸ”’ System + + )}
+ {/* Edit/Delete buttons - hidden for system collections */} + {!collection.isSystemCollection && ( +
+ + +
+ )}
{collection.description && (

diff --git a/scripts/add-system-collection-column.js b/scripts/add-system-collection-column.js new file mode 100644 index 0000000..fb31c21 --- /dev/null +++ b/scripts/add-system-collection-column.js @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +import dotenv from 'dotenv'; +import { neon } from '@neondatabase/serverless'; +import { generateUniqueSlug } from '../lib/slug-utils.js'; + +dotenv.config({ path: '.env.local' }); + +async function addSystemCollectionColumn() { + const sql = neon(process.env.POSTGRES_URL); + + try { + console.log('šŸ”§ Adding is_system_collection column to collections table...'); + + // Add the is_system_collection column + await sql` + ALTER TABLE collections + ADD COLUMN IF NOT EXISTS is_system_collection BOOLEAN DEFAULT false + `; + + console.log('āœ… Added is_system_collection column'); + + // Get all existing users who don't have an "All My Cards" collection + console.log('šŸ‘„ Finding users without "All My Cards" collection...'); + + const usersWithoutAllMyCards = await sql` + SELECT u.id, u.email + FROM users u + WHERE NOT EXISTS ( + SELECT 1 FROM collections c + WHERE c.user_id = u.id + AND c.name = 'All My Cards' + AND c.is_system_collection = true + ) + `; + + console.log(`Found ${usersWithoutAllMyCards.length} users without "All My Cards" collection`); + + // Get existing slugs for uniqueness + const existingSlugsData = await sql`SELECT slug FROM collections WHERE slug IS NOT NULL`; + const existingSlugs = (existingSlugsData || []).map(row => row.slug); + + // Create "All My Cards" collection for each user + for (const user of usersWithoutAllMyCards) { + try { + console.log(`Creating "All My Cards" for ${user.email}...`); + + // Generate unique slug + const uniqueSlug = await generateUniqueSlug("All My Cards", existingSlugs); + existingSlugs.push(uniqueSlug); + + // Create the collection + const collectionResult = await sql` + INSERT INTO collections ( + name, + description, + tcg, + is_public, + user_id, + slug, + is_system_collection, + created_at, + updated_at + ) + VALUES ( + 'All My Cards', + 'Automatically contains all cards you mark as owned. This collection cannot be deleted or made public.', + 'All', + false, + ${user.id}, + ${uniqueSlug}, + true, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP + ) + RETURNING id + `; + + const collection = collectionResult[0]; + + // Create owner permission + await sql` + INSERT INTO collection_permissions (collection_id, user_id, role, status, created_at) + VALUES (${collection.id}, ${user.id}, 'owner', 'active', CURRENT_TIMESTAMP) + `; + + console.log(` āœ… Created collection ID ${collection.id} with slug "${uniqueSlug}"`); + + } catch (error) { + console.error(` āŒ Failed to create collection for ${user.email}:`, error.message); + } + } + + console.log('\nšŸŽ‰ Migration completed successfully!'); + console.log('\nšŸ“‹ Summary:'); + console.log(` • Added is_system_collection column to collections table`); + console.log(` • Created "All My Cards" collections for ${usersWithoutAllMyCards.length} existing users`); + console.log(` • New users will automatically get this collection on registration`); + + } catch (error) { + console.error('āŒ Migration failed:', error.message); + console.error('Full error:', error); + process.exit(1); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + addSystemCollectionColumn(); +} + +export { addSystemCollectionColumn }; \ No newline at end of file