Server: - GET /api/v1/workshop — browse patches (search, tags, sort) - POST /api/v1/workshop — share a patch (auth required) - GET /api/v1/workshop/:id — single patch detail - DELETE /api/v1/workshop/:id — soft delete (owner/admin) - POST/DELETE /api/v1/workshop/:id/like — like/unlike - POST /api/v1/workshop/:id/report — flag for moderation Client: - Workshop page with nav bar (Sandbox/SynthQuest/Workshop tabs) - Search bar + tag filters (ambient, bass, drums, etc.) - Sort by recent/popular - Patch cards: title, author, tags, likes, module count - "Cargar" button loads patch into Sandbox - Share modal: title, description, tags, shares current canvas - User badge + login button in Workshop nav - Responsive: single column on mobile Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
176 lines
5.4 KiB
JavaScript
176 lines
5.4 KiB
JavaScript
import { eq, sql, desc, and, ilike, or, inArray } from 'drizzle-orm';
|
|
import { db, schema } from '../db/index.js';
|
|
import { authenticate } from '../middleware/auth.js';
|
|
|
|
export default async function workshopRoutes(fastify) {
|
|
|
|
// Browse patches (public)
|
|
fastify.get('/', async (request) => {
|
|
const { q, tags, sort = 'recent', page = 1 } = request.query;
|
|
const limit = 20;
|
|
const offset = (page - 1) * limit;
|
|
|
|
const conditions = [eq(schema.sharedPatches.isDeleted, false)];
|
|
|
|
if (q) {
|
|
conditions.push(
|
|
or(
|
|
ilike(schema.sharedPatches.title, `%${q}%`),
|
|
ilike(schema.sharedPatches.description, `%${q}%`)
|
|
)
|
|
);
|
|
}
|
|
|
|
if (tags) {
|
|
const tagList = tags.split(',').map(t => t.trim());
|
|
conditions.push(sql`${schema.sharedPatches.tags} && ARRAY[${sql.join(tagList.map(t => sql`${t}`), sql`, `)}]::text[]`);
|
|
}
|
|
|
|
const orderBy = sort === 'popular'
|
|
? desc(schema.sharedPatches.likesCount)
|
|
: desc(schema.sharedPatches.createdAt);
|
|
|
|
const patches = await db.select({
|
|
id: schema.sharedPatches.id,
|
|
title: schema.sharedPatches.title,
|
|
description: schema.sharedPatches.description,
|
|
tags: schema.sharedPatches.tags,
|
|
data: schema.sharedPatches.data,
|
|
likesCount: schema.sharedPatches.likesCount,
|
|
createdAt: schema.sharedPatches.createdAt,
|
|
userId: schema.sharedPatches.userId,
|
|
}).from(schema.sharedPatches)
|
|
.where(and(...conditions))
|
|
.orderBy(orderBy)
|
|
.limit(limit)
|
|
.offset(offset);
|
|
|
|
// Get usernames for patches
|
|
const userIds = [...new Set(patches.filter(p => p.userId).map(p => p.userId))];
|
|
let userMap = {};
|
|
if (userIds.length > 0) {
|
|
const users = await db.select({
|
|
id: schema.users.id,
|
|
username: schema.users.username,
|
|
avatarUrl: schema.users.avatarUrl,
|
|
}).from(schema.users).where(inArray(schema.users.id, userIds));
|
|
userMap = Object.fromEntries(users.map(u => [u.id, u]));
|
|
}
|
|
|
|
const result = patches.map(p => ({
|
|
...p,
|
|
author: userMap[p.userId] || null,
|
|
userId: undefined,
|
|
}));
|
|
|
|
return { patches: result, page: +page, limit };
|
|
});
|
|
|
|
// Get single patch
|
|
fastify.get('/:id', async (request, reply) => {
|
|
const [patch] = await db.select().from(schema.sharedPatches)
|
|
.where(and(
|
|
eq(schema.sharedPatches.id, request.params.id),
|
|
eq(schema.sharedPatches.isDeleted, false)
|
|
)).limit(1);
|
|
|
|
if (!patch) return reply.code(404).send({ error: 'Not found' });
|
|
|
|
let author = null;
|
|
if (patch.userId) {
|
|
const [user] = await db.select({
|
|
username: schema.users.username,
|
|
avatarUrl: schema.users.avatarUrl,
|
|
}).from(schema.users).where(eq(schema.users.id, patch.userId)).limit(1);
|
|
author = user || null;
|
|
}
|
|
|
|
return { ...patch, author };
|
|
});
|
|
|
|
// Share a patch (requires auth)
|
|
fastify.post('/', { preHandler: [authenticate] }, async (request) => {
|
|
const { title, description, tags, data } = request.body;
|
|
if (!title || !data) return { error: 'title and data required' };
|
|
|
|
const [patch] = await db.insert(schema.sharedPatches).values({
|
|
userId: request.user.id,
|
|
title,
|
|
description: description || '',
|
|
tags: tags || [],
|
|
data,
|
|
}).returning();
|
|
|
|
return patch;
|
|
});
|
|
|
|
// Delete own patch
|
|
fastify.delete('/:id', { preHandler: [authenticate] }, async (request, reply) => {
|
|
const [patch] = await db.select().from(schema.sharedPatches)
|
|
.where(eq(schema.sharedPatches.id, request.params.id)).limit(1);
|
|
|
|
if (!patch) return reply.code(404).send({ error: 'Not found' });
|
|
|
|
// Owner or admin can delete
|
|
if (patch.userId !== request.user.id && request.user.role !== 'admin') {
|
|
return reply.code(403).send({ error: 'Forbidden' });
|
|
}
|
|
|
|
await db.update(schema.sharedPatches)
|
|
.set({ isDeleted: true })
|
|
.where(eq(schema.sharedPatches.id, request.params.id));
|
|
|
|
return { ok: true };
|
|
});
|
|
|
|
// Like a patch
|
|
fastify.post('/:id/like', { preHandler: [authenticate] }, async (request, reply) => {
|
|
const patchId = request.params.id;
|
|
|
|
// Check if already liked
|
|
const [existing] = await db.select().from(schema.likes)
|
|
.where(and(
|
|
eq(schema.likes.userId, request.user.id),
|
|
eq(schema.likes.patchId, patchId)
|
|
)).limit(1);
|
|
|
|
if (existing) return { liked: true, message: 'Already liked' };
|
|
|
|
await db.insert(schema.likes).values({
|
|
userId: request.user.id,
|
|
patchId,
|
|
});
|
|
|
|
await db.update(schema.sharedPatches)
|
|
.set({ likesCount: sql`${schema.sharedPatches.likesCount} + 1` })
|
|
.where(eq(schema.sharedPatches.id, patchId));
|
|
|
|
return { liked: true };
|
|
});
|
|
|
|
// Unlike a patch
|
|
fastify.delete('/:id/like', { preHandler: [authenticate] }, async (request) => {
|
|
const patchId = request.params.id;
|
|
|
|
const result = await db.delete(schema.likes)
|
|
.where(and(
|
|
eq(schema.likes.userId, request.user.id),
|
|
eq(schema.likes.patchId, patchId)
|
|
));
|
|
|
|
await db.update(schema.sharedPatches)
|
|
.set({ likesCount: sql`GREATEST(${schema.sharedPatches.likesCount} - 1, 0)` })
|
|
.where(eq(schema.sharedPatches.id, patchId));
|
|
|
|
return { liked: false };
|
|
});
|
|
|
|
// Report/flag a patch
|
|
fastify.post('/:id/report', { preHandler: [authenticate] }, async (request) => {
|
|
await db.update(schema.sharedPatches)
|
|
.set({ isFlagged: true })
|
|
.where(eq(schema.sharedPatches.id, request.params.id));
|
|
return { ok: true };
|
|
});
|
|
}
|