Back to Home

AgriLink Marketplace

Frontend Engineer & UX Researcher
In progress
March, 2025
TypeScriptNext.jsTailwind CSSFigmaEscrowZustandTanstack Query
01

Overview

AgriLink is a digital marketplace built to connect smallholder farmers directly with buyers across Nigeria. Farmers list their produce, buyers browse and order, and every transaction is protected by an escrow payment system, funds are held until delivery is confirmed by both parties.

The platform was built as part of the Enyata Community × Interswitch Hackathon, working as a team with a backend engineer and a designer. The goal was to ship a real, working product, not a prototype, within the hackathon window.

My role: Frontend Engineer. I was responsible for the full frontend, architecture, migration from the original Lovable scaffold, state management, API integration, and production build. The project is still under active development as we continue stabilizing the backend and wiring up remaining flows.

Next.js 15Framework
Tailwind v4Styling
3 RolesFarmer · Buyer · Admin
PHP + MySQLBackend
02

The Problem

Smallholder farmers in Nigeria largely sell through middlemen who set the prices and take a significant cut. The farmer gets paid less, the buyer pays more, and trust between both parties is low because there's no accountability layer.

AgriLink addresses this by removing the middleman entirely. Farmers set their own prices. Buyers see exactly who they're buying from and where the produce is coming from. And the escrow system means neither party can be short-changed, payment only moves when delivery is confirmed.

The challenge from a product standpoint: three completely different user types, farmers, buyers, and admins, each with their own workflows, navigation patterns, and permissions, all living in the same application.

03

Where We Started

The project began as a Lovable-generated Vite + React SPA. Lovable is an AI product builder, useful for rapid prototyping, but not where you want to stay for a production codebase. The generated code had everything in a single router file, no clear separation between concerns, and no meaningful folder structure.

Rather than build on top of that codebase and inherit its limitations, I made the call to migrate the entire frontend to Next.js with the App Router. The UI was preserved, components were extracted and rebuilt cleanly, but the foundation was replaced entirely.

The migration wasn't about rewriting for the sake of it. Next.js's App Router gave us nested role-based layouts, server-side route protection via middleware, and a structure that could actually scale as features were added.

04

Frontend Architecture

The core architectural decision was using App Router route groups to isolate each user role. Each group gets its own layout.tsx, the farmer gets a sidebar, the buyer gets a bottom nav, the admin gets a full management sidebar, without any manual layout switching in page files.

src/
app/── Next.js App Router root
(auth)/── login, register
(farmer)/── farmer portal + sidebar layout
(buyer)/── buyer marketplace + bottom nav layout
(admin)/── admin dashboard + management layout
components/── ui/, common/, layout/, farmer/, buyer/, admin/
store/── Zustand: authStore, cartStore, uiStore
queries/── TanStack Query hooks per domain
services/── raw Axios API calls per domain
hooks/── useAuth, useMobile
lib/── axios instance, queryClient, mockData, utils
types/── user, product, order, payment interfaces
constants/── routes, queryKeys
middleware.ts── route protection + role guards
globals.css── Tailwind v4 theme + design tokens

Route protection lives in a single middleware.tsfile at the root. It reads the auth state from a cookie, checks the user's role, and redirects before any page renders. A farmer trying to access /admin gets bounced back to /farmer, no client-side flicker, no page load.

05

State Management

State is split cleanly between two layers: Zustand for client state and TanStack Query for server state. These two things should never be mixed.

ZUSTAND

Auth Store

Holds the logged-in user, token, and isAuthenticated flag. Persists to localStorage and also writes a cookie so the server-side middleware can read it without a round trip.

ZUSTAND

Cart Store

Tracks items a buyer adds before checkout. Persists across sessions so a buyer doesn't lose their cart on page refresh.

ZUSTAND

UI Store

Sidebar open/close state and modal visibility. Shared across layout and page components without prop drilling.

TANSTACK

Query Layer

Every API domain, listings, orders, payments, users, has its own query file with useQuery and useMutation hooks. Pages import from here, never directly from the service layer.

