Skip to content

Commit bd13be3

Browse files
ofriwclaude
andcommitted
Add PresentationMode components with reveal.js integration
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 47ced2c commit bd13be3

File tree

4 files changed

+808
-0
lines changed

4 files changed

+808
-0
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
.toggleButton {
2+
display: inline-flex;
3+
align-items: center;
4+
gap: 0.5rem;
5+
padding: 0.5rem 1rem;
6+
background: var(--ifm-color-primary);
7+
color: white;
8+
border: none;
9+
border-radius: 6px;
10+
font-size: 0.9rem;
11+
font-weight: 600;
12+
cursor: pointer;
13+
transition: all 0.2s ease;
14+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
15+
}
16+
17+
.toggleButton:hover:not(:disabled) {
18+
background: var(--ifm-color-primary-dark);
19+
transform: translateY(-1px);
20+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
21+
}
22+
23+
.toggleButton:active:not(:disabled) {
24+
transform: translateY(0);
25+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
26+
}
27+
28+
.toggleButton:disabled {
29+
opacity: 0.6;
30+
cursor: not-allowed;
31+
}
32+
33+
.icon {
34+
font-size: 1.2em;
35+
line-height: 1;
36+
}
37+
38+
.label {
39+
font-family: var(--ifm-font-family-base);
40+
}
41+
42+
.spinner {
43+
display: inline-block;
44+
animation: spin 1s linear infinite;
45+
}
46+
47+
@keyframes spin {
48+
from {
49+
transform: rotate(0deg);
50+
}
51+
to {
52+
transform: rotate(360deg);
53+
}
54+
}
55+
56+
.errorMessage {
57+
position: fixed;
58+
top: 50%;
59+
left: 50%;
60+
transform: translate(-50%, -50%);
61+
z-index: 9999;
62+
background: var(--ifm-background-color);
63+
border: 2px solid var(--ifm-color-danger);
64+
border-radius: 8px;
65+
padding: 2rem;
66+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
67+
max-width: 400px;
68+
text-align: center;
69+
}
70+
71+
.errorMessage p {
72+
color: var(--ifm-color-danger);
73+
margin-bottom: 1rem;
74+
}
75+
76+
.errorMessage button {
77+
padding: 0.5rem 1rem;
78+
background: var(--ifm-color-danger);
79+
color: white;
80+
border: none;
81+
border-radius: 4px;
82+
cursor: pointer;
83+
font-weight: 600;
84+
}
85+
86+
.errorMessage button:hover {
87+
background: var(--ifm-color-danger-dark);
88+
}
89+
90+
.loadingOverlay {
91+
position: fixed;
92+
top: 0;
93+
left: 0;
94+
right: 0;
95+
bottom: 0;
96+
z-index: 10000;
97+
background: #000;
98+
display: flex;
99+
align-items: center;
100+
justify-content: center;
101+
color: white;
102+
font-size: 1.5rem;
103+
font-weight: 600;
104+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import React, { useState, useEffect, useCallback, lazy, Suspense } from 'react';
2+
import useBaseUrl from '@docusaurus/useBaseUrl';
3+
import styles from './PresentationToggle.module.css';
4+
5+
// Lazy load RevealSlideshow to avoid bundling Reveal.js for all users
6+
const RevealSlideshow = lazy(() => import('./RevealSlideshow'));
7+
8+
interface PresentationToggleProps {
9+
lessonPath: string;
10+
}
11+
12+
export default function PresentationToggle({ lessonPath }: PresentationToggleProps) {
13+
const [isOpen, setIsOpen] = useState(false);
14+
const [presentation, setPresentation] = useState<any>(null);
15+
const [loading, setLoading] = useState(false);
16+
const [error, setError] = useState<string | null>(null);
17+
const [hasPresentation, setHasPresentation] = useState(false);
18+
const baseUrl = useBaseUrl('/');
19+
20+
// Check if presentation exists for this lesson
21+
useEffect(() => {
22+
const checkPresentation = async () => {
23+
try {
24+
const response = await fetch(`${baseUrl}presentations/manifest.json`);
25+
if (response.ok) {
26+
const manifest = await response.json();
27+
// Check if this lesson has a presentation
28+
setHasPresentation(!!manifest[lessonPath]);
29+
}
30+
} catch (err) {
31+
// Manifest doesn't exist yet, hide button
32+
setHasPresentation(false);
33+
}
34+
};
35+
36+
checkPresentation();
37+
}, [baseUrl, lessonPath]);
38+
39+
const loadPresentation = useCallback(async () => {
40+
setLoading(true);
41+
setError(null);
42+
43+
try {
44+
// Load manifest to get presentation URL
45+
const manifestResponse = await fetch(`${baseUrl}presentations/manifest.json`);
46+
if (!manifestResponse.ok) {
47+
throw new Error('Presentation manifest not found');
48+
}
49+
50+
const manifest = await manifestResponse.json();
51+
const lessonData = manifest[lessonPath];
52+
53+
if (!lessonData) {
54+
throw new Error('No presentation available for this lesson');
55+
}
56+
57+
// Load presentation data
58+
const presentationResponse = await fetch(`${baseUrl.replace(/\/$/, '')}${lessonData.presentationUrl}`);
59+
if (!presentationResponse.ok) {
60+
throw new Error('Failed to load presentation');
61+
}
62+
63+
const presentationData = await presentationResponse.json();
64+
setPresentation(presentationData);
65+
setIsOpen(true);
66+
} catch (err) {
67+
const errorMessage = err instanceof Error ? err.message : String(err);
68+
setError(errorMessage);
69+
console.error('Failed to load presentation:', err);
70+
} finally {
71+
setLoading(false);
72+
}
73+
}, [baseUrl, lessonPath]);
74+
75+
const handleToggle = useCallback(() => {
76+
if (isOpen) {
77+
setIsOpen(false);
78+
setPresentation(null);
79+
} else {
80+
loadPresentation();
81+
}
82+
}, [isOpen, loadPresentation]);
83+
84+
// Handle keyboard shortcut (Cmd/Ctrl + Shift + P)
85+
useEffect(() => {
86+
const handleKeyDown = (e: KeyboardEvent) => {
87+
// Cmd/Ctrl + Shift + P
88+
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'P') {
89+
e.preventDefault();
90+
handleToggle();
91+
}
92+
};
93+
94+
window.addEventListener('keydown', handleKeyDown);
95+
return () => window.removeEventListener('keydown', handleKeyDown);
96+
}, [handleToggle]);
97+
98+
// Don't show button if no presentation exists
99+
if (!hasPresentation) {
100+
return null;
101+
}
102+
103+
return (
104+
<>
105+
<button
106+
className={styles.toggleButton}
107+
onClick={handleToggle}
108+
disabled={loading}
109+
title="Toggle presentation mode (Cmd/Ctrl + Shift + P)"
110+
>
111+
{loading ? (
112+
<span className={styles.spinner}></span>
113+
) : (
114+
<span className={styles.icon}>🎭</span>
115+
)}
116+
<span className={styles.label}>
117+
{loading ? 'Loading...' : 'Present'}
118+
</span>
119+
</button>
120+
121+
{error && (
122+
<div className={styles.errorMessage}>
123+
<p>{error}</p>
124+
<button onClick={() => setError(null)}>Dismiss</button>
125+
</div>
126+
)}
127+
128+
{isOpen && presentation && (
129+
<Suspense fallback={<div className={styles.loadingOverlay}>Loading presentation...</div>}>
130+
<RevealSlideshow
131+
presentation={presentation}
132+
onClose={() => {
133+
setIsOpen(false);
134+
setPresentation(null);
135+
}}
136+
/>
137+
</Suspense>
138+
)}
139+
</>
140+
);
141+
}

0 commit comments

Comments
 (0)