Member Workshop Portal · AfifAI
Case study · Healthcare front end · 2025

A self-service workshop portal for health-organization members

Members of a large Israeli healthcare organization can browse health and wellbeing workshops by field, filter them by place, time, day and language, compare member and non-member prices, and manage their own registrations. I took it from an empty repository to a UX-complete, Hebrew-first front end in 13 days, with sign-in handed over from the organization's existing member portal.

React 18TypeScriptViteRedux ToolkitMUIreact-hook-formi18next (RTL)SSO token hand-off
Client
Large Israeli healthcare organization (confidential)
My role
Team lead & architect (hands-on)
Stage · timeline
UX-complete front end on contract-shaped mock data · Nov–Dec 2025
Status
Front end delivered
At a glance
4 to 1builds needed for local, dev, test and production. Config is read at runtime.
N to 1token-refresh calls when several requests hit a 401 at once
0extra logins. Members arrive already signed in from the existing portal.
13 daysfrom empty repo to 5 UX-complete screens
6filter facets, applied client-side with no round-trip per click
39React components in ≈6,000 lines of TypeScript and SCSS
80Hebrew UI strings. The layout is right-to-left first.
100%of commits linted and formatted by a pre-commit gate
The problem

A full workshop catalog, no self-service way in

The organization runs health and wellbeing workshops across many clinics: prenatal and breastfeeding courses, nutrition, fitness, parenting, stress management. Each workshop has several dates, instructors and languages, and two price tiers depending on the member's plan. Members needed one place to find a workshop that fits their week, see the price that applies to them, and later reschedule or cancel without calling a service center. It also had to work on a phone, in Hebrew, and without asking members to sign in again.

What I owned

Architecture and the front end, hands-on

  • Set the front-end architecture: routing with public, filtered and private layouts, state, API client, runtime configuration and i18n.
  • Designed the sign-in hand-off from the member portal: token exchange, typed user claims and per-environment login URLs.
  • Built the browse flow (home, field, workshop type, single workshop) and the “My workshops” area with reschedule and cancel.
  • Implemented the shared filter model and the responsive, right-to-left layout from the Figma designs.
  • Ran the pull-request flow and the terminology pass that aligned code and UI with the client's own vocabulary.
What I built

Five screens around one journey

Home

A hero banner, a “worth knowing” carousel that always shows whole cards, and a grid of workshop fields.

Field page

Topic chips plus a sidebar with 6 filters: location, in person or online, language, instructor, time of day and preferred days. One “Clear” resets them all.

Workshop type

Every scheduled run of a workshop, with sessions, location, instructor, member and non-member price, and seats left.

Single workshop

Meeting details, dates, language and both price tiers, with a single confirm action.

My workshops

Upcoming and past tabs. Each registration shows its cancel-by date and has cancel, change date (a modal of other runs) and add to calendar.

Architecture

One static build, configured at runtime, signed in by hand-off

The diagram is wider than the screen: swipe sideways to see all of it →

The front end ships as static files. Each environment differs only in a small JSON file that the app reads before it renders, so the same build can move from test to production unchanged. The workshops API was built separately; the UI already runs against mock data shaped exactly like its typed contract.

Design decisions

Decide the seams early, so integration is a swap

Start from the house React starter kit

Vite, Redux Toolkit, an axios lifecycle helper, i18n and lint hooks were ready on day one, so the first feature commit landed a day after the initial one. Trade-off: the starter kit carries dependencies this app doesn't need yet (maps, chat, calendar), which we left for a later clean-up.

Runtime config, not build-time env

The API base URL, mode and default language come from a JSON file loaded before the first render. One artefact serves every environment. Trade-off: one extra request at startup and a short blank frame while it loads.

Filters live in one form, at layout level

A single react-hook-form provider wraps the filter layout, so the sidebar and every page under it read the same state. The filtering itself is a memoised hook. Trade-off: filters aren't in the URL yet, so a filtered view can't be shared as a link.

Mock data in the contract's shape

Workshops, types, topics and sessions are typed models, and the mock data uses exactly those types. When the API is ready, the data source changes and the components don't. Trade-off: 15 TODO markers to track until integration.

Right-to-left first

