Skip to main content
James Music

Hi! I'm James. I'm blind.
I build things that work for everyone.

Software Engineer. Sound Engineer. Musician. Accessibility Expert.

25 years turning complex problems into production-grade solutions.

About Mr. Music

Born blind, I learned to think in systems before I could see them. That turned out to be a superpower.

I've spent 25 years as a software engineer, shipping everything from startup MVPs to enterprise platforms. I'm also a trained sound engineer, music producer, and multi-instrumentalist with over 30 years playing by ear.

I consult on accessibility because I live it — screen reader, braille display, the lot. When I build or advise, it works for everyone.

Book a 15-minute call

Pick a time that works for you. I'll send a Google Meet link.

Pick a day

Mon
Tue
Wed
Thu
Fri

June 2026

Notes from James

What I'm building, thinking about, and working on.

Check out in .

Building

The comment thread uses one form with two hidden inputs to serve three entity types

The portal has inline comment threads on sessions, milestones, and goals. All three use the same CommentThread component. It takes four props — a comments array, an addCommentAction server action, an entityType string that is one of session, milestone, or goal, and an entityId string. The component does not branch on entity type. It does not know whether it sits below a mixing session or an accessibility audit milestone. Two hidden inputs inside the form carry the routing information. One input holds the entityType. The other holds the entityId. When the form submits through useActionState, the server action reads both values from the FormData and uses conditional spread operators to set exactly one foreign key on the comment record — sessionId, milestoneId, or goalId. The component passes strings through. The server decides where they go. The avatar next to each comment is five chained string methods inline in the JSX — split the authorName on spaces, map each word to its first character, join into a string, slice to two characters, uppercase. Sarah Chen becomes SC. James becomes JA. The circle is 32 pixels wide with a border that changes colour based on a single boolean. When isJames is true, the border and the name text use the portal accent colour. When isJames is false, both use the muted glass colour. Two visual signals from one database field. The timestamp uses Intl.DateTimeFormat with en-GB locale, day numeric, month short, hour and minute in 24-hour format. The opacity is set to fifty percent so the time recedes behind the comment body. The time element has a machine-readable ISO datetime attribute for crawlers and assistive technology. The form input is a single text field with a thousand-character maxLength, a 44-pixel minimum height for WCAG touch compliance, and a placeholder that reads Add a comment. The label is visually hidden with sr-only but present in the accessibility tree. The submit button disables during pending state and shows three dots instead of Send. On the project detail page, a single scroll can pass through five or six independent CommentThread instances — one per milestone, one per session, one per goal — each with its own form, its own hidden inputs, its own useActionState hook. They do not share state. They do not interfere. One component, two hidden inputs, three entity types, zero conditional rendering.

softwareaccessibility
Building

The intake form validates on blur so the error appears before the submit button

Every service page has a contact form in the sidebar. All five — software, audio, production, accessibility, musician — render through the same ServiceIntakeForm component. The form validates each field individually when the client tabs or taps away from it, not when they hit submit. The handleBlur function receives the field config and the current value, runs validateField, and stores the result in an errors state object keyed by field name. A separate touched state object tracks which fields the client has interacted with. A required field that has never been focused does not show an error even though it is empty. The error only appears after the first blur. The client sees what is wrong as they fill in the form, field by field, not in a batch after they think they are done. When an error is present, aria-invalid is set to true on the input and aria-describedby points to the error paragraph's ID. A screen reader announces the problem immediately when focus leaves the field. The error ID is constructed from the field name plus -error so every instance is unique even when multiple fields fail simultaneously. The error paragraph has role alert so assistive technology treats it as a live region and reads it without the client needing to navigate to it. The honeypot check runs before any validation on submit. If the invisible website field has a value, the handler returns silently. No error message, no state change, no fetch call. The bot thinks it submitted. It did not. The submission guard is a useRef, not a useState. A ref read is synchronous — two rapid clicks both see the current value before either can change it. A state read is batched — two clicks could both read false before either update lands. The ref flips to true before the fetch call and back to false in the finally block. The entire form is driven by a FieldConfig array passed as a prop. Each config has a name, label, type, required flag, placeholder, and optional options array for selects. The component maps over the array and renders the right HTML element — input, select, or textarea — based on the type string. Five different service pages pass five different field arrays to the same component. The audio form asks for genre and track count. The software form asks for project type and budget range. Adding a new field to any service means adding one object to the array on that service page. No component change, no API update, no deployment beyond the page file.

softwareaccessibility
Building

The project showcase structures every case study as a four-part narrative

The data/projects.ts file defines every portfolio project with eight fields and a nested detail object containing four prose sections — challenge, approach, outcome, and techDeepDive. The eight top-level fields cover scanning. An id, a title, a subtitle, a description, a techStack array, an optional liveUrl, a metrics array, and a category string. The four nested fields cover reading. A visitor scanning the project cards sees the title, the category label, the metrics badges, and the tech stack pills. A visitor who clicks through sees the full story in four structured paragraphs. The separation from the software page's inline caseStudies array is deliberate. That array stores six-field marketing summaries designed for a three-second scan — title, category, optional URL, description, metrics, and techStack. The projects.ts file stores the deep breakdowns with narrative paragraphs that explain why, not just what. Different files for different audiences at different levels of commitment. Each project tells a story. The challenge section explains the problem — music teachers needed a unified platform, no existing solution combined all these needs. The approach section describes the architectural decisions — multi-tenant Next.js, role-based access, Prisma with PostgreSQL, NextAuth, Stripe Connect for multi-mentor payouts. The outcome section quantifies the result — shipped in eleven days, five milestones, twenty-eight phases, one hundred and thirteen individual plans executed. The techDeepDive section names every integration and every tool choice so a technical reader can assess the stack without asking. The metrics arrays carry the numbers that do not need paragraphs. Idea to launch in eleven days. Sub-second latency. Real-time AI responses. These sit as accent-coloured badges on the project cards so the achievements are visible before anyone reads a word. The techStack arrays list specific libraries — Next.js 14, TypeScript, Prisma, PostgreSQL, Stripe, Resend, Socket.io, Claude API, TikTok Live API — because vague labels like modern frontend framework tell a hiring manager nothing. The category labels position each project — SaaS Platform, Real-Time Streaming Platform, AI Product — so the reader knows the scale before they know the detail. Three projects, four narrative sections each, twelve paragraphs of context. One data file and every portfolio entry reads like a case study because the data structure forces it to be one.

software

If anything I've built or shared has been useful to you...

Buy me a coffee

Stay in the loop

Occasional updates on what I'm building, mixing tips, and accessibility insights. No spam.