This commit is contained in:
2025-08-22 23:08:39 -07:00
parent 9496030d41
commit 1758dc3153
7 changed files with 385 additions and 412 deletions

View File

@@ -1,373 +0,0 @@
---
// src/components/presentation/Presentation.astro
export interface Props {
autoStart?: boolean;
title?: string;
}
const { autoStart = false, title = "Start Presentation" } = Astro.props;
---
<button id="presentation-button" class="presentation-hidden mb-6 px-4 py-2 bg-blue-bright text-background rounded-lg hover:bg-blue transition-colors font-medium" type="button">
{title}
</button>
<div class="presentation-progress"></div>
<script define:vars={{ autoStart }}>
const button = document.getElementById("presentation-button");
let slides = [];
let slide = 0;
let presenting = false;
// Function to initialize slides
const initSlides = () => {
const slideElements = Array.from(document.querySelectorAll('.presentation-slide'));
console.log('Found slides:', slideElements.length); // Debug log
slides = slideElements.map((el) => el.outerHTML);
// Show button if we have slides and not auto-starting
if (slides.length && !autoStart) {
button.classList.remove("presentation-hidden");
}
// Auto-start if enabled and we have slides
if (autoStart && slides.length) {
setTimeout(() => {
startPresentation();
}, 100);
}
};
const nextSlide = () => {
if (slide === slides.length - 1) {
return slide;
}
return slide + 1;
};
const prevSlide = () => {
if (slide === 0) {
return slide;
}
return slide - 1;
};
const keyHandlers = {
ArrowRight: nextSlide,
ArrowLeft: prevSlide,
};
const setProgress = () => {
const progress = ((slide + 1) / slides.length) * 100;
document.body.style.setProperty('--presentation-progress', `${progress}%`);
};
const startPresentation = () => {
if (!slides.length) return;
button.innerHTML = "Resume presentation";
document.body.classList.add("presentation-overflow-hidden");
presenting = true;
// Create presentation container and content area
const container = document.createElement('div');
container.id = 'presentation-container';
container.className = 'presentation-container';
const content = document.createElement('main');
content.id = 'presentation-content';
// Initialize with first slide
const slideWrapper = document.createElement('div');
slideWrapper.innerHTML = slides[slide];
const slideElement = slideWrapper.querySelector('.presentation-slide');
if (slideElement) {
content.appendChild(slideElement);
} else {
content.innerHTML = slides[slide];
}
// Add slide counter
const counter = document.createElement('div');
counter.id = 'slide-counter';
counter.className = 'fixed bottom-8 right-8 bg-gray-800 bg-opacity-80 text-foreground px-4 py-2 rounded-lg font-mono z-20';
counter.textContent = `${slide + 1} / ${slides.length}`;
container.appendChild(content);
container.appendChild(counter);
document.body.appendChild(container);
setProgress();
initListeners();
console.log(`Presentation started with ${slides.length} slides`); // Debug log
};
const endPresentation = () => {
document.body.classList.remove("presentation-overflow-hidden");
presenting = false;
const container = document.getElementById('presentation-container');
if (container) {
container.remove();
}
};
const transition = (nextSlideIndex) => {
if (!presenting || nextSlideIndex === slide || !slides[nextSlideIndex]) {
return;
}
slide = nextSlideIndex;
const content = document.getElementById('presentation-content');
const counter = document.getElementById('slide-counter');
if (content) {
// Clear current content
content.innerHTML = '';
// Create a wrapper div and set the slide content
const slideWrapper = document.createElement('div');
slideWrapper.innerHTML = slides[slide];
// Ensure the slide has proper presentation styling
const slideElement = slideWrapper.querySelector('.presentation-slide');
if (slideElement) {
content.appendChild(slideElement);
} else {
// Fallback: just add the content directly
content.innerHTML = slides[slide];
}
}
if (counter) {
counter.textContent = `${slide + 1} / ${slides.length}`;
}
setProgress();
console.log(`Transitioned to slide ${slide + 1}/${slides.length}`); // Debug log
};
let listenersInitialized = false;
const initListeners = () => {
if (listenersInitialized) {
return;
}
listenersInitialized = true;
window.addEventListener("keyup", (ev) => {
console.log(`Key pressed: ${ev.key}`); // Debug log
ev.preventDefault();
const isEscape = ev.key === "Escape";
if (isEscape) {
endPresentation();
return;
}
const getSlide = keyHandlers[ev.key];
if (!getSlide) {
return;
}
const nextSlideIndex = getSlide();
console.log(`Current slide: ${slide}, Next slide: ${nextSlideIndex}`); // Debug log
transition(nextSlideIndex);
});
let touchstartX = 0;
let touchendX = 0;
const handleGesture = () => {
const magnitude = Math.abs(touchstartX - touchendX);
if (magnitude < 40) {
return;
}
if (touchendX < touchstartX) {
transition(nextSlide());
}
if (touchendX > touchstartX) {
transition(prevSlide());
}
};
document.addEventListener("touchstart", (ev) => {
touchstartX = ev.changedTouches[0].screenX;
}, false);
document.addEventListener("touchend", (event) => {
touchendX = event.changedTouches[0].screenX;
handleGesture();
}, false);
};
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initSlides);
} else {
// DOM is already loaded
initSlides();
}
// Also try again after a short delay to catch any dynamically loaded content
setTimeout(initSlides, 500);
// Initialize button click handler
if (button) {
button.addEventListener("click", startPresentation);
}
</script>
<style is:global>
.presentation-progress {
display: none;
}
.presentation-overflow-hidden {
overflow: hidden;
.presentation-hidden {
display: none;
}
.presentation-progress {
transition: width 1000ms;
display: block;
position: fixed;
z-index: 21;
top: 0px;
left: 0px;
width: var(--presentation-progress);
height: 0.25rem;
background: #fabd2f;
}
}
.presentation-container {
z-index: 10;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
background-color: #000000;
}
#presentation-content {
display: flex;
flex-direction: column;
background-color: #000000;
color: #ebdbb2;
box-sizing: border-box;
min-height: 100vh;
width: 100%;
padding: 4rem;
}
.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;
}
.presentation-slide.highlight {
background-color: #fabd2f;
color: #000000;
}
.presentation-slide.large {
font-size: x-large;
}
.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;
}
@media (max-width: 768px) {
#presentation-content {
padding: 2rem;
}
.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; }
}
.presentation-hidden {
display: none;
}
</style>

