DEPT simplifier front-end

Build a React app for your debt simplifier and deploy it globally on Vercel.

Introduction

30 Second Summary

Splitting expenses with friends always starts simple and ends messy. Someone paid for dinner, someone else covered the rental car, and now five people owe each other overlapping amounts that nobody can untangle.

In this project, you will build a React web application that talks to your existing AWS debt-simplification API. You will deploy it to Vercel so anyone with the link can create groups, log expenses, and see the minimum number of payments needed to settle up.

What You'll Build

You will open a browser, create a group of friends, log five messy shared expenses, and watch them collapse into two or three clean settlements on a live public URL you can send to anyone.

By the end of this project, you'll have:

  • A full-stack web app where you create groups, log expenses with a payer and split details, and see exactly who pays whom to settle all debts.
  • A live Vercel deployment on a public URL with automatic CI/CD that redeploys every time you push code to GitHub.
  • Hands-on experience with the full-stack development loop: writing frontend code locally, pushing to Git, and seeing it live within seconds.
  • Secret Mission: Add an animated step-by-step visualization of the debt simplification algorithm so users can watch the math unfold in real time.

Are there any prerequisites?

This project builds on top of your existing Splitwise API deployed on AWS (API Gateway + Lambda + DynamoDB). You need that backend running with CORS configured before you start.

You also need a GitHub account, a Vercel account, and Node.js installed on your machine.

Before We Start

You already have a powerful backend that simplifies group debts. Before writing any code, let's think about what's missing that would let real people actually use it.

Scaffold the React Project with Vite and Tailwind CSS

Your Splitwise backend API can simplify group debts, but right now the only way to use it is by typing curl commands in a terminal. Real users need a visual interface they can open in a browser.

In this step, you'll scaffold a React project using Vite for instant hot-reload development. You'll also wire up Tailwind CSS for styling and create an API service module that connects your frontend to the live AWS backend.

In this step, get ready to:
  • Create a React project with Vite and install Tailwind CSS.
  • Build a centralized API service module that talks to your AWS backend.
  • Set up the app layout shell with a styled header.
Create the project and configure Tailwind

You'll start by scaffolding the project folder, installing dependencies, and configuring Tailwind CSS as your utility-first styling framework.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your React project by running this command:
npm create vite@latest splitwise-frontend -- --template react

What prompts will I see?

Vite will ask two questions after scaffolding the files:

  • "Use rolldown-vite (Experimental)?" - select No.
  • "Install with npm and start now?" - select No. You'll install Tailwind first before running anything.
  • Move into the new project folder and install dependencies by running these commands:
cd splitwise-frontend
npm install
  • Confirm the project was created by running:
ls

You should see files like index.html, package.json, vite.config.js, and a src/ folder in the output.

  • Install Tailwind CSS and the Vite plugin by running this command:
npm install tailwindcss @tailwindcss/vite

Seeing permission errors on Windows?

If you installed Node.js through Chocolatey, npm may not have write access to its cache directory. Reinstall Node.js using the official installer from nodejs.org to fix this.

help me fix npm permission errors on Windows

  • Open your code editor by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the splitwise-frontend folder on your Desktop, then click Open.

Now you'll configure Vite to use the Tailwind plugin. Vite needs to know about Tailwind so it can process utility classes at build time.

  • In the VS Code Explorer sidebar, click on vite.config.js to open it.
  • Replace the entire contents of the file with this code:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
  ],
})

What does this code do?

The plugins array tells Vite which build-time tools to use. The react() plugin enables JSX and React Fast Refresh (hot-reload). The tailwindcss() plugin processes Tailwind utility classes directly in Vite's pipeline.

  • In the Explorer sidebar, expand the src/ folder and click on index.css to open it.
  • Delete everything in the file and replace it with this single line:
@import "tailwindcss";

What does this line do?

In Tailwind CSS v4, this single import replaces the old @tailwind base, @tailwind components, and @tailwind utilities directives. It loads all of Tailwind's styles with zero configuration needed for template paths.

Next, you need to connect your frontend to the live AWS API. Environment variables in Vite must be prefixed with VITE_ to be exposed to client code.

  • In the Explorer sidebar, right-click on the splitwise-frontend root folder area (below all existing files).
  • Click New File and name it .env.
  • Paste this line into the new file:
VITE_API_URL=https://inx49ahcbd.execute-api.ap-south-1.amazonaws.com
  • Save all three files (vite.config.js, src/index.css, .env).
  • Open a terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
  • Start the development server by running:
npm run dev

Open http://localhost:5173 in your browser. You should see the default Vite + React page with the animated logos. This confirms Vite and Tailwind are working together without errors.

Seeing a blank page or build errors?

Check that vite.config.js has both react() and tailwindcss() in the plugins array. Missing a comma between them is a common cause.

Confirm src/index.css contains only @import "tailwindcss"; with no leftover default Vite styles.

help me fix Vite build errors with Tailwind CSS

Build the API service module

Your frontend needs to communicate with six API endpoints on your AWS backend. Instead of scattering fetch calls throughout your components, you'll centralize all API logic in a single service file.

  • In the Explorer sidebar, right-click on the src/ folder.
  • Click New File and name it api.js.
  • Add the first set of API functions by pasting this code:
const API_URL = import.meta.env.VITE_API_URL;

// Creates a new expense group with named members
export async function createGroup(name, members) {
  const response = await fetch(`${API_URL}/groups`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, members }),
  });
  if (!response.ok) throw new Error('Failed to create group');
  return response.json();
}

// Fetches details for an existing group
export async function getGroup(groupId) {
  const response = await fetch(`${API_URL}/groups/${groupId}`);
  if (!response.ok) throw new Error('Failed to fetch group');
  return response.json();
}

