Major re-work!
Some checks failed
Docker Deploy / build-and-push (push) Has been cancelled

This commit is contained in:
2025-06-12 11:09:24 -06:00
parent 324449dd59
commit ab2eb7eeac
13 changed files with 545 additions and 348 deletions

View File

@ -0,0 +1,41 @@
import { Icon } from 'astro-icon/components';
import type { IconType, LucideIcon, AstroIconName, CustomIconComponent } from '../types';
interface IconRendererProps {
icon: IconType;
size?: number;
class?: string;
[key: string]: any; // For additional props like client:load for custom components
}
// Type guard functions
function isLucideIcon(icon: IconType): icon is LucideIcon {
return typeof icon === 'function' && icon.length <= 1; // Lucide icons are function components
}
function isAstroIconName(icon: IconType): icon is AstroIconName {
return typeof icon === 'string';
}
function isCustomComponent(icon: IconType): icon is CustomIconComponent {
return typeof icon === 'function' && !isLucideIcon(icon);
}
export default function IconRenderer({ icon, size, class: className, ...props }: IconRendererProps) {
if (isLucideIcon(icon)) {
const LucideComponent = icon;
return <LucideComponent size={size} class={className} {...props} />;
}
if (isAstroIconName(icon)) {
return <Icon name={icon} class={className} {...props} />;
}
if (isCustomComponent(icon)) {
const CustomComponent = icon;
return <CustomComponent class={className} {...props} />;
}
// Fallback
return null;
}

View File

