feat: Worksheet pagina met workshop materiaal
- /worksheet route met downloads, start prompt en tips - Custom directives: tip block, command block updates - Superpowers zip download voor deelnemers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f6be63dfb1
commit
44f75914cc
8 changed files with 247 additions and 11 deletions
71
content/worksheet.md
Normal file
71
content/worksheet.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
---
|
||||||
|
title: Workshop Materiaal
|
||||||
|
subtitle: Alles wat je nodig hebt tijdens de Claude Code Workshop
|
||||||
|
---
|
||||||
|
|
||||||
|
## Download Superpowers
|
||||||
|
|
||||||
|
Superpowers is een plugin voor Claude Code die extra mogelijkheden toevoegt. Denk aan gestructureerd plannen, test-driven development en slimmer debuggen. Tijdens de workshop gebruiken we deze plugin.
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
Download het zipbestand en installeer het in Claude Code. Ga naar Customize > Personal Plugin (+) > Create Plugin > Upload Plugin en zet daar de zipfile in (niet uitgepakt)
|
||||||
|
:::
|
||||||
|
|
||||||
|
[Download Superpowers (zip)](superpowers-main.zip)
|
||||||
|
|
||||||
|
## Start Prompt
|
||||||
|
|
||||||
|
Gebruik deze prompt als startpunt om te brainstormen met Claude Code over je project. Plak hem in je terminal en vervang `{HIER JE IDEE}` door jouw projectidee. Aan het einde kun je het plan nog aanpassen of extra's aan toevoegen.
|
||||||
|
|
||||||
|
:::command[Start prompt]
|
||||||
|
Ik wil {HIER JE IDEE}.
|
||||||
|
|
||||||
|
Stel me maximaal 5 vragen om mijn idee beter te begrijpen. Stel ze een voor een met AskUserQuestions, niet allemaal tegelijk.
|
||||||
|
|
||||||
|
Maak daarna een bouwplan (PRD) met:
|
||||||
|
- Wat het doet (in gewone taal)
|
||||||
|
- Hoe het eruitziet (beschrijf de interface)
|
||||||
|
- Zelfstandige bouwfasen (max 3-4), in volgorde van prioriteit. Fase 1 is het minimum dat waarde oplevert.
|
||||||
|
Elke fase levert iets werkends op.
|
||||||
|
|
||||||
|
Als ik het bouwplan goedkeur:
|
||||||
|
1. Sla het op als PRD.md in de projectmap
|
||||||
|
2. Maak een CLAUDE.md aan met een samenvatting van het project, de bouwfasen en hun status (allemaal "nog niet gestart")
|
||||||
|
3. Stop. Ga niet verder met bouwen tot ik het zeg.
|
||||||
|
|
||||||
|
Beperkingen:
|
||||||
|
- Alleen lokaal, geen hosting of externe diensten
|
||||||
|
- Gebruik HTML/CSS/JavaScript waar mogelijk (geen framework nodig voor eenvoudige projecten)
|
||||||
|
- Node.js als een server nodig is
|
||||||
|
- Python alleen als Node.js echt niet volstaat
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Tips tijdens het bouwen
|
||||||
|
|
||||||
|
### Sessiemanagement
|
||||||
|
- **Start een nieuwe sessie per bouwfase** - niet alles in een lange sessie proberen
|
||||||
|
- Elke sessie: open de PRD, zeg "bouw fase X" en go.
|
||||||
|
- Korte sessies zijn efficienter dan lange (minder context = minder tokens = snellere antwoorden)
|
||||||
|
- **Kernboodschap:** je PRD is je anker. Bewaar die goed, dan kun je altijd opnieuw starten.
|
||||||
|
|
||||||
|
|
||||||
|
### Waarom niet een lange sessie?
|
||||||
|
- Pro-limiet bereik je sneller in een lange sessie (meer context = meer tokenverbruik)
|
||||||
|
- Vroege signalen dat je limiet nadert: tragere antwoorden, kortere code, stappen overslaan
|
||||||
|
- Als je de melding "usage limit reached" krijgt ben je te laat en zul je een paar uur moeten wachten
|
||||||
|
- Tenzij je een Extra Usage Wallet hebt ingesteld (Settings > Usage)
|
||||||
|
- **Preventie is beter:** plan je sessies per fase, dan is de kans kleiner dat je tegen limieten loopt
|
||||||
|
|
||||||
|
|
||||||
|
### Workflow
|
||||||
|
1. Brainstorm en PRD schrijven (sessie 1)
|
||||||
|
2. Nieuwe sessie in dezelfde werkmap: "bouw fase 1" (sessie 2)
|
||||||
|
3. Nieuwe sessie: "bouw fase 2" (sessie 3)
|
||||||
|
4. Herhaal tot prototype klaar is
|
||||||
|
|
||||||
|
### Agents (voor gevorderden / Max-gebruikers)
|
||||||
|
- Claude Code zet zelf al subagents in wanneer het nuttig is - daar hoef je niks voor te doen
|
||||||
|
- Expliciet aansturen kan: "Zet waar mogelijk agents in om parallelle taken tegelijk te doen"
|
||||||
|
- **Let op:** agents verbruiken meer tokens, niet minder - elke agent heeft zijn eigen context
|
||||||
|
- Voor Pro-gebruikers die al tegen limieten aanlopen versnelt dat het probleem
|
||||||
|
- **Advies:** alleen actief inzetten als je op Max zit of een API key gebruikt
|
||||||
BIN
public/superpowers-main.zip
Normal file
BIN
public/superpowers-main.zip
Normal file
Binary file not shown.
|
|
@ -21,7 +21,7 @@ import { useKeyboardShortcuts } from './useKeyboardShortcuts';
|
||||||
* @param {string} props.content - Huidige Markdown content
|
* @param {string} props.content - Huidige Markdown content
|
||||||
* @param {function} props.onContentChange - Callback bij wijzigingen
|
* @param {function} props.onContentChange - Callback bij wijzigingen
|
||||||
*/
|
*/
|
||||||
export default function MarkdownEditor({ content, onContentChange }) {
|
export default function MarkdownEditor({ content, onContentChange, filename = 'installatie.md' }) {
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
@ -41,7 +41,7 @@ export default function MarkdownEditor({ content, onContentChange }) {
|
||||||
const response = await fetch('/__editor/save', {
|
const response = await fetch('/__editor/save', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ content }),
|
body: JSON.stringify({ content, filename }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
@ -88,7 +88,7 @@ export default function MarkdownEditor({ content, onContentChange }) {
|
||||||
<div className="flex items-center justify-between bg-warm-900 text-white px-4 py-2">
|
<div className="flex items-center justify-between bg-warm-900 text-white px-4 py-2">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<h2 className="font-display font-bold">Markdown Editor</h2>
|
<h2 className="font-display font-bold">Markdown Editor</h2>
|
||||||
<span className="text-warm-400 text-sm">installatie.md</span>
|
<span className="text-warm-400 text-sm">{filename}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-warm-400 text-xs">
|
<span className="text-warm-400 text-xs">
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,14 @@ function parseDirectives(text) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// :::tip
|
||||||
|
if (line.trim() === ':::tip') {
|
||||||
|
const result = parseEnclosed(lines, i);
|
||||||
|
blocks.push({ type: 'tip', content: result.content });
|
||||||
|
i = result.nextIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// :::steps
|
// :::steps
|
||||||
if (line.trim() === ':::steps') {
|
if (line.trim() === ':::steps') {
|
||||||
const result = parseEnclosed(lines, i);
|
const result = parseEnclosed(lines, i);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
* Afbeeldingpaden worden automatisch geprefixed met BASE_URL.
|
* Afbeeldingpaden worden automatisch geprefixed met BASE_URL.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
|
||||||
const BASE_URL = import.meta.env.BASE_URL;
|
const BASE_URL = import.meta.env.BASE_URL;
|
||||||
|
|
@ -48,6 +48,8 @@ function RenderBlock({ block, context }) {
|
||||||
return <WarningBlock content={block.content} />;
|
return <WarningBlock content={block.content} />;
|
||||||
case 'info':
|
case 'info':
|
||||||
return <InfoBlock content={block.content} />;
|
return <InfoBlock content={block.content} />;
|
||||||
|
case 'tip':
|
||||||
|
return <TipBlock content={block.content} />;
|
||||||
case 'steps':
|
case 'steps':
|
||||||
return <StepsBlock block={block} context={context} />;
|
return <StepsBlock block={block} context={context} />;
|
||||||
case 'checklist':
|
case 'checklist':
|
||||||
|
|
@ -196,6 +198,21 @@ function InfoBlock({ content }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipblok (gele achtergrond met lampje).
|
||||||
|
*/
|
||||||
|
function TipBlock({ content }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-amber-50 border border-amber-200 rounded-lg px-4 py-3 mb-6 flex gap-3">
|
||||||
|
<span className="text-xl leading-6 shrink-0">💡</span>
|
||||||
|
<div
|
||||||
|
className="prose-installatie prose-info"
|
||||||
|
dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Genummerde stappen met coral cirkel-iconen.
|
* Genummerde stappen met coral cirkel-iconen.
|
||||||
* Gebruikt pre-parsed steps uit de directive parser.
|
* Gebruikt pre-parsed steps uit de directive parser.
|
||||||
|
|
@ -274,15 +291,46 @@ function ChecklistVerifyBlock({ content }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Terminal commando blok (donkere achtergrond).
|
* Terminal commando blok (donkere achtergrond) met kopieerknop.
|
||||||
*/
|
*/
|
||||||
function CommandBlock({ command, content }) {
|
function CommandBlock({ command, content }) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = useCallback(() => {
|
||||||
|
navigator.clipboard.writeText(content).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
});
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3">
|
<div className="mb-6">
|
||||||
<div className="bg-warm-900 text-warm-100 rounded-lg px-4 py-2 font-mono text-sm">
|
<div className="bg-warm-900 rounded-lg overflow-hidden">
|
||||||
{command}
|
<div className="flex items-center justify-between px-4 py-2 border-b border-warm-700">
|
||||||
|
<span className="text-warm-400 text-sm font-medium">{command}</span>
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="text-warm-400 hover:text-white text-sm transition-colors flex items-center gap-1"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
Gekopieerd!
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
Kopieer
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="text-warm-100 px-4 py-3 font-mono text-sm whitespace-pre-wrap">{content}</pre>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-warm-500 text-sm mt-1">{content}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import Signup from './pages/Signup.jsx'
|
||||||
import WaitlistSignup from './pages/WaitlistSignup.jsx'
|
import WaitlistSignup from './pages/WaitlistSignup.jsx'
|
||||||
import WaitlistThankYou from './pages/WaitlistThankYou.jsx'
|
import WaitlistThankYou from './pages/WaitlistThankYou.jsx'
|
||||||
import Installatie from './pages/Installatie.jsx'
|
import Installatie from './pages/Installatie.jsx'
|
||||||
|
import Worksheet from './pages/Worksheet.jsx'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|
@ -23,6 +24,7 @@ createRoot(document.getElementById('root')).render(
|
||||||
<Route path="/wachtlijst-inschrijven" element={<WaitlistSignup />} />
|
<Route path="/wachtlijst-inschrijven" element={<WaitlistSignup />} />
|
||||||
<Route path="/wachtlijst-bedankt" element={<WaitlistThankYou />} />
|
<Route path="/wachtlijst-bedankt" element={<WaitlistThankYou />} />
|
||||||
<Route path="/installatie" element={<Installatie />} />
|
<Route path="/installatie" element={<Installatie />} />
|
||||||
|
<Route path="/worksheet" element={<Worksheet />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
|
|
|
||||||
105
src/pages/Worksheet.jsx
Normal file
105
src/pages/Worksheet.jsx
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
/**
|
||||||
|
* Worksheet.jsx - Materiaal voor workshop deelnemers
|
||||||
|
*
|
||||||
|
* Rendert content uit content/worksheet.md via de custom Markdown parser.
|
||||||
|
* Bevat downloads, prompts en ander workshopmateriaal.
|
||||||
|
*
|
||||||
|
* Editor modus: bereikbaar via /worksheet?editor=<secret>
|
||||||
|
* De secret wordt geconfigureerd in src/config/editor.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, lazy, Suspense } from 'react';
|
||||||
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
import { parseDocument } from '../lib/markdown/directives';
|
||||||
|
import { renderBlocks } from '../lib/markdown/render';
|
||||||
|
import { EDITOR_CONFIG } from '../config/editor';
|
||||||
|
import worksheetContent from '../../content/worksheet.md?raw';
|
||||||
|
|
||||||
|
// Editor lazy-loaded zodat het niet in de productie bundle zit
|
||||||
|
const MarkdownEditor = lazy(() => import('../components/editor/MarkdownEditor'));
|
||||||
|
|
||||||
|
function Worksheet() {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [content, setContent] = useState(worksheetContent);
|
||||||
|
|
||||||
|
// Editor alleen beschikbaar in dev mode (niet op productie)
|
||||||
|
const showEditor =
|
||||||
|
import.meta.env.DEV &&
|
||||||
|
EDITOR_CONFIG.enabled &&
|
||||||
|
searchParams.get('editor') === EDITOR_CONFIG.secret;
|
||||||
|
|
||||||
|
// Parse de Markdown content
|
||||||
|
const { frontmatter, blocks } = parseDocument(content);
|
||||||
|
|
||||||
|
// Context voor de renderer
|
||||||
|
const context = {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-warm-50">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white border-b border-warm-200 py-6">
|
||||||
|
<div className="container-page">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="text-coral-500 hover:text-coral-600 transition-colors inline-flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Terug naar workshop
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<main className="container-page py-12">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
{/* Titel en subtitel uit frontmatter */}
|
||||||
|
{frontmatter.title && (
|
||||||
|
<h1 className="heading-hero mb-4">{frontmatter.title}</h1>
|
||||||
|
)}
|
||||||
|
{frontmatter.subtitle && (
|
||||||
|
<p className="text-xl text-warm-600 mb-10">{frontmatter.subtitle}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Gerenderde Markdown blocks */}
|
||||||
|
{renderBlocks(blocks, context)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* Contact */}
|
||||||
|
<div className="text-center py-6">
|
||||||
|
<p className="text-warm-500">
|
||||||
|
Kom je er niet uit? Mail naar{' '}
|
||||||
|
<a href="mailto:frank@frankmeeuwsen.com" className="text-coral-500 hover:text-coral-600">
|
||||||
|
frank@frankmeeuwsen.com
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Editor overlay (alleen zichtbaar met juiste URL parameter) */}
|
||||||
|
{showEditor && (
|
||||||
|
<Suspense fallback={
|
||||||
|
<div className="fixed inset-0 bg-warm-900/50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-xl p-8 text-warm-600">Editor laden...</div>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<MarkdownEditor
|
||||||
|
content={content}
|
||||||
|
onContentChange={setContent}
|
||||||
|
filename="worksheet.md"
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-warm-900 text-warm-400 py-6">
|
||||||
|
<div className="container-page text-center text-sm">
|
||||||
|
© {new Date().getFullYear()} Frank Meeuwsen. Alle rechten voorbehouden.
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Worksheet;
|
||||||
|
|
@ -28,8 +28,10 @@ export function editorApiPlugin() {
|
||||||
req.on('data', chunk => { body += chunk; });
|
req.on('data', chunk => { body += chunk; });
|
||||||
req.on('end', () => {
|
req.on('end', () => {
|
||||||
try {
|
try {
|
||||||
const { content } = JSON.parse(body);
|
const { content, filename } = JSON.parse(body);
|
||||||
const filePath = path.join(rootDir, 'content', 'installatie.md');
|
// Bestandsnaam uit request, fallback naar installatie.md
|
||||||
|
const safeName = (filename || 'installatie.md').replace(/[^a-zA-Z0-9._-]/g, '');
|
||||||
|
const filePath = path.join(rootDir, 'content', safeName);
|
||||||
|
|
||||||
// Zorg dat de content directory bestaat
|
// Zorg dat de content directory bestaat
|
||||||
const dir = path.dirname(filePath);
|
const dir = path.dirname(filePath);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue