Add talks
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m45s

This commit is contained in:
2025-06-12 00:41:44 -06:00
parent 6b6b571dd6
commit bfa3784f03
8 changed files with 743 additions and 475 deletions

View File

@ -1,6 +1,13 @@
import { useComputed, useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
import { Home, NotebookPen, FileText, CodeXml, Terminal as TerminalIcon } from 'lucide-preact';
import {
Home,
NotebookPen,
FileText,
CodeXml,
Terminal as TerminalIcon,
Megaphone,
} from "lucide-preact";
interface NavigationBarProps {
currentPath: string;
@ -10,7 +17,7 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
const isScrolling = useSignal(false);
const prevScrollPos = useSignal(0);
const currentClientPath = useSignal(currentPath);
const isVisible = useComputed(() => {
if (prevScrollPos.value < 50) return true;
@ -35,28 +42,29 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
};
// Listen for astro:page-load event which fires after navigation completes
document.addEventListener('astro:page-load', handleAstroNavigation);
document.addEventListener("astro:page-load", handleAstroNavigation);
// Also listen for astro:after-swap as a backup
document.addEventListener('astro:after-swap', handleAstroNavigation);
document.addEventListener("astro:after-swap", handleAstroNavigation);
// Listen for regular navigation events as fallback
window.addEventListener('popstate', updatePath);
window.addEventListener("popstate", updatePath);
return () => {
document.removeEventListener('astro:page-load', handleAstroNavigation);
document.removeEventListener('astro:after-swap', handleAstroNavigation);
window.removeEventListener('popstate', updatePath);
document.removeEventListener("astro:page-load", handleAstroNavigation);
document.removeEventListener("astro:after-swap", handleAstroNavigation);
window.removeEventListener("popstate", updatePath);
};
}, []);
// Use the client path
const activePath = currentClientPath.value;
// Normalize path by removing trailing slashes for consistent comparison
const normalizedPath = activePath.endsWith('/') && activePath.length > 1
? activePath.slice(0, -1)
: activePath;
// Normalize path
const normalizedPath =
activePath.endsWith("/") && activePath.length > 1
? activePath.slice(0, -1)
: activePath;
const isPostsPath = (path: string) => {
return path.startsWith("/posts") || path.startsWith("/post/");
@ -125,7 +133,9 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
<li class="mx-0.5 sm:mx-1">
<a
href="/projects"
class={normalizedPath.startsWith("/projects") ? "menu-active" : ""}
class={
normalizedPath.startsWith("/projects") ? "menu-active" : ""
}
>
<div class="tooltip" data-tip="Projects">
<CodeXml size={18} class="sm:w-5 sm:h-5" />
@ -133,6 +143,17 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
</a>
</li>
<li class="mx-0.5 sm:mx-1">
<a
href="/talks"
class={normalizedPath.startsWith("/talks") ? "menu-active" : ""}
>
<div class="tooltip" data-tip="Talks">
<Megaphone size={18} class="sm:w-5 sm:h-5" />
</div>
</a>
</li>
<li class="mx-0.5 sm:mx-1">
<a
href="/terminal"

View File

@ -7,12 +7,12 @@ interface Skill {
level: number;
}
interface SkillsSectionProps {
interface ResumeSkillsProps {
title: string;
skills: Skill[];
}
export default function SkillsSection({ title, skills }: SkillsSectionProps) {
export default function ResumeSkills({ title, skills }: ResumeSkillsProps) {
const animatedLevels = useSignal<{ [key: string]: number }>({});
const hasAnimated = useSignal(false);
@ -29,10 +29,10 @@ export default function SkillsSection({ title, skills }: SkillsSectionProps) {
}
});
},
{ threshold: 0.3 }
{ threshold: 0.3 },
);
const skillsElement = document.getElementById('skills-section');
const skillsElement = document.getElementById("skills-section");
if (skillsElement) {
observer.observe(skillsElement);
}
@ -45,8 +45,7 @@ export default function SkillsSection({ title, skills }: SkillsSectionProps) {
}, [skills]);
const animateSkill = (skillId: string, targetLevel: number) => {
const duration = 1500; // 1.5 seconds
const steps = 60; // 60 frames for smooth animation
const steps = 60;
const increment = targetLevel / steps;
let currentStep = 0;
@ -55,7 +54,7 @@ export default function SkillsSection({ title, skills }: SkillsSectionProps) {
const currentValue = Math.min(increment * currentStep, targetLevel);
animatedLevels.value = {
...animatedLevels.value,
[skillId]: currentValue
[skillId]: currentValue,
};
currentStep++;
requestAnimationFrame(animate);
@ -73,18 +72,19 @@ export default function SkillsSection({ title, skills }: SkillsSectionProps) {
{skills.map((skill) => {
const currentLevel = animatedLevels.value[skill.id] || 0;
const progressValue = currentLevel * 20; // Convert 1-5 scale to 0-100
return (
<div key={skill.id}>
<label class="label p-1 sm:p-2">
<span class="label-text text-sm sm:text-base">{skill.name}</span>
<span class="label-text text-sm sm:text-base">
{skill.name}
</span>
</label>
<progress
class="progress progress-primary w-full h-2 sm:h-3 transition-all duration-100 ease-out"
value={progressValue}
max="100"
>
</progress>
></progress>
</div>
);
})}
@ -92,4 +92,4 @@ export default function SkillsSection({ title, skills }: SkillsSectionProps) {
</div>
</div>
);
}
}

View File

@ -0,0 +1,44 @@
---
import { Icon } from "astro-icon/components";
interface Talk {
id: string;
name: string;
description: string;
link: string;
}
export interface Props {
talk: Talk;
}
const { talk } = Astro.props;
---
<div
class="card bg-accent shadow-lg w-full sm:w-[calc(50%-1rem)] md:w-96 min-w-[280px] max-w-sm shrink"
>
<div class="card-body p-6">
<h2
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100"
>
{talk.name}
</h2>
<p class="text-center break-words my-4 text-base-100">
{talk.description}
</p>
<div class="card-actions justify-end mt-4">
<a
href={talk.link}
target="_blank"
rel="noopener noreferrer"
class="btn btn-circle btn-sm bg-base-100 hover:bg-base-200 text-accent"
aria-label={`Visit ${talk.name}`}
>
<Icon name="mdi:link" class="text-lg" />
</a>
</div>
</div>
</div>