The app root is RTL and every string goes through i18n from the start, with no hard-coded copy in components. That keeps a later Arabic or English version to a translation file. Trade-off: more care with icons, arrows and carousel direction.

Engineering log

The problems worth writing down

A two-week build still has its traps. These are the ones that shaped the code.

Nov 26

Members are already signed in somewhere else

Members sign in on the organization's main portal. Asking them to sign in again for workshops was not an option, and each environment uses a different portal address.

The portal redirects to a dedicated login route with a signed token. The app exchanges it for its own API token, decodes the claims into a typed user (plan eligibility, price tier, payment method) and opens “My workshops”. The login URL is resolved from the host name for each of 4 environments.

Nov 26

Hebrew names and a token payload inside a token

The user details arrive as a JSON string nested inside the JWT payload, and atob returns raw bytes rather than UTF-8 text, so Hebrew names come out garbled.

Normalise base64url, percent-encode each byte and decode as UTF-8, then parse twice: once for the payload and once for the nested claims. The rest of the app only sees a typed IUser.

Auth

Parallel requests, parallel refreshes

A page fires several API calls at once. When the access token expires, every one of them gets a 401, and each would start its own refresh.

A single in-flight refresh promise is shared by all failing requests. Each original request retries once with the new token. If the refresh fails, the session is cleared.

Infra

Four environments, one build

Baking environment values in at build time would mean a separate build per environment, and the build that was tested would not be the build that shipped.

A config gate component fetches appSettings.json and renders nothing until it arrives. Every API URL is computed from it lazily.

Nov 30 – Dec 4

What should “Morning + Sunday + Hebrew” mean?

With six facets, the obvious implementations disagree: does choosing two days narrow the list or widen it? And what about a workshop with no language listed?

Values within a facet are OR'ed, facets are AND'ed, an empty facet means no constraint, and a workshop missing a value is excluded only when that facet is in use. All of this is one memoised hook driven by the shared form. Try it below.

Dec 1

Carousel cards cut in half

At many widths the “worth knowing” carousel showed a half card at the edge, which reads as broken on a phone.

The visible count is computed from the container width, the fixed card width and the gap, so only whole cards are shown, and it is recalculated on resize.

Dec 2 – 3

The code's names were not the client's names

The code said “workspace”, “category” and “topic”. The client says field, workshop type and workshop run. That mismatch was starting to leak into the UI.

I did one terminology pass across components, routes and the Hebrew locale file, merged in a single reviewed pull request before integration started.

Try it

The filter rules, on synthetic workshops

This is a small re-implementation of the filter semantics with made-up data, not client code. Pick values: the list narrows across facets and widens within one.

Outcome

Where it landed

Area
Before
After
Finding a workshop
No member-facing catalog
Browse by field, 6 filters, both price tiers shown
Reschedule or cancel
Through a service center (≈3 min per call, estimate)
Self-service in “My workshops”, a few clicks
Sign-in
A separate login would have been needed
0 extra logins: token hand-off from the portal
Environments
One build per environment (4)
1 build, runtime config
Expired token, N calls
N refresh requests
1 shared refresh, each call retried once
Code quality
Style left to each developer
Every commit linted and formatted automatically

The front end is UX-complete across 5 screens and 7 routes and is waiting for the workshops API. Next steps: swap the mock sources for API calls, put filter state in the URL, and add tests around the filter hook and the token hand-off.

Stack

Technology stack

Front end
React 18, TypeScript 5, Vite 5, SCSS
State & data
Redux Toolkit thunks, axios with interceptors
UI
MUI 7, react-hook-form, react-slick, SVG icons via svgr
Auth
JWT hand-off from the member portal, access and refresh tokens
i18n
i18next with an HTTP backend, Hebrew RTL
Quality
ESLint, Prettier, Husky and lint-staged, PR reviews

Free scoping call · 30 minutes · No commitment

Bring one process. Leave with a plan and a number to beat.

Write a few lines: the process or system, what hurts today (time, errors, cost or an AI pilot that stalled) and who uses it. After a short call you get a written plan and a fixed quote within ≈3 days.

Get a free scoping callafif.mansour@gmail.com

Opens a pre-filled email · reply within one business day

The model reads. The code counts.

All projects