Add Docker deployment pipeline and site updates
Some checks failed
Build and Deploy / deploy (push) Failing after -2m4s

- Dockerfile (multi-stage Next.js standalone build)
- docker-compose.yml for Portainer stack
- Gitea Actions workflow for CI/CD
- Runner container config (Dockerfile.runner + compose)
- next.config.ts: enable standalone output
- Site content and image updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tim Hadwen
2026-03-06 20:29:02 +10:00
parent 1f8c46597b
commit 9065c5bf08
44 changed files with 1809 additions and 586 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
node_modules
.next
.git
.env*
.DS_Store
Dockerfile.runner
docker-compose.runner.yml
runner-data

View File

@@ -0,0 +1,24 @@
name: Build and Deploy
on:
push:
branches:
- main
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build Docker image
run: |
SHA=$(echo "${{ gitea.sha }}" | cut -c1-7)
docker build -t micromelon-website:$SHA -t micromelon-website:latest .
- name: Restart container
run: docker compose -f /opt/micromelon/docker-compose.yml up -d
- name: Prune old images
run: docker image prune -f

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM node:22-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM node:22-bookworm-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:22-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

2
Dockerfile.runner Normal file
View File

@@ -0,0 +1,2 @@
FROM gitea/act_runner:latest
RUN apk add --no-cache docker-cli docker-cli-compose git bash nodejs

View File

@@ -0,0 +1,61 @@
---
title: "Introducing the Micromelon VS Code Extension"
date: "2026-03-04"
categories: ["News & Updates"]
tags: ["Python", "VS Code", "Advanced"]
excerpt: "Connect to your rover, run Python scripts, and view live sensor data — all without leaving VS Code."
featuredImage: "/images/products/vscode-extension-sidebar.png"
---
We're excited to announce the release of the **Micromelon VS Code Extension** — a free extension that brings full rover control into the world's most popular code editor.
Whether your students are ready to graduate from the Micromelon Code Editor or you're a developer looking to integrate rover control into a bigger project, the VS Code extension makes it easy to connect, code, and run — all from one place.
## What It Does
The extension adds a dedicated Micromelon panel to VS Code's sidebar. From there, students can:
- **Connect to any rover** over Bluetooth Low Energy with a single click
- **Run Python scripts** on the rover instantly with F5
- **View live sensor data** — ultrasonic, colour, IR, accelerometer, and gyroscope — updated in real time
- **Use built-in code snippets** — type `mm-` to access ready-made templates for motors, sensors, LEDs, and sounds
It works with both physical Micromelon Rovers and the Robot Simulator, so students can test and iterate without hardware.
## Why VS Code?
VS Code is the most widely used code editor in the world, and it's free. Many schools already have it installed. By meeting students where they are, the extension removes friction and lets them focus on writing Python — not learning a new tool.
It's also the natural next step in the Micromelon learning pathway. Students who started with simplified blocks in Junior, progressed through the Code Editor's block and text modes, and are now ready for a professional development environment can make the jump without losing access to their rover.
## Getting Started
1. Install [VS Code](https://code.visualstudio.com/) (free)
2. Search for **Micromelon** in the Extensions marketplace and install
3. Install the Python library: `pip install micromelon`
4. Click the Micromelon icon in the sidebar and connect to your rover
5. Write your first script and hit F5
That's it — students can go from install to running code on a real robot in minutes.
## Built-In Code Snippets
The extension includes snippets for common tasks so students don't have to memorise the API. Type `mm-` and VS Code will suggest completions like:
- `mm-connect` — Connect to a rover by ID
- `mm-motors` — Drive forward, backward, and turn
- `mm-sensors` — Read ultrasonic, colour, and IR sensors
- `mm-leds` — Set LED colours
- `mm-sounds` — Play notes and melodies
Each snippet includes comments explaining what the code does, making them useful as learning tools as well as shortcuts.
## Works With the Simulator
Don't have a physical rover handy? The extension connects to the Micromelon Robot Simulator just as easily. Students can develop and test their code in the simulator, then deploy the exact same script to a real rover when they're ready.
## Download
The Micromelon VS Code Extension is available now for free on the [VS Code Marketplace](/download). It works on Windows and macOS.
Ready to try it? Head to the [Python page](/python) to learn more, or [download everything you need](/download) to get started.

View File

@@ -0,0 +1,51 @@
---
title: "Rover Repairs Made Easy With Online Return Requests"
date: "2026-03-04"
categories: ["News & Updates"]
tags: []
excerpt: "Submit a repair request online, get approved in under an hour, and receive everything you need to send your rover back for diagnosis and repair."
featuredImage: "/images/products/rover-render.jpg"
---
We know that when a rover goes down in a busy classroom, you need it fixed fast. That's why we've made the repair process as simple as possible — no phone calls, no back-and-forth emails. Just a quick online form and we handle the rest.
## How It Works
The entire process starts from our [Rover Repair Request](/rover-repair-request) page. Here's what to expect:
### 1. Submit the Form
Head to the [Rover Repair Request](/rover-repair-request) page and fill in your details — your name, school, contact info, and which rovers need attention. You can describe the issue and submit multiple rovers in a single request.
The form takes less than two minutes to complete.
### 2. Fast Approval
Our team reviews every request and typically approves them **within one hour** during business hours. There's no waiting days for a support ticket to be picked up.
### 3. RMA Form & Instructions
Once approved, you'll receive an email with a PDF containing your **RMA (Return Merchandise Authorisation) form** and step-by-step instructions for packaging and shipping your rover back to us.
Everything you need is in that one email — no hunting through support portals or knowledge bases.
### 4. Diagnosis & Repair
When we receive your rover, our team will diagnose the issue and carry out the repair. We'll keep you updated throughout the process and get your rover back to you as quickly as possible.
## Why We Built This
Teachers are busy. The last thing you need when a rover stops working is to navigate a complicated support process. We wanted something that respects your time:
- **No phone queues** — submit a request whenever it suits you
- **No waiting** — approvals typically happen within an hour
- **One email** — everything you need arrives in a single PDF
- **Transparent** — you always know where your rover is in the process
## Covered Under Warranty
Every Micromelon Rover comes with a **12-month warranty** covering manufacturing defects. If your rover is within warranty, repairs are covered at no cost. For out-of-warranty rovers, we'll provide a quote before proceeding with any work.
## Get Started
If you have a rover that needs attention, head to the [Rover Repair Request](/rover-repair-request) page and submit your request. We'll take it from there.

18
docker-compose.runner.yml Normal file
View File

@@ -0,0 +1,18 @@
services:
runner:
build:
context: .
dockerfile: Dockerfile.runner
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- /opt/micromelon:/opt/micromelon
- runner-data:/data
environment:
GITEA_INSTANCE_URL: ${GITEA_URL}
GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN}
GITEA_RUNNER_NAME: micromelon-runner
GITEA_RUNNER_LABELS: "self-hosted:host"
volumes:
runner-data:

10
docker-compose.yml Normal file
View File

@@ -0,0 +1,10 @@
services:
web:
image: micromelon-website:latest
container_name: micromelon-web
restart: unless-stopped
ports:
- "3000:3000"
environment:
- AIRTABLE_API_KEY=${AIRTABLE_API_KEY}
- AIRTABLE_BASE_ID=${AIRTABLE_BASE_ID}

View File

