DEV Community

Azhar Alvi
Azhar Alvi

Posted on

PHASE 4 — The Face

Scaffold a React app, log in through a real form, and do full CRUD without ever opening /docs again — then close the three gaps nobody builds: loading, empty, and error.

So by the end of Phase 3 my app had a memory [a database], a mouth [HTTP endpoints], and a lock on the door [auth]. It knew who you were and refused to show you anyone else's expenses. Genuinely satisfying. There was just one small, embarrassing detail: the only human who could actually use it was me, poking at /docs. My "app" was a Swagger page and a lot of faith. No screen, no login box, nothing a normal person could look at. It had everything except a face.

Phase 4 gives it one.

A confession before we start: I changed the plan

If you read the end of my Phase 3 write-up, I promised Phase 4 would be filtering, sorting, and pagination — making the backend list usable at scale. I changed my mind. I went with frontend.

My reasoning, honestly: I've been living in Python for four phases and I've never touched React. Building the UI now forces me to re-meet my own auth, tokens, CORS, and endpoints from the client's side of the wire — which is the best way to find out whether I actually understood them. The known cost, which I'm accepting on purpose: GET /expenses is still unbounded, so when pagination lands later I'll have to come back and revise this list UI. Flagged, accepted, moving on.

Let's call it PHASE 4 — The Face:

  • Scaffold a real React app with Vite
  • Write my first component and actually understand JSX
  • Build a login form that holds its own text [controlled inputs + useState]
  • Call the backend from the browser — and get mugged by CORS
  • Store the token, and reckon with where to store it
  • Fetch and render the expense list [useEffect]
  • Close the three gaps: loading, empty, error
  • Wire up add / delete / edit — full CRUD from the UI
  • Fix the bug that made login "do nothing until I refreshed"

First, the new world [everything here is different from the backend]

The backend runs on Python; you install libraries with pip and freeze them into requirements.txt. The frontend has an exact parallel universe, and learning the mapping made the whole thing click:

  • Node.js is the JavaScript runtime — the "Python interpreter" of the frontend. [I'm on v22.]
  • npm is Node's package manager — this is pip. [I'm on v11.]
  • package.json is requirements.txt's counterpart — except it updates itself automatically when you install. No manual freeze step over here. One less habit to remember on this side of the fence.
  • node_modules/ is venv/ — enormous, machine-specific, and it must never touch git.
  • Vite [French for "quick," said veet] is the tool that scaffolds the app and runs the hot-reloading dev server. It's the frontend's uvicorn --reload.

Two forks in the road, two flagged shortcuts:

  • Vite offered me TypeScript or plain JavaScript. I took plain JS. TypeScript is fantastic and production-common, but it's a second new language stacked on top of React, and one new language at a time is plenty. [TypeScript = "know this exists," not now.]
  • It also asked which linter — Oxlint or ESLint. [A linter is a spell-checker for code: it reads your code without running it and flags likely bugs and sloppy style.] I picked ESLint because it's the industry default — when I hit an error and google it, ESLint results are everywhere. Oxlint is newer and faster but less documented. [Oxlint = know this exists.]
npm create vite@latest frontend -- --template react
cd frontend
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

That last command lit up a starter page at http://localhost:5173 with a clicky "count is 0" button — which, I later learned, is useState doing its thing. My first commit of the phase was that untouched scaffold, on its own: a clean "React works" checkpoint I could always fall back to before I started breaking things.

Habit that carried over: before committing, I checked git wasn't about to swallow node_modules. Turns out Vite drops its own .gitignore inside frontend/ — and yes, git supports multiple .gitignore files, one per folder. I didn't take that on faith [never, with git]; I ran git check-ignore frontend/node_modules and it echoed the path back, which is git saying "yep, ignoring that." Verify, don't assume.

Step 1: My first component [JSX has three rules that bite]

A React component is deceptively simple: it's just a JavaScript function, with a Capitalized name, that returns markup. That markup is JSX — it looks like HTML but it's actually JavaScript wearing a costume. I overwrote Vite's demo App.jsx with my own [safe: it's boilerplate, and I'd just committed, so git checkout would bring it back instantly]:

