// pages.jsx — Otherworld.dev page components
// ─── Shared bits ────────────────────────────────────────────────────────
function Brand({ size = 26 }) {
return (
otherworld/ adam morgan
);
}
function Nav({ route, go }) {
const links = [
{ id: "home", label: "Index", path: "/" },
{ id: "markaloud", label: "Markaloud", path: "/markaloud" },
{ id: "work", label: "Work", path: "/work" },
{ id: "about", label: "About", path: "/about" },
{ id: "contact", label: "Contact", path: "/contact" },
];
const [open, setOpen] = React.useState(false);
const onLink = (id) => (e) => {
e.preventDefault();
setOpen(false);
go(id);
};
return (
);
}
function Footer({ go }) {
return (
© 2026 Otherworld Dev Ltd · Registered in England & Wales · Swansea, UK.
);
}
// Random-but-stable waveform values
function makeWave(n, seed = 7) {
const out = [];
let s = seed;
for (let i = 0; i < n; i++) {
s = (s * 9301 + 49297) % 233280;
const r = s / 233280;
// shape so middle bars are taller
const center = 1 - Math.abs(i - n / 2) / (n / 2);
const h = 12 + r * 48 * (0.6 + 0.4 * center);
out.push(Math.round(h));
}
return out;
}
function Waveform({ progress = 0.42, live = 0.46 }) {
const bars = React.useMemo(() => makeWave(56, 5), []);
return (
{bars.map((h, i) => {
const t = i / bars.length;
let cls = "bar";
if (t < progress) cls += " played";
if (Math.abs(t - live) < 0.018) cls += " live";
return
;
})}
);
}
// ─── Markaloud product mock (audio + AI-structured feedback) ────────────
function MarkaloudMock() {
return (
M
Final Viva — Eleanor Hughes
PHIL3402 · Submitted Tue 14:22 · 12:48 long
Reviewing
00:00
05:54
12:48
05:54
Strength
Clear framing of Kant's transcendental argument — pair this with a counter-example next time to score the top band.
07:21
Action
Citation drift around minute 7. Re-listen and tighten attribution; rubric criterion B2.
11:02
Next
Strong recovery in the conclusion. Suggest extending this thread into the written commentary.
▶
05:54 / 12:48
Rubric · QAA-aligned
);
}
// ─── Markaloud feature (three treatments) ───────────────────────────────
function MarkaloudFeature({ treatment, go }) {
if (treatment === "manifesto") {
return (
Featured product · Markaloud
Marking a viva should take minutes , not hours —
and feedback should feel like coaching ,
not a verdict.
Markaloud is an academic audio-marking tool. Educators upload a viva or oral assignment;
our model timestamps strengths, flags rubric gaps, and drafts constructive comments students
can actually act on.
Sector
UK higher education
Approach
Constructive · rubric-aligned
);
}
if (treatment === "split") {
return (
● Featured product
Markaloud.
Audio marking, {" "}
without the dread.
Markers record once. Markaloud transcribes, timestamps, and structures
constructive feedback against your rubric — so students get coaching, not just a grade.
Hourssaved
per marker, per cohort
Pilots
UK higher-ed · 2026
);
}
// spotlight (default)
return (
● Featured product · Live
Markaloud turns viva
recordings into constructive feedback.
An academic audio-marking tool for assignments and vivas. Upload once;
Markaloud timestamps the moments that matter and drafts feedback that's
aligned with your rubric — and your students.
Hourssaved
Per marker, per cohort
Pilots
UK higher-ed · 2026
);
}
// ─── Hero ────────────────────────────────────────────────────────────────
function Hero({ go }) {
return (
Now — shipping Markaloud beta
Engineering
for the messy,
physical world.
I'm Adam Morgan . I run
Otherworld Dev Ltd — a
software, AI & robotics engineering practice. Currently Lecturer in
Robotics at Swansea University; going
full-time independent from July 2026 .
Disciplines
Software · AI · Robotics · Hardware
Available
● From July 2026
Research
5 published · 1 in progress
arm-04 · pick + place · cycle 003
● LIVE
);
}
// ─── Work cards (used on home + work page) ───────────────────────────────
const WORKS = [
{
n: "01",
ttl: "Markaloud",
desc: "The featured product — academic audio-marking for vivas and oral assessments. Whisper-based transcription, rubric-aligned feedback, time-pinned comments.",
tags: ["AI", "SaaS", "Featured"],
href: "/markaloud",
vis: (
{Array.from({ length: 32 }).map((_, i) => {
const h = 12 + ((i * 13) % 70);
const lit = i > 10 && i < 18;
return ;
})}
05:54 · STRENGTH
),
},
{
n: "02",
ttl: "Shopping List",
desc: "A shared shopping list for Nextcloud — Vue 3 + TypeScript + PHP. Real-time sync, offline mode, ingredient parsing, auto-area detection.",
tags: ["Software", "Vue", "Nextcloud"],
href: "https://github.com/otherworld-dev/Shopping-List",
vis: (
{/* Cards: each row = a shopping item with a category color bar */}
{[
{ y: 14, w: 130, c: "var(--accent)", chk: true, cat: "PRODUCE" },
{ y: 32, w: 110, c: "var(--accent)", chk: true, cat: "PRODUCE" },
{ y: 50, w: 144, c: "#a78bfa", chk: false, cat: "DAIRY" },
{ y: 68, w: 122, c: "#a78bfa", chk: false, cat: "DAIRY" },
{ y: 86, w: 136, c: "#f59e0b", chk: false, cat: "BAKERY" },
].map((row, i) => (
{/* Checkbox */}
{row.chk && (
)}
{/* Item name + qty */}
))}
5 ITEMS · 3 AREAS
),
},
{
n: "03",
ttl: "Bifrost",
desc: "PyQt5 control GUI for a 6-axis robot arm — forward & inverse kinematics, 3D visualisation, sequence recorder. Talks RepRapFirmware over USB serial.",
tags: ["Robotics", "Python", "PyQt5"],
href: "https://github.com/otherworld-dev/Bifrost",
vis: (
{[[40,100],[40,78],[78,50],[118,44],[142,58],[156,50]].map(([x,y], i) => (
))}
TCP
),
},
{
n: "04",
ttl: "RSIPI",
desc: "Real-time KUKA control over the Robot Sensor Interface protocol — Python at 250 Hz, UDP/XML, trajectory generation, safety limits. MIT-licensed library.",
tags: ["Robotics", "Python", "Real-time"],
href: "https://github.com/otherworld-dev/rsi-pi",
vis: (
PC
KR
CTRL
RKorr
RIst · IPOC
4ms · 250Hz
{[80,90,100,110,120].map((x, i) => (
))}
),
},
{
n: "05",
ttl: "Drone tracker",
desc: "Real-time drone detection & tracking with a custom YOLOv11n model and a global-shutter USB camera. Two-state FSM: scanning → tracking, with offset logging.",
tags: ["AI", "Computer Vision", "Python"],
href: "https://github.com/otherworld-dev/Drone-Tracking",
vis: (
drone 0.89
● TRACKING
),
},
{
n: "06",
ttl: "ThorRR",
desc: "An open-source 6-DOF printable robot arm, forked from Thor and redesigned around RepRapFirmware + the Octopus Pro board. Stretches to 625 mm, ≤ £350 BOM.",
tags: ["Hardware", "3D Print", "Open Source"],
href: "https://github.com/otherworld-dev/ThorRR",
vis: (
{[
{ y: 96, w: 70, label: "BASE" },
{ y: 80, w: 56, label: "J1" },
{ y: 66, w: 44, label: "J2" },
{ y: 52, w: 38, label: "J3" },
{ y: 38, w: 30, label: "WRIST" },
{ y: 24, w: 22, label: "TOOL" },
].map((s, i) => (
{s.label}
))}
625 mm · 6-DOF
),
},
{
n: "07",
ttl: "DrillCam",
desc: "High-speed camera app for monitoring drilling — 309 fps on a global-shutter OV9281, lossless FFV1 capture, FFT vibration analysis, sub-pixel motion tracking.",
tags: ["AI", "Imaging", "Raspberry Pi"],
href: "https://github.com/otherworld-dev/Drill-Cam",
vis: (
{Array.from({ length: 38 }).map((_, i) => {
const x = 14 + i * 4.6;
const phase = i * 0.62;
const yMid = 40;
const a = 16 * (0.5 + 0.5 * Math.sin(i * 0.18));
const y1 = yMid - a * Math.sin(phase);
const y2 = yMid - a * Math.sin(phase + 0.62);
return 8 && i < 22 ? "var(--accent)" : "var(--fg-2)"} strokeWidth="1.2"/>;
})}
VIBRATION · 187Hz
309 fps
OV9281 · global shutter
),
},
];
function WorkGrid({ limit }) {
const items = limit ? WORKS.slice(0, limit) : WORKS;
const [stats, setStats] = React.useState({});
React.useEffect(() => {
let cancelled = false;
const ghRepos = items
.map(w => w.href || "")
.filter(href => href.startsWith("https://github.com/"))
.map(href => href.replace("https://github.com/", "").replace(/\/$/, ""));
if (!ghRepos.length) return;
Promise.all(ghRepos.map(async repo => {
try {
const r = await fetch(`https://api.github.com/repos/${repo}`);
if (!r.ok) return null;
const data = await r.json();
return [repo, {
stars: data.stargazers_count,
lang: data.language,
updated: data.pushed_at,
}];
} catch { return null; }
})).then(results => {
if (cancelled) return;
const map = {};
for (const r of results) if (r) map[r[0]] = r[1];
setStats(map);
});
return () => { cancelled = true; };
}, []);
function formatAge(iso) {
if (!iso) return null;
const d = Math.max(0, (Date.now() - new Date(iso).getTime()) / 86400000);
if (d < 1) return "today";
if (d < 2) return "yesterday";
if (d < 30) return `${Math.round(d)}d ago`;
if (d < 365) return `${Math.round(d / 30)}mo ago`;
return `${Math.round(d / 365)}y ago`;
}
return (
{items.map((w, i) => {
const isExternal = w.href && w.href.startsWith("http");
const isGh = w.href && w.href.startsWith("https://github.com/");
const repoKey = isGh ? w.href.replace("https://github.com/", "").replace(/\/$/, "") : null;
const s = repoKey && stats[repoKey];
const inner = (
{w.n} / {WORKS.length.toString().padStart(2, "0")}
{w.badge && (
{w.badge}
)}
{s && (
★ {s.stars}
{s.updated ? · {formatAge(s.updated)} : null}
)}
{w.vis}
{w.ttl}
{isExternal ? "↗" : "→"}
{w.desc}
{w.tags.map(t => {t} )}
);
const className = "work-card fade-in";
const delay = Math.min(i + 1, 5);
const style = { textDecoration: "none", color: "inherit", cursor: "pointer" };
if (w.href) {
return (
{inner}
);
}
return
{inner}
;
})}
);
}
// ─── Clients ─────────────────────────────────────────────────────────────
const CLIENTS = ["Thor Community", "ThorRR", "Bifrost", "Nextcloud", "KUKA RSI", "OV9281"];
function Clients() {
return (
{CLIENTS.map((c, i) => (
{c.toUpperCase()}
))}
);
}
// ─── Contact CTA (home strip) ────────────────────────────────────────────
function ContactCTA({ go }) {
return (
Open to work
Have a hard, weird,
cross-discipline problem?
I take on a small number of consulting and build engagements each year.
If yours bridges hardware, software, and people — I'd love to hear about it.
Engagement Build · Advise · Diligence
Disciplines Software · AI · Robotics
Response Within 48 hours
Based Swansea, UK · remote / hybrid
);
}
// Export to window for cross-file access
Object.assign(window, {
Nav, Footer, Hero, MarkaloudFeature, MarkaloudMock, WorkGrid,
Clients, ContactCTA, Brand, WORKS, CLIENTS, Waveform,
});