mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 11:03:50 +00:00
Broken
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
373
src/src/components/presentation/Presentation.astro
Normal file
373
src/src/components/presentation/Presentation.astro
Normal file
@@ -0,0 +1,373 @@
|
||||
---
|
||||
// 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>
|
||||
@@ -1,386 +0,0 @@
|
||||
// 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
13
src/src/components/presentation/Slide.astro
Normal file
13
src/src/components/presentation/Slide.astro
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
export interface Props {
|
||||
centered?: boolean
|
||||
highlight?: boolean
|
||||
large?: boolean
|
||||
}
|
||||
|
||||
const { centered, highlight, large} = Astro.props
|
||||
---
|
||||
|
||||
<section class="presentation-slide" class:list={{ centered, highlight, large }}>
|
||||
<slot />
|
||||
</section>
|
||||
@@ -1,34 +0,0 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
@@ -6,111 +6,68 @@ 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>
|
||||
import Slide from "@/components/presentation/Slide.astro";
|
||||
|
||||
<Slide 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?">
|
||||
|
||||
<Slide>
|
||||
## What is Python?
|
||||
|
||||
<FadeIn>
|
||||
|
||||
Python is a <Highlight color="yellow">high-level programming language</Highlight> that emphasizes:
|
||||
Python is a **high-level programming language** 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?">
|
||||
|
||||
<Slide>
|
||||
## 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>
|
||||
}
|
||||
/>
|
||||
### 🌐 Web Development
|
||||
Build websites and web applications
|
||||
|
||||
### 🤖 Data Science & AI
|
||||
Analyze data and build machine learning models
|
||||
|
||||
### 🎮 Game Development
|
||||
Create games and interactive applications
|
||||
|
||||
### 🔧 Automation
|
||||
Automate repetitive tasks
|
||||
</Slide>
|
||||
|
||||
<Slide title="Your First Python Program">
|
||||
|
||||
<Slide>
|
||||
## 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">
|
||||
|
||||
<Slide>
|
||||
## Variables and Data Types
|
||||
|
||||
<FadeIn>
|
||||
|
||||
### Creating Variables
|
||||
```python
|
||||
name = "Alice" # String
|
||||
@@ -119,48 +76,192 @@ height = 5.6 # Float
|
||||
is_student = True # Boolean
|
||||
```
|
||||
|
||||
</FadeIn>
|
||||
|
||||
<FadeIn delay={400}>
|
||||
|
||||
### Python is <Highlight color="green">dynamically typed</Highlight>
|
||||
### Python is **dynamically typed**
|
||||
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>
|
||||
<Slide>
|
||||
## Working with Strings
|
||||
|
||||
### String Operations
|
||||
```python
|
||||
# Creating strings
|
||||
greeting = "Hello"
|
||||
name = "World"
|
||||
|
||||
# Concatenation
|
||||
message = greeting + ", " + name + "!"
|
||||
print(message) # Output: Hello, World!
|
||||
|
||||
# String methods
|
||||
text = "python programming"
|
||||
print(text.upper()) # PYTHON PROGRAMMING
|
||||
print(text.capitalize()) # Python programming
|
||||
print(text.replace("python", "Python")) # Python programming
|
||||
```
|
||||
</Slide>
|
||||
|
||||
<Slide>
|
||||
## Numbers and Math
|
||||
|
||||
### Basic Math Operations
|
||||
```python
|
||||
# Arithmetic operators
|
||||
a = 10
|
||||
b = 3
|
||||
|
||||
print(a + b) # Addition: 13
|
||||
print(a - b) # Subtraction: 7
|
||||
print(a * b) # Multiplication: 30
|
||||
print(a / b) # Division: 3.333...
|
||||
print(a // b) # Floor division: 3
|
||||
print(a % b) # Modulo (remainder): 1
|
||||
print(a ** b) # Exponentiation: 1000
|
||||
```
|
||||
</Slide>
|
||||
|
||||
<Slide>
|
||||
## Lists - Storing Multiple Values
|
||||
|
||||
### Creating and Using Lists
|
||||
```python
|
||||
# Creating a list
|
||||
fruits = ["apple", "banana", "orange"]
|
||||
numbers = [1, 2, 3, 4, 5]
|
||||
|
||||
# Accessing items (indexing starts at 0)
|
||||
print(fruits[0]) # apple
|
||||
print(fruits[-1]) # orange (last item)
|
||||
|
||||
# Adding items
|
||||
fruits.append("grape")
|
||||
fruits.insert(1, "strawberry")
|
||||
|
||||
# List methods
|
||||
print(len(fruits)) # Length of list
|
||||
print("apple" in fruits) # Check if item exists
|
||||
```
|
||||
</Slide>
|
||||
|
||||
<Slide>
|
||||
## Control Flow - Making Decisions
|
||||
|
||||
### If Statements
|
||||
```python
|
||||
age = 18
|
||||
|
||||
if age >= 18:
|
||||
print("You can vote!")
|
||||
elif age >= 16:
|
||||
print("You can drive!")
|
||||
else:
|
||||
print("You're still young!")
|
||||
|
||||
# Comparison operators
|
||||
# == (equal), != (not equal)
|
||||
# > (greater), < (less)
|
||||
# >= (greater or equal), <= (less or equal)
|
||||
```
|
||||
</Slide>
|
||||
|
||||
<Slide>
|
||||
## Loops - Repeating Actions
|
||||
|
||||
### For Loops
|
||||
```python
|
||||
# Loop through a list
|
||||
fruits = ["apple", "banana", "orange"]
|
||||
for fruit in fruits:
|
||||
print(f"I like {fruit}")
|
||||
|
||||
# Loop through numbers
|
||||
for i in range(5):
|
||||
print(f"Count: {i}") # 0, 1, 2, 3, 4
|
||||
```
|
||||
|
||||
### While Loops
|
||||
```python
|
||||
count = 0
|
||||
while count < 3:
|
||||
print(f"Count is {count}")
|
||||
count += 1 # Same as count = count + 1
|
||||
```
|
||||
</Slide>
|
||||
|
||||
<Slide>
|
||||
## Functions - Organizing Your Code
|
||||
|
||||
### Creating Functions
|
||||
```python
|
||||
def greet(name):
|
||||
"""This function greets someone"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
def add_numbers(a, b):
|
||||
"""Add two numbers and return the result"""
|
||||
result = a + b
|
||||
return result
|
||||
|
||||
# Using functions
|
||||
message = greet("Alice")
|
||||
print(message) # Hello, Alice!
|
||||
|
||||
sum_result = add_numbers(5, 3)
|
||||
print(sum_result) # 8
|
||||
```
|
||||
</Slide>
|
||||
|
||||
<Slide>
|
||||
## Practice Exercise
|
||||
|
||||
### Build a Simple Calculator
|
||||
```python
|
||||
def calculator():
|
||||
print("Simple Calculator")
|
||||
print("Operations: +, -, *, /")
|
||||
|
||||
num1 = float(input("Enter first number: "))
|
||||
operation = input("Enter operation (+, -, *, /): ")
|
||||
num2 = float(input("Enter second number: "))
|
||||
|
||||
if operation == "+":
|
||||
result = num1 + num2
|
||||
elif operation == "-":
|
||||
result = num1 - num2
|
||||
elif operation == "*":
|
||||
result = num1 * num2
|
||||
elif operation == "/":
|
||||
if num2 != 0:
|
||||
result = num1 / num2
|
||||
else:
|
||||
return "Error: Division by zero!"
|
||||
else:
|
||||
return "Error: Invalid operation!"
|
||||
|
||||
return f"Result: {result}"
|
||||
|
||||
# Run the calculator
|
||||
print(calculator())
|
||||
```
|
||||
</Slide>
|
||||
|
||||
<Slide 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
|
||||
@@ -169,7 +270,7 @@ This curriculum covers the fundamentals of Python programming in an interactive,
|
||||
|
||||
### 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.
|
||||
The presentation above covers all the core concepts you need to start programming in Python. Each slide builds upon the previous one, taking you from basic concepts to writing your first functional program.
|
||||
|
||||
#### What You'll Need
|
||||
|
||||
@@ -178,4 +279,13 @@ Before we dive into the presentation slides above, let's set up our development
|
||||
- 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!
|
||||
#### Course Objectives
|
||||
|
||||
By the end of this presentation, you'll be able to:
|
||||
- Understand Python syntax and basic programming concepts
|
||||
- Work with variables, strings, numbers, and lists
|
||||
- Use conditional statements and loops
|
||||
- Create and use functions
|
||||
- Build a simple calculator program
|
||||
|
||||
The presentation automatically starts when you visit this page. Use arrow keys to navigate between slides, or escape to exit presentati
|
||||
|
||||
43
src/src/layouts/presentation.astro
Normal file
43
src/src/layouts/presentation.astro
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
// src/layouts/presentation.astro
|
||||
import "@/style/globals.css";
|
||||
import { ClientRouter } from "astro:transitions";
|
||||
|
||||
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}
|
||||
/>
|
||||
</head>
|
||||
<body class="bg-background text-foreground min-h-screen">
|
||||
<main class="w-full h-screen">
|
||||
<slot />
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import ContentLayout from "@/layouts/content.astro";
|
||||
import { Presentation } from "@/components/presentation/Presentation";
|
||||
import PresentationLayout from "@/layouts/presentation.astro";
|
||||
import Presentation from "@/components/presentation/Presentation.astro";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
|
||||
// Same pattern as your blog
|
||||
const resources = await getCollection("resources");
|
||||
const resource = resources.find(item => item.slug === slug);
|
||||
|
||||
@@ -23,51 +23,19 @@ const formattedDate = new Date(resource.data.date).toLocaleDateString("en-US", {
|
||||
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;
|
||||
---
|
||||
|
||||
<ContentLayout
|
||||
<LayoutComponent
|
||||
title={`${resource.data.title} | Timothy Pidashev`}
|
||||
description={resource.data.description}
|
||||
>
|
||||
<Presentation title="Start Presentation" />
|
||||
<Presentation autoStart={isCurriculum} 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>
|
||||
<Content />
|
||||
</div>
|
||||
</ContentLayout>
|
||||
</LayoutComponent>
|
||||
|
||||
Reference in New Issue
Block a user