Compare commits

..

3 Commits

Author SHA1 Message Date
2c5f64a769 Polishing animations 2026-03-30 11:18:36 -07:00
b2cd74385f Astro upgrade to v6 2026-03-30 09:53:51 -07:00
95081b8b77 Omit drafts from build 2025-11-11 09:28:59 -08:00
31 changed files with 2981 additions and 2752 deletions

View File

@@ -8,36 +8,36 @@
"preview": "astro preview"
},
"devDependencies": {
"@astrojs/react": "^4.4.0",
"@astrojs/react": "^5.0.2",
"@astrojs/tailwind": "^6.0.2",
"@tailwindcss/typography": "^0.5.16",
"@types/react": "^18.3.20",
"@types/react-dom": "^18.3.6",
"astro": "^5.14.1",
"tailwindcss": "^3.4.17"
"@tailwindcss/typography": "^0.5.19",
"@types/react": "^18.3.28",
"@types/react-dom": "^18.3.7",
"astro": "^6.1.2",
"tailwindcss": "^3.4.19"
},
"dependencies": {
"@astrojs/mdx": "^4.3.6",
"@astrojs/node": "^9.4.4",
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.6.0",
"@astrojs/mdx": "^5.0.3",
"@astrojs/node": "^10.0.4",
"@astrojs/rss": "^4.0.18",
"@astrojs/sitemap": "^3.7.2",
"@giscus/react": "^3.1.0",
"@pilcrowjs/object-parser": "^0.0.4",
"@react-hook/intersection-observer": "^3.1.2",
"@rehype-pretty/transformers": "^0.13.2",
"arctic": "^3.6.0",
"arctic": "^3.7.0",
"lucide-react": "^0.468.0",
"marked": "^15.0.8",
"marked": "^15.0.12",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.5.0",
"react-icons": "^5.6.0",
"react-responsive": "^10.0.1",
"reading-time": "^1.5.0",
"rehype-pretty-code": "^0.14.1",
"rehype-pretty-code": "^0.14.3",
"rehype-slug": "^6.0.0",
"schema-dts": "^1.1.5",
"shiki": "^3.12.2",
"typewriter-effect": "^2.21.0",
"unist-util-visit": "^5.0.0"
"shiki": "^3.23.0",
"typewriter-effect": "^2.22.0",
"unist-util-visit": "^5.1.0"
}
}

