Skip to main content

Chapter 5: Building the App

Time to build the core of GratitudeTree. By the end of this chapter, users can create entries, see them in a feed, edit, delete, and upload photos — all persisted in Firebase.

This is the biggest chapter in the tutorial. We'll build the entry creation form, define our data types, connect to Firestore, build the feed, add a post detail view with edit and delete, compress images, and clean up the display. Let's go.


What You'll Build

  • An entry creation form with text and photo toggle
  • TypeScript types for your data model
  • Firestore integration for real-time persistence
  • A social-media-style feed with entry cards
  • A post detail view with edit and delete
  • Image compression for photo uploads
  • Offline persistence

Step 1: Build the Entry Form

Right now your app has a nice shell but no functionality. Let's build the entry creation form. Send this to Claude Code:

>_Claude Code — Step 1
Send this to Claude Code:
> Ok. I want to now start building the entry form for a memory. It needs: a toggle between 'text' and 'photo' entry types. a textarea for text entries, a file input with image preview for photo entries, an optional caption field for photos, and a Save Entry button.
What to expect: Claude Code will create or update a page component with a form that toggles between text and photo modes, with appropriate inputs for each.
Tip: Claude Code will figure out where to put this based on your project structure. It may create a new page or update the existing Create page.
$ claude "Ok. I want to now start building the entry form for a memory. It needs: a toggle..."

Test it

npm run dev

Navigate to the Create tab. You should see a form with a text/photo toggle. Try switching between modes — the inputs should change. The Save button won't do anything yet, and that's expected.

As of now, nothing is getting stored. We need to define our data types and connect to Firebase first.


Step 2: Define Types and Set Up Firebase

Now let's define the data model and make sure Firebase is properly configured. Claude Code will ask you for any values it needs:

>_Claude Code — Step 2
Send this to Claude Code:
> Lets define the entry type for this project first and set up firebase as needed. Please ask me for what value are needed to set this up. I have already created a project in firebase console. Please make sure my firebase values are in a .env file.
What to expect: Claude Code will create a types file with the Entry type definition and ensure your Firebase config is reading from .env variables. It may ask you for your Firebase config values.
Tip: If Claude Code asks for your Firebase values, paste them from the Firebase Console (Project Settings → Your apps → Web app → Config). Make sure they go into the .env file in your project root.
$ claude "Lets define the entry type for this project first and set up firebase as needed...."
Where Is .env?

The .env file must be in your project root — the same folder that contains package.json. If it's nested inside src/ or anywhere else, it won't work. You must restart the dev server (Ctrl+C then npm run dev) after creating or editing .env.


Step 3: Wire Save to Firestore

Next step is wiring the Save button to actually write to Firestore. Send this to Claude Code:

>_Claude Code — Step 3
Send this to Claude Code:
> Wire up the handleSave in the entry form to actually write to Firestore. Create a Firestore service file if needed with functions to create, update, and delete entries. Use serverTimestamp for the createdAt field. Also set up a real-time listener with onSnapshot so entries update automatically.
Why: We're going straight to Firestore — no local state. When you save an entry, it goes to the cloud. When you load the feed, it reads from the cloud in real-time.
What to expect: Claude Code will create a Firestore service file and wire the form's save handler to write documents to Firestore.
$ claude "Wire up the handleSave in the entry form to actually write to Firestore. Create ..."

What just happened

Instead of storing entries in memory (where they'd disappear on refresh), we're saving directly to Firestore. This means:

  • Entries persist — refresh the page and they're still there
  • Real-time sync — changes appear instantly without refreshing
  • Cloud-hosted — your data lives on Firebase's servers

The onSnapshot listener is the key — it watches for any changes in the Firestore collection and automatically updates the UI. It's like a radio that's always tuned in.

Firestore rules (development)

Before you test saving entries, confirm Firestore Database → Rules in the Firebase console allows access to your entries collection while you're still before authentication. For local development, rules often look like this — open read/write on entries, then Publish if you see an unpublished changes banner:

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /entries/{entryId} {
allow read, write: if true;
}
}
}

Firestore Rules tab — development rules for the entries collection (publish before testing)

Temporary only

These rules let anyone read and write entries. That is fine on a personal dev project. In Chapter 6 — Authentication, you will replace them with rules that require a signed-in user and match each document's userId.


Step 4: Build the Feed

Now let's build the feed page where entries are displayed:

>_Claude Code — Step 4
Send this to Claude Code:
> Lets update the feed page to create feed type cards similar to social media apps where each entry is a feed item. Read from firestore to pull in the data for each feed card.
What to expect: Claude Code will update the Feed page to display entries as styled cards, reading data from Firestore in real-time.
Tip: The feed should show entries newest-first. Each card should display the date, content (text or image), and optional caption.
$ claude "Lets update the feed page to create feed type cards similar to social media apps..."

Test it

npm run dev
  1. Go to the Create tab and write a text entry
  2. Save it
  3. Switch to the Feed tab — your entry should appear as a card
  4. Refresh the page — the entry should still be there (this is the big moment!)
  5. Create a few more entries — they should stack newest-first

