Skip to content
AP.
01Production

ONEUX

One Next.js application serving five of a multinational company's brands: a social feed, push notifications, missions, events and rewards, inside a portal it does not own.

Scale / counted from the repository

brands served by one codebase
5
routes rendered from 7 page files
23
lines of frontend in the repository
121,227
component domains in the repository
68
test files in the repository
223
files carrying my commits
251
commits I authored, reachable from HEAD
210
image assets behind the brand themes
923
/oneux
Role
Senior Web Developer
Period
April — September 2026
Type
Multi-brand engagement platform
Stack
Next.js App Router, React, JavaScript, Tailwind CSS, React Query, PWA & web push

01

Overview

A user of this platform earns points, checks in daily, works through missions, reserves a seat at an event, submits work for a competition, follows a social feed and redeems vouchers: one login, one app, five brands. The brands share a voice but not a colour, a corner radius or an asset set, and the whole thing runs inside a frame owned by the company's parent portal. Three constraints shaped every frontend decision in it, and none of the three were mine to design.

Where a session starts, in order

  1. 01Portal hand-off
  2. 02Age gate
  3. 03Login
  4. 04Brand preference
  5. 05Feed

02

The challenge

Pages were data. Most routes did not exist as files: a catch-all route read a schema from the API and assembled the page from it, so one component could render a different layout per brand without a deploy, and seven page files served 23 routes. Getting a new section to look right meant getting it right for a schema I did not write. The app also lived inside someone else's window. It ran in a cross-origin frame owned by the parent portal, where cookies, storage, service workers and device permissions behave differently and some are simply unavailable, so anything that assumed them broke. And the category was regulated: age and brand preference had to be confirmed before a user could interact, and the separation between brands had to hold under every feature. Compliance was a constraint on the architecture, not a page at the end of a funnel.

03

My contribution

  • Built the notification layer a user meets: the banner that asks for permission, the toggle in settings, the notification list page, and the state deciding which of those appears.
  • Held the feed as primary maintainer — posts, stories, comments, likes, audio posts, filtering, and the onboarding tour wrapped around them — a module another engineer started.
  • Owned the event detail header: the distance from a user to an event, the permission states the browser reports, and the gate that once kept the feature to installed users.
  • Wrote the analytics contract for the feed, so a name and a payload are defined once instead of being pushed at every call site.
  • Carried the per-brand pass that followed every change: one component, five palettes, five sets of assets, no forks.

04