3172
src/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,57 @@
import React from 'react';
import { Code2, BookOpen, RocketIcon, Compass } from 'lucide-react';
import React, { useEffect, useRef, useState } from "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() {
const recentProjects = [
@@ -7,126 +59,134 @@ export default function CurrentFocus() {
title: "Darkbox",
description: "My gruvbox theme, with a pure black background",
href: "/projects/darkbox",
tech: ["Neovim", "Lua"]
tech: ["Neovim", "Lua"],
},
{
title: "Revive Auto Parts",
description: "A car parts listing site built for a client",
href: "/projects/reviveauto",
tech: ["Tanstack", "React Query", "Fastapi"]
tech: ["Tanstack", "React Query", "Fastapi"],
},
{
title: "Fhccenter",
description: "Website made for a private school",
href: "/projects/fhccenter",
tech: ["Nextjs", "Typescript", "Prisma"]
}
tech: ["Nextjs", "Typescript", "Prisma"],
},
];
return (
<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">
<h2 className="text-3xl sm:text-4xl font-bold text-center text-yellow-bright mb-8 sm:mb-12">
Current Focus
</h2>
<AnimateIn>
<h2 className="text-3xl sm:text-4xl font-bold text-center text-yellow-bright mb-8 sm:mb-12">
Current Focus
</h2>
</AnimateIn>
{/* Recent Projects Section */}
<div className="mb-8 sm:mb-16">
<div className="flex items-center justify-center gap-2 mb-6">
<Code2 className="text-yellow-bright" size={24} />
<h3 className="text-xl font-bold text-foreground/90">Recent Projects</h3>
</div>
<AnimateIn delay={100}>
<div className="flex items-center justify-center gap-2 mb-6">
<Code2 className="text-yellow-bright" size={24} />
<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">
{recentProjects.map((project) => (
<a
href={project.href}
key={project.title}
className="p-4 sm:p-6 rounded-lg border border-foreground/10 hover:border-yellow-bright/50
transition-all duration-300 group bg-background/50"
>
<h4 className="font-bold text-lg group-hover:text-yellow-bright transition-colors">
{project.title}
</h4>
<p className="text-foreground/70 mt-2 text-sm sm:text-base">{project.description}</p>
<div className="flex flex-wrap gap-2 mt-3">
{project.tech.map((tech) => (
<span key={tech} className="text-xs px-2 py-1 rounded-full bg-foreground/5 text-foreground/60">
{tech}
</span>
))}
</div>
</a>
{recentProjects.map((project, i) => (
<AnimateIn key={project.title} delay={200 + i * 100}>
<a
href={project.href}
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 h-full"
>
<h4 className="font-bold text-lg group-hover:text-yellow-bright transition-colors">
{project.title}
</h4>
<p className="text-foreground/70 mt-2 text-sm sm:text-base">{project.description}</p>
<div className="flex flex-wrap gap-2 mt-3">
{project.tech.map((tech) => (
<span key={tech} className="text-xs px-2 py-1 rounded-full bg-foreground/5 text-foreground/60">
{tech}
</span>
))}
</div>
</a>
</AnimateIn>
))}
</div>
</div>
{/* 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">
{/* What I'm Learning */}
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50">
<div className="flex items-center justify-center gap-2">
<BookOpen className="text-green-bright" size={24} />
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Currently Learning</h3>
<AnimateIn delay={100}>
<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">
<BookOpen className="text-green-bright" size={24} />
<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>
<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>
</AnimateIn>
{/* Project Interests */}
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50">
<div className="flex items-center justify-center gap-2">
<RocketIcon className="text-blue-bright" size={24} />
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Project Interests</h3>
<AnimateIn delay={200}>
<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">
<RocketIcon className="text-blue-bright" size={24} />
<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>
<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>
</AnimateIn>
{/* Areas to Explore */}
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50">
<div className="flex items-center justify-center gap-2">
<Compass className="text-purple-bright" size={24} />
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Want to Explore</h3>
<AnimateIn delay={300}>
<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">
<Compass className="text-purple-bright" size={24} />
<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>
<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>
</AnimateIn>
</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";
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 nextSection = document.querySelector("section")?.nextElementSibling;
if (nextSection) {
const offset = nextSection.offsetTop - (window.innerHeight - nextSection.offsetHeight) / 2; // Center the section
window.scrollTo({
top: offset,
behavior: "smooth"
});
const offset = (nextSection as HTMLElement).offsetTop - (window.innerHeight - (nextSection as HTMLElement).offsetHeight) / 2;
window.scrollTo({ 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 (
<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="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
src="/me.jpeg"
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"
/>
</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">
Timothy Pidashev
</h2>
<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>
</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>
</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>
</p>
</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">
"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-purple-bright"> methodically,</span>
<span className="text-blue-bright"> with attention to detail,</span>
<span className="text-green-bright"> and a refined process.</span>
</p>
<div className="flex justify-center">
<button
<div className="flex justify-center" style={anim(900)}>
<button
onClick={scrollToNext}
className="text-foreground/50 hover:text-yellow-bright transition-colors duration-300"
aria-label="Scroll to next section"

View File

@@ -1,64 +1,115 @@
import React from 'react';
import { Fish, Mountain, Book, Car } from 'lucide-react';
import React, { useEffect, useRef, useState } from "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() {
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 (
<div className="flex justify-center items-center w-full">
<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">
Outside of Programming
</h2>
<AnimateIn>
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8">
Outside of Programming
</h2>
</AnimateIn>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{interests.map((interest) => (
<div
key={interest.title}
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"
>
<div className="mb-3">
{interest.icon}
{interests.map((interest, i) => (
<AnimateIn key={interest.title} delay={100 + i * 100}>
<div
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 h-full"
>
<div className="mb-3">{interest.icon}</div>
<h3 className="font-bold text-foreground/90 mb-2">{interest.title}</h3>
<p className="text-sm text-foreground/70">{interest.description}</p>
</div>
<h3 className="font-bold text-foreground/90 mb-2">
{interest.title}
</h3>
<p className="text-sm text-foreground/70">
{interest.description}
</p>
</div>
</AnimateIn>
))}
</div>
<p className="text-center text-foreground/80 mt-8 max-w-2xl mx-auto text-sm md:text-base italic">
When I'm not writing code, you'll find me
<span className="text-blue-bright"> out on the water,</span>
<span className="text-green-bright"> hiking trails,</span>
<span className="text-purple-bright"> reading books,</span>
<span className="text-yellow-bright"> or modifying my current ride.</span>
</p>
<AnimateIn delay={500}>
<p className="text-center text-foreground/80 mt-8 max-w-2xl mx-auto text-sm md:text-base italic">
When I'm not writing code, you'll find me
<span className="text-red-bright"> walking with Christ,</span>
<span className="text-blue-bright"> out on the water,</span>
<span className="text-green-bright"> hiking trails,</span>
<span className="text-purple-bright"> or reading books.</span>
</p>
</AnimateIn>
</div>
</div>
);

View File

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

View File

@@ -1,98 +1,115 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
const Stats = () => {
const [stats, setStats] = useState<any>(null);
const [error, setError] = useState(false);
const [count, setCount] = useState(0);
const [isFinished, setIsFinished] = 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(() => {
setIsVisible(true);
const fetchStats = async () => {
try {
const res = await fetch("/api/wakatime/alltime");
const data = await res.json();
setStats(data.data);
startCounting(data.data.total_seconds);
} catch (error) {
console.error("Error fetching stats:", error);
}
};
fetchStats();
fetch("/api/wakatime/alltime")
.then((res) => {
if (!res.ok) throw new Error("API error");
return res.json();
})
.then((data) => setStats(data.data))
.catch(() => setError(true));
}, []);
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 steps = 60;
let currentStep = 0;
const timer = setInterval(() => {
currentStep += 1;
if (currentStep >= steps) {
setCount(totalSeconds);
setIsFinished(true);
clearInterval(timer);
return;
}
const progress = 1 - Math.pow(1 - currentStep / steps, 4);
setCount(Math.floor(totalSeconds * progress));
}, duration / steps);
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 formattedHours = hours.toLocaleString("en-US", {
minimumIntegerDigits: 4,
useGrouping: true
useGrouping: true,
});
return (
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-6">
<div className={`
text-2xl opacity-0
${isVisible ? "animate-fade-in-first" : ""}
`}>
<div ref={sectionRef} className="flex flex-col items-center justify-center min-h-[50vh] gap-6">
<div className={skipAnim ? "text-2xl opacity-80" : `text-2xl opacity-0 ${isVisible ? "animate-fade-in-first" : ""}`}>
I've spent
</div>
<div className="relative">
<div className="text-8xl text-center relative z-10">
<span className="font-bold relative">
<span className={`
bg-gradient-text opacity-0
${isVisible ? "animate-fade-in-second" : ""}
`}>
<span className={skipAnim ? "bg-gradient-text" : `bg-gradient-text opacity-0 ${isVisible ? "animate-fade-in-second" : ""}`}>
{formattedHours}
</span>
</span>
<span className={`
text-4xl opacity-0
${isVisible ? "animate-slide-in-hours" : ""}
`}>
<span className={skipAnim ? "text-4xl opacity-60 ml-4" : `text-4xl opacity-0 ${isVisible ? "animate-slide-in-hours" : ""}`}>
hours
</span>
</div>
</div>
<div className="flex flex-col items-center gap-3 text-center">
<div className={`
text-xl opacity-0
${isVisible ? "animate-fade-in-third" : ""}
`}>
<div className={skipAnim ? "text-xl opacity-80" : `text-xl opacity-0 ${isVisible ? "animate-fade-in-third" : ""}`}>
writing code & building apps
</div>
<div className={`
flex items-center gap-3 text-lg opacity-0
${isVisible ? "animate-fade-in-fourth" : ""}
`}>
<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" : ""}`}>
<span>since</span>
<span className="text-green-bright font-bold">{stats.range.start_text}</span>
</div>
@@ -100,15 +117,7 @@ const Stats = () => {
<style jsx>{`
.bg-gradient-text {
background: linear-gradient(
90deg,
#fbbf24,
#f59e0b,
#d97706,
#b45309,
#f59e0b,
#fbbf24
);
background: linear-gradient(90deg, #fbbf24, #f59e0b, #d97706, #b45309, #f59e0b, #fbbf24);
background-size: 200% auto;
color: transparent;
background-clip: text;
@@ -116,95 +125,32 @@ const Stats = () => {
-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 {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 0.8;
transform: translateY(0);
}
from { opacity: 0; transform: translateY(20px); }
to { opacity: 0.8; transform: translateY(0); }
}
@keyframes fadeInSecond {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideInHours {
0% {
opacity: 0;
transform: translateX(20px);
margin-left: 0;
}
100% {
opacity: 0.6;
transform: translateX(0);
margin-left: 1rem;
}
from { opacity: 0; transform: translateX(20px); margin-left: 0; }
to { opacity: 0.6; transform: translateX(0); margin-left: 1rem; }
}
@keyframes fadeInThird {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 0.8;
transform: translateY(0);
}
from { opacity: 0; transform: translateY(20px); }
to { opacity: 0.8; transform: translateY(0); }
}
@keyframes fadeInFourth {
0% {
opacity: 0;
transform: translateY(20px);
}
100% {
opacity: 0.6;
transform: translateY(0);
}
from { opacity: 0; transform: translateY(20px); }
to { opacity: 0.6; transform: translateY(0); }
}
.animate-fade-in-first {
animation: fadeInFirst 0.7s ease-out forwards;
}
.animate-fade-in-second {
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;
}
.animate-fade-in-first { 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-fourth { animation: fadeInFourth 0.7s ease-out 1s forwards; }
`}</style>
</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 { ActivityGrid } from "@/components/about/stats-activity";
const DetailedStats = () => {
const [stats, setStats] = useState(null);
const [activity, setActivity] = useState(null);
const [isVisible, setIsVisible] = useState(false);
const [stats, setStats] = useState<any>(null);
const [activity, setActivity] = useState<any>(null);
const [error, setError] = useState(false);
const [visible, setVisible] = useState(false);
const [skipAnim, setSkipAnim] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
fetch("/api/wakatime/detailed")
.then(res => res.json())
.then(data => {
setStats(data.data);
setIsVisible(true);
.then((res) => {
if (!res.ok) throw new Error();
return res.json();
})
.catch(error => {
console.error("Error fetching stats:", error);
});
.then((data) => setStats(data.data))
.catch(() => setError(true));
fetch("/api/wakatime/activity")
.then(res => res.json())
.then(data => {
setActivity(data.data);
fetch("/api/wakatime/activity")
.then((res) => {
if (!res.ok) throw new Error();
return res.json();
})
.catch(error => {
console.error("Error fetching activity:", error);
});
.then((data) => setActivity(data.data))
.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 = [
"bg-red-bright",
@@ -38,138 +69,163 @@ const DetailedStats = () => {
"bg-green-bright",
"bg-blue-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 (
<div className="flex flex-col gap-10 md:py-32 w-full max-w-[1200px] mx-auto px-4">
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright">
Weekly Statistics
</h2>
<div ref={containerRef} className="flex flex-col gap-10 md:py-32 w-full max-w-[1200px] mx-auto px-4 min-h-[50vh]">
{!stats ? null : (
<>
{/* 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 */}
<div className={`
grid grid-cols-1 md:grid-cols-2 gap-8
transition-all duration-700 transform
${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-4 opacity-0'}
`}>
{/* Total Time */}
<StatsCard
title="Total Time"
value={`${Math.round(stats.total_seconds / 3600 * 10) / 10}`}
unit="hours"
subtitle="this week"
color="text-yellow-bright"
icon={Clock}
iconColor="stroke-yellow-bright"
/>
{/* Stat Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{statCards.map((card, i) => {
const Icon = card.icon;
return (
<div
key={card.title}
className={`bg-background border border-foreground/10 rounded-lg p-6 ${card.borderHover} ${skipAnim ? "" : "transition-all duration-500 ease-out"}`}
style={skipAnim ? {} : {
transitionDelay: `${150 + i * 100}ms`,
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0)" : "translateY(24px)",
}}
>
<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 */}
<StatsCard
title="Daily Average"
value={`${Math.round(stats.daily_average / 3600 * 10) / 10}`}
unit="hours"
subtitle="per day"
color="text-orange-bright"
icon={CalendarClock}
iconColor="stroke-orange-bright"
/>
{/* Languages */}
<div
className={`bg-background border border-foreground/10 rounded-lg p-6 hover:border-purple-bright/50 ${skipAnim ? "" : "transition-all duration-700 ease-out"}`}
style={skipAnim ? {} : {
transitionDelay: "550ms",
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0)" : "translateY(24px)",
}}
>
<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 */}
<StatsCard
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"
icon={CodeXml}
iconColor="stroke-blue-bright"
/>
{/* OS */}
<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>
{/* Activity Grid */}
{activity && (
<div
className={skipAnim ? "" : "transition-all duration-700 ease-out"}
style={skipAnim ? {} : {
transitionDelay: "750ms",
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0)" : "translateY(24px)",
}}
>
<ActivityGrid data={activity} />
</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;

View File

@@ -1,78 +1,181 @@
import React from "react";
import { Check, Code, GitBranch, Star } from "lucide-react";
import React, { useEffect, useRef, useState } from "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() {
const timelineItems = [
{
year: "2024",
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.",
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} />
}
];
const lineRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [lineHeight, setLineHeight] = useState(0);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
// Animate line to full height over time
const el = lineRef.current;
if (el) {
setLineHeight(100);
}
observer.disconnect();
}
},
{ threshold: 0.1 }
);
observer.observe(container);
return () => observer.disconnect();
}, []);
return (
<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">
Journey Through Code
My Journey Through Code
</h2>
<div className="relative">
<div className="absolute left-4 sm:left-1/2 h-full w-0.5 bg-foreground/10 -translate-x-1/2" />
<div ref={containerRef} className="relative">
{/* 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">
{timelineItems.map((item, index) => (
<div key={item.year} className="relative mb-8 md:mb-12 last:mb-0">
<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>
<TimelineCard key={item.year} item={item} index={index} />
))}
</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) => {
if (!gridRef.current || !canvasRef.current) return;
const canvas = canvasRef.current;
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
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();
mouseRef.current.isDown = true;
mouseRef.current.lastClickTime = Date.now();
@@ -523,11 +530,10 @@ const Background: React.FC<BackgroundProps> = ({
gridRef.current = initGrid(displayWidth, displayHeight);
}
// Add mouse event listeners
canvas.addEventListener('mousedown', handleMouseDown, { signal });
canvas.addEventListener('mousemove', handleMouseMove, { signal });
canvas.addEventListener('mouseup', handleMouseUp, { signal });
canvas.addEventListener('mouseleave', handleMouseLeave, { signal });
// Bind to window so mouse events work even when content overlays the canvas
window.addEventListener('mousedown', handleMouseDown, { signal });
window.addEventListener('mousemove', handleMouseMove, { signal });
window.addEventListener('mouseup', handleMouseUp, { signal });
const handleVisibilityChange = () => {
if (document.hidden) {

View File

@@ -1,38 +1,43 @@
import React from "react";
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
import { AnimateIn } from "@/components/animate-in";
export const BlogHeader = () => {
return (
<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">
Latest Thoughts <br className="sm:hidden" />
& Writings
</h1>
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
<a
href="/rss"
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"
>
<RssIcon className="w-4 h-4" />
<span>RSS Feed</span>
</a>
<a
href="/blog/tags"
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"
>
<TagIcon className="w-4 h-4" />
<span>Browse Tags</span>
</a>
<a
href="/blog/popular"
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"
>
<TrendingUpIcon className="w-4 h-4" />
<span>Most Popular</span>
</a>
</div>
<AnimateIn>
<h1 className="text-2xl sm:text-3xl font-bold text-purple mb-3 text-center px-4 leading-relaxed">
Latest Thoughts <br className="sm:hidden" />
& Writings
</h1>
</AnimateIn>
<AnimateIn delay={100}>
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
<a
href="/rss"
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"
>
<RssIcon className="w-4 h-4" />
<span>RSS Feed</span>
</a>
<a
href="/blog/tags"
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"
>
<TagIcon className="w-4 h-4" />
<span>Browse Tags</span>
</a>
<a
href="/blog/popular"
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"
>
<TrendingUpIcon className="w-4 h-4" />
<span>Most Popular</span>
</a>
</div>
</AnimateIn>
</div>
);
};

View File

@@ -1,7 +1,8 @@
import React from "react";
import { AnimateIn } from "@/components/animate-in";
type BlogPost = {
slug: string;
id: string;
data: {
title: string;
author: string;
@@ -27,71 +28,73 @@ const formatDate = (dateString: string) => {
export const BlogPostList = ({ posts }: BlogPostListProps) => {
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">
{posts.map((post) => (
<li key={post.slug} className="group px-4 md:px-0">
<a
href={`/blog/${post.slug}`}
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 */}
<div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-background">
<img
src={post.data.image || "/blog/placeholder.png"}
alt={post.data.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
style={{ objectPosition: post.data.imagePosition || "center center" }}
/>
</div>
{/* Content container */}
<div className="w-full md:w-2/3 flex flex-col gap-2 md:gap-4 py-1 md:py-2">
{/* Title and meta info */}
<div className="space-y-1.5 md:space-y-3">
<h2 className="text-lg md:text-2xl font-semibold text-yellow group-hover:text-purple transition-colors duration-200 line-clamp-2">
{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>
<span className="text-foreground/50"></span>
<time dateTime={post.data.date} className="text-blue">
{formatDate(post.data.date)}
</time>
{posts.map((post, i) => (
<AnimateIn key={post.id} delay={i * 80}>
<li className="group px-4 md:px-0">
<a
href={`/blog/${post.id}`}
className="block"
>
<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">
{/* Image container with fixed aspect ratio */}
<div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-background flex-shrink-0">
<img
src={post.data.image || "/blog/placeholder.png"}
alt={post.data.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
style={{ objectPosition: post.data.imagePosition || "center center" }}
/>
</div>
{/* Content container */}
<div className="w-full md:w-2/3 flex flex-col gap-2 md:gap-3">
{/* Title and meta info */}
<div className="space-y-1.5 md:space-y-3">
<h2 className="text-lg md:text-2xl font-semibold text-yellow group-hover:text-purple transition-colors duration-200 line-clamp-2">
{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>
<span className="text-foreground/50"></span>
<time dateTime={post.data.date} className="text-blue">
{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>
{/* 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>
</article>
</a>
</li>
</article>
</a>
</li>
</AnimateIn>
))}
</ul>
</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.slug}`}
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 type { CollectionEntry } from "astro:content";
import { ProjectCard } from "@/components/projects/project-card";
import { AnimateIn } from "@/components/animate-in";
interface ProjectListProps {
projects: CollectionEntry<"projects">[];
}
export function ProjectList({ projects }: ProjectListProps) {
const latestProjects = projects.slice(0, 3);
const otherProjects = projects.slice(3);
return (
<div className="w-full max-w-6xl mx-auto pt-24 sm:pt-32">
<h1 className="text-2xl sm:text-3xl font-bold text-blue mb-12 text-center px-4 leading-relaxed">
Here's what I've been <br className="sm:hidden" />
building lately
</h1>
<div className="px-4 mb-16">
<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.slug} className="w-full max-w-md">
<ProjectCard project={project} />
</div>
))}
</div>
</div>
<div className="w-full max-w-6xl mx-auto pt-24 sm:pt-32 px-4">
<AnimateIn>
<h1 className="text-2xl sm:text-3xl font-bold text-blue mb-12 text-center leading-relaxed">
Here's what I've been <br className="sm:hidden" />
building lately
</h1>
</AnimateIn>
{otherProjects.length > 0 && (
<div className="px-4 pb-8">
<h2 className="text-xl font-bold text-foreground/90 mb-6">
All Projects
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr justify-items-center">
{otherProjects.map(project => (
<div key={project.slug} className="w-full max-w-md">
<ProjectCard project={project} />
</div>
))}
</div>
</div>
)}
<ul className="space-y-6 md:space-y-10">
{projects.map((project, i) => (
<AnimateIn key={project.id} delay={i * 80}>
<li className="group">
<a href={`/projects/${project.id}`} className="block">
<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">
{/* Image */}
<div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-foreground/5 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>
{/* 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>
);
}

View File

@@ -1,11 +1,149 @@
import React from "react";
import {
FileDown,
Github,
Linkedin,
Globe
import React, { useEffect, useRef, useState } from "react";
import {
FileDown,
Github,
Linkedin,
Globe
} 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 = {
name: "Timothy Pidashev",
title: "Software Engineer",
@@ -45,8 +183,8 @@ const resumeData = {
achievements: [
"Designed and built the entire application from the ground up, including auth",
"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",
"Created a smooth and responsive infinitely scrollable listings page",
"Implemented a filter provider to further narrow down search results and enhance the user experience",
"Created a smooth and responsive infinitely scrollable listings page",
"Automated deployment & testing processes reducing downtime by 60%"
]
}
@@ -57,22 +195,17 @@ const resumeData = {
school: "Clark College",
location: "Vancouver, WA",
period: "Graduating 2026",
achievements: []
achievements: [] as string[]
}
],
skills: {
technical: ["JavaScript", "TypeScript", "React", "Node.js", "Python", "SQL", "AWS", "Docker", "C", "Go", "Rust"],
soft: ["Leadership", "Problem Solving", "Communication", "Agile Methodologies"]
},
certifications: [
{
name: "AWS Certified Solutions Architect",
issuer: "Amazon Web Services",
date: "2022"
}
]
};
// --- Component ---
const Resume = () => {
const handleDownloadPDF = () => {
window.open("/timothy-pidashev-resume.pdf", "_blank");
@@ -83,188 +216,198 @@ const Resume = () => {
<div className="space-y-16">
{/* Header */}
<header className="text-center space-y-6">
<h1 className="text-6xl font-bold text-aqua-bright">{resumeData.name}</h1>
<h2 className="text-3xl text-foreground/80">{resumeData.title}</h2>
<div className="flex flex-col md:flex-row justify-center gap-4 md:gap-6 text-foreground/60 text-lg">
<a href={`mailto:${resumeData.contact.email}`} className="hover:text-foreground transition-colors duration-200">
{resumeData.contact.email}
</a>
<span className="hidden md:inline"></span>
<a href={`tel:${resumeData.contact.phone}`} className="hover:text-foreground transition-colors duration-200">
{resumeData.contact.phone}
</a>
<span className="hidden md:inline"></span>
<span>{resumeData.contact.location}</span>
</div>
<div className="flex justify-center items-center gap-6 text-lg">
<a href={`https://${resumeData.contact.github}`}
target="_blank"
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
>
<Github size={18} />
GitHub
</a>
<a href={`https://${resumeData.contact.linkedin}`}
target="_blank"
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
>
<Linkedin size={18} />
LinkedIn
</a>
<button
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>
<h1 className="text-6xl font-bold text-aqua-bright">{resumeData.name}</h1>
</Section>
<Section delay={150}>
<h2 className="text-3xl text-foreground/80">{resumeData.title}</h2>
</Section>
<Section delay={300}>
<div className="flex flex-col md:flex-row justify-center gap-4 md:gap-6 text-foreground/60 text-lg">
<a href={`mailto:${resumeData.contact.email}`} className="hover:text-foreground transition-colors duration-200">
{resumeData.contact.email}
</a>
<span className="hidden md:inline"></span>
<a href={`tel:${resumeData.contact.phone}`} className="hover:text-foreground transition-colors duration-200">
{resumeData.contact.phone}
</a>
<span className="hidden md:inline"></span>
<span>{resumeData.contact.location}</span>
</div>
</Section>
<Section delay={450}>
<div className="flex justify-center items-center gap-6 text-lg">
<a href={`https://${resumeData.contact.github}`}
target="_blank"
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
>
<Github size={18} />
GitHub
</a>
<a href={`https://${resumeData.contact.linkedin}`}
target="_blank"
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
>
<Linkedin size={18} />
LinkedIn
</a>
<button
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>
{/* Summary */}
<section className="space-y-4">
<h3 className="text-3xl font-bold text-yellow-bright">Professional Summary</h3>
<TypedSection heading="Professional Summary">
<p className="text-xl leading-relaxed">{resumeData.summary}</p>
</section>
</TypedSection>
{/* Experience */}
<section className="space-y-8">
<h3 className="text-3xl font-bold text-yellow-bright">Experience</h3>
{resumeData.experience.map((exp, index) => (
<div key={index} className="space-y-4">
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<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>
<TypedSection heading="Experience">
<div className="space-y-8">
{resumeData.experience.map((exp, index) => (
<Section key={index} delay={index * 100}>
<div className="space-y-4">
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<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 className="text-foreground/60 text-lg font-medium">{exp.period}</div>
</div>
<ul className="list-disc pl-6 space-y-3">
{exp.achievements.map((achievement, i) => (
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
))}
</ul>
</div>
))}
</section>
</Section>
))}
</div>
</TypedSection>
{/* Contract Work */}
<section className="space-y-8">
<h3 className="text-3xl font-bold text-yellow-bright">Contract Work</h3>
{resumeData.contractWork.map((project, index) => (
<div key={index} className="space-y-4">
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<div>
<div className="flex items-center gap-3">
<h4 className="text-2xl font-semibold text-green-bright">{project.title}</h4>
{project.url && (
<a
href={project.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-bright hover:text-blue transition-colors duration-200 opacity-60 hover:opacity-100"
aria-label={`Visit ${project.title}`}
>
<Globe size={16} strokeWidth={1.5} />
</a>
<TypedSection heading="Contract Work">
<div className="space-y-8">
{resumeData.contractWork.map((project, index) => (
<Section key={index} delay={index * 100}>
<div className="space-y-4">
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<div>
<div className="flex items-center gap-3">
<h4 className="text-2xl font-semibold text-green-bright">{project.title}</h4>
{project.url && (
<a
href={project.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-bright hover:text-blue transition-colors duration-200 opacity-60 hover:opacity-100"
>
<Globe size={16} strokeWidth={1.5} />
</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 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((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>
</Section>
))}
</div>
</TypedSection>
{/* Education */}
<section className="space-y-8">
<h3 className="text-3xl font-bold text-yellow-bright">Education</h3>
{resumeData.education.map((edu, index) => (
<div key={index} className="space-y-4">
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<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>
<TypedSection heading="Education">
<div className="space-y-8">
{resumeData.education.map((edu, index) => (
<Section key={index}>
<div className="space-y-4">
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
<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 className="text-foreground/60 text-lg font-medium">{edu.period}</div>
</div>
<ul className="list-disc pl-6 space-y-3">
{edu.achievements.map((achievement, i) => (
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
))}
</ul>
</div>
))}
</section>
</Section>
))}
</div>
</TypedSection>
{/* Skills */}
<section className="space-y-8">
<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>
*/}
<SkillsSection />
</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;

View File

@@ -1,7 +1,9 @@
import { defineCollection, z } from "astro:content";
import { glob } from "astro/loaders";
export const collections = {
blog: defineCollection({
loader: glob({ pattern: "**/*.mdx", base: "./src/content/blog" }),
schema: z.object({
title: z.string(),
description: z.string(),
@@ -12,9 +14,11 @@ export const collections = {
}),
image: z.string().optional(),
imagePosition: z.string().optional(),
isDraft: z.boolean().optional()
}),
}),
projects: defineCollection({
loader: glob({ pattern: "**/*.mdx", base: "./src/content/projects" }),
schema: z.object({
title: z.string(),
description: z.string(),
@@ -22,7 +26,7 @@ export const collections = {
demoUrl: z.string().url().optional(),
techStack: z.array(z.string()),
date: z.string(),
image: z.string().optional(),
image: z.string().optional()
}),
})
};

View File

@@ -5,4 +5,5 @@ author: Timothy Pidashev
tags: ["uefi", "coreboot", "firmware", "chromebooks"]
date: 2025-09-15
image: "/blog/breaking-the-chromebook-cage/thumbnail.png"
isDraft: true
---

View File

@@ -5,6 +5,7 @@ author: Timothy Pidashev
tags: [t440p, coreboot, thinkpad]
date: 2025-01-15
image: "/blog/thinkpad-t440p-coreboot-guide/thumbnail.png"
isDraft: true
---
import { Commands, Command, CommandSequence } from "@/components/mdx/command";
@@ -24,6 +25,7 @@ import Advertisement from '@/content/blog/components/thinkpad-t440p-coreboot-gui
Don't pipe anyone's scripts to **sh** blindly, including mine - <a href="https://github.com/timmypidashev/scripts" target="_blank" rel="noopener noreferrer">audit the source</a>.
## Getting Started
The Thinkpad T440p is a powerful and versatile laptop that can be further enhanced by installing coreboot,
an open-source BIOS replacement. This guide will walk you through the process of corebooting your T440p,
including flashing the BIOS chip and installing the necessary software.
@@ -201,22 +203,64 @@ Configuring coreboot is really where most of your time will be spent. To help ou
I've created several handy configs that should suit most use cases, and can be easily
tweaked to your liking. Here is a list of whats available:
1. GRUB2
### 1. GRUB2 (Recommended)
This configuration features GRUB2 as the bootloader, and contains 3 secondary payloads,
which the user can opt in/out of:
* memtest built in
* nvramcui built in
* coreinfo built in
GRUB2 is the recommended payload for most users. It boots Linux directly without needing a
separate bootloader installation on disk. This configuration includes three secondary payloads:
This configuration also includes the dGPU option rom as well for T440p's featuring the gt730m on board.
- **memtest86+** - Memory testing utility
- **nvramcui** - CMOS/NVRAM settings editor
- **coreinfo** - System information viewer
2. SeaBIOS
If your T440p has the optional GT730M dGPU, the GRUB2 config also includes the
necessary VGA option ROM for it.
3. edk2
### 2. SeaBIOS
> NOTE: Show the user how to choose the appropriate config, as well as building a custom config below.
SeaBIOS provides a traditional BIOS interface, making it the most compatible option.
Choose this if you need to boot operating systems that expect a legacy BIOS, such
as Windows or BSD.
### 3. edk2 (UEFI)
edk2 provides a UEFI firmware interface. Choose this if you prefer UEFI boot or
need UEFI-specific features.
---
If using the interactive script, it will prompt you to choose a payload and apply
a preset configuration automatically. You can also choose to open the full
configuration menu (`make nconfig`) to customize further.
For manual configuration, first copy the extracted blobs into place:
<CommandSequence
commands={[
"mkdir -p ~/t440p-coreboot/coreboot/3rdparty/blobs/mainboard/lenovo/haswell",
"mkdir -p ~/t440p-coreboot/coreboot/3rdparty/blobs/cpu/intel/haswell",
"cp ~/t440p-coreboot/ifd.bin ~/t440p-coreboot/coreboot/3rdparty/blobs/mainboard/lenovo/haswell/descriptor.bin",
"cp ~/t440p-coreboot/me.bin ~/t440p-coreboot/coreboot/3rdparty/blobs/mainboard/lenovo/haswell/me.bin",
"cp ~/t440p-coreboot/gbe.bin ~/t440p-coreboot/coreboot/3rdparty/blobs/mainboard/lenovo/haswell/gbe.bin",
"cp ~/t440p-coreboot/mrc.bin ~/t440p-coreboot/coreboot/3rdparty/blobs/cpu/intel/haswell/mrc.bin"
]}
description="Copy firmware blobs into the coreboot source tree"
client:load
/>
Then open the configuration menu:
<Command
description="Open coreboot configuration"
command="cd ~/t440p-coreboot/coreboot && make nconfig"
client:load
/>
Key settings to configure:
- **Mainboard** &rarr; Mainboard vendor: **Lenovo** &rarr; Mainboard model: **ThinkPad T440p**
- **Chipset** &rarr; Add Intel descriptor.bin, ME firmware, and GbE configuration (set paths to your blobs)
- **Chipset** &rarr; Add haswell MRC file (set path to mrc.bin)
- **Payload** &rarr; Choose your preferred payload (GRUB2, SeaBIOS, or edk2)
## Building and Flashing
@@ -238,7 +282,7 @@ Once the coreboot build has completed, split the built ROM for the 8MB(bottom) c
commands={[
"cd ~/t440p-coreboot/coreboot/build",
"dd if=coreboot.rom of=bottom.rom bs=1M count=8",
"dd if=coreboot.rom of=top.rom bs=1M skin=8"
"dd if=coreboot.rom of=top.rom bs=1M skip=8"
]}
description="Split the built ROM for both EEPROM chips"
client:load

View File

@@ -41,7 +41,7 @@ export function getArticleSchema(post: CollectionEntry<"blog">) {
"@context": "https://schema.org",
"@type": "Article",
headline: post.data.title,
url: `${import.meta.env.SITE}/blog/${post.slug}/`,
url: `${import.meta.env.SITE}/blog/${post.id}/`,
description: post.data.excerpt,
datePublished: post.data.date.toString(),
publisher: {

View File

@@ -8,7 +8,7 @@ import Timeline from "@/components/about/timeline";
import CurrentFocus from "@/components/about/current-focus";
import OutsideCoding from "@/components/about/outside-coding";
---
<ContentLayout
<ContentLayout
title="About | Timothy Pidashev"
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 />
</section>
<section class="flex items-center justify-center py-16">
<AllTimeStats client:only="react" />
<section class="min-h-[60vh] flex items-center justify-center py-16">
<AllTimeStats client:load />
</section>
<section class="flex items-center justify-center py-16">
<DetailedStats client:only="react" />
<section class="min-h-screen flex items-center justify-center py-16">
<DetailedStats client:load />
</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 />
</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 />
</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 />
</section>
</div>

View File

@@ -2,7 +2,14 @@ import type { APIRoute } from 'astro';
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 {
const response = await fetch(
'https://wakatime.com/api/v1/users/current/summaries?range=last_6_months', {

View File

@@ -1,22 +1,35 @@
import type { APIRoute } from "astro";
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
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 {
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`);
return new Response(stdout, {
status: 200,
headers: {
"Content-Type": "application/json"
const response = await fetch(
"https://wakatime.com/api/v1/users/current/all_time_since_today",
{
headers: {
Authorization: `Basic ${Buffer.from(WAKATIME_API_KEY).toString("base64")}`,
},
}
);
const data = await response.json();
return new Response(JSON.stringify(data), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
return new Response(JSON.stringify({ error: "Failed to fetch stats" }), {
status: 500
});
console.error("WakaTime alltime API error:", error);
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 () => {
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 {
const response = await fetch(
'https://wakatime.com/api/v1/users/current/stats/last_7_days?timeout=15', {

View File

@@ -1,5 +1,5 @@
---
import { getCollection } from "astro:content";
import { getCollection, render } from "astro:content";
import { Image } from "astro:assets";
import ContentLayout from "@/layouts/content.astro";
import { getArticleSchema } from "@/lib/structuredData";
@@ -11,17 +11,17 @@ const { slug } = Astro.params;
// Fetch blog posts
const posts = await getCollection("blog");
const post = posts.find(post => post.slug === slug);
const post = posts.find(post => post.id === slug);
if (!post) {
if (!post || (!import.meta.env.DEV && post.data.isDraft === true)) {
return new Response(null, {
status: 404,
statusText: 'Not found'
statusText: "Not found"
});
}
// Dynamically render the content
const { Content } = await post.render();
const { Content } = await render(post);
// Format the date
const formattedDate = new Date(post.data.date).toLocaleDateString("en-US", {
@@ -46,7 +46,7 @@ const breadcrumbsStructuredData = {
"@type": "ListItem",
position: 2,
name: post.data.title,
item: `${import.meta.env.SITE}/blog/${post.slug}/`,
item: `${import.meta.env.SITE}/blog/${post.id}/`,
},
],
};
@@ -65,7 +65,7 @@ const jsonLd = {
<div class="relative max-w-8xl mx-auto">
<article class="prose prose-invert prose-lg mx-auto max-w-4xl">
{post.data.image && (
<div class="-mx-4 sm:mx-0 mb-8">
<div class="-mx-4 sm:mx-0 mb-4">
<Image
src={post.data.image}
alt={post.data.title}
@@ -76,20 +76,18 @@ const jsonLd = {
/>
</div>
)}
<h1 class="text-3xl pt-4">{post.data.title}</h1>
<p class="lg:text-2xl sm:text-lg">{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">
<span class="text-orange">{post.data.author}</span>
<span class="text-foreground/50">•</span>
<time dateTime={post.data.date instanceof Date ? post.data.date.toISOString() : post.data.date} class="text-blue">
{formattedDate}
</time>
</div>
<h1 class="text-3xl !mt-2 !mb-2">{post.data.title}</h1>
<p class="lg:text-2xl sm:text-lg !mt-0 !mb-3">{post.data.description}</p>
<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-foreground/50">•</span>
<time dateTime={post.data.date instanceof Date ? post.data.date.toISOString() : post.data.date} class="text-blue">
{formattedDate}
</time>
</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) => (
<span
<span
class="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
onclick={`window.location.href='/blog/tag/${tag}'`}
>

View File

@@ -5,7 +5,7 @@ import { BlogHeader } from "@/components/blog/header";
import { BlogPostList } from "@/components/blog/post-list";
const posts = (await getCollection("blog", ({ data }) => {
return data.isDraft !== true;
return import.meta.env.DEV || data.isDraft !== true;
})).sort((a, b) => {
return b.data.date.valueOf() - a.data.date.valueOf()
}).map(post => ({

View File

@@ -5,7 +5,7 @@ import ContentLayout from "@/layouts/content.astro";
import TagList from "@/components/blog/tag-list";
const posts = (await getCollection("blog", ({ data }) => {
return data.isDraft !== true;
return import.meta.env.DEV || data.isDraft !== true;
})).sort((a, b) => {
return b.data.date.valueOf() - a.data.date.valueOf()
}).map(post => ({

View File

@@ -1,7 +1,7 @@
---
export const prerender = true;
import { getCollection } from "astro:content";
import { getCollection, render } from "astro:content";
import ContentLayout from "@/layouts/content.astro";
import { Comments } from "@/components/blog/comments";
@@ -9,13 +9,13 @@ import { Comments } from "@/components/blog/comments";
export async function getStaticPaths() {
const projects = await getCollection("projects");
return projects.map(project => ({
params: { slug: project.slug },
params: { slug: project.id },
props: { project },
}));
}
const { project } = Astro.props;
const { Content } = await project.render();
const { Content } = await render(project);
---
<ContentLayout title={`${project.data.title} | Timothy Pidashev`}>

View File

@@ -16,11 +16,11 @@ export async function GET(context: APIContext) {
title: post.data.title,
pubDate: post.data.date,
description: post.data.description,
link: `/blog/${post.slug}/`,
link: `/blog/${post.id}/`,
author: post.data.author,
categories: post.data.tags,
enclosure: post.data.image ? {
url: new URL(`blog/${post.slug}/thumbnail.png`, context.site).toString(),
url: new URL(`blog/${post.id}/thumbnail.png`, context.site).toString(),
type: 'image/jpeg',
length: 0
} : undefined