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 {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 [isDirty, setIsDirty] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
|
@ -41,7 +41,7 @@ export default function MarkdownEditor({ content, onContentChange }) {
|
|||
const response = await fetch('/__editor/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content }),
|
||||
body: JSON.stringify({ content, filename }),
|
||||
});
|
||||
|
||||
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 gap-3">
|
||||
<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 className="flex items-center gap-2">
|
||||
<span className="text-warm-400 text-xs">
|
||||
|
|
|
|||
|
|
@ -101,6 +101,14 @@ function parseDirectives(text) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// :::tip
|
||||
if (line.trim() === ':::tip') {
|
||||
const result = parseEnclosed(lines, i);
|
||||
blocks.push({ type: 'tip', content: result.content });
|
||||
i = result.nextIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
// :::steps
|
||||
if (line.trim() === ':::steps') {
|
||||
const result = parseEnclosed(lines, i);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
* Afbeeldingpaden worden automatisch geprefixed met BASE_URL.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { marked } from 'marked';
|
||||
|
||||
const BASE_URL = import.meta.env.BASE_URL;
|
||||
|
|
@ -48,6 +48,8 @@ function RenderBlock({ block, context }) {
|
|||
return <WarningBlock content={block.content} />;
|
||||
case 'info':
|
||||
return <InfoBlock content={block.content} />;
|
||||
case 'tip':
|
||||
return <TipBlock content={block.content} />;
|
||||
case 'steps':
|
||||
return <StepsBlock block={block} context={context} />;
|
||||
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.
|
||||
* 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 }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(content).then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="bg-warm-900 text-warm-100 rounded-lg px-4 py-2 font-mono text-sm">
|
||||
{command}
|
||||
<div className="mb-6">
|
||||
<div className="bg-warm-900 rounded-lg overflow-hidden">
|
||||
<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>
|
||||
<p className="text-warm-500 text-sm mt-1">{content}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import Signup from './pages/Signup.jsx'
|
|||
import WaitlistSignup from './pages/WaitlistSignup.jsx'
|
||||
import WaitlistThankYou from './pages/WaitlistThankYou.jsx'
|
||||
import Installatie from './pages/Installatie.jsx'
|
||||
import Worksheet from './pages/Worksheet.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
|
|
@ -23,6 +24,7 @@ createRoot(document.getElementById('root')).render(
|
|||
<Route path="/wachtlijst-inschrijven" element={<WaitlistSignup />} />
|
||||
<Route path="/wachtlijst-bedankt" element={<WaitlistThankYou />} />
|
||||
<Route path="/installatie" element={<Installatie />} />
|
||||
<Route path="/worksheet" element={<Worksheet />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</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('end', () => {
|
||||
try {
|
||||
const { content } = JSON.parse(body);
|
||||
const filePath = path.join(rootDir, 'content', 'installatie.md');
|
||||
const { content, filename } = JSON.parse(body);
|
||||
// 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
|
||||
const dir = path.dirname(filePath);
|
||||
|
|
|
|||
Loading…
Reference in a new issue