23e5e17be5
Projects now support multiple links (source code, demo, etc) each with a label. Migrated all projects to use the new schema.
199 lines
6.6 KiB
Plaintext
199 lines
6.6 KiB
Plaintext
---
|
|
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
|
import TableOfContents from '@/components/TableOfContents.astro'
|
|
import { badgeVariants } from '@/lib/badge-variants'
|
|
import Layout from '@/layouts/Layout.astro'
|
|
import { getAllProjects } from '@/lib/data-utils'
|
|
import { Icon } from 'astro-icon/components'
|
|
import { Image } from 'astro:assets'
|
|
import { render } from 'astro:content'
|
|
import PageHead from '@/components/PageHead.astro'
|
|
|
|
export async function getStaticPaths() {
|
|
const projects = await getAllProjects()
|
|
return projects.map((project) => ({
|
|
params: { id: project.id },
|
|
props: project,
|
|
}))
|
|
}
|
|
|
|
const project = Astro.props
|
|
const { Content, headings } = await render(project)
|
|
|
|
const structuredData = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Projects',
|
|
headline: project.data.name,
|
|
datePublished: project.data.startDate || '',
|
|
dateModified: project.data.endDate || project.data.startDate || '',
|
|
author: {
|
|
'@type': 'Person',
|
|
name: 'Author',
|
|
},
|
|
image: new URL(`/image/${project.id}.png`, Astro.site).toString(),
|
|
keywords: project.data.tags ? project.data.tags.join(', ') : '',
|
|
description: project.data.description || '',
|
|
}
|
|
const currentUrl = Astro.url;
|
|
---
|
|
|
|
<Layout canonicalUrl={currentUrl} isWide={true}>
|
|
<PageHead slot="head" title={project.data.name} />
|
|
<script
|
|
type="application/ld+json"
|
|
is:inline
|
|
set:html={JSON.stringify(structuredData)}
|
|
/>
|
|
|
|
<div class="grid grid-cols-1 xl:grid-cols-[1fr_minmax(0,60rem)_1fr] gap-4 xl:gap-6 xl:px-8">
|
|
<!-- Header section - spans center column only on xl, full width otherwise -->
|
|
<header class="xl:col-start-2 px-4 sm:px-6 lg:px-8 xl:px-0">
|
|
<Breadcrumbs
|
|
items={[
|
|
{ href: '/projects', label: 'Projects', icon: 'lucide:folder' },
|
|
{ label: project.data.name, icon: 'lucide:folder-open' },
|
|
]}
|
|
/>
|
|
|
|
<div class="mt-6 flex flex-col gap-y-6 text-start">
|
|
<div class="flex flex-col">
|
|
<h1
|
|
class="mb-2 text-3xl leading-tight font-medium text-pretty sm:text-5xl"
|
|
>
|
|
{project.data.name}
|
|
</h1>
|
|
|
|
<div class="flex flex-wrap justify-start gap-2">
|
|
{
|
|
project.data.tags && project.data.tags.length > 0 ? (
|
|
project.data.tags.map((tag) => (
|
|
<a
|
|
href={`/tags/${tag}`}
|
|
class={badgeVariants({ variant: 'secondary' })}
|
|
>
|
|
<Icon name="lucide:hash" class="size-3" />
|
|
{tag}
|
|
</a>
|
|
))
|
|
) : (
|
|
<span class="text-muted-foreground text-sm">
|
|
No tags available
|
|
</span>
|
|
)
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Image
|
|
src={project.data.image}
|
|
alt={project.data.name}
|
|
class="mt-6 h-[500px] w-full rounded-3xl object-cover"
|
|
loading="lazy"
|
|
fetchpriority="low"
|
|
width={1600}
|
|
height={1000}
|
|
/>
|
|
</header>
|
|
|
|
<!-- Left sidebar (TOC) -->
|
|
{headings.length > 0 && (
|
|
<aside class="hidden xl:block xl:col-start-1 xl:row-start-2 justify-self-end self-stretch">
|
|
<nav class="sticky top-24 w-48">
|
|
<TableOfContents headings={headings} />
|
|
</nav>
|
|
</aside>
|
|
)}
|
|
|
|
<!-- Main content -->
|
|
<main class="xl:col-start-2 xl:row-start-2 flex flex-col gap-y-6 px-4 sm:px-6 lg:px-8 xl:px-0">
|
|
<!-- Mobile TOC -->
|
|
{headings.length > 0 && (
|
|
<div class="xl:hidden">
|
|
<TableOfContents headings={headings} />
|
|
</div>
|
|
)}
|
|
|
|
<article class="prose max-w-none" aria-label="Project">
|
|
<Content />
|
|
</article>
|
|
|
|
<!-- Mobile project details -->
|
|
<div class="xl:hidden rounded-xl border p-4">
|
|
<div class="flex flex-col gap-4 rounded-xl border bg-card p-4 shadow-md">
|
|
<h2 class="text-lg font-semibold">Project Details</h2>
|
|
<div class="flex flex-col gap-2 text-sm text-muted-foreground">
|
|
<p>{project.data.description}</p>
|
|
</div>
|
|
|
|
{(project.data.links || project.data.link) && (
|
|
<>
|
|
<hr class="my-4 border-t" />
|
|
|
|
<div class="flex flex-col gap-2 text-sm text-muted-foreground">
|
|
<h3 class="text-base font-semibold">Project Links</h3>
|
|
<ul class="list-disc pl-4">
|
|
{project.data.links ? (
|
|
project.data.links.map((link) => (
|
|
<li>
|
|
<a href={link.url} target="_blank" rel="noopener noreferrer" aria-label={link.label}>
|
|
{link.label}
|
|
</a>
|
|
</li>
|
|
))
|
|
) : project.data.link ? (
|
|
<li>
|
|
<a href={project.data.link} target="_blank" rel="noopener noreferrer" aria-label="Project link">
|
|
{project.data.link}
|
|
</a>
|
|
</li>
|
|
) : null}
|
|
</ul>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<!-- Right sidebar (Project Details) -->
|
|
<aside class="hidden xl:block xl:col-start-3 xl:row-start-2 justify-self-start self-stretch">
|
|
<div class="sticky top-24 w-48">
|
|
<div class="flex flex-col gap-4 rounded-xl border bg-card p-4 shadow-md">
|
|
<h2 class="text-lg font-semibold">Project Details</h2>
|
|
<div class="flex flex-col gap-2 text-sm text-muted-foreground">
|
|
<p>{project.data.description}</p>
|
|
</div>
|
|
|
|
{(project.data.links || project.data.link) && (
|
|
<>
|
|
<hr class="my-4 border-t" />
|
|
|
|
<div class="flex flex-col gap-2 text-sm text-muted-foreground">
|
|
<h3 class="text-base font-semibold">Project Links</h3>
|
|
<ul class="list-disc pl-4">
|
|
{project.data.links ? (
|
|
project.data.links.map((link) => (
|
|
<li>
|
|
<a href={link.url} target="_blank" rel="noopener noreferrer" aria-label={link.label}>
|
|
{link.label}
|
|
</a>
|
|
</li>
|
|
))
|
|
) : project.data.link ? (
|
|
<li>
|
|
<a href={project.data.link} target="_blank" rel="noopener noreferrer" aria-label="Project link">
|
|
{project.data.link}
|
|
</a>
|
|
</li>
|
|
) : null}
|
|
</ul>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</Layout>
|