Skip to content
Web systemClient project2026

EA Central

Wholesale e-commerce with ordering via WhatsApp — React storefront, admin panel, and Node/PostgreSQL API

React 18TypeScriptVite 6React Router 7CSS custom propertiesNode.js 20
Runs onWebMobile (responsive)Admin panel PWA
eacentral.com.br
products in the catalog
349
device models across 6 brands
293
parts: storefront, panel, and API
3
of prices recalculated on the server
100%

01

Overview

EA Central Atacados is an electronics and accessories distributor in Brás (São Paulo) that used to sell through a PDF catalog and a list on WhatsApp. The project started as a single-page HTML prototype and was rebuilt by me as a three-part full-stack application: the React 18 + TypeScript storefront (Vercel), the Express + Prisma + PostgreSQL API (Railway), and the admin panel, in the same front end, where the client manages products, categories, brands and device models, banners, orders, and settings. The wholesale business rule lives on the server: the quantity price table belongs to the product, not the model — 100 screen protectors for the A05 and 100 for the iPhone 13 pay the 200 tier —, and every order is recalculated from the database before it becomes a WhatsApp message. The catalog was imported from the original PDF with a custom extraction and classification pipeline, and the site is already running in production with real orders arriving through the panel.

My role

Solo full-stack developer — domain modeling, REST API with Prisma/PostgreSQL, React/TypeScript front end for the storefront and the panel, catalog import, integrations (WhatsApp, SuperFrete, Meta Pixel/CAPI, Pix), Vercel + Railway deployment, and continuous evolution driven by the client's orders.


Category

Web system

Year

2026

Status

Client project

Platforms

WebMobile (responsive)Admin panel PWA

The storefront and the panel are the same React front end; the API is the only one that talks to the database and the only one that decides prices. WhatsApp is the closing channel, not the system.

Storefront + Admin panel

React 18 · Vercel

TypeScriptReact Router 7Vite

REST API

Express · Railway

PrismaZodJWTPino

Data and media

PostgreSQL + volume

Postgres 16UploadsMigrations

Sync: POST /api/orders recalculates the order from the database and returns the WhatsApp URL with the message ready.


02

The Problem

The store owner served customers on WhatsApp with a PDF catalog: the customer sent a list by voice message, the sales consultant calculated wholesale prices by hand, got the discount tier wrong, and lost orders in the conversation. There was no filter by device model — the main criterion for anyone buying cases and screen protectors — and no record of orders outside the salesperson's phone.

03 — The Solution

A fast storefront built for people putting together big orders: a catalog filtered by category, brand, and device model, a product page with per-model prices and discount tiers, a persistent cart with color and version selection, shipping quotes by CEP (Brazilian postal code), and a button that registers the order in the API and opens WhatsApp with the message ready. On the other side, a panel where the client maintains the catalog and tracks orders, with export for picking and checking.


04

Inside the product

The system in action, recorded from the real application with a demo store — no screen is a mockup.

01

Wholesale pricing

The discount tier kicks in on its own when the volume closes

The customer adds screen protectors for several models; the cart sums everything by product and says how much is left for the next tier. Once it goes past 200 units, the price of each line drops — and the server recalculates the entire order before generating the message.

  • Table per product, built in the panel with just the minimum quantity of each tier
  • Color selection with per-unit distribution and men's/women's version
  • Real shipping rates by CEP (SuperFrete) or in-store pickup
Wholesale pricing
02

Compatibility

Filter by device and price per model

People buying a case search by phone, not by product name. The catalog filters by brand and model, and the product page shows the models as chips in the release order defined in the panel — with a different price when a model costs more.

  • 293 models across 6 brands, global registry and per-product linking
  • Natural order (NOTE 7, NOTE 8, NOTE 8 PRO…) instead of alphabetical
  • Price exception per model registered only for the ones that depart from the standard
Compatibility
03

Admin panel

A form that changes with the category

A case asks for device model and colors; a cable asks for a connector; a speaker asks for none of that. Three flags on the category govern what appears, and a shortcut reveals the hidden section when the item breaks the rule.

  • Price tiers with the end of each one calculated from the next
  • Compatible devices with search and per-brand selection
  • Upload validated by content, rotation fixed, and downscaling to 1600 px
Admin panel
04

Orders

Status, item-by-item detail, and export for picking

Each order stores the name and price at the time of purchase. The panel shows the items with model, colors, and tip, changes status in one click, and exports the filtered selection to Excel or PDF — a list to check the month, or one order per page for picking in the stockroom.

  • Export walks through up to 2,000 orders, not just the loaded page
  • XLSX with values as numbers and dates as dates
  • PDF with repeated header, zebra striping, and marked continuation
Orders
05

Mobile

Built for people putting together the order on the phone

Adding doesn't open the drawer: a notice with a shortcut appears and the counter pulses. The drawer, when opened from the icon, is a bottom sheet with the page visible behind it. In the panel, ↑ and ↓ replace dragging for people editing the model order with their finger.

  • Persistent cart — a refresh doesn't wipe a list of 15 products
  • Floating WhatsApp button on every page
  • Admin panel PWA with offline page
Mobile

05

6 video walkthroughs

Each video shows a complete task, end to end. Click to watch with controls.


06

Key Features

01

Progressive pricing by quantity

Each product has its own table (1–99 · 100+ · 300+). The tier is earned by the sum of all models of the same product in the cart, which shows how much is left for the next step. The server validates the table and recalculates everything on the order.

