153 lines
4.4 KiB
TypeScript
153 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useParams } from "next/navigation";
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import ReactMarkdown from "react-markdown";
|
|
|
|
export default function ProjectDetailPage() {
|
|
const { id } = useParams();
|
|
const [project, setProject] = useState(null);
|
|
|
|
useEffect(() => {
|
|
if (!id) return;
|
|
const url = new URL(`${process.env.NEXT_PUBLIC_API_BASE_URL}/items/projects/${id}`);
|
|
url.searchParams.set(
|
|
"fields",
|
|
"submission_id,title,uploader,category,tags,p_image.filename_disk,p_image.title,p_files.directus_files_id.filename_disk"
|
|
);
|
|
url.searchParams.set("limit", "1");
|
|
|
|
fetch(url.toString())
|
|
.then((res) => res.json())
|
|
.then((data) => setProject(data.data))
|
|
.catch(() => setProject(null));
|
|
}, [id]);
|
|
|
|
if (!project) {
|
|
return (
|
|
<div className="p-6 max-w-5xl mx-auto">
|
|
<div className="mb-4">
|
|
<Link href="/projects" className="text-accent underline">
|
|
← Back to Projects
|
|
</Link>
|
|
</div>
|
|
<p className="text-muted-foreground">Loading project…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const imageSrc = project.p_image?.filename_disk
|
|
? `${process.env.NEXT_PUBLIC_ASSET_URL || "https://forms.lasereverything.net"}/assets/${project.p_image.filename_disk}`
|
|
: null;
|
|
|
|
const fileList: string[] = Array.isArray(project.p_files)
|
|
? project.p_files
|
|
.map((f: any) => f?.directus_files_id?.filename_disk)
|
|
.filter(Boolean)
|
|
: [];
|
|
|
|
return (
|
|
<div className="p-6 max-w-5xl mx-auto">
|
|
<style jsx global>{`
|
|
.file-pill {
|
|
display: inline-block;
|
|
font-size: 0.8rem;
|
|
padding: 0.25rem 0.5rem;
|
|
background-color: var(--muted);
|
|
color: var(--foreground);
|
|
border-radius: 0.25rem;
|
|
margin-right: 0.25rem;
|
|
margin-bottom: 0.25rem;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
.file-pill:hover {
|
|
background-color: #ffde59;
|
|
color: #000;
|
|
}
|
|
`}</style>
|
|
|
|
<div className="mb-4">
|
|
<Link href="/projects" className="text-accent underline">
|
|
← Back to Projects
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="md:col-span-1">
|
|
<div className="border rounded overflow-hidden bg-card">
|
|
{imageSrc ? (
|
|
<Image
|
|
src={imageSrc}
|
|
alt={project.p_image?.title || "Project image"}
|
|
width={800}
|
|
height={800}
|
|
className="w-full h-auto object-cover"
|
|
/>
|
|
) : (
|
|
<div className="p-6 text-sm text-muted-foreground">No preview image</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-4">
|
|
<h3 className="text-sm font-semibold mb-2">Files</h3>
|
|
{fileList.length > 0 ? (
|
|
<div className="flex flex-wrap">
|
|
{fileList.map((fname, i) => (
|
|
<a
|
|
key={i}
|
|
className="file-pill"
|
|
href={`${process.env.NEXT_PUBLIC_ASSET_URL || "https://forms.lasereverything.net"}/assets/${fname}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
title={fname}
|
|
>
|
|
{fname}
|
|
</a>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground">No files attached.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<h1 className="text-2xl font-bold mb-2">{project.title}</h1>
|
|
<p className="text-sm text-muted-foreground mb-1">
|
|
Uploaded by: {project.uploader || "—"}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground mb-2">
|
|
Category: {project.category || "—"}
|
|
</p>
|
|
<div className="flex flex-wrap gap-1">
|
|
{Array.isArray(project.tags) && project.tags.length > 0 ? (
|
|
project.tags.map((tag, i) => (
|
|
<a
|
|
key={i}
|
|
href={`/projects?query=${encodeURIComponent(tag)}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs bg-muted text-foreground rounded px-2 py-0.5 hover:bg-accent hover:text-background transition-colors"
|
|
title={`Search for ${tag}`}
|
|
>
|
|
{tag}
|
|
</a>
|
|
))
|
|
) : (
|
|
<span className="text-xs text-muted-foreground">No tags</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Optional long description if you add one later */}
|
|
{project.body ? (
|
|
<div className="prose dark:prose-invert mt-6">
|
|
<ReactMarkdown>{project.body}</ReactMarkdown>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|