Polishing animations

This commit is contained in:
2026-03-30 11:18:36 -07:00
parent b2cd74385f
commit 2c5f64a769
19 changed files with 1426 additions and 996 deletions

View File

@@ -1,5 +1,57 @@
import React from 'react'; import React, { useEffect, useRef, useState } from "react";
import { Code2, BookOpen, RocketIcon, Compass } from 'lucide-react'; import { Code2, BookOpen, RocketIcon, Compass } from "lucide-react";
function AnimateIn({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
const [skip, setSkip] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom > 0;
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
if (inView && isReload) {
setSkip(true);
setVisible(true);
return;
}
if (inView) {
requestAnimationFrame(() => setVisible(true));
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ threshold: 0.15 }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
return (
<div
ref={ref}
className={skip ? "" : "transition-all duration-700 ease-out"}
style={skip ? {} : {
transitionDelay: `${delay}ms`,
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0)" : "translateY(24px)",
}}
>
{children}
</div>
);
}
export default function CurrentFocus() { export default function CurrentFocus() {
const recentProjects = [ const recentProjects = [
@@ -7,126 +59,134 @@ export default function CurrentFocus() {
title: "Darkbox", title: "Darkbox",
description: "My gruvbox theme, with a pure black background", description: "My gruvbox theme, with a pure black background",
href: "/projects/darkbox", href: "/projects/darkbox",
tech: ["Neovim", "Lua"] tech: ["Neovim", "Lua"],
}, },
{ {
title: "Revive Auto Parts", title: "Revive Auto Parts",
description: "A car parts listing site built for a client", description: "A car parts listing site built for a client",
href: "/projects/reviveauto", href: "/projects/reviveauto",
tech: ["Tanstack", "React Query", "Fastapi"] tech: ["Tanstack", "React Query", "Fastapi"],
}, },
{ {
title: "Fhccenter", title: "Fhccenter",
description: "Website made for a private school", description: "Website made for a private school",
href: "/projects/fhccenter", href: "/projects/fhccenter",
tech: ["Nextjs", "Typescript", "Prisma"] tech: ["Nextjs", "Typescript", "Prisma"],
} },
]; ];
return ( return (
<div className="flex justify-center items-center w-full"> <div className="flex justify-center items-center w-full">
<div className="w-full max-w-6xl p-4 sm:px-6 py-6 sm:py-8"> <div className="w-full max-w-6xl p-4 sm:px-6 py-6 sm:py-8">
<h2 className="text-3xl sm:text-4xl font-bold text-center text-yellow-bright mb-8 sm:mb-12"> <AnimateIn>
Current Focus <h2 className="text-3xl sm:text-4xl font-bold text-center text-yellow-bright mb-8 sm:mb-12">
</h2> Current Focus
</h2>
</AnimateIn>
{/* Recent Projects Section */} {/* Recent Projects Section */}
<div className="mb-8 sm:mb-16"> <div className="mb-8 sm:mb-16">
<div className="flex items-center justify-center gap-2 mb-6"> <AnimateIn delay={100}>
<Code2 className="text-yellow-bright" size={24} /> <div className="flex items-center justify-center gap-2 mb-6">
<h3 className="text-xl font-bold text-foreground/90">Recent Projects</h3> <Code2 className="text-yellow-bright" size={24} />
</div> <h3 className="text-xl font-bold text-foreground/90">Recent Projects</h3>
</div>
</AnimateIn>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 max-w-5xl mx-auto"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 max-w-5xl mx-auto">
{recentProjects.map((project) => ( {recentProjects.map((project, i) => (
<a <AnimateIn key={project.title} delay={200 + i * 100}>
href={project.href} <a
key={project.title} href={project.href}
className="p-4 sm:p-6 rounded-lg border border-foreground/10 hover:border-yellow-bright/50 className="block p-4 sm:p-6 rounded-lg border border-foreground/10 hover:border-yellow-bright/50
transition-all duration-300 group bg-background/50" transition-all duration-300 group bg-background/50 h-full"
> >
<h4 className="font-bold text-lg group-hover:text-yellow-bright transition-colors"> <h4 className="font-bold text-lg group-hover:text-yellow-bright transition-colors">
{project.title} {project.title}
</h4> </h4>
<p className="text-foreground/70 mt-2 text-sm sm:text-base">{project.description}</p> <p className="text-foreground/70 mt-2 text-sm sm:text-base">{project.description}</p>
<div className="flex flex-wrap gap-2 mt-3"> <div className="flex flex-wrap gap-2 mt-3">
{project.tech.map((tech) => ( {project.tech.map((tech) => (
<span key={tech} className="text-xs px-2 py-1 rounded-full bg-foreground/5 text-foreground/60"> <span key={tech} className="text-xs px-2 py-1 rounded-full bg-foreground/5 text-foreground/60">
{tech} {tech}
</span> </span>
))} ))}
</div> </div>
</a> </a>
</AnimateIn>
))} ))}
</div> </div>
</div> </div>
{/* Current Learning & Interests */} {/* Current Learning & Interests */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 max-w-5xl mx-auto"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 max-w-5xl mx-auto">
{/* What I'm Learning */} <AnimateIn delay={100}>
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50"> <div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50 h-full">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<BookOpen className="text-green-bright" size={24} /> <BookOpen className="text-green-bright" size={24} />
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Currently Learning</h3> <h3 className="text-lg sm:text-xl font-bold text-foreground/90">Currently Learning</h3>
</div>
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
<span>Rust Programming</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
<span>WebAssembly with Rust</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
<span>HTTP/3 & WebTransport</span>
</li>
</ul>
</div> </div>
<ul className="space-y-3 text-sm sm:text-base text-foreground/70"> </AnimateIn>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
<span>Rust Programming</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
<span>WebAssembly with Rust</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
<span>HTTP/3 & WebTransport</span>
</li>
</ul>
</div>
{/* Project Interests */} <AnimateIn delay={200}>
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50"> <div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50 h-full">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<RocketIcon className="text-blue-bright" size={24} /> <RocketIcon className="text-blue-bright" size={24} />
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Project Interests</h3> <h3 className="text-lg sm:text-xl font-bold text-foreground/90">Project Interests</h3>
</div>
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
<span>AI Model Integration</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
<span>Rust Systems Programming</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
<span>Cross-platform WASM Apps</span>
</li>
</ul>
</div> </div>
<ul className="space-y-3 text-sm sm:text-base text-foreground/70"> </AnimateIn>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
<span>AI Model Integration</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
<span>Rust Systems Programming</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
<span>Cross-platform WASM Apps</span>
</li>
</ul>
</div>
{/* Areas to Explore */} <AnimateIn delay={300}>
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50"> <div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50 h-full">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<Compass className="text-purple-bright" size={24} /> <Compass className="text-purple-bright" size={24} />
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Want to Explore</h3> <h3 className="text-lg sm:text-xl font-bold text-foreground/90">Want to Explore</h3>
</div>
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
<span>LLM Fine-tuning</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
<span>Rust 2024 Edition</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
<span>Real-time Web Transport</span>
</li>
</ul>
</div> </div>
<ul className="space-y-3 text-sm sm:text-base text-foreground/70"> </AnimateIn>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
<span>LLM Fine-tuning</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
<span>Rust 2024 Edition</span>
</li>
<li className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
<span>Real-time Web Transport</span>
</li>
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,56 +1,98 @@
import React from "react"; import React, { useEffect, useRef, useState } from "react";
import { ChevronDownIcon } from "@/components/icons"; import { ChevronDownIcon } from "@/components/icons";
export default function Intro() { export default function Intro() {
const [visible, setVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom > 0;
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
if (inView && isReload) {
setVisible(true);
return;
}
if (inView) {
// Fresh navigation — animate in
requestAnimationFrame(() => setVisible(true));
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ threshold: 0.2 }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
const scrollToNext = () => { const scrollToNext = () => {
const nextSection = document.querySelector("section")?.nextElementSibling; const nextSection = document.querySelector("section")?.nextElementSibling;
if (nextSection) { if (nextSection) {
const offset = nextSection.offsetTop - (window.innerHeight - nextSection.offsetHeight) / 2; // Center the section const offset = (nextSection as HTMLElement).offsetTop - (window.innerHeight - (nextSection as HTMLElement).offsetHeight) / 2;
window.scrollTo({ window.scrollTo({ top: offset, behavior: "smooth" });
top: offset,
behavior: "smooth"
});
} }
}; };
const anim = (delay: number) =>
({
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0)" : "translateY(20px)",
transition: `all 0.7s ease-out ${delay}ms`,
}) as React.CSSProperties;
return ( return (
<div className="w-full max-w-4xl px-4"> <div ref={ref} className="w-full max-w-4xl px-4">
<div className="space-y-8 md:space-y-12"> <div className="space-y-8 md:space-y-12">
<div className="flex flex-col sm:flex-row items-center sm:items-center justify-center gap-8 sm:gap-16"> <div className="flex flex-col sm:flex-row items-center sm:items-center justify-center gap-8 sm:gap-16">
<div className="w-32 h-32 sm:w-48 sm:h-48 shrink-0"> <div
className="w-32 h-32 sm:w-48 sm:h-48 shrink-0"
style={anim(0)}
>
<img <img
src="/me.jpeg" src="/me.jpeg"
alt="Timothy Pidashev" alt="Timothy Pidashev"
className="rounded-lg object-cover w-full h-full ring-2 ring-yellow-bright hover:ring-orange-bright transition-all duration-300" className="rounded-lg object-cover w-full h-full ring-2 ring-yellow-bright hover:ring-orange-bright transition-all duration-300"
/> />
</div> </div>
<div className="text-center sm:text-left space-y-4 sm:space-y-6"> <div className="text-center sm:text-left space-y-4 sm:space-y-6" style={anim(150)}>
<h2 className="text-xl sm:text-5xl font-bold text-yellow-bright"> <h2 className="text-xl sm:text-5xl font-bold text-yellow-bright">
Timothy Pidashev Timothy Pidashev
</h2> </h2>
<div className="text-sm sm:text-xl text-foreground/70 space-y-2 sm:space-y-3"> <div className="text-sm sm:text-xl text-foreground/70 space-y-2 sm:space-y-3">
<p className="flex items-center justify-center font-bold sm:justify-start gap-2"> <p className="flex items-center justify-center font-bold sm:justify-start gap-2" style={anim(300)}>
<span className="text-blue">Software Systems Engineer</span> <span className="text-blue">Software Systems Engineer</span>
</p> </p>
<p className="flex items-center justify-center font-bold sm:justify-start gap-2"> <p className="flex items-center justify-center font-bold sm:justify-start gap-2" style={anim(450)}>
<span className="text-green">Open Source Enthusiast</span> <span className="text-green">Open Source Enthusiast</span>
</p> </p>
<p className="flex items-center justify-center font-bold sm:justify-start gap-2"> <p className="flex items-center justify-center font-bold sm:justify-start gap-2" style={anim(600)}>
<span className="text-yellow">Coffee Connoisseur</span> <span className="text-yellow">Coffee Connoisseur</span>
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<div className="space-y-8"> <div className="space-y-8" style={anim(750)}>
<p className="text-foreground/80 text-center text-base sm:text-2xl italic max-w-3xl mx-auto font-medium"> <p className="text-foreground/80 text-center text-base sm:text-2xl italic max-w-3xl mx-auto font-medium">
"Turning coffee into code" isn't just a clever phrase "Turning coffee into code" isn't just a clever phrase
<span className="text-aqua-bright"> it's how I approach each project:</span> <span className="text-aqua-bright"> it's how I approach each project:</span>
<span className="text-purple-bright"> methodically,</span> <span className="text-purple-bright"> methodically,</span>
<span className="text-blue-bright"> with attention to detail,</span> <span className="text-blue-bright"> with attention to detail,</span>
<span className="text-green-bright"> and a refined process.</span> <span className="text-green-bright"> and a refined process.</span>
</p> </p>
<div className="flex justify-center"> <div className="flex justify-center" style={anim(900)}>
<button <button
onClick={scrollToNext} onClick={scrollToNext}
className="text-foreground/50 hover:text-yellow-bright transition-colors duration-300" className="text-foreground/50 hover:text-yellow-bright transition-colors duration-300"
aria-label="Scroll to next section" aria-label="Scroll to next section"

View File

@@ -1,64 +1,115 @@
import React from 'react'; import React, { useEffect, useRef, useState } from "react";
import { Fish, Mountain, Book, Car } from 'lucide-react'; import { Cross, Fish, Mountain, Book } from "lucide-react";
function AnimateIn({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
const [skip, setSkip] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom > 0;
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
if (inView && isReload) {
setSkip(true);
setVisible(true);
return;
}
if (inView) {
requestAnimationFrame(() => setVisible(true));
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ threshold: 0.15 }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
return (
<div
ref={ref}
className={skip ? "" : "transition-all duration-700 ease-out"}
style={skip ? {} : {
transitionDelay: `${delay}ms`,
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0) scale(1)" : "translateY(20px) scale(0.97)",
}}
>
{children}
</div>
);
}
const interests = [
{
icon: <Cross className="text-red-bright" size={20} />,
title: "Faith",
description: "My walk with Jesus is the foundation of everything I do, guiding my purpose and perspective",
},
{
icon: <Fish className="text-blue-bright" size={20} />,
title: "Fishing",
description: "Finding peace and adventure on the water, always looking for the next great fishing spot",
},
{
icon: <Mountain className="text-green-bright" size={20} />,
title: "Hiking",
description: "Exploring trails with friends and seeking out scenic viewpoints in nature",
},
{
icon: <Book className="text-purple-bright" size={20} />,
title: "Reading",
description: "Deep diving into novels & technical books that expand my horizons & captivate my mind",
},
];
export default function OutsideCoding() { export default function OutsideCoding() {
const interests = [
{
icon: <Fish className="text-blue-bright" size={20} />,
title: "Fishing",
description: "Finding peace and adventure on the water, always looking for the next great fishing spot"
},
{
icon: <Mountain className="text-green-bright" size={20} />,
title: "Hiking",
description: "Exploring trails with friends and seeking out scenic viewpoints in nature"
},
{
icon: <Book className="text-purple-bright" size={20} />,
title: "Reading",
description: "Deep diving into novels & technical books that expand my horizons & captivate my mind"
},
{
icon: <Car className="text-yellow-bright" size={20} />,
title: "Project Cars",
description: "Working on automotive projects, modifying & restoring sporty sedans"
}
];
return ( return (
<div className="flex justify-center items-center w-full"> <div className="flex justify-center items-center w-full">
<div className="w-full max-w-4xl px-4 py-8"> <div className="w-full max-w-4xl px-4 py-8">
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8"> <AnimateIn>
Outside of Programming <h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8">
</h2> Outside of Programming
</h2>
</AnimateIn>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6"> <div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{interests.map((interest) => ( {interests.map((interest, i) => (
<div <AnimateIn key={interest.title} delay={100 + i * 100}>
key={interest.title} <div
className="flex flex-col items-center text-center p-4 rounded-lg border border-foreground/10 className="flex flex-col items-center text-center p-4 rounded-lg border border-foreground/10
hover:border-yellow-bright/50 transition-all duration-300 bg-background/50" hover:border-yellow-bright/50 transition-all duration-300 bg-background/50 h-full"
> >
<div className="mb-3"> <div className="mb-3">{interest.icon}</div>
{interest.icon} <h3 className="font-bold text-foreground/90 mb-2">{interest.title}</h3>
<p className="text-sm text-foreground/70">{interest.description}</p>
</div> </div>
<h3 className="font-bold text-foreground/90 mb-2"> </AnimateIn>
{interest.title}
</h3>
<p className="text-sm text-foreground/70">
{interest.description}
</p>
</div>
))} ))}
</div> </div>
<p className="text-center text-foreground/80 mt-8 max-w-2xl mx-auto text-sm md:text-base italic"> <AnimateIn delay={500}>
When I'm not writing code, you'll find me <p className="text-center text-foreground/80 mt-8 max-w-2xl mx-auto text-sm md:text-base italic">
<span className="text-blue-bright"> out on the water,</span> When I'm not writing code, you'll find me
<span className="text-green-bright"> hiking trails,</span> <span className="text-red-bright"> walking with Christ,</span>
<span className="text-purple-bright"> reading books,</span> <span className="text-blue-bright"> out on the water,</span>
<span className="text-yellow-bright"> or modifying my current ride.</span> <span className="text-green-bright"> hiking trails,</span>
</p> <span className="text-purple-bright"> or reading books.</span>
</p>
</AnimateIn>
</div> </div>
</div> </div>
); );

View File

@@ -1,34 +1,19 @@
import React, { useState, useEffect } from 'react'; import React from "react";
export const ActivityGrid = () => { interface ActivityDay {
const [data, setData] = useState([]); grand_total: { total_seconds: number };
const [loading, setLoading] = useState(true); date: string;
const [error, setError] = useState(null); }
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; interface ActivityGridProps {
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; data: ActivityDay[];
}
useEffect(() => { export const ActivityGrid = ({ data }: ActivityGridProps) => {
const fetchData = async () => { const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
try { const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const response = await fetch('/api/wakatime');
if (!response.ok) {
throw new Error('Failed to fetch data');
}
const result = await response.json();
setData(result.data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData(); const getIntensity = (hours: number) => {
}, []);
// Get intensity based on coding hours (0-4 for different shades)
const getIntensity = (hours) => {
if (hours === 0) return 0; if (hours === 0) return 0;
if (hours < 2) return 1; if (hours < 2) return 1;
if (hours < 4) return 2; if (hours < 4) return 2;
@@ -36,20 +21,18 @@ export const ActivityGrid = () => {
return 4; return 4;
}; };
// Get color class based on intensity const getColorClass = (intensity: number) => {
const getColorClass = (intensity) => { if (intensity === 0) return "bg-foreground/5";
if (intensity === 0) return 'bg-foreground/5'; if (intensity === 1) return "bg-green-DEFAULT/30";
if (intensity === 1) return 'bg-green-DEFAULT/30'; if (intensity === 2) return "bg-green-DEFAULT/60";
if (intensity === 2) return 'bg-green-DEFAULT/60'; if (intensity === 3) return "bg-green-DEFAULT/80";
if (intensity === 3) return 'bg-green-DEFAULT/80'; return "bg-green-bright";
return 'bg-green-bright';
}; };
// Group data by week const weeks: ActivityDay[][] = [];
const weeks = []; let currentWeek: ActivityDay[] = [];
let currentWeek = [];
if (data && data.length > 0) {
if (data.length > 0) {
data.forEach((day, index) => { data.forEach((day, index) => {
currentWeek.push(day); currentWeek.push(day);
if (currentWeek.length === 7 || index === data.length - 1) { if (currentWeek.length === 7 || index === data.length - 1) {
@@ -59,31 +42,19 @@ export const ActivityGrid = () => {
}); });
} }
if (loading) { if (!data || data.length === 0) {
return ( return null;
<div className="bg-background border border-foreground/10 rounded-lg p-6">
<div className="text-lg text-aqua-bright mb-6">Loading activity data...</div>
</div>
);
}
if (error) {
return (
<div className="bg-background border border-foreground/10 rounded-lg p-6">
<div className="text-lg text-red-bright mb-6">Error loading activity: {error}</div>
</div>
);
} }
return ( return (
<div className="bg-background border border-foreground/10 rounded-lg p-6 hover:border-foreground/20 transition-colors"> <div className="bg-background border border-foreground/10 rounded-lg p-6 hover:border-foreground/20 transition-colors">
<div className="text-lg text-aqua-bright mb-6">Activity</div> <div className="text-lg text-aqua-bright mb-6">Activity</div>
<div className="flex gap-4"> <div className="flex gap-4">
{/* Days labels */} {/* Days labels */}
<div className="flex flex-col gap-2 pt-6 text-xs"> <div className="flex flex-col gap-2 pt-6 text-xs">
{days.map((day, i) => ( {days.map((day, i) => (
<div key={day} className="h-3 text-foreground/60">{i % 2 === 0 ? day : ''}</div> <div key={day} className="h-3 text-foreground/60">{i % 2 === 0 ? day : ""}</div>
))} ))}
</div> </div>
{/* Grid */} {/* Grid */}
@@ -94,17 +65,16 @@ export const ActivityGrid = () => {
{week.map((day, dayIndex) => { {week.map((day, dayIndex) => {
const hours = day.grand_total.total_seconds / 3600; const hours = day.grand_total.total_seconds / 3600;
const intensity = getIntensity(hours); const intensity = getIntensity(hours);
return ( return (
<div <div
key={dayIndex} key={dayIndex}
className={`w-3 h-3 rounded-sm ${getColorClass(intensity)} className={`w-3 h-3 rounded-sm ${getColorClass(intensity)}
hover:ring-1 hover:ring-foreground/30 transition-all cursor-pointer hover:ring-1 hover:ring-foreground/30 transition-all cursor-pointer
group relative`} group relative`}
> >
{/* Tooltip */} <div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 p-2
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 p-2 bg-background border border-foreground/10 rounded-md opacity-0
bg-background border border-foreground/10 rounded-md opacity-0
group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap text-xs"> group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap text-xs">
{hours.toFixed(1)} hours on {day.date} {hours.toFixed(1)} hours on {day.date}
</div> </div>
@@ -120,10 +90,10 @@ export const ActivityGrid = () => {
const date = new Date(week[0].date); const date = new Date(week[0].date);
const isFirstOfMonth = date.getDate() <= 7; const isFirstOfMonth = date.getDate() <= 7;
return ( return (
<div <div
key={i} key={i}
className="w-3 mx-1" className="w-3 mx-1"
style={{ marginLeft: i === 0 ? '0' : undefined }} style={{ marginLeft: i === 0 ? "0" : undefined }}
> >
{isFirstOfMonth && months[date.getMonth()]} {isFirstOfMonth && months[date.getMonth()]}
</div> </div>
@@ -136,10 +106,7 @@ export const ActivityGrid = () => {
<div className="flex items-center gap-2 mt-4 text-xs text-foreground/60"> <div className="flex items-center gap-2 mt-4 text-xs text-foreground/60">
<span>Less</span> <span>Less</span>
{[0, 1, 2, 3, 4].map((intensity) => ( {[0, 1, 2, 3, 4].map((intensity) => (
<div <div key={intensity} className={`w-3 h-3 rounded-sm ${getColorClass(intensity)}`} />
key={intensity}
className={`w-3 h-3 rounded-sm ${getColorClass(intensity)}`}
/>
))} ))}
<span>More</span> <span>More</span>
</div> </div>

View File

@@ -1,98 +1,115 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
const Stats = () => { const Stats = () => {
const [stats, setStats] = useState<any>(null); const [stats, setStats] = useState<any>(null);
const [error, setError] = useState(false);
const [count, setCount] = useState(0); const [count, setCount] = useState(0);
const [isFinished, setIsFinished] = useState(false);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [skipAnim, setSkipAnim] = useState(false);
const hasAnimated = useRef(false);
const sectionRef = useRef<HTMLDivElement>(null);
// Fetch data on mount
useEffect(() => { useEffect(() => {
setIsVisible(true); fetch("/api/wakatime/alltime")
.then((res) => {
const fetchStats = async () => { if (!res.ok) throw new Error("API error");
try { return res.json();
const res = await fetch("/api/wakatime/alltime"); })
const data = await res.json(); .then((data) => setStats(data.data))
setStats(data.data); .catch(() => setError(true));
startCounting(data.data.total_seconds);
} catch (error) {
console.error("Error fetching stats:", error);
}
};
fetchStats();
}, []); }, []);
const startCounting = (totalSeconds: number) => { // Observe visibility — skip animation if already in view on mount
useEffect(() => {
const el = sectionRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom > 0;
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
if (inView && isReload) {
setSkipAnim(true);
setIsVisible(true);
return;
}
if (inView) {
requestAnimationFrame(() => setIsVisible(true));
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold: 0.3 }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
// Start counter when both visible and data is ready
useEffect(() => {
if (!isVisible || !stats || hasAnimated.current) return;
hasAnimated.current = true;
const totalSeconds = stats.total_seconds;
const duration = 2000; const duration = 2000;
const steps = 60; const steps = 60;
let currentStep = 0; let currentStep = 0;
const timer = setInterval(() => { const timer = setInterval(() => {
currentStep += 1; currentStep += 1;
if (currentStep >= steps) { if (currentStep >= steps) {
setCount(totalSeconds); setCount(totalSeconds);
setIsFinished(true);
clearInterval(timer); clearInterval(timer);
return; return;
} }
const progress = 1 - Math.pow(1 - currentStep / steps, 4); const progress = 1 - Math.pow(1 - currentStep / steps, 4);
setCount(Math.floor(totalSeconds * progress)); setCount(Math.floor(totalSeconds * progress));
}, duration / steps); }, duration / steps);
return () => clearInterval(timer); return () => clearInterval(timer);
}; }, [isVisible, stats]);
if (!stats) return null; if (error) return null;
if (!stats) return <div ref={sectionRef} className="min-h-[50vh]" />;
const hours = Math.floor(count / 3600); const hours = Math.floor(count / 3600);
const formattedHours = hours.toLocaleString("en-US", { const formattedHours = hours.toLocaleString("en-US", {
minimumIntegerDigits: 4, minimumIntegerDigits: 4,
useGrouping: true useGrouping: true,
}); });
return ( return (
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-6"> <div ref={sectionRef} className="flex flex-col items-center justify-center min-h-[50vh] gap-6">
<div className={` <div className={skipAnim ? "text-2xl opacity-80" : `text-2xl opacity-0 ${isVisible ? "animate-fade-in-first" : ""}`}>
text-2xl opacity-0
${isVisible ? "animate-fade-in-first" : ""}
`}>
I've spent I've spent
</div> </div>
<div className="relative"> <div className="relative">
<div className="text-8xl text-center relative z-10"> <div className="text-8xl text-center relative z-10">
<span className="font-bold relative"> <span className="font-bold relative">
<span className={` <span className={skipAnim ? "bg-gradient-text" : `bg-gradient-text opacity-0 ${isVisible ? "animate-fade-in-second" : ""}`}>
bg-gradient-text opacity-0
${isVisible ? "animate-fade-in-second" : ""}
`}>
{formattedHours} {formattedHours}
</span> </span>
</span> </span>
<span className={` <span className={skipAnim ? "text-4xl opacity-60 ml-4" : `text-4xl opacity-0 ${isVisible ? "animate-slide-in-hours" : ""}`}>
text-4xl opacity-0
${isVisible ? "animate-slide-in-hours" : ""}
`}>
hours hours
</span> </span>
</div> </div>
</div> </div>
<div className="flex flex-col items-center gap-3 text-center"> <div className="flex flex-col items-center gap-3 text-center">
<div className={` <div className={skipAnim ? "text-xl opacity-80" : `text-xl opacity-0 ${isVisible ? "animate-fade-in-third" : ""}`}>
text-xl opacity-0
${isVisible ? "animate-fade-in-third" : ""}
`}>
writing code & building apps writing code & building apps
</div> </div>
<div className={skipAnim ? "flex items-center gap-3 text-lg opacity-60" : `flex items-center gap-3 text-lg opacity-0 ${isVisible ? "animate-fade-in-fourth" : ""}`}>
<div className={`
flex items-center gap-3 text-lg opacity-0
${isVisible ? "animate-fade-in-fourth" : ""}
`}>
<span>since</span> <span>since</span>
<span className="text-green-bright font-bold">{stats.range.start_text}</span> <span className="text-green-bright font-bold">{stats.range.start_text}</span>
</div> </div>
@@ -100,15 +117,7 @@ const Stats = () => {
<style jsx>{` <style jsx>{`
.bg-gradient-text { .bg-gradient-text {
background: linear-gradient( background: linear-gradient(90deg, #fbbf24, #f59e0b, #d97706, #b45309, #f59e0b, #fbbf24);
90deg,
#fbbf24,
#f59e0b,
#d97706,
#b45309,
#f59e0b,
#fbbf24
);
background-size: 200% auto; background-size: 200% auto;
color: transparent; color: transparent;
background-clip: text; background-clip: text;
@@ -116,95 +125,32 @@ const Stats = () => {
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
.animate-gradient {
animation: gradient 4s linear infinite;
}
@keyframes gradient {
0% { background-position: 0% 50%; }
100% { background-position: 200% 50%; }
}
@keyframes fadeInFirst { @keyframes fadeInFirst {
0% { from { opacity: 0; transform: translateY(20px); }
opacity: 0; to { opacity: 0.8; transform: translateY(0); }
transform: translateY(20px);
}
100% {
opacity: 0.8;
transform: translateY(0);
}
} }
@keyframes fadeInSecond { @keyframes fadeInSecond {
0% { from { opacity: 0; transform: translateY(20px); }
opacity: 0; to { opacity: 1; transform: translateY(0); }
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
} }
@keyframes slideInHours { @keyframes slideInHours {
0% { from { opacity: 0; transform: translateX(20px); margin-left: 0; }
opacity: 0; to { opacity: 0.6; transform: translateX(0); margin-left: 1rem; }
transform: translateX(20px);
margin-left: 0;
}
100% {
opacity: 0.6;
transform: translateX(0);
margin-left: 1rem;
}
} }
@keyframes fadeInThird { @keyframes fadeInThird {
0% { from { opacity: 0; transform: translateY(20px); }
opacity: 0; to { opacity: 0.8; transform: translateY(0); }
transform: translateY(20px);
}
100% {
opacity: 0.8;
transform: translateY(0);
}
} }
@keyframes fadeInFourth { @keyframes fadeInFourth {
0% { from { opacity: 0; transform: translateY(20px); }
opacity: 0; to { opacity: 0.6; transform: translateY(0); }
transform: translateY(20px);
}
100% {
opacity: 0.6;
transform: translateY(0);
}
} }
.animate-fade-in-first { .animate-fade-in-first { animation: fadeInFirst 0.7s ease-out forwards; }
animation: fadeInFirst 0.7s ease-out forwards; .animate-fade-in-second { animation: fadeInSecond 0.7s ease-out 0.4s forwards; }
} .animate-slide-in-hours { animation: slideInHours 0.7s ease-out 0.6s forwards; }
.animate-fade-in-third { animation: fadeInThird 0.7s ease-out 0.8s forwards; }
.animate-fade-in-second { .animate-fade-in-fourth { animation: fadeInFourth 0.7s ease-out 1s forwards; }
animation: fadeInSecond 0.7s ease-out forwards;
animation-delay: 0.4s;
}
.animate-slide-in-hours {
animation: slideInHours 0.7s ease-out forwards;
animation-delay: 0.6s;
}
.animate-fade-in-third {
animation: fadeInThird 0.7s ease-out forwards;
animation-delay: 0.8s;
}
.animate-fade-in-fourth {
animation: fadeInFourth 0.7s ease-out forwards;
animation-delay: 1s;
}
`}</style> `}</style>
</div> </div>
); );

View File

@@ -1,35 +1,66 @@
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { Clock, CalendarClock, CodeXml, Computer } from "lucide-react"; import { Clock, CalendarClock, CodeXml, Computer } from "lucide-react";
import { ActivityGrid } from "@/components/about/stats-activity"; import { ActivityGrid } from "@/components/about/stats-activity";
const DetailedStats = () => { const DetailedStats = () => {
const [stats, setStats] = useState(null); const [stats, setStats] = useState<any>(null);
const [activity, setActivity] = useState(null); const [activity, setActivity] = useState<any>(null);
const [isVisible, setIsVisible] = useState(false); const [error, setError] = useState(false);
const [visible, setVisible] = useState(false);
const [skipAnim, setSkipAnim] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
fetch("/api/wakatime/detailed") fetch("/api/wakatime/detailed")
.then(res => res.json()) .then((res) => {
.then(data => { if (!res.ok) throw new Error();
setStats(data.data); return res.json();
setIsVisible(true);
}) })
.catch(error => { .then((data) => setStats(data.data))
console.error("Error fetching stats:", error); .catch(() => setError(true));
});
fetch("/api/wakatime/activity") fetch("/api/wakatime/activity")
.then(res => res.json()) .then((res) => {
.then(data => { if (!res.ok) throw new Error();
setActivity(data.data); return res.json();
}) })
.catch(error => { .then((data) => setActivity(data.data))
console.error("Error fetching activity:", error); .catch(() => {});
});
}, []); }, []);
if (!stats) return null; useEffect(() => {
const el = containerRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom > 0;
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
if (inView && isReload) {
setSkipAnim(true);
setVisible(true);
return;
}
if (inView) {
requestAnimationFrame(() => setVisible(true));
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ threshold: 0.1, rootMargin: "-15% 0px" }
);
observer.observe(el);
return () => observer.disconnect();
}, [stats]);
if (error) return null;
const progressColors = [ const progressColors = [
"bg-red-bright", "bg-red-bright",
@@ -38,138 +69,163 @@ const DetailedStats = () => {
"bg-green-bright", "bg-green-bright",
"bg-blue-bright", "bg-blue-bright",
"bg-purple-bright", "bg-purple-bright",
"bg-aqua-bright" "bg-aqua-bright",
]; ];
const statCards = stats
? [
{
title: "Total Time",
value: `${Math.round((stats.total_seconds / 3600) * 10) / 10}`,
unit: "hours",
subtitle: "this week",
color: "text-yellow-bright",
borderHover: "hover:border-yellow-bright/50",
icon: Clock,
iconColor: "stroke-yellow-bright",
},
{
title: "Daily Average",
value: `${Math.round((stats.daily_average / 3600) * 10) / 10}`,
unit: "hours",
subtitle: "per day",
color: "text-orange-bright",
borderHover: "hover:border-orange-bright/50",
icon: CalendarClock,
iconColor: "stroke-orange-bright",
},
{
title: "Primary Editor",
value: stats.editors?.[0]?.name || "None",
unit: `${Math.round(stats.editors?.[0]?.percent || 0)}%`,
subtitle: "of the time",
color: "text-blue-bright",
borderHover: "hover:border-blue-bright/50",
icon: CodeXml,
iconColor: "stroke-blue-bright",
},
{
title: "Operating System",
value: stats.operating_systems?.[0]?.name || "None",
unit: `${Math.round(stats.operating_systems?.[0]?.percent || 0)}%`,
subtitle: "of the time",
color: "text-green-bright",
borderHover: "hover:border-green-bright/50",
icon: Computer,
iconColor: "stroke-green-bright",
},
]
: [];
const languages =
stats?.languages?.slice(0, 7).map((lang: any, index: number) => ({
name: lang.name,
percent: Math.round(lang.percent),
time: Math.round((lang.total_seconds / 3600) * 10) / 10 + " hrs",
color: progressColors[index % progressColors.length],
})) || [];
return ( return (
<div className="flex flex-col gap-10 md:py-32 w-full max-w-[1200px] mx-auto px-4"> <div ref={containerRef} className="flex flex-col gap-10 md:py-32 w-full max-w-[1200px] mx-auto px-4 min-h-[50vh]">
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright"> {!stats ? null : (
Weekly Statistics <>
</h2> {/* Header */}
<h2
className={`text-2xl md:text-4xl font-bold text-center text-yellow-bright ${skipAnim ? "" : "transition-all duration-700 ease-out"}`}
style={skipAnim ? {} : {
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0)" : "translateY(20px)",
}}
>
Weekly Statistics
</h2>
{/* Top Stats Grid */} {/* Stat Cards */}
<div className={` <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
grid grid-cols-1 md:grid-cols-2 gap-8 {statCards.map((card, i) => {
transition-all duration-700 transform const Icon = card.icon;
${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-4 opacity-0'} return (
`}> <div
{/* Total Time */} key={card.title}
<StatsCard className={`bg-background border border-foreground/10 rounded-lg p-6 ${card.borderHover} ${skipAnim ? "" : "transition-all duration-500 ease-out"}`}
title="Total Time" style={skipAnim ? {} : {
value={`${Math.round(stats.total_seconds / 3600 * 10) / 10}`} transitionDelay: `${150 + i * 100}ms`,
unit="hours" opacity: visible ? 1 : 0,
subtitle="this week" transform: visible ? "translateY(0)" : "translateY(24px)",
color="text-yellow-bright" }}
icon={Clock} >
iconColor="stroke-yellow-bright" <div className="flex gap-4 items-center">
/> <div className="p-3 rounded-lg bg-foreground/5">
<Icon className={`w-6 h-6 ${card.iconColor}`} strokeWidth={1.5} />
</div>
<div className="flex flex-col">
<div className={`${card.color} text-sm mb-1`}>{card.title}</div>
<div className="flex items-baseline gap-2">
<div className="text-2xl font-bold">{card.value}</div>
<div className="text-lg opacity-80">{card.unit}</div>
</div>
<div className="text-xs opacity-50 mt-0.5">{card.subtitle}</div>
</div>
</div>
</div>
);
})}
</div>
{/* Daily Average */} {/* Languages */}
<StatsCard <div
title="Daily Average" className={`bg-background border border-foreground/10 rounded-lg p-6 hover:border-purple-bright/50 ${skipAnim ? "" : "transition-all duration-700 ease-out"}`}
value={`${Math.round(stats.daily_average / 3600 * 10) / 10}`} style={skipAnim ? {} : {
unit="hours" transitionDelay: "550ms",
subtitle="per day" opacity: visible ? 1 : 0,
color="text-orange-bright" transform: visible ? "translateY(0)" : "translateY(24px)",
icon={CalendarClock} }}
iconColor="stroke-orange-bright" >
/> <div className="text-purple-bright mb-6 text-lg">Languages</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-5">
{languages.map((lang: any, i: number) => (
<div key={lang.name} className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium">{lang.name}</span>
<span className="text-sm opacity-70 tabular-nums">{lang.time}</span>
</div>
<div className="flex gap-3 items-center">
<div className="flex-grow h-2 bg-foreground/5 rounded-full overflow-hidden">
<div
className={`h-full ${lang.color} rounded-full`}
style={{
width: visible ? `${lang.percent}%` : "0%",
opacity: 0.85,
transition: skipAnim ? "none" : `width 1s ease-out ${700 + i * 80}ms`,
}}
/>
</div>
<span className="text-xs text-foreground/50 min-w-[36px] text-right tabular-nums">
{lang.percent}%
</span>
</div>
</div>
))}
</div>
</div>
{/* Editors */} {/* Activity Grid */}
<StatsCard {activity && (
title="Primary Editor" <div
value={stats.editors?.[0]?.name || "None"} className={skipAnim ? "" : "transition-all duration-700 ease-out"}
unit={`${Math.round(stats.editors?.[0]?.percent || 0)}%`} style={skipAnim ? {} : {
subtitle="of the time" transitionDelay: "750ms",
color="text-blue-bright" opacity: visible ? 1 : 0,
icon={CodeXml} transform: visible ? "translateY(0)" : "translateY(24px)",
iconColor="stroke-blue-bright" }}
/> >
<ActivityGrid data={activity} />
{/* OS */} </div>
<StatsCard )}
title="Operating System" </>
value={stats.operating_systems?.[0]?.name || "None"}
unit={`${Math.round(stats.operating_systems?.[0]?.percent || 0)}%`}
subtitle="of the time"
color="text-green-bright"
icon={Computer}
iconColor="stroke-green-bright"
/>
</div>
{/* Languages */}
<div className={`
transition-all duration-700 delay-200 transform
${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-4 opacity-0'}
`}>
<DetailCard
title="Languages"
items={stats.languages?.slice(0, 7).map((lang, index) => ({
name: lang.name,
value: Math.round(lang.percent) + '%',
time: Math.round(lang.total_seconds / 3600 * 10) / 10 + ' hrs',
color: progressColors[index % progressColors.length]
})) || []}
titleColor="text-purple-bright"
/>
{/* Activity Grid */}
{activity && (
<div className={`
transition-all duration-700 delay-300 transform
${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-4 opacity-0'}
`}>
<ActivityGrid data={activity} />
</div>
)} )}
</div>
</div> </div>
); );
}; };
const StatsCard = ({ title, value, unit, subtitle, color, icon: Icon, iconColor }) => (
<div className="bg-background border border-foreground/10 rounded-lg p-6 hover:border-yellow transition-colors flex items-center justify-center">
<div className="flex gap-3 items-center">
<Icon className={`w-6 h-6 ${iconColor}`} strokeWidth={1.5} />
<div className="flex flex-col items-center">
<div className={`${color} text-lg mb-1`}>{title}</div>
<div className="flex items-baseline gap-2">
<div className="text-2xl font-bold">{value}</div>
<div className="text-lg opacity-80">{unit}</div>
</div>
<div className="text-sm opacity-60 mt-1">{subtitle}</div>
</div>
</div>
</div>
);
const DetailCard = ({ title, items, titleColor }) => (
<div className="bg-background border border-foreground/10 rounded-lg p-6 hover:border-yellow transition-colors">
<div className={`${titleColor} mb-6 text-lg`}>{title}</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-6">
{items.map((item) => (
<div key={item.name} className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<span className="text-base font-medium">{item.name}</span>
<span className="text-base opacity-80">{item.value}</span>
</div>
<div className="flex gap-3 items-center">
<div className="flex-grow h-2 bg-foreground/5 rounded-full overflow-hidden">
<div
className={`h-full ${item.color} rounded-full transition-all duration-1000`}
style={{
width: item.value,
opacity: '0.8'
}}
/>
</div>
<span className="text-sm text-foreground/60 min-w-[70px] text-right">{item.time}</span>
</div>
</div>
))}
</div>
</div>
);
export default DetailedStats; export default DetailedStats;

View File

@@ -1,78 +1,181 @@
import React from "react"; import React, { useEffect, useRef, useState } from "react";
import { Check, Code, GitBranch, Star } from "lucide-react"; import { Check, Code, GitBranch, Star, Rocket } from "lucide-react";
const timelineItems = [
{
year: "2026",
title: "Present",
description: "Building domain-specific languages, diving deep into the Salesforce ecosystem, and writing production Java and Python daily. The craft keeps evolving.",
technologies: ["Java", "Python", "Salesforce", "DSLs"],
icon: <Rocket className="text-red-bright" size={20} />,
},
{
year: "2024",
title: "Shipping & Scaling",
description: "The wisdom of past ventures now flows through my work, whether crafting elegant CRUD applications or embarking on bold projects that expand my limits.",
technologies: ["Rust", "Typescript", "Go", "Postgres"],
icon: <Code className="text-yellow-bright" size={20} />,
},
{
year: "2022",
title: "Diving Deeper",
description: "The worlds of systems programming and scalable infrastructure collided as I explored low-level C++ graphics programming and containerization with Docker.",
technologies: ["C++", "Cmake", "Docker", "Docker Compose"],
icon: <GitBranch className="text-green-bright" size={20} />,
},
{
year: "2020",
title: "Exploring the Stack",
description: "Starting with pure HTML and CSS, I explored the foundations of web development, gradually venturing into JavaScript and React to bring my static pages to life.",
technologies: ["Javascript", "Tailwind", "React", "Express"],
icon: <Star className="text-blue-bright" size={20} />,
},
{
year: "2018",
title: "Starting the Journey",
description: "An elective Python class in 8th grade transformed my keen interest in programming into a relentless obsession, one that drove me to constantly explore new depths.",
technologies: ["Python", "Discord.py", "Asyncio", "Sqlite"],
icon: <Check className="text-purple-bright" size={20} />,
},
];
function TimelineCard({ item, index }: { item: (typeof timelineItems)[number]; index: number }) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
const [skip, setSkip] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom > 0;
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
if (inView && isReload) {
setSkip(true);
setVisible(true);
return;
}
if (inView) {
requestAnimationFrame(() => setVisible(true));
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ threshold: 0.2 }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
const isLeft = index % 2 === 0;
return (
<div ref={ref} className="relative mb-8 md:mb-12 last:mb-0">
<div className={`flex flex-col sm:flex-row items-start ${isLeft ? "sm:flex-row-reverse" : ""}`}>
{/* Node */}
<div
className={`
absolute -left-8 sm:left-1/2 w-6 h-6 sm:w-8 sm:h-8 bg-background
rounded-full border-2 border-yellow-bright sm:-translate-x-1/2
flex items-center justify-center z-10
${skip ? "" : "transition-all duration-500"}
${visible ? "scale-100 opacity-100" : "scale-0 opacity-0"}
`}
>
{item.icon}
</div>
{/* Card */}
<div
className={`
w-full sm:w-[calc(50%-32px)]
${isLeft ? "sm:pr-8 md:pr-12" : "sm:pl-8 md:pl-12"}
${skip ? "" : "transition-all duration-700 ease-out"}
${visible
? "opacity-100 translate-x-0"
: `opacity-0 ${isLeft ? "sm:translate-x-8" : "sm:-translate-x-8"} translate-y-4 sm:translate-y-0`
}
`}
>
<div
className="p-4 sm:p-6 bg-background/50 rounded-lg border border-foreground/10
hover:border-yellow-bright/50 transition-colors duration-300"
>
<span className="text-xs sm:text-sm font-mono text-yellow-bright">{item.year}</span>
<h3 className="text-lg sm:text-xl font-bold text-foreground/90 mt-2">{item.title}</h3>
<p className="text-sm sm:text-base text-foreground/70 mt-2">{item.description}</p>
<div className="flex flex-wrap gap-2 mt-3">
{item.technologies.map((tech) => (
<span
key={tech}
className="px-2 py-1 text-xs sm:text-sm rounded-full bg-foreground/5
text-foreground/60 hover:text-yellow-bright transition-colors duration-300"
>
{tech}
</span>
))}
</div>
</div>
</div>
</div>
</div>
);
}
export default function Timeline() { export default function Timeline() {
const timelineItems = [ const lineRef = useRef<HTMLDivElement>(null);
{ const containerRef = useRef<HTMLDivElement>(null);
year: "2024", const [lineHeight, setLineHeight] = useState(0);
title: "Present",
description: "The wisdom of past ventures now flows through my work, whether crafting elegant CRUD applications or embarking on bold projects that expand my limits.", useEffect(() => {
technologies: ["Rust", "Typescript", "Go", "Postgres"], const container = containerRef.current;
icon: <Code className="text-yellow-bright" size={20} /> if (!container) return;
},
{ const observer = new IntersectionObserver(
year: "2022", ([entry]) => {
title: "Diving Deeper", if (entry.isIntersecting) {
description: "The worlds of systems programming and scalable infrastructure collided as I explored low-level C++ graphics programming and containerization with Docker.", // Animate line to full height over time
technologies: ["C++", "Cmake", "Docker", "Docker Compose"], const el = lineRef.current;
icon: <GitBranch className="text-green-bright" size={20} /> if (el) {
}, setLineHeight(100);
{ }
year: "2020", observer.disconnect();
title: "Exploring the Stack", }
description: "Starting with pure HTML and CSS, I explored the foundations of web development, gradually venturing into JavaScript and React to bring my static pages to life.", },
technologies: ["Javascript", "Tailwind", "React", "Express"], { threshold: 0.1 }
icon: <Star className="text-blue-bright" size={20} /> );
},
{ observer.observe(container);
year: "2018", return () => observer.disconnect();
title: "Starting the Journey", }, []);
description: "An elective Python class in 8th grade transformed my keen interest in programming into a relentless obsession, one that drove me to constantly explore new depths.",
technologies: ["Python", "Discord.py", "Asyncio", "Sqlite"],
icon: <Check className="text-purple-bright" size={20} />
}
];
return ( return (
<div className="w-full max-w-6xl px-4 py-8 relative z-0"> <div className="w-full max-w-6xl px-4 py-8 relative z-0">
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8 md:mb-12"> <h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8 md:mb-12">
Journey Through Code My Journey Through Code
</h2> </h2>
<div className="relative"> <div ref={containerRef} className="relative">
<div className="absolute left-4 sm:left-1/2 h-full w-0.5 bg-foreground/10 -translate-x-1/2" /> {/* Animated vertical line */}
<div className="absolute left-4 sm:left-1/2 h-full w-0.5 -translate-x-1/2">
<div
ref={lineRef}
className="w-full bg-foreground/10 transition-all duration-[1500ms] ease-out origin-top"
style={{ height: `${lineHeight}%` }}
/>
</div>
<div className="ml-8 sm:ml-0"> <div className="ml-8 sm:ml-0">
{timelineItems.map((item, index) => ( {timelineItems.map((item, index) => (
<div key={item.year} className="relative mb-8 md:mb-12 last:mb-0"> <TimelineCard key={item.year} item={item} index={index} />
<div className={`flex flex-col sm:flex-row items-start ${
index % 2 === 0 ? 'sm:flex-row-reverse' : ''
}`}>
<div className="absolute -left-8 sm:left-1/2 w-6 h-6 sm:w-8 sm:h-8 bg-background
rounded-full border-2 border-yellow-bright sm:-translate-x-1/2
flex items-center justify-center z-10">
{item.icon}
</div>
<div className={`w-full sm:w-[calc(50%-32px)] ${
index % 2 === 0 ? 'sm:pr-8 md:pr-12' : 'sm:pl-8 md:pl-12'
}`}>
<div className="p-4 sm:p-6 bg-background/50 rounded-lg border border-foreground/10
hover:border-yellow-bright/50 transition-colors duration-300">
<span className="text-xs sm:text-sm font-mono text-yellow-bright">{item.year}</span>
<h3 className="text-lg sm:text-xl font-bold text-foreground/90 mt-2">{item.title}</h3>
<p className="text-sm sm:text-base text-foreground/70 mt-2">{item.description}</p>
<div className="flex flex-wrap gap-2 mt-3">
{item.technologies.map((tech) => (
<span key={tech}
className="px-2 py-1 text-xs sm:text-sm rounded-full bg-foreground/5
text-foreground/60 hover:text-yellow-bright transition-colors duration-300">
{tech}
</span>
))}
</div>
</div>
</div>
</div>
</div>
))} ))}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,50 @@
import React, { useEffect, useRef, useState } from "react";
interface AnimateInProps {
children: React.ReactNode;
delay?: number;
threshold?: number;
}
export function AnimateIn({ children, delay = 0, threshold = 0.15 }: AnimateInProps) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
requestAnimationFrame(() => setVisible(true));
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ threshold }
);
observer.observe(el);
return () => observer.disconnect();
}, [threshold]);
return (
<div
ref={ref}
className="transition-all duration-700 ease-out"
style={{
transitionDelay: `${delay}ms`,
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0)" : "translateY(24px)",
}}
>
{children}
</div>
);
}

View File

@@ -383,13 +383,20 @@ const Background: React.FC<BackgroundProps> = ({
const handleMouseDown = (e: MouseEvent) => { const handleMouseDown = (e: MouseEvent) => {
if (!gridRef.current || !canvasRef.current) return; if (!gridRef.current || !canvasRef.current) return;
const canvas = canvasRef.current; const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect(); const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left; const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top; const mouseY = e.clientY - rect.top;
// Ignore clicks outside the canvas bounds
if (mouseX < 0 || mouseX > rect.width || mouseY < 0 || mouseY > rect.height) return;
// Prevent text selection when interacting with the canvas
e.preventDefault();
const cellSize = getCellSize(); const cellSize = getCellSize();
mouseRef.current.isDown = true; mouseRef.current.isDown = true;
mouseRef.current.lastClickTime = Date.now(); mouseRef.current.lastClickTime = Date.now();
@@ -523,11 +530,10 @@ const Background: React.FC<BackgroundProps> = ({
gridRef.current = initGrid(displayWidth, displayHeight); gridRef.current = initGrid(displayWidth, displayHeight);
} }
// Add mouse event listeners // Bind to window so mouse events work even when content overlays the canvas
canvas.addEventListener('mousedown', handleMouseDown, { signal }); window.addEventListener('mousedown', handleMouseDown, { signal });
canvas.addEventListener('mousemove', handleMouseMove, { signal }); window.addEventListener('mousemove', handleMouseMove, { signal });
canvas.addEventListener('mouseup', handleMouseUp, { signal }); window.addEventListener('mouseup', handleMouseUp, { signal });
canvas.addEventListener('mouseleave', handleMouseLeave, { signal });
const handleVisibilityChange = () => { const handleVisibilityChange = () => {
if (document.hidden) { if (document.hidden) {

View File

@@ -1,38 +1,43 @@
import React from "react"; import React from "react";
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react"; import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
import { AnimateIn } from "@/components/animate-in";
export const BlogHeader = () => { export const BlogHeader = () => {
return ( return (
<div className="w-full max-w-6xl mx-auto px-4 pt-24 sm:pt-24"> <div className="w-full max-w-6xl mx-auto px-4 pt-24 sm:pt-24">
<h1 className="text-2xl sm:text-3xl font-bold text-purple mb-3 text-center px-4 leading-relaxed"> <AnimateIn>
Latest Thoughts <br className="sm:hidden" /> <h1 className="text-2xl sm:text-3xl font-bold text-purple mb-3 text-center px-4 leading-relaxed">
& Writings Latest Thoughts <br className="sm:hidden" />
</h1> & Writings
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base"> </h1>
<a </AnimateIn>
href="/rss" <AnimateIn delay={100}>
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-orange hover:text-orange-bright hover:border-orange/50 transition-colors duration-200" <div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
> <a
<RssIcon className="w-4 h-4" /> href="/rss"
<span>RSS Feed</span> className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-orange hover:text-orange-bright hover:border-orange/50 transition-colors duration-200"
</a> >
<RssIcon className="w-4 h-4" />
<a <span>RSS Feed</span>
href="/blog/tags" </a>
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-aqua hover:text-aqua-bright hover:border-aqua/50 transition-colors duration-200"
> <a
<TagIcon className="w-4 h-4" /> href="/blog/tags"
<span>Browse Tags</span> className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-aqua hover:text-aqua-bright hover:border-aqua/50 transition-colors duration-200"
</a> >
<TagIcon className="w-4 h-4" />
<a <span>Browse Tags</span>
href="/blog/popular" </a>
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-blue hover:text-blue-bright hover:border-blue/50 transition-colors duration-200"
> <a
<TrendingUpIcon className="w-4 h-4" /> href="/blog/popular"
<span>Most Popular</span> className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-blue hover:text-blue-bright hover:border-blue/50 transition-colors duration-200"
</a> >
</div> <TrendingUpIcon className="w-4 h-4" />
<span>Most Popular</span>
</a>
</div>
</AnimateIn>
</div> </div>
); );
}; };

View File

@@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { AnimateIn } from "@/components/animate-in";
type BlogPost = { type BlogPost = {
id: string; id: string;
@@ -27,71 +28,73 @@ const formatDate = (dateString: string) => {
export const BlogPostList = ({ posts }: BlogPostListProps) => { export const BlogPostList = ({ posts }: BlogPostListProps) => {
return ( return (
<div className="w-full max-w-6xl mx-auto"> <div className="w-full max-w-6xl mx-auto">
<ul className="space-y-6 md:space-y-10"> <ul className="space-y-6 md:space-y-10">
{posts.map((post) => ( {posts.map((post, i) => (
<li key={post.id} className="group px-4 md:px-0"> <AnimateIn key={post.id} delay={i * 80}>
<a <li className="group px-4 md:px-0">
href={`/blog/${post.id}`} <a
className="block" href={`/blog/${post.id}`}
> className="block"
<article className="flex flex-col md:flex-row gap-4 md:gap-8 pb-6 md:pb-10 border-b border-foreground/20 last:border-b-0 p-2 md:p-4 rounded-lg group-hover:outline group-hover:outline-2 group-hover:outline-purple transition-all duration-200"> >
{/* Image container with fixed aspect ratio */} <article className="flex flex-col md:flex-row md:items-center gap-4 md:gap-8 p-2 md:p-4 border-b border-foreground/20 last:border-b-0 rounded-lg group-hover:outline group-hover:outline-2 group-hover:outline-purple transition-all duration-200">
<div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-background"> {/* Image container with fixed aspect ratio */}
<img <div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-background flex-shrink-0">
src={post.data.image || "/blog/placeholder.png"} <img
alt={post.data.title} src={post.data.image || "/blog/placeholder.png"}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" alt={post.data.title}
style={{ objectPosition: post.data.imagePosition || "center center" }} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/> style={{ objectPosition: post.data.imagePosition || "center center" }}
</div> />
</div>
{/* Content container */}
<div className="w-full md:w-2/3 flex flex-col gap-2 md:gap-4 py-1 md:py-2"> {/* Content container */}
{/* Title and meta info */} <div className="w-full md:w-2/3 flex flex-col gap-2 md:gap-3">
<div className="space-y-1.5 md:space-y-3"> {/* Title and meta info */}
<h2 className="text-lg md:text-2xl font-semibold text-yellow group-hover:text-purple transition-colors duration-200 line-clamp-2"> <div className="space-y-1.5 md:space-y-3">
{post.data.title} <h2 className="text-lg md:text-2xl font-semibold text-yellow group-hover:text-purple transition-colors duration-200 line-clamp-2">
</h2> {post.data.title}
</h2>
<div className="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
<span className="text-orange">{post.data.author}</span> <div className="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
<span className="text-foreground/50"></span> <span className="text-orange">{post.data.author}</span>
<time dateTime={post.data.date} className="text-blue"> <span className="text-foreground/50"></span>
{formatDate(post.data.date)} <time dateTime={post.data.date} className="text-blue">
</time> {formatDate(post.data.date)}
</time>
</div>
</div>
{/* Description */}
<p className="text-foreground/90 text-sm md:text-lg leading-relaxed line-clamp-2 mt-0.5 md:mt-0">
{post.data.description}
</p>
{/* Tags */}
<div className="flex flex-wrap gap-1.5 md:gap-3 mt-1 md:mt-2">
{post.data.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
onClick={(e) => {
e.preventDefault();
window.location.href = `/blog/tag/${tag}`;
}}
>
#{tag}
</span>
))}
{post.data.tags.length > 3 && (
<span className="text-xs md:text-base text-foreground/60">
+{post.data.tags.length - 3}
</span>
)}
</div> </div>
</div> </div>
</article>
{/* Description */} </a>
<p className="text-foreground/90 text-sm md:text-lg leading-relaxed line-clamp-2 mt-0.5 md:mt-0"> </li>
{post.data.description} </AnimateIn>
</p>
{/* Tags */}
<div className="flex flex-wrap gap-1.5 md:gap-3 mt-1 md:mt-2">
{post.data.tags.slice(0, 3).map((tag) => (
<span
key={tag}
className="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
onClick={(e) => {
e.preventDefault();
window.location.href = `/blog/tag/${tag}`;
}}
>
#{tag}
</span>
))}
{post.data.tags.length > 3 && (
<span className="text-xs md:text-base text-foreground/60">
+{post.data.tags.length - 3}
</span>
)}
</div>
</div>
</article>
</a>
</li>
))} ))}
</ul> </ul>
</div> </div>

View File

@@ -1,78 +0,0 @@
import React from "react"
import type { CollectionEntry } from "astro:content";
interface ProjectCardProps {
project: CollectionEntry<"projects">;
}
export function ProjectCard({ project }: ProjectCardProps) {
const hasLinks = project.data.githubUrl || project.data.demoUrl;
return (
<article className="group relative h-full">
<a
href={`/projects/${project.id}`}
className="block rounded-lg border-2 border-foreground/20
hover:border-blue transition-all duration-300
bg-background overflow-hidden h-full flex flex-col"
>
<div className="aspect-video w-full border-b border-foreground/20 bg-foreground/5 overflow-hidden flex-shrink-0">
{project.data.image ? (
<img
src={project.data.image}
alt={`${project.data.title} preview`}
className="w-full h-full object-cover object-center group-hover:scale-105 transition-transform duration-300"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-foreground/30">
<span className="text-sm">No preview available</span>
</div>
)}
</div>
<div className="p-4 sm:p-6 space-y-3 flex flex-col flex-grow">
<h3 className="text-lg sm:text-xl font-bold group-hover:text-blue transition-colors">
{project.data.title}
</h3>
<div className="flex flex-wrap gap-2">
{project.data.techStack.map(tech => (
<span key={tech} className="text-xs px-2 py-1 rounded-full bg-purple-bright/10 text-purple-bright">
{tech}
</span>
))}
</div>
<p className="text-foreground/70 text-sm sm:text-base flex-grow">
{project.data.description}
</p>
{hasLinks && (
<div className="flex gap-4 pt-3 border-t border-foreground/10 mt-auto">
{project.data.githubUrl && (
<a
href={project.data.githubUrl}
className="text-sm text-blue hover:text-blue-bright
transition-colors z-10"
onClick={(e) => e.stopPropagation()}
>
View Source
</a>
)}
{project.data.demoUrl && (
<a
href={project.data.demoUrl}
className="text-sm text-green hover:text-green-bright
transition-colors z-10"
onClick={(e) => e.stopPropagation()}
>
Live Link
</a>
)}
</div>
)}
</div>
</a>
</article>
);
}

View File

@@ -1,49 +1,100 @@
import React from "react"; import React from "react";
import type { CollectionEntry } from "astro:content"; import type { CollectionEntry } from "astro:content";
import { ProjectCard } from "@/components/projects/project-card"; import { AnimateIn } from "@/components/animate-in";
interface ProjectListProps { interface ProjectListProps {
projects: CollectionEntry<"projects">[]; projects: CollectionEntry<"projects">[];
} }
export function ProjectList({ projects }: ProjectListProps) { export function ProjectList({ projects }: ProjectListProps) {
const latestProjects = projects.slice(0, 3);
const otherProjects = projects.slice(3);
return ( return (
<div className="w-full max-w-6xl mx-auto pt-24 sm:pt-32"> <div className="w-full max-w-6xl mx-auto pt-24 sm:pt-32 px-4">
<h1 className="text-2xl sm:text-3xl font-bold text-blue mb-12 text-center px-4 leading-relaxed"> <AnimateIn>
Here's what I've been <br className="sm:hidden" /> <h1 className="text-2xl sm:text-3xl font-bold text-blue mb-12 text-center leading-relaxed">
building lately Here's what I've been <br className="sm:hidden" />
</h1> building lately
</h1>
<div className="px-4 mb-16"> </AnimateIn>
<h2 className="text-xl font-bold text-foreground/90 mb-6">
Featured Projects
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr justify-items-center">
{latestProjects.map(project => (
<div key={project.id} className="w-full max-w-md">
<ProjectCard project={project} />
</div>
))}
</div>
</div>
{otherProjects.length > 0 && ( <ul className="space-y-6 md:space-y-10">
<div className="px-4 pb-8"> {projects.map((project, i) => (
<h2 className="text-xl font-bold text-foreground/90 mb-6"> <AnimateIn key={project.id} delay={i * 80}>
All Projects <li className="group">
</h2> <a href={`/projects/${project.id}`} className="block">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr justify-items-center"> <article className="flex flex-col md:flex-row md:items-center gap-4 md:gap-8 p-2 md:p-4 border-b border-foreground/20 last:border-b-0 rounded-lg group-hover:outline group-hover:outline-2 group-hover:outline-blue transition-all duration-200">
{otherProjects.map(project => ( {/* Image */}
<div key={project.id} className="w-full max-w-md"> <div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-foreground/5 flex-shrink-0">
<ProjectCard project={project} /> {project.data.image ? (
</div> <img
))} src={project.data.image}
</div> alt={`${project.data.title} preview`}
</div> className="w-full h-full object-cover object-center group-hover:scale-105 transition-transform duration-300"
)} />
) : (
<div className="w-full h-full flex items-center justify-center text-foreground/30">
<span className="text-sm">No preview available</span>
</div>
)}
</div>
{/* Content */}
<div className="w-full md:w-2/3 flex flex-col gap-2 md:gap-3">
<h2 className="text-lg md:text-2xl font-semibold text-yellow group-hover:text-blue transition-colors duration-200 line-clamp-2">
{project.data.title}
</h2>
<p className="text-foreground/90 text-sm md:text-lg leading-relaxed line-clamp-2">
{project.data.description}
</p>
{/* Tech stack */}
<div className="flex flex-wrap gap-1.5 md:gap-2 mt-1">
{project.data.techStack.map((tech) => (
<span
key={tech}
className="text-xs md:text-sm px-2 py-0.5 rounded-full bg-purple-bright/10 text-purple-bright"
>
{tech}
</span>
))}
</div>
{/* Links */}
{(project.data.githubUrl || project.data.demoUrl) && (
<div className="flex gap-4 mt-1">
{project.data.githubUrl && (
<span
className="text-sm text-foreground/50 hover:text-blue-bright transition-colors"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
window.open(project.data.githubUrl, "_blank");
}}
>
Source
</span>
)}
{project.data.demoUrl && (
<span
className="text-sm text-foreground/50 hover:text-green-bright transition-colors"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
window.open(project.data.demoUrl, "_blank");
}}
>
Live
</span>
)}
</div>
)}
</div>
</article>
</a>
</li>
</AnimateIn>
))}
</ul>
</div> </div>
); );
} }

View File

@@ -1,11 +1,149 @@
import React from "react"; import React, { useEffect, useRef, useState } from "react";
import { import {
FileDown, FileDown,
Github, Github,
Linkedin, Linkedin,
Globe Globe
} from "lucide-react"; } from "lucide-react";
// --- Typewriter hook ---
function useTypewriter(text: string, trigger: boolean, speed = 12) {
const [displayed, setDisplayed] = useState("");
const [done, setDone] = useState(false);
useEffect(() => {
if (!trigger) return;
let i = 0;
setDisplayed("");
setDone(false);
const interval = setInterval(() => {
i++;
setDisplayed(text.slice(0, i));
if (i >= text.length) {
setDone(true);
clearInterval(interval);
}
}, speed);
return () => clearInterval(interval);
}, [trigger, text, speed]);
return { displayed, done };
}
// --- Visibility hook ---
function useScrollVisible(threshold = 0.1) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const rect = el.getBoundingClientRect();
if (rect.top < window.innerHeight && rect.bottom > 0) {
requestAnimationFrame(() => setVisible(true));
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
observer.disconnect();
}
},
{ threshold }
);
observer.observe(el);
return () => observer.disconnect();
}, [threshold]);
return { ref, visible };
}
// --- Section fade-in ---
function Section({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) {
const { ref, visible } = useScrollVisible();
return (
<div
ref={ref}
className="transition-all duration-700 ease-out"
style={{
transitionDelay: `${delay}ms`,
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0)" : "translateY(24px)",
}}
>
{children}
</div>
);
}
// --- Typed heading + fade-in body ---
function TypedSection({
heading,
headingClass = "text-3xl font-bold text-yellow-bright",
children,
}: {
heading: string;
headingClass?: string;
children: React.ReactNode;
}) {
const { ref, visible } = useScrollVisible();
const { displayed, done } = useTypewriter(heading, visible, 20);
return (
<div ref={ref} className="space-y-4">
<h3 className={headingClass} style={{ minHeight: "1.2em" }}>
{visible ? displayed : "\u00A0"}
{visible && !done && <span className="animate-pulse text-foreground/40">|</span>}
</h3>
<div
className="transition-all duration-500 ease-out"
style={{
opacity: done ? 1 : 0,
transform: done ? "translateY(0)" : "translateY(12px)",
}}
>
{children}
</div>
</div>
);
}
// --- Staggered skill tags ---
function SkillTags({ skills, trigger }: { skills: string[]; trigger: boolean }) {
return (
<div className="flex flex-wrap gap-3">
{skills.map((skill, i) => (
<span
key={i}
className="border border-foreground/20 px-4 py-2 rounded-lg hover:border-foreground/40 transition-all duration-500 ease-out"
style={{
transitionDelay: `${i * 60}ms`,
opacity: trigger ? 1 : 0,
transform: trigger ? "translateY(0) scale(1)" : "translateY(12px) scale(0.95)",
}}
>
{skill}
</span>
))}
</div>
);
}
// --- Data ---
const resumeData = { const resumeData = {
name: "Timothy Pidashev", name: "Timothy Pidashev",
title: "Software Engineer", title: "Software Engineer",
@@ -45,8 +183,8 @@ const resumeData = {
achievements: [ achievements: [
"Designed and built the entire application from the ground up, including auth", "Designed and built the entire application from the ground up, including auth",
"Engineered a tagging system to optimize search results by keywords and relativity", "Engineered a tagging system to optimize search results by keywords and relativity",
"Implemented a filter provider to further narrow down search results and enchance the user experience", "Implemented a filter provider to further narrow down search results and enhance the user experience",
"Created a smooth and responsive infinitely scrollable listings page", "Created a smooth and responsive infinitely scrollable listings page",
"Automated deployment & testing processes reducing downtime by 60%" "Automated deployment & testing processes reducing downtime by 60%"
] ]
} }
@@ -57,22 +195,17 @@ const resumeData = {
school: "Clark College", school: "Clark College",
location: "Vancouver, WA", location: "Vancouver, WA",
period: "Graduating 2026", period: "Graduating 2026",
achievements: [] achievements: [] as string[]
} }
], ],
skills: { skills: {
technical: ["JavaScript", "TypeScript", "React", "Node.js", "Python", "SQL", "AWS", "Docker", "C", "Go", "Rust"], technical: ["JavaScript", "TypeScript", "React", "Node.js", "Python", "SQL", "AWS", "Docker", "C", "Go", "Rust"],
soft: ["Leadership", "Problem Solving", "Communication", "Agile Methodologies"] soft: ["Leadership", "Problem Solving", "Communication", "Agile Methodologies"]
}, },
certifications: [
{
name: "AWS Certified Solutions Architect",
issuer: "Amazon Web Services",
date: "2022"
}
]
}; };
// --- Component ---
const Resume = () => { const Resume = () => {
const handleDownloadPDF = () => { const handleDownloadPDF = () => {
window.open("/timothy-pidashev-resume.pdf", "_blank"); window.open("/timothy-pidashev-resume.pdf", "_blank");
@@ -83,188 +216,198 @@ const Resume = () => {
<div className="space-y-16"> <div className="space-y-16">
{/* Header */} {/* Header */}
<header className="text-center space-y-6"> <header className="text-center space-y-6">
<h1 className="text-6xl font-bold text-aqua-bright">{resumeData.name}</h1> <Section>
<h2 className="text-3xl text-foreground/80">{resumeData.title}</h2> <h1 className="text-6xl font-bold text-aqua-bright">{resumeData.name}</h1>
<div className="flex flex-col md:flex-row justify-center gap-4 md:gap-6 text-foreground/60 text-lg"> </Section>
<a href={`mailto:${resumeData.contact.email}`} className="hover:text-foreground transition-colors duration-200"> <Section delay={150}>
{resumeData.contact.email} <h2 className="text-3xl text-foreground/80">{resumeData.title}</h2>
</a> </Section>
<span className="hidden md:inline"></span> <Section delay={300}>
<a href={`tel:${resumeData.contact.phone}`} className="hover:text-foreground transition-colors duration-200"> <div className="flex flex-col md:flex-row justify-center gap-4 md:gap-6 text-foreground/60 text-lg">
{resumeData.contact.phone} <a href={`mailto:${resumeData.contact.email}`} className="hover:text-foreground transition-colors duration-200">
</a> {resumeData.contact.email}
<span className="hidden md:inline"></span> </a>
<span>{resumeData.contact.location}</span> <span className="hidden md:inline"></span>
</div> <a href={`tel:${resumeData.contact.phone}`} className="hover:text-foreground transition-colors duration-200">
<div className="flex justify-center items-center gap-6 text-lg"> {resumeData.contact.phone}
<a href={`https://${resumeData.contact.github}`} </a>
target="_blank" <span className="hidden md:inline"></span>
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2" <span>{resumeData.contact.location}</span>
> </div>
<Github size={18} /> </Section>
GitHub <Section delay={450}>
</a> <div className="flex justify-center items-center gap-6 text-lg">
<a href={`https://${resumeData.contact.github}`}
<a href={`https://${resumeData.contact.linkedin}`} target="_blank"
target="_blank" className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2" >
> <Github size={18} />
<Linkedin size={18} /> GitHub
LinkedIn </a>
</a> <a href={`https://${resumeData.contact.linkedin}`}
<button target="_blank"
onClick={handleDownloadPDF} className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2" >
> <Linkedin size={18} />
<FileDown size={18} /> LinkedIn
Resume </a>
</button> <button
</div> onClick={handleDownloadPDF}
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
>
<FileDown size={18} />
Resume
</button>
</div>
</Section>
</header> </header>
{/* Summary */} {/* Summary */}
<section className="space-y-4"> <TypedSection heading="Professional Summary">
<h3 className="text-3xl font-bold text-yellow-bright">Professional Summary</h3>
<p className="text-xl leading-relaxed">{resumeData.summary}</p> <p className="text-xl leading-relaxed">{resumeData.summary}</p>
</section> </TypedSection>
{/* Experience */} {/* Experience */}
<section className="space-y-8"> <TypedSection heading="Experience">
<h3 className="text-3xl font-bold text-yellow-bright">Experience</h3> <div className="space-y-8">
{resumeData.experience.map((exp, index) => ( {resumeData.experience.map((exp, index) => (
<div key={index} className="space-y-4"> <Section key={index} delay={index * 100}>
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2"> <div className="space-y-4">
<div> <div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<h4 className="text-2xl font-semibold text-green-bright">{exp.title}</h4> <div>
<div className="text-foreground/60 text-lg">{exp.company} - {exp.location}</div> <h4 className="text-2xl font-semibold text-green-bright">{exp.title}</h4>
<div className="text-foreground/60 text-lg">{exp.company} - {exp.location}</div>
</div>
<div className="text-foreground/60 text-lg font-medium">{exp.period}</div>
</div>
<ul className="list-disc pl-6 space-y-3">
{exp.achievements.map((a, i) => (
<li key={i} className="text-lg leading-relaxed">{a}</li>
))}
</ul>
</div> </div>
<div className="text-foreground/60 text-lg font-medium">{exp.period}</div> </Section>
</div> ))}
<ul className="list-disc pl-6 space-y-3"> </div>
{exp.achievements.map((achievement, i) => ( </TypedSection>
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
))}
</ul>
</div>
))}
</section>
{/* Contract Work */} {/* Contract Work */}
<section className="space-y-8"> <TypedSection heading="Contract Work">
<h3 className="text-3xl font-bold text-yellow-bright">Contract Work</h3> <div className="space-y-8">
{resumeData.contractWork.map((project, index) => ( {resumeData.contractWork.map((project, index) => (
<div key={index} className="space-y-4"> <Section key={index} delay={index * 100}>
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2"> <div className="space-y-4">
<div> <div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<div className="flex items-center gap-3"> <div>
<h4 className="text-2xl font-semibold text-green-bright">{project.title}</h4> <div className="flex items-center gap-3">
{project.url && ( <h4 className="text-2xl font-semibold text-green-bright">{project.title}</h4>
<a {project.url && (
href={project.url} <a
target="_blank" href={project.url}
rel="noopener noreferrer" target="_blank"
className="text-blue-bright hover:text-blue transition-colors duration-200 opacity-60 hover:opacity-100" rel="noopener noreferrer"
aria-label={`Visit ${project.title}`} className="text-blue-bright hover:text-blue transition-colors duration-200 opacity-60 hover:opacity-100"
> >
<Globe size={16} strokeWidth={1.5} /> <Globe size={16} strokeWidth={1.5} />
</a> </a>
)}
</div>
<div className="text-foreground/60 text-lg">{project.type}</div>
</div>
<div className="text-foreground/60 text-lg font-medium">Since {project.startDate}</div>
</div>
<div className="space-y-4">
{project.responsibilities && (
<div>
<h5 className="text-lg text-aqua font-semibold mb-2">Responsibilities</h5>
<ul className="list-disc pl-6 space-y-3">
{project.responsibilities.map((r, i) => (
<li key={i} className="text-lg leading-relaxed">{r}</li>
))}
</ul>
</div>
)}
{project.achievements && (
<div>
<h5 className="text-lg text-aqua font-semibold mb-2">Key Achievements</h5>
<ul className="list-disc pl-6 space-y-3">
{project.achievements.map((a, i) => (
<li key={i} className="text-lg leading-relaxed">{a}</li>
))}
</ul>
</div>
)} )}
</div> </div>
<div className="text-foreground/60 text-lg">{project.type}</div>
</div> </div>
<div className="text-foreground/60 text-lg font-medium">Since {project.startDate}</div> </Section>
</div> ))}
<div className="space-y-4"> </div>
{project.responsibilities && ( </TypedSection>
<div>
<h5 className="text-lg text-aqua font-semibold mb-2">Responsibilities</h5>
<ul className="list-disc pl-6 space-y-3">
{project.responsibilities.map((responsibility, i) => (
<li key={i} className="text-lg leading-relaxed">{responsibility}</li>
))}
</ul>
</div>
)}
{project.achievements && (
<div>
<h5 className="text-lg text-aqua font-semibold mb-2">Key Achievements</h5>
<ul className="list-disc pl-6 space-y-3">
{project.achievements.map((achievement, i) => (
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
))}
</ul>
</div>
)}
</div>
</div>
))}
</section>
{/* Education */} {/* Education */}
<section className="space-y-8"> <TypedSection heading="Education">
<h3 className="text-3xl font-bold text-yellow-bright">Education</h3> <div className="space-y-8">
{resumeData.education.map((edu, index) => ( {resumeData.education.map((edu, index) => (
<div key={index} className="space-y-4"> <Section key={index}>
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2"> <div className="space-y-4">
<div> <div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<h4 className="text-2xl font-semibold text-green-bright">{edu.degree}</h4> <div>
<div className="text-foreground/60 text-lg">{edu.school} - {edu.location}</div> <h4 className="text-2xl font-semibold text-green-bright">{edu.degree}</h4>
<div className="text-foreground/60 text-lg">{edu.school} - {edu.location}</div>
</div>
<div className="text-foreground/60 text-lg font-medium">{edu.period}</div>
</div>
{edu.achievements.length > 0 && (
<ul className="list-disc pl-6 space-y-3">
{edu.achievements.map((a, i) => (
<li key={i} className="text-lg leading-relaxed">{a}</li>
))}
</ul>
)}
</div> </div>
<div className="text-foreground/60 text-lg font-medium">{edu.period}</div> </Section>
</div> ))}
<ul className="list-disc pl-6 space-y-3"> </div>
{edu.achievements.map((achievement, i) => ( </TypedSection>
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
))}
</ul>
</div>
))}
</section>
{/* Skills */} {/* Skills */}
<section className="space-y-8"> <SkillsSection />
<h3 className="text-3xl font-bold text-yellow-bright">Skills</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-4">
<h4 className="text-2xl font-semibold text-green-bright">Technical Skills</h4>
<div className="flex flex-wrap gap-3">
{resumeData.skills.technical.map((skill, index) => (
<span key={index}
className="border border-foreground/20 px-4 py-2 rounded-lg hover:border-foreground/40 transition-colors duration-200">
{skill}
</span>
))}
</div>
</div>
<div className="space-y-4">
<h4 className="text-2xl font-semibold text-green-bright">Soft Skills</h4>
<div className="flex flex-wrap gap-3">
{resumeData.skills.soft.map((skill, index) => (
<span key={index}
className="border border-foreground/20 px-4 py-2 rounded-lg hover:border-foreground/40 transition-colors duration-200">
{skill}
</span>
))}
</div>
</div>
</div>
</section>
{/* Certifications */}
{/* Temporarily Hidden
<section className="space-y-6 mb-16">
<h3 className="text-3xl font-bold text-yellow-bright">Certifications</h3>
{resumeData.certifications.map((cert, index) => (
<div key={index} className="space-y-2">
<h4 className="text-2xl font-semibold text-green-bright">{cert.name}</h4>
<div className="text-foreground/60 text-lg">{cert.issuer} - {cert.date}</div>
</div>
))}
</section>
*/}
</div> </div>
</div> </div>
); );
}; };
// --- Skills section ---
function SkillsSection() {
const { ref, visible } = useScrollVisible();
const { displayed, done } = useTypewriter("Skills", visible, 20);
return (
<div ref={ref} className="space-y-8">
<h3 className="text-3xl font-bold text-yellow-bright" style={{ minHeight: "1.2em" }}>
{visible ? displayed : "\u00A0"}
{visible && !done && <span className="animate-pulse text-foreground/40">|</span>}
</h3>
<div
className="grid grid-cols-1 md:grid-cols-2 gap-8 transition-all duration-500 ease-out"
style={{
opacity: done ? 1 : 0,
transform: done ? "translateY(0)" : "translateY(12px)",
}}
>
<div className="space-y-4">
<h4 className="text-2xl font-semibold text-green-bright">Technical Skills</h4>
<SkillTags skills={resumeData.skills.technical} trigger={done} />
</div>
<div className="space-y-4">
<h4 className="text-2xl font-semibold text-green-bright">Soft Skills</h4>
<SkillTags skills={resumeData.skills.soft} trigger={done} />
</div>
</div>
</div>
);
}
export default Resume; export default Resume;

View File

@@ -8,7 +8,7 @@ import Timeline from "@/components/about/timeline";
import CurrentFocus from "@/components/about/current-focus"; import CurrentFocus from "@/components/about/current-focus";
import OutsideCoding from "@/components/about/outside-coding"; import OutsideCoding from "@/components/about/outside-coding";
--- ---
<ContentLayout <ContentLayout
title="About | Timothy Pidashev" title="About | Timothy Pidashev"
description="A software engineer passionate about the web, open source, and building innovative solutions." description="A software engineer passionate about the web, open source, and building innovative solutions."
> >
@@ -17,23 +17,23 @@ import OutsideCoding from "@/components/about/outside-coding";
<Intro client:load /> <Intro client:load />
</section> </section>
<section class="flex items-center justify-center py-16"> <section class="min-h-[60vh] flex items-center justify-center py-16">
<AllTimeStats client:only="react" /> <AllTimeStats client:load />
</section> </section>
<section class="flex items-center justify-center py-16"> <section class="min-h-screen flex items-center justify-center py-16">
<DetailedStats client:only="react" /> <DetailedStats client:load />
</section> </section>
<section class="flex items-center justify-center py-16"> <section class="min-h-[80vh] flex items-center justify-center py-16">
<Timeline client:load /> <Timeline client:load />
</section> </section>
<section class="flex items-center justify-center py-16"> <section class="min-h-[80vh] flex items-center justify-center py-16">
<CurrentFocus client:load /> <CurrentFocus client:load />
</section> </section>
<section class="flex items-center justify-center py-16"> <section class="min-h-[50vh] flex items-center justify-center py-16">
<OutsideCoding client:load /> <OutsideCoding client:load />
</section> </section>
</div> </div>

View File

@@ -2,7 +2,14 @@ import type { APIRoute } from 'astro';
export const GET: APIRoute = async () => { export const GET: APIRoute = async () => {
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY; const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
if (!WAKATIME_API_KEY) {
return new Response(
JSON.stringify({ error: "WAKATIME_API_KEY not configured" }),
{ status: 503, headers: { "Content-Type": "application/json" } }
);
}
try { try {
const response = await fetch( const response = await fetch(
'https://wakatime.com/api/v1/users/current/summaries?range=last_6_months', { 'https://wakatime.com/api/v1/users/current/summaries?range=last_6_months', {

View File

@@ -1,22 +1,35 @@
import type { APIRoute } from "astro"; import type { APIRoute } from "astro";
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
export const GET: APIRoute = async () => { export const GET: APIRoute = async () => {
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
if (!WAKATIME_API_KEY) {
return new Response(
JSON.stringify({ error: "WAKATIME_API_KEY not configured" }),
{ status: 503, headers: { "Content-Type": "application/json" } }
);
}
try { try {
const { stdout } = await execAsync(`curl -H "Authorization: Basic ${import.meta.env.WAKATIME_API_KEY}" https://wakatime.com/api/v1/users/current/all_time_since_today`); const response = await fetch(
"https://wakatime.com/api/v1/users/current/all_time_since_today",
return new Response(stdout, { {
status: 200, headers: {
headers: { Authorization: `Basic ${Buffer.from(WAKATIME_API_KEY).toString("base64")}`,
"Content-Type": "application/json" },
} }
);
const data = await response.json();
return new Response(JSON.stringify(data), {
status: 200,
headers: { "Content-Type": "application/json" },
}); });
} catch (error) { } catch (error) {
return new Response(JSON.stringify({ error: "Failed to fetch stats" }), { console.error("WakaTime alltime API error:", error);
status: 500 return new Response(
}); JSON.stringify({ error: "Failed to fetch stats" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
} }
} };

View File

@@ -3,7 +3,14 @@ import type { APIRoute } from 'astro';
export const GET: APIRoute = async () => { export const GET: APIRoute = async () => {
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY; const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
if (!WAKATIME_API_KEY) {
return new Response(
JSON.stringify({ error: "WAKATIME_API_KEY not configured" }),
{ status: 503, headers: { "Content-Type": "application/json" } }
);
}
try { try {
const response = await fetch( const response = await fetch(
'https://wakatime.com/api/v1/users/current/stats/last_7_days?timeout=15', { 'https://wakatime.com/api/v1/users/current/stats/last_7_days?timeout=15', {

View File

@@ -65,7 +65,7 @@ const jsonLd = {
<div class="relative max-w-8xl mx-auto"> <div class="relative max-w-8xl mx-auto">
<article class="prose prose-invert prose-lg mx-auto max-w-4xl"> <article class="prose prose-invert prose-lg mx-auto max-w-4xl">
{post.data.image && ( {post.data.image && (
<div class="-mx-4 sm:mx-0 mb-8"> <div class="-mx-4 sm:mx-0 mb-4">
<Image <Image
src={post.data.image} src={post.data.image}
alt={post.data.title} alt={post.data.title}
@@ -76,20 +76,18 @@ const jsonLd = {
/> />
</div> </div>
)} )}
<h1 class="text-3xl pt-4">{post.data.title}</h1> <h1 class="text-3xl !mt-2 !mb-2">{post.data.title}</h1>
<p class="lg:text-2xl sm:text-lg">{post.data.description}</p> <p class="lg:text-2xl sm:text-lg !mt-0 !mb-3">{post.data.description}</p>
<div class="mt-4 md:mt-6"> <div class="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80"> <span class="text-orange">{post.data.author}</span>
<span class="text-orange">{post.data.author}</span> <span class="text-foreground/50">•</span>
<span class="text-foreground/50">•</span> <time dateTime={post.data.date instanceof Date ? post.data.date.toISOString() : post.data.date} class="text-blue">
<time dateTime={post.data.date instanceof Date ? post.data.date.toISOString() : post.data.date} class="text-blue"> {formattedDate}
{formattedDate} </time>
</time>
</div>
</div> </div>
<div class="flex flex-wrap gap-2 mt-4 md:mt-6"> <div class="flex flex-wrap gap-2 mt-2">
{post.data.tags.map((tag) => ( {post.data.tags.map((tag) => (
<span <span
class="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200" class="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
onclick={`window.location.href='/blog/tag/${tag}'`} onclick={`window.location.href='/blog/tag/${tag}'`}
> >