mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 11:03:50 +00:00
Compare commits
1 Commits
2c5f64a769
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36c6484c81 |
@@ -8,36 +8,36 @@
|
|||||||
"preview": "astro preview"
|
"preview": "astro preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/react": "^5.0.2",
|
"@astrojs/react": "^4.4.0",
|
||||||
"@astrojs/tailwind": "^6.0.2",
|
"@astrojs/tailwind": "^6.0.2",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@types/react": "^18.3.28",
|
"@types/react": "^18.3.20",
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.6",
|
||||||
"astro": "^6.1.2",
|
"astro": "^5.14.1",
|
||||||
"tailwindcss": "^3.4.19"
|
"tailwindcss": "^3.4.17"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^5.0.3",
|
"@astrojs/mdx": "^4.3.6",
|
||||||
"@astrojs/node": "^10.0.4",
|
"@astrojs/node": "^9.4.4",
|
||||||
"@astrojs/rss": "^4.0.18",
|
"@astrojs/rss": "^4.0.12",
|
||||||
"@astrojs/sitemap": "^3.7.2",
|
"@astrojs/sitemap": "^3.6.0",
|
||||||
"@giscus/react": "^3.1.0",
|
"@giscus/react": "^3.1.0",
|
||||||
"@pilcrowjs/object-parser": "^0.0.4",
|
"@pilcrowjs/object-parser": "^0.0.4",
|
||||||
"@react-hook/intersection-observer": "^3.1.2",
|
"@react-hook/intersection-observer": "^3.1.2",
|
||||||
"@rehype-pretty/transformers": "^0.13.2",
|
"@rehype-pretty/transformers": "^0.13.2",
|
||||||
"arctic": "^3.7.0",
|
"arctic": "^3.6.0",
|
||||||
"lucide-react": "^0.468.0",
|
"lucide-react": "^0.468.0",
|
||||||
"marked": "^15.0.12",
|
"marked": "^15.0.8",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-icons": "^5.6.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-responsive": "^10.0.1",
|
"react-responsive": "^10.0.1",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
"rehype-pretty-code": "^0.14.3",
|
"rehype-pretty-code": "^0.14.1",
|
||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"schema-dts": "^1.1.5",
|
"schema-dts": "^1.1.5",
|
||||||
"shiki": "^3.23.0",
|
"shiki": "^3.12.2",
|
||||||
"typewriter-effect": "^2.22.0",
|
"typewriter-effect": "^2.21.0",
|
||||||
"unist-util-visit": "^5.1.0"
|
"unist-util-visit": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3168
src/pnpm-lock.yaml
generated
3168
src/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,57 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React from 'react';
|
||||||
import { Code2, BookOpen, RocketIcon, Compass } from "lucide-react";
|
import { Code2, BookOpen, RocketIcon, Compass } from 'lucide-react';
|
||||||
|
|
||||||
function AnimateIn({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const [skip, setSkip] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const inView = rect.top < window.innerHeight && rect.bottom > 0;
|
|
||||||
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
|
|
||||||
|
|
||||||
if (inView && isReload) {
|
|
||||||
setSkip(true);
|
|
||||||
setVisible(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (inView) {
|
|
||||||
requestAnimationFrame(() => setVisible(true));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setVisible(true);
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.15 }
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.observe(el);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={skip ? "" : "transition-all duration-700 ease-out"}
|
|
||||||
style={skip ? {} : {
|
|
||||||
transitionDelay: `${delay}ms`,
|
|
||||||
opacity: visible ? 1 : 0,
|
|
||||||
transform: visible ? "translateY(0)" : "translateY(24px)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CurrentFocus() {
|
export default function CurrentFocus() {
|
||||||
const recentProjects = [
|
const recentProjects = [
|
||||||
@@ -59,134 +7,126 @@ export default function CurrentFocus() {
|
|||||||
title: "Darkbox",
|
title: "Darkbox",
|
||||||
description: "My gruvbox theme, with a pure black background",
|
description: "My gruvbox theme, with a pure black background",
|
||||||
href: "/projects/darkbox",
|
href: "/projects/darkbox",
|
||||||
tech: ["Neovim", "Lua"],
|
tech: ["Neovim", "Lua"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Revive Auto Parts",
|
title: "Revive Auto Parts",
|
||||||
description: "A car parts listing site built for a client",
|
description: "A car parts listing site built for a client",
|
||||||
href: "/projects/reviveauto",
|
href: "/projects/reviveauto",
|
||||||
tech: ["Tanstack", "React Query", "Fastapi"],
|
tech: ["Tanstack", "React Query", "Fastapi"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Fhccenter",
|
title: "Fhccenter",
|
||||||
description: "Website made for a private school",
|
description: "Website made for a private school",
|
||||||
href: "/projects/fhccenter",
|
href: "/projects/fhccenter",
|
||||||
tech: ["Nextjs", "Typescript", "Prisma"],
|
tech: ["Nextjs", "Typescript", "Prisma"]
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center w-full">
|
<div className="flex justify-center items-center w-full">
|
||||||
<div className="w-full max-w-6xl p-4 sm:px-6 py-6 sm:py-8">
|
<div className="w-full max-w-6xl p-4 sm:px-6 py-6 sm:py-8">
|
||||||
<AnimateIn>
|
<h2 className="text-3xl sm:text-4xl font-bold text-center text-yellow-bright mb-8 sm:mb-12">
|
||||||
<h2 className="text-3xl sm:text-4xl font-bold text-center text-yellow-bright mb-8 sm:mb-12">
|
Current Focus
|
||||||
Current Focus
|
</h2>
|
||||||
</h2>
|
|
||||||
</AnimateIn>
|
|
||||||
|
|
||||||
{/* Recent Projects Section */}
|
{/* Recent Projects Section */}
|
||||||
<div className="mb-8 sm:mb-16">
|
<div className="mb-8 sm:mb-16">
|
||||||
<AnimateIn delay={100}>
|
<div className="flex items-center justify-center gap-2 mb-6">
|
||||||
<div className="flex items-center justify-center gap-2 mb-6">
|
<Code2 className="text-yellow-bright" size={24} />
|
||||||
<Code2 className="text-yellow-bright" size={24} />
|
<h3 className="text-xl font-bold text-foreground/90">Recent Projects</h3>
|
||||||
<h3 className="text-xl font-bold text-foreground/90">Recent Projects</h3>
|
</div>
|
||||||
</div>
|
|
||||||
</AnimateIn>
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 max-w-5xl mx-auto">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 max-w-5xl mx-auto">
|
||||||
{recentProjects.map((project, i) => (
|
{recentProjects.map((project) => (
|
||||||
<AnimateIn key={project.title} delay={200 + i * 100}>
|
<a
|
||||||
<a
|
href={project.href}
|
||||||
href={project.href}
|
key={project.title}
|
||||||
className="block p-4 sm:p-6 rounded-lg border border-foreground/10 hover:border-yellow-bright/50
|
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 h-full"
|
transition-all duration-300 group bg-background/50"
|
||||||
>
|
>
|
||||||
<h4 className="font-bold text-lg group-hover:text-yellow-bright transition-colors">
|
<h4 className="font-bold text-lg group-hover:text-yellow-bright transition-colors">
|
||||||
{project.title}
|
{project.title}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-foreground/70 mt-2 text-sm sm:text-base">{project.description}</p>
|
<p className="text-foreground/70 mt-2 text-sm sm:text-base">{project.description}</p>
|
||||||
<div className="flex flex-wrap gap-2 mt-3">
|
<div className="flex flex-wrap gap-2 mt-3">
|
||||||
{project.tech.map((tech) => (
|
{project.tech.map((tech) => (
|
||||||
<span key={tech} className="text-xs px-2 py-1 rounded-full bg-foreground/5 text-foreground/60">
|
<span key={tech} className="text-xs px-2 py-1 rounded-full bg-foreground/5 text-foreground/60">
|
||||||
{tech}
|
{tech}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</AnimateIn>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Current Learning & Interests */}
|
{/* Current Learning & Interests */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 max-w-5xl mx-auto">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-8 max-w-5xl mx-auto">
|
||||||
<AnimateIn delay={100}>
|
{/* What I'm Learning */}
|
||||||
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50 h-full">
|
<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">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<BookOpen className="text-green-bright" size={24} />
|
<BookOpen className="text-green-bright" size={24} />
|
||||||
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Currently Learning</h3>
|
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Currently Learning</h3>
|
||||||
</div>
|
|
||||||
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
|
|
||||||
<span>Rust Programming</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
|
|
||||||
<span>WebAssembly with Rust</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-green-bright flex-shrink-0" />
|
|
||||||
<span>HTTP/3 & WebTransport</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</AnimateIn>
|
<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 delay={200}>
|
{/* Project Interests */}
|
||||||
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50 h-full">
|
<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">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<RocketIcon className="text-blue-bright" size={24} />
|
<RocketIcon className="text-blue-bright" size={24} />
|
||||||
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Project Interests</h3>
|
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Project Interests</h3>
|
||||||
</div>
|
|
||||||
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
|
|
||||||
<span>AI Model Integration</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
|
|
||||||
<span>Rust Systems Programming</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-blue-bright flex-shrink-0" />
|
|
||||||
<span>Cross-platform WASM Apps</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</AnimateIn>
|
<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 delay={300}>
|
{/* Areas to Explore */}
|
||||||
<div className="space-y-4 p-4 sm:p-6 rounded-lg border border-foreground/10 bg-background/50 h-full">
|
<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">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Compass className="text-purple-bright" size={24} />
|
<Compass className="text-purple-bright" size={24} />
|
||||||
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Want to Explore</h3>
|
<h3 className="text-lg sm:text-xl font-bold text-foreground/90">Want to Explore</h3>
|
||||||
</div>
|
|
||||||
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
|
|
||||||
<span>LLM Fine-tuning</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
|
|
||||||
<span>Rust 2024 Edition</span>
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-2">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
|
|
||||||
<span>Real-time Web Transport</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</AnimateIn>
|
<ul className="space-y-3 text-sm sm:text-base text-foreground/70">
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
|
||||||
|
<span>LLM Fine-tuning</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
|
||||||
|
<span>Rust 2024 Edition</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-purple-bright flex-shrink-0" />
|
||||||
|
<span>Real-time Web Transport</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,98 +1,56 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React from "react";
|
||||||
import { ChevronDownIcon } from "@/components/icons";
|
import { ChevronDownIcon } from "@/components/icons";
|
||||||
|
|
||||||
export default function Intro() {
|
export default function Intro() {
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const inView = rect.top < window.innerHeight && rect.bottom > 0;
|
|
||||||
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
|
|
||||||
|
|
||||||
if (inView && isReload) {
|
|
||||||
setVisible(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (inView) {
|
|
||||||
// Fresh navigation — animate in
|
|
||||||
requestAnimationFrame(() => setVisible(true));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setVisible(true);
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.2 }
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.observe(el);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const scrollToNext = () => {
|
const scrollToNext = () => {
|
||||||
const nextSection = document.querySelector("section")?.nextElementSibling;
|
const nextSection = document.querySelector("section")?.nextElementSibling;
|
||||||
if (nextSection) {
|
if (nextSection) {
|
||||||
const offset = (nextSection as HTMLElement).offsetTop - (window.innerHeight - (nextSection as HTMLElement).offsetHeight) / 2;
|
const offset = nextSection.offsetTop - (window.innerHeight - nextSection.offsetHeight) / 2; // Center the section
|
||||||
window.scrollTo({ top: offset, behavior: "smooth" });
|
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 (
|
return (
|
||||||
<div ref={ref} className="w-full max-w-4xl px-4">
|
<div className="w-full max-w-4xl px-4">
|
||||||
<div className="space-y-8 md:space-y-12">
|
<div className="space-y-8 md:space-y-12">
|
||||||
<div className="flex flex-col sm:flex-row items-center sm:items-center justify-center gap-8 sm:gap-16">
|
<div className="flex flex-col sm:flex-row items-center sm:items-center justify-center gap-8 sm:gap-16">
|
||||||
<div
|
<div className="w-32 h-32 sm:w-48 sm:h-48 shrink-0">
|
||||||
className="w-32 h-32 sm:w-48 sm:h-48 shrink-0"
|
|
||||||
style={anim(0)}
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
src="/me.jpeg"
|
src="/me.jpeg"
|
||||||
alt="Timothy Pidashev"
|
alt="Timothy Pidashev"
|
||||||
className="rounded-lg object-cover w-full h-full ring-2 ring-yellow-bright hover:ring-orange-bright transition-all duration-300"
|
className="rounded-lg object-cover w-full h-full ring-2 ring-yellow-bright hover:ring-orange-bright transition-all duration-300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center sm:text-left space-y-4 sm:space-y-6" style={anim(150)}>
|
<div className="text-center sm:text-left space-y-4 sm:space-y-6">
|
||||||
<h2 className="text-xl sm:text-5xl font-bold text-yellow-bright">
|
<h2 className="text-xl sm:text-5xl font-bold text-yellow-bright">
|
||||||
Timothy Pidashev
|
Timothy Pidashev
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-sm sm:text-xl text-foreground/70 space-y-2 sm:space-y-3">
|
<div className="text-sm sm:text-xl text-foreground/70 space-y-2 sm:space-y-3">
|
||||||
<p className="flex items-center justify-center font-bold sm:justify-start gap-2" style={anim(300)}>
|
<p className="flex items-center justify-center font-bold sm:justify-start gap-2">
|
||||||
<span className="text-blue">Software Systems Engineer</span>
|
<span className="text-blue">Software Systems Engineer</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="flex items-center justify-center font-bold sm:justify-start gap-2" style={anim(450)}>
|
<p className="flex items-center justify-center font-bold sm:justify-start gap-2">
|
||||||
<span className="text-green">Open Source Enthusiast</span>
|
<span className="text-green">Open Source Enthusiast</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="flex items-center justify-center font-bold sm:justify-start gap-2" style={anim(600)}>
|
<p className="flex items-center justify-center font-bold sm:justify-start gap-2">
|
||||||
<span className="text-yellow">Coffee Connoisseur</span>
|
<span className="text-yellow">Coffee Connoisseur</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-8" style={anim(750)}>
|
<div className="space-y-8">
|
||||||
<p className="text-foreground/80 text-center text-base sm:text-2xl italic max-w-3xl mx-auto font-medium">
|
<p className="text-foreground/80 text-center text-base sm:text-2xl italic max-w-3xl mx-auto font-medium">
|
||||||
"Turning coffee into code" isn't just a clever phrase –
|
"Turning coffee into code" isn't just a clever phrase –
|
||||||
<span className="text-aqua-bright"> it's how I approach each project:</span>
|
<span className="text-aqua-bright"> it's how I approach each project:</span>
|
||||||
<span className="text-purple-bright"> methodically,</span>
|
<span className="text-purple-bright"> methodically,</span>
|
||||||
<span className="text-blue-bright"> with attention to detail,</span>
|
<span className="text-blue-bright"> with attention to detail,</span>
|
||||||
<span className="text-green-bright"> and a refined process.</span>
|
<span className="text-green-bright"> and a refined process.</span>
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center" style={anim(900)}>
|
<div className="flex justify-center">
|
||||||
<button
|
<button
|
||||||
onClick={scrollToNext}
|
onClick={scrollToNext}
|
||||||
className="text-foreground/50 hover:text-yellow-bright transition-colors duration-300"
|
className="text-foreground/50 hover:text-yellow-bright transition-colors duration-300"
|
||||||
aria-label="Scroll to next section"
|
aria-label="Scroll to next section"
|
||||||
|
|||||||
@@ -1,115 +1,64 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React from 'react';
|
||||||
import { Cross, Fish, Mountain, Book } from "lucide-react";
|
import { Fish, Mountain, Book, Car } from 'lucide-react';
|
||||||
|
|
||||||
function AnimateIn({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const [skip, setSkip] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const inView = rect.top < window.innerHeight && rect.bottom > 0;
|
|
||||||
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
|
|
||||||
|
|
||||||
if (inView && isReload) {
|
|
||||||
setSkip(true);
|
|
||||||
setVisible(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (inView) {
|
|
||||||
requestAnimationFrame(() => setVisible(true));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setVisible(true);
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.15 }
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.observe(el);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={skip ? "" : "transition-all duration-700 ease-out"}
|
|
||||||
style={skip ? {} : {
|
|
||||||
transitionDelay: `${delay}ms`,
|
|
||||||
opacity: visible ? 1 : 0,
|
|
||||||
transform: visible ? "translateY(0) scale(1)" : "translateY(20px) scale(0.97)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const interests = [
|
|
||||||
{
|
|
||||||
icon: <Cross className="text-red-bright" size={20} />,
|
|
||||||
title: "Faith",
|
|
||||||
description: "My walk with Jesus is the foundation of everything I do, guiding my purpose and perspective",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Fish className="text-blue-bright" size={20} />,
|
|
||||||
title: "Fishing",
|
|
||||||
description: "Finding peace and adventure on the water, always looking for the next great fishing spot",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Mountain className="text-green-bright" size={20} />,
|
|
||||||
title: "Hiking",
|
|
||||||
description: "Exploring trails with friends and seeking out scenic viewpoints in nature",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <Book className="text-purple-bright" size={20} />,
|
|
||||||
title: "Reading",
|
|
||||||
description: "Deep diving into novels & technical books that expand my horizons & captivate my mind",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function OutsideCoding() {
|
export default function OutsideCoding() {
|
||||||
|
const interests = [
|
||||||
|
{
|
||||||
|
icon: <Fish className="text-blue-bright" size={20} />,
|
||||||
|
title: "Fishing",
|
||||||
|
description: "Finding peace and adventure on the water, always looking for the next great fishing spot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Mountain className="text-green-bright" size={20} />,
|
||||||
|
title: "Hiking",
|
||||||
|
description: "Exploring trails with friends and seeking out scenic viewpoints in nature"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Book className="text-purple-bright" size={20} />,
|
||||||
|
title: "Reading",
|
||||||
|
description: "Deep diving into novels & technical books that expand my horizons & captivate my mind"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Car className="text-yellow-bright" size={20} />,
|
||||||
|
title: "Project Cars",
|
||||||
|
description: "Working on automotive projects, modifying & restoring sporty sedans"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center w-full">
|
<div className="flex justify-center items-center w-full">
|
||||||
<div className="w-full max-w-4xl px-4 py-8">
|
<div className="w-full max-w-4xl px-4 py-8">
|
||||||
<AnimateIn>
|
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8">
|
||||||
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8">
|
Outside of Programming
|
||||||
Outside of Programming
|
</h2>
|
||||||
</h2>
|
|
||||||
</AnimateIn>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||||
{interests.map((interest, i) => (
|
{interests.map((interest) => (
|
||||||
<AnimateIn key={interest.title} delay={100 + i * 100}>
|
<div
|
||||||
<div
|
key={interest.title}
|
||||||
className="flex flex-col items-center text-center p-4 rounded-lg border border-foreground/10
|
className="flex flex-col items-center text-center p-4 rounded-lg border border-foreground/10
|
||||||
hover:border-yellow-bright/50 transition-all duration-300 bg-background/50 h-full"
|
hover:border-yellow-bright/50 transition-all duration-300 bg-background/50"
|
||||||
>
|
>
|
||||||
<div className="mb-3">{interest.icon}</div>
|
<div className="mb-3">
|
||||||
<h3 className="font-bold text-foreground/90 mb-2">{interest.title}</h3>
|
{interest.icon}
|
||||||
<p className="text-sm text-foreground/70">{interest.description}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</AnimateIn>
|
<h3 className="font-bold text-foreground/90 mb-2">
|
||||||
|
{interest.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-foreground/70">
|
||||||
|
{interest.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimateIn delay={500}>
|
<p className="text-center text-foreground/80 mt-8 max-w-2xl mx-auto text-sm md:text-base italic">
|
||||||
<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
|
||||||
When I'm not writing code, you'll find me
|
<span className="text-blue-bright"> out on the water,</span>
|
||||||
<span className="text-red-bright"> walking with Christ,</span>
|
<span className="text-green-bright"> hiking trails,</span>
|
||||||
<span className="text-blue-bright"> out on the water,</span>
|
<span className="text-purple-bright"> reading books,</span>
|
||||||
<span className="text-green-bright"> hiking trails,</span>
|
<span className="text-yellow-bright"> or modifying my current ride.</span>
|
||||||
<span className="text-purple-bright"> or reading books.</span>
|
</p>
|
||||||
</p>
|
|
||||||
</AnimateIn>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,19 +1,34 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect } from 'react';
|
||||||
|
|
||||||
interface ActivityDay {
|
export const ActivityGrid = () => {
|
||||||
grand_total: { total_seconds: number };
|
const [data, setData] = useState([]);
|
||||||
date: string;
|
const [loading, setLoading] = useState(true);
|
||||||
}
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
interface ActivityGridProps {
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
data: ActivityDay[];
|
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
}
|
|
||||||
|
|
||||||
export const ActivityGrid = ({ data }: ActivityGridProps) => {
|
useEffect(() => {
|
||||||
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
const fetchData = async () => {
|
||||||
const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getIntensity = (hours: number) => {
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Get intensity based on coding hours (0-4 for different shades)
|
||||||
|
const getIntensity = (hours) => {
|
||||||
if (hours === 0) return 0;
|
if (hours === 0) return 0;
|
||||||
if (hours < 2) return 1;
|
if (hours < 2) return 1;
|
||||||
if (hours < 4) return 2;
|
if (hours < 4) return 2;
|
||||||
@@ -21,18 +36,20 @@ export const ActivityGrid = ({ data }: ActivityGridProps) => {
|
|||||||
return 4;
|
return 4;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getColorClass = (intensity: number) => {
|
// Get color class based on intensity
|
||||||
if (intensity === 0) return "bg-foreground/5";
|
const getColorClass = (intensity) => {
|
||||||
if (intensity === 1) return "bg-green-DEFAULT/30";
|
if (intensity === 0) return 'bg-foreground/5';
|
||||||
if (intensity === 2) return "bg-green-DEFAULT/60";
|
if (intensity === 1) return 'bg-green-DEFAULT/30';
|
||||||
if (intensity === 3) return "bg-green-DEFAULT/80";
|
if (intensity === 2) return 'bg-green-DEFAULT/60';
|
||||||
return "bg-green-bright";
|
if (intensity === 3) return 'bg-green-DEFAULT/80';
|
||||||
|
return 'bg-green-bright';
|
||||||
};
|
};
|
||||||
|
|
||||||
const weeks: ActivityDay[][] = [];
|
// Group data by week
|
||||||
let currentWeek: ActivityDay[] = [];
|
const weeks = [];
|
||||||
|
let currentWeek = [];
|
||||||
if (data && data.length > 0) {
|
|
||||||
|
if (data.length > 0) {
|
||||||
data.forEach((day, index) => {
|
data.forEach((day, index) => {
|
||||||
currentWeek.push(day);
|
currentWeek.push(day);
|
||||||
if (currentWeek.length === 7 || index === data.length - 1) {
|
if (currentWeek.length === 7 || index === data.length - 1) {
|
||||||
@@ -42,19 +59,31 @@ export const ActivityGrid = ({ data }: ActivityGridProps) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (loading) {
|
||||||
return null;
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background border border-foreground/10 rounded-lg p-6 hover:border-foreground/20 transition-colors">
|
<div className="bg-background border border-foreground/10 rounded-lg p-6 hover:border-foreground/20 transition-colors">
|
||||||
<div className="text-lg text-aqua-bright mb-6">Activity</div>
|
<div className="text-lg text-aqua-bright mb-6">Activity</div>
|
||||||
|
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{/* Days labels */}
|
{/* Days labels */}
|
||||||
<div className="flex flex-col gap-2 pt-6 text-xs">
|
<div className="flex flex-col gap-2 pt-6 text-xs">
|
||||||
{days.map((day, i) => (
|
{days.map((day, i) => (
|
||||||
<div key={day} className="h-3 text-foreground/60">{i % 2 === 0 ? day : ""}</div>
|
<div key={day} className="h-3 text-foreground/60">{i % 2 === 0 ? day : ''}</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* Grid */}
|
{/* Grid */}
|
||||||
@@ -65,16 +94,17 @@ export const ActivityGrid = ({ data }: ActivityGridProps) => {
|
|||||||
{week.map((day, dayIndex) => {
|
{week.map((day, dayIndex) => {
|
||||||
const hours = day.grand_total.total_seconds / 3600;
|
const hours = day.grand_total.total_seconds / 3600;
|
||||||
const intensity = getIntensity(hours);
|
const intensity = getIntensity(hours);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={dayIndex}
|
key={dayIndex}
|
||||||
className={`w-3 h-3 rounded-sm ${getColorClass(intensity)}
|
className={`w-3 h-3 rounded-sm ${getColorClass(intensity)}
|
||||||
hover:ring-1 hover:ring-foreground/30 transition-all cursor-pointer
|
hover:ring-1 hover:ring-foreground/30 transition-all cursor-pointer
|
||||||
group relative`}
|
group relative`}
|
||||||
>
|
>
|
||||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 p-2
|
{/* Tooltip */}
|
||||||
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">
|
group-hover:opacity-100 transition-opacity z-10 whitespace-nowrap text-xs">
|
||||||
{hours.toFixed(1)} hours on {day.date}
|
{hours.toFixed(1)} hours on {day.date}
|
||||||
</div>
|
</div>
|
||||||
@@ -90,10 +120,10 @@ export const ActivityGrid = ({ data }: ActivityGridProps) => {
|
|||||||
const date = new Date(week[0].date);
|
const date = new Date(week[0].date);
|
||||||
const isFirstOfMonth = date.getDate() <= 7;
|
const isFirstOfMonth = date.getDate() <= 7;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="w-3 mx-1"
|
className="w-3 mx-1"
|
||||||
style={{ marginLeft: i === 0 ? "0" : undefined }}
|
style={{ marginLeft: i === 0 ? '0' : undefined }}
|
||||||
>
|
>
|
||||||
{isFirstOfMonth && months[date.getMonth()]}
|
{isFirstOfMonth && months[date.getMonth()]}
|
||||||
</div>
|
</div>
|
||||||
@@ -106,7 +136,10 @@ export const ActivityGrid = ({ data }: ActivityGridProps) => {
|
|||||||
<div className="flex items-center gap-2 mt-4 text-xs text-foreground/60">
|
<div className="flex items-center gap-2 mt-4 text-xs text-foreground/60">
|
||||||
<span>Less</span>
|
<span>Less</span>
|
||||||
{[0, 1, 2, 3, 4].map((intensity) => (
|
{[0, 1, 2, 3, 4].map((intensity) => (
|
||||||
<div 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>
|
<span>More</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,115 +1,98 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
const Stats = () => {
|
const Stats = () => {
|
||||||
const [stats, setStats] = useState<any>(null);
|
const [stats, setStats] = useState<any>(null);
|
||||||
const [error, setError] = useState(false);
|
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
|
const [isFinished, setIsFinished] = useState(false);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [skipAnim, setSkipAnim] = useState(false);
|
|
||||||
const hasAnimated = useRef(false);
|
|
||||||
const sectionRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Fetch data on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/wakatime/alltime")
|
setIsVisible(true);
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) throw new Error("API error");
|
const fetchStats = async () => {
|
||||||
return res.json();
|
try {
|
||||||
})
|
const res = await fetch("/api/wakatime/alltime");
|
||||||
.then((data) => setStats(data.data))
|
const data = await res.json();
|
||||||
.catch(() => setError(true));
|
setStats(data.data);
|
||||||
|
startCounting(data.data.total_seconds);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching stats:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchStats();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Observe visibility — skip animation if already in view on mount
|
const startCounting = (totalSeconds: number) => {
|
||||||
useEffect(() => {
|
|
||||||
const el = sectionRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const inView = rect.top < window.innerHeight && rect.bottom > 0;
|
|
||||||
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
|
|
||||||
|
|
||||||
if (inView && isReload) {
|
|
||||||
setSkipAnim(true);
|
|
||||||
setIsVisible(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (inView) {
|
|
||||||
requestAnimationFrame(() => setIsVisible(true));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setIsVisible(true);
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.3 }
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.observe(el);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Start counter when both visible and data is ready
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isVisible || !stats || hasAnimated.current) return;
|
|
||||||
hasAnimated.current = true;
|
|
||||||
|
|
||||||
const totalSeconds = stats.total_seconds;
|
|
||||||
const duration = 2000;
|
const duration = 2000;
|
||||||
const steps = 60;
|
const steps = 60;
|
||||||
let currentStep = 0;
|
let currentStep = 0;
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
currentStep += 1;
|
currentStep += 1;
|
||||||
|
|
||||||
if (currentStep >= steps) {
|
if (currentStep >= steps) {
|
||||||
setCount(totalSeconds);
|
setCount(totalSeconds);
|
||||||
|
setIsFinished(true);
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const progress = 1 - Math.pow(1 - currentStep / steps, 4);
|
const progress = 1 - Math.pow(1 - currentStep / steps, 4);
|
||||||
setCount(Math.floor(totalSeconds * progress));
|
setCount(Math.floor(totalSeconds * progress));
|
||||||
}, duration / steps);
|
}, duration / steps);
|
||||||
|
|
||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [isVisible, stats]);
|
};
|
||||||
|
|
||||||
if (error) return null;
|
if (!stats) return null;
|
||||||
if (!stats) return <div ref={sectionRef} className="min-h-[50vh]" />;
|
|
||||||
|
|
||||||
const hours = Math.floor(count / 3600);
|
const hours = Math.floor(count / 3600);
|
||||||
const formattedHours = hours.toLocaleString("en-US", {
|
const formattedHours = hours.toLocaleString("en-US", {
|
||||||
minimumIntegerDigits: 4,
|
minimumIntegerDigits: 4,
|
||||||
useGrouping: true,
|
useGrouping: true
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={sectionRef} className="flex flex-col items-center justify-center min-h-[50vh] gap-6">
|
<div 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" : ""}`}>
|
<div className={`
|
||||||
|
text-2xl opacity-0
|
||||||
|
${isVisible ? "animate-fade-in-first" : ""}
|
||||||
|
`}>
|
||||||
I've spent
|
I've spent
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="text-8xl text-center relative z-10">
|
<div className="text-8xl text-center relative z-10">
|
||||||
<span className="font-bold relative">
|
<span className="font-bold relative">
|
||||||
<span className={skipAnim ? "bg-gradient-text" : `bg-gradient-text opacity-0 ${isVisible ? "animate-fade-in-second" : ""}`}>
|
<span className={`
|
||||||
|
bg-gradient-text opacity-0
|
||||||
|
${isVisible ? "animate-fade-in-second" : ""}
|
||||||
|
`}>
|
||||||
{formattedHours}
|
{formattedHours}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className={skipAnim ? "text-4xl opacity-60 ml-4" : `text-4xl opacity-0 ${isVisible ? "animate-slide-in-hours" : ""}`}>
|
<span className={`
|
||||||
|
text-4xl opacity-0
|
||||||
|
${isVisible ? "animate-slide-in-hours" : ""}
|
||||||
|
`}>
|
||||||
hours
|
hours
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-3 text-center">
|
<div className="flex flex-col items-center gap-3 text-center">
|
||||||
<div className={skipAnim ? "text-xl opacity-80" : `text-xl opacity-0 ${isVisible ? "animate-fade-in-third" : ""}`}>
|
<div className={`
|
||||||
|
text-xl opacity-0
|
||||||
|
${isVisible ? "animate-fade-in-third" : ""}
|
||||||
|
`}>
|
||||||
writing code & building apps
|
writing code & building apps
|
||||||
</div>
|
</div>
|
||||||
<div className={skipAnim ? "flex items-center gap-3 text-lg opacity-60" : `flex items-center gap-3 text-lg opacity-0 ${isVisible ? "animate-fade-in-fourth" : ""}`}>
|
|
||||||
|
<div className={`
|
||||||
|
flex items-center gap-3 text-lg opacity-0
|
||||||
|
${isVisible ? "animate-fade-in-fourth" : ""}
|
||||||
|
`}>
|
||||||
<span>since</span>
|
<span>since</span>
|
||||||
<span className="text-green-bright font-bold">{stats.range.start_text}</span>
|
<span className="text-green-bright font-bold">{stats.range.start_text}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,7 +100,15 @@ const Stats = () => {
|
|||||||
|
|
||||||
<style jsx>{`
|
<style jsx>{`
|
||||||
.bg-gradient-text {
|
.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;
|
background-size: 200% auto;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
@@ -125,32 +116,95 @@ const Stats = () => {
|
|||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeInFirst {
|
.animate-gradient {
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
animation: gradient 4s linear infinite;
|
||||||
to { opacity: 0.8; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
@keyframes fadeInSecond {
|
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
@keyframes slideInHours {
|
|
||||||
from { opacity: 0; transform: translateX(20px); margin-left: 0; }
|
|
||||||
to { opacity: 0.6; transform: translateX(0); margin-left: 1rem; }
|
|
||||||
}
|
|
||||||
@keyframes fadeInThird {
|
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
|
||||||
to { opacity: 0.8; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
@keyframes fadeInFourth {
|
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
|
||||||
to { opacity: 0.6; transform: translateY(0); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-fade-in-first { animation: fadeInFirst 0.7s ease-out forwards; }
|
@keyframes gradient {
|
||||||
.animate-fade-in-second { animation: fadeInSecond 0.7s ease-out 0.4s forwards; }
|
0% { background-position: 0% 50%; }
|
||||||
.animate-slide-in-hours { animation: slideInHours 0.7s ease-out 0.6s forwards; }
|
100% { background-position: 200% 50%; }
|
||||||
.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; }
|
|
||||||
|
@keyframes fadeInFirst {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInSecond {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInThird {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInFourth {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
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;
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,66 +1,35 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Clock, CalendarClock, CodeXml, Computer } from "lucide-react";
|
import { Clock, CalendarClock, CodeXml, Computer } from "lucide-react";
|
||||||
|
|
||||||
import { ActivityGrid } from "@/components/about/stats-activity";
|
import { ActivityGrid } from "@/components/about/stats-activity";
|
||||||
|
|
||||||
const DetailedStats = () => {
|
const DetailedStats = () => {
|
||||||
const [stats, setStats] = useState<any>(null);
|
const [stats, setStats] = useState(null);
|
||||||
const [activity, setActivity] = useState<any>(null);
|
const [activity, setActivity] = useState(null);
|
||||||
const [error, setError] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const [skipAnim, setSkipAnim] = useState(false);
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/wakatime/detailed")
|
fetch("/api/wakatime/detailed")
|
||||||
.then((res) => {
|
.then(res => res.json())
|
||||||
if (!res.ok) throw new Error();
|
.then(data => {
|
||||||
return res.json();
|
setStats(data.data);
|
||||||
|
setIsVisible(true);
|
||||||
})
|
})
|
||||||
.then((data) => setStats(data.data))
|
.catch(error => {
|
||||||
.catch(() => setError(true));
|
console.error("Error fetching stats:", error);
|
||||||
|
});
|
||||||
|
|
||||||
fetch("/api/wakatime/activity")
|
fetch("/api/wakatime/activity")
|
||||||
.then((res) => {
|
.then(res => res.json())
|
||||||
if (!res.ok) throw new Error();
|
.then(data => {
|
||||||
return res.json();
|
setActivity(data.data);
|
||||||
})
|
})
|
||||||
.then((data) => setActivity(data.data))
|
.catch(error => {
|
||||||
.catch(() => {});
|
console.error("Error fetching activity:", error);
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
if (!stats) return null;
|
||||||
const el = containerRef.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const inView = rect.top < window.innerHeight && rect.bottom > 0;
|
|
||||||
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
|
|
||||||
|
|
||||||
if (inView && isReload) {
|
|
||||||
setSkipAnim(true);
|
|
||||||
setVisible(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (inView) {
|
|
||||||
requestAnimationFrame(() => setVisible(true));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setVisible(true);
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.1, rootMargin: "-15% 0px" }
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.observe(el);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, [stats]);
|
|
||||||
|
|
||||||
if (error) return null;
|
|
||||||
|
|
||||||
const progressColors = [
|
const progressColors = [
|
||||||
"bg-red-bright",
|
"bg-red-bright",
|
||||||
@@ -69,163 +38,138 @@ const DetailedStats = () => {
|
|||||||
"bg-green-bright",
|
"bg-green-bright",
|
||||||
"bg-blue-bright",
|
"bg-blue-bright",
|
||||||
"bg-purple-bright",
|
"bg-purple-bright",
|
||||||
"bg-aqua-bright",
|
"bg-aqua-bright"
|
||||||
];
|
];
|
||||||
|
|
||||||
const statCards = stats
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
title: "Total Time",
|
|
||||||
value: `${Math.round((stats.total_seconds / 3600) * 10) / 10}`,
|
|
||||||
unit: "hours",
|
|
||||||
subtitle: "this week",
|
|
||||||
color: "text-yellow-bright",
|
|
||||||
borderHover: "hover:border-yellow-bright/50",
|
|
||||||
icon: Clock,
|
|
||||||
iconColor: "stroke-yellow-bright",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Daily Average",
|
|
||||||
value: `${Math.round((stats.daily_average / 3600) * 10) / 10}`,
|
|
||||||
unit: "hours",
|
|
||||||
subtitle: "per day",
|
|
||||||
color: "text-orange-bright",
|
|
||||||
borderHover: "hover:border-orange-bright/50",
|
|
||||||
icon: CalendarClock,
|
|
||||||
iconColor: "stroke-orange-bright",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Primary Editor",
|
|
||||||
value: stats.editors?.[0]?.name || "None",
|
|
||||||
unit: `${Math.round(stats.editors?.[0]?.percent || 0)}%`,
|
|
||||||
subtitle: "of the time",
|
|
||||||
color: "text-blue-bright",
|
|
||||||
borderHover: "hover:border-blue-bright/50",
|
|
||||||
icon: CodeXml,
|
|
||||||
iconColor: "stroke-blue-bright",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Operating System",
|
|
||||||
value: stats.operating_systems?.[0]?.name || "None",
|
|
||||||
unit: `${Math.round(stats.operating_systems?.[0]?.percent || 0)}%`,
|
|
||||||
subtitle: "of the time",
|
|
||||||
color: "text-green-bright",
|
|
||||||
borderHover: "hover:border-green-bright/50",
|
|
||||||
icon: Computer,
|
|
||||||
iconColor: "stroke-green-bright",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const languages =
|
|
||||||
stats?.languages?.slice(0, 7).map((lang: any, index: number) => ({
|
|
||||||
name: lang.name,
|
|
||||||
percent: Math.round(lang.percent),
|
|
||||||
time: Math.round((lang.total_seconds / 3600) * 10) / 10 + " hrs",
|
|
||||||
color: progressColors[index % progressColors.length],
|
|
||||||
})) || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="flex flex-col gap-10 md:py-32 w-full max-w-[1200px] mx-auto px-4 min-h-[50vh]">
|
<div className="flex flex-col gap-10 md:py-32 w-full max-w-[1200px] mx-auto px-4">
|
||||||
{!stats ? null : (
|
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright">
|
||||||
<>
|
Weekly Statistics
|
||||||
{/* Header */}
|
</h2>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Stat Cards */}
|
{/* Top Stats Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className={`
|
||||||
{statCards.map((card, i) => {
|
grid grid-cols-1 md:grid-cols-2 gap-8
|
||||||
const Icon = card.icon;
|
transition-all duration-700 transform
|
||||||
return (
|
${isVisible ? 'translate-y-0 opacity-100' : 'translate-y-4 opacity-0'}
|
||||||
<div
|
`}>
|
||||||
key={card.title}
|
{/* Total Time */}
|
||||||
className={`bg-background border border-foreground/10 rounded-lg p-6 ${card.borderHover} ${skipAnim ? "" : "transition-all duration-500 ease-out"}`}
|
<StatsCard
|
||||||
style={skipAnim ? {} : {
|
title="Total Time"
|
||||||
transitionDelay: `${150 + i * 100}ms`,
|
value={`${Math.round(stats.total_seconds / 3600 * 10) / 10}`}
|
||||||
opacity: visible ? 1 : 0,
|
unit="hours"
|
||||||
transform: visible ? "translateY(0)" : "translateY(24px)",
|
subtitle="this week"
|
||||||
}}
|
color="text-yellow-bright"
|
||||||
>
|
icon={Clock}
|
||||||
<div className="flex gap-4 items-center">
|
iconColor="stroke-yellow-bright"
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Languages */}
|
{/* Daily Average */}
|
||||||
<div
|
<StatsCard
|
||||||
className={`bg-background border border-foreground/10 rounded-lg p-6 hover:border-purple-bright/50 ${skipAnim ? "" : "transition-all duration-700 ease-out"}`}
|
title="Daily Average"
|
||||||
style={skipAnim ? {} : {
|
value={`${Math.round(stats.daily_average / 3600 * 10) / 10}`}
|
||||||
transitionDelay: "550ms",
|
unit="hours"
|
||||||
opacity: visible ? 1 : 0,
|
subtitle="per day"
|
||||||
transform: visible ? "translateY(0)" : "translateY(24px)",
|
color="text-orange-bright"
|
||||||
}}
|
icon={CalendarClock}
|
||||||
>
|
iconColor="stroke-orange-bright"
|
||||||
<div className="text-purple-bright mb-6 text-lg">Languages</div>
|
/>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-5">
|
|
||||||
{languages.map((lang: any, i: number) => (
|
|
||||||
<div key={lang.name} className="flex flex-col gap-2">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<span className="text-sm font-medium">{lang.name}</span>
|
|
||||||
<span className="text-sm opacity-70 tabular-nums">{lang.time}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 items-center">
|
|
||||||
<div className="flex-grow h-2 bg-foreground/5 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className={`h-full ${lang.color} rounded-full`}
|
|
||||||
style={{
|
|
||||||
width: visible ? `${lang.percent}%` : "0%",
|
|
||||||
opacity: 0.85,
|
|
||||||
transition: skipAnim ? "none" : `width 1s ease-out ${700 + i * 80}ms`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-foreground/50 min-w-[36px] text-right tabular-nums">
|
|
||||||
{lang.percent}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Activity Grid */}
|
{/* Editors */}
|
||||||
{activity && (
|
<StatsCard
|
||||||
<div
|
title="Primary Editor"
|
||||||
className={skipAnim ? "" : "transition-all duration-700 ease-out"}
|
value={stats.editors?.[0]?.name || "None"}
|
||||||
style={skipAnim ? {} : {
|
unit={`${Math.round(stats.editors?.[0]?.percent || 0)}%`}
|
||||||
transitionDelay: "750ms",
|
subtitle="of the time"
|
||||||
opacity: visible ? 1 : 0,
|
color="text-blue-bright"
|
||||||
transform: visible ? "translateY(0)" : "translateY(24px)",
|
icon={CodeXml}
|
||||||
}}
|
iconColor="stroke-blue-bright"
|
||||||
>
|
/>
|
||||||
<ActivityGrid data={activity} />
|
|
||||||
</div>
|
{/* 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>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StatsCard = ({ title, value, unit, subtitle, color, icon: Icon, iconColor }) => (
|
||||||
|
<div className="bg-background border border-foreground/10 rounded-lg p-6 hover:border-yellow transition-colors flex items-center justify-center">
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
<Icon className={`w-6 h-6 ${iconColor}`} strokeWidth={1.5} />
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className={`${color} text-lg mb-1`}>{title}</div>
|
||||||
|
<div className="flex items-baseline gap-2">
|
||||||
|
<div className="text-2xl font-bold">{value}</div>
|
||||||
|
<div className="text-lg opacity-80">{unit}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm opacity-60 mt-1">{subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DetailCard = ({ title, items, titleColor }) => (
|
||||||
|
<div className="bg-background border border-foreground/10 rounded-lg p-6 hover:border-yellow transition-colors">
|
||||||
|
<div className={`${titleColor} mb-6 text-lg`}>{title}</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-6">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div key={item.name} className="flex flex-col gap-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-base font-medium">{item.name}</span>
|
||||||
|
<span className="text-base opacity-80">{item.value}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
<div className="flex-grow h-2 bg-foreground/5 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full ${item.color} rounded-full transition-all duration-1000`}
|
||||||
|
style={{
|
||||||
|
width: item.value,
|
||||||
|
opacity: '0.8'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-foreground/60 min-w-[70px] text-right">{item.time}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export default DetailedStats;
|
export default DetailedStats;
|
||||||
|
|||||||
@@ -1,181 +1,78 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React from "react";
|
||||||
import { Check, Code, GitBranch, Star, Rocket } from "lucide-react";
|
import { Check, Code, GitBranch, Star } from "lucide-react";
|
||||||
|
|
||||||
const timelineItems = [
|
|
||||||
{
|
|
||||||
year: "2026",
|
|
||||||
title: "Present",
|
|
||||||
description: "Building domain-specific languages, diving deep into the Salesforce ecosystem, and writing production Java and Python daily. The craft keeps evolving.",
|
|
||||||
technologies: ["Java", "Python", "Salesforce", "DSLs"],
|
|
||||||
icon: <Rocket className="text-red-bright" size={20} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: "2024",
|
|
||||||
title: "Shipping & Scaling",
|
|
||||||
description: "The wisdom of past ventures now flows through my work, whether crafting elegant CRUD applications or embarking on bold projects that expand my limits.",
|
|
||||||
technologies: ["Rust", "Typescript", "Go", "Postgres"],
|
|
||||||
icon: <Code className="text-yellow-bright" size={20} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: "2022",
|
|
||||||
title: "Diving Deeper",
|
|
||||||
description: "The worlds of systems programming and scalable infrastructure collided as I explored low-level C++ graphics programming and containerization with Docker.",
|
|
||||||
technologies: ["C++", "Cmake", "Docker", "Docker Compose"],
|
|
||||||
icon: <GitBranch className="text-green-bright" size={20} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: "2020",
|
|
||||||
title: "Exploring the Stack",
|
|
||||||
description: "Starting with pure HTML and CSS, I explored the foundations of web development, gradually venturing into JavaScript and React to bring my static pages to life.",
|
|
||||||
technologies: ["Javascript", "Tailwind", "React", "Express"],
|
|
||||||
icon: <Star className="text-blue-bright" size={20} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
year: "2018",
|
|
||||||
title: "Starting the Journey",
|
|
||||||
description: "An elective Python class in 8th grade transformed my keen interest in programming into a relentless obsession, one that drove me to constantly explore new depths.",
|
|
||||||
technologies: ["Python", "Discord.py", "Asyncio", "Sqlite"],
|
|
||||||
icon: <Check className="text-purple-bright" size={20} />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function TimelineCard({ item, index }: { item: (typeof timelineItems)[number]; index: number }) {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const [skip, setSkip] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const inView = rect.top < window.innerHeight && rect.bottom > 0;
|
|
||||||
const isReload = performance.getEntriesByType?.("navigation")?.[0]?.type === "reload";
|
|
||||||
|
|
||||||
if (inView && isReload) {
|
|
||||||
setSkip(true);
|
|
||||||
setVisible(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (inView) {
|
|
||||||
requestAnimationFrame(() => setVisible(true));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setVisible(true);
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold: 0.2 }
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.observe(el);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isLeft = index % 2 === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="relative mb-8 md:mb-12 last:mb-0">
|
|
||||||
<div className={`flex flex-col sm:flex-row items-start ${isLeft ? "sm:flex-row-reverse" : ""}`}>
|
|
||||||
{/* Node */}
|
|
||||||
<div
|
|
||||||
className={`
|
|
||||||
absolute -left-8 sm:left-1/2 w-6 h-6 sm:w-8 sm:h-8 bg-background
|
|
||||||
rounded-full border-2 border-yellow-bright sm:-translate-x-1/2
|
|
||||||
flex items-center justify-center z-10
|
|
||||||
${skip ? "" : "transition-all duration-500"}
|
|
||||||
${visible ? "scale-100 opacity-100" : "scale-0 opacity-0"}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{item.icon}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Card */}
|
|
||||||
<div
|
|
||||||
className={`
|
|
||||||
w-full sm:w-[calc(50%-32px)]
|
|
||||||
${isLeft ? "sm:pr-8 md:pr-12" : "sm:pl-8 md:pl-12"}
|
|
||||||
${skip ? "" : "transition-all duration-700 ease-out"}
|
|
||||||
${visible
|
|
||||||
? "opacity-100 translate-x-0"
|
|
||||||
: `opacity-0 ${isLeft ? "sm:translate-x-8" : "sm:-translate-x-8"} translate-y-4 sm:translate-y-0`
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="p-4 sm:p-6 bg-background/50 rounded-lg border border-foreground/10
|
|
||||||
hover:border-yellow-bright/50 transition-colors duration-300"
|
|
||||||
>
|
|
||||||
<span className="text-xs sm:text-sm font-mono text-yellow-bright">{item.year}</span>
|
|
||||||
<h3 className="text-lg sm:text-xl font-bold text-foreground/90 mt-2">{item.title}</h3>
|
|
||||||
<p className="text-sm sm:text-base text-foreground/70 mt-2">{item.description}</p>
|
|
||||||
<div className="flex flex-wrap gap-2 mt-3">
|
|
||||||
{item.technologies.map((tech) => (
|
|
||||||
<span
|
|
||||||
key={tech}
|
|
||||||
className="px-2 py-1 text-xs sm:text-sm rounded-full bg-foreground/5
|
|
||||||
text-foreground/60 hover:text-yellow-bright transition-colors duration-300"
|
|
||||||
>
|
|
||||||
{tech}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Timeline() {
|
export default function Timeline() {
|
||||||
const lineRef = useRef<HTMLDivElement>(null);
|
const timelineItems = [
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
{
|
||||||
const [lineHeight, setLineHeight] = useState(0);
|
year: "2024",
|
||||||
|
title: "Present",
|
||||||
useEffect(() => {
|
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.",
|
||||||
const container = containerRef.current;
|
technologies: ["Rust", "Typescript", "Go", "Postgres"],
|
||||||
if (!container) return;
|
icon: <Code className="text-yellow-bright" size={20} />
|
||||||
|
},
|
||||||
const observer = new IntersectionObserver(
|
{
|
||||||
([entry]) => {
|
year: "2022",
|
||||||
if (entry.isIntersecting) {
|
title: "Diving Deeper",
|
||||||
// Animate line to full height over time
|
description: "The worlds of systems programming and scalable infrastructure collided as I explored low-level C++ graphics programming and containerization with Docker.",
|
||||||
const el = lineRef.current;
|
technologies: ["C++", "Cmake", "Docker", "Docker Compose"],
|
||||||
if (el) {
|
icon: <GitBranch className="text-green-bright" size={20} />
|
||||||
setLineHeight(100);
|
},
|
||||||
}
|
{
|
||||||
observer.disconnect();
|
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.",
|
||||||
{ threshold: 0.1 }
|
technologies: ["Javascript", "Tailwind", "React", "Express"],
|
||||||
);
|
icon: <Star className="text-blue-bright" size={20} />
|
||||||
|
},
|
||||||
observer.observe(container);
|
{
|
||||||
return () => observer.disconnect();
|
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} />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl px-4 py-8 relative z-0">
|
<div className="w-full max-w-6xl px-4 py-8 relative z-0">
|
||||||
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8 md:mb-12">
|
<h2 className="text-2xl md:text-4xl font-bold text-center text-yellow-bright mb-8 md:mb-12">
|
||||||
My Journey Through Code
|
Journey Through Code
|
||||||
</h2>
|
</h2>
|
||||||
<div ref={containerRef} className="relative">
|
<div className="relative">
|
||||||
{/* Animated vertical line */}
|
<div className="absolute left-4 sm:left-1/2 h-full w-0.5 bg-foreground/10 -translate-x-1/2" />
|
||||||
<div className="absolute left-4 sm:left-1/2 h-full w-0.5 -translate-x-1/2">
|
|
||||||
<div
|
|
||||||
ref={lineRef}
|
|
||||||
className="w-full bg-foreground/10 transition-all duration-[1500ms] ease-out origin-top"
|
|
||||||
style={{ height: `${lineHeight}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ml-8 sm:ml-0">
|
<div className="ml-8 sm:ml-0">
|
||||||
{timelineItems.map((item, index) => (
|
{timelineItems.map((item, index) => (
|
||||||
<TimelineCard key={item.year} item={item} index={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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -383,20 +383,13 @@ const Background: React.FC<BackgroundProps> = ({
|
|||||||
|
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
if (!gridRef.current || !canvasRef.current) return;
|
if (!gridRef.current || !canvasRef.current) return;
|
||||||
|
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const mouseX = e.clientX - rect.left;
|
const mouseX = e.clientX - rect.left;
|
||||||
const mouseY = e.clientY - rect.top;
|
const mouseY = e.clientY - rect.top;
|
||||||
|
|
||||||
// Ignore clicks outside the canvas bounds
|
|
||||||
if (mouseX < 0 || mouseX > rect.width || mouseY < 0 || mouseY > rect.height) return;
|
|
||||||
|
|
||||||
// Prevent text selection when interacting with the canvas
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const cellSize = getCellSize();
|
const cellSize = getCellSize();
|
||||||
|
|
||||||
mouseRef.current.isDown = true;
|
mouseRef.current.isDown = true;
|
||||||
mouseRef.current.lastClickTime = Date.now();
|
mouseRef.current.lastClickTime = Date.now();
|
||||||
|
|
||||||
@@ -530,10 +523,11 @@ const Background: React.FC<BackgroundProps> = ({
|
|||||||
gridRef.current = initGrid(displayWidth, displayHeight);
|
gridRef.current = initGrid(displayWidth, displayHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind to window so mouse events work even when content overlays the canvas
|
// Add mouse event listeners
|
||||||
window.addEventListener('mousedown', handleMouseDown, { signal });
|
canvas.addEventListener('mousedown', handleMouseDown, { signal });
|
||||||
window.addEventListener('mousemove', handleMouseMove, { signal });
|
canvas.addEventListener('mousemove', handleMouseMove, { signal });
|
||||||
window.addEventListener('mouseup', handleMouseUp, { signal });
|
canvas.addEventListener('mouseup', handleMouseUp, { signal });
|
||||||
|
canvas.addEventListener('mouseleave', handleMouseLeave, { signal });
|
||||||
|
|
||||||
const handleVisibilityChange = () => {
|
const handleVisibilityChange = () => {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
|
|||||||
@@ -1,43 +1,38 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
|
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
|
||||||
import { AnimateIn } from "@/components/animate-in";
|
|
||||||
|
|
||||||
export const BlogHeader = () => {
|
export const BlogHeader = () => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl mx-auto px-4 pt-24 sm:pt-24">
|
<div className="w-full max-w-6xl mx-auto px-4 pt-24 sm:pt-24">
|
||||||
<AnimateIn>
|
<h1 className="text-2xl sm:text-3xl font-bold text-purple mb-3 text-center px-4 leading-relaxed">
|
||||||
<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" />
|
||||||
Latest Thoughts <br className="sm:hidden" />
|
& Writings
|
||||||
& Writings
|
</h1>
|
||||||
</h1>
|
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
|
||||||
</AnimateIn>
|
<a
|
||||||
<AnimateIn delay={100}>
|
href="/rss"
|
||||||
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
|
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-orange hover:text-orange-bright hover:border-orange/50 transition-colors duration-200"
|
||||||
<a
|
>
|
||||||
href="/rss"
|
<RssIcon className="w-4 h-4" />
|
||||||
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"
|
<span>RSS Feed</span>
|
||||||
>
|
</a>
|
||||||
<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"
|
||||||
<a
|
>
|
||||||
href="/blog/tags"
|
<TagIcon className="w-4 h-4" />
|
||||||
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"
|
<span>Browse Tags</span>
|
||||||
>
|
</a>
|
||||||
<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"
|
||||||
<a
|
>
|
||||||
href="/blog/popular"
|
<TrendingUpIcon className="w-4 h-4" />
|
||||||
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"
|
<span>Most Popular</span>
|
||||||
>
|
</a>
|
||||||
<TrendingUpIcon className="w-4 h-4" />
|
</div>
|
||||||
<span>Most Popular</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</AnimateIn>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AnimateIn } from "@/components/animate-in";
|
|
||||||
|
|
||||||
type BlogPost = {
|
type BlogPost = {
|
||||||
id: string;
|
slug: string;
|
||||||
data: {
|
data: {
|
||||||
title: string;
|
title: string;
|
||||||
author: string;
|
author: string;
|
||||||
@@ -28,73 +27,71 @@ const formatDate = (dateString: string) => {
|
|||||||
|
|
||||||
export const BlogPostList = ({ posts }: BlogPostListProps) => {
|
export const BlogPostList = ({ posts }: BlogPostListProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl mx-auto">
|
<div className="w-full max-w-6xl mx-auto">
|
||||||
<ul className="space-y-6 md:space-y-10">
|
<ul className="space-y-6 md:space-y-10">
|
||||||
{posts.map((post, i) => (
|
{posts.map((post) => (
|
||||||
<AnimateIn key={post.id} delay={i * 80}>
|
<li key={post.slug} className="group px-4 md:px-0">
|
||||||
<li className="group px-4 md:px-0">
|
<a
|
||||||
<a
|
href={`/blog/${post.slug}`}
|
||||||
href={`/blog/${post.id}`}
|
className="block"
|
||||||
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">
|
||||||
<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 */}
|
||||||
{/* Image container with fixed aspect ratio */}
|
<div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-background">
|
||||||
<div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-background flex-shrink-0">
|
<img
|
||||||
<img
|
src={post.data.image || "/blog/placeholder.png"}
|
||||||
src={post.data.image || "/blog/placeholder.png"}
|
alt={post.data.title}
|
||||||
alt={post.data.title}
|
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
style={{ objectPosition: post.data.imagePosition || "center center" }}
|
||||||
style={{ objectPosition: post.data.imagePosition || "center center" }}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Content container */}
|
||||||
{/* Content container */}
|
<div className="w-full md:w-2/3 flex flex-col gap-2 md:gap-4 py-1 md:py-2">
|
||||||
<div className="w-full md:w-2/3 flex flex-col gap-2 md:gap-3">
|
{/* Title and meta info */}
|
||||||
{/* Title and meta info */}
|
<div className="space-y-1.5 md:space-y-3">
|
||||||
<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">
|
||||||
<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}
|
||||||
{post.data.title}
|
</h2>
|
||||||
</h2>
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
|
||||||
<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-orange">{post.data.author}</span>
|
<span className="text-foreground/50">•</span>
|
||||||
<span className="text-foreground/50">•</span>
|
<time dateTime={post.data.date} className="text-blue">
|
||||||
<time dateTime={post.data.date} className="text-blue">
|
{formatDate(post.data.date)}
|
||||||
{formatDate(post.data.date)}
|
</time>
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="text-foreground/90 text-sm md:text-lg leading-relaxed line-clamp-2 mt-0.5 md:mt-0">
|
|
||||||
{post.data.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Tags */}
|
|
||||||
<div className="flex flex-wrap gap-1.5 md:gap-3 mt-1 md:mt-2">
|
|
||||||
{post.data.tags.slice(0, 3).map((tag) => (
|
|
||||||
<span
|
|
||||||
key={tag}
|
|
||||||
className="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
window.location.href = `/blog/tag/${tag}`;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
#{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{post.data.tags.length > 3 && (
|
|
||||||
<span className="text-xs md:text-base text-foreground/60">
|
|
||||||
+{post.data.tags.length - 3}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
|
||||||
</a>
|
{/* Description */}
|
||||||
</li>
|
<p className="text-foreground/90 text-sm md:text-lg leading-relaxed line-clamp-2 mt-0.5 md:mt-0">
|
||||||
</AnimateIn>
|
{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>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
78
src/src/components/projects/project-card.tsx
Normal file
78
src/src/components/projects/project-card.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,100 +1,49 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { CollectionEntry } from "astro:content";
|
import type { CollectionEntry } from "astro:content";
|
||||||
import { AnimateIn } from "@/components/animate-in";
|
import { ProjectCard } from "@/components/projects/project-card";
|
||||||
|
|
||||||
interface ProjectListProps {
|
interface ProjectListProps {
|
||||||
projects: CollectionEntry<"projects">[];
|
projects: CollectionEntry<"projects">[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProjectList({ projects }: ProjectListProps) {
|
export function ProjectList({ projects }: ProjectListProps) {
|
||||||
|
const latestProjects = projects.slice(0, 3);
|
||||||
|
const otherProjects = projects.slice(3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-6xl mx-auto pt-24 sm:pt-32 px-4">
|
<div className="w-full max-w-6xl mx-auto pt-24 sm:pt-32">
|
||||||
<AnimateIn>
|
<h1 className="text-2xl sm:text-3xl font-bold text-blue mb-12 text-center px-4 leading-relaxed">
|
||||||
<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" />
|
||||||
Here's what I've been <br className="sm:hidden" />
|
building lately
|
||||||
building lately
|
</h1>
|
||||||
</h1>
|
|
||||||
</AnimateIn>
|
<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>
|
||||||
|
|
||||||
<ul className="space-y-6 md:space-y-10">
|
{otherProjects.length > 0 && (
|
||||||
{projects.map((project, i) => (
|
<div className="px-4 pb-8">
|
||||||
<AnimateIn key={project.id} delay={i * 80}>
|
<h2 className="text-xl font-bold text-foreground/90 mb-6">
|
||||||
<li className="group">
|
All Projects
|
||||||
<a href={`/projects/${project.id}`} className="block">
|
</h2>
|
||||||
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr justify-items-center">
|
||||||
{/* Image */}
|
{otherProjects.map(project => (
|
||||||
<div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-foreground/5 flex-shrink-0">
|
<div key={project.slug} className="w-full max-w-md">
|
||||||
{project.data.image ? (
|
<ProjectCard project={project} />
|
||||||
<img
|
</div>
|
||||||
src={project.data.image}
|
))}
|
||||||
alt={`${project.data.title} preview`}
|
</div>
|
||||||
className="w-full h-full object-cover object-center group-hover:scale-105 transition-transform duration-300"
|
</div>
|
||||||
/>
|
)}
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex items-center justify-center text-foreground/30">
|
|
||||||
<span className="text-sm">No preview available</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="w-full md:w-2/3 flex flex-col gap-2 md:gap-3">
|
|
||||||
<h2 className="text-lg md:text-2xl font-semibold text-yellow group-hover:text-blue transition-colors duration-200 line-clamp-2">
|
|
||||||
{project.data.title}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p className="text-foreground/90 text-sm md:text-lg leading-relaxed line-clamp-2">
|
|
||||||
{project.data.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Tech stack */}
|
|
||||||
<div className="flex flex-wrap gap-1.5 md:gap-2 mt-1">
|
|
||||||
{project.data.techStack.map((tech) => (
|
|
||||||
<span
|
|
||||||
key={tech}
|
|
||||||
className="text-xs md:text-sm px-2 py-0.5 rounded-full bg-purple-bright/10 text-purple-bright"
|
|
||||||
>
|
|
||||||
{tech}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Links */}
|
|
||||||
{(project.data.githubUrl || project.data.demoUrl) && (
|
|
||||||
<div className="flex gap-4 mt-1">
|
|
||||||
{project.data.githubUrl && (
|
|
||||||
<span
|
|
||||||
className="text-sm text-foreground/50 hover:text-blue-bright transition-colors"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
window.open(project.data.githubUrl, "_blank");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Source
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{project.data.demoUrl && (
|
|
||||||
<span
|
|
||||||
className="text-sm text-foreground/50 hover:text-green-bright transition-colors"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
window.open(project.data.demoUrl, "_blank");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Live
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</AnimateIn>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,149 +1,11 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
FileDown,
|
FileDown,
|
||||||
Github,
|
Github,
|
||||||
Linkedin,
|
Linkedin,
|
||||||
Globe
|
Globe
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
// --- Typewriter hook ---
|
|
||||||
|
|
||||||
function useTypewriter(text: string, trigger: boolean, speed = 12) {
|
|
||||||
const [displayed, setDisplayed] = useState("");
|
|
||||||
const [done, setDone] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!trigger) return;
|
|
||||||
|
|
||||||
let i = 0;
|
|
||||||
setDisplayed("");
|
|
||||||
setDone(false);
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
i++;
|
|
||||||
setDisplayed(text.slice(0, i));
|
|
||||||
if (i >= text.length) {
|
|
||||||
setDone(true);
|
|
||||||
clearInterval(interval);
|
|
||||||
}
|
|
||||||
}, speed);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, [trigger, text, speed]);
|
|
||||||
|
|
||||||
return { displayed, done };
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Visibility hook ---
|
|
||||||
|
|
||||||
function useScrollVisible(threshold = 0.1) {
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const el = ref.current;
|
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
if (rect.top < window.innerHeight && rect.bottom > 0) {
|
|
||||||
requestAnimationFrame(() => setVisible(true));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
setVisible(true);
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ threshold }
|
|
||||||
);
|
|
||||||
|
|
||||||
observer.observe(el);
|
|
||||||
return () => observer.disconnect();
|
|
||||||
}, [threshold]);
|
|
||||||
|
|
||||||
return { ref, visible };
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Section fade-in ---
|
|
||||||
|
|
||||||
function Section({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) {
|
|
||||||
const { ref, visible } = useScrollVisible();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className="transition-all duration-700 ease-out"
|
|
||||||
style={{
|
|
||||||
transitionDelay: `${delay}ms`,
|
|
||||||
opacity: visible ? 1 : 0,
|
|
||||||
transform: visible ? "translateY(0)" : "translateY(24px)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Typed heading + fade-in body ---
|
|
||||||
|
|
||||||
function TypedSection({
|
|
||||||
heading,
|
|
||||||
headingClass = "text-3xl font-bold text-yellow-bright",
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
heading: string;
|
|
||||||
headingClass?: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
const { ref, visible } = useScrollVisible();
|
|
||||||
const { displayed, done } = useTypewriter(heading, visible, 20);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="space-y-4">
|
|
||||||
<h3 className={headingClass} style={{ minHeight: "1.2em" }}>
|
|
||||||
{visible ? displayed : "\u00A0"}
|
|
||||||
{visible && !done && <span className="animate-pulse text-foreground/40">|</span>}
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
className="transition-all duration-500 ease-out"
|
|
||||||
style={{
|
|
||||||
opacity: done ? 1 : 0,
|
|
||||||
transform: done ? "translateY(0)" : "translateY(12px)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Staggered skill tags ---
|
|
||||||
|
|
||||||
function SkillTags({ skills, trigger }: { skills: string[]; trigger: boolean }) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
{skills.map((skill, i) => (
|
|
||||||
<span
|
|
||||||
key={i}
|
|
||||||
className="border border-foreground/20 px-4 py-2 rounded-lg hover:border-foreground/40 transition-all duration-500 ease-out"
|
|
||||||
style={{
|
|
||||||
transitionDelay: `${i * 60}ms`,
|
|
||||||
opacity: trigger ? 1 : 0,
|
|
||||||
transform: trigger ? "translateY(0) scale(1)" : "translateY(12px) scale(0.95)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{skill}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Data ---
|
|
||||||
|
|
||||||
const resumeData = {
|
const resumeData = {
|
||||||
name: "Timothy Pidashev",
|
name: "Timothy Pidashev",
|
||||||
title: "Software Engineer",
|
title: "Software Engineer",
|
||||||
@@ -183,8 +45,8 @@ const resumeData = {
|
|||||||
achievements: [
|
achievements: [
|
||||||
"Designed and built the entire application from the ground up, including auth",
|
"Designed and built the entire application from the ground up, including auth",
|
||||||
"Engineered a tagging system to optimize search results by keywords and relativity",
|
"Engineered a tagging system to optimize search results by keywords and relativity",
|
||||||
"Implemented a filter provider to further narrow down search results and enhance the user experience",
|
"Implemented a filter provider to further narrow down search results and enchance the user experience",
|
||||||
"Created a smooth and responsive infinitely scrollable listings page",
|
"Created a smooth and responsive infinitely scrollable listings page",
|
||||||
"Automated deployment & testing processes reducing downtime by 60%"
|
"Automated deployment & testing processes reducing downtime by 60%"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -195,17 +57,22 @@ const resumeData = {
|
|||||||
school: "Clark College",
|
school: "Clark College",
|
||||||
location: "Vancouver, WA",
|
location: "Vancouver, WA",
|
||||||
period: "Graduating 2026",
|
period: "Graduating 2026",
|
||||||
achievements: [] as string[]
|
achievements: []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
skills: {
|
skills: {
|
||||||
technical: ["JavaScript", "TypeScript", "React", "Node.js", "Python", "SQL", "AWS", "Docker", "C", "Go", "Rust"],
|
technical: ["JavaScript", "TypeScript", "React", "Node.js", "Python", "SQL", "AWS", "Docker", "C", "Go", "Rust"],
|
||||||
soft: ["Leadership", "Problem Solving", "Communication", "Agile Methodologies"]
|
soft: ["Leadership", "Problem Solving", "Communication", "Agile Methodologies"]
|
||||||
},
|
},
|
||||||
|
certifications: [
|
||||||
|
{
|
||||||
|
name: "AWS Certified Solutions Architect",
|
||||||
|
issuer: "Amazon Web Services",
|
||||||
|
date: "2022"
|
||||||
|
}
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Component ---
|
|
||||||
|
|
||||||
const Resume = () => {
|
const Resume = () => {
|
||||||
const handleDownloadPDF = () => {
|
const handleDownloadPDF = () => {
|
||||||
window.open("/timothy-pidashev-resume.pdf", "_blank");
|
window.open("/timothy-pidashev-resume.pdf", "_blank");
|
||||||
@@ -216,198 +83,188 @@ const Resume = () => {
|
|||||||
<div className="space-y-16">
|
<div className="space-y-16">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="text-center space-y-6">
|
<header className="text-center space-y-6">
|
||||||
<Section>
|
<h1 className="text-6xl font-bold text-aqua-bright">{resumeData.name}</h1>
|
||||||
<h1 className="text-6xl font-bold text-aqua-bright">{resumeData.name}</h1>
|
<h2 className="text-3xl text-foreground/80">{resumeData.title}</h2>
|
||||||
</Section>
|
<div className="flex flex-col md:flex-row justify-center gap-4 md:gap-6 text-foreground/60 text-lg">
|
||||||
<Section delay={150}>
|
<a href={`mailto:${resumeData.contact.email}`} className="hover:text-foreground transition-colors duration-200">
|
||||||
<h2 className="text-3xl text-foreground/80">{resumeData.title}</h2>
|
{resumeData.contact.email}
|
||||||
</Section>
|
</a>
|
||||||
<Section delay={300}>
|
<span className="hidden md:inline">•</span>
|
||||||
<div className="flex flex-col md:flex-row justify-center gap-4 md:gap-6 text-foreground/60 text-lg">
|
<a href={`tel:${resumeData.contact.phone}`} className="hover:text-foreground transition-colors duration-200">
|
||||||
<a href={`mailto:${resumeData.contact.email}`} className="hover:text-foreground transition-colors duration-200">
|
{resumeData.contact.phone}
|
||||||
{resumeData.contact.email}
|
</a>
|
||||||
</a>
|
<span className="hidden md:inline">•</span>
|
||||||
<span className="hidden md:inline">•</span>
|
<span>{resumeData.contact.location}</span>
|
||||||
<a href={`tel:${resumeData.contact.phone}`} className="hover:text-foreground transition-colors duration-200">
|
</div>
|
||||||
{resumeData.contact.phone}
|
<div className="flex justify-center items-center gap-6 text-lg">
|
||||||
</a>
|
<a href={`https://${resumeData.contact.github}`}
|
||||||
<span className="hidden md:inline">•</span>
|
target="_blank"
|
||||||
<span>{resumeData.contact.location}</span>
|
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
|
||||||
</div>
|
>
|
||||||
</Section>
|
<Github size={18} />
|
||||||
<Section delay={450}>
|
GitHub
|
||||||
<div className="flex justify-center items-center gap-6 text-lg">
|
</a>
|
||||||
<a href={`https://${resumeData.contact.github}`}
|
|
||||||
target="_blank"
|
<a href={`https://${resumeData.contact.linkedin}`}
|
||||||
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
|
target="_blank"
|
||||||
>
|
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
|
||||||
<Github size={18} />
|
>
|
||||||
GitHub
|
<Linkedin size={18} />
|
||||||
</a>
|
LinkedIn
|
||||||
<a href={`https://${resumeData.contact.linkedin}`}
|
</a>
|
||||||
target="_blank"
|
<button
|
||||||
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
|
onClick={handleDownloadPDF}
|
||||||
>
|
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
|
||||||
<Linkedin size={18} />
|
>
|
||||||
LinkedIn
|
<FileDown size={18} />
|
||||||
</a>
|
Resume
|
||||||
<button
|
</button>
|
||||||
onClick={handleDownloadPDF}
|
</div>
|
||||||
className="text-blue-bright hover:text-blue transition-colors duration-200 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<FileDown size={18} />
|
|
||||||
Resume
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
<TypedSection heading="Professional Summary">
|
<section className="space-y-4">
|
||||||
|
<h3 className="text-3xl font-bold text-yellow-bright">Professional Summary</h3>
|
||||||
<p className="text-xl leading-relaxed">{resumeData.summary}</p>
|
<p className="text-xl leading-relaxed">{resumeData.summary}</p>
|
||||||
</TypedSection>
|
</section>
|
||||||
|
|
||||||
{/* Experience */}
|
{/* Experience */}
|
||||||
<TypedSection heading="Experience">
|
<section className="space-y-8">
|
||||||
<div className="space-y-8">
|
<h3 className="text-3xl font-bold text-yellow-bright">Experience</h3>
|
||||||
{resumeData.experience.map((exp, index) => (
|
{resumeData.experience.map((exp, index) => (
|
||||||
<Section key={index} delay={index * 100}>
|
<div key={index} className="space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
|
||||||
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
|
<div>
|
||||||
<div>
|
<h4 className="text-2xl font-semibold text-green-bright">{exp.title}</h4>
|
||||||
<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 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>
|
||||||
</Section>
|
<div className="text-foreground/60 text-lg font-medium">{exp.period}</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
<ul className="list-disc pl-6 space-y-3">
|
||||||
</TypedSection>
|
{exp.achievements.map((achievement, i) => (
|
||||||
|
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Contract Work */}
|
{/* Contract Work */}
|
||||||
<TypedSection heading="Contract Work">
|
<section className="space-y-8">
|
||||||
<div className="space-y-8">
|
<h3 className="text-3xl font-bold text-yellow-bright">Contract Work</h3>
|
||||||
{resumeData.contractWork.map((project, index) => (
|
{resumeData.contractWork.map((project, index) => (
|
||||||
<Section key={index} delay={index * 100}>
|
<div key={index} className="space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
|
||||||
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-2">
|
<div>
|
||||||
<div>
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<h4 className="text-2xl font-semibold text-green-bright">{project.title}</h4>
|
||||||
<h4 className="text-2xl font-semibold text-green-bright">{project.title}</h4>
|
{project.url && (
|
||||||
{project.url && (
|
<a
|
||||||
<a
|
href={project.url}
|
||||||
href={project.url}
|
target="_blank"
|
||||||
target="_blank"
|
rel="noopener noreferrer"
|
||||||
rel="noopener noreferrer"
|
className="text-blue-bright hover:text-blue transition-colors duration-200 opacity-60 hover:opacity-100"
|
||||||
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} />
|
<Globe size={16} strokeWidth={1.5} />
|
||||||
</a>
|
</a>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-foreground/60 text-lg">{project.type}</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-foreground/60 text-lg font-medium">Since {project.startDate}</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{project.responsibilities && (
|
|
||||||
<div>
|
|
||||||
<h5 className="text-lg text-aqua font-semibold mb-2">Responsibilities</h5>
|
|
||||||
<ul className="list-disc pl-6 space-y-3">
|
|
||||||
{project.responsibilities.map((r, i) => (
|
|
||||||
<li key={i} className="text-lg leading-relaxed">{r}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{project.achievements && (
|
|
||||||
<div>
|
|
||||||
<h5 className="text-lg text-aqua font-semibold mb-2">Key Achievements</h5>
|
|
||||||
<ul className="list-disc pl-6 space-y-3">
|
|
||||||
{project.achievements.map((a, i) => (
|
|
||||||
<li key={i} className="text-lg leading-relaxed">{a}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-foreground/60 text-lg">{project.type}</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
<div className="text-foreground/60 text-lg font-medium">Since {project.startDate}</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
<div className="space-y-4">
|
||||||
</TypedSection>
|
{project.responsibilities && (
|
||||||
|
<div>
|
||||||
{/* Education */}
|
<h5 className="text-lg text-aqua font-semibold mb-2">Responsibilities</h5>
|
||||||
<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">
|
<ul className="list-disc pl-6 space-y-3">
|
||||||
{edu.achievements.map((a, i) => (
|
{project.responsibilities.map((responsibility, i) => (
|
||||||
<li key={i} className="text-lg leading-relaxed">{a}</li>
|
<li key={i} className="text-lg leading-relaxed">{responsibility}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
{project.achievements && (
|
||||||
|
<div>
|
||||||
|
<h5 className="text-lg text-aqua font-semibold mb-2">Key Achievements</h5>
|
||||||
|
<ul className="list-disc pl-6 space-y-3">
|
||||||
|
{project.achievements.map((achievement, i) => (
|
||||||
|
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Education */}
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
<div className="text-foreground/60 text-lg font-medium">{edu.period}</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
<ul className="list-disc pl-6 space-y-3">
|
||||||
</TypedSection>
|
{edu.achievements.map((achievement, i) => (
|
||||||
|
<li key={i} className="text-lg leading-relaxed">{achievement}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Skills */}
|
{/* Skills */}
|
||||||
<SkillsSection />
|
<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>
|
||||||
|
*/}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Skills section ---
|
|
||||||
|
|
||||||
function SkillsSection() {
|
|
||||||
const { ref, visible } = useScrollVisible();
|
|
||||||
const { displayed, done } = useTypewriter("Skills", visible, 20);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="space-y-8">
|
|
||||||
<h3 className="text-3xl font-bold text-yellow-bright" style={{ minHeight: "1.2em" }}>
|
|
||||||
{visible ? displayed : "\u00A0"}
|
|
||||||
{visible && !done && <span className="animate-pulse text-foreground/40">|</span>}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="grid grid-cols-1 md:grid-cols-2 gap-8 transition-all duration-500 ease-out"
|
|
||||||
style={{
|
|
||||||
opacity: done ? 1 : 0,
|
|
||||||
transform: done ? "translateY(0)" : "translateY(12px)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h4 className="text-2xl font-semibold text-green-bright">Technical Skills</h4>
|
|
||||||
<SkillTags skills={resumeData.skills.technical} trigger={done} />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h4 className="text-2xl font-semibold text-green-bright">Soft Skills</h4>
|
|
||||||
<SkillTags skills={resumeData.skills.soft} trigger={done} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Resume;
|
export default Resume;
|
||||||
|
|||||||
@@ -5,5 +5,4 @@ author: Timothy Pidashev
|
|||||||
tags: ["uefi", "coreboot", "firmware", "chromebooks"]
|
tags: ["uefi", "coreboot", "firmware", "chromebooks"]
|
||||||
date: 2025-09-15
|
date: 2025-09-15
|
||||||
image: "/blog/breaking-the-chromebook-cage/thumbnail.png"
|
image: "/blog/breaking-the-chromebook-cage/thumbnail.png"
|
||||||
isDraft: true
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ author: Timothy Pidashev
|
|||||||
tags: [t440p, coreboot, thinkpad]
|
tags: [t440p, coreboot, thinkpad]
|
||||||
date: 2025-01-15
|
date: 2025-01-15
|
||||||
image: "/blog/thinkpad-t440p-coreboot-guide/thumbnail.png"
|
image: "/blog/thinkpad-t440p-coreboot-guide/thumbnail.png"
|
||||||
isDraft: true
|
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Commands, Command, CommandSequence } from "@/components/mdx/command";
|
import { Commands, Command, CommandSequence } from "@/components/mdx/command";
|
||||||
@@ -25,7 +24,6 @@ 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>.
|
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
|
## Getting Started
|
||||||
|
|
||||||
The Thinkpad T440p is a powerful and versatile laptop that can be further enhanced by installing coreboot,
|
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,
|
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.
|
including flashing the BIOS chip and installing the necessary software.
|
||||||
@@ -203,64 +201,22 @@ 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
|
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:
|
tweaked to your liking. Here is a list of whats available:
|
||||||
|
|
||||||
### 1. GRUB2 (Recommended)
|
1. GRUB2
|
||||||
|
|
||||||
GRUB2 is the recommended payload for most users. It boots Linux directly without needing a
|
This configuration features GRUB2 as the bootloader, and contains 3 secondary payloads,
|
||||||
separate bootloader installation on disk. This configuration includes three secondary payloads:
|
which the user can opt in/out of:
|
||||||
|
|
||||||
|
* memtest built in
|
||||||
|
* nvramcui built in
|
||||||
|
* coreinfo built in
|
||||||
|
|
||||||
- **memtest86+** - Memory testing utility
|
This configuration also includes the dGPU option rom as well for T440p's featuring the gt730m on board.
|
||||||
- **nvramcui** - CMOS/NVRAM settings editor
|
|
||||||
- **coreinfo** - System information viewer
|
|
||||||
|
|
||||||
If your T440p has the optional GT730M dGPU, the GRUB2 config also includes the
|
2. SeaBIOS
|
||||||
necessary VGA option ROM for it.
|
|
||||||
|
|
||||||
### 2. SeaBIOS
|
3. edk2
|
||||||
|
|
||||||
SeaBIOS provides a traditional BIOS interface, making it the most compatible option.
|
> NOTE: Show the user how to choose the appropriate config, as well as building a custom config below.
|
||||||
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** → Mainboard vendor: **Lenovo** → Mainboard model: **ThinkPad T440p**
|
|
||||||
- **Chipset** → Add Intel descriptor.bin, ME firmware, and GbE configuration (set paths to your blobs)
|
|
||||||
- **Chipset** → Add haswell MRC file (set path to mrc.bin)
|
|
||||||
- **Payload** → Choose your preferred payload (GRUB2, SeaBIOS, or edk2)
|
|
||||||
|
|
||||||
## Building and Flashing
|
## Building and Flashing
|
||||||
|
|
||||||
@@ -282,7 +238,7 @@ Once the coreboot build has completed, split the built ROM for the 8MB(bottom) c
|
|||||||
commands={[
|
commands={[
|
||||||
"cd ~/t440p-coreboot/coreboot/build",
|
"cd ~/t440p-coreboot/coreboot/build",
|
||||||
"dd if=coreboot.rom of=bottom.rom bs=1M count=8",
|
"dd if=coreboot.rom of=bottom.rom bs=1M count=8",
|
||||||
"dd if=coreboot.rom of=top.rom bs=1M skip=8"
|
"dd if=coreboot.rom of=top.rom bs=1M skin=8"
|
||||||
]}
|
]}
|
||||||
description="Split the built ROM for both EEPROM chips"
|
description="Split the built ROM for both EEPROM chips"
|
||||||
client:load
|
client:load
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { defineCollection, z } from "astro:content";
|
import { defineCollection, z } from "astro:content";
|
||||||
import { glob } from "astro/loaders";
|
|
||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
blog: defineCollection({
|
blog: defineCollection({
|
||||||
loader: glob({ pattern: "**/*.mdx", base: "./src/content/blog" }),
|
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
@@ -14,11 +12,9 @@ export const collections = {
|
|||||||
}),
|
}),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
imagePosition: z.string().optional(),
|
imagePosition: z.string().optional(),
|
||||||
isDraft: z.boolean().optional()
|
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
projects: defineCollection({
|
projects: defineCollection({
|
||||||
loader: glob({ pattern: "**/*.mdx", base: "./src/content/projects" }),
|
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
@@ -26,7 +22,7 @@ export const collections = {
|
|||||||
demoUrl: z.string().url().optional(),
|
demoUrl: z.string().url().optional(),
|
||||||
techStack: z.array(z.string()),
|
techStack: z.array(z.string()),
|
||||||
date: z.string(),
|
date: z.string(),
|
||||||
image: z.string().optional()
|
image: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
@@ -41,7 +41,7 @@ export function getArticleSchema(post: CollectionEntry<"blog">) {
|
|||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "Article",
|
"@type": "Article",
|
||||||
headline: post.data.title,
|
headline: post.data.title,
|
||||||
url: `${import.meta.env.SITE}/blog/${post.id}/`,
|
url: `${import.meta.env.SITE}/blog/${post.slug}/`,
|
||||||
description: post.data.excerpt,
|
description: post.data.excerpt,
|
||||||
datePublished: post.data.date.toString(),
|
datePublished: post.data.date.toString(),
|
||||||
publisher: {
|
publisher: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Timeline from "@/components/about/timeline";
|
|||||||
import CurrentFocus from "@/components/about/current-focus";
|
import CurrentFocus from "@/components/about/current-focus";
|
||||||
import OutsideCoding from "@/components/about/outside-coding";
|
import OutsideCoding from "@/components/about/outside-coding";
|
||||||
---
|
---
|
||||||
<ContentLayout
|
<ContentLayout
|
||||||
title="About | Timothy Pidashev"
|
title="About | Timothy Pidashev"
|
||||||
description="A software engineer passionate about the web, open source, and building innovative solutions."
|
description="A software engineer passionate about the web, open source, and building innovative solutions."
|
||||||
>
|
>
|
||||||
@@ -17,23 +17,23 @@ import OutsideCoding from "@/components/about/outside-coding";
|
|||||||
<Intro client:load />
|
<Intro client:load />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="min-h-[60vh] flex items-center justify-center py-16">
|
<section class="flex items-center justify-center py-16">
|
||||||
<AllTimeStats client:load />
|
<AllTimeStats client:only="react" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="min-h-screen flex items-center justify-center py-16">
|
<section class="flex items-center justify-center py-16">
|
||||||
<DetailedStats client:load />
|
<DetailedStats client:only="react" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="min-h-[80vh] flex items-center justify-center py-16">
|
<section class="flex items-center justify-center py-16">
|
||||||
<Timeline client:load />
|
<Timeline client:load />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="min-h-[80vh] flex items-center justify-center py-16">
|
<section class="flex items-center justify-center py-16">
|
||||||
<CurrentFocus client:load />
|
<CurrentFocus client:load />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="min-h-[50vh] flex items-center justify-center py-16">
|
<section class="flex items-center justify-center py-16">
|
||||||
<OutsideCoding client:load />
|
<OutsideCoding client:load />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,14 +2,7 @@ import type { APIRoute } from 'astro';
|
|||||||
|
|
||||||
export const GET: APIRoute = async () => {
|
export const GET: APIRoute = async () => {
|
||||||
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
|
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
|
||||||
|
|
||||||
if (!WAKATIME_API_KEY) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ error: "WAKATIME_API_KEY not configured" }),
|
|
||||||
{ status: 503, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
'https://wakatime.com/api/v1/users/current/summaries?range=last_6_months', {
|
'https://wakatime.com/api/v1/users/current/summaries?range=last_6_months', {
|
||||||
|
|||||||
@@ -1,35 +1,22 @@
|
|||||||
import type { APIRoute } from "astro";
|
import type { APIRoute } from "astro";
|
||||||
|
import { exec } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
export const GET: APIRoute = async () => {
|
export const GET: APIRoute = async () => {
|
||||||
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
|
|
||||||
|
|
||||||
if (!WAKATIME_API_KEY) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ error: "WAKATIME_API_KEY not configured" }),
|
|
||||||
{ status: 503, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
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`);
|
||||||
"https://wakatime.com/api/v1/users/current/all_time_since_today",
|
|
||||||
{
|
return new Response(stdout, {
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${Buffer.from(WAKATIME_API_KEY).toString("base64")}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return new Response(JSON.stringify(data), {
|
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("WakaTime alltime API error:", error);
|
return new Response(JSON.stringify({ error: "Failed to fetch stats" }), {
|
||||||
return new Response(
|
status: 500
|
||||||
JSON.stringify({ error: "Failed to fetch stats" }),
|
});
|
||||||
{ status: 500, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -3,14 +3,7 @@ import type { APIRoute } from 'astro';
|
|||||||
|
|
||||||
export const GET: APIRoute = async () => {
|
export const GET: APIRoute = async () => {
|
||||||
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
|
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
|
||||||
|
|
||||||
if (!WAKATIME_API_KEY) {
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({ error: "WAKATIME_API_KEY not configured" }),
|
|
||||||
{ status: 503, headers: { "Content-Type": "application/json" } }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
'https://wakatime.com/api/v1/users/current/stats/last_7_days?timeout=15', {
|
'https://wakatime.com/api/v1/users/current/stats/last_7_days?timeout=15', {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
import { getCollection, render } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
import { Image } from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import ContentLayout from "@/layouts/content.astro";
|
import ContentLayout from "@/layouts/content.astro";
|
||||||
import { getArticleSchema } from "@/lib/structuredData";
|
import { getArticleSchema } from "@/lib/structuredData";
|
||||||
@@ -11,17 +11,17 @@ const { slug } = Astro.params;
|
|||||||
|
|
||||||
// Fetch blog posts
|
// Fetch blog posts
|
||||||
const posts = await getCollection("blog");
|
const posts = await getCollection("blog");
|
||||||
const post = posts.find(post => post.id === slug);
|
const post = posts.find(post => post.slug === slug);
|
||||||
|
|
||||||
if (!post || (!import.meta.env.DEV && post.data.isDraft === true)) {
|
if (!post) {
|
||||||
return new Response(null, {
|
return new Response(null, {
|
||||||
status: 404,
|
status: 404,
|
||||||
statusText: "Not found"
|
statusText: 'Not found'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamically render the content
|
// Dynamically render the content
|
||||||
const { Content } = await render(post);
|
const { Content } = await post.render();
|
||||||
|
|
||||||
// Format the date
|
// Format the date
|
||||||
const formattedDate = new Date(post.data.date).toLocaleDateString("en-US", {
|
const formattedDate = new Date(post.data.date).toLocaleDateString("en-US", {
|
||||||
@@ -46,7 +46,7 @@ const breadcrumbsStructuredData = {
|
|||||||
"@type": "ListItem",
|
"@type": "ListItem",
|
||||||
position: 2,
|
position: 2,
|
||||||
name: post.data.title,
|
name: post.data.title,
|
||||||
item: `${import.meta.env.SITE}/blog/${post.id}/`,
|
item: `${import.meta.env.SITE}/blog/${post.slug}/`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -65,7 +65,7 @@ const jsonLd = {
|
|||||||
<div class="relative max-w-8xl mx-auto">
|
<div class="relative max-w-8xl mx-auto">
|
||||||
<article class="prose prose-invert prose-lg mx-auto max-w-4xl">
|
<article class="prose prose-invert prose-lg mx-auto max-w-4xl">
|
||||||
{post.data.image && (
|
{post.data.image && (
|
||||||
<div class="-mx-4 sm:mx-0 mb-4">
|
<div class="-mx-4 sm:mx-0 mb-8">
|
||||||
<Image
|
<Image
|
||||||
src={post.data.image}
|
src={post.data.image}
|
||||||
alt={post.data.title}
|
alt={post.data.title}
|
||||||
@@ -76,18 +76,20 @@ const jsonLd = {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h1 class="text-3xl !mt-2 !mb-2">{post.data.title}</h1>
|
<h1 class="text-3xl pt-4">{post.data.title}</h1>
|
||||||
<p class="lg:text-2xl sm:text-lg !mt-0 !mb-3">{post.data.description}</p>
|
<p class="lg:text-2xl sm:text-lg">{post.data.description}</p>
|
||||||
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
|
<div class="mt-4 md:mt-6">
|
||||||
<span class="text-orange">{post.data.author}</span>
|
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
|
||||||
<span class="text-foreground/50">•</span>
|
<span class="text-orange">{post.data.author}</span>
|
||||||
<time dateTime={post.data.date instanceof Date ? post.data.date.toISOString() : post.data.date} class="text-blue">
|
<span class="text-foreground/50">•</span>
|
||||||
{formattedDate}
|
<time dateTime={post.data.date instanceof Date ? post.data.date.toISOString() : post.data.date} class="text-blue">
|
||||||
</time>
|
{formattedDate}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2 mt-2">
|
<div class="flex flex-wrap gap-2 mt-4 md:mt-6">
|
||||||
{post.data.tags.map((tag) => (
|
{post.data.tags.map((tag) => (
|
||||||
<span
|
<span
|
||||||
class="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
|
class="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
|
||||||
onclick={`window.location.href='/blog/tag/${tag}'`}
|
onclick={`window.location.href='/blog/tag/${tag}'`}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { BlogHeader } from "@/components/blog/header";
|
|||||||
import { BlogPostList } from "@/components/blog/post-list";
|
import { BlogPostList } from "@/components/blog/post-list";
|
||||||
|
|
||||||
const posts = (await getCollection("blog", ({ data }) => {
|
const posts = (await getCollection("blog", ({ data }) => {
|
||||||
return import.meta.env.DEV || data.isDraft !== true;
|
return data.isDraft !== true;
|
||||||
})).sort((a, b) => {
|
})).sort((a, b) => {
|
||||||
return b.data.date.valueOf() - a.data.date.valueOf()
|
return b.data.date.valueOf() - a.data.date.valueOf()
|
||||||
}).map(post => ({
|
}).map(post => ({
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import ContentLayout from "@/layouts/content.astro";
|
|||||||
import TagList from "@/components/blog/tag-list";
|
import TagList from "@/components/blog/tag-list";
|
||||||
|
|
||||||
const posts = (await getCollection("blog", ({ data }) => {
|
const posts = (await getCollection("blog", ({ data }) => {
|
||||||
return import.meta.env.DEV || data.isDraft !== true;
|
return data.isDraft !== true;
|
||||||
})).sort((a, b) => {
|
})).sort((a, b) => {
|
||||||
return b.data.date.valueOf() - a.data.date.valueOf()
|
return b.data.date.valueOf() - a.data.date.valueOf()
|
||||||
}).map(post => ({
|
}).map(post => ({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
|
||||||
import { getCollection, render } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
|
|
||||||
import ContentLayout from "@/layouts/content.astro";
|
import ContentLayout from "@/layouts/content.astro";
|
||||||
import { Comments } from "@/components/blog/comments";
|
import { Comments } from "@/components/blog/comments";
|
||||||
@@ -9,13 +9,13 @@ import { Comments } from "@/components/blog/comments";
|
|||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const projects = await getCollection("projects");
|
const projects = await getCollection("projects");
|
||||||
return projects.map(project => ({
|
return projects.map(project => ({
|
||||||
params: { slug: project.id },
|
params: { slug: project.slug },
|
||||||
props: { project },
|
props: { project },
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { project } = Astro.props;
|
const { project } = Astro.props;
|
||||||
const { Content } = await render(project);
|
const { Content } = await project.render();
|
||||||
---
|
---
|
||||||
|
|
||||||
<ContentLayout title={`${project.data.title} | Timothy Pidashev`}>
|
<ContentLayout title={`${project.data.title} | Timothy Pidashev`}>
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ export async function GET(context: APIContext) {
|
|||||||
title: post.data.title,
|
title: post.data.title,
|
||||||
pubDate: post.data.date,
|
pubDate: post.data.date,
|
||||||
description: post.data.description,
|
description: post.data.description,
|
||||||
link: `/blog/${post.id}/`,
|
link: `/blog/${post.slug}/`,
|
||||||
author: post.data.author,
|
author: post.data.author,
|
||||||
categories: post.data.tags,
|
categories: post.data.tags,
|
||||||
enclosure: post.data.image ? {
|
enclosure: post.data.image ? {
|
||||||
url: new URL(`blog/${post.id}/thumbnail.png`, context.site).toString(),
|
url: new URL(`blog/${post.slug}/thumbnail.png`, context.site).toString(),
|
||||||
type: 'image/jpeg',
|
type: 'image/jpeg',
|
||||||
length: 0
|
length: 0
|
||||||
} : undefined
|
} : undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user