refactor: fix linting and typechecking errors
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRight, Newspaper } from "lucide-react";
|
||||
import { Button } from "~/components/ui/button";
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Card,
|
||||
@@ -22,12 +22,12 @@ export default function ArticlesPage() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
setLoading(false);
|
||||
};
|
||||
fetchArticles();
|
||||
void fetchArticles();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section className="animate-fade-in-up prose prose-zinc dark:prose-invert max-w-none">
|
||||
<section className="animate-fade-in-up prose prose-zinc max-w-none dark:prose-invert">
|
||||
<div className="flex items-start gap-3">
|
||||
<Newspaper className="h-8 w-8 text-primary" />
|
||||
<div>
|
||||
|
||||
@@ -1,82 +1,92 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { BreadcrumbUpdater } from "~/components/BreadcrumbUpdater";
|
||||
|
||||
interface BlogPostMetadata {
|
||||
title: string;
|
||||
publishedAt: string;
|
||||
tags?: string[];
|
||||
summary?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
params: Promise<{ slug: string }>;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const contentDir = path.join(process.cwd(), "src/content/blog");
|
||||
const files = fs.readdirSync(contentDir);
|
||||
const contentDir = path.join(process.cwd(), "src/content/blog");
|
||||
const files = fs.readdirSync(contentDir);
|
||||
|
||||
return files
|
||||
.filter((file) => file.endsWith(".mdx"))
|
||||
.map((file) => ({
|
||||
slug: file.replace(".mdx", ""),
|
||||
}));
|
||||
return files
|
||||
.filter((file) => file.endsWith(".mdx"))
|
||||
.map((file) => ({
|
||||
slug: file.replace(".mdx", ""),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps) {
|
||||
const { slug } = await params;
|
||||
try {
|
||||
const { metadata } = await import(`~/content/blog/${slug}.mdx`);
|
||||
return metadata;
|
||||
} catch (e) {
|
||||
return {
|
||||
title: "Post Not Found",
|
||||
};
|
||||
}
|
||||
const { slug } = await params;
|
||||
try {
|
||||
const { metadata } = (await import(`~/content/blog/${slug}.mdx`)) as {
|
||||
metadata: BlogPostMetadata;
|
||||
};
|
||||
return metadata;
|
||||
} catch {
|
||||
return {
|
||||
title: "Post Not Found",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default async function BlogPost({ params }: PageProps) {
|
||||
const { slug } = await params;
|
||||
let Post;
|
||||
let metadata;
|
||||
const { slug } = await params;
|
||||
let Post;
|
||||
let metadata;
|
||||
|
||||
try {
|
||||
const content = await import(`~/content/blog/${slug}.mdx`);
|
||||
Post = content.default;
|
||||
metadata = content.metadata;
|
||||
} catch (e) {
|
||||
notFound();
|
||||
}
|
||||
try {
|
||||
const content = (await import(`~/content/blog/${slug}.mdx`)) as {
|
||||
default: React.ComponentType;
|
||||
metadata: BlogPostMetadata;
|
||||
};
|
||||
Post = content.default;
|
||||
metadata = content.metadata;
|
||||
} catch {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="animate-fade-in-up space-y-8">
|
||||
<BreadcrumbUpdater title={metadata.title} />
|
||||
<div className="mb-8">
|
||||
{/* <Button variant="ghost" asChild className="-ml-4 text-muted-foreground mb-4">
|
||||
return (
|
||||
<article className="animate-fade-in-up space-y-8">
|
||||
<BreadcrumbUpdater title={metadata.title} />
|
||||
<div className="mb-8">
|
||||
{/* <Button variant="ghost" asChild className="-ml-4 text-muted-foreground mb-4">
|
||||
<Link href="/blog">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to Blog
|
||||
</Link>
|
||||
</Button> */}
|
||||
|
||||
<h1 className="text-3xl font-bold mb-4">{metadata.title}</h1>
|
||||
<h1 className="mb-4 text-3xl font-bold">{metadata.title}</h1>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-center text-muted-foreground mb-6">
|
||||
<time dateTime={metadata.publishedAt}>{metadata.publishedAt}</time>
|
||||
{metadata.tags && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{metadata.tags.map((tag: string) => (
|
||||
<Badge key={tag} variant="secondary">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-6 flex flex-wrap items-center gap-4 text-muted-foreground">
|
||||
<time dateTime={metadata.publishedAt}>{metadata.publishedAt}</time>
|
||||
{metadata.tags && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{metadata.tags.map((tag: string) => (
|
||||
<Badge key={tag} variant="secondary">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-zinc dark:prose-invert max-w-none">
|
||||
<Post />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
<div className="prose prose-zinc max-w-none dark:prose-invert">
|
||||
<Post />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
+89
-69
@@ -1,87 +1,107 @@
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "~/components/ui/card";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "~/components/ui/card";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { BookOpen } from "lucide-react";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
interface BlogPost {
|
||||
title: string;
|
||||
publishedAt: string;
|
||||
summary: string;
|
||||
tags?: string[];
|
||||
slug: string;
|
||||
}
|
||||
|
||||
// Helper to get blog posts
|
||||
async function getBlogPosts() {
|
||||
const contentDir = path.join(process.cwd(), "src/content/blog");
|
||||
const files = fs.readdirSync(contentDir);
|
||||
async function getBlogPosts(): Promise<BlogPost[]> {
|
||||
const contentDir = path.join(process.cwd(), "src/content/blog");
|
||||
const files = fs.readdirSync(contentDir);
|
||||
|
||||
const posts = await Promise.all(
|
||||
files
|
||||
.filter((file) => file.endsWith(".mdx"))
|
||||
.map(async (file) => {
|
||||
const slug = file.replace(".mdx", "");
|
||||
// Dynamic import to get metadata
|
||||
const { metadata } = await import(`~/content/blog/${file}`);
|
||||
return {
|
||||
slug,
|
||||
...metadata,
|
||||
};
|
||||
})
|
||||
);
|
||||
const posts = await Promise.all(
|
||||
files
|
||||
.filter((file) => file.endsWith(".mdx"))
|
||||
.map(async (file) => {
|
||||
const slug = file.replace(".mdx", "");
|
||||
// Dynamic import to get metadata
|
||||
const { metadata } = (await import(`~/content/blog/${file}`)) as {
|
||||
metadata: Omit<BlogPost, "slug">;
|
||||
};
|
||||
return {
|
||||
slug,
|
||||
...metadata,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return posts.sort((a, b) => {
|
||||
if (new Date(a.publishedAt) > new Date(b.publishedAt)) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
return posts.sort((a, b) => {
|
||||
if (new Date(a.publishedAt) > new Date(b.publishedAt)) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: "Blog",
|
||||
description: "Thoughts, tutorials, and project deep-dives.",
|
||||
title: "Blog",
|
||||
description: "Thoughts, tutorials, and project deep-dives.",
|
||||
};
|
||||
|
||||
export default async function BlogPage() {
|
||||
const posts = await getBlogPosts();
|
||||
const posts = await getBlogPosts();
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<section className="animate-fade-in-up prose prose-zinc dark:prose-invert max-w-none">
|
||||
<div className="flex items-start gap-3">
|
||||
<BookOpen className="h-8 w-8 text-primary" />
|
||||
<div>
|
||||
<h1 className="mb-2 text-2xl font-bold">Blog</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-lg text-muted-foreground">
|
||||
Deep dives into my projects, tutorials, and thoughts on technology.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div className="grid gap-6 animate-fade-in-up-delay-2">
|
||||
{posts.map((post) => (
|
||||
<Link key={post.slug} href={`/blog/${post.slug}`} className="block card-hover">
|
||||
<Card className="h-full transition-colors hover:bg-muted/50">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start">
|
||||
<CardTitle className="text-xl mb-2">{post.title}</CardTitle>
|
||||
<span className="text-sm text-muted-foreground whitespace-nowrap ml-4">
|
||||
{post.publishedAt}
|
||||
</span>
|
||||
</div>
|
||||
<CardDescription className="text-base">
|
||||
{post.summary}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{post.tags?.map((tag: string) => (
|
||||
<Badge key={tag} variant="secondary">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<section className="animate-fade-in-up prose prose-zinc max-w-none dark:prose-invert">
|
||||
<div className="flex items-start gap-3">
|
||||
<BookOpen className="h-8 w-8 text-primary" />
|
||||
<div>
|
||||
<h1 className="mb-2 text-2xl font-bold">Blog</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<p className="mt-2 text-lg text-muted-foreground">
|
||||
Deep dives into my projects, tutorials, and thoughts on technology.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div className="animate-fade-in-up-delay-2 grid gap-6">
|
||||
{posts.map((post) => (
|
||||
<Link
|
||||
key={post.slug}
|
||||
href={`/blog/${post.slug}`}
|
||||
className="card-hover block"
|
||||
>
|
||||
<Card className="h-full transition-colors hover:bg-muted/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<CardTitle className="mb-2 text-xl">{post.title}</CardTitle>
|
||||
<span className="ml-4 whitespace-nowrap text-sm text-muted-foreground">
|
||||
{post.publishedAt}
|
||||
</span>
|
||||
</div>
|
||||
<CardDescription className="text-base">
|
||||
{post.summary}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{post.tags?.map((tag: string) => (
|
||||
<Badge key={tag} variant="secondary">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-8
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect, useCallback } from "react";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs";
|
||||
import {
|
||||
Card,
|
||||
@@ -22,8 +22,6 @@ import {
|
||||
ChevronRight,
|
||||
AlertCircle,
|
||||
Loader2,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
Eye,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -116,7 +114,7 @@ function PDFViewer({ url, title, type }: PDFViewerProps) {
|
||||
}
|
||||
};
|
||||
|
||||
loadPDF();
|
||||
void loadPDF();
|
||||
}, [url, isClient]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -177,7 +175,7 @@ function PDFViewer({ url, title, type }: PDFViewerProps) {
|
||||
}
|
||||
};
|
||||
|
||||
renderPage();
|
||||
void renderPage();
|
||||
}, [pdfDoc, pageNum, scale, rotation]);
|
||||
|
||||
const nextPage = () => {
|
||||
@@ -481,7 +479,7 @@ export default function CVPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section className="animate-fade-in-up prose prose-zinc dark:prose-invert max-w-none">
|
||||
<section className="animate-fade-in-up prose prose-zinc max-w-none dark:prose-invert">
|
||||
<div className="flex items-start gap-3">
|
||||
<FileText className="h-8 w-8 text-primary" />
|
||||
<div>
|
||||
@@ -520,8 +518,6 @@ export default function CVPage() {
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function ExperiencePage() {
|
||||
const renderExperienceSection = (
|
||||
title: string,
|
||||
experiences: typeof researchExperience,
|
||||
delay: number = 1,
|
||||
delay = 1,
|
||||
) => (
|
||||
<section className="animate-fade-in-up space-y-6">
|
||||
<h2 className="text-2xl font-bold">{title}</h2>
|
||||
@@ -118,7 +118,7 @@ export default function ExperiencePage() {
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
{/* Header */}
|
||||
<section className="animate-fade-in-up prose prose-zinc dark:prose-invert max-w-none">
|
||||
<section className="animate-fade-in-up prose prose-zinc max-w-none dark:prose-invert">
|
||||
<div className="flex items-start gap-3">
|
||||
<Building className="h-8 w-8 text-primary" />
|
||||
<div>
|
||||
|
||||
+6
-7
@@ -1,4 +1,3 @@
|
||||
|
||||
import Script from "next/script";
|
||||
import type { Metadata } from "next";
|
||||
import { env } from "~/env";
|
||||
@@ -26,20 +25,20 @@ export default function RootLayout({ children }: React.PropsWithChildren) {
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body
|
||||
className="flex min-h-screen flex-col bg-background font-sans text-foreground relative"
|
||||
className="relative flex min-h-screen flex-col bg-background font-sans text-foreground"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{/* Background Elements */}
|
||||
<div className="fixed inset-0 -z-10 overflow-hidden pointer-events-none flex items-center justify-center">
|
||||
<div className="pointer-events-none fixed inset-0 -z-10 flex items-center justify-center overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#80808012_1px,transparent_1px),linear-gradient(to_bottom,#80808012_1px,transparent_1px)] bg-[size:24px_24px] [mask-image:radial-gradient(ellipse_60%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
|
||||
<div className="w-[800px] h-[800px] bg-neutral-400/40 dark:bg-neutral-500/30 rounded-full blur-3xl animate-blob"></div>
|
||||
<div className="animate-blob h-[800px] w-[800px] rounded-full bg-neutral-400/40 blur-3xl dark:bg-neutral-500/30"></div>
|
||||
</div>
|
||||
|
||||
{env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
|
||||
<Script
|
||||
defer
|
||||
src={
|
||||
env.NEXT_PUBLIC_UMAMI_SCRIPT_URL ||
|
||||
env.NEXT_PUBLIC_UMAMI_SCRIPT_URL ??
|
||||
"https://analytics.umami.is/script.js"
|
||||
}
|
||||
data-website-id={env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
||||
@@ -48,9 +47,9 @@ export default function RootLayout({ children }: React.PropsWithChildren) {
|
||||
|
||||
<BreadcrumbProvider>
|
||||
<Navigation />
|
||||
<div className="flex flex-1 pt-24 flex-col lg:flex-row">
|
||||
<div className="flex flex-1 flex-col pt-24 lg:flex-row">
|
||||
<Sidebar />
|
||||
<div className="flex-1 min-w-0 lg:pl-96">
|
||||
<div className="min-w-0 flex-1 lg:pl-96">
|
||||
<div className="mx-auto max-w-screen-xl px-6 sm:px-8 lg:pl-0 lg:pr-8">
|
||||
<main className="pb-8 pt-4">
|
||||
<BreadcrumbWrapper />
|
||||
|
||||
+13
-14
@@ -1,19 +1,20 @@
|
||||
import {
|
||||
ArrowUpRight,
|
||||
Code,
|
||||
FlaskConical,
|
||||
Users,
|
||||
GraduationCap,
|
||||
Building,
|
||||
MapPin,
|
||||
Mail,
|
||||
ExternalLink,
|
||||
BookOpen,
|
||||
School,
|
||||
Award,
|
||||
Calendar,
|
||||
BookOpen,
|
||||
Building,
|
||||
Code,
|
||||
ExternalLink,
|
||||
FlaskConical,
|
||||
GraduationCap,
|
||||
Mail,
|
||||
MapPin,
|
||||
School,
|
||||
Users
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -21,9 +22,7 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Badge } from "~/components/ui/badge";
|
||||
import { researchInterests, education, experiences, awards } from "~/lib/data";
|
||||
import { awards, education, experiences, researchInterests } from "~/lib/data";
|
||||
|
||||
export default function HomePage() {
|
||||
const researchExperience = experiences.filter(
|
||||
|
||||
+180
-203
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
@@ -14,21 +13,14 @@ import Link from "next/link";
|
||||
import { ArrowUpRight, Play, BookOpen, FolderGit2, Github } from "lucide-react";
|
||||
import { projects } from "~/lib/data";
|
||||
import { ImageWithSkeleton } from "~/components/ui/image-with-skeleton";
|
||||
import { CardSkeleton } from "~/components/ui/skeletons";
|
||||
|
||||
export default function ProjectsPage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
const featuredProjects = projects.filter((p) => p.featured);
|
||||
const otherProjects = projects.filter((p) => !p.featured);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<section className="prose prose-zinc dark:prose-invert max-w-none">
|
||||
<section className="prose prose-zinc max-w-none dark:prose-invert">
|
||||
<div className="flex items-start gap-3">
|
||||
<FolderGit2 className="h-8 w-8 text-primary" />
|
||||
<div>
|
||||
@@ -45,87 +37,207 @@ export default function ProjectsPage() {
|
||||
<section className="animate-fade-in-up space-y-6">
|
||||
<h2 className="text-2xl font-bold">Featured Work</h2>
|
||||
<div className="space-y-8">
|
||||
{loading ? (
|
||||
<>
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
</>
|
||||
) : (
|
||||
featuredProjects.map((project, index) => (
|
||||
{featuredProjects.map((project, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`animate-fade-in-up-delay-${Math.min(index + 1, 4)} card-hover`}
|
||||
>
|
||||
<Card className="overflow-hidden">
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
{/* Project Image */}
|
||||
{project.image && (
|
||||
<div className="lg:w-1/3">
|
||||
<div className="flex items-center justify-center p-4 lg:h-full">
|
||||
<ImageWithSkeleton
|
||||
src={project.image}
|
||||
alt={project.imageAlt ?? project.title}
|
||||
width={400}
|
||||
height={300}
|
||||
className="h-auto w-full object-contain"
|
||||
containerClassName="w-full rounded-xl shadow-md overflow-hidden"
|
||||
priority={index === 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project Content */}
|
||||
<div className="card-content-stretch flex flex-1 flex-col p-6">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div>
|
||||
<CardTitle className="break-words text-xl leading-tight">
|
||||
{project.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-2 break-words text-base leading-relaxed">
|
||||
{project.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
|
||||
<p className="break-words leading-relaxed text-muted-foreground">
|
||||
{project.longDescription}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
className="break-words"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 sm:flex-shrink-0">
|
||||
{project.link && project.link.startsWith("/") && (
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover"
|
||||
>
|
||||
<Link href={project.link}>
|
||||
{project.title ===
|
||||
"LaTeX Introduction Tutorial" ? (
|
||||
<>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Watch Tutorial
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BookOpen className="mr-2 h-4 w-4" />
|
||||
Learn More
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{project.websiteLink && (
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover"
|
||||
>
|
||||
<Link
|
||||
href={project.websiteLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ArrowUpRight className="mr-2 h-4 w-4" />
|
||||
Visit Site
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{project.gitLink && (
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover"
|
||||
>
|
||||
<Link
|
||||
href={project.gitLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
View Code
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{project.link &&
|
||||
!project.link.startsWith("/") &&
|
||||
!project.websiteLink &&
|
||||
!project.gitLink && (
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover"
|
||||
>
|
||||
<Link
|
||||
href={project.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ArrowUpRight className="mr-2 h-4 w-4" />
|
||||
View Project
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Other Projects */}
|
||||
{otherProjects.length > 0 && (
|
||||
<section className="animate-fade-in-up space-y-6">
|
||||
<h2 className="text-2xl font-bold">Additional Projects</h2>
|
||||
<div className="grid-equal-height grid gap-6 md:grid-cols-2">
|
||||
{otherProjects.map((project, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`animate-fade-in-up-delay-${Math.min(index + 1, 4)} card-hover`}
|
||||
>
|
||||
<Card className="overflow-hidden">
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
{/* Project Image */}
|
||||
{project.image && (
|
||||
<div className="lg:w-1/3">
|
||||
<div className="flex items-center justify-center p-4 lg:h-full">
|
||||
<ImageWithSkeleton
|
||||
src={project.image}
|
||||
alt={project.imageAlt || project.title}
|
||||
width={400}
|
||||
height={300}
|
||||
className="h-auto w-full object-contain"
|
||||
containerClassName="w-full rounded-xl shadow-md overflow-hidden"
|
||||
priority={index === 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Card className="card-full-height flex flex-col">
|
||||
{project.image && (
|
||||
<div className="flex h-48 items-center justify-center p-4">
|
||||
<ImageWithSkeleton
|
||||
src={project.image}
|
||||
alt={project.imageAlt ?? project.title}
|
||||
width={400}
|
||||
height={250}
|
||||
className="h-auto max-h-full w-full object-contain"
|
||||
containerClassName="w-full h-full flex items-center justify-center rounded-xl shadow-md overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Project Content */}
|
||||
<div className="card-content-stretch flex flex-1 flex-col p-6">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex flex-1 flex-col p-6">
|
||||
<div className="flex flex-1 flex-col">
|
||||
<CardHeader className="p-0">
|
||||
<div>
|
||||
<CardTitle className="break-words text-xl leading-tight">
|
||||
<CardTitle className="break-words text-lg leading-tight">
|
||||
{project.title}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-2 break-words text-base leading-relaxed">
|
||||
{project.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<CardDescription className="mt-2 break-words leading-relaxed">
|
||||
{project.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<p className="break-words leading-relaxed text-muted-foreground">
|
||||
{project.longDescription}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<CardContent className="flex flex-1 flex-col p-0 pt-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
className="break-words"
|
||||
className="break-words text-xs"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 sm:flex-shrink-0">
|
||||
<div className="mt-auto flex gap-2 pt-4">
|
||||
{project.link && project.link.startsWith("/") && (
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover"
|
||||
className="button-hover sm:flex-shrink-0"
|
||||
>
|
||||
<Link href={project.link}>
|
||||
{project.title ===
|
||||
"LaTeX Introduction Tutorial" ? (
|
||||
<>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Watch Tutorial
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BookOpen className="mr-2 h-4 w-4" />
|
||||
Learn More
|
||||
</>
|
||||
)}
|
||||
<BookOpen className="mr-2 h-4 w-4" />
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
@@ -134,7 +246,7 @@ export default function ProjectsPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover"
|
||||
className="button-hover sm:flex-shrink-0"
|
||||
>
|
||||
<Link
|
||||
href={project.websiteLink}
|
||||
@@ -151,7 +263,7 @@ export default function ProjectsPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover"
|
||||
className="button-hover sm:flex-shrink-0"
|
||||
>
|
||||
<Link
|
||||
href={project.gitLink}
|
||||
@@ -171,7 +283,7 @@ export default function ProjectsPage() {
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover"
|
||||
className="button-hover sm:flex-shrink-0"
|
||||
>
|
||||
<Link
|
||||
href={project.link}
|
||||
@@ -184,147 +296,12 @@ export default function ProjectsPage() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Other Projects */}
|
||||
{otherProjects.length > 0 && (
|
||||
<section className="animate-fade-in-up space-y-6">
|
||||
<h2 className="text-2xl font-bold">Additional Projects</h2>
|
||||
<div className="grid-equal-height grid gap-6 md:grid-cols-2">
|
||||
{loading ? (
|
||||
<>
|
||||
<CardSkeleton />
|
||||
<CardSkeleton />
|
||||
</>
|
||||
) : (
|
||||
otherProjects.map((project, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`animate-fade-in-up-delay-${Math.min(index + 1, 4)} card-hover`}
|
||||
>
|
||||
<Card className="card-full-height flex flex-col">
|
||||
{project.image && (
|
||||
<div className="flex h-48 items-center justify-center p-4">
|
||||
<ImageWithSkeleton
|
||||
src={project.image}
|
||||
alt={project.imageAlt || project.title}
|
||||
width={400}
|
||||
height={250}
|
||||
className="h-auto max-h-full w-full object-contain"
|
||||
containerClassName="w-full h-full flex items-center justify-center rounded-xl shadow-md overflow-hidden"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 flex-col p-6">
|
||||
<div className="flex flex-1 flex-col">
|
||||
<CardHeader className="p-0">
|
||||
<div>
|
||||
<CardTitle className="break-words text-lg leading-tight">
|
||||
{project.title}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<CardDescription className="mt-2 break-words leading-relaxed">
|
||||
{project.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-1 flex-col p-0 pt-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.map((tag) => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant="secondary"
|
||||
className="break-words text-xs"
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-auto flex gap-2 pt-4">
|
||||
{project.link && project.link.startsWith("/") && (
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover sm:flex-shrink-0"
|
||||
>
|
||||
<Link href={project.link}>
|
||||
<BookOpen className="mr-2 h-4 w-4" />
|
||||
Learn More
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{project.websiteLink && (
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover sm:flex-shrink-0"
|
||||
>
|
||||
<Link
|
||||
href={project.websiteLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ArrowUpRight className="mr-2 h-4 w-4" />
|
||||
Visit Site
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{project.gitLink && (
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover sm:flex-shrink-0"
|
||||
>
|
||||
<Link
|
||||
href={project.gitLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Github className="mr-2 h-4 w-4" />
|
||||
View Code
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{project.link &&
|
||||
!project.link.startsWith("/") &&
|
||||
!project.websiteLink &&
|
||||
!project.gitLink && (
|
||||
<Button
|
||||
variant="outline"
|
||||
asChild
|
||||
className="button-hover sm:flex-shrink-0"
|
||||
>
|
||||
<Link
|
||||
href={project.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<ArrowUpRight className="mr-2 h-4 w-4" />
|
||||
View Project
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
BookOpenText,
|
||||
FileText,
|
||||
Presentation,
|
||||
BookOpen,
|
||||
Monitor,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import { Skeleton } from "~/components/ui/skeleton";
|
||||
import { CardSkeleton } from "~/components/ui/skeletons";
|
||||
import type { Publication } from "~/lib/bibtex";
|
||||
import { parseBibtex } from "~/lib/bibtex";
|
||||
@@ -27,10 +25,10 @@ import { parseBibtex } from "~/lib/bibtex";
|
||||
export default function PublicationsPage() {
|
||||
const [publications, setPublications] = useState<Publication[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const tagsToStrip = ["paperUrl", "posterUrl"];
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/publications.bib")
|
||||
void fetch("/publications.bib")
|
||||
.then((res) => res.text())
|
||||
.then((text) => {
|
||||
const pubs = parseBibtex(text);
|
||||
@@ -85,7 +83,7 @@ export default function PublicationsPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section className="animate-fade-in-up prose prose-zinc dark:prose-invert max-w-none">
|
||||
<section className="animate-fade-in-up prose prose-zinc max-w-none dark:prose-invert">
|
||||
<div className="flex items-start gap-3">
|
||||
<BookOpenText className="h-8 w-8 text-primary" />
|
||||
<div>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function TripsPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section className="animate-fade-in-up prose prose-zinc dark:prose-invert max-w-none">
|
||||
<section className="animate-fade-in-up prose prose-zinc max-w-none dark:prose-invert">
|
||||
<div className="flex items-start gap-3">
|
||||
<Plane className="h-8 w-8 text-primary" />
|
||||
<div>
|
||||
@@ -63,9 +63,7 @@ export default function TripsPage() {
|
||||
<ImageWithSkeleton
|
||||
src={image}
|
||||
alt={
|
||||
trip.alts && trip.alts[imgIndex]
|
||||
? trip.alts[imgIndex]
|
||||
: `${trip.title} - image ${imgIndex + 1}`
|
||||
trip.alts?.[imgIndex] ?? `${trip.title} - image ${imgIndex + 1}`
|
||||
}
|
||||
width={250}
|
||||
height={200}
|
||||
|
||||
Reference in New Issue
Block a user