This commit is contained in:
41
src/components/IconRenderer.tsx
Normal file
41
src/components/IconRenderer.tsx
Normal 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;
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
Reference in New Issue
Block a user