Case study · Frontend
FRREDSS
Siting biomass power plants across California, from a map.
- Role
- Led the frontend from 2024: the rebuild, saved runs, map layers and the data loader
- Where
- UC Davis
- Frontend
- React, TypeScript, Leaflet, Recharts, Bootstrap
- Year
- 2024 to 2025. Live at forestdss.ucdavis.edu

Summary
FRREDSS answers a planner's question: if we build a biomass power plant here, where does the wood come from, what does it cost and what does it emit? It runs ten models over forest data for the whole state. I took over a frontend that had been quiet for two years and made it a routed app with runs you can save and share.
94%
of commits since July 2024 are mine
10
models behind every run, from harvest cost to life-cycle emissions
11
map layers, from fire hazard zones to sawmills
4
major upgrades in one rebuild: React, Leaflet, Router and TypeScript
My part
What I shipped
I joined in July 2024 and led the frontend from there, with a backend API and a data loader on the side. The repo had been quiet since 2022.
- Rebuild
- React 17 to 18, react-leaflet 2 to 4, React Router 5 to 6 and TypeScript 3 to 4.9, then one map page split into routes.
- Landing page
- A home page with the models, the publications and the app's features, plus analytics.
- Saved runs
- A save flow that collects user details, stores the run and returns a short link that reopens it with every input in place, front end and API.
- Map
- Place search, new sources for the fire hazard and substation layers, two new layers, and a legend tied to the map.
- Safety rails
- A warning before leaving a running model, validated forms, and Jest for tests.
- Data
- Scripts that split statewide data by county and bulk-load it into PostgreSQL with COPY, alongside a batch job on the campus cluster that prepares the clusters.
01 · Landing page
A front door for a research tool
The old app opened straight onto the map. The new home page says what the models do and links the research behind them before anyone sets an input.



02 · Product
Pick a site, set the inputs, run
The facility goes where you click or search. Inputs cover the forest treatment, the harvest system, transport and the plant itself, each starting from a sensible default.


03 · Design system
Forest green on paper white
- Brand#385542
- Hover#2F4538
- Action#069E39
- Mint#E4FDE6
- Paper#F0F8F1
- Error#FC031C
- Substation#3388FF
System UI
Medium Density Target (SDI-55)
Bootstrap's system stack. No web fonts in a form-heavy tool people keep open all day.
- Bootstrap 4 with reactstrap, so every input, select and validation message behaves the same way.
- One brand green for headers, buttons and the hero overlay, with pale greens for backgrounds.
- The hero is a slow aerial video under a translucent panel, so the headline stays readable over moving canopy.
04 · Tech
Ten models, one map
1 · Browser
React and Leaflet
TypeScript, react-leaflet, Recharts and Victory for inputs, maps and results. The models run on the API.
2 · API
Express on Azure
Runs the models. One call sets up the whole run, then one call per year carries the chosen clusters and the search radius forward.
3 · Data
PostgreSQL and PostGIS
Treated forest clusters by county and year, boxed by latitude and longitude in SQL, then trimmed to a radius in Node.
4 · Pipeline
Batch loads on the cluster
A SLURM job on the campus cluster prepares the clusters; a separate script loads county files with COPY and moves loaded and failed files aside, so a rerun picks up where it stopped.
Routes before features
Moving to React Router first gave saved runs somewhere to live: /pages/model/:modelID.
Runs as short links
A run is stored on the server and shared as a short link that restores the model, the inputs and the map points.
Don't lose a run
A run can take a while, so leaving the page mid-run asks first.
- React 18
- TypeScript
- Leaflet
- Recharts
- Victory
- Bootstrap
- Express
- PostgreSQL
- PostGIS
- Azure
- Python
- SLURM
05 · Code
Open a shared link, get the same run
A saved run lives at /pages/model/:modelID. On load, the page fetches it and puts back the model, every input and both sets of coordinates before anyone touches the map.
Lines 614 to 640, unedited apart from indentation.
useEffect(() => {
const fetchOriginalUrl = async () => {
if (modelID && !hasLoaded) {
try {
setIsLoading(true);
const result = await getShortUrlData(modelID);
setTeaModel(result.allYearInputs.teaModel);
setTeaInputs(result.allYearInputs.teaInputs);
setFacilityCoordinates({
lat: result.allYearInputs.facilityLat,
lng: result.allYearInputs.facilityLng
});
setBiomassCoordinates(result.biomassCoordinates);
setFrcsInputs(result.frcsInputs);
setTransportInputs(result.transportInputs);
} catch (error) {
console.error('Error fetching original URL:', error);
} finally {
setIsLoading(false);
setHasLoaded(true);
}
}
};
fetchOriginalUrl();
}, [modelID]);06 · Next
What I'd ship next
- 01Renew the Mapbox token, or move the base map to tiles that don't expire, so the map isn't blank.
- 02Find why a default run times out on the per-year call, and show progress year by year while it works.
- 03Store cluster centers as geometry with a spatial index and filter the radius in SQL, instead of a box in SQL and a radius in Node.
- 04Serve the substation layer from the app instead of a personal GitHub repo.
- 05A phone layout for the model page.