Thinking of ways to build out a presentation system

This commit is contained in:
2025-08-21 22:18:04 -07:00
parent 7992fcbd49
commit 30f264a6bb
14 changed files with 891 additions and 76 deletions

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
)}
</>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@@ -25,4 +25,16 @@ export const collections = {
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()),
}),
}),
};

View 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!

View File

@@ -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>

View 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>

View File

@@ -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 description = "Testing the new polygon background styles";
const allResources = (await getCollection("resources")).sort(
(a, b) => b.data.date.valueOf() - a.data.date.valueOf()
);
---
<ResourcesLayout title={title} description={description}>
</ResourcesLayout>
<ContentLayout
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>