// Logs a new expense to a group
export async function addExpense(groupId, expense) {
  const response = await fetch(`${API_URL}/groups/${groupId}/expenses`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(expense),
  });
  if (!response.ok) throw new Error('Failed to add expense');
  return response.json();
}

What does this code do?

The API_URL constant reads your backend URL from the .env file using Vite's import.meta.env syntax. Each function wraps a single API endpoint: createGroup sends a POST to create groups, getGroup fetches group details, and addExpense logs an expense.

If the server responds with an error status, each function throws an Error. This pattern lets your UI components catch failures and show user-friendly messages.

  • Add the remaining read functions below the existing code in src/api.js:
// Fetches all expenses for a group
export async function getExpenses(groupId) {
  const response = await fetch(`${API_URL}/groups/${groupId}/expenses`);
  if (!response.ok) throw new Error('Failed to fetch expenses');
  return response.json();
}

// Calculates simplified settlements for a group
export async function getSettlements(groupId) {
  const response = await fetch(`${API_URL}/groups/${groupId}/settle`);
  if (!response.ok) throw new Error('Failed to calculate settlements');
  return response.json();
}

// Fetches step-by-step settlement algorithm breakdown
export async function getSettlementSteps(groupId) {
  const response = await fetch(`${API_URL}/groups/${groupId}/settle/steps`);
  if (!response.ok) throw new Error('Failed to fetch settlement steps');
  return response.json();
}

What do these functions do?

These three functions handle the read-side of your API. getExpenses fetches the full expense list. getSettlements asks the algorithm to simplify debts. getSettlementSteps returns the step-by-step breakdown of how the algorithm works.

  • Save src/api.js.

Since nothing imports this file yet, you won't see any change in the browser. The next substep puts it to use.

✔️ Awesome, I've got everything!

Great. Confirm you've saved src/api.js and it appears in your Explorer sidebar under the src/ folder.

ⓧ I'd like to double check the full code

Here is the complete src/api.js file. Make sure yours matches exactly:

const API_URL = import.meta.env.VITE_API_URL;

export async function createGroup(name, members) {
  const response = await fetch(`${API_URL}/groups`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, members }),
  });
  if (!response.ok) throw new Error('Failed to create group');
  return response.json();
}

export async function getGroup(groupId) {
  const response = await fetch(`${API_URL}/groups/${groupId}`);
  if (!response.ok) throw new Error('Failed to fetch group');
  return response.json();
}
export async function addExpense(groupId, expense) {
  const response = await fetch(`${API_URL}/groups/${groupId}/expenses`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(expense),
  });
  if (!response.ok) throw new Error('Failed to add expense');
  return response.json();
}

export async function getExpenses(groupId) {
  const response = await fetch(`${API_URL}/groups/${groupId}/expenses`);
  if (!response.ok) throw new Error('Failed to fetch expenses');
  return response.json();
}
export async function getSettlements(groupId) {
  const response = await fetch(`${API_URL}/groups/${groupId}/settle`);
  if (!response.ok) throw new Error('Failed to calculate settlements');
  return response.json();
}

export async function getSettlementSteps(groupId) {
  const response = await fetch(`${API_URL}/groups/${groupId}/settle/steps`);
  if (!response.ok) throw new Error('Failed to fetch settlement steps');
  return response.json();
}
Set up the app shell

The default Vite boilerplate shows a counter demo. You'll replace it with your own layout shell: a header and a main content area that will conditionally show your group components.

  • Before editing App.jsx, create the component folder and placeholder files that App will import. In the Explorer sidebar, right-click on the src/ folder and click New Folder.
  • Name the folder components.
  • Right-click on the new components/ folder and create a new file called GroupForm.jsx.
  • Paste this placeholder code:
// Placeholder - will be built in the next step
function GroupForm({ onGroupCreated }) {
  return <div className="text-gray-400">GroupForm loading...</div>
}

export default GroupForm
  • Create another file in components/ called GroupView.jsx.
  • Paste this placeholder code:
// Placeholder - will be built in the next step
function GroupView({ group, onReset }) {
  return <div className="text-gray-400">GroupView loading...</div>
}

export default GroupView

Why create placeholder files?

The App component you're about to write imports GroupForm and GroupView. If those files don't exist, Vite will throw an import error. These placeholders let the app run cleanly now. You'll replace them with real components in the next step.

  • Save both placeholder files.
  • Now open src/App.jsx in the Explorer sidebar.
  • Delete all the existing content and replace it with this layout shell:
import { useState } from 'react'
import GroupForm from './components/GroupForm'
import GroupView from './components/GroupView'

function App() {
  const [group, setGroup] = useState(null)

  function handleGroupCreated(groupData) {
    setGroup(groupData)
  }

  function handleReset() {
    setGroup(null)
  }

  return (
    <div className="min-h-screen bg-gray-50">
      <header className="bg-indigo-600 text-white py-4 px-6 shadow-md">
        <h1 className="text-2xl font-bold">Splitwise Debt Simplifier</h1>
        <p className="text-indigo-200 text-sm">Minimize transactions, maximize clarity</p>
      </header>
      <main className="max-w-2xl mx-auto py-8 px-4">
        {group ? (
          <GroupView group={group} onReset={handleReset} />
        ) : (
          <GroupForm onGroupCreated={handleGroupCreated} />
        )}
      </main>
    </div>
  )
}

export default App

What does this code do?

The group state variable tracks whether the user has created a group yet. When group is null, the app shows GroupForm. After a group is created, it switches to GroupView.

The handleReset function sets group back to null, returning the user to the creation form. The Tailwind classes style the header with an indigo background and white text.

  • Save src/App.jsx.

Before you check the browser, what do you expect to see? The old Vite counter demo, or something new?

