For now sunset resources work

This commit is contained in:
2025-09-11 08:55:05 -07:00
parent 384cb82efb
commit f355373ba1
7 changed files with 9 additions and 1230 deletions

View File

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

View File

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

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

View File

@@ -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()),
}),
}),
})
};

View File

@@ -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.
* Its 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>

View File

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

View File

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