feat(onboarding): mnemonic codebook with print/PDF option
After saving all 12 mnemonics, players see their full codebook — each letter with its Morse pattern (· —) and mnemonic phrase in a grid layout. Two actions: Print/Save PDF (window.print) and Start Playing. Operator name appears on the printed page. Print CSS hides UI chrome and renders a clean 3-column grid. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+5
-1
@@ -61,7 +61,11 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{route === 'onboarding' && token && (
|
{route === 'onboarding' && token && (
|
||||||
<OnboardingPage token={token} onComplete={() => setRoute('game')} />
|
<OnboardingPage
|
||||||
|
token={token}
|
||||||
|
operatorName={profile?.display_name ?? null}
|
||||||
|
onComplete={() => setRoute('game')}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{route === 'game' && token && (
|
{route === 'game' && token && (
|
||||||
<GamePage token={token} onLogout={() => { logout(); setRoute('login') }} />
|
<GamePage token={token} onLogout={() => { logout(); setRoute('login') }} />
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { NOVICE_LETTERS, MORSE } from '../lib/morse'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
mnemonics: Record<string, string>
|
||||||
|
operatorName: string | null
|
||||||
|
onStartPlaying: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function morseDisplay(pattern: string): string {
|
||||||
|
return pattern.split('').map(c => c === '.' ? '·' : '—').join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MnemonicCodebook({ mnemonics, operatorName, onStartPlaying }: Props) {
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: 680, margin: '0 auto' }}>
|
||||||
|
|
||||||
|
{/* Header — hidden when printing, replaced by print-only title */}
|
||||||
|
<div className="no-print" style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
|
||||||
|
<h1 style={{ fontFamily: 'var(--font-header)', fontSize: '1.8rem', marginBottom: '0.25rem' }}>
|
||||||
|
Your Codebook is Ready!
|
||||||
|
</h1>
|
||||||
|
<p style={{ color: 'var(--color-brown)' }}>
|
||||||
|
Print it out or save as PDF to keep with you during practice.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Print-only title */}
|
||||||
|
<div style={{ display: 'none' }} className="print-title">
|
||||||
|
<h1 style={{ fontFamily: 'var(--font-header)', textAlign: 'center', marginBottom: '0.25rem' }}>
|
||||||
|
Morse Code Codebook
|
||||||
|
</h1>
|
||||||
|
{operatorName && (
|
||||||
|
<p style={{ textAlign: 'center', marginBottom: '1rem', fontStyle: 'italic' }}>
|
||||||
|
Operator: {operatorName}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Letter grid */}
|
||||||
|
<div
|
||||||
|
className="codebook-grid"
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))',
|
||||||
|
gap: '0.75rem',
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{NOVICE_LETTERS.map(letter => {
|
||||||
|
const pattern = MORSE[letter] ?? ''
|
||||||
|
const phrase = mnemonics[letter]
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={letter}
|
||||||
|
className="codebook-card"
|
||||||
|
style={{
|
||||||
|
background: 'white',
|
||||||
|
border: '2px solid var(--color-gold)',
|
||||||
|
borderRadius: '0.4rem',
|
||||||
|
padding: '0.75rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Letter + pattern */}
|
||||||
|
<div style={{ display: 'flex', alignItems: 'baseline', gap: '0.6rem', marginBottom: '0.4rem' }}>
|
||||||
|
<span style={{
|
||||||
|
fontFamily: 'var(--font-header)',
|
||||||
|
fontSize: '2rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
color: 'var(--color-forest)',
|
||||||
|
lineHeight: 1,
|
||||||
|
}}>
|
||||||
|
{letter}
|
||||||
|
</span>
|
||||||
|
<span style={{
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: '1.1rem',
|
||||||
|
color: 'var(--color-gold)',
|
||||||
|
letterSpacing: '0.2em',
|
||||||
|
}}>
|
||||||
|
{morseDisplay(pattern)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mnemonic phrase */}
|
||||||
|
<p style={{
|
||||||
|
fontSize: '0.9rem',
|
||||||
|
color: phrase ? 'var(--color-forest)' : '#aaa',
|
||||||
|
fontStyle: phrase ? 'normal' : 'italic',
|
||||||
|
lineHeight: 1.3,
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}>
|
||||||
|
{phrase ?? 'skipped'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="no-print" style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center', flexWrap: 'wrap' }}>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost"
|
||||||
|
onClick={() => window.print()}
|
||||||
|
>
|
||||||
|
🖨 Print / Save PDF
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn"
|
||||||
|
onClick={onStartPlaying}
|
||||||
|
style={{ fontSize: '1.05rem' }}
|
||||||
|
>
|
||||||
|
Start Playing →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
@media print {
|
||||||
|
.print-title { display: block !important; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -61,3 +61,21 @@ input:focus { outline: 2px solid var(--color-gold); outline-offset: 1px; }
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Print styles */
|
||||||
|
@media print {
|
||||||
|
.no-print { display: none !important; }
|
||||||
|
body { background: white; }
|
||||||
|
.app-shell { max-width: 100%; padding: 0; }
|
||||||
|
.codebook-grid {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: repeat(3, 1fr) !important;
|
||||||
|
gap: 0.5rem !important;
|
||||||
|
}
|
||||||
|
.codebook-card {
|
||||||
|
border: 1px solid #999 !important;
|
||||||
|
background: white !important;
|
||||||
|
break-inside: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,20 +1,41 @@
|
|||||||
|
import { useState, useCallback } from 'react'
|
||||||
import { MnemonicBuilder } from '../components/MnemonicBuilder'
|
import { MnemonicBuilder } from '../components/MnemonicBuilder'
|
||||||
|
import { MnemonicCodebook } from '../components/MnemonicCodebook'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
token: string
|
token: string
|
||||||
|
operatorName: string | null
|
||||||
onComplete: () => void
|
onComplete: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OnboardingPage({ token, onComplete }: Props) {
|
export function OnboardingPage({ token, operatorName, onComplete }: Props) {
|
||||||
|
const [mnemonics, setMnemonics] = useState<Record<string, string> | null>(null)
|
||||||
|
|
||||||
|
const handleBuilderComplete = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/mnemonics', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
const data = await r.json()
|
||||||
|
setMnemonics(data)
|
||||||
|
} catch {
|
||||||
|
onComplete()
|
||||||
|
}
|
||||||
|
}, [token, onComplete])
|
||||||
|
|
||||||
|
if (mnemonics) {
|
||||||
|
return <MnemonicCodebook mnemonics={mnemonics} operatorName={operatorName} onStartPlaying={onComplete} />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 style={{ fontFamily: 'var(--font-header)', marginBottom: '0.5rem' }}>
|
<h1 style={{ fontFamily: 'var(--font-header)', marginBottom: '0.5rem' }}>
|
||||||
Build Your Mnemonics
|
Build Your Codebook
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ color: 'var(--color-brown)', marginBottom: '2rem' }}>
|
<p style={{ color: 'var(--color-brown)', marginBottom: '2rem' }}>
|
||||||
Create a memorable phrase for each Morse letter. Short words for dots, long words for dashes.
|
Create a memorable phrase for each Morse letter. Short words for dots, long words for dashes.
|
||||||
</p>
|
</p>
|
||||||
<MnemonicBuilder token={token} onComplete={onComplete} />
|
<MnemonicBuilder token={token} onComplete={handleBuilderComplete} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user