From 982654c3ef4dca8e7ce4c45601cf7e51fdb84e31 Mon Sep 17 00:00:00 2001 From: Jose Luis Date: Sat, 21 Mar 2026 20:33:53 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Phase=203=20=E2=80=94=20Workshop=20(com?= =?UTF-8?q?munity=20patch=20sharing)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- packages/client/src/App.jsx | 7 +- packages/client/src/components/Workshop.jsx | 228 ++++++++++++++++++++ packages/client/src/index.css | 115 ++++++++++ packages/client/src/main.jsx | 16 +- packages/client/src/services/api.js | 11 + packages/server/src/index.js | 2 + packages/server/src/routes/workshop.js | 175 +++++++++++++++ 7 files changed, 548 insertions(+), 6 deletions(-) create mode 100644 packages/client/src/components/Workshop.jsx create mode 100644 packages/server/src/routes/workshop.js diff --git a/packages/client/src/App.jsx b/packages/client/src/App.jsx index b37b446..af9caf1 100644 --- a/packages/client/src/App.jsx +++ b/packages/client/src/App.jsx @@ -15,7 +15,7 @@ import { usePinchZoom } from './hooks/usePinchZoom.js'; import { getModulesByCategory } from './engine/moduleRegistry.js'; import { useAuth } from './services/AuthContext.jsx'; -export default function App({ onSwitchToGame }) { +export default function App({ onSwitchToGame, onSwitchToWorkshop }) { const [, forceUpdate] = useState(0); const containerRef = useRef(null); const portPositions = useRef({}); @@ -281,6 +281,11 @@ export default function App({ onSwitchToGame }) { 🎮 Game )} + {onSwitchToWorkshop && !isMobile && ( + + )} Reaktor {!isMobile &&
} {!isMobile && ( diff --git a/packages/client/src/components/Workshop.jsx b/packages/client/src/components/Workshop.jsx new file mode 100644 index 0000000..3596ede --- /dev/null +++ b/packages/client/src/components/Workshop.jsx @@ -0,0 +1,228 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { workshop as workshopApi } from '../services/api.js'; +import { useAuth } from '../services/AuthContext.jsx'; +import { state, deserialize } from '../engine/state.js'; +import { serialize } from '../engine/state.js'; +import { rebuildGraph } from '../engine/audioEngine.js'; + +const TAGS = ['ambient', 'bass', 'drums', 'pad', 'lead', 'fx', 'chiptune', 'experimental']; + +function ShareModal({ onClose, onShared }) { + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [selectedTags, setSelectedTags] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const handleShare = async () => { + if (!title.trim()) { setError('Titulo requerido'); return; } + if (state.modules.length === 0) { setError('No hay modulos en el canvas'); return; } + + setLoading(true); + setError(''); + try { + const patchData = serialize(); + await workshopApi.share({ + title: title.trim(), + description: description.trim(), + tags: selectedTags, + data: patchData, + }); + onShared?.(); + onClose(); + } catch (err) { + setError(err.message); + } + setLoading(false); + }; + + return ( +
+
e.stopPropagation()} style={{ gap: 14 }}> +

Compartir Patch

+ +
+ + setTitle(e.target.value)} /> + + +