mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 02:53:51 +00:00
Thinking of ways to build out a presentation system
This commit is contained in:
24
src/src/components/presentation/FadeIn.tsx
Normal file
24
src/src/components/presentation/FadeIn.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// src/components/presentation/FadeIn.tsx
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface FadeInProps {
|
||||||
|
delay?: number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FadeIn: React.FC<FadeInProps> = ({
|
||||||
|
delay = 0,
|
||||||
|
children,
|
||||||
|
className = ""
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-animate="fade-in"
|
||||||
|
style={delay ? { animationDelay: `${delay}ms` } : undefined}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
30
src/src/components/presentation/Highlight.tsx
Normal file
30
src/src/components/presentation/Highlight.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// src/components/presentation/Highlight.tsx
|
||||||
|
interface HighlightProps {
|
||||||
|
color?: 'yellow' | 'blue' | 'green' | 'red' | 'purple';
|
||||||
|
delay?: number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Highlight: React.FC<HighlightProps> = ({
|
||||||
|
color = 'yellow',
|
||||||
|
delay = 0,
|
||||||
|
children
|
||||||
|
}) => {
|
||||||
|
const colorClasses = {
|
||||||
|
yellow: 'bg-yellow-bright/20 text-yellow-bright',
|
||||||
|
blue: 'bg-blue-bright/20 text-blue-bright',
|
||||||
|
green: 'bg-green-bright/20 text-green-bright',
|
||||||
|
red: 'bg-red-bright/20 text-red-bright',
|
||||||
|
purple: 'bg-purple-bright/20 text-purple-bright',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-animate="highlight"
|
||||||
|
style={delay ? { animationDelay: `${delay}ms` } : undefined}
|
||||||
|
className={`px-2 py-1 rounded ${colorClasses[color]}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
36
src/src/components/presentation/ImageWithCaption.tsx
Normal file
36
src/src/components/presentation/ImageWithCaption.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// src/components/presentation/ImageWithCaption.tsx
|
||||||
|
interface ImageWithCaptionProps {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
caption?: string;
|
||||||
|
width?: string;
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageWithCaption: React.FC<ImageWithCaptionProps> = ({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
caption,
|
||||||
|
width = "100%",
|
||||||
|
delay = 0
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<figure
|
||||||
|
data-animate="fade-in"
|
||||||
|
style={delay ? { animationDelay: `${delay}ms` } : undefined}
|
||||||
|
className="text-center my-8"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
style={{ width, maxWidth: '100%' }}
|
||||||
|
className="rounded-lg shadow-lg mx-auto"
|
||||||
|
/>
|
||||||
|
{caption && (
|
||||||
|
<figcaption className="mt-4 text-sm italic text-gray-400">
|
||||||
|
{caption}
|
||||||
|
</figcaption>
|
||||||
|
)}
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
};
|
||||||
386
src/src/components/presentation/Presentation.tsx
Normal file
386
src/src/components/presentation/Presentation.tsx
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
// src/components/presentation/Presentation.tsx
|
||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
|
||||||
|
interface PresentationProps {
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Presentation: React.FC<PresentationProps> = ({
|
||||||
|
title = "Start Presentation"
|
||||||
|
}) => {
|
||||||
|
const [slides, setSlides] = useState<string[]>([]);
|
||||||
|
const [currentSlide, setCurrentSlide] = useState(0);
|
||||||
|
const [isPresenting, setIsPresenting] = useState(false);
|
||||||
|
const [isBlackedOut, setIsBlackedOut] = useState(false);
|
||||||
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
|
const [animationIndex, setAnimationIndex] = useState(0);
|
||||||
|
const [currentAnimations, setCurrentAnimations] = useState<Element[]>([]);
|
||||||
|
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Initialize slides when component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
const initializeSlides = () => {
|
||||||
|
const slideElements = Array.from(document.querySelectorAll('.presentation-slide'));
|
||||||
|
console.log('Found slides:', slideElements.length);
|
||||||
|
|
||||||
|
const slideHTML = slideElements.map(el => el.outerHTML);
|
||||||
|
setSlides(slideHTML);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run after a short delay to ensure all components are rendered
|
||||||
|
const timer = setTimeout(initializeSlides, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Setup slide animations
|
||||||
|
const setupSlideAnimations = () => {
|
||||||
|
if (!contentRef.current) return;
|
||||||
|
|
||||||
|
const animations = Array.from(contentRef.current.querySelectorAll('[data-animate]'));
|
||||||
|
setCurrentAnimations(animations);
|
||||||
|
setAnimationIndex(0);
|
||||||
|
|
||||||
|
// Hide all animated elements initially
|
||||||
|
animations.forEach(el => {
|
||||||
|
const element = el as HTMLElement;
|
||||||
|
element.style.opacity = '0';
|
||||||
|
element.style.transform = 'translateY(20px)';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show next animation
|
||||||
|
const showNextAnimation = (): boolean => {
|
||||||
|
if (animationIndex < currentAnimations.length) {
|
||||||
|
const element = currentAnimations[animationIndex] as HTMLElement;
|
||||||
|
element.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
||||||
|
element.style.opacity = '1';
|
||||||
|
element.style.transform = 'translateY(0)';
|
||||||
|
setAnimationIndex(prev => prev + 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show previous animation
|
||||||
|
const showPrevAnimation = (): boolean => {
|
||||||
|
if (animationIndex > 0) {
|
||||||
|
const newIndex = animationIndex - 1;
|
||||||
|
const element = currentAnimations[newIndex] as HTMLElement;
|
||||||
|
element.style.opacity = '0';
|
||||||
|
element.style.transform = 'translateY(20px)';
|
||||||
|
setAnimationIndex(newIndex);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display slide
|
||||||
|
const displaySlide = (index: number) => {
|
||||||
|
if (index >= 0 && index < slides.length && contentRef.current) {
|
||||||
|
contentRef.current.innerHTML = slides[index];
|
||||||
|
setCurrentSlide(index);
|
||||||
|
setupSlideAnimations();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Navigation functions
|
||||||
|
const nextSlide = () => {
|
||||||
|
if (showNextAnimation()) return;
|
||||||
|
|
||||||
|
if (currentSlide < slides.length - 1) {
|
||||||
|
displaySlide(currentSlide + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevSlide = () => {
|
||||||
|
if (showPrevAnimation()) return;
|
||||||
|
|
||||||
|
if (currentSlide > 0) {
|
||||||
|
displaySlide(currentSlide - 1);
|
||||||
|
// Show all animations on previous slide
|
||||||
|
setTimeout(() => {
|
||||||
|
currentAnimations.forEach(el => {
|
||||||
|
const element = el as HTMLElement;
|
||||||
|
element.style.opacity = '1';
|
||||||
|
element.style.transform = 'translateY(0)';
|
||||||
|
});
|
||||||
|
setAnimationIndex(currentAnimations.length);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start presentation
|
||||||
|
const startPresentation = () => {
|
||||||
|
setIsPresenting(true);
|
||||||
|
displaySlide(0);
|
||||||
|
document.body.classList.add('presentation-active');
|
||||||
|
};
|
||||||
|
|
||||||
|
// End presentation
|
||||||
|
const endPresentation = () => {
|
||||||
|
setIsPresenting(false);
|
||||||
|
setIsBlackedOut(false);
|
||||||
|
setShowHelp(false);
|
||||||
|
document.body.classList.remove('presentation-active');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Keyboard controls
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyPress = (event: KeyboardEvent) => {
|
||||||
|
if (!isPresenting) return;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Escape':
|
||||||
|
endPresentation();
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
case ' ':
|
||||||
|
event.preventDefault();
|
||||||
|
nextSlide();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
event.preventDefault();
|
||||||
|
prevSlide();
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
case 'B':
|
||||||
|
event.preventDefault();
|
||||||
|
setIsBlackedOut(prev => !prev);
|
||||||
|
break;
|
||||||
|
case '?':
|
||||||
|
event.preventDefault();
|
||||||
|
setShowHelp(prev => !prev);
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
event.preventDefault();
|
||||||
|
displaySlide(0);
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
event.preventDefault();
|
||||||
|
displaySlide(slides.length - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyPress);
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyPress);
|
||||||
|
}, [isPresenting, currentSlide, animationIndex, currentAnimations, slides.length]);
|
||||||
|
|
||||||
|
const progress = slides.length > 0 ? ((currentSlide + 1) / slides.length) * 100 : 0;
|
||||||
|
|
||||||
|
if (slides.length === 0) {
|
||||||
|
return null; // Don't show button if no slides
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Start Presentation Button */}
|
||||||
|
<button
|
||||||
|
onClick={startPresentation}
|
||||||
|
className="mb-6 px-4 py-2 bg-blue-bright text-background rounded-lg hover:bg-blue transition-colors font-medium"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Presentation Container */}
|
||||||
|
{isPresenting && (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="fixed inset-0 z-[9999] bg-black text-foreground flex flex-col"
|
||||||
|
>
|
||||||
|
{/* Main Content */}
|
||||||
|
<div
|
||||||
|
ref={contentRef}
|
||||||
|
className="flex-1 flex items-center justify-center p-8 overflow-auto"
|
||||||
|
>
|
||||||
|
<div className="text-4xl text-center text-yellow-bright">
|
||||||
|
Loading slide...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Slide Counter */}
|
||||||
|
<div className="fixed bottom-8 right-8 bg-gray-800 bg-opacity-80 text-foreground px-4 py-2 rounded-lg font-mono">
|
||||||
|
{currentSlide + 1} / {slides.length}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Help Toggle */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowHelp(prev => !prev)}
|
||||||
|
className="fixed bottom-8 left-8 bg-gray-800 bg-opacity-80 text-blue-bright border border-gray-600 px-4 py-2 rounded-lg font-mono hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="fixed bottom-0 left-0 h-1 bg-yellow-bright transition-all duration-300"
|
||||||
|
style={{ width: `${progress}%` }} />
|
||||||
|
|
||||||
|
{/* Blackout Overlay */}
|
||||||
|
{isBlackedOut && (
|
||||||
|
<div className="fixed inset-0 bg-black z-[10000] transition-opacity duration-300" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Help Overlay */}
|
||||||
|
{showHelp && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-[10001]">
|
||||||
|
<div className="bg-gray-800 p-8 rounded-lg max-w-md">
|
||||||
|
<h3 className="text-xl font-bold text-yellow-bright mb-6 text-center">
|
||||||
|
🎮 Presentation Controls
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<kbd className="bg-gray-900 px-2 py-1 rounded text-blue-bright font-mono text-sm">→</kbd>
|
||||||
|
<kbd className="bg-gray-900 px-2 py-1 rounded text-blue-bright font-mono text-sm">Space</kbd>
|
||||||
|
<span>Next slide/animation</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<kbd className="bg-gray-900 px-2 py-1 rounded text-blue-bright font-mono text-sm">←</kbd>
|
||||||
|
<span>Previous slide/animation</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<kbd className="bg-gray-900 px-2 py-1 rounded text-blue-bright font-mono text-sm">B</kbd>
|
||||||
|
<span>Toggle blackout</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<kbd className="bg-gray-900 px-2 py-1 rounded text-blue-bright font-mono text-sm">Home</kbd>
|
||||||
|
<span>First slide</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<kbd className="bg-gray-900 px-2 py-1 rounded text-blue-bright font-mono text-sm">End</kbd>
|
||||||
|
<span>Last slide</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<kbd className="bg-gray-900 px-2 py-1 rounded text-blue-bright font-mono text-sm">Esc</kbd>
|
||||||
|
<span>Exit presentation</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<kbd className="bg-gray-900 px-2 py-1 rounded text-blue-bright font-mono text-sm">?</kbd>
|
||||||
|
<span>Toggle this help</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Global styles for presentation mode */}
|
||||||
|
{isPresenting && (
|
||||||
|
<style jsx global>{`
|
||||||
|
.presentation-active {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-active > *:not(.fixed) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-slide {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-slide.centered {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-title-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 2rem;
|
||||||
|
left: 2rem;
|
||||||
|
right: 2rem;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-slide h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #fabd2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-slide h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #83a598;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-slide h3 {
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: #b8bb26;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-slide p {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-slide ul, .presentation-slide ol {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-slide li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-slide code {
|
||||||
|
background: #282828;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-family: 'Comic Code', monospace;
|
||||||
|
color: #d3869b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-slide pre {
|
||||||
|
background: #282828;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presentation-slide pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
color: #ebdbb2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-animate="fade-in"] {
|
||||||
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.presentation-slide h1 { font-size: 2rem; }
|
||||||
|
.presentation-slide h2 { font-size: 1.75rem; }
|
||||||
|
.presentation-slide h3 { font-size: 1.5rem; }
|
||||||
|
.presentation-slide p { font-size: 1.25rem; }
|
||||||
|
.presentation-slide ul, .presentation-slide ol { font-size: 1.1rem; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
34
src/src/components/presentation/Slide.tsx
Normal file
34
src/src/components/presentation/Slide.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// src/components/presentation/Slide.tsx
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface SlideProps {
|
||||||
|
title?: string;
|
||||||
|
centered?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Slide: React.FC<SlideProps> = ({
|
||||||
|
title,
|
||||||
|
centered = false,
|
||||||
|
children,
|
||||||
|
className = ""
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={`presentation-slide ${centered ? 'centered' : ''} ${className}`}
|
||||||
|
data-title={title}
|
||||||
|
>
|
||||||
|
{title && (
|
||||||
|
<div className="slide-title-overlay">
|
||||||
|
<h1 className="text-4xl md:text-6xl font-bold text-yellow-bright mb-8">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="slide-content">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
25
src/src/components/presentation/SlideIn.tsx
Normal file
25
src/src/components/presentation/SlideIn.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// src/components/presentation/SlideIn.tsx
|
||||||
|
interface SlideInProps {
|
||||||
|
direction?: 'left' | 'right' | 'up' | 'down';
|
||||||
|
delay?: number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SlideIn: React.FC<SlideInProps> = ({
|
||||||
|
direction = 'up',
|
||||||
|
delay = 0,
|
||||||
|
children,
|
||||||
|
className = ""
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-animate="slide-in"
|
||||||
|
data-direction={direction}
|
||||||
|
style={delay ? { animationDelay: `${delay}ms` } : undefined}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
33
src/src/components/presentation/TwoColumn.tsx
Normal file
33
src/src/components/presentation/TwoColumn.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// src/components/presentation/TwoColumn.tsx
|
||||||
|
interface TwoColumnProps {
|
||||||
|
leftWidth?: string;
|
||||||
|
rightWidth?: string;
|
||||||
|
gap?: string;
|
||||||
|
left: React.ReactNode;
|
||||||
|
right: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TwoColumn: React.FC<TwoColumnProps> = ({
|
||||||
|
leftWidth = "1fr",
|
||||||
|
rightWidth = "1fr",
|
||||||
|
gap = "2rem",
|
||||||
|
left,
|
||||||
|
right
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="grid items-start w-full md:grid-cols-2 grid-cols-1"
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `${leftWidth} ${rightWidth}`,
|
||||||
|
gap: gap
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="left-column">
|
||||||
|
{left}
|
||||||
|
</div>
|
||||||
|
<div className="right-column">
|
||||||
|
{right}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -25,4 +25,16 @@ export const collections = {
|
|||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
resources: defineCollection({
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
date: z.coerce.date().transform((date) => {
|
||||||
|
return new Date(date.setUTCHours(12, 0, 0, 0));
|
||||||
|
}),
|
||||||
|
duration: z.string(),
|
||||||
|
image: z.string().optional(),
|
||||||
|
tags: z.array(z.string()),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
181
src/src/content/resources/curriculum/python/intro-to-python.mdx
Normal file
181
src/src/content/resources/curriculum/python/intro-to-python.mdx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
---
|
||||||
|
title: "Introduction to Python Programming"
|
||||||
|
description: "A comprehensive introduction to Python programming fundamentals for beginners"
|
||||||
|
date: 2025-01-20
|
||||||
|
duration: "2 hours"
|
||||||
|
tags: ["python", "programming", "beginner", "fundamentals"]
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Slide } from "@/components/presentation/Slide";
|
||||||
|
import { FadeIn } from "@/components/presentation/FadeIn";
|
||||||
|
import { Highlight } from "@/components/presentation/Highlight";
|
||||||
|
import { TwoColumn } from "@/components/presentation/TwoColumn";
|
||||||
|
|
||||||
|
<Slide title="Introduction to Python Programming" centered>
|
||||||
|
|
||||||
|
# Welcome to Python! 🐍
|
||||||
|
|
||||||
|
**A beginner-friendly programming language**
|
||||||
|
|
||||||
|
<FadeIn delay={500}>
|
||||||
|
|
||||||
|
- Easy to learn and read
|
||||||
|
- Powerful and versatile
|
||||||
|
- Great for beginners
|
||||||
|
- Used by major companies
|
||||||
|
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
</Slide>
|
||||||
|
|
||||||
|
<Slide title="What is Python?">
|
||||||
|
|
||||||
|
## What is Python?
|
||||||
|
|
||||||
|
<FadeIn>
|
||||||
|
|
||||||
|
Python is a <Highlight color="yellow">high-level programming language</Highlight> that emphasizes:
|
||||||
|
|
||||||
|
- **Readability** - Code that looks like English
|
||||||
|
- **Simplicity** - Easy to learn and use
|
||||||
|
- **Versatility** - Can be used for many things
|
||||||
|
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
<FadeIn delay={600}>
|
||||||
|
|
||||||
|
### Created by Guido van Rossum in 1991
|
||||||
|
|
||||||
|
Named after "Monty Python's Flying Circus" 🎭
|
||||||
|
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
</Slide>
|
||||||
|
|
||||||
|
<Slide title="Why Learn Python?">
|
||||||
|
|
||||||
|
## Why Learn Python?
|
||||||
|
|
||||||
|
<TwoColumn
|
||||||
|
left={
|
||||||
|
<FadeIn>
|
||||||
|
<h3>🌐 Web Development</h3>
|
||||||
|
<p>Build websites and web applications</p>
|
||||||
|
|
||||||
|
<h3>🤖 Data Science & AI</h3>
|
||||||
|
<p>Analyze data and build machine learning models</p>
|
||||||
|
</FadeIn>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<FadeIn delay={300}>
|
||||||
|
<h3>🎮 Game Development</h3>
|
||||||
|
<p>Create games and interactive applications</p>
|
||||||
|
|
||||||
|
<h3>🔧 Automation</h3>
|
||||||
|
<p>Automate repetitive tasks</p>
|
||||||
|
</FadeIn>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Slide>
|
||||||
|
|
||||||
|
<Slide title="Your First Python Program">
|
||||||
|
|
||||||
|
## Your First Python Program
|
||||||
|
|
||||||
|
<FadeIn>
|
||||||
|
|
||||||
|
Let's write the classic "Hello, World!" program:
|
||||||
|
|
||||||
|
```python
|
||||||
|
print("Hello, World!")
|
||||||
|
```
|
||||||
|
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
<FadeIn delay={500}>
|
||||||
|
|
||||||
|
### How to run it:
|
||||||
|
1. Open a text editor
|
||||||
|
2. Type the code above
|
||||||
|
3. Save as `hello.py`
|
||||||
|
4. Run with: `python hello.py`
|
||||||
|
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
</Slide>
|
||||||
|
|
||||||
|
<Slide title="Variables and Data Types">
|
||||||
|
|
||||||
|
## Variables and Data Types
|
||||||
|
|
||||||
|
<FadeIn>
|
||||||
|
|
||||||
|
### Creating Variables
|
||||||
|
```python
|
||||||
|
name = "Alice" # String
|
||||||
|
age = 25 # Integer
|
||||||
|
height = 5.6 # Float
|
||||||
|
is_student = True # Boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
<FadeIn delay={400}>
|
||||||
|
|
||||||
|
### Python is <Highlight color="green">dynamically typed</Highlight>
|
||||||
|
You don't need to declare the type!
|
||||||
|
|
||||||
|
```python
|
||||||
|
x = 42 # x is an integer
|
||||||
|
x = "Hello" # Now x is a string
|
||||||
|
```
|
||||||
|
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
</Slide>
|
||||||
|
|
||||||
|
<Slide title="Next Steps" centered>
|
||||||
|
|
||||||
|
## 🎉 Congratulations!
|
||||||
|
|
||||||
|
<FadeIn>
|
||||||
|
|
||||||
|
You've learned the basics of Python programming!
|
||||||
|
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
<FadeIn delay={400}>
|
||||||
|
|
||||||
|
### What's Next?
|
||||||
|
- Practice with small projects
|
||||||
|
- Learn about modules and packages
|
||||||
|
- Explore Python libraries
|
||||||
|
- Build something cool!
|
||||||
|
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
<FadeIn delay={800}>
|
||||||
|
|
||||||
|
**Ready to code? Let's build something amazing! 🚀**
|
||||||
|
|
||||||
|
</FadeIn>
|
||||||
|
|
||||||
|
</Slide>
|
||||||
|
|
||||||
|
## Course Materials
|
||||||
|
|
||||||
|
This curriculum covers the fundamentals of Python programming in an interactive, hands-on format. Students will work through practical examples and complete coding exercises to reinforce their learning.
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
Before we dive into the presentation slides above, let's set up our development environment and understand what we'll be covering in this course.
|
||||||
|
|
||||||
|
#### What You'll Need
|
||||||
|
|
||||||
|
- A computer with Python installed
|
||||||
|
- A text editor or IDE (like VS Code)
|
||||||
|
- Terminal/command prompt access
|
||||||
|
- About 2 hours of focused time
|
||||||
|
|
||||||
|
Ready to start? Click the "Start Presentation" button above to begin with the interactive slides!
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
---
|
|
||||||
import "@/style/globals.css";
|
|
||||||
import { ClientRouter } from "astro:transitions";
|
|
||||||
|
|
||||||
import Header from "@/components/header";
|
|
||||||
import Footer from "@/components/footer";
|
|
||||||
import Background from "@/components/background";
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { title, description } = Astro.props;
|
|
||||||
const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
|
||||||
---
|
|
||||||
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<title>{title}</title>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<!-- OpenGraph -->
|
|
||||||
<meta property="og:image" content={ogImage} />
|
|
||||||
<meta property="og:image:width" content="1200" />
|
|
||||||
<meta property="og:image:height" content="630" />
|
|
||||||
<!-- Twitter -->
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta name="twitter:image" content={ogImage} />
|
|
||||||
<meta name="twitter:description" content={description} />
|
|
||||||
<!-- Basic meta description for search engines -->
|
|
||||||
<meta name="description" content={description} />
|
|
||||||
<!-- Also used in OpenGraph for social media sharing -->
|
|
||||||
<meta property="og:description" content={description} />
|
|
||||||
<link rel="icon" type="image/jpeg" href="/me.jpeg" />
|
|
||||||
<ClientRouter
|
|
||||||
defaultTransition={false}
|
|
||||||
handleFocus={false}
|
|
||||||
/>
|
|
||||||
<style>
|
|
||||||
::view-transition-new(:root) {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
::view-transition-old(:root) {
|
|
||||||
animation: 90ms ease-out both fade-out;
|
|
||||||
}
|
|
||||||
@keyframes fade-out {
|
|
||||||
from { opacity: 1; }
|
|
||||||
to { opacity: 0; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body class="bg-background text-foreground min-h-screen flex flex-col">
|
|
||||||
<main class="flex-1 flex flex-col">
|
|
||||||
<div class="max-w-5xl mx-auto pt-12 px-4 py-8 flex-1">
|
|
||||||
<Background layout="content" position="right" client:only="react" transition:persist />
|
|
||||||
<div>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
<Background layout="content" position="left" client:only="react" transition:persist />
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<script>
|
|
||||||
document.addEventListener("astro:after-navigation", () => {
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
73
src/src/pages/resources/[...slug].astro
Normal file
73
src/src/pages/resources/[...slug].astro
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import ContentLayout from "@/layouts/content.astro";
|
||||||
|
import { Presentation } from "@/components/presentation/Presentation";
|
||||||
|
|
||||||
|
const { slug } = Astro.params;
|
||||||
|
|
||||||
|
// Same pattern as your blog
|
||||||
|
const resources = await getCollection("resources");
|
||||||
|
const resource = resources.find(item => item.slug === slug);
|
||||||
|
|
||||||
|
if (!resource) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 404,
|
||||||
|
statusText: 'Not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Content } = await resource.render();
|
||||||
|
|
||||||
|
const formattedDate = new Date(resource.data.date).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric"
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<ContentLayout
|
||||||
|
title={`${resource.data.title} | Timothy Pidashev`}
|
||||||
|
description={resource.data.description}
|
||||||
|
>
|
||||||
|
<Presentation title="Start Presentation" />
|
||||||
|
|
||||||
|
<div class="relative max-w-8xl mx-auto">
|
||||||
|
<article class="prose prose-invert prose-lg mx-auto max-w-4xl">
|
||||||
|
|
||||||
|
{resource.data.image && (
|
||||||
|
<div class="-mx-4 sm:mx-0 mb-8">
|
||||||
|
<img
|
||||||
|
src={resource.data.image}
|
||||||
|
alt={resource.data.title}
|
||||||
|
class="w-full h-auto rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h1 class="text-3xl pt-4">{resource.data.title}</h1>
|
||||||
|
<p class="lg:text-2xl sm:text-lg">{resource.data.description}</p>
|
||||||
|
|
||||||
|
<div class="mt-4 md:mt-6">
|
||||||
|
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
|
||||||
|
<span class="text-purple">{resource.data.duration}</span>
|
||||||
|
<span class="text-foreground/50">•</span>
|
||||||
|
<time dateTime={resource.data.date instanceof Date ? resource.data.date.toISOString() : resource.data.date} class="text-blue">
|
||||||
|
{formattedDate}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2 mt-4 md:mt-6">
|
||||||
|
{resource.data.tags.map((tag) => (
|
||||||
|
<span class="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200">
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="prose prose-invert prose-lg max-w-none">
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</ContentLayout>
|
||||||
@@ -1,9 +1,61 @@
|
|||||||
---
|
---
|
||||||
import ResourcesLayout from "@/layouts/resources.astro";
|
// src/pages/resources/index.astro - SIMPLE LISTING
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import ContentLayout from "@/layouts/content.astro";
|
||||||
|
|
||||||
const title = "Resources Test";
|
const allResources = (await getCollection("resources")).sort(
|
||||||
const description = "Testing the new polygon background styles";
|
(a, b) => b.data.date.valueOf() - a.data.date.valueOf()
|
||||||
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<ResourcesLayout title={title} description={description}>
|
<ContentLayout
|
||||||
</ResourcesLayout>
|
title="Resources | Timothy Pidashev"
|
||||||
|
description="Educational resources, curriculum materials, and presentation slides."
|
||||||
|
>
|
||||||
|
<div class="max-w-6xl mx-auto pt-24 sm:pt-32 px-4">
|
||||||
|
<h1 class="text-2xl sm:text-3xl font-bold text-purple mb-12 text-center leading-relaxed">
|
||||||
|
Educational Resources <br className="sm:hidden" />
|
||||||
|
& Materials
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{allResources.map((resource) => (
|
||||||
|
<a
|
||||||
|
href={`/resources/${resource.slug}`}
|
||||||
|
class="group block p-6 bg-background border border-foreground/20 rounded-lg hover:border-purple-bright/50 transition-all duration-300"
|
||||||
|
>
|
||||||
|
{resource.data.image && (
|
||||||
|
<div class="aspect-video w-full mb-4 rounded-md overflow-hidden bg-foreground/5">
|
||||||
|
<img
|
||||||
|
src={resource.data.image}
|
||||||
|
alt={resource.data.title}
|
||||||
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h3 class="text-xl font-bold text-foreground/90 group-hover:text-purple-bright transition-colors mb-2">
|
||||||
|
{resource.data.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p class="text-foreground/70 mb-4 line-clamp-3">
|
||||||
|
{resource.data.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4 text-sm text-foreground/60 mb-4">
|
||||||
|
<span>⏱️ {resource.data.duration}</span>
|
||||||
|
<span>📅 {resource.data.date.toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{resource.data.tags.map((tag) => (
|
||||||
|
<span class="text-xs px-2 py-1 bg-foreground/10 text-foreground/70 rounded">
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ContentLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user