diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f248dd6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.next +.git +.env* +.DS_Store +Dockerfile.runner +docker-compose.runner.yml +runner-data diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..d5de311 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -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 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a5cf743 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Dockerfile.runner b/Dockerfile.runner new file mode 100644 index 0000000..38d3da3 --- /dev/null +++ b/Dockerfile.runner @@ -0,0 +1,2 @@ +FROM gitea/act_runner:latest +RUN apk add --no-cache docker-cli docker-cli-compose git bash nodejs diff --git a/content/resources/introducing-the-micromelon-vs-code-extension.mdx b/content/resources/introducing-the-micromelon-vs-code-extension.mdx new file mode 100644 index 0000000..8d625b0 --- /dev/null +++ b/content/resources/introducing-the-micromelon-vs-code-extension.mdx @@ -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. diff --git a/content/resources/rover-repairs-made-easy-with-online-return-requests.mdx b/content/resources/rover-repairs-made-easy-with-online-return-requests.mdx new file mode 100644 index 0000000..3eb1fe5 --- /dev/null +++ b/content/resources/rover-repairs-made-easy-with-online-return-requests.mdx @@ -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. diff --git a/docker-compose.runner.yml b/docker-compose.runner.yml new file mode 100644 index 0000000..ca52127 --- /dev/null +++ b/docker-compose.runner.yml @@ -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: diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4e21ab7 --- /dev/null +++ b/docker-compose.yml @@ -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} diff --git a/next.config.ts b/next.config.ts index e9ffa30..68a6c64 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", }; export default nextConfig; diff --git a/public/images/awards/world-robot-summit.png b/public/images/awards/world-robot-summit.png new file mode 100644 index 0000000..c194bbb Binary files /dev/null and b/public/images/awards/world-robot-summit.png differ diff --git a/public/images/partners/aisnsw.jpg b/public/images/partners/aisnsw.jpg new file mode 100644 index 0000000..0585fb4 Binary files /dev/null and b/public/images/partners/aisnsw.jpg differ diff --git a/public/images/partners/makerhero.svg b/public/images/partners/makerhero.svg new file mode 100644 index 0000000..58f482f --- /dev/null +++ b/public/images/partners/makerhero.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/partners/qsite.webp b/public/images/partners/qsite.webp new file mode 100644 index 0000000..c2daf6e Binary files /dev/null and b/public/images/partners/qsite.webp differ diff --git a/public/images/partners/qut-robotics.jpeg b/public/images/partners/qut-robotics.jpeg new file mode 100644 index 0000000..54e3044 Binary files /dev/null and b/public/images/partners/qut-robotics.jpeg differ diff --git a/public/images/partners/robotics-australia-group.png b/public/images/partners/robotics-australia-group.png new file mode 100644 index 0000000..3168376 Binary files /dev/null and b/public/images/partners/robotics-australia-group.png differ diff --git a/public/images/partners/uq-racing.jpg b/public/images/partners/uq-racing.jpg new file mode 100644 index 0000000..0033802 Binary files /dev/null and b/public/images/partners/uq-racing.jpg differ diff --git a/public/images/products/code-editor-with-ipad.png b/public/images/products/code-editor-with-ipad.png new file mode 100644 index 0000000..e37449e Binary files /dev/null and b/public/images/products/code-editor-with-ipad.png differ diff --git a/public/images/products/from-rover-to-software.gif b/public/images/products/from-rover-to-software.gif new file mode 100644 index 0000000..80d4e32 Binary files /dev/null and b/public/images/products/from-rover-to-software.gif differ diff --git a/public/images/products/junior-app.png b/public/images/products/junior-app.png new file mode 100644 index 0000000..4eb2ffe Binary files /dev/null and b/public/images/products/junior-app.png differ diff --git a/public/images/products/junior-blocks-interface.png b/public/images/products/junior-blocks-interface.png new file mode 100644 index 0000000..88ec7a8 Binary files /dev/null and b/public/images/products/junior-blocks-interface.png differ diff --git a/public/images/products/junior-hero.png b/public/images/products/junior-hero.png new file mode 100644 index 0000000..ac58490 Binary files /dev/null and b/public/images/products/junior-hero.png differ diff --git a/public/images/products/junior-icon.png b/public/images/products/junior-icon.png new file mode 100644 index 0000000..e665131 Binary files /dev/null and b/public/images/products/junior-icon.png differ diff --git a/public/images/products/junior-rover-view.png b/public/images/products/junior-rover-view.png new file mode 100644 index 0000000..a8594b1 Binary files /dev/null and b/public/images/products/junior-rover-view.png differ diff --git a/public/images/products/vscode-extension-sidebar.png b/public/images/products/vscode-extension-sidebar.png new file mode 100644 index 0000000..d2627c1 Binary files /dev/null and b/public/images/products/vscode-extension-sidebar.png differ diff --git a/src/app/about-us/page.tsx b/src/app/about-us/page.tsx index 1c0d828..82ded2b 100644 --- a/src/app/about-us/page.tsx +++ b/src/app/about-us/page.tsx @@ -63,7 +63,7 @@ export default function AboutUsPage() { {/* Founders */} -
+
@@ -116,7 +116,7 @@ export default function AboutUsPage() {
{/* Schools */} -
+
- r.categories.includes("Getting Started") || - r.categories.includes("Guides") - ) - .slice(0, 3); - return ( <> {/* Hero */} @@ -59,7 +50,7 @@ export default function CodeEditorPage() {
{/* Three Coding Modes */} -
+
{/* Classroom Management */} -
+
{/* System Requirements */} -
+

@@ -312,11 +303,10 @@ export default function CodeEditorPage() {

- {/* Related Resources */} - {/* CTA */} diff --git a/src/app/junior/page.tsx b/src/app/junior/page.tsx index bb6b68b..7b28bbd 100644 --- a/src/app/junior/page.tsx +++ b/src/app/junior/page.tsx @@ -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.

-
+
-
Micromelon Rover used with Junior app
@@ -58,55 +71,81 @@ export default function JuniorPage() {
{/* What is Junior */} -
+
-
+
+
+ Micromelon Junior app icon +
+
+

+ Built for Younger Learners +

+

+ 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. +

+
+
+
+ {features.map((feature) => ( +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
+ +
+ + {/* Screenshots */} +
+ +

- Built for Younger Learners + See It in Action

-

- 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. +

+ A clean block-based interface for building programs, and a live + Rover View showing real-time sensor data.

-
-
-

- Simplified Interface -

-

- A pared-back design so students can focus on learning concepts - rather than navigating menus. -

-
-
-

- Tablet-First -

-

- Available on iPad and Android. Perfect for schools using - tablets in the classroom. -

-
-
-

- Block-Based Coding -

-

- Drag-and-drop blocks to build programs. No typing required, - making it accessible for early primary students. -

-
-
-

- Works with the Rover -

-

- Connect to a real Micromelon Rover and see programs come to - life, the same rover students will use as they progress. -

-
+
+
+
+ Micromelon Junior block programming interface with movement and sensor blocks +

+ Drag-and-drop blocks to build programs +

+
+
+ Micromelon Junior Rover View showing ultrasonic, IR, and colour sensor readings +

+ Live sensor data from the connected rover +

@@ -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 */} -
- -
-

- Available On -

-

- iPad and Android tablets. Download from the App Store or Google - Play. -

-
- -
-
-
-
- - {/* Related Resources */} - {/* CTA */} diff --git a/src/app/page.tsx b/src/app/page.tsx index e0abe3d..ae0677e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -56,7 +56,7 @@ export default function HomePage() {
{/* Learning Pathway */} - + {/* Product Cards */}
@@ -87,7 +87,7 @@ export default function HomePage() {
Micromelon Junior {/* Schools */} -
+
))}
+

+ and many more... +

diff --git a/src/app/platform/page.tsx b/src/app/platform/page.tsx index 9e2f0c6..dc481c6 100644 --- a/src/app/platform/page.tsx +++ b/src/app/platform/page.tsx @@ -31,7 +31,7 @@ export default function PlatformPage() {
{/* Micromelon Rover */} -
+
@@ -70,16 +70,16 @@ export default function PlatformPage() { {/* Junior */} -
+
Micromelon Junior app
@@ -159,7 +159,7 @@ export default function PlatformPage() {
{/* Python Library */} -
+
diff --git a/src/app/python/layout.tsx b/src/app/python/layout.tsx new file mode 100644 index 0000000..508a4ab --- /dev/null +++ b/src/app/python/layout.tsx @@ -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; +} diff --git a/src/app/python/page.tsx b/src/app/python/page.tsx index a9a6e14..452ccab 100644 --- a/src/app/python/page.tsx +++ b/src/app/python/page.tsx @@ -1,63 +1,772 @@ -import type { Metadata } from "next"; -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"; +"use client"; -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.", +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; +import { Container } from "@/components/layout/Container"; + +const features = [ + { + title: "Full Rover API", + description: + "Control motors, read sensors, play sounds, and set LEDs with a clean, well-documented Python API.", + icon: ">>>", + }, + { + title: "Works with the Simulator", + description: + "Connect to a simulated rover just as easily as a physical one. Test and iterate without hardware.", + icon: "SIM", + }, + { + title: "Use Your Own Editor", + description: + "VS Code, PyCharm, Jupyter Notebooks, or a plain terminal. Students work in the tools real developers use.", + icon: "IDE", + }, + { + title: "Advanced Projects", + description: + "Combine the rover with other Python libraries for computer vision, data analysis, machine learning, and more.", + icon: "AI", + }, +]; + +const codeLines = [ + "from micromelon import *", + "import time", + "import math", + "import random", + "", + "# ─── Configuration ───────────────────────", + "", + "ROVER_ID = 1", + "MAX_SPEED = 50", + "TURN_SPEED = 30", + "SCAN_INTERVAL = 0.1", + "OBSTACLE_THRESHOLD = 15", + "WALL_FOLLOW_DIST = 20", + "GRID_SIZE = 100", + "", + "# LED colour presets", + "COLOUR_IDLE = (0, 80, 255)", + "COLOUR_MOVING = (0, 255, 60)", + "COLOUR_WARN = (255, 180, 0)", + "COLOUR_DANGER = (255, 0, 0)", + "COLOUR_SCANNING = (180, 0, 255)", + "COLOUR_SUCCESS = (0, 255, 180)", + "", + "# Musical notes for feedback", + "NOTE_START = [(440, 150), (554, 150), (659, 200)]", + "NOTE_ALERT = [(880, 100), (660, 100)]", + "NOTE_DONE = [(523, 200), (659, 200), (784, 300)]", + "", + "# ─── Connect ─────────────────────────────", + "", + "rc = RoverController()", + "rc.connectBLE(ROVER_ID)", + "print(f'Connected to rover {ROVER_ID}')", + "", + "# ─── Utility functions ────────────────────", + "", + "def clamp(value, lo, hi):", + " return max(lo, min(hi, value))", + "", + "def lerp(a, b, t):", + " return a + (b - a) * clamp(t, 0, 1)", + "", + "def distance_between(p1, p2):", + " dx = p2[0] - p1[0]", + " dy = p2[1] - p1[1]", + " return math.sqrt(dx * dx + dy * dy)", + "", + "def angle_to(p1, p2):", + " dx = p2[0] - p1[0]", + " dy = p2[1] - p1[1]", + " return math.degrees(math.atan2(dy, dx))", + "", + "def blend_colour(c1, c2, t):", + " return tuple(", + " int(lerp(c1[i], c2[i], t))", + " for i in range(3)", + " )", + "", + "# ─── LED controller ────────────────────────", + "", + "class StatusLEDs:", + " def __init__(self, controller):", + " self.rc = controller", + " self.current = (0, 0, 0)", + "", + " def set(self, colour):", + " if colour != self.current:", + " self.rc.setLEDColour(*colour)", + " self.current = colour", + "", + " def pulse(self, colour, duration=0.5):", + " steps = 10", + " for i in range(steps):", + " t = i / steps", + " bright = blend_colour((0, 0, 0), colour, t)", + " self.set(bright)", + " time.sleep(duration / (steps * 2))", + " for i in range(steps, 0, -1):", + " t = i / steps", + " bright = blend_colour((0, 0, 0), colour, t)", + " self.set(bright)", + " time.sleep(duration / (steps * 2))", + "", + " def flash(self, colour, times=3):", + " for _ in range(times):", + " self.set(colour)", + " time.sleep(0.1)", + " self.set((0, 0, 0))", + " time.sleep(0.1)", + "", + "leds = StatusLEDs(rc)", + "", + "# ─── Sound controller ───────────────────────", + "", + "def play_melody(notes):", + " for freq, dur in notes:", + " rc.playNote(freq, dur)", + " time.sleep(dur / 1000)", + "", + "def beep(freq=440, ms=100):", + " rc.playNote(freq, ms)", + "", + "# ─── Sensor reading ────────────────────────", + "", + "class SensorBuffer:", + " def __init__(self, size=5):", + " self.size = size", + " self.buffer = []", + "", + " def add(self, value):", + " self.buffer.append(value)", + " if len(self.buffer) > self.size:", + " self.buffer.pop(0)", + "", + " def average(self):", + " if not self.buffer:", + " return 0", + " return sum(self.buffer) / len(self.buffer)", + "", + " def minimum(self):", + " return min(self.buffer) if self.buffer else 0", + "", + " def trend(self):", + " if len(self.buffer) < 2:", + " return 0", + " return self.buffer[-1] - self.buffer[0]", + "", + "front_sensor = SensorBuffer(5)", + "", + "# ─── Movement primitives ─────────────────────", + "", + "def move_forward(speed=MAX_SPEED):", + " leds.set(COLOUR_MOVING)", + " rc.forward(clamp(speed, 0, MAX_SPEED))", + "", + "def move_backward(speed=MAX_SPEED):", + " leds.set(COLOUR_WARN)", + " rc.backward(clamp(speed, 0, MAX_SPEED))", + "", + "def turn_left(degrees=90):", + " leds.set(COLOUR_SCANNING)", + " rc.turn(-abs(degrees))", + "", + "def turn_right(degrees=90):", + " leds.set(COLOUR_SCANNING)", + " rc.turn(abs(degrees))", + "", + "def stop():", + " rc.stop()", + " leds.set(COLOUR_IDLE)", + "", + "# ─── Obstacle avoidance ─────────────────────", + "", + "def scan_surroundings():", + " distances = {}", + " for angle in [-90, -45, 0, 45, 90]:", + " rc.turn(angle)", + " time.sleep(0.2)", + " reading = rc.readUltrasonic()", + " front_sensor.add(reading)", + " distances[angle] = front_sensor.average()", + " rc.turn(-angle)", + " return distances", + "", + "def find_best_direction(scan):", + " best_angle = 0", + " best_dist = 0", + " for angle, dist in scan.items():", + " if dist > best_dist:", + " best_dist = dist", + " best_angle = angle", + " return best_angle, best_dist", + "", + "def avoid_obstacle():", + " stop()", + " leds.flash(COLOUR_DANGER)", + " play_melody(NOTE_ALERT)", + "", + " move_backward(20)", + " time.sleep(0.5)", + " stop()", + "", + " scan = scan_surroundings()", + " best_angle, best_dist = find_best_direction(scan)", + "", + " if best_dist < OBSTACLE_THRESHOLD:", + " turn_right(180)", + " else:", + " rc.turn(best_angle)", + "", + " time.sleep(0.3)", + "", + "# ─── Wall following ───────────────────────", + "", + "def follow_wall(side='right', steps=50):", + " for i in range(steps):", + " front = rc.readUltrasonic()", + " front_sensor.add(front)", + "", + " if front < OBSTACLE_THRESHOLD:", + " avoid_obstacle()", + " continue", + "", + " progress = i / steps", + " colour = blend_colour(", + " COLOUR_MOVING, COLOUR_SUCCESS, progress", + " )", + " leds.set(colour)", + "", + " speed = clamp(", + " int(front * 1.5), 10, MAX_SPEED", + " )", + " move_forward(speed)", + " time.sleep(SCAN_INTERVAL)", + "", + " stop()", + "", + "# ─── Patrol patterns ────────────────────────", + "", + "def patrol_square(size=30, laps=2):", + " for lap in range(laps):", + " for side in range(4):", + " move_forward(MAX_SPEED)", + " time.sleep(size / MAX_SPEED)", + "", + " front = rc.readUltrasonic()", + " if front < OBSTACLE_THRESHOLD:", + " avoid_obstacle()", + "", + " turn_right(90)", + " time.sleep(0.5)", + "", + " beep(523 + lap * 100)", + " stop()", + "", + "def patrol_zigzag(width=40, legs=6):", + " for i in range(legs):", + " move_forward(MAX_SPEED)", + " time.sleep(width / MAX_SPEED)", + "", + " front = rc.readUltrasonic()", + " if front < OBSTACLE_THRESHOLD:", + " avoid_obstacle()", + " continue", + "", + " if i % 2 == 0:", + " turn_right(90)", + " move_forward(15)", + " turn_right(90)", + " else:", + " turn_left(90)", + " move_forward(15)", + " turn_left(90)", + "", + " time.sleep(0.3)", + " stop()", + "", + "def patrol_spiral(loops=5):", + " for i in range(loops):", + " duration = 0.5 + i * 0.3", + " speed = clamp(MAX_SPEED - i * 5, 15, MAX_SPEED)", + "", + " move_forward(speed)", + " time.sleep(duration)", + "", + " front = rc.readUltrasonic()", + " if front < OBSTACLE_THRESHOLD:", + " avoid_obstacle()", + "", + " turn_right(90)", + " time.sleep(0.3)", + "", + " progress = i / loops", + " colour = blend_colour(", + " COLOUR_MOVING, COLOUR_SCANNING, progress", + " )", + " leds.set(colour)", + " stop()", + "", + "# ─── Mission runner ────────────────────────", + "", + "MISSIONS = {", + " 'square': patrol_square,", + " 'zigzag': patrol_zigzag,", + " 'spiral': patrol_spiral,", + " 'wall': follow_wall,", + "}", + "", + "def run_mission(name, **kwargs):", + " if name not in MISSIONS:", + " print(f'Unknown mission: {name}')", + " return", + "", + " print(f'Starting mission: {name}')", + " leds.pulse(COLOUR_SUCCESS)", + " play_melody(NOTE_START)", + " time.sleep(0.5)", + "", + " try:", + " MISSIONS[name](**kwargs)", + " except KeyboardInterrupt:", + " stop()", + " print('Mission aborted')", + " return", + "", + " play_melody(NOTE_DONE)", + " leds.pulse(COLOUR_SUCCESS)", + " print(f'Mission complete: {name}')", + "", + "# ─── Main loop ─────────────────────────────", + "", + "print('Rover ready. Starting patrol...')", + "play_melody(NOTE_START)", + "time.sleep(1)", + "", + "missions = ['square', 'zigzag', 'spiral', 'wall']", + "", + "for mission in missions:", + " run_mission(mission)", + " time.sleep(2)", + "", + "# Final shutdown", + "stop()", + "leds.flash(COLOUR_SUCCESS, times=5)", + "play_melody(NOTE_DONE)", + "rc.disconnect()", + "print('All missions complete. Rover offline.')", +]; + +// --- Typing code animation with typos --- + +// Keys near each letter on a QWERTY keyboard +const NEARBY_KEYS: Record = { + a: "sq", b: "vn", c: "xv", d: "sf", e: "wr", f: "dg", g: "fh", + h: "gj", i: "uo", j: "hk", k: "jl", l: "k;", m: "n,", n: "bm", + o: "ip", p: "o[", q: "wa", r: "et", s: "ad", t: "ry", u: "yi", + v: "cb", w: "qe", x: "zc", y: "tu", z: "xs", }; -export default function PythonPage() { - const resources = getAllResources(); - const relatedResources = resources - .filter((r) => r.categories.includes("Advanced Guides")) - .slice(0, 3); +function nearbyTypo(char: string): string { + const lower = char.toLowerCase(); + const candidates = NEARBY_KEYS[lower]; + if (!candidates) return char; + const typo = candidates[Math.floor(Math.random() * candidates.length)]; + return char === lower ? typo : typo.toUpperCase(); +} + +type TypingState = { + displayed: string; // what's on screen + srcPos: number; // position in the source text + typoCharsLeft: number; // chars to backspace before resuming +}; + +function TypingCode({ lines }: { lines: string[] }) { + const full = lines.join("\n"); + const [state, setState] = useState({ + displayed: "", + srcPos: 0, + typoCharsLeft: 0, + }); + const [started, setStarted] = useState(false); + const ref = useRef(null); + const gutterRef = useRef(null); + const codeRef = useRef(null); + + // Start when scrolled into view + useEffect(() => { + const el = ref.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setStarted(true); + observer.disconnect(); + } + }, + { threshold: 0.2 } + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + // Typing engine + useEffect(() => { + if (!started) return; + const { displayed, srcPos, typoCharsLeft } = state; + + // Done + if (srcPos >= full.length && typoCharsLeft === 0) return; + + let delay: number; + + // Currently backspacing a typo + if (typoCharsLeft > 0) { + delay = 60 + Math.random() * 40; + const id = setTimeout(() => { + setState({ + displayed: displayed.slice(0, -1), + srcPos, + typoCharsLeft: typoCharsLeft - 1, + }); + }, delay); + return () => clearTimeout(id); + } + + // Normal typing + const nextChar = full[srcPos]; + const prevChar = srcPos > 0 ? full[srcPos - 1] : null; + const nextIsNewline = nextChar === "\n"; + const isBlankLine = + nextIsNewline && srcPos + 1 < full.length && full[srcPos + 1] === "\n"; + + if (prevChar === "\n") { + delay = isBlankLine ? 150 + Math.random() * 200 : 600 + Math.random() * 1400; + } else if (nextChar === " " && full[srcPos + 1] === " ") { + delay = 15 + Math.random() * 20; + } else if ("()[]{}:.,=*'\"".includes(nextChar ?? "")) { + delay = 50 + Math.random() * 80; + } else if (nextChar === " ") { + delay = 30 + Math.random() * 50; + } else { + delay = 25 + Math.random() * 55; + } + + if (Math.random() < 0.03) { + delay += 200 + Math.random() * 400; + } + + // Maybe make a typo on regular letters (not whitespace/punctuation/newlines) + const isLetter = /^[a-zA-Z]$/.test(nextChar ?? ""); + const shouldTypo = isLetter && Math.random() < 0.04; + + if (shouldTypo) { + // Type 1-2 wrong characters, then backspace them + const typoLen = Math.random() < 0.7 ? 1 : 2; + let typoChars = ""; + for (let i = 0; i < typoLen; i++) { + typoChars += nearbyTypo(full[srcPos + i] ?? nextChar); + } + const id = setTimeout(() => { + setState({ + displayed: displayed + typoChars, + srcPos, + typoCharsLeft: typoLen, + }); + }, delay); + return () => clearTimeout(id); + } + + // Normal character + const id = setTimeout(() => { + setState({ + displayed: displayed + nextChar, + srcPos: srcPos + 1, + typoCharsLeft: 0, + }); + }, delay); + return () => clearTimeout(id); + }, [started, state, full]); + + // Auto-scroll code container (not the page) + useEffect(() => { + const gutter = gutterRef.current; + const code = codeRef.current; + if (gutter) gutter.scrollTop = gutter.scrollHeight; + if (code) code.scrollTop = code.scrollHeight; + }, [state.displayed]); + + const done = state.srcPos >= full.length && state.typoCharsLeft === 0; + + // Split displayed text into lines for line numbers + const displayedLines = state.displayed.split("\n"); return ( - <> +
+
+ {/* Line numbers */} +
+ {displayedLines.map((_, i) => ( +
{i + 1}
+ ))} +
+ {/* Code */} +
+          
+            {state.displayed}
+            {!done && }
+          
+        
+
+
+ ); +} + +// --- Page --- + +export default function PythonPage() { + return ( +
+ {/* Cursor blink keyframes */} + + {/* Hero */} -
+
-

+
+ pip install micromelon +
+

Micromelon Python Library

-

+

A dedicated Python module for controlling Micromelon Rovers and simulated rovers from any Python environment. Perfect for senior students ready to move beyond the Code Editor.

-
- - $ pip install micromelon - -
- - + +
+
+
+
+ + + + + terminal + +
+
+ + $ pip install micromelon + + +
+
+
+
+
+ + {/* Code Example */} +
+ +
+
+ {/* Window controls */} +
+ + + +
+ {/* Editor tabs */} +
+ {[ + { name: "rover_control.py", active: true }, + { name: "config.py", active: false }, + { name: "sensors.py", active: false }, + { name: "README.md", active: false }, + ].map((tab) => ( +
+ + {tab.name} +
+ ))} +
+ +
+
+

+ Code in Any Python Environment +

+

+ The Micromelon Python library gives students full control of + their rover using standard Python. Use it in VS Code, PyCharm, + Jupyter Notebooks, or any environment that runs Python. +

+
+
+
+
+ + {/* VS Code Extension */} +
+ +
+
+
+ VS Code Extension +
+

+ Micromelon for VS Code +

+

+ Connect to your rover, run scripts, and view live sensor data + without leaving your editor. Hit F5 to deploy your code to the + rover instantly. +

+
+ {[ + { icon: "BLE", title: "One-Click Connect", desc: "Connect to any rover by ID over Bluetooth Low Energy" }, + { icon: "▶", title: "Run with F5", desc: "Deploy and run your Python script on the rover instantly" }, + { icon: "◉", title: "Live Sensors", desc: "Real-time dashboard for ultrasonic, colour, IR, accelerometer & gyroscope" }, + { icon: "{ }", title: "Code Snippets", desc: "Type mm- for ready-made templates for motors, sensors, LEDs & sounds" }, + ].map((item) => ( +
+
+ {item.icon} +
+

{item.title}

+

{item.desc}

+
+ ))}
Micromelon Python library used in VS Code
@@ -65,97 +774,162 @@ export default function PythonPage() {
{/* Features */} -
- -
-

- Code in Any Python Environment -

-

- The Micromelon Python library gives students full control of their - rover using standard Python. Use it in VS Code, PyCharm, Jupyter - Notebooks, or any environment that runs Python. -

-
-
-

- Full Rover API -

-

- Control motors, read sensors, play sounds, and set LEDs with a - clean, well-documented Python API. -

-
-
-

- Works with the Simulator -

-

- Connect to a simulated rover just as easily as a physical one. - Test and iterate without hardware. -

-
-
-

- Use Your Own Editor -

-

- VS Code, PyCharm, Jupyter Notebooks, or a plain terminal. - Students work in the tools real developers use. -

-
-
-

- Advanced Projects -

-

- Combine the rover with other Python libraries for computer - vision, data analysis, machine learning, and more. -

-
+
+ +
+ {features.map((feature) => ( +
+
+ {feature.icon} +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ))}
-
- +
{/* Pathway */} - - - {/* Related Resources */} - +
+ +
+

+ From Blocks to Python +

+

+ The Python library is the final step. Students who started with + blocks in Junior and progressed through the Code Editor can now + write professional Python. +

+
+ {[ + { + step: 1, + label: "Junior", + detail: "Simplified blocks", + href: "/junior", + }, + { + step: 2, + label: "Code Editor", + detail: "Blocks, mixed & Python", + href: "/code-editor", + }, + { + step: 3, + label: "Python", + detail: "VS Code & beyond", + href: "/python", + }, + ].map((s, i) => ( + + ))} +
+
+
+
{/* CTA */} -
- -
-

- Ready to Get Started? -

-

- Install the library and start controlling your rover in minutes. -

-
- - + Ready to Get Started? + +

+ Install the library and start controlling your rover in minutes. +

+
+ + View on PyPI + + + Advanced Guides + +
-
-
+
- +
); } diff --git a/src/app/resources/[slug]/page.tsx b/src/app/resources/[slug]/page.tsx index 818e7a8..6c637d8 100644 --- a/src/app/resources/[slug]/page.tsx +++ b/src/app/resources/[slug]/page.tsx @@ -231,12 +231,12 @@ export default async function ResourcePage({ params }: ResourcePageProps) {
{/* Activity Content */} -
+
-
+
@@ -299,12 +299,12 @@ export default async function ResourcePage({ params }: ResourcePageProps) {
{/* Content */} -
+
-
+
diff --git a/src/app/robot-simulator/page.tsx b/src/app/robot-simulator/page.tsx index 6c3b766..9e781f7 100644 --- a/src/app/robot-simulator/page.tsx +++ b/src/app/robot-simulator/page.tsx @@ -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 */} -
- -
-
-

- The Robot Simulator +
+ Simulator demo showing a virtual rover navigating an exercise +
+
+ +
+

+ Micromelon Robot Simulator

-

+

Filled with virtual exercises, the Simulator is great for homework and running complex challenges. No physical robot needed.

-
+
-
-
- Micromelon Robot Simulator showing a virtual rover in a 3D environment -
-
-
+ +
{/* Simulator Demo */} -
+
@@ -83,14 +73,13 @@ export default function RobotSimulatorPage() {
-
+
Simulator demo showing a virtual rover navigating an exercise
@@ -98,7 +87,7 @@ export default function RobotSimulatorPage() {
{/* Simulator Features */} -
+
{/* Code Editor Connection */} -
+
-
-

- Program With The Micromelon Code Editor -

-

- 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. -

-
- +
+
+

+ Program With The Micromelon Code Editor +

+

+ 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. +

+
+ +
+
+
+ Micromelon software running on multiple devices
{/* System Requirements */} -
+

@@ -218,11 +218,11 @@ export default function RobotSimulatorPage() {

- {/* Related Resources */} - {/* CTA */} diff --git a/src/app/rover-expansion-3d-printing/page.tsx b/src/app/rover-expansion-3d-printing/page.tsx index 65cd0fc..0331dc7 100644 --- a/src/app/rover-expansion-3d-printing/page.tsx +++ b/src/app/rover-expansion-3d-printing/page.tsx @@ -57,7 +57,7 @@ export default function RoverExpansion3DPrintingPage() {
{/* 3D Printing Crash Course */} -
+

@@ -93,7 +93,7 @@ export default function RoverExpansion3DPrintingPage() {

{/* Build Guides */} -
+
{/* CAD Models */} -
+
- - - ), + 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: ( - - - - ), + 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: ( - - - - ), + 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: ( - - - - - ), - }, - { - 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: ( - - - - ), + 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 ( +
+

+ {group.category} +

+
+ {group.items.map((item) => ( +
+ +
+
{item.name}
+
{item.description}
+
+
+ ))} +
+
+ ); +} 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 */} -
- -
-
-

- The Micromelon Rover +
+
+ Micromelon Rover +
+
+ +
+

+ Micromelon Rover

-

- 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.

-
- -
-
- Micromelon Rover -
-
- + +

{/* Activities */} -
+
-
- {activities.map((activity) => ( - -
{activity.icon}
-

- {activity.title} -

-

- {activity.description} -

-
- ))} + +
+
- {/* Teachable Concepts */} -
+ {/* Built for the Classroom */} +
-
- {teachableConcepts.map((concept) => ( - + {classroomCards.map((card) => { + const content = ( + <> +
+ {card.title} +
+
+

+ {card.title} +

+

+ {card.description} +

+
+ + ); + + return card.href ? ( + + {content} + + ) : ( +
+ {content} +
+ ); + })} +
+
+
+ + {/* Learning Pathway */} + + + {/* Code Editor CTA */} +
+ +
+

+ Curious How To Program The Rover? +

+

+ 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. +

+
+ - {concept} - - ))} + EXPLORE THE CODE EDITOR + +
+
+ Micromelon Code Editor showing block and text coding side by side +
- {/* 3D Printing */} -
+ {/* 3D Printed Attachments */} +
-
-

- Use 3D Printing to go further! +
+

+ Ready for More? Expand Your Rover

-

- 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. +

+ Once students are comfortable, optional 3D printed attachments open up new challenges. No printer? Buy them pre-printed.

-
- - -
- -

- - {/* Easy to Manage */} -
- -
-
- Rover class set carry case with charging dock -
-
-

- Easy to Manage -

-

- 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. -

-
+
+ {products + .filter((p) => p.category === "attachments") + .sort(() => Math.random() - 0.5) + .slice(0, 8) + .map((attachment) => ( +
+
+ {attachment.name} +
+
+

+ {attachment.name} +

+

+ {attachment.description} +

+
+
+ ))}
- -
- - {/* Add Your Own Electronics */} -
- -
-

- Add Your Own Electronics -

-

- Rovers have built-in expansion headers compatible with a range of - 3rd party electronics including Raspberry Pi, Arduino and other - sensors. -

+
+ + Free Print Files + +
{/* Tech Specs */} -
- -
-
-

- Tech Specs -

-
    - {techSpecs.map((spec) => ( -
  • - - {spec} -
  • - ))} -
-
-
- Micromelon Rover technical specifications +
+
+ +
+ +
+

Tech Specs

+

+ Everything students need to explore robotics, electronics, and programming — built into one compact robot. +

+
+
+ +
+ +
+
- {/* Related Resources */} - - - {/* Code Editor CTA */} -
+ {/* Made in Australia */} +
-
-

- Curious How To Program The Rover? -

-

- 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. -

-
- +
+ Australian Made and Owned +
+

+ Designed, Tested & Made in Australia +

+

+ Every Micromelon Rover is designed, assembled, and tested right here in Australia. +

diff --git a/src/app/store/page.tsx b/src/app/store/page.tsx index 4f37c5e..790b5d2 100644 --- a/src/app/store/page.tsx +++ b/src/app/store/page.tsx @@ -666,7 +666,7 @@ export default function StorePage() { )} {attachmentItems.length > 0 && ( -
+

Attachments

@@ -679,7 +679,7 @@ export default function StorePage() { )} {parsedStudents > 0 && ( -
+

Software

@@ -692,7 +692,7 @@ export default function StorePage() { )} {sparePartItems.length > 0 && ( -
+

Spare Parts

diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index ebf89e4..c29297c 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -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 ( -
+
{/* Brand */} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 08715b7..cdfc90e 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -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 (
setOpen(true)} onMouseLeave={() => setOpen(false)}> {item.label} @@ -26,14 +29,19 @@ function DesktopNavItem({ item, pathname }: { item: NavItem; pathname: string }) {open && ( -
+
{item.children.map((child) => ( {child.label} @@ -47,9 +55,8 @@ function DesktopNavItem({ item, pathname }: { item: NavItem; pathname: string }) return ( {item.label} @@ -60,9 +67,13 @@ export function Header() { const [mobileOpen, setMobileOpen] = useState(false); const [mobileSubmenu, setMobileSubmenu] = useState(null); const pathname = usePathname(); + const isDark = pathname === "/python"; return ( -
+
@@ -72,13 +83,14 @@ export function Header() { width={200} height={40} priority + className={isDark ? "brightness-0 invert" : ""} /> {/* Desktop nav */}

{subtitle}

-
- {steps.map((s, i) => { - const isActive = activeStep === s.step; - return ( + {animate ? ( + + ) : ( +
+ {steps.map((s, i) => (
{i > 0 && ( )} - -

- Step {s.step} -

-

- {s.label} -

-

{s.detail}

- +
- ); - })} -
+ ))} +
+ )}
diff --git a/src/components/ui/LearningPathwayAnimated.tsx b/src/components/ui/LearningPathwayAnimated.tsx new file mode 100644 index 0000000..193556c --- /dev/null +++ b/src/components/ui/LearningPathwayAnimated.tsx @@ -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 ( +
+ {steps.map((s, i) => ( +
+ {i > 0 && ( + + → + + )} + +
+ ))} +
+ ); +} diff --git a/src/components/ui/RelatedResources.tsx b/src/components/ui/RelatedResources.tsx index 595e43d..1ed7c40 100644 --- a/src/components/ui/RelatedResources.tsx +++ b/src/components/ui/RelatedResources.tsx @@ -18,7 +18,7 @@ export function RelatedResources({ if (resources.length === 0) return null; return ( -
+