Check your browser at http://localhost:5173. The Vite dev server auto-reloads on save. You should now see an indigo header reading "Splitwise Debt Simplifier" with the subtitle "Minimize transactions, maximize clarity" and the placeholder text below it.

Still seeing the old Vite page or an error?

Make sure you saved src/App.jsx. Unsaved files show a dot next to the filename in VS Code's tab bar.

If you see a module import error mentioning GroupForm or GroupView, confirm both placeholder files exist at src/components/GroupForm.jsx and src/components/GroupView.jsx.

help me fix my Vite app not showing the updated component

Confirm src/main.jsx already imports './index.css'. The Vite template includes this by default, so no changes are needed there.

✔️ Awesome, I've got everything!

Great. Double check that you've saved all files and your browser shows the indigo header.

ⓧ I'd like to double check the full code

Here is the complete src/App.jsx. Scroll up and confirm yours matches:

import { useState } from 'react'
import GroupForm from './components/GroupForm'
import GroupView from './components/GroupView'

function App() {
  const [group, setGroup] = useState(null)

  function handleGroupCreated(groupData) {
    setGroup(groupData)
  }

  function handleReset() {
    setGroup(null)
  }

  return (
    <div className="min-h-screen bg-gray-50">
      <header className="bg-indigo-600 text-white py-4 px-6 shadow-md">
        <h1 className="text-2xl font-bold">Splitwise Debt Simplifier</h1>
        <p className="text-indigo-200 text-sm">Minimize transactions, maximize clarity</p>
      </header>
      <main className="max-w-2xl mx-auto py-8 px-4">
        {group ? (
          <GroupView group={group} onReset={handleReset} />
        ) : (
          <GroupForm onGroupCreated={handleGroupCreated} />
        )}
      </main>
    </div>
  )
}

export default App

And the supporting config files:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [
    react(),
    tailwindcss(),
  ],
})
@import "tailwindcss";
VITE_API_URL=https://inx49ahcbd.execute-api.ap-south-1.amazonaws.com

Your project skeleton is ready. You have a styled header, a centralized API layer, and a dev server with instant hot-reload. Next up, you'll build the group creation form and prove that your frontend can talk to your live AWS API.

Build the Group Creation Flow

Your React app has a layout shell, but right now the browser shows an error because the components it references do not exist yet.

Without a way to create groups, nothing else in the app works. This is the entry point for every user journey and the fastest way to prove your frontend can talk to your live AWS API.

In this step, get ready to:
  • Build a GroupForm component with inputs, validation, and error handling.
  • Build a GroupView component that displays the created group and its members.
  • Verify the frontend successfully calls your live AWS API to create a group.
Create the GroupForm component logic

The GroupForm handles everything about creating a new group. It manages form inputs, validates that at least two members are provided, calls your API, and displays errors if something goes wrong.

  • Create a src/components folder inside your project.
  • Create a new file called GroupForm.jsx inside src/components/ and add the component logic by pasting this code:
import { useState } from 'react'
import { createGroup } from '../api'

function GroupForm({ onGroupCreated }) {
  const [name, setName] = useState('')
  const [membersInput, setMembersInput] = useState('')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)

  async function handleSubmit(e) {
    e.preventDefault()
    setError(null)
    setLoading(true)
    // Parse comma-separated input into array of names
    const members = membersInput.split(',').map(m => m.trim()).filter(Boolean)

    if (members.length < 2) {
      setError('Please enter at least 2 members')
      setLoading(false)
      return
    }

    try {
      const data = await createGroup(name, members)
      onGroupCreated({ ...data, name, members })
    } catch (err) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }

What does this code do?

  • Four pieces of state track the group name, comma-separated members input, loading status, and any error message.
  • handleSubmit prevents the default form refresh, splits the comma string into an array, and validates at least 2 members exist.
  • The try/catch/finally block calls your API, passes the result up to the parent via onGroupCreated, or catches any network error and displays it.
Add the GroupForm JSX
  • Still in GroupForm.jsx, add the form's return statement directly below the handleSubmit function:
  // Form UI with error alert, name input, members input, and submit button
  return (
    <form onSubmit={handleSubmit} className="bg-white rounded-lg shadow p-6 space-y-4">
      <h2 className="text-xl font-semibold text-gray-800">Create a New Group</h2>

      {error && (
        <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded">
          {error}
        </div>
      )}

      <div>
        <label className="block text-sm font-medium text-gray-700 mb-1">Group Name</label>
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="e.g., Trip Friends"
          required
          className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"
        />
      </div>
  • Immediately below the Group Name <div> block, add the members input and submit button to complete the form:
      <div>
        <label className="block text-sm font-medium text-gray-700 mb-1">Members (comma-separated)</label>
        <input
          type="text"
          value={membersInput}
          onChange={(e) => setMembersInput(e.target.value)}
          placeholder="e.g., Alice, Bob, Charlie"
          required
          className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"
        />
      </div>

      <button
        type="submit"
        disabled={loading}
        className="w-full bg-indigo-600 text-white py-2 rounded font-medium hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed"
      >
        {loading ? 'Creating...' : 'Create Group'}
      </button>
    </form>
  )
}

export default GroupForm

What does the JSX do?

  • The {error && ...} block conditionally renders a red alert box when an API call fails or validation triggers.
  • Both inputs are controlled components tied to React state, so the form always reflects what the learner types.
  • The submit button shows "Creating..." and becomes disabled while the API call is in flight, preventing double submissions.
  • Save GroupForm.jsx.
Create the GroupView component

Once a group is created, the app needs to display it. GroupView shows the group name, a unique ID, and each member as a styled badge.

  • Create a new file called GroupView.jsx inside src/components/ and add the component setup:
