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>
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
.env*
|
||||
.DS_Store
|
||||
Dockerfile.runner
|
||||
docker-compose.runner.yml
|
||||
runner-data
|
||||
24
.gitea/workflows/deploy.yml
Normal 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
@@ -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
@@ -0,0 +1,2 @@
|
||||
FROM gitea/act_runner:latest
|
||||
RUN apk add --no-cache docker-cli docker-cli-compose git bash nodejs
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
BIN
public/images/awards/world-robot-summit.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/images/partners/aisnsw.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
1
public/images/partners/makerhero.svg
Normal 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 |
BIN
public/images/partners/qsite.webp
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
public/images/partners/qut-robotics.jpeg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
public/images/partners/robotics-australia-group.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/images/partners/uq-racing.jpg
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/images/products/code-editor-with-ipad.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/images/products/from-rover-to-software.gif
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
public/images/products/junior-app.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
public/images/products/junior-blocks-interface.png
Normal file
|
After Width: | Height: | Size: 872 KiB |
BIN
public/images/products/junior-hero.png
Normal file
|
After Width: | Height: | Size: 828 KiB |
BIN
public/images/products/junior-icon.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/images/products/junior-rover-view.png
Normal file
|
After Width: | Height: | Size: 804 KiB |
BIN
public/images/products/vscode-extension-sidebar.png
Normal file
|
After Width: | Height: | Size: 539 KiB |
@@ -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"
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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,55 +71,81 @@ 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>
|
||||
<p className="mt-6 text-lg text-foreground-light">
|
||||
Junior is designed for lesson plans that focus on computational
|
||||
thinking fundamentals like sequencing, loops, and simple
|
||||
decision-making, without the complexity of a full coding
|
||||
environment.
|
||||
</p>
|
||||
</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">
|
||||
{feature.title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-foreground-light">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</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">
|
||||
Built for Younger Learners
|
||||
See It in Action
|
||||
</h2>
|
||||
<p className="mt-6 text-lg text-foreground-light">
|
||||
Junior is designed for lesson plans that focus on computational
|
||||
thinking fundamentals like sequencing, loops, and simple
|
||||
decision-making, without the complexity of a full coding
|
||||
environment.
|
||||
<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 className="mt-10 grid gap-6 sm:grid-cols-2">
|
||||
<div className="rounded-xl border border-border bg-white p-6">
|
||||
<h3 className="text-lg font-semibold text-foreground">
|
||||
Simplified Interface
|
||||
</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.
|
||||
</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.
|
||||
</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.
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
</Container>
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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">
|
||||
← 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">
|
||||
← {returnLabel}
|
||||
</Button>
|
||||
|
||||
@@ -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">
|
||||
<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
|
||||
<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="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>
|
||||
</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,26 +175,37 @@ 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">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl">
|
||||
Program With The Micromelon Code Editor
|
||||
</h2>
|
||||
<p className="mt-6 text-foreground-light">
|
||||
Write the same code for a simulated rover as you would for a real
|
||||
Micromelon Rover. Switch between blocks and text, and run your
|
||||
programs instantly in the Simulator.
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<Button href="/code-editor">Explore the Code Editor</Button>
|
||||
<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>
|
||||
<p className="mt-6 text-foreground-light">
|
||||
Write the same code for a simulated rover as you would for a real
|
||||
Micromelon Rover. Switch between blocks and text, and run your
|
||||
programs instantly in the Simulator.
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<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 */}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
<section className="relative min-h-[60vh] overflow-hidden">
|
||||
<div className="absolute inset-0">
|
||||
<Image
|
||||
src="/images/hero/rover-hero.jpg"
|
||||
alt="Micromelon Rover"
|
||||
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 text-foreground-light">
|
||||
Long battery life, tough, and packed with sensors to make a
|
||||
great classroom tool. Connect and run code in seconds.
|
||||
<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 flex flex-col gap-3 sm:flex-row">
|
||||
<Button href="/store" variant="primary">
|
||||
<div className="mt-8">
|
||||
<Button href="/store" className="bg-foreground text-white hover:bg-foreground-light">
|
||||
Build Your Kit
|
||||
</Button>
|
||||
<Button href="/download" variant="outline">
|
||||
Download Code Editor
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Image
|
||||
src="/images/hero/rover-hero.jpg"
|
||||
alt="Micromelon Rover"
|
||||
width={700}
|
||||
height={467}
|
||||
className="w-full rounded-2xl shadow-lg"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</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"
|
||||
>
|
||||
{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"
|
||||
>
|
||||
{concept}
|
||||
</span>
|
||||
))}
|
||||
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 Printing */}
|
||||
<section className="border-t border-border bg-muted py-20">
|
||||
{/* 3D Printed Attachments */}
|
||||
<section className="bg-foreground 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!
|
||||
<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-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 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 className="mt-8 flex flex-col items-center gap-4 sm:flex-row sm:justify-center">
|
||||
<Button href="/rover-expansion-3d-printing">
|
||||
Expansion & 3D Printing
|
||||
</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 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>
|
||||
|
||||
{/* 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 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>
|
||||
</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">
|
||||
<Image
|
||||
src="/images/products/rover-specs.png"
|
||||
alt="Micromelon Rover technical specifications"
|
||||
width={560}
|
||||
height={400}
|
||||
className="w-full max-w-lg rounded-2xl"
|
||||
/>
|
||||
<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=""
|
||||
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?
|
||||
</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>
|
||||
<div className="mt-8">
|
||||
<Button
|
||||
href="/code-editor"
|
||||
className="bg-foreground text-white hover:bg-foreground-light"
|
||||
>
|
||||
EXPLORE THE CODE EDITOR
|
||||
</Button>
|
||||
<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 & <span className="text-[#F5A623]">Made in Australia</span>
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-white/70">
|
||||
Every Micromelon Rover is designed, assembled, and tested right here in Australia.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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"
|
||||
|
||||
91
src/components/ui/ActivityCarousel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
<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 (
|
||||
{animate ? (
|
||||
<AnimatedSteps />
|
||||
) : (
|
||||
<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">
|
||||
→
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
30
src/components/ui/LearningPathwayAnimated.tsx
Normal 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">
|
||||
→
|
||||
</span>
|
||||
)}
|
||||
<StepCard step={s} isActive={i === activeIndex} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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" },
|
||||
];
|
||||
|
||||