Step 5: Add Post Detail View with Edit and Delete

Each entry needs a detail view where users can edit or delete it:

>_Claude Code — Step 5
Send this to Claude Code:
> For each feed card, add a tap/click handler so that when I tap a post, it opens a detail view showing the full post and its posting date. Include an Edit action that lets me change the text or add an image, and a Delete action so I can remove the post. The header should include a back icon that returns to the feed and closes the post detail view.
What to expect: Claude Code will add a post detail view that opens when you tap a card. It will include the full post content, date, edit functionality, delete with confirmation, and a back button.
$ claude "For each feed card, add a tap/click handler so that when I tap a post, it opens ..."

Test the full CRUD flow

  1. Create — make a new entry, it appears in the feed
  2. Read — tap an entry card to open the detail view
  3. Update — tap edit, change the text, save — it updates
  4. Delete — tap delete, confirm — the entry disappears
  5. Refresh — all changes should persist

Step 6: Clean Up and Review

Let's have Claude Code review everything and fix any issues:

>_Claude Code
Try asking:
> Review my project and make sure everything is working correctly. Check for any errors or issues with the entry form, feed, detail view, and Firestore integration.
Tip: When in doubt, ask Claude Code to review your work. It's like having a senior developer check your code.
$ claude "Review my project and make sure everything is working correctly. Check for any e..."

Step 7: Add Offline Persistence

Firestore has built-in offline support. When the user goes offline, reads come from a local cache and writes are queued until the connection returns. Let's enable it:

>_Claude Code — Step 7
Send this to Claude Code:
> Update src/lib/firebase.ts to use initializeFirestore with persistentLocalCache and persistentMultipleTabManager for offline persistence, instead of the default getFirestore. This is the modern Firebase v10+ approach.
Why: Offline persistence lets the app work without internet — reads come from IndexedDB cache and writes queue until reconnection.
What to expect: Claude Code will update firebase.ts to use initializeFirestore with persistence configuration instead of getFirestore.
$ claude "Update src/lib/firebase.ts to use initializeFirestore with persistentLocalCache ..."

Step 8: Add Image Compression

Phone cameras take huge photos (3000x4000 pixels, 4MB+). We need to compress them before uploading:

>_Claude Code — Step 8
Send this to Claude Code:
> Create an image compression utility at src/lib/image.ts. The compressImage function should take a File, load it into an Image element, resize it proportionally to max 1200x1200, draw it to a Canvas, and export as JPEG at 80% quality. This reduces 4MB phone photos to ~200-400KB. Update AddEntryPage as needed
Why: Compressing images before uploading saves bandwidth, reduces storage costs, and makes the feed load faster.
What to expect: Claude Code will create an image compression utility and integrate it into the photo upload flow.
$ claude "Create an image compression utility at src/lib/image.ts. The compressImage funct..."
note

Your images may not look nice when uploading to the feed at first — the display might cut them off or stretch them. Don't worry, we'll fix that next.


Step 9: Fix Image Display in Feed

Let's clean up how images appear in the feed:

>_Claude Code — Step 9
Send this to Claude Code:
> Improve how uploaded images appear in the feed cards: the entire image should always be visible (no cropping or clipping that hides parts of the photo). Scale images responsively for mobile—fit within the feed width, preserve aspect ratio, and avoid rendering at native full resolution so nothing overflows or dominates the screen.
What to expect: Claude Code will adjust feed image styling so the full photo is visible, scaled for mobile widths with aspect ratio preserved, without inappropriate cropping or oversized raw dimensions.
$ claude "Improve how uploaded images appear in the feed cards: the entire image should al..."

Step 10: Set Up Storage Security Rules

In the Firebase console, go to Storage → Rules and set up basic rules. For now, allow reads and writes with a file size limit. We'll lock this down further when we add authentication in the next chapter.


Step 11: Commit Your Progress (Optional)

If you're using Git:

git add .
git commit -m "Add entries: creation form, Firestore persistence, feed, detail view, image compression"
Running Into Issues?

Don't hesitate to paste error messages directly into Claude Code and ask it to help you debug. You can also copy errors from your terminal and ask Claude Code to explain what went wrong and how to fix it.

⚠️Common Issues

Checkpoint

Checkpoint — End of Chapter 5

Your app should now:

  • You can create text entries and they appear in the Feed
  • You can create photo entries with image preview and optional caption
  • Entries persist across page refreshes (stored in Firestore)
  • You can tap an entry to open a detail view
  • You can edit any entry from the detail view
  • You can delete any entry with confirmation
  • Images are compressed before upload
  • Images display properly scaled in the feed (not cut off)
  • The Firebase console shows your entries as documents

What's Next

Your app has real functionality with persistent data. But right now, anyone can see everyone's entries. In the next chapter, we add authentication — login, signup, and private data.

𝕏

Follow @parvsondhi for build threads, tips, and updates on this tutorial.

Next: Chapter 6 — Authentication →