@@ -1,7 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: "standalone",
};
export default nextConfig;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2676.54 425.2"><defs><style>.cls-1{fill:#1a1a1a;}.cls-2{fill:#ffdb00;fill-rule:evenodd;}</style></defs><path class="cls-1" d="M721.93,369.49q-34.87,0-60.76-17.93t-40.08-48.05Q606.9,273.39,606.9,237q0-37.35,16.18-67.72a120,120,0,0,1,46.56-48.06q30.38-17.67,72.7-17.67,42.82,0,72.21,17.92a118.62,118.62,0,0,1,44.82,48.31Q874.79,200.19,874.8,237V363.52H806.58V321.19h-1a106.24,106.24,0,0,1-18.68,24.4A83,83,0,0,1,759.77,363Q743.83,369.49,721.93,369.49Zm19.42-58.26q19.9,0,34.36-10a64.11,64.11,0,0,0,22.16-27.14A92,92,0,0,0,805.59,236q0-20.92-7.72-37.6a64.69,64.69,0,0,0-22.16-26.64q-14.46-10-34.36-10-20.43,0-35.11,10a63.83,63.83,0,0,0-22.41,26.64q-7.73,16.69-7.72,37.6a91.82,91.82,0,0,0,7.72,38.09,63.27,63.27,0,0,0,22.41,27.14Q720.93,311.23,741.35,311.23Z" transform="translate(-79.06 0)"/><path class="cls-1" d="M939.53,363.52V0h68.22V206.66h40.34l61.25-97.11H1184l-69.71,107.56q32.36,13.45,50.29,41.33t17.93,62.25v42.83h-68.22V320.69a53.14,53.14,0,0,0-7.72-28.13,58.64,58.64,0,0,0-20.17-20.17,53.17,53.17,0,0,0-27.89-7.47h-50.79v98.6Z" transform="translate(-79.06 0)"/><path class="cls-1" d="M1365.29,363.52q-43.32,0-75.44-16.69t-49.8-45.56q-17.68-28.87-17.68-64.74,0-40.34,16.44-70.21T1283.62,120q28.38-16.44,64.24-16.43,42.83,0,69.47,17.92a104.94,104.94,0,0,1,39.09,48.06q12.43,30.13,12.45,67,0,5-.5,11.71a54,54,0,0,1-1.5,10.2h-171.8a59,59,0,0,0,13.7,25.9,60.06,60.06,0,0,0,24.4,15.68q14.68,5.24,33.11,5.23H1437v58.27Zm-70.71-150.89h106.06a95.13,95.13,0,0,0-3.48-17.68,52.57,52.57,0,0,0-7-14.69,47.26,47.26,0,0,0-10.46-10.95,45.79,45.79,0,0,0-13.94-7,58.27,58.27,0,0,0-17.43-2.49,52.27,52.27,0,0,0-22.16,4.48A47.23,47.23,0,0,0,1310,176.28a58.55,58.55,0,0,0-10.2,16.93A88.17,88.17,0,0,0,1294.58,212.63Z" transform="translate(-79.06 0)"/><path class="cls-1" d="M1514.68,363.52V181.76q0-33.85,19.17-53t53-19.17h70.22v58.26h-58.27a15.72,15.72,0,0,0-11.2,4.49,15,15,0,0,0-4.73,11.45V363.52Z" transform="translate(-79.06 0)"/><path class="cls-1" d="M1699.91,363.52V14.94h47.81V164.83h183.75V14.94h47.8V363.52h-47.8V207.15H1747.72V363.52Z" transform="translate(-79.06 0)"/><path class="cls-1" d="M2182.44,363.52q-41.82,0-74.2-15.69t-50.54-44.32q-18.18-28.63-18.17-67,0-37.83,15.43-68T2097.79,121q27.38-17.43,63.74-17.43,39.33,0,65.48,16.93a104.49,104.49,0,0,1,39.09,45.56q12.95,28.65,13,64c0,3.66-.09,7.47-.25,11.45a60,60,0,0,1-1.25,10.46H2088.33q2.48,23.41,15.43,39.59a80.4,80.4,0,0,0,33.12,24.4q20.16,8.22,44.57,8.22h66.22v39.34ZM2087.83,219.6h144.91a110.37,110.37,0,0,0-2-20.16,81.72,81.72,0,0,0-7-20.92,68.08,68.08,0,0,0-13.19-18.42,61.14,61.14,0,0,0-20.42-13.2q-12.21-5-28.63-5-17.45,0-30.63,6.72a72.28,72.28,0,0,0-22.66,17.93,80.77,80.77,0,0,0-14.44,25.15A104.65,104.65,0,0,0,2087.83,219.6Z" transform="translate(-79.06 0)"/><path class="cls-1" d="M2335.81,363.52V181.76q0-33.85,19.17-53t53-19.17h52.28v39.34h-43.82q-16.42,0-25.64,9.46t-9.21,26.4V363.52Z" transform="translate(-79.06 0)"/><path class="cls-1" d="M2622.14,369.49q-38.85,0-68.72-17.68a130.57,130.57,0,0,1-47.31-47.8q-17.43-30.12-17.43-67.48t17.43-67.47a130.75,130.75,0,0,1,47.31-47.81q29.86-17.67,68.72-17.67t69,17.67a128.2,128.2,0,0,1,47.31,47.81q17.19,30.13,17.18,67.47T2738.41,304a128,128,0,0,1-47.31,47.8Q2661,369.48,2622.14,369.49Zm.49-39.34q25.89,0,45.32-12.45a86.12,86.12,0,0,0,30.38-33.61q10.93-21.16,11-47.56t-11-47.55A86.19,86.19,0,0,0,2668,155.37q-19.42-12.45-45.32-12.45-26.4,0-46.06,12.45A85.34,85.34,0,0,0,2546,189q-11,21.17-11,47.55t11,47.56a85.27,85.27,0,0,0,30.62,33.61Q2596.23,330.15,2622.63,330.15Z" transform="translate(-79.06 0)"/><path class="cls-1" d="M559.6,113q0-28.87-13-52.29a100.61,100.61,0,0,0-36.11-37.59Q487.4,9,456,9q-30.88,0-54.53,14.2a99.38,99.38,0,0,0-36.6,37.59q-13,23.41-12.95,52.29V273.38a31.33,31.33,0,0,1-4.48,16.44,34.36,34.36,0,0,1-11.95,11.95,31.33,31.33,0,0,1-16.43,4.48,30,30,0,0,1-16.19-4.48,35,35,0,0,1-11.7-11.95,31.33,31.33,0,0,1-4.48-16.44V113q0-28.87-12.95-52.29a100,100,0,0,0-36.35-37.59Q214,9,182.63,9q-30.88,0-54.28,14.2A99.89,99.89,0,0,0,92,60.75Q79.06,84.17,79.06,113V363.52h71.2V104.57a31.33,31.33,0,0,1,4.49-16.43,35,35,0,0,1,11.7-12,30,30,0,0,1,16.18-4.48,31.27,31.27,0,0,1,16.43,4.48,34.3,34.3,0,0,1,12,12,31.42,31.42,0,0,1,4.48,16.43V264.92q0,30.38,13.44,53.78A97.69,97.69,0,0,0,266,355.55Q289.7,369,319.08,369q29.87,0,53.28-13.44a100.13,100.13,0,0,0,37.1-36.85q13.68-23.4,13.69-53.78V104.57a31.33,31.33,0,0,1,4.48-16.43,34.36,34.36,0,0,1,11.95-12,32.39,32.39,0,0,1,32.87,0,32.31,32.31,0,0,1,11.7,12,32.78,32.78,0,0,1,4.23,16.43V237.25H559.6Z" transform="translate(-79.06 0)"/><polygon class="cls-2" points="516.18 363.5 480.56 425.2 409.3 425.2 373.69 363.5 409.3 301.83 480.56 301.83 516.18 363.5"/></svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 KiB

View File