function App() {
  return (
    <div>
      <h1>Smart Expense Manager</h1>
      <p>Frontend is alive.</p>
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Three JSX rules that trip everyone, now filed away:

  • One single root element. You can't return two side-by-side tags; wrap them in a parent. [React will yell at you.]
  • className, not class — because class is a reserved word in JavaScript.
  • { } to drop a JS value into markup{2 + 2} renders 4.

export default App is what lets main.jsx do import App from './App.jsx' — the JS version of exposing a name so another module can import it. I saved the file, and the browser instantly swapped to my text without a refresh. That's hot reload, and it's addictive.

Step 2: A login form that holds its own text [useState]

My instinct was to store what the user types in a normal variable — let email = ''. Wrong, and here's the precise why: React only updates the screen when it re-renders, and a plain variable changing doesn't trigger one. The box would go stale.

The fix is useState, a hook — a special function that lets a component remember a value across re-renders and re-render when that value changes.

import { useState } from 'react'

function App() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  function handleSubmit(event) {
    event.preventDefault()
    console.log('Logging in with:', email, password)
  }

  return (
    <div>
      <h1>Smart Expense Manager</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          placeholder="Email"
          value={email}
          onChange={(event) => setEmail(event.target.value)}
        />
        <input
          type="password"
          placeholder="Password"
          value={password}
          onChange={(event) => setPassword(event.target.value)}
        />
        <button type="submit">Log in</button>
      </form>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

New ideas, in plain language:

  • const [email, setEmail] = useState('') returns a pair — the current value and a setter. That [...] unpacking is array destructuring. The iron rule: never reassign email directly — always call setEmail(...). The setter is what tells React "this changed, re-render."
  • Controlled input — the two-way loop that is the React form pattern: value={email} makes the box always display state, onChange pushes every keystroke back into state. State is the single source of truth; the input just mirrors it.
  • import { useState } with braces is a named import; import App without braces is a default import. The braces are the tell.

And the one that will get me someday:

Gotcha #1 — event.preventDefault() is not optional. By default, submitting an HTML form makes the browser reload the entire page [a genuine 1990s behavior]. In a React app that wipes all your state and blanks the screen. preventDefault() cancels it so I stay in control. I know for a fact I'll forget this line one day and watch my page flash — but now I'll know exactly why.

I opened DevTools, typed in both boxes [text appeared as I typed — the loop works], clicked Log in, and watched Logging in with: ... print to the console with no page reload. First real interaction, done.

Step 3: Call the backend — and get mugged by CORS

Now the browser had to actually talk to FastAPI. Two new concepts collided here.

First, async / await and fetch. Network calls take time, and JavaScript can't freeze the browser waiting. So fetch [the browser's built-in HTTP client — the JS version of Python's requests] returns a Promise, an "I'll have your answer later" placeholder. await pauses this function until it resolves, and any function using await must be marked async [same word as FastAPI's async def].

Second — and this is the single biggest gotcha of the whole phase:

Gotcha #2 — the login endpoint is the odd one out: it wants FORM data, not JSON. My /auth/login uses OAuth2PasswordRequestForm, which reads application/x-www-form-urlencoded, not JSON. Send it JSON and you get a baffling 422. So I built the body with URLSearchParams and set the matching Content-Type. Bank this: the OAuth2 login endpoint is form-encoded; every other endpoint in my API speaks JSON. Mixing these two up is the classic source of 422s.

async function handleSubmit(event) {
  event.preventDefault()
  const formBody = new URLSearchParams()
  formBody.append('username', email)   // OAuth2 calls the field "username" — I put my email there
  formBody.append('password', password)

  const response = await fetch('http://localhost:8000/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: formBody,
  })
  const data = await response.json()
  console.log('Status:', response.status, 'Body:', data)
}
Enter fullscreen mode Exit fullscreen mode

My mentor predicted this would fail, and it did — with a big red wall:

Gotcha #3 — CORS, the thing my roadmap warned trips up everyone. Access to fetch at 'http://localhost:8000/auth/login' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header... My frontend runs on port 5173, my backend on 8000 — different port means the browser considers them different origins, and it refused to let my code read the response.

CORS [Cross-Origin Resource Sharing] is a browser security feature, not a bug in my code. The scenario it prevents: you're logged into your bank in one tab, you visit a sketchy site in another, and that site's JavaScript quietly fires requests at your bank using your session. To stop that, the browser won't let JS on origin A read a response from origin B unless server B explicitly says it allows A. Server B says so with an Access-Control-Allow-Origin header — which my FastAPI wasn't sending. Getting the error actually meant my request was leaving the browser correctly; the backend just hadn't granted permission yet.

The fix lives on the backend, via FastAPI's CORSMiddleware [a middleware is code that wraps every request/response — perfect for stamping a header onto everything]. Bonus: it ships inside FastAPI, so no install, no requirements.txt change this time.

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],  # the whitelist — explicit, never "*"
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
Enter fullscreen mode Exit fullscreen mode

Two decisions worth stating:

  • Never allow_origins=["*"]. It defeats the point, and the browser flat-out forbids "*" together with allow_credentials=True. Always list explicit origins. [Flagged shortcut: I hardcoded the dev URL. Production reads allowed origins from an env var and lists the real Vercel domain — that's my Phase 9 deploy task.]
  • allow_headers=["*"] matters because I'm about to start sending an Authorization header on every protected request.

uvicorn hot-reloaded, I clicked Log in again, and the CORS error was gone — replaced by a clean 401, because my dev database was freshly regenerated and had zero users. A 401 round-trip was a success: the browser-to-backend pipe now worked end to end. I registered a test user through /docs, typed the same credentials into my form, and got 200 with a glorious { access_token: "eyJ...", token_type: "bearer" }. My React app had fetched its own JWT.

Step 4: Store the token [the one real security decision this phase]

The token vanished the instant handleSubmit finished. I needed to keep it — every future request has to attach it, and a refresh shouldn't log me out. Enter localStorage: a tiny per-origin key-value store built into the browser, persists across reloads, dead-simple API [setItem, getItem, removeItem], values are always strings.

if (response.ok) {
  const data = await response.json()
  localStorage.setItem('token', data.access_token)
}
Enter fullscreen mode Exit fullscreen mode

[response.ok is a boolean that's true for any 2xx — the clean way to ask "did this succeed?" instead of eyeballing status numbers.]

Here's where I put the security hat back on, because it genuinely matters:

localStorage is readable by any JavaScript running on my page. So if an attacker ever injects a script into my app — that's an XSS [cross-site scripting] attack — they can read the token straight out of storage and impersonate the user. That's the real risk, stated plainly.

  • What I did [flagged learning shortcut]: store the JWT in localStorage. Simple, easy to inspect while learning, sidesteps cookie/CORS-credential complexity.
  • What production does: store it in an httpOnly, Secure, SameSite cookie. httpOnly means JavaScript literally cannot read it, so even a successful XSS can't steal it, and the browser attaches it automatically. [Know this exists. Not building it now.]

React's own defense is worth knowing too: it auto-escapes any text you render, which neutralizes most XSS by default. The danger zone is a prop literally named dangerouslySetInnerHTML — the scary name is the warning. Don't use it and you dodge the common trap.

I logged in, opened DevTools → Application → Local Storage, saw my token key sitting there, refreshed the page, and it survived. That persistence is the whole point.

Step 5: The expense list [useEffect, and the infinite-loop trap]

Reading data needs useEffect, and I'm glad I learned the trap before hitting it. A component function re-runs on every render. So you can't just drop a fetch in the component body — it'd fire on every render, hammer the API, and [if it sets state] spin into an infinite re-render loop.

useEffect lets you say "run this as a side effect, but only at specific times." The dependency array [second argument] controls when:

  • [] empty → run once, right after the component first mounts. Exactly what "fetch when the page loads" wants.
  • [x] → also re-run when x changes.
  • omitted entirely → run after every render. The infinite-loop trap. Don't.

One quirk: the effect function itself can't be async, so the pattern is to define an async function inside it and call it immediately.

import { useState, useEffect } from 'react'

function ExpenseList() {
  const [expenses, setExpenses] = useState([])

  useEffect(() => {
    async function fetchExpenses() {
      const token = localStorage.getItem('token')
      const response = await fetch('http://localhost:8000/expenses', {
        headers: { Authorization: `Bearer ${token}` },
      })
      const data = await response.json()
      setExpenses(data)
    }
    fetchExpenses()
  }, [])
  // ...
}
Enter fullscreen mode Exit fullscreen mode

That `Bearer ${token}` is a template literal — backticks let you embed a variable with ${...}. It produces the exact string my /docs "Authorize" button sends, which is how the app proves who it is on every protected request. The fetch came back with a clean empty array [] — correct, since my test user had no expenses yet. Authentication working from the client side, first time.

Step 6: The three gaps nobody builds [loading, empty, error]

This is the heart of the phase. And it starts with a gotcha that burns everyone:

Gotcha #4 — fetch does NOT throw on a 404 or 500. It only rejects when the network itself fails [server down, CORS block]. A 401 or 500 is, to fetch, a perfectly "successful" round-trip that happens to carry a bad status. So you have to check response.ok yourself and deliberately throw on a bad status. Coming from Python's requests, this genuinely surprised me.

I handled both failure kinds with try / catch / finally [same shape as Python]. finally runs no matter what — the perfect home for setLoading(false), so a loading spinner can never get stuck on screen. And I rendered the states with early returns in strict priority order: loading → error → empty → list.

const [expenses, setExpenses] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)

useEffect(() => {
  async function fetchExpenses() {
    const token = localStorage.getItem('token')
    try {
      const response = await fetch('http://localhost:8000/expenses', {
        headers: { Authorization: `Bearer ${token}` },
      })
      if (!response.ok) {
        throw new Error(`Request failed: ${response.status}`)
      }
      const data = await response.json()
      setExpenses(data)
    } catch (err) {
      setError('Could not load expenses. Please try again.')
      console.error(err)
    } finally {
      setLoading(false)
    }
  }
  fetchExpenses()
}, [])
Enter fullscreen mode Exit fullscreen mode

The part tutorials skip — I tested the error gap on purpose. I hit Ctrl+C on the backend, refreshed the browser, and instead of a blank frozen page I got my red "Could not load expenses. Please try again." That's the adversarial habit surviving even without the GAN framing: don't just build the happy path, actively try to break it.

Then rendering the list with .map() — the array method that turns each expense object into an <li>. Every mapped element needs a key — a stable unique id so React can track rows across re-renders. Use the database id, never the array index [index breaks the moment the list reorders or an item is removed].

While wiring this, a small honest scope decision surfaced: my mentor's example row showed a category field. My Expense model doesn't have one. Adding it isn't a display tweak — it's a new column, a migration, and schema changes. That's literally what my roadmap's Phase 7 [AI categorization] is for. So I dropped it rather than scope-creep a clean foundation phase. [category → deferred to Phase 7.] My real ExpenseRead returns id, amount, description, spent_on, and timestamps — and a nice detail: because amount is a Decimal, Pydantic serializes it to a string on purpose, to protect money from float rounding.

Step 7: Full CRUD from the UI [add, delete, edit]

Add [POST]. Same JSON pattern I'll use everywhere except login: Content-Type: application/json and body: JSON.stringify({...}). On success, I clear the form and re-fetch the list so the new row appears — letting the server stay the single source of truth. To make that re-fetch reusable, I pulled fetchExpenses out of useEffect into the component body, so both the initial load and every post-write refresh could call it.

async function handleAdd(event) {
  event.preventDefault()
  const token = localStorage.getItem('token')
  const response = await fetch('http://localhost:8000/expenses', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({ description, amount, spent_on: spentOn }),
  })
  if (response.ok) {
    setDescription(''); setAmount(''); setSpentOn('')
    fetchExpenses()
  }
}
Enter fullscreen mode Exit fullscreen mode

[A lovely bit of luck: an <input type="date"> gives a date-picker whose value is already a "YYYY-MM-DD" string — exactly what my Python date field wants, zero conversion.]

Adding the always-visible form forced a rendering upgrade. Early returns worked when the whole component was one state, but the form has to show even in the empty state. So I switched to inline conditional rendering with &&:

{loading && <p>Loading expenses…</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
{!loading && !error && expenses.length === 0 && <p>No expenses yet.</p>}
{!loading && !error && expenses.length > 0 && ( /* the list */ )}
Enter fullscreen mode Exit fullscreen mode

The lesson worth banking: early-return when the entire component is one state; inline && when only part of the UI changes.

Gotcha #5 — one missing ! showed two contradictory states at once. For a while my app displayed "Could not load expenses" and "No expenses yet" simultaneously — which is impossible; those are supposed to be mutually exclusive. The cause was hilariously small: my empty-state guard was missing the !error check. One character [!] fixed it. A whole render bug lived in a single missing symbol.

Delete [DELETE]. Two things I hadn't met:

Gotcha #6 — a 204 has no body, so don't parse one. My DELETE returns 204 No Content — by definition, empty. Calling response.json() on it would throw. Just check response.ok and move on.

Gotcha #7 — passing an argument to a handler needs an arrow wrapper. To delete a specific row I wrote onClick={() => handleDelete(expense.id)}. If I'd written onClick={handleDelete(expense.id)} [no arrow], React would call it immediately during render — deleting everything the instant the list appeared. The arrow hands React a function to call later, on click. Rule of thumb I locked in: no args → pass the reference [onClick={handleLogout}]; need args → wrap in an arrow.

Edit [PATCH]. The biggest UI concept: per-row edit mode. I track which row is editing with a single editingId [null = nobody], and render with a ternary — "if this row is being edited, show inputs; otherwise show text + buttons." Each branch has to return one element, so I wrapped the siblings in a Fragment [<>...</>] — an invisible grouping tag that adds no extra <div> to the page.

PATCH is a partial update: my backend's ExpenseUpdate has all-optional fields, and it saves with payload.model_dump(exclude_unset=True)setattrdb.commit()db.refresh(). On success I close edit mode and re-fetch, same source-of-truth habit.

With edit working, I'd hit the phase's finish line: log in through the UI and do full create/read/update/delete without ever opening /docs.

Step 8: The bug that made login "do nothing until I refreshed"

This one deserves its own section because it taught me the deepest React lesson of the phase. My symptom: I'd log in, and nothing happened — until I refreshed the page, at which point it worked perfectly.

The cause, once I understood it, is fundamental:

Gotcha #8 — React does not watch localStorage. My login handler wrote the token to localStorage, but React only re-renders when a piece of state changes. Writing to storage is completely invisible to it. And my ExpenseList fetched only once, on mount — so on a fresh load it fired with Bearer null, failed, and never ran again. Logging in stashed a token, but nothing told React anything had changed. A refresh "fixed" it only because it re-mounted the whole app after the token was already in storage.

The fix was my first taste of lifting state up: the token belongs in App [the parent], because App is what decides between "logged out" and "logged in." I put the token in React state, seeded from storage, and set it on login:

const [token, setToken] = useState(localStorage.getItem('token'))  // survives refresh

// in the login success branch:
localStorage.setItem('token', data.access_token)
setToken(data.access_token)  // <-- THE missing trigger. State change → re-render → instant switch.

// gate the whole UI on it:
if (token) {
  return (
    <div>
      <h1>Smart Expense Manager</h1>
      <button onClick={handleLogout}>Log out</button>
      <ExpenseList />
    </div>
  )
}
// otherwise fall through to the login form
Enter fullscreen mode Exit fullscreen mode

Logout is just login run backwards — clear both copies of the token:

function handleLogout() {
  localStorage.removeItem('token')  // so a refresh won't silently log me back in
  setToken(null)                    // so the gate falls through to the login form
}
Enter fullscreen mode Exit fullscreen mode

Two more real bugs fell out of this, and both are honest lessons:

  • The stray-render leak. After adding the gated view, logging out still showed the expense list — because an earlier <ExpenseList /> was still sitting in the logged-out return from a previous step. I'd added the new one without removing the old. ExpenseList should appear in exactly one place. A quick file search [it should show up twice: the import + one render] caught it. Lesson: when you restructure, hunt down the leftovers.
  • Incognito confused me for a minute. Logged in on a normal window, I opened incognito, logged in there, and the two windows behaved independently. That's correctlocalStorage is isolated per browser profile, so incognito has its own separate store. [Imagine if it didn't; that'd be a leak.] Two normal tabs share storage but not live React state, so one won't visually update until it refreshes. [Live cross-tab sync via the storage event = know this exists.]

Bonus: so how does clicking "Save" actually change the database?

This confused me, so I'll write down the model that fixed it: there are three separate programs, and none can touch the others' memory.

  1. My React app — JavaScript in the browser. It knows nothing about the database. All it can do is send HTTP requests.
  2. My FastAPI backend — Python in the uvicorn terminal. The only program that talks to the database.
  3. SQLite — the expenses.db file. It only ever hears from the backend.

So the Save button does not save to the database. It sends a PATCH note to the backend asking it to save. The backend checks my token, finds my row [scoped to my user id — my Phase 3 ownership filter riding along], sets the new values, and runs db.commit() — that single line is the actual write to disk. Then it replies 200, and my browser re-fetches so the screen matches what the database now says. Every operation in the app is that same shape: Create = POST + commit, Delete = DELETE + commit, Update = PATCH + commit, Read = GET [no commit — nothing changes]. Once that one round-trip clicked, the whole app made sense.

Stuff I want to remember [the honest takeaways]

  • The frontend has an exact mirror of the backend's tools: Node = Python, npm = pip, package.json = requirements.txt [but it auto-freezes], node_modules = venv, Vite = uvicorn.
  • A component is a Capitalized function returning JSX. JSX rules that bite: one root element, className not class, { } for JS values.
  • useState returns a [value, setter] pair. Never reassign the value — always call the setter. The setter is the only thing that triggers a re-render.
  • React only re-renders on state change. It does not watch localStorage. This one fact caused my nastiest bug.
  • Controlled inputs [value + onChange] make state the single source of truth for a form.
  • event.preventDefault() stops the browser's default full-page reload on form submit. You will forget this once.
  • CORS is a browser security feature, not a bug in your code. Different port = different origin; the fix is CORSMiddleware on the backend, with an explicit origin whitelist [never "*" with credentials].
  • The OAuth2 login endpoint is the odd one out — it wants form-encoded data. Everything else speaks JSON. Mixing them up is the classic 422.
  • fetch does NOT throw on 4xx/5xx. Check response.ok yourself and throw deliberately. Use try/catch/finally, and put setLoading(false) in finally so it can never get stuck.
  • Build the happy path, then close the three gaps: loading, empty, error — and actually test the error state by killing the backend.
  • .map() renders lists; every item needs a stable key — use the DB id, never the array index.
  • && for show-or-nothing; ternary ? : for A-or-B; Fragments <>...</> to group siblings without an extra <div>.
  • Passing args to a handler needs an arrow wrapper [() => fn(id)] or it fires during render. No args → pass the reference.
  • A 204 response has no body — don't call .json() on it.
  • JWT in localStorage is a flagged shortcut [readable by any JS → XSS risk]. Production uses an httpOnly Secure SameSite cookie. React auto-escapes text; avoid dangerouslySetInnerHTML.
  • The Save button doesn't save — it asks the backend to. db.commit() is the real write. The browser, the backend, and the database are three separate programs passing notes.
  • When you restructure, hunt the leftovers — a stray render leaked my list into the logged-out view.
  • Commit at every green checkpoint, with a subject + a "why." Small and often beats one heroic commit.

Next up: Phase 5. The app now has a memory, a mouth, a lock, and — finally — a face a human can log into and use. But that face is honest to a fault: it's a raw list with no styling, no summaries, and a login that just... stops working after 30 minutes with a confusing error [my JWTs expire, and I don't handle it yet]. Phase 5 is where the face gets some expression — a dashboard, spending insights, and the polish that turns "it works" into "I'd actually use this." That, and I owe this app an "expired-token → send me back to login" fix I flagged and walked away from. See you there.

Top comments (0)