A medical platform connecting hospitals and patients worldwide. I worked on the main site, per-hospital SaaS websites, and the internal admin. The team included overseas developers and QA, so issues and PRs were written in English.
Frontend Developer · SaaS projects (owned the website builder and booking system)
GitHub Issues/Projects, two-week sprints, team wiki documentation, Slack
Next.js
React
TypeScript
SWR
React Hook Form
Framer Motion
Tailwind CSS
next-auth
Storybook
Services
SaaS: a website builder that gives each hospital its own domain site. Owner
Admin: internal tool for hospitals, doctors, bookings, and content. Legacy migration
iCH: the main site for hospital search and booking. SEO improvements
SaaS · 2023.06 ~ 2024.12
A SaaS that builds a website for each hospital
I owned the SaaS frontend, from feature work to release management. The core challenges were 40+ languages including Arabic, and per-hospital domains.
Page editor and renderer
Hospitals edit pages and multi-level header and footer menus in the admin, and the SaaS renders them as is. Menu order changes by drag and drop, and each item can pick a phone, email, or location icon. Reorderable dynamic forms are built with useFieldArray.
Top: admin header and footer menu editors / Bottom: multi-level header rendered on a live hospital site (hospital details blurred)
40+ languages, LTR and RTL
Tailwind direction classes like pl and mr don't flip in RTL, so dir ternaries were spreading through the code. I documented rules for switching to CSS logical properties (ps, pe, start, end) and the responsive priority pitfalls in the wiki and shared them with the team. Translation for all 40+ languages was automated with GPTs.
Global consultation booking system
I refined the booking screens with Framer Motion interactions and designed walk-in booking, cancellation, and refund flows with the backend at the API level. I owned booking development while clients grew from about 50 to over 1,000.
Pages search engines can read, and faster
Pages that needed search visibility but were rendered client-side moved to SSR and SSG, and rarely changing SSR pages moved to SSG with On-Demand Revalidation.
Sequential server-side API calls were parallelized with Promise.all, or Promise.allSettled where the page must render even if some fail, cutting initial HTML load from 1.79s to 1.2s (~33%). I also fixed the Azure Search based sitemap generation to recover ~5% of missing pages and added per-language alternate links.
Admin · 2023.03 ~ 2023.06
Moving a legacy admin to SWR and React Hook Form
The admin had passed through many hands: 1,000+ line components, copy-pasted code, and a dependency on Redux and the unmaintained Formik. I proposed switching to React Hook Form in a sprint meeting, it was adopted right away, and I took on the detail page migration and form conversion.
289kB → 155kB
First Load JS shared by all
208kB → 47.3kB
_app chunk
363kB → 201kB
First Load JS on many pages
next build output, Before on the left and After on the right (paths blurred). Redux → SWR, Formik → React Hook Form, react-select removed
A hook that reads shared data without knowing the search params
SWR stores data using the full API URL as its key. Admin URLs carried search params like ?page=1&limit=10, so reusing data another component had already fetched meant passing the same params down through props.
I set a rule to name data when fetching it and built a hook that finds the key in the SWR cache by that name and reads the data, removing the prop drilling. It was used in 18 admin files, and I documented why and how in the team wiki. (2023.04-05)
// Parent: name the data when fetching it
getArticleContributorsSWR(searchOption, 'getArticleContributors')
// Child: read it by name, without the search params
const { data } = useSWRGetCacheData('getArticleContributors')
// Revalidating by name is also built into SWR 2.0
mutate(key => typeof key === 'string'
&& key.includes('getArticleContributors'))
I also built revalidation by name, which SWR 2.0's mutate filter function could already do. Today I'd use array keys with a fixed name prefix and a custom hook that reads search params from the URL, removing the need to search the cache at all.
Duplicate submits that disabled didn't stop
Clicking save quickly stored the same data twice. Routing inside the SWR mutation's onSuccess meant React Hook Form's isSubmitting didn't wait for that promise, so the button re-enabled too early.
await the upsert inside the function passed to handleSubmit
disabled={isSubmitting} on the submit button
await router.push outside onSuccess
Type the upsert prop as returning a Promise
Shared in the team wiki and on my blog. Duplicate delete requests were fixed the same way
Troubleshooting
Problems I traced to the root
Many problems didn't end in the frontend. I wrote down reproduction steps and evidence and solved them with backend and QA in the same issue.
Seeing someone else's session after logging in
Admin, next-auth, 2024.06-07
Symptom
Logging in as account A sometimes put account B's email in the session. It reproduced on a colleague's PC that had never used that account, so it wasn't a frontend cache issue. Narrowing it down, it happened when someone opened the Logo tab in web app settings and another person logged in before the server restarted.
Cause
setInterceptor(session), written for the browser, was also called in an API route. The server process is shared by all users, and the cleanup code sat after a return, so that person's token stayed on the shared axios instance.
axios runs request interceptors after merging call options. Even though next-auth passed the user's own token in the header when calling userinfo at login, the leftover interceptor overwrote it last, fetching user info with someone else's token.
Fix
Moving the cleanup before return stopped tokens from lingering, but a concurrency gap remained while awaiting. So API routes stopped using the shared instance: getServerInstance(req) creates an axios instance with the token for each request and passes it to service functions. I also added an ESLint rule that blocks changing interceptors in server code.
Result
I wrote a test that calls userinfo 50 times concurrently with different tokens and compares the returned emails. Before the fix, responses got mixed up; after, all matched. The test stays in place to catch the same mistake.
Proving why Google search visibility dropped
iCH, SEO, 2024.02
Context
Search visibility of the main site kept dropping. Lazy loading was the common guess, but I focused on the fact that most SEO keywords come from descriptions entered in the admin.
Check
I counted translations in the production DB whose description was empty or under 200 characters: about 30K for hospitals, 340K for doctors, and 46K for specialties.
Follow-up
I split the work into issues: description validation in the admin, per-field translation to cut costs, and replacing meaningless div and span tags with semantic ones.
Table names and counts blurred
Subdomain hosting cost that grew with every hospital
SaaS, Azure Application Gateway, 2024.07-11
Problem
Each demo subdomain SaaS was deployed separately per hospital, so hosting cost grew with every new hospital.
Design
I proposed routing many subdomains through one Application Gateway to a single CSR App Service without changing URLs, and validated it in a test environment. Hospital info kept in runtime environment variables was replaced by a metadata API call.
Docs
I documented the setup and the arm64 Docker image issue on M1 Macs (solved with buildx) in the wiki.