2026-08-24 22:13:12 -04:00
|
|
|
import { sql } from '../../../lib/sql.js';
|
|
|
|
|
import { getUserFromRequest } from '../../../lib/permission-middleware';
|
2026-09-01 10:23:16 -04:00
|
|
|
import { validatePalette, validateLayout, DEFAULT_LAYOUT } from '../../../lib/frame-palette.js';
|
2026-08-24 22:13:12 -04:00
|
|
|
|
|
|
|
|
export default async function handler(req, res) {
|
|
|
|
|
try {
|
|
|
|
|
const user = await getUserFromRequest(req);
|
|
|
|
|
if (!user) {
|
|
|
|
|
return res.status(401).json({ error: 'Authentication required' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const frameId = parseInt(req.query.id, 10);
|
|
|
|
|
if (!Number.isInteger(frameId)) {
|
|
|
|
|
return res.status(400).json({ error: 'Invalid frame id' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const found = await sql`
|
|
|
|
|
SELECT * FROM custom_frames
|
|
|
|
|
WHERE id = ${frameId} AND user_id = ${user.userId}
|
|
|
|
|
`;
|
|
|
|
|
if (found.rows.length === 0) {
|
|
|
|
|
return res.status(404).json({ error: 'Frame not found' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (req.method === 'GET') {
|
|
|
|
|
return res.status(200).json({ frame: found.rows[0] });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (req.method === 'PUT') {
|
|
|
|
|
const name = typeof req.body?.name === 'string' ? req.body.name.trim() : '';
|
|
|
|
|
if (!name || name.length > 100) {
|
|
|
|
|
return res.status(400).json({ error: 'Frame name is required (100 characters max)' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { palette, error } = validatePalette(req.body?.palette);
|
|
|
|
|
if (error) {
|
|
|
|
|
return res.status(400).json({ error });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clash = await sql`
|
|
|
|
|
SELECT id FROM custom_frames
|
|
|
|
|
WHERE user_id = ${user.userId}
|
|
|
|
|
AND lower(name) = ${name.toLowerCase()}
|
|
|
|
|
AND id <> ${frameId}
|
|
|
|
|
`;
|
|
|
|
|
if (clash.rows.length > 0) {
|
|
|
|
|
return res.status(409).json({ error: 'You already have a frame with that name' });
|
|
|
|
|
}
|
|
|
|
|
|
feat(designer): inline symbol icons, frame textures, community sharing, print sheets
- {CODE} tokens in description/actions/flavor render as inline symbol
icons (RichText), including inside cost pips
- custom frames gain an optional background texture (upload/replace/
remove via /api/custom-frames/[id]/texture); renders behind panels
- custom games can be shared to the community (is_public): toggle in the
game space, public listing at /community/games, read-only game view,
/api/public/games endpoints (no auth, public rows only)
- /designer/print: multi-card print sheets on US Letter at 300dpi
(63x88mm cards, 3x3 or 2x2, dashed cut guides, full-sheet PNG export)
- migration 1787711511000
2026-08-24 22:48:52 -04:00
|
|
|
const textureUrl =
|
|
|
|
|
req.body?.texture_url === null || req.body?.texture_url === ''
|
|
|
|
|
? null
|
|
|
|
|
: typeof req.body?.texture_url === 'string'
|
|
|
|
|
? req.body.texture_url.trim()
|
|
|
|
|
: found.rows[0].texture_url;
|
|
|
|
|
|
2026-09-01 10:23:16 -04:00
|
|
|
const layoutInput = req.body?.layout === undefined ? found.rows[0].layout : req.body.layout;
|
|
|
|
|
const { layout, error: layoutError } = validateLayout(layoutInput || {});
|
|
|
|
|
if (layoutError) {
|
|
|
|
|
return res.status(400).json({ error: layoutError });
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-24 22:13:12 -04:00
|
|
|
const updated = await sql`
|
|
|
|
|
UPDATE custom_frames SET
|
|
|
|
|
name = ${name}, palette = ${sql.json(palette)},
|
feat(designer): inline symbol icons, frame textures, community sharing, print sheets
- {CODE} tokens in description/actions/flavor render as inline symbol
icons (RichText), including inside cost pips
- custom frames gain an optional background texture (upload/replace/
remove via /api/custom-frames/[id]/texture); renders behind panels
- custom games can be shared to the community (is_public): toggle in the
game space, public listing at /community/games, read-only game view,
/api/public/games endpoints (no auth, public rows only)
- /designer/print: multi-card print sheets on US Letter at 300dpi
(63x88mm cards, 3x3 or 2x2, dashed cut guides, full-sheet PNG export)
- migration 1787711511000
2026-08-24 22:48:52 -04:00
|
|
|
texture_url = ${textureUrl},
|
2026-09-01 10:23:16 -04:00
|
|
|
layout = ${sql.json(layout)},
|
2026-08-24 22:13:12 -04:00
|
|
|
updated_at = CURRENT_TIMESTAMP
|
|
|
|
|
WHERE id = ${frameId}
|
2026-09-01 10:23:16 -04:00
|
|
|
RETURNING id, name, palette, texture_url, frame_image_url, layout, created_at, updated_at
|
2026-08-24 22:13:12 -04:00
|
|
|
`;
|
|
|
|
|
return res.status(200).json({ frame: updated.rows[0] });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (req.method === 'DELETE') {
|
|
|
|
|
// Designs using this frame fall back to their starter frame_id
|
|
|
|
|
// (custom_frame_id drops to NULL via FK).
|
|
|
|
|
await sql`DELETE FROM custom_frames WHERE id = ${frameId}`;
|
|
|
|
|
return res.status(200).json({ message: 'Frame deleted' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return res.status(405).json({ error: 'Method not allowed' });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Custom frame API error:', error);
|
|
|
|
|
return res.status(500).json({ error: 'Internal server error' });
|
|
|
|
|
}
|
|
|
|
|
}
|