@ -1,14 +1,7 @@
import { useComputed, useSignal } from "@preact/signals";
import { useEffect } from "preact/hooks";
import {
Home,
NotebookPen,
BriefcaseBusiness,
CodeXml,
Terminal as TerminalIcon,
Megaphone,
} from "lucide-preact";
import { navigationItems } from '../config/data';
import type { LucideIcon } from '../types';
interface NavigationBarProps {
currentPath: string;
@ -26,6 +19,9 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
return prevScrollPos.value > currentPos;
});
// Filter out disabled navigation items
const enabledNavigationItems = navigationItems.filter(item => item.enabled !== false);
// Update client path when location changes
useEffect(() => {
const updatePath = () => {
@ -67,15 +63,6 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
? activePath.slice(0, -1)
: activePath;
const iconMap = {
Home,
NotebookPen,
BriefcaseBusiness,
CodeXml,
TerminalIcon,
Megaphone,
};
useEffect(() => {
let scrollTimer: ReturnType<typeof setTimeout> | undefined;
@ -106,8 +93,8 @@ export default function NavigationBar({ currentPath }: NavigationBarProps) {
>
<div class="overflow-visible">
<ul class="menu menu-horizontal bg-base-200 rounded-box p-1.5 sm:p-2 shadow-lg flex flex-nowrap whitespace-nowrap">
{navigationItems.map((item) => {
const Icon = iconMap[item.icon as keyof typeof iconMap];
{enabledNavigationItems.map((item) => {
const Icon = item.icon as LucideIcon;
const isActive = item.isActive
? item.isActive(normalizedPath)
: normalizedPath === item.path;

View File

@ -1,8 +1,8 @@
---
import { Icon } from 'astro-icon/components';
import type { Project } from '../config/data';
import { Icon } from "astro-icon/components";
import type { Project } from '../types';
export interface Props {
interface Props {
project: Project;
}

View File

@ -8,11 +8,10 @@ interface Skill {
}
interface ResumeSkillsProps {
title: string;
skills: Skill[];
}
export default function ResumeSkills({ title, skills }: ResumeSkillsProps) {
export default function ResumeSkills({ skills }: ResumeSkillsProps) {
const animatedLevels = useSignal<{ [key: string]: number }>({});
const hasAnimated = useSignal(false);
@ -65,31 +64,26 @@ export default function ResumeSkills({ title, skills }: ResumeSkillsProps) {
};
return (
<div id="skills-section" class="card bg-base-200 shadow-xl mb-4 sm:mb-6">
<div class="card-body p-4 sm:p-6">
<h2 class="card-title text-xl sm:text-2xl">{title || "Skills"}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 sm:gap-4">
{skills.map((skill) => {
const currentLevel = animatedLevels.value[skill.id] || 0;
const progressValue = currentLevel * 20; // Convert 1-5 scale to 0-100
<div id="skills-section" class="grid grid-cols-1 md:grid-cols-2 gap-3 sm:gap-4">
{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>
</label>
<progress
class="progress progress-primary w-full h-2 sm:h-3 transition-all duration-100 ease-out"
value={progressValue}
max="100"
></progress>
</div>
);
})}
</div>
</div>
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>
</label>
<progress
class="progress progress-primary w-full h-2 sm:h-3 transition-all duration-100 ease-out"
value={progressValue}
max="100"
></progress>
</div>
);
})}
</div>
);
}

View File

@ -2,22 +2,37 @@
import { Icon } from 'astro-icon/components';
import SpotifyIcon from './SpotifyIcon';
import { socialLinks } from '../config/data';
// Helper function to check if icon is a string (Astro icon)
function isAstroIcon(icon: any): icon is string {
return typeof icon === 'string';
}
// Helper function to check if icon is SpotifyIcon component
function isSpotifyIcon(icon: any): boolean {
return icon === SpotifyIcon;
}
---
<div class="flex flex-row gap-1 sm:gap-4 text-3xl">
{socialLinks.map((link) => (
link.id === 'spotify' ? (
<SpotifyIcon profileUrl={link.url} client:load />
) : (
<a
href={link.url}
target={link.url.startsWith('http') ? '_blank' : undefined}
rel={link.url.startsWith('http') ? 'noopener noreferrer' : undefined}
aria-label={link.ariaLabel}
class="hover:text-primary transition-colors"
>
<Icon name={link.icon} />
</a>
)
))}
{socialLinks.map((link) => {
if (isSpotifyIcon(link.icon)) {
return (
<SpotifyIcon profileUrl={link.url} client:load />
);
} else if (isAstroIcon(link.icon)) {
return (
<a
href={link.url}
target={link.url.startsWith('http') ? '_blank' : undefined}
rel={link.url.startsWith('http') ? 'noopener noreferrer' : undefined}
aria-label={link.ariaLabel}
class="hover:text-primary transition-colors"
>
<Icon name={link.icon} />
</a>
);
}
return null;
})}
</div>

View File

@ -1,8 +1,8 @@
---
import { Icon } from "astro-icon/components";
import type { Talk } from '../config/data';
import type { Talk } from '../types';
export interface Props {
interface Props {
talk: Talk;
}
@ -16,13 +16,29 @@ const { talk } = Astro.props;
<h2
class="card-title text-xl md:text-2xl font-bold justify-center text-center break-words text-base-100"
>
{talk.name}
<a
href={talk.link}
target="_blank"
rel="noopener noreferrer"
class="hover:text-primary transition-colors"
>
{talk.name}
</a>
</h2>
<p class="text-center break-words my-4 text-base-100">
{talk.description}
</p>
<div class="flex flex-col gap-2 mb-4 text-sm">
{talk.date && (
<div class="flex items-center gap-2">
<span class="font-semibold">Date:</span>
<span>{talk.date}</span>
</div>
)}
</div>
<div class="card-actions justify-end mt-4">
<a
href={talk.link}

View File

@ -1,17 +1,28 @@
---
import { Icon } from 'astro-icon/components';
import { techLinks } from '../config/data';
// Helper function to check if icon is a string (Astro icon)
function isAstroIcon(icon: any): icon is string {
return typeof icon === 'string';
}
---
<div class="flex flex-row gap-1 sm:gap-4 text-3xl">
{techLinks.map((link) => (
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
aria-label={link.ariaLabel}
class="hover:text-primary transition-colors"
>
<Icon name={link.icon} />
</a>
))}
{techLinks.map((link) => {
if (isAstroIcon(link.icon)) {
return (
<a
href={link.url}
target="_blank"
rel="noopener noreferrer"
aria-label={link.ariaLabel}
class="hover:text-primary transition-colors"
>
<Icon name={link.icon} />
</a>
);
}
return null;
})}
</div>