migrate to vercel; bump version
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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" } }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
};
|
||||
@@ -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
|
||||
})),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user