mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 02:53:51 +00:00
For now sunset resources work
This commit is contained in:
@@ -1,877 +0,0 @@
|
||||
---
|
||||
import Background from "@/components/background";
|
||||
---
|
||||
|
||||
<button id="presentation-button" class="presentation-hidden" type="button"
|
||||
>Start Presentation</button
|
||||
>
|
||||
|
||||
<div class="presentation-progress"></div>
|
||||
|
||||
<!-- Background components for presentation mode -->
|
||||
<div class="presentation-backgrounds">
|
||||
<Background layout="content" position="right" client:only="react" transition:persist />
|
||||
<Background layout="content" position="left" client:only="react" transition:persist />
|
||||
</div>
|
||||
|
||||
<!-- Fullscreen preview background -->
|
||||
<div id="fullscreen-preview" class="fullscreen-preview-hidden">
|
||||
<Background layout="index" client:only="react" />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const button = document.getElementById(
|
||||
"presentation-button"
|
||||
) as HTMLButtonElement;
|
||||
|
||||
let slides = Array.from(document.querySelectorAll(".presentation-slide"));
|
||||
|
||||
let slide = 0;
|
||||
let step = 0; // Current step within the slide
|
||||
let presenter = false;
|
||||
let fullscreenPreview = 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];
|
||||
|
||||
// Animation classes for step-through content
|
||||
const animationClasses = {
|
||||
'fade-in': 'animate-fade-in-step',
|
||||
'slide-in-left': 'animate-slide-in-left-step',
|
||||
'slide-in-right': 'animate-slide-in-right-step',
|
||||
'slide-in-up': 'animate-slide-in-up-step',
|
||||
'slide-in-down': 'animate-slide-in-down-step',
|
||||
'type-in': 'animate-type-in-step',
|
||||
'scale-in': 'animate-scale-in-step',
|
||||
'bounce-in': 'animate-bounce-in-step'
|
||||
};
|
||||
|
||||
const getCurrentSlideSteps = () => {
|
||||
const currentSlide = slides[slide];
|
||||
return Array.from(currentSlide.querySelectorAll('[step]'))
|
||||
.sort((a, b) => {
|
||||
const stepA = parseInt((a as HTMLElement).getAttribute('step') || '0');
|
||||
const stepB = parseInt((b as HTMLElement).getAttribute('step') || '0');
|
||||
return stepA - stepB;
|
||||
});
|
||||
};
|
||||
|
||||
const getMaxSteps = () => {
|
||||
const steps = getCurrentSlideSteps();
|
||||
if (steps.length === 0) return 0;
|
||||
const lastStep = steps[steps.length - 1] as HTMLElement;
|
||||
return parseInt(lastStep.getAttribute('step') || '0');
|
||||
};
|
||||
|
||||
const showStepsUpTo = (targetStep: number, isReverse: boolean = false) => {
|
||||
const steps = getCurrentSlideSteps();
|
||||
|
||||
steps.forEach((stepElement) => {
|
||||
const element = stepElement as HTMLElement;
|
||||
const elementStep = parseInt(element.getAttribute('step') || '0');
|
||||
const animationType = element.getAttribute('animation') || 'fade-in';
|
||||
|
||||
// Remove all animation classes first
|
||||
Object.values(animationClasses).forEach(cls => {
|
||||
element.classList.remove(cls);
|
||||
element.classList.remove(cls.replace('-step', '-reverse-step'));
|
||||
});
|
||||
element.classList.remove('step-hidden', 'step-visible');
|
||||
|
||||
if (elementStep <= targetStep) {
|
||||
// Show this step with animation
|
||||
element.classList.add('step-visible');
|
||||
if (elementStep === targetStep) {
|
||||
// Apply animation only to the current step
|
||||
const baseAnimationClass = animationClasses[animationType as keyof typeof animationClasses] || animationClasses['fade-in'];
|
||||
const animationClass = isReverse ? baseAnimationClass.replace('-step', '-reverse-step') : baseAnimationClass;
|
||||
|
||||
// Special handling for type-in animation
|
||||
if (animationType === 'type-in') {
|
||||
if (isReverse) {
|
||||
startTypeAnimation(element, false);
|
||||
} else {
|
||||
startTypeAnimation(element, true);
|
||||
}
|
||||
} else {
|
||||
element.classList.add(animationClass);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Hide this step
|
||||
element.classList.add('step-hidden');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const startTypeAnimation = (element: HTMLElement, isForward: boolean) => {
|
||||
const text = element.textContent || '';
|
||||
const duration = isForward ? 1500 : 1000; // ms
|
||||
const steps = Math.max(text.length, 1);
|
||||
const stepDuration = duration / steps;
|
||||
|
||||
if (isForward) {
|
||||
// Type in: reveal characters one by one
|
||||
element.textContent = '';
|
||||
element.style.opacity = '1';
|
||||
|
||||
let currentIndex = 0;
|
||||
const typeInterval = setInterval(() => {
|
||||
if (currentIndex < text.length) {
|
||||
element.textContent = text.substring(0, currentIndex + 1);
|
||||
currentIndex++;
|
||||
} else {
|
||||
clearInterval(typeInterval);
|
||||
}
|
||||
}, stepDuration);
|
||||
|
||||
// Store interval reference for cleanup
|
||||
(element as any)._typeInterval = typeInterval;
|
||||
} else {
|
||||
// Type out: hide characters one by one from the end
|
||||
let currentLength = text.length;
|
||||
const typeInterval = setInterval(() => {
|
||||
if (currentLength > 0) {
|
||||
element.textContent = text.substring(0, currentLength - 1);
|
||||
currentLength--;
|
||||
} else {
|
||||
clearInterval(typeInterval);
|
||||
element.style.opacity = '0';
|
||||
}
|
||||
}, stepDuration);
|
||||
|
||||
// Store interval reference for cleanup
|
||||
(element as any)._typeInterval = typeInterval;
|
||||
}
|
||||
};
|
||||
|
||||
const resetSlideSteps = () => {
|
||||
const steps = getCurrentSlideSteps();
|
||||
steps.forEach((stepElement) => {
|
||||
const element = stepElement as HTMLElement;
|
||||
|
||||
// Clear any running type animations
|
||||
if ((element as any)._typeInterval) {
|
||||
clearInterval((element as any)._typeInterval);
|
||||
(element as any)._typeInterval = null;
|
||||
}
|
||||
|
||||
Object.values(animationClasses).forEach(cls => {
|
||||
element.classList.remove(cls);
|
||||
element.classList.remove(cls.replace('-step', '-reverse-step'));
|
||||
});
|
||||
element.classList.remove('step-hidden', 'step-visible');
|
||||
element.classList.add('step-hidden');
|
||||
|
||||
// Reset text content and styles for type-in elements
|
||||
if (element.getAttribute('animation') === 'type-in') {
|
||||
element.style.opacity = '0';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const nextStep = () => {
|
||||
const maxSteps = getMaxSteps();
|
||||
|
||||
if (step < maxSteps) {
|
||||
step++;
|
||||
showStepsUpTo(step, false);
|
||||
return { slide, step };
|
||||
} else {
|
||||
// Move to next slide
|
||||
if (slide < slides.length - 1) {
|
||||
const nextSlideIndex = nextSlide();
|
||||
return { slide: nextSlideIndex, step: 0 };
|
||||
}
|
||||
return { slide, step };
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (step > 0) {
|
||||
step--;
|
||||
showStepsUpTo(step, true);
|
||||
return { slide, step };
|
||||
} else {
|
||||
// Move to previous slide
|
||||
if (slide > 0) {
|
||||
const prevSlideIndex = prevSlide();
|
||||
// Set to max steps of previous slide
|
||||
const tempSlide = slide;
|
||||
slide = prevSlideIndex;
|
||||
const maxSteps = getMaxSteps();
|
||||
slide = tempSlide;
|
||||
return { slide: prevSlideIndex, step: maxSteps };
|
||||
}
|
||||
return { slide, step };
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullscreenPreview = () => {
|
||||
const previewElement = document.getElementById('fullscreen-preview');
|
||||
if (!previewElement) return;
|
||||
|
||||
fullscreenPreview = !fullscreenPreview;
|
||||
|
||||
if (fullscreenPreview) {
|
||||
previewElement.classList.remove('fullscreen-preview-hidden');
|
||||
previewElement.classList.add('fullscreen-preview-visible');
|
||||
} else {
|
||||
previewElement.classList.remove('fullscreen-preview-visible');
|
||||
previewElement.classList.add('fullscreen-preview-hidden');
|
||||
}
|
||||
};
|
||||
|
||||
const keyHandlers: Record<string, () => { slide: number, step: number } | void> = {
|
||||
ArrowRight: nextStep,
|
||||
ArrowLeft: prevStep,
|
||||
p: toggleFullscreenPreview,
|
||||
P: toggleFullscreenPreview,
|
||||
};
|
||||
|
||||
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);
|
||||
// Show steps for current slide
|
||||
showStepsUpTo(step, false);
|
||||
} else {
|
||||
slides[i].classList.add("inactive");
|
||||
// Reset steps for non-current slides
|
||||
const tempSlide = slide;
|
||||
slide = i;
|
||||
resetSlideSteps();
|
||||
slide = tempSlide;
|
||||
|
||||
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;
|
||||
step = 0; // Reset step
|
||||
displaySlides();
|
||||
setProgress();
|
||||
initListeners();
|
||||
};
|
||||
|
||||
const endPresentation = () => {
|
||||
document.body.classList.remove("presentation-overflow-hidden");
|
||||
|
||||
presenting = false;
|
||||
step = 0;
|
||||
fullscreenPreview = false;
|
||||
|
||||
// Hide fullscreen preview if active
|
||||
const previewElement = document.getElementById('fullscreen-preview');
|
||||
if (previewElement) {
|
||||
previewElement.classList.remove('fullscreen-preview-visible');
|
||||
previewElement.classList.add('fullscreen-preview-hidden');
|
||||
}
|
||||
|
||||
slides.map((s) => {
|
||||
s.classList.remove("active", "inactive", ...transitionClasses);
|
||||
// Reset all steps
|
||||
const tempSlide = slide;
|
||||
slides.forEach((_, i) => {
|
||||
slide = i;
|
||||
resetSlideSteps();
|
||||
});
|
||||
slide = tempSlide;
|
||||
});
|
||||
};
|
||||
|
||||
const setPresenter = () => {
|
||||
presenter = true;
|
||||
document.body.classList.add("presentation-presenter");
|
||||
};
|
||||
|
||||
const setProgress = () => {
|
||||
const maxSteps = getMaxSteps();
|
||||
const totalSteps = slides.reduce((acc, _, i) => {
|
||||
const tempSlide = slide;
|
||||
slide = i;
|
||||
const steps = getMaxSteps();
|
||||
slide = tempSlide;
|
||||
return acc + Math.max(1, steps);
|
||||
}, 0);
|
||||
|
||||
let currentProgress = 0;
|
||||
for (let i = 0; i < slide; i++) {
|
||||
const tempSlide = slide;
|
||||
slide = i;
|
||||
const steps = getMaxSteps();
|
||||
slide = tempSlide;
|
||||
currentProgress += Math.max(1, steps);
|
||||
}
|
||||
currentProgress += step;
|
||||
|
||||
const progress = (currentProgress / totalSteps) * 100;
|
||||
document.body.style.setProperty('--presentation-progress', `${progress}%`);
|
||||
};
|
||||
|
||||
const transition = (nextSlide: number, nextStep: number = 0) => {
|
||||
if (!presenting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (slide === nextSlide && step === nextStep) {
|
||||
return;
|
||||
}
|
||||
|
||||
slides.forEach((s) => s.classList.remove(...transitionClasses));
|
||||
|
||||
slide = nextSlide;
|
||||
step = nextStep;
|
||||
|
||||
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 getNextPosition = keyHandlers[ev.key];
|
||||
|
||||
if (!getNextPosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = getNextPosition();
|
||||
if (result && 'slide' in result) {
|
||||
const { slide: nextSlideIndex, step: nextStepIndex } = result;
|
||||
transition(nextSlideIndex, nextStepIndex);
|
||||
}
|
||||
});
|
||||
|
||||
let touchstartX = 0;
|
||||
let touchendX = 0;
|
||||
const handleGesture = () => {
|
||||
const magnitude = Math.abs(touchstartX - touchendX);
|
||||
|
||||
if (magnitude < 40) {
|
||||
// Ignore since this could be a scroll up/down
|
||||
return;
|
||||
}
|
||||
|
||||
if (touchendX < touchstartX) {
|
||||
const { slide: nextSlideIndex, step: nextStepIndex } = nextStep();
|
||||
transition(nextSlideIndex, nextStepIndex);
|
||||
}
|
||||
if (touchendX > touchstartX) {
|
||||
const { slide: prevSlideIndex, step: prevStepIndex } = prevStep();
|
||||
transition(prevSlideIndex, prevStepIndex);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener(
|
||||
"touchstart",
|
||||
(ev) => {
|
||||
touchstartX = ev.changedTouches[0].screenX;
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
"touchend",
|
||||
(event) => {
|
||||
touchendX = event.changedTouches[0].screenX;
|
||||
handleGesture();
|
||||
},
|
||||
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 background positioning */
|
||||
.presentation-backgrounds {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 5; /* Below slides but above normal content */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.presentation-overflow-hidden .presentation-backgrounds {
|
||||
display: block;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.presentation-overflow-hidden:not(.presentation-presenter) .presentation-backgrounds {
|
||||
z-index: 5; /* Ensure it's visible during presentation */
|
||||
}
|
||||
.fullscreen-preview-hidden {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fullscreen-preview-visible {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Step animation styles */
|
||||
.step-hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.step-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Animation keyframes */
|
||||
@keyframes fadeInStep {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOutStep {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeftStep {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutLeftStep {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRightStep {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutRightStep {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInUpStep {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutUpStep {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInDownStep {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideOutDownStep {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleInStep {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleOutStep {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceInStep {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceOutStep {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
30% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typeInStep {
|
||||
/* This is now unused - keeping for backward compatibility */
|
||||
from { opacity: 1; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes typeOutStep {
|
||||
/* This is now unused - keeping for backward compatibility */
|
||||
from { opacity: 1; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
.animate-fade-in-step {
|
||||
animation: fadeInStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in-reverse-step {
|
||||
animation: fadeOutStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-left-step {
|
||||
animation: slideInLeftStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-left-reverse-step {
|
||||
animation: slideOutLeftStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-right-step {
|
||||
animation: slideInRightStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-right-reverse-step {
|
||||
animation: slideOutRightStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-up-step {
|
||||
animation: slideInUpStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-up-reverse-step {
|
||||
animation: slideOutUpStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-down-step {
|
||||
animation: slideInDownStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-down-reverse-step {
|
||||
animation: slideOutDownStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-scale-in-step {
|
||||
animation: scaleInStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-scale-in-reverse-step {
|
||||
animation: scaleOutStep 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-bounce-in-step {
|
||||
animation: bounceInStep 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-bounce-in-reverse-step {
|
||||
animation: bounceOutStep 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-type-in-step {
|
||||
/* JavaScript-controlled animation - no CSS animation needed */
|
||||
}
|
||||
|
||||
.animate-type-in-reverse-step {
|
||||
/* JavaScript-controlled animation - no CSS animation needed */
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
export interface Props {
|
||||
centered?: boolean
|
||||
highlight?: boolean
|
||||
large?: boolean
|
||||
}
|
||||
const { centered = true, highlight, large } = Astro.props
|
||||
---
|
||||
|
||||
<section class="presentation-slide" class:list={{ centered, highlight, large }}>
|
||||
<slot />
|
||||
</section>
|
||||
8
src/src/content/blog/breaking-the-chromebook-cage.mdx
Normal file
8
src/src/content/blog/breaking-the-chromebook-cage.mdx
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: Breaking the Chromebook Cage
|
||||
description: From breaking Chromebooks as a student to breaking Chromebooks to stop students from breaking Chromebooks.
|
||||
author: Timothy Pidashev
|
||||
tags: ["uefi", "coreboot", "firmware", "chromebooks"]
|
||||
date: 2025-09-15
|
||||
image: "/blog/breaking-the-chromebook-cage/thumbnail.png"
|
||||
---
|
||||
@@ -24,17 +24,5 @@ export const collections = {
|
||||
date: z.string(),
|
||||
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()),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
};
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
---
|
||||
title: "0101 - Welcome to the Terminal"
|
||||
description: "Week 1 | Lesson 1"
|
||||
date: 2025-09-27
|
||||
duration: "45 minutes"
|
||||
tags: ["terminal"]
|
||||
---
|
||||
|
||||
import Slide from "@/components/resources/slide.astro";
|
||||
import Presentation from "@/components/resources/presentation.astro";
|
||||
|
||||
import { Video } from "@/components/mdx/video";
|
||||
|
||||
<Presentation />
|
||||
|
||||
<Slide large>
|
||||
* The terminal is a way to **talk to your computer** using words instead of clicking.
|
||||
* It’s like giving your computer instructions directly.
|
||||
* Powerful, fast, and used by programmers every day!
|
||||
* It's sometimes called a **command-line** because it accepts commands as instructions
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide large>
|
||||
## Video Introduction
|
||||
<Video client:load
|
||||
title="What is the terminal and why should I use it? // Developer Fundamentals"
|
||||
url="https://timmypidashev.us-sea-1.linodeobjects.com/curriculum%2Fterminal%2F01.mp4"
|
||||
attribution="https://www.youtube.com/watch?v=lZ7Kix9bjPI"
|
||||
/>
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide large>
|
||||
## Opening the Terminal
|
||||
|
||||
1. Open your laptops and power them on
|
||||
2. When prompted to login, enter your credentials:
|
||||
**Username:** your first and last name (all lowercase, no spaces) `timothypidashev`
|
||||
**Password:** your birthday in MMDDYYYY format (without slashes) `08052004`
|
||||
> Once logged in, help the person to your right and left,
|
||||
> we will continue when everybody is ready!
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide large>
|
||||
## It's Lonely in Here...
|
||||
|
||||
Now you should see a **prompt** that looks something like this:
|
||||
```fish
|
||||
timothypidashev@laptop1 ~>
|
||||
```
|
||||
This might look intimidating, but it's actually telling you useful information!
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide large>
|
||||
## Understanding the Prompt
|
||||
|
||||
```fish
|
||||
timothypidashev@laptop1 ~>
|
||||
```
|
||||
|
||||
Breaking it down:
|
||||
- **`timothypidashev`** - This is **your username**. It tells you who is currently using the computer
|
||||
- **`@laptop1`** - This is the **computer's name** (hostname). Useful when working with multiple computers!
|
||||
- **`~`** - This is your **current location** in the computer. The `~` symbol means you're in your "home" folder
|
||||
- **`>`** - This is the **prompt symbol**. It's waiting for you to type a command!
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide large>
|
||||
## Your First Command - `whoami`
|
||||
|
||||
Let's try our first command! Type this and press **Enter**:
|
||||
|
||||
```fish
|
||||
whoami
|
||||
```
|
||||
|
||||
- This command asks the computer: "Who am I?"
|
||||
- The computer should respond with your username!
|
||||
|
||||
> **Try it!** Type `whoami` and press Enter
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide>
|
||||
## Where Am I? - The `pwd` Command
|
||||
|
||||
Now let's find out exactly where we are in the computer. Type:
|
||||
|
||||
```fish
|
||||
pwd
|
||||
```
|
||||
|
||||
- `pwd` stands for **"Print Working Directory"**
|
||||
- It tells you the **full path** to where you currently are
|
||||
- You should see something like: `/home/timothypidashev`
|
||||
|
||||
> **Try it!** Type `pwd` and press Enter
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide>
|
||||
## Looking Around - The `ls` Command
|
||||
|
||||
Let's see what's in our current location. Type:
|
||||
|
||||
```fish
|
||||
ls
|
||||
```
|
||||
|
||||
- `ls` stands for **"list"** - it shows you all the files and folders in your current location
|
||||
- You might see folders like `Desktop`, `Documents`, `Downloads`, etc.
|
||||
|
||||
> **Try it!** Type `ls` and press Enter. What do you see?
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide>
|
||||
## Making Your Mark - The `mkdir` Command
|
||||
|
||||
Let's create your first folder using the terminal! Type:
|
||||
|
||||
```fish
|
||||
mkdir my-first-folder
|
||||
```
|
||||
|
||||
- `mkdir` stands for **"make directory"** (folder and directory mean the same thing)
|
||||
- Now type `ls` again to see your new folder appear!
|
||||
|
||||
> **Try it!** Create a folder, then list the contents to see it
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide large>
|
||||
## Moving Around - The `cd` Command
|
||||
|
||||
Let's go inside the folder we just created. Type:
|
||||
|
||||
```bash
|
||||
cd my-first-folder
|
||||
```
|
||||
|
||||
- `cd` stands for **"change directory"** - it moves you to a different location
|
||||
- Notice how your prompt changes! The `~` might change to show your new location
|
||||
- Type `pwd` to confirm where you are now
|
||||
|
||||
> **Try it!** Move into your folder and check your location
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide large>
|
||||
## Going Back - `cd ..`
|
||||
|
||||
To go back to the previous folder (your home), type:
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
```
|
||||
|
||||
- The `..` means **"parent directory"** - the folder that contains your current folder
|
||||
- You can also type `cd ~` or just `cd` to go back to your home folder anytime
|
||||
|
||||
> **Try it!** Go back to your home folder
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide large>
|
||||
## Creating Files - The `touch` Command
|
||||
|
||||
Let's create your first file using the terminal. Type:
|
||||
|
||||
```bash
|
||||
touch hello.txt
|
||||
```
|
||||
|
||||
- `touch` creates a new, empty file
|
||||
- Type `ls` to see your new file appear alongside your folder
|
||||
|
||||
> **Try it!** Create a file and list the contents to see it
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide large>
|
||||
## Cleaning Up - The `rm` Command
|
||||
|
||||
Let's clean up by removing the file we just created:
|
||||
|
||||
```bash
|
||||
rm hello.txt
|
||||
```
|
||||
|
||||
- `rm` stands for **"remove"** - it deletes files
|
||||
- ⚠️ **Warning**: `rm` permanently deletes files - there's no trash can in the terminal!
|
||||
- Type `ls` to confirm the file is gone
|
||||
|
||||
> **Try it!** Remove your file and verify it's deleted
|
||||
|
||||
---
|
||||
</Slide>
|
||||
|
||||
<Slide large>
|
||||
|
||||
## Command Summary
|
||||
|
||||
Here are the commands we learned today:
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `whoami` | Shows your username |
|
||||
| `pwd` | Shows your current location |
|
||||
| `ls` | Lists files and folders |
|
||||
| `mkdir` | Creates a new folder |
|
||||
| `cd` | Changes to a different folder |
|
||||
| `cd ..` | Goes to the parent folder |
|
||||
| `touch` | Creates a new file |
|
||||
| `rm` | Removes/deletes a file |
|
||||
|
||||
> BYTE! | Create a file named `.hidden` and see if you can find it with `ls -a`!
|
||||
|
||||
---
|
||||
</Slide>
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
export const prerender = true;
|
||||
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
import ResourceLayout from "@/layouts/resource.astro";
|
||||
|
||||
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();
|
||||
---
|
||||
|
||||
<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>
|
||||
</article>
|
||||
</ResourceLayout>
|
||||
@@ -1,61 +0,0 @@
|
||||
---
|
||||
// src/pages/resources/index.astro - SIMPLE LISTING
|
||||
import { getCollection } from "astro:content";
|
||||
import ContentLayout from "@/layouts/content.astro";
|
||||
|
||||
const allResources = (await getCollection("resources")).sort(
|
||||
(a, b) => b.data.date.valueOf() - a.data.date.valueOf()
|
||||
);
|
||||
---
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user