@@ -63,7 +63,7 @@ export default function AboutUsPage() {
</section>
{/* Founders */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<SectionHeading title="Meet the Founders" />
<div className="mx-auto grid max-w-4xl gap-12 md:grid-cols-2">
@@ -116,7 +116,7 @@ export default function AboutUsPage() {
</section>
{/* Schools */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<SectionHeading
title="Over 120 Schools Using Micromelon"

View File

@@ -3,8 +3,7 @@ import Image from "next/image";
import { Container } from "@/components/layout/Container";
import { Button } from "@/components/ui/Button";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { RelatedResources } from "@/components/ui/RelatedResources";
import { getAllResources } from "@/lib/resources";
import { LearningPathway } from "@/components/ui/LearningPathway";
export const metadata: Metadata = {
title: "Code Editor & Classroom Organizer",
@@ -13,14 +12,6 @@ export const metadata: Metadata = {
};
export default function CodeEditorPage() {
const resources = getAllResources();
const relatedResources = resources
.filter((r) =>
r.categories.includes("Getting Started") ||
r.categories.includes("Guides")
)
.slice(0, 3);
return (
<>
{/* Hero */}
@@ -59,7 +50,7 @@ export default function CodeEditorPage() {
</section>
{/* Three Coding Modes */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<SectionHeading
title="Three Ways to Code"
@@ -165,7 +156,7 @@ export default function CodeEditorPage() {
</section>
{/* Classroom Management */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<SectionHeading
title="Classroom Management"
@@ -299,7 +290,7 @@ export default function CodeEditorPage() {
</section>
{/* System Requirements */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-2xl font-bold tracking-tight text-foreground">
@@ -312,11 +303,10 @@ export default function CodeEditorPage() {
</Container>
</section>
{/* Related Resources */}
<RelatedResources
resources={relatedResources}
seeAllHref="/resources/guides"
seeAllLabel="See all guides"
{/* Learning Pathway */}
<LearningPathway
activeStep={2}
subtitle="Students start with simplified blocks in Junior, progress to the Code Editor's block and text modes, then advance to professional Python."
/>
{/* CTA */}

View File

@@ -3,8 +3,6 @@ import Image from "next/image";
import { Container } from "@/components/layout/Container";
import { Button } from "@/components/ui/Button";
import { LearningPathway } from "@/components/ui/LearningPathway";
import { RelatedResources } from "@/components/ui/RelatedResources";
import { getAllResources } from "@/lib/resources";
export const metadata: Metadata = {
title: "Micromelon Junior",
@@ -12,12 +10,30 @@ export const metadata: Metadata = {
"A simplified coding app for young learners. Teach the basics of computational thinking with an easy-to-use interface on iPad and Android.",
};
export default function JuniorPage() {
const resources = getAllResources();
const relatedResources = resources
.filter((r) => r.categories.includes("Getting Started"))
.slice(0, 4);
const features = [
{
title: "Simplified Interface",
description:
"A pared-back design so students can focus on learning concepts rather than navigating menus.",
},
{
title: "Tablet-First",
description:
"Available on iPad and Android. Perfect for schools using tablets in the classroom.",
},
{
title: "Block-Based Coding",
description:
"Drag-and-drop blocks to build programs. No typing required, making it accessible for early primary students.",
},
{
title: "Works with the Rover",
description:
"Connect to a real Micromelon Rover and see programs come to life, the same rover students will use as they progress.",
},
];
export default function JuniorPage() {
return (
<>
{/* Hero */}
@@ -34,22 +50,19 @@ export default function JuniorPage() {
computational thinking with a friendly, easy-to-navigate
interface.
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<div className="mt-8">
<Button href="/download" variant="primary">
Download
</Button>
<Button href="/code-editor" variant="outline">
See the Code Editor
</Button>
</div>
</div>
<div className="flex justify-center">
<Image
src="/images/products/rover-render.jpg"
alt="Micromelon Rover used with Junior app"
width={600}
height={400}
className="w-full rounded-2xl shadow-lg"
src="/images/products/junior-hero.png"
alt="Micromelon Junior app interface showing block-based coding"
width={2326}
height={1856}
className="w-full"
priority
/>
</div>
@@ -58,9 +71,19 @@ export default function JuniorPage() {
</section>
{/* What is Junior */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<div className="mx-auto max-w-3xl">
<div className="grid items-center gap-12 md:grid-cols-2">
<div className="flex justify-center">
<Image
src="/images/products/junior-icon.png"
alt="Micromelon Junior app icon"
width={240}
height={240}
className="rounded-[54px] shadow-lg"
/>
</div>
<div>
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
Built for Younger Learners
</h2>
@@ -70,45 +93,61 @@ export default function JuniorPage() {
decision-making, without the complexity of a full coding
environment.
</p>
<div className="mt-10 grid gap-6 sm:grid-cols-2">
<div className="rounded-xl border border-border bg-white p-6">
</div>
</div>
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{features.map((feature) => (
<div key={feature.title} className="rounded-xl border border-border bg-white p-6">
<h3 className="text-lg font-semibold text-foreground">
Simplified Interface
{feature.title}
</h3>
<p className="mt-2 text-sm text-foreground-light">
A pared-back design so students can focus on learning concepts
rather than navigating menus.
{feature.description}
</p>
</div>
<div className="rounded-xl border border-border bg-white p-6">
<h3 className="text-lg font-semibold text-foreground">
Tablet-First
</h3>
<p className="mt-2 text-sm text-foreground-light">
Available on iPad and Android. Perfect for schools using
tablets in the classroom.
))}
</div>
</Container>
</section>
{/* Screenshots */}
<section className="bg-white py-20">
<Container>
<div className="mx-auto max-w-3xl text-center">
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
See It in Action
</h2>
<p className="mt-4 text-lg text-foreground-light">
A clean block-based interface for building programs, and a live
Rover View showing real-time sensor data.
</p>
</div>
<div className="rounded-xl border border-border bg-white p-6">
<h3 className="text-lg font-semibold text-foreground">
Block-Based Coding
</h3>
<p className="mt-2 text-sm text-foreground-light">
Drag-and-drop blocks to build programs. No typing required,
making it accessible for early primary students.
<div className="mt-12 grid gap-8 md:grid-cols-2">
<div>
<Image
src="/images/products/junior-blocks-interface.png"
alt="Micromelon Junior block programming interface with movement and sensor blocks"
width={2326}
height={1856}
className="w-full"
/>
<p className="mt-4 text-center text-sm font-medium text-foreground-light">
Drag-and-drop blocks to build programs
</p>
</div>
<div className="rounded-xl border border-border bg-white p-6">
<h3 className="text-lg font-semibold text-foreground">
Works with the Rover
</h3>
<p className="mt-2 text-sm text-foreground-light">
Connect to a real Micromelon Rover and see programs come to
life, the same rover students will use as they progress.
<div>
<Image
src="/images/products/junior-rover-view.png"
alt="Micromelon Junior Rover View showing ultrasonic, IR, and colour sensor readings"
width={2326}
height={1856}
className="w-full"
/>
<p className="mt-4 text-center text-sm font-medium text-foreground-light">
Live sensor data from the connected rover
</p>
</div>
</div>
</div>
</Container>
</section>
@@ -117,33 +156,7 @@ export default function JuniorPage() {
title="From Blocks to Python"
subtitle="Junior is the starting point. When students are ready, they move to the full Code Editor with blocks, mixed mode, and Python, all using the same Micromelon Rover."
activeStep={1}
/>
{/* Available On */}
<section className="border-t border-border bg-muted py-20">
<Container>
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-2xl font-bold tracking-tight text-foreground">
Available On
</h2>
<p className="mt-4 text-foreground-light">
iPad and Android tablets. Download from the App Store or Google
Play.
</p>
<div className="mt-8">
<Button href="/download" variant="primary">
Go to Downloads
</Button>
</div>
</div>
</Container>
</section>
{/* Related Resources */}
<RelatedResources
resources={relatedResources}
seeAllHref="/resources"
seeAllLabel="See all resources"
className="bg-muted"
/>
{/* CTA */}

View File

@@ -56,7 +56,7 @@ export default function HomePage() {
</section>
{/* Learning Pathway */}
<LearningPathway className="border-t border-border bg-muted" />
<LearningPathway className="bg-muted" animate />
{/* Product Cards */}
<section className="py-20">
@@ -87,7 +87,7 @@ export default function HomePage() {
<div className="group overflow-hidden rounded-2xl border border-border bg-white shadow-sm transition-shadow hover:shadow-lg">
<div className="relative aspect-[4/3] overflow-hidden">
<Image
src="/images/products/rover-render.jpg"
src="/images/products/junior-hero.png"
alt="Micromelon Junior"
fill
className="object-cover transition-transform group-hover:scale-105"
@@ -152,7 +152,7 @@ export default function HomePage() {
</section>
{/* Schools */}
<section className="border-t border-border bg-muted py-16">
<section className="bg-muted py-16">
<Container>
<SectionHeading
title="Over 120 Schools Using Micromelon"
@@ -168,6 +168,9 @@ export default function HomePage() {
</p>
))}
</div>
<p className="mt-6 text-center text-sm font-medium text-foreground-light">
and many more...
</p>
</Container>
</section>

View File

@@ -31,7 +31,7 @@ export default function PlatformPage() {
</section>
{/* Micromelon Rover */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<div className="grid items-center gap-12 md:grid-cols-2">
<div className="flex justify-center">
@@ -70,16 +70,16 @@ export default function PlatformPage() {
<LearningPathway className="bg-white" />
{/* Junior */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<div className="grid items-center gap-12 md:grid-cols-2">
<div className="flex justify-center">
<Image
src="/images/products/rover-render.jpg"
alt="Micromelon Junior app"
width={560}
height={400}
className="w-full max-w-lg rounded-2xl"
src="/images/products/junior-hero.png"
alt="Micromelon Junior app interface showing block-based coding"
width={2326}
height={1856}
className="w-full max-w-lg rounded-2xl shadow-lg"
/>
</div>
<div>
@@ -159,7 +159,7 @@ export default function PlatformPage() {
</section>
{/* Python Library */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<div className="grid items-center gap-12 md:grid-cols-2">
<div className="flex justify-center">

15
src/app/python/layout.tsx Normal file
View File

@@ -0,0 +1,15 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Micromelon Python Library",
description:
"A dedicated Python module for connecting and controlling Micromelon Rovers and simulated rovers. Perfect for senior students and advanced projects.",
};
export default function PythonLayout({
children,
}: {
children: React.ReactNode;
}) {
return children;
}

File diff suppressed because it is too large Load Diff

View File

@@ -231,12 +231,12 @@ export default async function ResourcePage({ params }: ResourcePageProps) {
</section>
{/* Activity Content */}
<section className="border-t border-border bg-muted py-16">
<section className="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">
<div className="mt-12 pt-8">
<Button href="/resources" variant="outline" size="sm">
&larr; Return to Resources
</Button>
@@ -299,12 +299,12 @@ export default async function ResourcePage({ params }: ResourcePageProps) {
</section>
{/* Content */}
<section className="border-t border-border bg-muted py-16">
<section className="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">
<div className="mt-12 pt-8">
<Button href={backHref} variant="outline" size="sm">
&larr; {returnLabel}
</Button>

View File

@@ -3,8 +3,7 @@ import Image from "next/image";
import { Container } from "@/components/layout/Container";
import { Button } from "@/components/ui/Button";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { RelatedResources } from "@/components/ui/RelatedResources";
import { getAllResources } from "@/lib/resources";
import { LearningPathway } from "@/components/ui/LearningPathway";
export const metadata: Metadata = {
title: "The Robot Simulator",
@@ -13,50 +12,41 @@ export const metadata: Metadata = {
};
export default function RobotSimulatorPage() {
const resources = getAllResources();
const relatedResources = resources
.filter((r) => r.categories.includes("Simulator Activities"))
.slice(0, 3);
return (
<>
{/* Hero */}
<section className="bg-white py-16 sm:py-24">
<section className="relative min-h-[80vh] overflow-hidden">
<Image
src="/images/products/simulator-demo.gif"
alt="Simulator demo showing a virtual rover navigating an exercise"
fill
className="object-cover object-bottom"
unoptimized
priority
/>
<div className="absolute inset-0 bg-black/60" />
<div className="relative z-10 flex min-h-[80vh] items-center py-16 sm:py-24">
<Container>
<div className="grid items-center gap-12 md:grid-cols-2">
<div>
<h1 className="text-4xl font-bold tracking-tight text-foreground sm:text-5xl">
The Robot Simulator
<div className="mx-auto max-w-2xl text-center">
<h1 className="text-4xl font-bold tracking-tight text-white sm:text-5xl">
Micromelon Robot Simulator
</h1>
<p className="mt-6 text-lg text-foreground-light">
<p className="mt-6 text-lg text-white/80">
Filled with virtual exercises, the Simulator is great for homework
and running complex challenges. No physical robot needed.
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<div className="mt-8">
<Button href="/download" variant="primary">
Download
</Button>
<Button href="/resources/simulator-activities" variant="outline">
Simulator Activities
</Button>
</div>
</div>
<div>
<Image
src="/images/products/simulator-screenshot.png"
alt="Micromelon Robot Simulator showing a virtual rover in a 3D environment"
width={1500}
height={934}
className="w-full rounded-2xl border border-border shadow-xl"
priority
/>
</div>
</div>
</Container>
</div>
</section>
{/* Simulator Demo */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-white py-20">
<Container>
<div className="grid items-center gap-12 md:grid-cols-2">
<div>
@@ -83,14 +73,13 @@ export default function RobotSimulatorPage() {
</li>
</ul>
</div>
<div className="flex justify-center">
<div>
<Image
src="/images/products/simulator-demo.gif"
alt="Simulator demo showing a virtual rover navigating an exercise"
src="/images/products/simulator-screenshot.png"
alt="Micromelon Robot Simulator showing a virtual rover in a 3D environment"
width={1500}
height={494}
className="w-full rounded-2xl border border-border shadow-lg"
unoptimized
height={934}
className="w-full rounded-2xl border border-border shadow-xl"
/>
</div>
</div>
@@ -98,7 +87,7 @@ export default function RobotSimulatorPage() {
</section>
{/* Simulator Features */}
<section className="bg-white py-20">
<section className="bg-muted py-20">
<Container>
<SectionHeading
title="Built-In Exercises & Environments"
@@ -186,9 +175,10 @@ export default function RobotSimulatorPage() {
</section>
{/* Code Editor Connection */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-white py-20">
<Container>
<div className="mx-auto max-w-3xl text-center">
<div className="grid items-center gap-12 md:grid-cols-2">
<div>
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
Program With The Micromelon Code Editor
</h2>
@@ -201,11 +191,21 @@ export default function RobotSimulatorPage() {
<Button href="/code-editor">Explore the Code Editor</Button>
</div>
</div>
<div className="flex justify-center">
<Image
src="/images/content/eab6ac-simulator-on-devices.png"
alt="Micromelon software running on multiple devices"
width={560}
height={400}
className="w-full max-w-lg rounded-2xl"
/>
</div>
</div>
</Container>
</section>
{/* System Requirements */}
<section className="bg-white py-20">
<section className="bg-muted py-20">
<Container>
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-2xl font-bold tracking-tight text-foreground">
@@ -218,11 +218,11 @@ export default function RobotSimulatorPage() {
</Container>
</section>
{/* Related Resources */}
<RelatedResources
resources={relatedResources}
seeAllHref="/resources/simulator-activities"
seeAllLabel="See all Simulator Activities"
{/* Learning Pathway */}
<LearningPathway
activeSteps={[2, 3]}
subtitle="The Simulator works with the Code Editor and Python. Write the same code for a virtual rover as you would for a physical one."
/>
{/* CTA */}

View File

@@ -57,7 +57,7 @@ export default function RoverExpansion3DPrintingPage() {
</section>
{/* 3D Printing Crash Course */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<div className="mx-auto max-w-3xl text-center">
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
@@ -93,7 +93,7 @@ export default function RoverExpansion3DPrintingPage() {
</section>
{/* Build Guides */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<SectionHeading
title="Build Guides"
@@ -157,7 +157,7 @@ export default function RoverExpansion3DPrintingPage() {
</section>
{/* CAD Models */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<SectionHeading
title="CAD Models"

View File

@@ -1,314 +1,375 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { Container } from "@/components/layout/Container";
import { Button } from "@/components/ui/Button";
import { SectionHeading } from "@/components/ui/SectionHeading";
import { Card } from "@/components/ui/Card";
import { RelatedResources } from "@/components/ui/RelatedResources";
import { LearningPathway } from "@/components/ui/LearningPathway";
import { ActivityCarousel } from "@/components/ui/ActivityCarousel";
import { getAllResources } from "@/lib/resources";
import { products } from "@/data/products";
export const metadata: Metadata = {
title: "The Micromelon Rover",
description:
"Long battery life, tough, and packed with sensors to make a great classroom tool. Connect and run code in seconds.",
"The fastest way to get students coding real robots. Tough enough for every classroom.",
};
const activities = [
const capabilities = [
{
title: "Maze Solving",
description:
"Program the rover to solve the maze. Use your ultrasonic and IR sensors to detect the walls and find the path to success.",
icon: (
<svg className="h-8 w-8 text-brand" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg>
),
category: "Sensors",
items: [
{ name: "Ultrasonic Distance", description: "Measure distance to objects up to 2 m away" },
{ name: "3x Colour Sensors", description: "Detect colours and line markings on the ground" },
{ name: "2x IR Distance", description: "Short-range proximity detection on left and right" },
{ name: "3-Axis Accelerometer", description: "Sense tilt, impacts, and orientation changes" },
{ name: "3-Axis Gyroscope", description: "Track rotation and angular velocity" },
{ name: "Battery Sensor", description: "Monitor voltage and current in real time" },
],
},
{
title: "Balance Challenges",
description:
"The accelerometer and gyroscope are how the rover senses movement. Use these sensors with maths and physics concepts to teach the rover to balance.",
icon: (
<svg className="h-8 w-8 text-brand" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v17.25m0 0c-1.472 0-2.882.265-4.185.75M12 20.25c1.472 0 2.882.265 4.185.75M18.75 4.97A48.416 48.416 0 0012 4.5c-2.291 0-4.545.16-6.75.47m13.5 0c1.01.143 2.01.317 3 .52m-3-.52l2.62 10.726c.122.499-.106 1.028-.589 1.202a5.988 5.988 0 01-2.031.352 5.988 5.988 0 01-2.031-.352c-.483-.174-.711-.703-.59-1.202L18.75 4.971zm-16.5.52c.99-.203 1.99-.377 3-.52m0 0l2.62 10.726c.122.499-.106 1.028-.589 1.202a5.989 5.989 0 01-2.031.352 5.989 5.989 0 01-2.031-.352c-.483-.174-.711-.703-.59-1.202L5.25 4.971z" />
</svg>
),
category: "Motors",
items: [
{ name: "2x Motorised Tracks", description: "Differential drive for precise movement and turning" },
{ name: "2x Servo Connectors", description: "Plug in servo motors for arms, grippers, and more" },
],
},
{
title: "Driving Practice",
description:
"Design and code an algorithm that uses all of the sensors to drive a course while watching for road markings, turning signs, and other robot drivers.",
icon: (
<svg className="h-8 w-8 text-brand" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 6.75V15m6-6v8.25m.503 3.498l4.875-2.437c.381-.19.622-.58.622-1.006V4.82c0-.836-.88-1.38-1.628-1.006l-3.869 1.934c-.317.159-.69.159-1.006 0L9.503 3.252a1.125 1.125 0 00-1.006 0L3.622 5.689C3.24 5.88 3 6.27 3 6.695V19.18c0 .836.88 1.38 1.628 1.006l3.869-1.934c.317-.159.69-.159 1.006 0l4.994 2.497c.317.158.69.158 1.006 0z" />
</svg>
),
category: "Output",
items: [
{ name: "8x RGB LEDs", description: "Programmable colour LEDs for feedback and display" },
{ name: "Buzzer", description: "Play tones and melodies through code" },
],
},
{
title: "Sumo",
description:
"Use a combination of sensors to create a program to push opponents out of the arena. Raise the stakes with 3D printed attachments.",
icon: (
<svg className="h-8 w-8 text-brand" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.21 0 003 2.48z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M12 18a3.75 3.75 0 00.495-7.467 5.99 5.99 0 00-1.925 3.546 5.974 5.974 0 01-2.133-1A3.75 3.75 0 0012 18z" />
</svg>
),
},
{
title: "Line Following",
description:
"Learn the basics of branching and loops by coding the rover to use its colour sensors to detect and follow the line.",
icon: (
<svg className="h-8 w-8 text-brand" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 9h16.5m-16.5 6.75h16.5" />
</svg>
),
category: "Power & Expansion",
items: [
{ name: "3000 mAh Battery", description: "Rechargeable lithium battery built in" },
{ name: "USB-C Charging", description: "Standard USB-C port for fast, convenient recharging" },
{ name: "6 Hour Battery Life", description: "A full day of classroom use on a single charge" },
{ name: "2 Hour Charge Time", description: "Quick turnaround between classes" },
{ name: "Bluetooth Connectivity", description: "Wireless connection to tablets, laptops, and desktops" },
{ name: "Universal Header", description: "Connect Raspberry Pi, Arduino, and 3rd party electronics" },
{ name: "3D Print Mount", description: "Clip-on expansion points for custom student designs" },
],
},
];
const teachableConcepts = [
"Accelerometer",
"Algorithm Design",
"Branching",
"Colour Sensors",
"Functions",
"Gyroscope",
"IR Distance Sensor",
"Loops",
"Maths",
"Ultrasonic",
"Variables",
function getActivities() {
const resources = getAllResources();
return resources
.filter((r) =>
r.categories.some((c) => c === "Activities" || c === "Simulator Activities")
)
.map((r) => ({
title: r.title.replace(/^Activity:\s*/, ""),
image: r.featuredImage || "/images/resources/placeholder.png",
href: `/resources/${r.slug}`,
}))
.sort(() => Math.random() - 0.5);
}
const classroomCards = [
{
title: "No Experience Needed",
description:
"Teachers don't need coding experience to run lessons. Drag-and-drop blocks, guided activities, and classroom controls make it easy to get started.",
image: "/images/products/code-editor-drag-blocks.gif",
href: null,
unoptimized: true,
},
{
title: "Works on Any Device",
description:
"The Micromelon Code Editor runs on Windows, macOS, and iPads. Students can use whatever devices your school already has.",
image: "/images/products/code-editor-devices.png",
href: null,
unoptimized: false,
},
{
title: "Easy to Manage",
description:
"Class sets come in a hard carry case with an included charging dock. All ten robots charge at once from a single outlet.",
image: "/images/products/case.png",
href: null,
unoptimized: false,
},
{
title: "Add Your Own Electronics",
description:
"Built-in expansion headers are compatible with Raspberry Pi, Arduino, and other 3rd party sensors and electronics.",
image: "/images/products/servo-programming.jpg",
href: null,
unoptimized: false,
},
];
const techSpecs = [
"Ultrasonic Distance Sensor",
"3x Colour Sensors",
"3 Axis Accelerometer",
"2x IR Distance Sensors",
"3 Axis Gyroscope",
"2x Motorised Tracks",
"2x Servo Motor Connectors",
"Universal Expansion Header",
"Battery voltage & current sensor",
"Rechargeable Battery",
];
function SpecGroupDark({ group }: { group: typeof capabilities[number] }) {
return (
<div>
<h3 className="text-sm font-semibold uppercase tracking-wider text-brand">
{group.category}
</h3>
<dl className="mt-3 space-y-3">
{group.items.map((item) => (
<div key={item.name} className="flex items-start gap-2">
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-brand" />
<div>
<dt className="text-sm font-medium text-white">{item.name}</dt>
<dd className="text-sm text-white/50">{item.description}</dd>
</div>
</div>
))}
</dl>
</div>
);
}
export default function RoverPage() {
const resources = getAllResources();
const relatedResources = resources
.filter((r) =>
r.categories.includes("Getting Started") ||
r.categories.includes("Activities")
)
.slice(0, 3);
const activities = getActivities();
return (
<>
{/* Hero */}
<section className="bg-white py-16 sm:py-24">
<Container>
<div className="grid items-center gap-12 md:grid-cols-2">
<div>
<h1 className="text-4xl font-bold tracking-tight text-foreground sm:text-5xl">
The Micromelon Rover
</h1>
<p className="mt-6 text-lg text-foreground-light">
Long battery life, tough, and packed with sensors to make a
great classroom tool. Connect and run code in seconds.
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Button href="/store" variant="primary">
Build Your Kit
</Button>
<Button href="/download" variant="outline">
Download Code Editor
</Button>
</div>
</div>
<div>
<section className="relative min-h-[60vh] overflow-hidden">
<div className="absolute inset-0">
<Image
src="/images/hero/rover-hero.jpg"
alt="Micromelon Rover"
width={700}
height={467}
className="w-full rounded-2xl shadow-lg"
fill
className="object-cover"
priority
/>
</div>
<div className="relative z-10 flex min-h-[60vh] items-center py-16 sm:py-20">
<Container className="w-full">
<div className="max-w-md">
<h1 className="text-4xl font-bold tracking-tight text-white sm:text-5xl">
Micromelon Rover
</h1>
<p className="mt-6 text-lg font-semibold text-white">
The fastest way to get students coding real robots. Tough enough for every classroom.
</p>
<div className="mt-8">
<Button href="/store" className="bg-foreground text-white hover:bg-foreground-light">
Build Your Kit
</Button>
</div>
</div>
</Container>
</div>
</section>
{/* Activities */}
<section className="border-t border-border bg-muted py-20">
<section className="bg-white py-20">
<Container>
<SectionHeading
title="Activities"
subtitle="Explore the wide range of activities students can complete with the Micromelon Rover."
/>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{activities.map((activity) => (
<Card key={activity.title}>
<div className="mb-4">{activity.icon}</div>
<h3 className="text-lg font-semibold text-foreground">
{activity.title}
</h3>
<p className="mt-2 text-sm text-foreground-light">
{activity.description}
</p>
</Card>
))}
<ActivityCarousel activities={activities} />
<div className="mt-10 text-center">
<Button href="/resources/activities" variant="outline">
Explore All Activities
</Button>
</div>
</Container>
</section>
{/* Teachable Concepts */}
<section className="bg-white py-20">
{/* Built for the Classroom */}
<section className="bg-muted py-20">
<Container>
<SectionHeading
title="Teachable Concepts"
subtitle="The Micromelon Rover covers a wide range of digital technologies and STEM concepts."
title="Built for the Classroom"
subtitle="Designed to be durable, expandable, and easy for teachers to manage."
/>
<div className="flex flex-wrap justify-center gap-3">
{teachableConcepts.map((concept) => (
<span
key={concept}
className="rounded-full border border-border bg-muted px-4 py-2 text-sm font-medium text-foreground"
<div className="grid gap-8 md:grid-cols-2">
{classroomCards.map((card) => {
const content = (
<>
<div className="relative overflow-hidden" style={{ aspectRatio: "16 / 10" }}>
<Image
src={card.image}
alt={card.title}
width={600}
height={375}
className="h-full w-full object-cover"
{...(card.unoptimized ? { unoptimized: true } : {})}
/>
</div>
<div className="p-6">
<h3 className="text-lg font-semibold text-foreground">
{card.title}
</h3>
<p className="mt-2 text-sm text-foreground-light">
{card.description}
</p>
</div>
</>
);
return card.href ? (
<Link
key={card.title}
href={card.href}
className="group overflow-hidden rounded-2xl border border-border bg-white shadow-sm transition-shadow hover:shadow-md"
>
{concept}
</span>
{content}
</Link>
) : (
<div
key={card.title}
className="overflow-hidden rounded-2xl border border-border bg-white shadow-sm"
>
{content}
</div>
);
})}
</div>
</Container>
</section>
{/* Learning Pathway */}
<LearningPathway
subtitle="The Micromelon Rover is the hardware students use at every stage — from simplified blocks in Junior to professional Python."
/>
{/* Code Editor CTA */}
<section className="bg-foreground pt-16 pb-0">
<Container>
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold text-white sm:text-4xl" style={{ fontFamily: "monospace" }}>
Curious How To Program The Rover?
</h2>
<p className="mt-4 text-lg text-white/70">
The best thing about the Rover is how easy it is to program. Use
blocks and text at the same time with our Code Editor.
</p>
<div className="mt-8">
<Link
href="/code-editor"
className="inline-flex items-center justify-center rounded-lg border-2 border-brand px-6 py-3 font-medium text-brand transition-colors hover:bg-brand hover:text-foreground"
>
EXPLORE THE CODE EDITOR
</Link>
</div>
</div>
</Container>
<div className="mt-12">
<Image
src="/images/products/from-rover-to-software.gif"
alt="Micromelon Code Editor showing block and text coding side by side"
width={1280}
height={400}
className="w-full"
unoptimized
/>
</div>
</section>
{/* 3D Printed Attachments */}
<section className="bg-foreground py-20">
<Container>
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
Ready for More? Expand Your Rover
</h2>
<p className="mt-4 text-white/60">
Once students are comfortable, optional 3D printed attachments open up new challenges. No printer? Buy them pre-printed.
</p>
</div>
<div className="mt-12 grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
{products
.filter((p) => p.category === "attachments")
.sort(() => Math.random() - 0.5)
.slice(0, 8)
.map((attachment) => (
<div
key={attachment.slug}
className="overflow-hidden rounded-xl border border-white/10 bg-white/5"
>
<div className="relative aspect-square bg-white/10">
<Image
src={attachment.image}
alt={attachment.name}
fill
className="object-cover"
/>
</div>
<div className="p-4">
<h3 className="text-sm font-semibold text-white">
{attachment.name}
</h3>
<p className="mt-1 text-xs text-white/50 line-clamp-2">
{attachment.description}
</p>
</div>
</div>
))}
</div>
</Container>
</section>
{/* 3D Printing */}
<section className="border-t border-border bg-muted py-20">
<Container>
<div className="mx-auto max-w-3xl text-center">
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
Use 3D Printing to go further!
</h2>
<p className="mt-6 text-foreground-light">
Out of the box Micromelon Rovers are full of sensors and tools
catering for all skill levels. For more advanced exercises,
students can design and 3D print their own clip on extensions for
the rover.
</p>
<div className="mt-8 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
<Button href="/rover-expansion-3d-printing">
Expansion &amp; 3D Printing
<div className="mt-10 flex flex-wrap justify-center gap-4">
<Link
href="/rover-expansion-3d-printing"
className="inline-flex items-center justify-center rounded-lg border-2 border-white/30 px-6 py-3 font-medium text-white transition-colors hover:border-white hover:bg-white/10"
>
Free Print Files
</Link>
<Button href="/store" variant="primary">
Buy Pre-Printed
</Button>
<Button href="/store" variant="outline">
Build Your Kit
</Button>
</div>
</div>
</Container>
</section>
{/* Easy to Manage */}
<section className="bg-white py-20">
<Container>
<div className="grid items-center gap-12 md:grid-cols-2">
<div className="flex justify-center">
<Image
src="/images/products/case.png"
alt="Rover class set carry case with charging dock"
width={560}
height={400}
className="w-full max-w-lg rounded-2xl"
/>
</div>
<div>
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
Easy to Manage
</h2>
<p className="mt-6 text-foreground-light">
Rover class sets come in a hard carry case with included
charging dock. All ten robots can be charged at once from a
single outlet.
</p>
</div>
</div>
</Container>
</section>
{/* Add Your Own Electronics */}
<section className="border-t border-border bg-muted py-20">
<Container>
<div className="mx-auto max-w-3xl text-center">
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
Add Your Own Electronics
</h2>
<p className="mt-6 text-foreground-light">
Rovers have built-in expansion headers compatible with a range of
3rd party electronics including Raspberry Pi, Arduino and other
sensors.
</p>
</div>
</Container>
</section>
{/* Tech Specs */}
<section className="bg-white py-20">
<Container>
<div className="grid items-center gap-12 md:grid-cols-2">
<div>
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
Tech Specs
</h2>
<ul className="mt-8 grid grid-cols-1 gap-3 sm:grid-cols-2">
{techSpecs.map((spec) => (
<li
key={spec}
className="flex items-start gap-2 text-foreground-light"
>
<span className="mt-1.5 h-1.5 w-1.5 flex-shrink-0 rounded-full bg-brand" />
{spec}
</li>
))}
</ul>
</div>
<div className="flex justify-center">
<section className="relative bg-foreground py-20 overflow-hidden">
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<Image
src="/images/products/rover-specs.png"
alt="Micromelon Rover technical specifications"
width={560}
height={400}
className="w-full max-w-lg rounded-2xl"
alt=""
width={800}
height={570}
className="w-full max-w-3xl"
style={{ opacity: 0.08, filter: "blur(1px) invert(1) brightness(2)" }}
aria-hidden="true"
/>
</div>
<Container className="relative">
<div className="mx-auto max-w-2xl text-center">
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">Tech Specs</h2>
<p className="mt-4 text-white/60">
Everything students need to explore robotics, electronics, and programming built into one compact robot.
</p>
</div>
<div className="mt-12 grid gap-10 sm:grid-cols-2 lg:grid-cols-3">
<SpecGroupDark group={capabilities[0]} />
<div className="space-y-8">
<SpecGroupDark group={capabilities[1]} />
<SpecGroupDark group={capabilities[2]} />
</div>
<SpecGroupDark group={capabilities[3]} />
</div>
</Container>
</section>
{/* Related Resources */}
<RelatedResources
resources={relatedResources}
seeAllHref="/resources"
seeAllLabel="See all resources"
/>
{/* Code Editor CTA */}
<section className="bg-brand py-16">
{/* Made in Australia */}
<section className="bg-foreground py-12">
<Container>
<div className="text-center">
<h2 className="text-3xl font-bold text-foreground sm:text-4xl">
Curious How To Program The Rover?
<div className="flex flex-col items-center gap-6 sm:flex-row sm:justify-center sm:gap-10">
<Image
src="/images/awards/australian-made-owned.png"
alt="Australian Made and Owned"
width={100}
height={100}
className="h-20 w-auto"
/>
<div className="text-center sm:text-left">
<h2 className="text-2xl font-bold tracking-tight text-white sm:text-3xl">
Designed, Tested &amp; <span className="text-[#F5A623]">Made in Australia</span>
</h2>
<p className="mt-4 text-lg text-foreground-light">
The best thing about the Rover is how easy it is to program. Use
blocks and text at the same time with our Code Editor.
<p className="mt-2 text-sm text-white/70">
Every Micromelon Rover is designed, assembled, and tested right here in Australia.
</p>
<div className="mt-8">
<Button
href="/code-editor"
className="bg-foreground text-white hover:bg-foreground-light"
>
EXPLORE THE CODE EDITOR
</Button>
</div>
</div>
</Container>

View File

@@ -666,7 +666,7 @@ export default function StorePage() {
)}
{attachmentItems.length > 0 && (
<div className="border-t border-border pt-3">
<div className="pt-3">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Attachments
</p>
@@ -679,7 +679,7 @@ export default function StorePage() {
)}
{parsedStudents > 0 && (
<div className="border-t border-border pt-3">
<div className="pt-3">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Software
</p>
@@ -692,7 +692,7 @@ export default function StorePage() {
)}
{sparePartItems.length > 0 && (
<div className="border-t border-border pt-3">
<div className="pt-3">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Spare Parts
</p>

View File

@@ -1,11 +1,17 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { usePathname } from "next/navigation";
import { footerNavigation } from "@/data/navigation";
import { Container } from "./Container";
export function Footer() {
const pathname = usePathname();
const isDark = pathname === "/python";
return (
<footer className="border-t border-border bg-foreground text-white">
<footer className="text-white" style={{ background: isDark ? "#111111" : "#1a1a1a" }}>
<Container className="py-12">
<div className="grid gap-8 md:grid-cols-4">
{/* Brand */}

View File

@@ -7,18 +7,21 @@ import { usePathname } from "next/navigation";
import { navigation, type NavItem } from "@/data/navigation";
import { Container } from "./Container";
function DesktopNavItem({ item, pathname }: { item: NavItem; pathname: string }) {
function DesktopNavItem({ item, pathname, isDark }: { item: NavItem; pathname: string; isDark?: boolean }) {
const [open, setOpen] = useState(false);
const isActive = pathname === item.href || item.children?.some((c) => pathname === c.href);
const linkStyle = isActive
? { color: isDark ? "#F5A623" : "#d4900e" }
: { color: isDark ? "#d4d4d4" : "#1a1a1a" };
if (item.children) {
return (
<div className="relative" onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
<Link
href={item.href}
className={`px-3 py-2 text-sm font-medium transition-colors ${
isActive ? "text-brand-dark" : "text-foreground hover:text-brand-dark"
}`}
className="px-3 py-2 text-sm font-medium transition-colors"
style={linkStyle}
>
{item.label}
<svg className="ml-1 inline-block h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -26,14 +29,19 @@ function DesktopNavItem({ item, pathname }: { item: NavItem; pathname: string })
</svg>
</Link>
{open && (
<div className="absolute left-0 top-full z-50 mt-1 w-64 rounded-lg border border-border bg-white py-2 shadow-lg">
<div
className="absolute left-0 top-full z-50 mt-1 w-64 rounded-lg py-2 shadow-lg"
style={isDark ? { background: "#1a1a1a", border: "1px solid #525252" } : { background: "#fff", border: "1px solid #e8e8e8" }}
>
{item.children.map((child) => (
<Link
key={child.href}
href={child.href}
className={`block px-4 py-2 text-sm transition-colors ${
pathname === child.href ? "bg-muted text-brand-dark" : "text-foreground hover:bg-muted hover:text-brand-dark"
}`}
className="block px-4 py-2 text-sm transition-colors"
style={pathname === child.href
? { color: isDark ? "#F5A623" : "#d4900e" }
: { color: isDark ? "#d4d4d4" : "#1a1a1a" }
}
>
{child.label}
</Link>
@@ -47,9 +55,8 @@ function DesktopNavItem({ item, pathname }: { item: NavItem; pathname: string })
return (
<Link
href={item.href}
className={`px-3 py-2 text-sm font-medium transition-colors ${
isActive ? "text-brand-dark" : "text-foreground hover:text-brand-dark"
}`}
className="px-3 py-2 text-sm font-medium transition-colors"
style={linkStyle}
>
{item.label}
</Link>
@@ -60,9 +67,13 @@ export function Header() {
const [mobileOpen, setMobileOpen] = useState(false);
const [mobileSubmenu, setMobileSubmenu] = useState<string | null>(null);
const pathname = usePathname();
const isDark = pathname === "/python";
return (
<header className="fixed top-0 z-50 w-full border-b border-border bg-white/95 backdrop-blur-sm">
<header
className={`fixed top-0 z-50 w-full ${isDark ? "" : "backdrop-blur-sm"}`}
style={isDark ? { background: "#111111", borderBottom: "1px solid #333" } : { background: "rgba(255,255,255,0.95)", borderBottom: "1px solid #e8e8e8" }}
>
<Container>
<div className="flex h-16 items-center justify-between">
<Link href="/" className="flex items-center">
@@ -72,13 +83,14 @@ export function Header() {
width={200}
height={40}
priority
className={isDark ? "brightness-0 invert" : ""}
/>
</Link>
{/* Desktop nav */}
<nav className="hidden items-center gap-1 lg:flex">
{navigation.map((item) => (
<DesktopNavItem key={item.href} item={item} pathname={pathname} />
<DesktopNavItem key={item.href} item={item} pathname={pathname} isDark={isDark} />
))}
<Link
href="/store"

View File

@@ -0,0 +1,91 @@
"use client";
import { useEffect, useRef, useState, useCallback } from "react";
import Image from "next/image";
import Link from "next/link";
interface Activity {
title: string;
image: string;
href: string;
}
function shuffle<T>(arr: T[]): T[] {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
export function ActivityCarousel({ activities }: { activities: Activity[] }) {
const wrapperRef = useRef<HTMLDivElement>(null);
const [offset, setOffset] = useState(0);
const [cardWidth, setCardWidth] = useState(200);
const [items] = useState(() => shuffle(activities));
const GAP = 16;
const VISIBLE = 5;
const maxOffset = Math.max(0, items.length - VISIBLE);
useEffect(() => {
function calc() {
const el = wrapperRef.current;
if (!el) return;
const w = (el.offsetWidth - GAP * (VISIBLE - 1)) / VISIBLE;
setCardWidth(Math.floor(w));
}
calc();
window.addEventListener("resize", calc);
return () => window.removeEventListener("resize", calc);
}, []);
const scrollRight = useCallback(() => {
setOffset((prev) => (prev >= maxOffset ? 0 : prev + 1));
}, [maxOffset]);
// Auto-scroll every 5 seconds
useEffect(() => {
const interval = setInterval(scrollRight, 5000);
return () => clearInterval(interval);
}, [scrollRight]);
const translateX = -(offset * (cardWidth + GAP));
return (
<div ref={wrapperRef} className="relative">
<div className="overflow-hidden py-2">
<div
className="flex transition-transform duration-500 ease-in-out"
style={{ gap: GAP, transform: `translateX(${translateX}px)` }}
>
{items.map((activity) => (
<Link
key={activity.href}
href={activity.href}
className="group shrink-0 overflow-hidden rounded-xl border border-border bg-white shadow-sm transition-shadow hover:shadow-md"
style={{ width: cardWidth }}
>
<div className="relative aspect-[4/3] bg-muted">
<Image
src={activity.image}
alt={activity.title}
fill
className="object-cover transition-transform group-hover:scale-105"
sizes={`${cardWidth}px`}
/>
</div>
<div className="px-3 py-2.5">
<h3 className="text-sm font-semibold text-foreground line-clamp-2">
{activity.title}
</h3>
</div>
</Link>
))}
</div>
</div>
</div>
);
}

View File

@@ -1,5 +1,6 @@
import Link from "next/link";
import { Container } from "@/components/layout/Container";
import { AnimatedSteps } from "./LearningPathwayAnimated";
const steps = [
{
@@ -22,15 +23,46 @@ const steps = [
},
];
export { steps };
export function StepCard({ step, isActive }: { step: typeof steps[number]; isActive: boolean }) {
return (
<Link
href={step.href}
className={`w-44 rounded-xl py-5 text-center shadow-sm transition-all duration-500 hover:shadow-md ${
isActive
? "border-2 border-brand bg-white"
: "border border-border bg-white"
}`}
>
<p
className={`text-sm font-medium transition-colors duration-500 ${
isActive ? "text-brand-dark" : "text-muted-foreground"
}`}
>
Step {step.step}
</p>
<p className="text-lg font-bold text-foreground">
{step.label}
</p>
<p className="text-xs text-foreground-light">{step.detail}</p>
</Link>
);
}
export function LearningPathway({
title = "From Blocks to Python",
subtitle = "Start with simplified blocks and progress all the way to professional Python, all using the same Micromelon Rover.",
activeStep,
activeSteps,
animate = false,
className = "bg-white",
}: {
title?: string;
subtitle?: string;
activeStep?: number;
activeSteps?: number[];
animate?: boolean;
className?: string;
}) {
return (
@@ -41,40 +73,25 @@ export function LearningPathway({
{title}
</h2>
<p className="mt-6 text-foreground-light">{subtitle}</p>
{animate ? (
<AnimatedSteps />
) : (
<div className="mt-10 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
{steps.map((s, i) => {
const isActive = activeStep === s.step;
return (
{steps.map((s, i) => (
<div key={s.step} className="flex items-center gap-4 max-sm:flex-col">
{i > 0 && (
<span className="text-2xl text-muted-foreground max-sm:rotate-90">
&rarr;
</span>
)}
<Link
href={s.href}
className={`w-44 rounded-xl py-5 text-center shadow-sm transition-shadow hover:shadow-md ${
isActive
? "border-2 border-brand bg-white"
: "border border-border bg-white"
}`}
>
<p
className={`text-sm font-medium ${
isActive ? "text-brand-dark" : "text-muted-foreground"
}`}
>
Step {s.step}
</p>
<p className="text-lg font-bold text-foreground">
{s.label}
</p>
<p className="text-xs text-foreground-light">{s.detail}</p>
</Link>
<StepCard
step={s}
isActive={activeStep === s.step || !!activeSteps?.includes(s.step)}
/>
</div>
);
})}
))}
</div>
)}
</div>
</Container>
</section>

View File

@@ -0,0 +1,30 @@
"use client";
import { useState, useEffect } from "react";
import { steps, StepCard } from "./LearningPathway";
export function AnimatedSteps() {
const [activeIndex, setActiveIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setActiveIndex((prev) => (prev + 1) % steps.length);
}, 4000);
return () => clearInterval(interval);
}, []);
return (
<div className="mt-10 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
{steps.map((s, i) => (
<div key={s.step} className="flex items-center gap-4 max-sm:flex-col">
{i > 0 && (
<span className="text-2xl text-muted-foreground max-sm:rotate-90">
&rarr;
</span>
)}
<StepCard step={s} isActive={i === activeIndex} />
</div>
))}
</div>
);
}

View File

@@ -18,7 +18,7 @@ export function RelatedResources({
if (resources.length === 0) return null;
return (
<section className="border-t border-border bg-muted py-20">
<section className="bg-muted py-20">
<Container>
<SectionHeading
title="Related Resources"

View File

@@ -44,4 +44,10 @@ export const awards: Award[] = [
year: "2022",
image: "/images/awards/pl.jpg",
},
{
title: "World Robot Summit",
organisation: "Trade and Investment Queensland",
year: "2018",
image: "/images/awards/world-robot-summit.png",
},
];

View File

@@ -16,4 +16,10 @@ export const partners: Partner[] = [
{ name: "University of Queensland", logo: "/images/partners/uq.png" },
{ name: "UQ MARS", logo: "/images/partners/mars.png" },
{ name: "World Science Festival", logo: "/images/partners/worldsciencefair.png" },
{ name: "UQ Racing", logo: "/images/partners/uq-racing.jpg" },
{ name: "QUT Robotics Club", logo: "/images/partners/qut-robotics.jpeg" },
{ name: "Independent Schools NSW", logo: "/images/partners/aisnsw.jpg" },
{ name: "MakerHero", logo: "/images/partners/makerhero.svg" },
{ name: "Robotics Australia Group", logo: "/images/partners/robotics-australia-group.png" },
{ name: "QSITE", logo: "/images/partners/qsite.webp" },
];