02

Brand → model compatibility

A global registry of 6 brands and 293 models, linked product by product. The catalog filters by device, the product page shows the models as chips in the order defined in the panel and allows a price exception per model (the A16 case costs more).

03

Wholesale cart

Persisted in localStorage, grouped by product, with the newly added item on top. Color selection with per-unit distribution, men's/women's version, connectors (USB-C tip, Micro USB, Lightning), and real shipping rates by CEP via SuperFrete or in-store pickup.

04

WhatsApp checkout

POST /api/orders registers the order, recalculates prices from the database, generates the code, and returns the WhatsApp URL with the message ready. The consultant checks the item-by-item detail in the panel; the conversation gets only the summary.

05

Store owner panel

Products with a form that changes by category (a speaker doesn't ask for a phone model), bulk selection, image upload with validation and resizing, categories, banners, testimonials, brands & models with drag-to-reorder, and site settings.

06

Excel and PDF export without a library

Order list (two sheets) or order by order (picking sheet, one per page). Both formats are written in the browser with custom code — PDF xref and zip CRC covered by tests — so there is no need to load hundreds of kB for a simple case.

07

Meta Pixel + Conversions API tracking

Browser events with cookie consent and deduplication against the events sent by the server. The CAPI token lives only in the API; the panel shows the integration status and fires a test event.

08

Security and operations

JWT + bcrypt, ADMIN/EDITOR roles, Zod on every input, Helmet, CORS allowlist, per-route rate limiting, Pino logs with credential redaction, uploads validated by file content, and a panel PWA with an offline page.


07

Product screens

Real screenshots of the product, organized by module. Click any screen to enlarge it.

Hero with banner carousel and a call to browse the catalog
Categories with image and item count
Featured products with starting-from and minimum prices
Benefits and a call to build the order
Payment methods and brands carried
Catalog with side filters and sorting
Cases filtered by Samsung → A05
Search by name or brand
Category with connector filter
Page with model chips and model search
Buy more and save: 200+ unit tier
Per-model price exception highlighted on the chips

08

On mobile

Most store owners put together their orders on their phone, at their own store's counter — the storefront and the panel were designed for that screen first.

Home
Catalog
Product
Added
Cart
Menu
Admin panel · orders
Admin panel · models

09

Technologies

Front-end

React 18TypeScriptVite 6React Router 7CSS custom properties

Back-end

Node.js 20Express 4Prisma 5Zod

Database

PostgreSQL 16

Deployment

Vercel (vitrine + painel)Railway (API + Postgres + volume)

Integrations

WhatsAppSuperFrete (frete)Meta Pixel + Conversions APIViaCEPPix BR CodeSharp (imagens)

Libraries

jsonwebtokenbcryptHelmetexpress-rate-limitPinomulterqrcode-generatorPlaywright (e2e)

10

Design Decisions

The identity came from the original prototype — amber #F7A81B on black, Archivo in heavy italic for headings — and was turned into CSS tokens reused by the storefront and the panel. Choosing color and version happens in the cart, not on the product page: picking a color in the middle of 60 model chips was too much information at purchase time. On mobile, adding doesn't open the drawer — a short notice with a shortcut appears and the counter pulses — and the drawer, when opened, is a bottom sheet with the page visible behind it. The panel shows only what the category uses and has ↑↓ shortcuts next to drag-and-drop, designed for people editing from their phone.

11

Technical Challenges

The biggest challenge was the counter's pricing policy: the discount tier belongs to the product, summing its models, the price can vary by model, and the cart has to show progress once per product. I solved it with aggregateQuantities and priceForLine shared between front end and API, with the server as the source of truth and the browser as a tested mirror. Second: the catalog arrived as a PDF, with no color, no model, and inconsistent names — a Python pipeline extracted, classified, and reviewed the items, and the panel gained bulk actions (color option, tip, weight, and dimensions) so the client could finish the registration without opening 349 forms. Third: Excel/PDF export in the browser without a library, with one order per page, line wrapping instead of ellipses, and a marked continuation when an order doesn't fit on the sheet.


12

Results

  • Site in production at eacentral.com.br with 349 products, 14 categories, and 293 device models entered by the client in the panel
  • Wholesale orders arrive registered and recalculated on the server — the consultant checks them in the panel instead of deciphering voice messages
  • Catalog imported from the original PDF with a custom extraction and classification pipeline, with no manual typing
  • Self-sufficient client: prices, tiers, models, banners, and shipping are edited in the panel, with no deploy

13

Learnings

  • Pricing rule on the server: the browser displays, the database decides. pricing.ts exists on both sides, with a test that compares the two implementations case by case — the total on screen is the total on the order
  • Four attributes as real relations (colors, tiers, compatibility, connectors) instead of a generic EAV: fast filtering in Postgres, and the panel gets a screen for each one instead of a key/value form
  • Connector modeled as connector + side (input/output), not as a closed pair like 'USB-A → USB-C' — a 3-in-1 charger has three outputs, and registering a new connector is one row in the table
  • Reordering lists writes the entire list in a single transaction: moving one item changes the position of everything between origin and destination, and one request per row would leave the list half-saved if the 4G dropped
  • Writing XLSX and PDF by hand is worth it when the case is small and well defined — and only with tests on the xref and the CRC, because a wrong byte doesn't throw an error on generation, it gives 'corrupted file' in the store owner's hands
  • The client sends orders as voice messages and screen recordings; turning that into structured prompts for development became part of the project's process