import { useState } from 'react'
import ExpenseForm from './ExpenseForm'
import ExpenseList from './ExpenseList'
import Settlements from './Settlements'

function GroupView({ group, onReset }) {
  // Track expenses and a refresh key to trigger re-fetches
  const [expenses, setExpenses] = useState([])
  const [refreshKey, setRefreshKey] = useState(0)

  function handleExpenseAdded() {
    setRefreshKey(prev => prev + 1)
  }

  return (
    <div className="space-y-6">
      <div className="bg-white rounded-lg shadow p-6">
        <div className="flex justify-between items-center">
          <div>
            <h2 className="text-xl font-semibold text-gray-800">{group.name}</h2>
            <p className="text-gray-500 text-sm">ID: {group.groupId}</p>
          </div>
          <button
            onClick={onReset}
            className="text-sm text-indigo-600 hover:text-indigo-800 font-medium"
          >
            Start New Group
          </button>
        </div>
  • Below the Start New Group button's closing </div>, add the member badges and child component slots:
        <div className="mt-3 flex flex-wrap gap-2">
          {group.members.map(member => (
            <span key={member} className="bg-indigo-50 text-indigo-700 px-3 py-1 rounded-full text-sm">
              {member}
            </span>
          ))}
        </div>
      </div>

      {/* Child components for expenses and settlements */}
      <ExpenseForm
        groupId={group.groupId}
        members={group.members}
        onExpenseAdded={handleExpenseAdded}
      />

      <ExpenseList groupId={group.groupId} refreshKey={refreshKey} />

      <Settlements groupId={group.groupId} expenseCount={refreshKey} />
    </div>
  )
}

export default GroupView

What does GroupView do?

GroupView is the container for the entire group experience. It displays the group info card at the top, then renders child components for expenses and settlements below it.

The refreshKey state increments each time an expense is added. Child components use this key to know when to re-fetch their data from the API.

Create placeholder components for the next steps

GroupView imports three components that you will build in the next steps. Vite crashes the entire app if an import points to a file that does not exist. You need to create minimal placeholders now.

  • Create src/components/ExpenseForm.jsx with this placeholder:
// Placeholder - built in Step 3
function ExpenseForm() {
  return <div className="text-gray-400 text-sm">Expense form coming soon.</div>
}

export default ExpenseForm
  • Create src/components/ExpenseList.jsx with this placeholder:
// Placeholder - built in Step 3
function ExpenseList() {
  return <div className="text-gray-400 text-sm">Expense list coming soon.</div>
}

export default ExpenseList
  • Create src/components/Settlements.jsx with this placeholder:
// Placeholder - built in Step 4
function Settlements() {
  return <div className="text-gray-400 text-sm">Settlements coming soon.</div>
}

export default Settlements
  • Save all five files in src/components/.
  • Check your browser at http://localhost:5173. You should see a white card with a "Create a New Group" heading, a Group Name input, a Members input, and an indigo Create Group button.

Still seeing an error in the browser?

  • Check that all five files are saved inside src/components/ (not src/ directly). The import paths use ./components/.
  • If you see "Failed to resolve import", one of the placeholder files might be missing. Confirm ExpenseForm.jsx, ExpenseList.jsx, and Settlements.jsx all exist.
  • Try refreshing the page. Vite sometimes needs a manual refresh after previously missing files are created. Help me debug a 'Failed to resolve import' error in my Vite React app.
Test the full group creation flow

Your form is rendering and your API service is ready. Before you run this test, what do you think will happen when you click Create Group? Will the page change, or stay the same?

  • Type Trip Friends in the Group Name field.
  • Type Alice, Bob, Charlie in the Members field.
  • Click Create Group.

The page transitions from the form to the GroupView. You should see "Trip Friends" as the heading, a group ID below it, and three indigo badges showing Alice, Bob, and Charlie.

  • Open your browser's DevTools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
  • Click the Network tab.
  • Click Start New Group to go back to the form, then submit again with any group name and members.
  • In the Network tab, find the groups request. Confirm it shows a POST method, a 200 or 201 status, and the response body contains a groupId.

Form submits but nothing happens?

  • Check the browser console for CORS errors. If you see "Access-Control-Allow-Origin", your API Gateway CORS configuration may need updating. Confirm your API allows requests from http://localhost:5173.
  • Verify that your .env file has the correct VITE_API_URL value. Changes to .env require restarting the dev server with Ctrl+C then npm run dev.
  • If the button stays on "Creating..." forever, the API might be unreachable. Check that the URL in .env does not have a trailing slash. Help me debug my API connection.

✔️ Awesome, I've got everything!

Great. Double check you have saved both GroupForm.jsx and GroupView.jsx in the src/components/ folder.

ⓧ I'd like to double check the full code

Scroll up and confirm each chunk is present in order. Your GroupForm.jsx should have: imports, four state hooks, the handleSubmit function, and the return with a form containing an error display, two input divs, and a submit button.

Your GroupView.jsx should have: imports for ExpenseForm, ExpenseList, and Settlements, two state hooks, handleExpenseAdded, and a return with the group info card, member badges via .map(), and three child component references.

Your frontend can now create groups on your live AWS backend and display the result instantly. Next up, you will build the expense logger so users can record who paid for what.

Build the Expense Logger

Your group creation flow works. Users can create a group and see all their members displayed. But a group without expenses is just a list of names.

In this step, you will build the core data-entry piece where users log who paid for what. Each expense gets split equally across all members and sent to your live AWS API.

In this step, get ready to:
  • Create an ExpenseForm component that submits expenses to your API.
  • Create an ExpenseList component that fetches and displays all expenses.
  • Wire both components into GroupView so adding an expense refreshes the list automatically.
Create the ExpenseForm component

