deckhearth/pages/api/custom-cards/[id].js
Randall Stillwell 421c5e5ee5 feat(designer): add card designer with starter frames, live preview, and PNG export
- custom_cards migration + CRUD API with catalog twin sync so designs
  appear in My Cards, lists, and decks via normal card joins
- artwork upload to MinIO under card-art/
- /designer page: form-driven live preview, 4 starter frames, PNG export
- /my-designs gallery with edit/delete
- Designer nav entry in sidebar + mobile drawer
2026-08-24 14:53:06 -05:00

91 lines
3.1 KiB
JavaScript

import { sql } from '../../../lib/sql.js';
import { getUserFromRequest } from '../../../lib/permission-middleware';
import { syncCatalogCard, ensureOwnedRow } from './index.js';
export default async function handler(req, res) {
try {
const user = await getUserFromRequest(req);
if (!user) {
return res.status(401).json({ error: 'Authentication required' });
}
const designId = parseInt(req.query.id, 10);
if (!Number.isInteger(designId)) {
return res.status(400).json({ error: 'Invalid design id' });
}
const found = await sql`
SELECT * FROM custom_cards
WHERE id = ${designId} AND user_id = ${user.userId}
`;
if (found.rows.length === 0) {
return res.status(404).json({ error: 'Design not found' });
}
const existing = found.rows[0];
if (req.method === 'GET') {
return res.status(200).json({ design: existing });
}
if (req.method === 'PUT') {
const f = pickFields(req.body);
if (!f.name) {
return res.status(400).json({ error: 'Name is required' });
}
const updated = await sql`
UPDATE custom_cards SET
name = ${f.name}, mana_cost = ${f.manaCost},
card_type = ${f.cardType}, rarity = ${f.rarity},
rules_text = ${f.rulesText}, actions = ${f.actions},
power = ${f.power}, toughness = ${f.toughness},
frame_id = ${f.frameId}, artwork_url = ${f.artworkUrl},
updated_at = CURRENT_TIMESTAMP
WHERE id = ${designId}
RETURNING *
`;
const design = updated.rows[0];
const cardId = await syncCatalogCard(design, f);
if (cardId) {
await ensureOwnedRow(user.userId, cardId);
if (!design.card_id) {
const linked = await sql`
UPDATE custom_cards SET card_id = ${cardId}
WHERE id = ${designId} RETURNING *
`;
return res.status(200).json({ design: linked.rows[0] });
}
}
return res.status(200).json({ design });
}
if (req.method === 'DELETE') {
// The catalog twin stays (it may already live in lists/decks);
// removing the design row simply detaches future edits from it.
await sql`DELETE FROM custom_cards WHERE id = ${designId}`;
return res.status(200).json({ message: 'Design deleted' });
}
return res.status(405).json({ error: 'Method not allowed' });
} catch (error) {
console.error('Custom card API error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
function pickFields(body) {
const str = (v) => (typeof v === 'string' ? v.trim() : null);
return {
name: str(body.name),
manaCost: str(body.mana_cost) || str(body.manaCost),
cardType: str(body.card_type) || str(body.cardType),
rarity: str(body.rarity),
rulesText: str(body.rules_text) || str(body.description),
actions: str(body.actions),
power: str(body.power),
toughness: str(body.toughness),
frameId: str(body.frame_id) || str(body.frameId) || 'classic',
artworkUrl: str(body.artwork_url) || str(body.artworkUrl),
};
}