One specific decision worth calling out: the auth store writes the session to a cookie manually on every login and signup. This is necessary because Next.js middleware runs on the server and can't read localStorage. Without the cookie, middleware would have no way to know who's logged in, and every protected route would redirect to login incorrectly.

06

API Integration

The backend is a PHP REST API with MySQL, built by the team's backend engineer. Every endpoint returns a consistent { success, message, data }envelope. Rather than unwrapping this shape in every page component, the Axios response interceptor handles it globally, by the time data reaches a query hook, it's already unwrapped.

src/lib/axios.ts
// Response interceptor, unwrap { success, message, data }
api.interceptors.response.use(
  (response) => {
    if (response.data && "data" in response.data) {
      response.data = response.data.data;
    }
    return response;
  },
  (error) => {
    const message =
      error.response?.data?.error ??
      error.response?.data?.message ??
      "Something went wrong";

    if (error.response?.status === 401) {
      // Clear auth + redirect to login
      localStorage.removeItem("auth-storage");
      document.cookie = "auth-storage=;path=/;max-age=0";
      window.location.href = "/auth/login";
    }

    return Promise.reject(new Error(message));
  }
);

The services layer is separated from the query layer deliberately. Services contain only Axios calls, no React, no hooks, no side effects. Queries wrap services in useQuery or useMutation. This means when the API shape changes, only the service file needs updating.

Mock → API Switch

During development, the frontend ran entirely on mock data before the backend was ready. A single environment variable, NEXT_PUBLIC_API_READY, controls whether queries hit the real API or serve placeholder data. Flipping it to true activates all live queries at once. No conditional logic scattered across pages.

.env.local
NEXT_PUBLIC_API_BASE_URL=https://your-api-url.com/api
NEXT_PUBLIC_API_READY=true   # false = mock data, true = live API
07

Role-Based Flows

Three distinct user experiences live in the same application. Each has its own layout, navigation, and set of actions.

↳ Farmer Portal

The farmer's world is centered on their listings and incoming orders. The dashboard shows active listings, pending orders, and total earnings at a glance. From the orders page, a farmer can accept or reject an order, accepting locks in the escrow, rejecting triggers a refund. The wallet page shows available balance (released escrow) versus pending balance (held escrow), with a withdraw flow for bank transfer.

↳ Buyer Marketplace

Buyers land on a filterable marketplace, searchable by crop name, farmer name, location, and crop type. Clicking a listing opens a detail page with a quantity stepper and a total price calculator. Hitting "Buy Now" routes to checkout, where the order is placed and payment goes into escrow. The buyer's orders page lets them confirm delivery once they've received their produce, which releases the payment to the farmer.

↳ Admin Dashboard

Admins have a bird's-eye view of the platform, total users, active listings, transaction volume, and pending orders. The users table allows search, role filtering, and suspension. The listings table lets admins approve or suspend any listing. The payments table shows all escrow transactions with the ability to manually release or refund, a safety valve for disputes.

08

Key Decisions

Design Intent

Lovable (Vite SPA) as the starting point

Engineering Solution

Migrated to Next.js App Router, needed nested layouts per role and server-side route protection. Kept the UI, replaced the foundation.

Design Intent

App Router vs Page Router

Engineering Solution

App Router. Three separate role portals with different layouts and auth rules map naturally to route groups. Page Router would have required manual layout wrapping on every page.

Design Intent

React Context for auth (original Lovable code)

Engineering Solution

Replaced with Zustand. Context re-renders the entire tree on every auth state change. Zustand is selective, only components that subscribe to a specific slice re-render.

Design Intent

Inline mock data in page components

Engineering Solution

Moved to query layer with placeholderData. Pages don't know or care whether data is mock or real, the query hook decides based on the API_READY flag.

Design Intent

Tailwind CSS v3

Engineering Solution

Upgraded to Tailwind v4. CSS-first config means design tokens live in globals.css as native CSS variables, not in a JavaScript config file. Better dark mode support and no separate PostCSS setup.