The expense form needs three inputs: what the expense was for, how much it cost, and who paid. On submit, it calculates an equal split across all group members and sends the data to your API.

  • Create a new file src/components/ExpenseForm.jsx in VS Code.
  • Paste the component logic (imports, state, and submit handler) by copying this code:
import { useState } from 'react'
import { addExpense } from '../api'

function ExpenseForm({ groupId, members, onExpenseAdded }) {
  const [description, setDescription] = useState('')
  const [amount, setAmount] = useState('')
  const [paidBy, setPaidBy] = useState(members[0] || '')
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)

  async function handleSubmit(e) {
    e.preventDefault()
    setError(null)
    setLoading(true)
    // Calculate equal split amount for each member
    const numAmount = parseFloat(amount)
    const splitAmount = numAmount / members.length
    const splits = members.map(member => ({
      memberId: member,
      amount: Math.round(splitAmount * 100) / 100,
    }))
    try {
      await addExpense(groupId, { description, amount: numAmount, paidBy, splits })
      setDescription('')
      setAmount('')
      onExpenseAdded()
    } catch (err) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }

What does this code do?

The component tracks five pieces of state: the description, amount, paidBy selection, a loading flag, and any error message.

When the form submits, handleSubmit divides the total amount equally among all members and builds a splits array. After a successful API call, it resets the form fields and calls onExpenseAdded() to tell the parent component to refresh the expense list.

  • Below the closing } of handleSubmit, add the form's JSX template with the input fields:
  // Form UI with description, amount, and paidBy fields
  return (
    <form onSubmit={handleSubmit} className="bg-white rounded-lg shadow p-6 space-y-4">
      <h3 className="text-lg font-semibold text-gray-800">Add Expense</h3>
      {error && (
        <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded text-sm">
          {error}
        </div>
      )}
      <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
        <input
          type="text"
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          placeholder="Description"
          required
          className="border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"
        />
        <input
          type="number"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
          placeholder="Amount"
          min="0.01"
          step="0.01"
          required
          className="border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"
        />
  • Directly below the second <input />, add the paidBy dropdown, the split note, the submit button, and close the component:
        <select
          value={paidBy}
          onChange={(e) => setPaidBy(e.target.value)}
          className="border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500"
        >
          {members.map(member => (
            <option key={member} value={member}>{member}</option>
          ))}
        </select>
      </div>

      <p className="text-xs text-gray-500">Split equally among all {members.length} members</p>

      <button
        type="submit"
        disabled={loading}
        className="bg-green-600 text-white px-4 py-2 rounded font-medium hover:bg-green-700 disabled:opacity-50"
      >
        {loading ? 'Adding...' : 'Add Expense'}
      </button>
    </form>
  )
}

export default ExpenseForm

What does this JSX do?

The form renders a three-column grid with a text input for the description, a number input for the amount, and a <select> dropdown populated with all group members.

The button shows "Adding..." while the request is in flight and disables itself to prevent double-submissions.

  • Save src/components/ExpenseForm.jsx.
  • Switch to your browser at http://localhost:5173. Create a group (you will still see an error for the missing components, which you will fix next).

Seeing a blank page or console errors?

The app will show an import error because ExpenseList.jsx and Settlements.jsx do not exist yet. This is expected. You will create ExpenseList next, and Settlements in the following step.

If you see a different error, check that your file is saved and that the import path ../api matches the location of your src/api.js file.

Still stuck? Help me debug my ExpenseForm import error.

Create the ExpenseList component

The expense list fetches all expenses from your API whenever the group loads or a new expense is added. A refreshKey prop from GroupView increments on each new expense, triggering a re-fetch.

  • Create a new file src/components/ExpenseList.jsx and paste the data-fetching logic:
import { useState, useEffect } from 'react'
import { getExpenses } from '../api'

function ExpenseList({ groupId, refreshKey }) {
  const [expenses, setExpenses] = useState([])
  const [loading, setLoading] = useState(true)

  // Re-fetch expenses whenever groupId or refreshKey changes
  useEffect(() => {
    async function fetchExpenses() {
      setLoading(true)
      try {
        const data = await getExpenses(groupId)
        setExpenses(Array.isArray(data) ? data : data.expenses || [])
      } catch (err) {
        console.error('Failed to fetch expenses:', err)
      } finally {
        setLoading(false)
      }
    }
    fetchExpenses()
  }, [groupId, refreshKey])

  if (loading) return <p className="text-gray-500 text-center">Loading expenses...</p>
  if (expenses.length === 0) return <p className="text-gray-400 text-center text-sm">No expenses yet. Add one above.</p>

What does this code do?

The useEffect hook runs the fetch function every time groupId or refreshKey changes. Since GroupView increments refreshKey after each new expense, the list re-fetches automatically.

The Array.isArray(data) ? data : data.expenses || [] line handles both possible response shapes from the API (a direct array or an object with an expenses property).

  • Below the early return statements, add the JSX that renders each expense as a card:
  // Render expense cards
  return (
    <div className="bg-white rounded-lg shadow p-6">
      <h3 className="text-lg font-semibold text-gray-800 mb-3">Expenses ({expenses.length})</h3>
      <div className="space-y-2">
        {expenses.map((expense, i) => (
          <div key={expense.expenseId || i} className="flex justify-between items-center border-b border-gray-100 pb-2">
            <div>
              <p className="font-medium text-gray-700">{expense.description}</p>
              <p className="text-sm text-gray-500">Paid by {expense.paidBy}</p>
            </div>
            <span className="font-semibold text-gray-800">${expense.amount}</span>
          </div>
        ))}
      </div>
    </div>
  )
}

export default ExpenseList
  • Save src/components/ExpenseList.jsx.
  • Switch to your browser. The page will still show an error because Settlements.jsx is still missing.

