- Product pages (Code Editor, Robot Simulator): text-left/image-right hero layout - Related resources capped at 3 items on all product pages - Making Music activities renamed to I, II, III - New Maze I/II/III and Sumo I/II/III difficulty-graded activities - YouTube demo videos restored on 12 activity pages from old site - Activity pages: two-column hero with coding skills & rover concepts tags - Blog/news pages: same two-column hero layout with date - Resource type extended with codingSkills, roverConcepts, tags fields - Removed raw "Relevant Coding Skills/Rover Concepts" text from activity MDX Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
317 lines
10 KiB
TypeScript
317 lines
10 KiB
TypeScript
import { notFound } from "next/navigation";
|
|
import Image from "next/image";
|
|
import { Metadata } from "next";
|
|
import { MDXRemote } from "next-mdx-remote/rsc";
|
|
import { Container } from "@/components/layout/Container";
|
|
import { Button } from "@/components/ui/Button";
|
|
import { getAllResources, getResourceBySlug } from "@/lib/resources";
|
|
import { ResourcesGrid } from "../resources-client";
|
|
|
|
function MdxLink(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
|
|
const { href, children, ...rest } = props;
|
|
const isExternal = href?.startsWith("http");
|
|
return (
|
|
<a
|
|
href={href}
|
|
{...(isExternal ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
{...rest}
|
|
>
|
|
{children}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
const mdxComponents = {
|
|
a: MdxLink,
|
|
};
|
|
|
|
const NEWS_CATEGORIES = ["News & Updates", "Customer Stories"];
|
|
const ACTIVITY_CATEGORIES = ["Activities", "Simulator Activities"];
|
|
|
|
const CATEGORY_SLUGS: Record<string, {
|
|
name: string;
|
|
description: string;
|
|
matchCategories?: string[];
|
|
filterCategories?: string[];
|
|
}> = {
|
|
"getting-started": {
|
|
name: "Getting Started",
|
|
description: "Everything you need to get up and running with the Micromelon platform.",
|
|
},
|
|
activities: {
|
|
name: "Activities",
|
|
description: "Hands-on activities for the Micromelon Rover and Robot Simulator.",
|
|
matchCategories: ["Activities", "Simulator Activities"],
|
|
filterCategories: ["Activities", "Simulator Activities"],
|
|
},
|
|
"sensor-guides": {
|
|
name: "Sensor Guides",
|
|
description: "Learn how each sensor on the Micromelon Rover works and how to code it.",
|
|
},
|
|
"3d-printing-guides": {
|
|
name: "3D Printing Guides",
|
|
description: "Guides for designing, printing, and troubleshooting rover attachments.",
|
|
},
|
|
"advanced-guides": {
|
|
name: "Advanced Guides",
|
|
description: "I2C, UART, OpenMV, and other advanced topics for experienced users.",
|
|
},
|
|
"build-guides": {
|
|
name: "Build Guides",
|
|
description: "Step-by-step instructions for building rover attachments.",
|
|
},
|
|
"design-blogs": {
|
|
name: "Design Blogs",
|
|
description: "Read about the design process behind rover attachments.",
|
|
},
|
|
};
|
|
|
|
interface ResourcePageProps {
|
|
params: Promise<{ slug: string }>;
|
|
}
|
|
|
|
export function generateStaticParams() {
|
|
const resources = getAllResources();
|
|
const resourceParams = resources.map((resource) => ({
|
|
slug: resource.slug,
|
|
}));
|
|
const categoryParams = Object.keys(CATEGORY_SLUGS).map((slug) => ({
|
|
slug,
|
|
}));
|
|
return [...resourceParams, ...categoryParams];
|
|
}
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: ResourcePageProps): Promise<Metadata> {
|
|
const { slug } = await params;
|
|
|
|
const category = CATEGORY_SLUGS[slug];
|
|
if (category) {
|
|
return {
|
|
title: `${category.name} - Resources`,
|
|
description: category.description,
|
|
};
|
|
}
|
|
|
|
const resource = getResourceBySlug(slug);
|
|
if (!resource) return {};
|
|
return {
|
|
title: resource.title,
|
|
description: resource.excerpt,
|
|
};
|
|
}
|
|
|
|
export default async function ResourcePage({ params }: ResourcePageProps) {
|
|
const { slug } = await params;
|
|
|
|
/* Category page */
|
|
const category = CATEGORY_SLUGS[slug];
|
|
if (category) {
|
|
const cats = category.matchCategories || [category.name];
|
|
const resources = getAllResources()
|
|
.filter((r) => r.categories.some((c) => cats.includes(c)))
|
|
.map(({ content, ...meta }) => meta);
|
|
|
|
return (
|
|
<>
|
|
<section className="bg-white pt-16 sm:pt-20">
|
|
<Container>
|
|
<Button href="/resources" variant="outline" size="sm" className="mb-8">
|
|
← All Resources
|
|
</Button>
|
|
</Container>
|
|
</section>
|
|
<ResourcesGrid
|
|
resources={resources}
|
|
title={category.name}
|
|
subtitle={category.description}
|
|
filterCategories={category.filterCategories}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
/* Individual resource page */
|
|
const resource = getResourceBySlug(slug);
|
|
|
|
if (!resource) {
|
|
notFound();
|
|
}
|
|
|
|
const isNews = resource.categories.some((cat) =>
|
|
NEWS_CATEGORIES.includes(cat)
|
|
);
|
|
const isActivity = resource.categories.some((cat) =>
|
|
ACTIVITY_CATEGORIES.includes(cat)
|
|
);
|
|
|
|
if (isActivity) {
|
|
return (
|
|
<>
|
|
{/* Activity Hero */}
|
|
<section className="bg-white py-16 sm:py-24">
|
|
<Container>
|
|
<Button
|
|
href="/resources"
|
|
variant="outline"
|
|
size="sm"
|
|
className="mb-8"
|
|
>
|
|
← Back to Resources
|
|
</Button>
|
|
<div className="grid items-center gap-12 md:grid-cols-2">
|
|
<div>
|
|
<div className="mb-3 flex flex-wrap gap-1.5">
|
|
{resource.categories.map((cat) => (
|
|
<span
|
|
key={cat}
|
|
className="rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium text-muted-foreground"
|
|
>
|
|
{cat}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<h1 className="text-4xl font-bold tracking-tight text-foreground sm:text-5xl">
|
|
{resource.title}
|
|
</h1>
|
|
{(resource.codingSkills.length > 0 || resource.roverConcepts.length > 0) && (
|
|
<div className="mt-6 flex flex-wrap gap-x-8 gap-y-4">
|
|
{resource.codingSkills.length > 0 && (
|
|
<div>
|
|
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
Coding Skills
|
|
</p>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{resource.codingSkills.map((skill) => (
|
|
<span
|
|
key={skill}
|
|
className="rounded-full border border-brand/30 bg-brand/5 px-2.5 py-0.5 text-xs font-medium text-foreground"
|
|
>
|
|
{skill}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{resource.roverConcepts.length > 0 && (
|
|
<div>
|
|
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
Rover Concepts
|
|
</p>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{resource.roverConcepts.map((concept) => (
|
|
<span
|
|
key={concept}
|
|
className="rounded-full border border-foreground/20 bg-foreground/5 px-2.5 py-0.5 text-xs font-medium text-foreground"
|
|
>
|
|
{concept}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{resource.featuredImage && (
|
|
<div>
|
|
<Image
|
|
src={resource.featuredImage}
|
|
alt={resource.title}
|
|
width={700}
|
|
height={700}
|
|
className="w-full rounded-2xl"
|
|
priority
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
|
|
{/* Activity Content */}
|
|
<section className="border-t border-border bg-muted py-16">
|
|
<Container className="max-w-3xl">
|
|
<article className="mdx-content">
|
|
<MDXRemote source={resource.content} components={mdxComponents} />
|
|
</article>
|
|
<div className="mt-12 border-t border-border pt-8">
|
|
<Button href="/resources" variant="outline" size="sm">
|
|
← Return to Resources
|
|
</Button>
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const backHref = isNews ? "/news" : "/resources";
|
|
const backLabel = isNews ? "Back to News" : "Back to Resources";
|
|
const returnLabel = isNews ? "Return to News" : "Return to Resources";
|
|
|
|
return (
|
|
<>
|
|
{/* Hero */}
|
|
<section className="bg-white py-16 sm:py-24">
|
|
<Container>
|
|
<Button href={backHref} variant="outline" size="sm" className="mb-8">
|
|
← {backLabel}
|
|
</Button>
|
|
<div className="grid items-center gap-12 md:grid-cols-2">
|
|
<div>
|
|
<div className="mb-3 flex flex-wrap gap-1.5">
|
|
{resource.categories.map((cat) => (
|
|
<span
|
|
key={cat}
|
|
className="rounded-full bg-muted px-2.5 py-0.5 text-xs font-medium text-muted-foreground"
|
|
>
|
|
{cat}
|
|
</span>
|
|
))}
|
|
</div>
|
|
<h1 className="text-4xl font-bold tracking-tight text-foreground sm:text-5xl">
|
|
{resource.title}
|
|
</h1>
|
|
<p className="mt-3 text-muted-foreground">
|
|
{new Date(resource.date).toLocaleDateString("en-AU", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
})}
|
|
</p>
|
|
</div>
|
|
{resource.featuredImage && (
|
|
<div>
|
|
<Image
|
|
src={resource.featuredImage}
|
|
alt={resource.title}
|
|
width={700}
|
|
height={467}
|
|
className="w-full rounded-2xl"
|
|
priority
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
|
|
{/* Content */}
|
|
<section className="border-t border-border bg-muted py-16">
|
|
<Container className="max-w-3xl">
|
|
<article className="mdx-content">
|
|
<MDXRemote source={resource.content} components={mdxComponents} />
|
|
</article>
|
|
<div className="mt-12 border-t border-border pt-8">
|
|
<Button href={backHref} variant="outline" size="sm">
|
|
← {returnLabel}
|
|
</Button>
|
|
</div>
|
|
</Container>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|