migrate to vercel; bump version

This commit is contained in:
2026-03-31 12:11:30 -07:00
parent 11f05e0d6f
commit 99e4e65d92
106 changed files with 399 additions and 553 deletions
+18
View File
@@ -0,0 +1,18 @@
---
import IndexLayout from "@/layouts/index.astro";
import GlitchText from "@/components/404/glitched-text";
const title = "404 Not Found";
---
<IndexLayout title="404 | Timothy Pidashev" description="Page not found">
<main class="min-h-screen flex flex-col items-center justify-center p-4 text-center">
<GlitchText client:only />
<p class="text-xl text-orange mb-8">Whoops! This page doesn't exist :(</p>
<button
onclick="window.history.back()"
class="underline text-green hover:opacity-70 transition-opacity"
>
go back
</button>
</main>
</IndexLayout>
+40
View File
@@ -0,0 +1,40 @@
---
import "@/style/globals.css"
import ContentLayout from "@/layouts/content.astro";
import Intro from "@/components/about/intro";
import AllTimeStats from "@/components/about/stats-alltime";
import DetailedStats from "@/components/about/stats-detailed";
import Timeline from "@/components/about/timeline";
import CurrentFocus from "@/components/about/current-focus";
import OutsideCoding from "@/components/about/outside-coding";
---
<ContentLayout
title="About | Timothy Pidashev"
description="A software engineer passionate about the web, open source, and building innovative solutions."
>
<div class="min-h-screen">
<section class="h-screen flex items-center justify-center">
<Intro client:load />
</section>
<section class="min-h-[60vh] flex items-center justify-center py-16">
<AllTimeStats client:load />
</section>
<section class="min-h-screen flex items-center justify-center py-16">
<DetailedStats client:load />
</section>
<section class="min-h-[80vh] flex items-center justify-center py-16">
<Timeline client:load />
</section>
<section class="min-h-[80vh] flex items-center justify-center py-16">
<CurrentFocus client:load />
</section>
<section class="min-h-[50vh] flex items-center justify-center py-16">
<OutsideCoding client:load />
</section>
</div>
</ContentLayout>
+39
View File
@@ -0,0 +1,39 @@
import type { APIRoute } from 'astro';
export const GET: APIRoute = async () => {
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
if (!WAKATIME_API_KEY) {
return new Response(
JSON.stringify({ error: "WAKATIME_API_KEY not configured" }),
{ status: 503, headers: { "Content-Type": "application/json" } }
);
}
try {
const response = await fetch(
'https://wakatime.com/api/v1/users/current/summaries?range=last_6_months', {
headers: {
'Authorization': `Basic ${Buffer.from(WAKATIME_API_KEY).toString('base64')}`
}
}
);
const data = await response.json();
return new Response(
JSON.stringify({ data: data.data }),
{
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
);
} catch (error) {
console.error('API Error:', error);
return new Response(
JSON.stringify({ error: 'Failed to fetch WakaTime data' }),
{ status: 500 }
);
}
}
+35
View File
@@ -0,0 +1,35 @@
import type { APIRoute } from "astro";
export const GET: APIRoute = async () => {
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
if (!WAKATIME_API_KEY) {
return new Response(
JSON.stringify({ error: "WAKATIME_API_KEY not configured" }),
{ status: 503, headers: { "Content-Type": "application/json" } }
);
}
try {
const response = await fetch(
"https://wakatime.com/api/v1/users/current/all_time_since_today",
{
headers: {
Authorization: `Basic ${Buffer.from(WAKATIME_API_KEY).toString("base64")}`,
},
}
);
const data = await response.json();
return new Response(JSON.stringify(data), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("WakaTime alltime API error:", error);
return new Response(
JSON.stringify({ error: "Failed to fetch stats" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
};
+40
View File
@@ -0,0 +1,40 @@
// src/pages/api/wakatime/detailed.ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async () => {
const WAKATIME_API_KEY = import.meta.env.WAKATIME_API_KEY;
if (!WAKATIME_API_KEY) {
return new Response(
JSON.stringify({ error: "WAKATIME_API_KEY not configured" }),
{ status: 503, headers: { "Content-Type": "application/json" } }
);
}
try {
const response = await fetch(
'https://wakatime.com/api/v1/users/current/stats/last_7_days?timeout=15', {
headers: {
'Authorization': `Basic ${Buffer.from(WAKATIME_API_KEY).toString('base64')}`
}
}
);
const data = await response.json();
return new Response(
JSON.stringify({ data: data.data }),
{
status: 200,
headers: {
'Content-Type': 'application/json'
}
}
);
} catch (error) {
console.error('API Error:', error);
return new Response(
JSON.stringify({ error: 'Failed to fetch WakaTime data' }),
{ status: 500 }
);
}
}
+104
View File
@@ -0,0 +1,104 @@
---
import { getCollection, render } from "astro:content";
import { Image } from "astro:assets";
import ContentLayout from "@/layouts/content.astro";
import { getArticleSchema } from "@/lib/structuredData";
import { blogWebsite } from "@/lib/structuredData";
import { Comments } from "@/components/blog/comments";
// This is a dynamic route in SSR mode
const { slug } = Astro.params;
// Fetch blog posts
const posts = await getCollection("blog");
const post = posts.find(post => post.id === slug);
if (!post || (!import.meta.env.DEV && post.data.isDraft === true)) {
return new Response(null, {
status: 404,
statusText: "Not found"
});
}
// Dynamically render the content
const { Content } = await render(post);
// Format the date
const formattedDate = new Date(post.data.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
});
const articleStructuredData = getArticleSchema(post);
const breadcrumbsStructuredData = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: [
{
"@type": "ListItem",
position: 1,
name: "Blog",
item: `${import.meta.env.SITE}/blog/`,
},
{
"@type": "ListItem",
position: 2,
name: post.data.title,
item: `${import.meta.env.SITE}/blog/${post.id}/`,
},
],
};
const jsonLd = {
"@context": "https://schema.org",
"@graph": [articleStructuredData, breadcrumbsStructuredData, blogWebsite],
};
---
<ContentLayout
title={`${post.data.title} | Timothy Pidashev`}
description={post.data.description}
>
<script type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
<div class="relative max-w-8xl mx-auto">
<article class="prose prose-invert prose-lg mx-auto max-w-4xl">
{post.data.image && (
<div class="-mx-4 sm:mx-0 mb-4">
<Image
src={post.data.image}
alt={post.data.title}
class="w-full h-auto rounded-lg object-cover"
width={1200}
height={630}
quality={100}
/>
</div>
)}
<h1 class="text-3xl !mt-2 !mb-2">{post.data.title}</h1>
<p class="lg:text-2xl sm:text-lg !mt-0 !mb-3">{post.data.description}</p>
<div class="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
<span class="text-orange">{post.data.author}</span>
<span class="text-foreground/50">•</span>
<time dateTime={post.data.date instanceof Date ? post.data.date.toISOString() : post.data.date} class="text-blue">
{formattedDate}
</time>
</div>
<div class="flex flex-wrap gap-2 mt-2">
{post.data.tags.map((tag) => (
<span
class="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
onclick={`window.location.href='/blog/tag/${tag}'`}
>
#{tag}
</span>
))}
</div>
<div class="prose prose-invert prose-lg max-w-none">
<Content />
</div>
</article>
<Comments client:idle />
</div>
</ContentLayout>
+29
View File
@@ -0,0 +1,29 @@
---
import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro";
import { BlogHeader } from "@/components/blog/header";
import { BlogPostList } from "@/components/blog/post-list";
const posts = (await getCollection("blog", ({ data }) => {
return import.meta.env.DEV || data.isDraft !== true;
})).sort((a, b) => {
return b.data.date.valueOf() - a.data.date.valueOf()
}).map(post => ({
...post,
data: {
...post.data,
date: post.data.date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
})
}
}));
---
<ContentLayout
title="Blog | Timothy Pidashev"
description="My experiences and technical insights into software development and the ever-evolving world of programming."
>
<BlogHeader client:load />
<BlogPostList posts={posts} client:load />
</ContentLayout>
+28
View File
@@ -0,0 +1,28 @@
---
import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro";
import TagList from "@/components/blog/tag-list";
const posts = (await getCollection("blog", ({ data }) => {
return import.meta.env.DEV || data.isDraft !== true;
})).sort((a, b) => {
return b.data.date.valueOf() - a.data.date.valueOf()
}).map(post => ({
...post,
data: {
...post.data,
date: post.data.date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
})
}
}));
---
<ContentLayout
title="Blog | Timothy Pidashev"
description="My experiences and technical insights into software development and the ever-evolving world of programming."
>
<TagList posts={posts} />
</ContentLayout>
+15
View File
@@ -0,0 +1,15 @@
---
export const prerender = false;
import "@/style/globals.css"
import IndexLayout from "@/layouts/index.astro";
import Hero from "@/components/hero";
---
<IndexLayout
title="Timothy Pidashev"
description="Software engineer passionate about systems programming, artificial intelligence, and building innovative solutions. Personal site featuring my projects, technical blog, and research."
>
<Hero client:load />
</IndexLayout>
+68
View File
@@ -0,0 +1,68 @@
---
export const prerender = true;
import { getCollection, render } from "astro:content";
import ContentLayout from "@/layouts/content.astro";
import { Comments } from "@/components/blog/comments";
export async function getStaticPaths() {
const projects = await getCollection("projects");
return projects.map(project => ({
params: { slug: project.id },
props: { project },
}));
}
const { project } = Astro.props;
const { Content } = await render(project);
---
<ContentLayout title={`${project.data.title} | Timothy Pidashev`}>
<article class="w-full mx-auto px-4 pt-6 sm:pt-12">
{/* Image Section */}
{project.data.image && (
<div class="w-full rounded-lg overflow-hidden mb-8 border-2 border-foreground/20 aspect-video">
<img
src={project.data.image}
alt={project.data.title}
class="w-full h-full object-cover"
/>
</div>
)}
<header class="mb-8">
<h1 class="text-3xl sm:text-4xl font-bold text-yellow-bright mb-4">
{project.data.title}
</h1>
<div class="flex flex-wrap gap-2 mb-4">
{project.data.techStack.map(tech => (
<span class="text-xs px-2 py-1 rounded-full bg-purple-bright/10 text-purple-bright">
{tech}
</span>
))}
</div>
<div class="flex gap-4">
{project.data.githubUrl && (
<a href={project.data.githubUrl}
class="text-foreground/70 hover:text-blue-bright transition-colors">
View Source
</a>
)}
{project.data.demoUrl && (
<a href={project.data.demoUrl}
class="text-foreground/70 hover:text-green-bright transition-colors">
Live Link
</a>
)}
</div>
</header>
<div class="prose prose-invert prose-lg max-w-none">
<Content />
</div>
</article>
<Comments client:idle />
</ContentLayout>
+18
View File
@@ -0,0 +1,18 @@
---
import { getCollection } from "astro:content";
import ContentLayout from "@/layouts/content.astro";
import { ProjectList } from "@/components/projects/project-list";
const projects = (await getCollection("projects", ({ data }) => {
return data.isDraft !== true;
})).sort(
(a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);
---
<ContentLayout
title="Projects | Timothy Pidashev"
description="My portfolio of software, including web apps, dev tools, and passion projects."
>
<ProjectList projects={projects} client:load />
</ContentLayout>
+15
View File
@@ -0,0 +1,15 @@
---
import "@/style/globals.css"
import ContentLayout from "@/layouts/content.astro";
import Resume from "@/components/resume";
---
<ContentLayout
title="Resume | Timothy Pidashev"
description = "My professional experience, skills, and development journey in software engineering."
>
<div class="flex items-center justify-center w-full">
<Resume client:load />
</div>
</ContentLayout>
+13
View File
@@ -0,0 +1,13 @@
import type { APIRoute } from "astro";
const getRobotsTxt = (sitemapURL: URL) => `
User-agent: *
Allow: /
Sitemap: ${sitemapURL.href}
`;
export const GET: APIRoute = ({ site }) => {
const sitemapURL = new URL("sitemap-index.xml", site);
return new Response(getRobotsTxt(sitemapURL));
};
+31
View File
@@ -0,0 +1,31 @@
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import type { APIContext } from "astro";
export async function GET(context: APIContext) {
const blog = await getCollection("blog", ({ data }) => {
return import.meta.env.DEV || data.isDraft !== true;
});
const sortedPosts = blog
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
return rss({
title: "Timothy Pidashev",
description: "My experiences and technical insights into software development and the ever-evolving world of programming.",
site: context.site!,
items: sortedPosts.map((post) => ({
title: post.data.title,
pubDate: post.data.date,
description: post.data.description,
link: `/blog/${post.id}/`,
author: post.data.author,
categories: post.data.tags,
enclosure: post.data.image ? {
url: new URL(`blog/${post.id}/thumbnail.png`, context.site).toString(),
type: 'image/jpeg',
length: 0
} : undefined
})),
});
}