To unblock the app, you need a placeholder for the missing Settlements component. You will build the real version in the next step.

  • Create src/components/Settlements.jsx with this placeholder so the app can render:
function Settlements() {
  return null
}

export default Settlements
  • Save src/components/Settlements.jsx.
  • Switch to your browser. The app should now load without errors and show the group creation form.

Still seeing import errors?

Make sure all three component files are in src/components/ and are named exactly ExpenseForm.jsx, ExpenseList.jsx, and Settlements.jsx. File names are case-sensitive.

Need more help? Help me fix my component import errors in Vite.

Test the full expense logging flow

Before you run this test, what do you expect to happen when you create a group and add an expense? Do you think the expense will appear in the list immediately, or will you need to refresh the page?

  • In your browser, create a new group with the name Trip Friends and members Alice, Bob, Charlie.
  • In the Add Expense form, enter Dinner for the description, 90 for the amount, and select Alice as the payer.
  • Click Add Expense.
  • Add two more expenses with different payers (e.g., Taxi for $30 paid by Bob, Snacks for $15 paid by Charlie).

Each expense appears in the Expenses list immediately after you click the button. No page refresh needed. The list shows the count updating (e.g., "Expenses (3)") along with each description, payer, and amount.

  • Open DevTools with Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows) and click the Network tab.
  • Add another expense and confirm you see a successful POST request to your API Gateway URL followed by a GET that fetches the updated list.

Expenses not appearing after you click Add Expense?

Check the Network tab for a failed POST request. A CORS error means your API Gateway CORS configuration may not include http://localhost:5173 as an allowed origin. A 400 error means the request body does not match what your Lambda function expects.

If the POST succeeds but the list stays empty, check the GET response. The ExpenseList component handles both array and object responses, but if your API returns a different shape you may need to adjust the parsing line.

Still stuck? Help me debug why my expenses aren't showing up in the list.

✔️ Awesome, I've got everything!

Confirm you have saved both ExpenseForm.jsx and ExpenseList.jsx. Your expense logger is complete.

ⓧ I'd like to double check the full code

Scroll up and confirm each section is present in your files, in order:

  • ExpenseForm.jsx: Imports and state declarations, then the handleSubmit function with the splits calculation, then the form JSX with the three-column input grid, the split note, and the submit button.
  • ExpenseList.jsx: Imports and state, then the useEffect that calls getExpenses, the two early-return loading/empty states, and then the return JSX mapping expenses to cards.

Your expense logger is live. Users can now log who paid for what, and the list updates instantly. Next up, you will build the Settlements view where the real magic happens: turning those messy expenses into the minimum number of payments needed to settle all debts.

Display the Simplified Settlements

Your expense logging flow works perfectly. Users can create groups, add expenses, and see them listed in real time.

Now comes the entire point of the application: showing users how their messy overlapping expenses collapse into the minimum number of clean transfers. This is the payoff moment where 5+ expenses across multiple payers get reduced to 2-3 simple settlements.

In this step, get ready to:
  • Build the Settlements component with a Settle Up button that calls your AWS API.
  • Display simplified transaction cards showing who pays whom and how much.
  • Add a visual summary highlighting the transaction reduction.
Create the Settlements component with a Settle Up button

The Settlements component needs to call your getSettlements API function when the user clicks a button, then store the result in state for display.

  • Create a new file src/components/Settlements.jsx and add the following code:
import { useState } from 'react'
import { getSettlements } from '../api'