Design Intent

Generic updateStatus() for order actions

Engineering Solution

Split into dedicated accept(), reject(), confirmDelivery(), each mapping to a specific backend endpoint. Clearer intent, easier to debug, matches the API contract.

09

Challenges

Migrating from React (Vite) to Next.js

This was the heaviest part of the project. The original Lovable codebase was a standard Vite + React SPA, React Router for navigation, a single context for auth, everything client-side. Moving that to Next.js App Router meant touching virtually every file: replacing Link and useNavigate from React Router with Next.js equivalents, splitting the single DashboardLayout into role-specific layout.tsx files, converting the auth context to Zustand, and making sure every interactive component had "use client" at the top.

What made it manageable was taking it one layer at a time, types first, then lib and stores, then shared components, then layouts, then pages, and running bun dev after each layer to catch breaks early. Trying to migrate everything at once and then debug would have been a nightmare. The step-by-step approach meant each problem was isolated and fixable on its own.

Tailwind CSS v4 Breaking Styles

We were on Tailwind v3 at the start. When we upgraded to v4 mid-build, a chunk of the styling broke silently,things that looked fine in development started rendering incorrectly in production. The root cause was that v4 dropped the JavaScript config file entirely. Colors, fonts, and animations that were previously defined in tailwind.config.ts had to be moved into the CSS file itself using the new @theme block.

The fix wasn't just a config change,it meant going through the globals CSS file and rewriting how every design token was declared. Custom colors like sidebar-background and status colors like success and warning had to be re-expressed as native CSS variables mapped through @theme. Once that was done, shadcn/ui components picked up the tokens correctly and the visual layer was stable again. The lesson: don't upgrade a styling framework mid-project unless you're ready to rewrite the foundation.

CORS Errors After Connecting the Live API

Once the backend was deployed and I swapped in the real API URL, every request the frontend made was blocked. The browser was rejecting responses because the backend wasn't sending the right headers to allow cross-origin requests,the frontend runs on a different domain than the API, so the browser treats every request as potentially unsafe until the server explicitly says otherwise.

This wasn't a frontend fix,CORS is configured on the server, not the client. I flagged it to the backend engineer with the exact headers needed: Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers. Once those were added to the PHP API's response headers, the requests went through cleanly. It's one of those issues that looks alarming in the browser console but has a straightforward fix once you understand what's actually happening.

10

Key Screens

A visual walkthrough of the core pages across all three user roles.

↳ Landing Page

AgriLink landing page hero sectionAgriLink featured listings sectionAgriLink featured listings sectionAgriLink trust section

↳ Auth, Login & Register

AgriLink login pageAgriLink register page with role selector

↳ Farmer Portal

Farmer dashboard with stats and recent ordersFarmer listings gridFarmer orders page with accept and rejectFarmer wallet with balance and transaction history

↳ Buyer Portal

Buyer marketplace with filtersBuyer orders with confirm delivery

↳ Admin Portal

Admin dashboard overviewAdmin users management tableAdmin users listing tableAdmin payments and escrow management

11

Current State

✓ Shipped

Full frontend migration to Next.js. All three role portals, farmer, buyer, admin, built and functional. Auth with role-based routing. All pages wired to TanStack Query with mock data fallback. Live API connected across all domains. Production build passing.

◷ Active Development

End-to-end flow testing across all user journeys. Notification system integration. Payment gateway (Interswitch) wiring. Performance optimization pass. Accessibility audit. Backend stabilization with the full API surface live.

This project is actively being developed in collaboration with the backend engineer. The architecture is stable and the core flows are functional, what remains is hardening, testing, and wiring the final integrations before the submission deadline.

Stack

Full Tech Stack

Next.js 15TypeScriptTailwind CSS v4shadcn/uiZustandTanStack Query v5AxiosReact Hook FormZodSonnerLucide ReactBunPHP 8 (Backend)MySQL (Backend)
Connect on Linkedin

© 2026 All rights reserved.