Inside the product

  1. 01

    A page is a schema, not a file

    src/app/(app)/[[...page]]/page.js · ui_schema

    Most routes were not files. One catch-all route read a schema from the API and assembled the page from it, which is how seven page files served 23 routes and how a brand could change a layout without a deploy. Styling arrived the same way: Tailwind plus a block of CSS variables injected from the API, so the same class meant a different colour on each brand. A section was finished when it was right for a schema I did not write.

  2. 02

    The gate comes before the feed

    brand-pref-popup.jsx · community-hub-guard.jsx · login-popup.jsx

    Age and brand preference had to be confirmed before a user could interact, so the gate sits in the onboarding order rather than at the end of a funnel. The preference popup calls its completion callback only when the save succeeds, which means nobody slips past consent without recording a choice. Around it the guard lets a logged-out visitor read the feed instead of blocking them, and traffic arriving from the portal re-asks the age gate rather than waving a returning user through.

  3. 03

    Nothing assumes a browser it does not own

    use-push-notif.js · notif-provider.jsx · TopNotifBanner.jsx

    A user would install the app, grant notifications, leave the tab until the session expired, and come back to be told they were not subscribed while the device said otherwise. It never reproduced on desktop: on iOS web push exists only for an installed app, so the bug lived where most of the team could not look. The cause was load order — initialisation ran before the login check resolved, read a false value and wrote it down — plus a second copy of the subscription state, so the screen and the truth could drift apart. The fix was an effect that rebuilt state once login resolved, and one source of truth instead of two. The banner then lost its local state and the popup tucked inside it, so it could be mounted anywhere in the layout.

  4. 04

    One feed, five brands, no forks

    community-hub/** · BRAND_THEMES · BRAND_SOURCE_MAP

    The cheap route is a copy of the component per brand, or a branch on the brand name inside the markup. Both work for a week; after that every small change is five changes and the five quietly stop behaving the same. So the per-brand values became data — colour themes, the skip button on the tour, the source label, the notification category colours — read from maps instead of conditionals, and the feed stayed one component on one code path. It sat behind 923 image assets, and the list alone took 22 commits of adjustment that never touched structure.

  5. 05

    Distance behind a state machine

    detail-header.jsx · info-header-track.jsx

    The event header tells a user how far they are from the venue: haversine, metres under a kilometre and kilometres above it. The device decides the rest, and it reports more than granted or denied — idle, loading, granted, denied, unavailable — so each state gets its own sentence, because a user who refused permission and a user whose device cannot answer need different words. The feature began behind a check for installed users; once the handling was good enough for an ordinary browser, the check came off.

  6. 06

    An analytics contract other teams consume

    use-datalayer.js · community-hub/**

    Events leave through one hook rather than a push at every call site, so a name and its payload are defined once: post and story interactions, suggested profiles, the interest popup, navigation, and the call to action on a challenge. Renaming an event a dashboard already reads is the risky half, and that happens in the same change as the consumer update, never as a quiet rename that breaks someone else's report a week later.

05

Technical approach

Brand differences are data

A value that changes per brand goes into a map, never an `if (brand === ...)` scattered through markup. One lookup replaces five conditionals, and the day a sixth brand arrives it is an entry rather than an audit.

BRAND_THEMES · BRAND_SOURCE_MAP · SKIP_BUTTON_STYLES

Logic out of the view

One hook per resource, with React Query holding the cache and the optimistic update for a like or a comment; pure helpers turning an API response into a view model, so the mapping is testable without a render; components that draw what they are handed. The permission banner ended as the clearest case: no local state, no popup inside it, mountable anywhere because it no longer decides anything.

one hook per resource · *-utils.js · display-only components

Ask the browser first

On a platform like this the expensive discoveries are environmental. Whether push exists, whether storage survives, whether a permission prompt can be raised at all: inside another origin's frame, several of those answers are no. Finding that out early turns it into architecture, which is cheap. Finding it out in a ticket eight weeks later is not.

framed origin · installed-only web push · device permissions

Built with

Framework
Next.js, App RouterReactJavaScript
Interface
Tailwind CSSSCSSBrand CSS injected from the API
Data and state
React QueryREST API layerServer-rendered catch-all routes
Platform
Service worker via next-pwaFirebase Cloud MessagingInstall prompt
Test and quality
VitestTesting LibrarySnapshotsSonarQube coverage

06

Where it stands

  • 01

    The foundation is not mine

    The service worker and the FCM token manager under the notification layer were another engineer's, and so was the permission module. What I owned above them was the ordering bug and the state model, which is why the recovery case has a test.

  • 02

    Neither are the first versions

    The feed and the profile revamp were both started by other engineers. I maintained and extended them, including the switch that lets one brand run the new profile page while the rest fall back to the old one. That line stays visible on purpose: a page that claims everything reads like a page that checked nothing.

  • 03

    No screenshots

    The product is private and runs inside a portal I do not own, so the cover and any media slot stay schematic rather than pretending to be captures of something.

  • 04

    No repository to link

    Where other entries here link public code, this one cannot. The numbers are counted from a private repository, the brands are described rather than named, and no endpoint or customer figure appears on this page.

  • 05

    Test coverage is uneven

    The modules most likely to regress carry tests: the banner in each of its states, the recovery race, the notification page, the brand preference popup, and the event header with a snapshot. Everything else I touched does not, because deadlines are real and the suite was written where a mistake would be expensive.

07

Outcome

Five brands run one component layer and one code path, so a change made for one arrives for the others instead of being re-made four times. The parts I held shipped on all five. What the brands gained commercially belongs to the brand owner and stays unpublished; everything numbered above is counted from the repository.

Next project

nextanative →