function Settlements({ groupId, expenseCount }) {
  const [settlements, setSettlements] = useState(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState(null)
  async function handleSettle() {
    setError(null)
    setLoading(true)
    try {
      const data = await getSettlements(groupId)
      setSettlements(data)
    } catch (err) {
      setError(err.message)
    } finally {
      setLoading(false)
    }
  }
  return (
    <div className="bg-white rounded-lg shadow p-6">
      <div className="flex justify-between items-center mb-4">
        <h3 className="text-lg font-semibold text-gray-800">Settlements</h3>
        <button onClick={handleSettle} disabled={loading} className="bg-indigo-600 text-white px-4 py-2 rounded font-medium hover:bg-indigo-700 disabled:opacity-50">{loading ? 'Calculating...' : 'Settle Up'}</button>
      </div>
    </div>
  )
}

export default Settlements

What does this code do?

  • settlements state starts as null and stores the API response after the user clicks the button.
  • handleSettle calls your AWS Lambda backend via getSettlements(groupId) and handles loading and error states around the request.
  • The button shows "Calculating..." while the request is in flight and disables itself to prevent duplicate clicks.
  • Save Settlements.jsx.
  • Switch back to your browser at http://localhost:5173. Create a group and add a couple of expenses. You should see a new "Settlements" section at the bottom with a Settle Up button.

Don't see the Settlements section?

  • Check that the filename is exactly Settlements.jsx (capital S) inside src/components/. The import in GroupView.jsx expects this exact path.
  • If you see a blank page or errors in the console, check that the export default Settlements line is present at the bottom of the file.

Why isn't my Settlements component rendering?

Add the settlement results display

The button calls the API, but nothing displays yet. You need to render the settlement transactions as cards and show a green summary banner highlighting how many transactions were reduced.

  • In Settlements.jsx, find the closing </div> of the mb-4 flex container (the one wrapping the heading and button). Add the following code below it, but still inside the outer <div>:
      {error && (
        <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded text-sm">
          {error}
        </div>
      )}

      {settlements && (
        <div className="space-y-3">
          {settlements.transactions && settlements.transactions.length > 0 ? (
            <>
              <div className="bg-green-50 border border-green-200 rounded px-4 py-2 text-sm text-green-800">
                Simplified to {settlements.transactions.length} transaction{settlements.transactions.length !== 1 ? 's' : ''}
              </div>
              {settlements.transactions.map((t, i) => (
                <div key={i} className="flex justify-between items-center bg-gray-50 rounded px-4 py-3">
                  <span className="text-gray-700">
                    <span className="font-semibold">{t.from}</span> pays <span className="font-semibold">{t.to}</span>
                  </span>
                  <span className="font-bold text-indigo-600">${t.amount.toFixed(2)}</span>
                </div>
              ))}
            </>
          ) : (
            <p className="text-gray-500 text-sm">All settled! No transactions needed.</p>
          )}
        </div>
      )}

What does this code do?

  • The error block shows a red alert if the API call fails.
  • The green banner (bg-green-50) tells the user how many transactions the algorithm simplified their expenses into.
  • Each settlement card displays "X pays Y" with the exact dollar amount, styled distinctly from expense cards to make the simplification visually obvious.
  • If everyone is already settled (no transactions needed), a friendly message appears instead.
  • Save Settlements.jsx.
  • In your browser, create a group with 3 members (e.g. Alice, Bob, Charlie) and add 3-4 expenses with different payers.
  • Click Settle Up. You should see a green banner showing the simplified count (e.g. "Simplified to 2 transactions") and cards below it stating who pays whom.

Seeing an error after clicking Settle Up?

  • Open the browser DevTools Network tab and check the response from the /settle endpoint. If you see a CORS error, confirm your API Gateway has CORS configured for your localhost origin.
  • If you see "Failed to calculate settlements", make sure you have at least one expense added before clicking Settle Up.

Help me debug why my Settle Up button returns an error.

Test the full end-to-end flow

Your GroupView component already has a Start New Group button in the header that calls onReset, clearing the app state back to the group creation form. This lets users try different expense scenarios without refreshing the page.

Before you run this final test, what do you think will happen if you enter 5 expenses across 3 payers and click Settle Up? How many settlements do you expect?

  • In your browser, click Start New Group to reset the app.
  • Create a new group with 3-4 members.
  • Add 4-5 expenses with different payers and varying amounts.
  • Click Settle Up and observe the results.

The settlements section should show fewer transactions than the number of expenses you entered. Each settlement clearly states who pays whom and the exact dollar amount. The numbers balance out correctly across all members.

Why fewer settlements than expenses?

Your backend algorithm calculates net balances first (who is overall owed money, who overall owes), then matches debtors to creditors with the minimum number of transfers. Five messy expenses between four people might collapse into just 2 clean payments.

✔️ Awesome, I've got everything!

Great. Double check you have saved Settlements.jsx and that the full flow works: create group, add expenses, settle up, and start a new group.

ⓧ I'd like to double check the full code

Your Settlements.jsx should contain both chunks you added above, in order:

  • Chunk 1: The imports, state variables, handleSettle function, and the outer container with the heading and Settle Up button.
  • Chunk 2: The error display, the green summary banner, and the settlement transaction cards (all inside the outer container <div>, below the button section).

Scroll up to confirm each chunk is present in the correct order. The component should export as export default Settlements at the bottom.

Your app now delivers the full Splitwise experience from start to finish. Users create a group, log their messy expenses, and watch the algorithm collapse them into the minimum number of clean transfers. Next up, you will deploy this to a live public URL so anyone can use it.

Deploy to Vercel via GitHub

Your React app works perfectly on your machine, but nobody else can use it yet. Friends who want to split expenses would need to clone your code, install dependencies, and run a local dev server.

Deploying to Vercel gives you a live public URL with automatic HTTPS and a global CDN. Every time you push code to GitHub, Vercel automatically redeploys your app within seconds.

In this step, get ready to:
  • Push your project to a GitHub repository.
  • Deploy the app to Vercel with environment variables configured.
  • Verify the full flow works on a public URL with automatic CI/CD.
Initialize Git and push to GitHub

Before you can deploy, your code needs to live in a Git repository on GitHub. Vercel pulls your source code from GitHub and builds it automatically.

  • Stop the dev server by pressing Ctrl+C in your terminal.
  • Initialize a Git repository by running this command:
git init

You should see a message confirming the repository was initialized.

  • Create a .gitignore file to exclude files that should not be committed. Create it by running:
echo "node_modules
.env" > .gitignore
  • Stage all files and create your first commit by running these commands:
git add .
git commit -m "Initial commit"

You should see a summary listing all the files committed.

Seeing a Git identity error?

If Git asks you to set your name and email, run these two commands first (replacing the placeholder values with your own), then retry the commit:

git config --global user.email "your-email@example.com"
git config --global user.name "Your Name"

help me fix this Git identity error when committing

  • Open your browser and navigate to https://github.com/new to create a new repository.
  • Enter splitwise-frontend as the repository name.
  • Leave the repository set to Public.
  • Do NOT check any of the initialization options (no README, no .gitignore, no license).
  • Click Create repository.

GitHub shows you a page with setup commands. You need the HTTPS URL displayed at the top of the page.

  • Connect your local repo to GitHub and push your code by running these commands:
git remote add origin [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/splitwise-frontend.git"]]
git branch -M main
git push -u origin main

You should see output confirming your branch was pushed to the remote.

  • Refresh your GitHub repository page in the browser.

You should see all your project files listed on GitHub, including src/, vite.config.js, and package.json.

Push failed or authentication error?

GitHub requires authentication for pushing. If you see a permission error, you may need to set up a personal access token or SSH key in your GitHub account settings.

help me authenticate with GitHub so I can push my code

Add the Vercel SPA configuration

Single-page apps like React handle routing in the browser, not on the server. Without a rewrite rule, refreshing on any page other than the root would return a 404.

  • Create a vercel.json file in the root of your splitwise-frontend folder with the SPA rewrite rule. Paste this content:
{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}

What does this rewrite do?

The rewrite rule tells Vercel to serve index.html for every URL path. This lets React Router (or any client-side routing) handle navigation without the server returning 404 errors on page refreshes.

  • Save vercel.json.
  • Commit and push this file to GitHub by running:
git add vercel.json
git commit -m "Add Vercel SPA rewrite config"
git push

You should see the push succeed. Your GitHub repo now has the vercel.json file.

✔️ Awesome, I've got everything!

Great. Confirm you have saved vercel.json and pushed it to GitHub.

ⓧ I'd like to double check the full code

Here is the complete vercel.json file:

{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
Deploy on Vercel and test the live app

Now that your code is on GitHub, Vercel can pull it, build it, and serve it on a global CDN in one click.

  • Open your browser and navigate to https://vercel.com/new.
  • If prompted, connect your GitHub account to Vercel.
  • Find and select your splitwise-frontend repository from the list.
  • Click Import.

Vercel auto-detects that your project uses Vite and pre-fills the build settings.

  • Before deploying, expand the Environment Variables section.
  • In the Key field, enter VITE_API_URL.
  • In the Value field, enter https://inx49ahcbd.execute-api.ap-south-1.amazonaws.com.
  • Click Add to save the environment variable.
  • Click Deploy.

Why set the environment variable here?

Your .env file is in .gitignore and never gets pushed to GitHub. Vercel needs its own copy of VITE_API_URL so that the build process can inject it into your frontend code at deploy time.

Vercel shows a build log. After about a minute, you should see a "Congratulations" screen with a preview of your deployed site and a .vercel.app URL.

  • Click on the deployed URL (or copy it from the dashboard) to open your live app.

Before you test, what do you expect to happen when you create a group on this live URL? Will it hit your real AWS backend or fail?

  • Test the full flow: create a group with a name and members, add a few expenses, then click Settle Up.

Everything works exactly as it did locally. Your live Vercel app calls your AWS API Gateway, which triggers Lambda, which reads and writes to DynamoDB.

Seeing API errors on the live site?

If you see fetch errors in the browser console, check these common causes:

  • Confirm you added VITE_API_URL in Vercel's environment variables with no trailing slash.
  • Verify your AWS API has CORS configured to allow requests from your Vercel domain.
  • Try redeploying from the Vercel dashboard after fixing the environment variable.

help me debug API errors on my deployed Vercel app

Now confirm that CI/CD is working by pushing a small change.

  • In VS Code, open src/App.jsx and change the subtitle text from Minimize transactions, maximize clarity to something else (like Split expenses with friends).
  • Save the file.
  • Commit and push the change by running:
git add .
git commit -m "Update subtitle"
git push
  • Open your Vercel dashboard and watch the Deployments section.

Within seconds, Vercel detects the push and starts a new deployment. Once it finishes, refresh your live URL and you should see the updated subtitle text.

What just happened?

This is CI/CD in action. Every push to your main branch on GitHub triggers Vercel to pull the latest code, rebuild it, and deploy the new version globally. No manual deploys needed.

Secret mission

Animated Settlement Visualization

Your app shows the final simplified settlements, but the algorithm's journey from messy expenses to clean transfers is invisible. Build an animated step-by-step visualization that reveals how the debt simplification algorithm works, complete with playback controls and color-coded transactions.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs on the free Vercel Hobby plan with no ongoing costs, so there is no financial pressure to clean up immediately.

Resources you used:

  • Local splitwise-frontend project folder on your Desktop.
  • GitHub repository containing your frontend code.
  • Vercel project and deployment (Hobby plan, free).

Keep everything running

No action needed. Choose this if you are still actively building or want to keep your live Vercel URL accessible.

  • Your Vercel deployment stays live at your .vercel.app URL indefinitely on the free Hobby plan.
  • Your GitHub repository remains available for future changes and automatic redeployments.
  • Your local project folder stays on your Desktop for continued development.

Pause - I'll come back to this later

Stop running processes to free up local memory. Your Vercel deployment and GitHub repo stay intact for when you return.

  • If your dev server is still running in the terminal, press Ctrl+C to stop it.
  • Close VS Code to free memory.
  • Your Vercel deployment continues to serve your app at no cost. No action needed there.

Delete - I don't want to use this again

Remove all project resources completely. This cannot be undone.

  • Delete the local project folder. On macOS/Linux, run the command below. On Windows, run rmdir /s /q %USERPROFILE%\Desktop\splitwise-frontend in Command Prompt.
rm -rf ~/Desktop/splitwise-frontend
  • Delete the Vercel project. Go to your project in the Vercel dashboard, click Settings, scroll to the bottom of the General page, and click Delete. Type the project name to confirm.
  • Delete the GitHub repository. Go to your repository on GitHub, click Settings, scroll down to the Danger Zone section, and click Delete this repository. Type the full repository name to confirm.

That's a Wrap!

That's a Wrap!

Nice work! You just built and shipped a full-stack web application from scratch. Your Splitwise Debt Simplifier now lives on a public URL that anyone can visit to create expense groups, log shared costs, and see debts simplified in real time.

You've learned how to:

  • Build a complete React frontend with Vite and Tailwind CSS v4, wiring up components that manage state, handle user input, and display dynamic data.
  • Connect a frontend to a live AWS serverless API (Lambda + API Gateway + DynamoDB) using a centralized service module with async fetch calls.
  • Deploy a single-page application globally on Vercel with automatic CI/CD that redeploys every time you push to GitHub.
  • Secret Mission: Built an animated step-by-step visualization of the debt simplification algorithm with playback controls and color-coded amounts.

Ready to quiz yourself?