View File

@@ -0,0 +1,326 @@
<button id="presentation-button" class="presentation-hidden" type="button"
>Start Presentation</button
>
<div class="presentation-progress"></div>
<script>
const button = document.getElementById(
"presentation-button"
) as HTMLButtonElement;
let slides = Array.from(document.querySelectorAll(".presentation-slide"));
let slide = 0;
let presenter = false;
const presentationId = window.location.href;
const nextSlide = () => {
if (slide === slides.length - 1) {
return slide;
}
return slide + 1;
};
const prevSlide = () => {
if (slide === 0) {
return slide;
}
return slide - 1;
};
const nextClass = "presentation-next";
const currClass = "presentation-current";
const prevClass = "presentation-prev";
const transitionClasses = [nextClass, currClass, prevClass];
const keyHandlers: Record<string, () => number> = {
ArrowRight: nextSlide,
ArrowLeft: prevSlide,
};
const displaySlides = () => {
for (let i = 0; i < slides.length; i++) {
slides[i].classList.remove("active", "inactive", ...transitionClasses);
if (i === slide) {
slides[i].classList.add("active", currClass);
} else {
slides[i].classList.add("inactive");
if (i > slide) {
slides[i].classList.add(nextClass);
} else {
slides[i].classList.add(prevClass);
}
}
}
};
let presenting = false
const startPresentation = () => {
button.innerHTML = "Resume presentation";
document.body.classList.add("presentation-overflow-hidden");
presenting = true
displaySlides();
setProgress();
initListeners()
};
const endPresentation = () => {
document.body.classList.remove("presentation-overflow-hidden");
presenting = false
slides.map((s) =>
s.classList.remove("active", "inactive", ...transitionClasses)
);
};
const setPresenter = () => {
presenter = true;
document.body.classList.add("presentation-presenter")
};
const setProgress = () => {
const progress = ((slide+1)/slides.length)*100;
document.body.style.setProperty('--presentation-progress', `${progress}%`)
}
const transition = (nextSlide: number) => {
if (!presenting) {
return
}
if (slide === nextSlide) {
return;
}
slides.forEach((s) => s.classList.remove(...transitionClasses));
slide = nextSlide;
displaySlides();
setProgress();
};
let listenersInitialized = false
const initListeners = () => {
if (listenersInitialized) {
return
}
listenersInitialized= true
window.addEventListener("keyup", (ev) => {
ev.preventDefault();
const isEscape = ev.key === "Escape";
if (isEscape) {
endPresentation();
return;
}
const isSpace = ev.key === " ";
if (isSpace) {
setPresenter();
return;
}
const getSlide = keyHandlers[ev.key];
if (!getSlide) {
return;
}
const nextSlide = getSlide();
transition(nextSlide);
});
let touchstartX = 0;
let touchendX = 0;
const handleGesure = () => {
const magnitude = Math.abs(touchstartX - touchendX);
if (magnitude < 40) {
// Ignore since this could be a scroll up/down
return;
}
if (touchendX < touchstartX) {
transition(nextSlide());
}
if (touchendX > touchstartX) {
transition(prevSlide());
}
};
document.addEventListener(
"touchstart",
(ev) => {
touchstartX = ev.changedTouches[0].screenX;
},
false
);
document.addEventListener(
"touchend",
(event) => {
touchendX = event.changedTouches[0].screenX;
handleGesure();
},
false
);
}
// If there is no presentation on the page then we don't initialize
if (slides.length) {
button.classList.remove("presentation-hidden");
button.addEventListener("click", startPresentation);
}
</script>
<style is:global>
.presentation-progress {
display: none;
}
.presentation-overflow-hidden {
overflow: hidden;
visibility: hidden;
.presentation-hidden {
display: none;
}
h1, h2, h3, h4 {
font-size: xx-large;
}
.presentation-slide.large {
font-size: x-large;
}
.presentation-progress {
transition: width 1000ms;
display: block;
visibility: visible;
position: absolute;
z-index: 20;
top:0px;
left: 0px;
width: var(--presentation-progress);
height: .25rem;
background: var(--color-brand-muted);
}
.presentation-slide {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
visibility: visible;
transition: transform 300ms ease-in-out;
display: flex;
flex-direction: column;
background-color: var(--color-base);
color: var(--color-on-base);
box-sizing: border-box;
min-height: 100vh;
width: 100%;
padding: 2rem 4rem;
z-index: 10;
overflow: auto;
&.centered {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
&.highlight{
background-color: var(--color-brand);
color: var(--color-on-brand)
}
.presentation-slide-only {
display: block;
}
.astro-code {
filter: none;
}
img {
max-height: 80vh;
}
}
&.presentation-presenter {
.presentation-slide {
border: none;
border-bottom: solid 8px var(--color-brand);
}
.presentation-note {
position: absolute;
bottom: 24px;
opacity: .8;
right: 24px;
left: 25%;
z-index: 999;
}
}
}
.presentation-slide-only {
display: none;
}
.presentation-next {
transform: translateX(100%);
}
.presentation-current {
transform: translateX(0%);
}
.presentation-prev {
transform: translateX(-100%);
}
.presentation-note {
display: none;
}
.presentation-presenter {
.presentation-slide {
border: dotted 8px var(--color-brand);
}
/* ensure that notes are visible if presentation mode is active, even if
not presenting */
.presentation-note {
display: block;
/* intentionally obnoxios color to draw attention */
background-color: crimson;
padding: 24px;
color: white;
font-size: xx-large;
}
}
</style>

View File

@@ -4,8 +4,7 @@ export interface Props {
highlight?: boolean
large?: boolean
}
const { centered, highlight, large} = Astro.props
const { centered = true, highlight, large } = Astro.props
---
<section class="presentation-slide" class:list={{ centered, highlight, large }}>

View File

@@ -6,9 +6,12 @@ duration: "2 hours"
tags: ["python", "programming", "beginner", "fundamentals"]
---
import Slide from "@/components/presentation/Slide.astro";
import Slide from "@/components/resources/slide.astro";
import Presentation from "@/components/resources/presentation.astro";
<Slide centered>
<Presentation />
<Slide large>
# Welcome to Python! 🐍
**A beginner-friendly programming language**
@@ -250,7 +253,7 @@ print(calculator())
```
</Slide>
<Slide centered>
<Slide >
## 🎉 Congratulations!
You've learned the basics of Python programming!

View File

@@ -1,8 +1,11 @@
---
// src/layouts/presentation.astro
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;
@@ -34,10 +37,34 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
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">
<main class="w-full h-screen">
<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

@@ -2,6 +2,7 @@
export const prerender = true;
import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro";
import { Comments } from "@/components/blog/comments";

View File

@@ -1,41 +1,31 @@
---
export const prerender = true;
import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro";
import PresentationLayout from "@/layouts/presentation.astro";
import Presentation from "@/components/presentation/Presentation.astro";
const { slug } = Astro.params;
import ResourceLayout from "@/layouts/resource.astro";
const resources = await getCollection("resources");
const resource = resources.find(item => item.slug === slug);
if (!resource) {
return new Response(null, {
status: 404,
statusText: 'Not found'
});
export async function getStaticPaths() {
const resources = await getCollection("resources");
return resources.map(resource => ({
params: { slug: resource.slug },
props: { resource },
}));
}
const { resource } = Astro.props;
const { Content } = await resource.render();
const formattedDate = new Date(resource.data.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
});
// Check if this is a curriculum resource to auto-start presentation
const isCurriculum = resource.slug.includes('curriculum');
const LayoutComponent = isCurriculum ? PresentationLayout : ContentLayout;
---
<LayoutComponent
title={`${resource.data.title} | Timothy Pidashev`}
description={resource.data.description}
>
<Presentation autoStart={isCurriculum} title="Start Presentation" />
<div class="relative max-w-8xl mx-auto">
<ResourceLayout title={`${resource.data.title} | Timothy Pidashev`}>
<article class="w-full mx-auto px-4 pt-6 sm:pt-12">
<header class="mb-8">
<h1 class="text-3xl sm:text-4xl font-bold text-yellow-bright mb-4">
{resource.data.title}
</h1>
</header>
<div class="prose prose-invert prose-lg max-w-none">
<Content />
</div>
</LayoutComponent>
</article>
</ResourceLayout>