Build a Creator Idea Vault
Build a full-stack idea vault using Laravel, React, and Firebase for authentication and storage.
Introduction
30 Second Summary
Every creator has had that moment where a brilliant idea strikes at 2 AM, only to be forgotten by morning. Worse, even if you write it down, how do you prove when you first had the idea?
In this project, you will build Originly, a mobile-first idea capture app that stores your ideas in a secure vault with tamper-proof server timestamps proving exactly when each idea was recorded.
What You'll Build
A React web application powered by Firebase that handles user authentication, real-time database storage, and client-side search in a mobile-optimized interface.
By the end of this project, you'll have:
- A React + Vite web application with Firebase Authentication for email/password sign-up and sign-in.
- A Firestore database storing ideas with server-generated timestamps that prove when each idea was captured.
- A mobile-optimized UI with instant idea capture, real-time updates, and a searchable vault view.
- Secret Mission: Add a view-only sharing link so creators can share a single idea without exposing their entire vault.
Are there any prerequisites?
Basic JavaScript and HTML/CSS knowledge is required. Familiarity with React components (props, state, event handlers) will help you follow along smoothly.
Set Up the Project and Firebase
Every app starts with a foundation. Before you can capture ideas, you need a project structure, a build tool to run your code, and a backend service to handle authentication and data storage.
In this step, you will scaffold a React app using Vite, connect it to Firebase, and configure the services that power Originly's login system and database.
In this step, get ready to:
- Scaffold a React + Vite project and install the Firebase SDK.
- Create a Firebase project with Authentication and Firestore enabled.
- Connect your app to Firebase by creating a configuration file.
Scaffold the React + Vite project and install Firebase
Vite is a fast build tool that gives you instant browser reloads as you edit code. You will use it to scaffold a React project called originly on your Desktop.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal, and pressing Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Scaffold the Originly project by running this command:
npm create vite@latest originly -- --template react
What does this command do?
This uses Vite's scaffolding tool to create a new folder called originly with a React project inside it. The --template react flag tells Vite to set up React for you automatically.
- Confirm the folder was created by running:
ls
You should see originly listed in the output.
- Navigate into the project and install Firebase by running these commands:
cd originly
npm install firebase
npm install
What does this install?
The npm install firebase command adds the Firebase SDK to your project. This single package includes Authentication, Firestore, and every other Firebase service you might need.
The npm install command installs all remaining dependencies listed in the project's package.json file.
- Start the development server by running:
npm run dev
You should see a local URL in your terminal output (typically http://localhost:5173). Open that URL in your browser.
You should see the default Vite + React welcome page with a counter button.
Don't see the welcome page?
- Make sure you ran npm install before npm run dev. Without installing dependencies first, the dev server cannot start.
- Check that the terminal shows no red error messages. If you see a port conflict, try stopping other running servers.
- Help me fix my Vite dev server
Now that the scaffold works, you will clean it up for the Originly project. Keep the dev server running in this terminal.
- Open VS Code by pressing Cmd+Space (macOS) or the Windows key (Windows), typing VS Code, and pressing Enter.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the originly folder, and click Open.
- In the Explorer sidebar on the left, click index.html to open it.
- Find the line that says:
<title>Vite + React</title>
- Replace that entire <title> line with:
<title>Originly</title>
- Also find and delete the favicon line (<link rel="icon" .../>) so your <head> section looks like this:
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Originly</title>
</head>
- Now open src/main.jsx from the Explorer sidebar.
- Replace all the content of src/main.jsx with the following:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./App.css";
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>
);
Why change the CSS import?
The default scaffold imports index.css at the root level. Originly will keep all styles in a single App.css file for simplicity. This change ensures the correct stylesheet loads when you add styling later.
- Save both files (Cmd+S on macOS, Ctrl+S on Windows).
- Check your browser. The page may show an error or a blank screen because the default App.jsx still references assets you will replace. That is expected for now.
Create and configure the Firebase project
Firebase provides authentication and a real-time database in one platform. You will create a Firebase project, turn on the login service, and create the database where ideas will be stored.
- Open console.firebase.google.com in your browser.
- Click Add project.
- Enter Originly as the project name.
- Follow the on-screen prompts (you can disable Google Analytics for this project) and click Create project.
- Wait for the project to finish creating, then click Continue.
Next, enable the email/password sign-in method so users can create accounts.
- In the left sidebar, click Security.
- Click Authentication.
- Click the Sign-in method tab.
- Click Email/password.
- Toggle the Enable switch on.
- Click Save.
What is Firebase Authentication?
Firebase Authentication handles user sign-up, login, and session management for you. By enabling the Email/password provider, users can register with an email and password without you building the login system from scratch.
Now create the Firestore database where ideas will be stored.
- In the left sidebar, click Databases & Storage.
- Click Firestore.
- Click Create database.
- Select a location for your database (pick the region closest to you).
- Select Test mode for the security rules starting mode.
- Click Create.
What is Firestore?
Firestore is a cloud-hosted NoSQL database. It stores data as documents inside collections.
You are starting in test mode so your app can read and write freely during development. You will lock it down with proper security rules in a later step.
Finally, register a web app so Firebase gives you the configuration values your code needs.
- Click the gear icon next to Project Overview in the top left, then click Project settings.
- Scroll down to the Your apps section.
- Click the web icon (</>) to register a new web app.
- Enter Originly as the app nickname.
- Click Register app.
- Firebase will display a code snippet containing your firebaseConfig object. Keep this page open. You will copy these values in the next substep.
Keep this tab open
You will need the firebaseConfig values (apiKey, authDomain, projectId, etc.) in the next substep. Do not close this browser tab.
Set up the Firebase configuration file
Your React app needs to know which Firebase project to connect to. You will create a configuration file that initializes Firebase and exports the Auth and Firestore services for use throughout the app.
- In VS Code, right-click the src folder in the Explorer sidebar and click New File.
- Name the file firebase.js and press Enter.
- Paste the following code into src/firebase.js, replacing the placeholder values with the real values from your Firebase Console tab:
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
// Your Firebase project configuration
const firebaseConfig = {
apiKey: "your-api-key-here",
authDomain: "your-project-id.firebaseapp.com",
projectId: "your-project-id-here",
storageBucket: "your-project-id.firebasestorage.app",
messagingSenderId: "your-sender-id-here",
appId: "your-app-id-here"
};
// Initialize Firebase and export services for use in other components
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
What does this code do?
- initializeApp(firebaseConfig) connects your app to your specific Firebase project using the config values you copied.
- getAuth(app) gives you the Authentication service, which you will use to sign users up and in.
- getFirestore(app) gives you the Firestore database service, which you will use to save and retrieve ideas.
- Both are exported so any component in your app can import and use them.
Do not commit your config values to a public repository
While Firebase API keys are safe for client-side use (they identify your project, not authenticate requests), it is still good practice to keep them out of public code. For this learning project, pasting them directly is fine.
- Save src/firebase.js.
- Check the terminal where your dev server is running. You should see no new errors.
- Check your browser. The page should still load without crashing (it may look broken from the scaffold cleanup, but there should be no white screen with a red error overlay).
Seeing a module not found error?
- Make sure you ran npm install firebase earlier. Without the Firebase package installed, the imports in firebase.js will fail.
- Double-check that the file is saved at src/firebase.js (inside the src folder, not the project root).
- Help me fix this module error
✔️ Awesome, I've got everything!
Great. Make sure you have saved all three files: index.html, src/main.jsx, and src/firebase.js.
ⓧ I'd like to double check the full code
Here are the three files you should have at this point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Originly</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./App.css";
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>
);
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "your-api-key-here",
authDomain: "your-project-id.firebaseapp.com",
projectId: "your-project-id-here",
storageBucket: "your-project-id.firebasestorage.app",
messagingSenderId: "your-sender-id-here",
appId: "your-app-id-here"
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
Your project foundation is solid. You have a running React app connected to Firebase with both Authentication and Firestore ready to go. Next up, you will build the login and sign-up flow so users can create accounts and access their personal vault.
Build Authentication
In Step 1, you set up the Originly project and connected it to Firebase. Now it's time to build the feature that keeps each creator's vault private: authentication.
Firebase Authentication handles all the security complexity (password hashing, session management, account recovery) behind a few function calls. You'll build a form that lets users sign up or sign in, then wire it into your app so only authenticated users can access the vault.
In this step, get ready to:
- Build an authentication form with email and password inputs that toggles between sign-up and sign-in modes.
- Add auth state tracking so the app shows the login form or vault based on whether a user is signed in.
- Add a Sign Out button to let users log out.
Create the authentication form component
This component handles both registration and login in one form. A toggle button switches between "Sign Up" and "Sign In" modes, keeping the interface clean.
- Inside your src folder, create a new folder called components.
- Create a new file called Auth.jsx inside src/components/ and add the following code:
import { useState } from "react";
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
} from "firebase/auth";
import { auth } from "../firebase";
function Auth() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isSignUp, setIsSignUp] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
try {
if (isSignUp) {
// Register a new user with Firebase Auth
await createUserWithEmailAndPassword(auth, email, password);
} else {
// Sign in an existing user
await signInWithEmailAndPassword(auth, email, password);
}
} catch (err) {
// Display Firebase's error message to the user
setError(err.message);
}
};
return (
<div className="auth-container">
<h1>Originly</h1>
<p className="auth-tagline">Capture your ideas. Prove they're yours.</p>
<form onSubmit={handleSubmit} className="auth-form">
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{error && <p className="error-message">{error}</p>}
<button type="submit" className="auth-btn">
{isSignUp ? "Sign Up" : "Sign In"}
</button>
</form>
<button
onClick={() => setIsSignUp(!isSignUp)}
className="toggle-auth-btn"
>
{isSignUp
? "Already have an account? Sign In"
: "Need an account? Sign Up"}
</button>
</div>
);
}
export default Auth;
What does this code do?
- createUserWithEmailAndPassword(auth, email, password) registers a brand-new user in your Firebase project.
- signInWithEmailAndPassword(auth, email, password) authenticates an existing user and starts a session.
- The isSignUp state variable controls which function runs when the form is submitted.
- If either call fails (invalid email, weak password, wrong credentials), the catch block stores the error message in state so it appears on screen.
- Save src/components/Auth.jsx.
You won't see this component in the browser yet. The next substep wires it into your main App component.
Add auth state management to the app
Your app needs to know whether someone is logged in. Firebase Authentication provides a listener called onAuthStateChanged that fires every time the auth state changes (sign in, sign out, page refresh).
Your App component will show the Auth form when no user is logged in, and show the vault layout when a user is authenticated.
- The App component will import two vault components (IdeaForm and IdeaList) that you will build in later steps. Create placeholder files now so the app does not crash.
- Create src/components/IdeaForm.jsx with this placeholder code:
// Placeholder - full component built in Step 3
function IdeaForm() {
return null;
}
export default IdeaForm;
- Create src/components/IdeaList.jsx with this placeholder code:
// Placeholder - full component built in Step 4
function IdeaList() {
return null;
}
export default IdeaList;
Why create placeholders?
React will crash if you import a file that does not exist. These empty components let your app render while you build the real vault features in the next steps.
- Now replace the entire contents of src/App.jsx with the following code:
import { useState, useEffect } from "react";
import { onAuthStateChanged, signOut } from "firebase/auth";
import { auth } from "./firebase";
import Auth from "./components/Auth";
import IdeaForm from "./components/IdeaForm";
import IdeaList from "./components/IdeaList";
import "./App.css";
function App() {
// Track the currently signed-in user
const [user, setUser] = useState(null);
// Show a loading screen while Firebase checks auth state
const [loading, setLoading] = useState(true);
useEffect(() => {
// Listen for auth state changes (sign in, sign out, page refresh)
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
setLoading(false);
});
// Clean up the listener when the component unmounts
return () => unsubscribe();
}, []);
const handleSignOut = async () => {
await signOut(auth);
};
if (loading) {
return <div className="loading">Loading...</div>;
}
// No user logged in - show the auth form
if (!user) {
return <Auth />;
}
// User is logged in - show the vault layout
return (
<div className="app">
<header className="app-header">
<h1>Originly</h1>
<button onClick={handleSignOut} className="sign-out-btn">
Sign Out
</button>
</header>
<main>
<IdeaForm userId={user.uid} />
<IdeaList userId={user.uid} />
</main>
</div>
);
}
export default App;
What does this code do?
- onAuthStateChanged registers a listener that runs every time a user signs in, signs out, or refreshes the page. It updates the user state variable.
- While Firebase is checking auth state on page load, loading is true so you avoid a flash of the login form.
- If user is null, the component renders <Auth />. Otherwise it renders the vault layout with a Sign Out button.
- handleSignOut calls Firebase's signOut function, which clears the session and triggers onAuthStateChanged to update user back to null.
- Now replace the entire contents of src/App.css with the following styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #0f0f0f;
color: #f5f5f5;
min-height: 100vh;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 1.2rem;
color: #888;
}
/* Auth Styles */
.auth-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1.5rem;
}
.auth-container h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: #fff;
}
.auth-tagline {
color: #888;
margin-bottom: 2rem;
text-align: center;
}
.auth-form {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.auth-form input {
padding: 1rem;
border: 1px solid #333;
border-radius: 8px;
background: #1a1a1a;
color: #f5f5f5;
font-size: 1rem;
}
.auth-btn {
padding: 1rem;
border: none;
border-radius: 8px;
background: #4f46e5;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
.auth-btn:hover {
background: #4338ca;
}
.toggle-auth-btn {
margin-top: 1rem;
background: none;
border: none;
color: #4f46e5;
cursor: pointer;
font-size: 0.9rem;
}
.error-message {
color: #ef4444;
font-size: 0.9rem;
}
/* App Layout */
.app {
max-width: 600px;
margin: 0 auto;
padding: 1rem;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
margin-bottom: 1rem;
}
.app-header h1 {
font-size: 1.5rem;
}
.sign-out-btn {
padding: 0.5rem 1rem;
border: 1px solid #333;
border-radius: 6px;
background: none;
color: #888;
cursor: pointer;
font-size: 0.85rem;
}
.sign-out-btn:hover {
border-color: #ef4444;
color: #ef4444;
}
What do these styles do?
- The global reset (*) removes default browser margins and padding for consistent spacing.
- The .auth-container uses flexbox to center the login form vertically and horizontally on the full screen.
- The .app class constrains the vault layout to 600px max-width, keeping it comfortable on both mobile and desktop.
- The dark color scheme (#0f0f0f background, #f5f5f5 text) gives Originly a modern, focused feel.
- Save both src/App.jsx and src/App.css.
- Return to your browser. You should see the Originly sign-in form centered on a dark background with the tagline "Capture your ideas. Prove they're yours." below the title.
Don't see the auth form?
- Check your terminal for errors. If you see a message about a module not being found, make sure src/components/IdeaForm.jsx and src/components/IdeaList.jsx exist with the placeholder code.
- Verify that src/components/Auth.jsx imports from "../firebase" (two dots, going up one directory level).
- Make sure you replaced the entire contents of App.jsx and App.css. Leftover default Vite content can cause conflicts.
Help me debug my Originly authentication form not rendering in the browser.
Now test the authentication flow to make sure everything works.
- Click Need an account? Sign Up at the bottom of the form to switch to sign-up mode.
- Enter a test email (like test@example.com) and a password with at least 6 characters.
- Click Sign Up.
After signing up successfully, the app switches to the vault layout showing the "Originly" header and a Sign Out button. The page is otherwise empty because the vault components are still placeholders.
- Click Sign Out to return to the auth form.
- Sign back in with the same email and password to confirm login works.
Seeing an error message on the form?
- If Firebase says the password is too weak, use a password with at least 6 characters.
- If you see an error about invalid credentials when signing in, make sure you are using the exact email and password you signed up with.
- If you see an error mentioning the email provider not being enabled, go back to the Firebase Console and confirm that Email/password is enabled under Security > Authentication > Sign-in method.
Help me fix a Firebase Authentication error when signing up or signing in to my Originly app.
✔️ Awesome, I've got everything!
Great. Double check you have saved all your files, then move on.
ⓧ I'd like to double check the full code
Here is the complete code for each file at this point:
import { useState } from "react";
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
} from "firebase/auth";
import { auth } from "../firebase";
function Auth() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isSignUp, setIsSignUp] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
try {
if (isSignUp) {
await createUserWithEmailAndPassword(auth, email, password);
} else {
await signInWithEmailAndPassword(auth, email, password);
}
} catch (err) {
setError(err.message);
}
};
return (
<div className="auth-container">
<h1>Originly</h1>
<p className="auth-tagline">Capture your ideas. Prove they're yours.</p>
<form onSubmit={handleSubmit} className="auth-form">
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{error && <p className="error-message">{error}</p>}
<button type="submit" className="auth-btn">
{isSignUp ? "Sign Up" : "Sign In"}
</button>
</form>
<button
onClick={() => setIsSignUp(!isSignUp)}
className="toggle-auth-btn"
>
{isSignUp
? "Already have an account? Sign In"
: "Need an account? Sign Up"}
</button>
</div>
);
}
export default Auth;
import { useState, useEffect } from "react";
import { onAuthStateChanged, signOut } from "firebase/auth";
import { auth } from "./firebase";
import Auth from "./components/Auth";
import IdeaForm from "./components/IdeaForm";
import IdeaList from "./components/IdeaList";
import "./App.css";
function App() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
setLoading(false);
});
return () => unsubscribe();
}, []);
const handleSignOut = async () => {
await signOut(auth);
};
if (loading) {
return <div className="loading">Loading...</div>;
}
if (!user) {
return <Auth />;
}
return (
<div className="app">
<header className="app-header">
<h1>Originly</h1>
<button onClick={handleSignOut} className="sign-out-btn">
Sign Out
</button>
</header>
<main>
<IdeaForm userId={user.uid} />
<IdeaList userId={user.uid} />
</main>
</div>
);
}
export default App;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #0f0f0f;
color: #f5f5f5;
min-height: 100vh;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 1.2rem;
color: #888;
}
/* Auth Styles */
.auth-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1.5rem;
}
.auth-container h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: #fff;
}
.auth-tagline {
color: #888;
margin-bottom: 2rem;
text-align: center;
}
.auth-form {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.auth-form input {
padding: 1rem;
border: 1px solid #333;
border-radius: 8px;
background: #1a1a1a;
color: #f5f5f5;
font-size: 1rem;
}
.auth-btn {
padding: 1rem;
border: none;
border-radius: 8px;
background: #4f46e5;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
.auth-btn:hover {
background: #4338ca;
}
.toggle-auth-btn {
margin-top: 1rem;
background: none;
border: none;
color: #4f46e5;
cursor: pointer;
font-size: 0.9rem;
}
.error-message {
color: #ef4444;
font-size: 0.9rem;
}
/* App Layout */
.app {
max-width: 600px;
margin: 0 auto;
padding: 1rem;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
margin-bottom: 1rem;
}
.app-header h1 {
font-size: 1.5rem;
}
.sign-out-btn {
padding: 0.5rem 1rem;
border: 1px solid #333;
border-radius: 6px;
background: none;
color: #888;
cursor: pointer;
font-size: 0.85rem;
}
.sign-out-btn:hover {
border-color: #ef4444;
color: #ef4444;
}
Your authentication flow is complete. Users can sign up, sign in, and sign out. Next up, you'll build the idea capture form so authenticated users can start saving ideas to their vault.
Build the Idea Capture Form
You have a working authentication flow. Users can sign up and sign in to Originly.
Now you need a way for users to actually capture their ideas. In this step, you will build a form that writes ideas to Firestore with server-generated timestamps and lock down the database so only the idea's owner can access it.
In this step, get ready to:
- Create an idea capture form with a textarea and submit button.
- Save ideas to Firestore with tamper-proof server timestamps.
- Set up Firestore security rules so only the owner can access their ideas.
Create the idea input component
The capture form is the core interaction in Originly. It needs to be fast and frictionless, especially on mobile where most ideas strike.
- Create a new file called IdeaForm.jsx inside the src/components folder.
- Add the following code to build the idea form component:
import { useState } from "react";
import { collection, addDoc, serverTimestamp } from "firebase/firestore";
import { db } from "../firebase";
function IdeaForm({ userId }) {
const [text, setText] = useState("");
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!text.trim()) return;
setSaving(true);
try {
// Write a new document to the "ideas" collection
await addDoc(collection(db, "ideas"), {
text: text.trim(),
userId: userId,
createdAt: serverTimestamp(),
});
// Clear the form and show confirmation
setText("");
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} catch (err) {
console.error("Error saving idea:", err);
}
setSaving(false);
};
return (
<form onSubmit={handleSubmit} className="idea-form">
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="What's your idea?"
rows={3}
required
/>
<button type="submit" disabled={saving} className="capture-btn">
{saving ? "Saving..." : "Capture Idea"}
</button>
{saved && <p className="saved-message">Idea captured!</p>}
</form>
);
}
export default IdeaForm;
What does this code do?
This component handles both the UI and the save logic for capturing ideas.
- addDoc creates a new document in the ideas collection every time the user submits.
- serverTimestamp() tells Firebase to stamp the document with the server's clock, not the user's device clock. This makes timestamps tamper-proof.
- The saving state disables the button during the write to prevent duplicate submissions.
- The saved state briefly shows a green confirmation message, then hides it after 2 seconds.
- Save src/components/IdeaForm.jsx.
Seeing an import error in your terminal?
If your dev server shows an error about IdeaList not being found, that is expected. App.jsx already imports IdeaList which you have not created yet. You will build that component in the next step.
help me fix a module not found error for IdeaList in my React app
Now add the mobile-first styles for the form. These give the textarea and button large touch targets that feel natural on a phone screen.
- Open src/App.css and add the following styles at the bottom of the file, below the existing .sign-out-btn:hover rule:
/* Idea Form */
.idea-form {
margin-bottom: 2rem;
}
.idea-form textarea {
width: 100%;
padding: 1rem;
border: 1px solid #333;
border-radius: 8px;
background: #1a1a1a;
color: #f5f5f5;
font-size: 1rem;
resize: vertical;
min-height: 80px;
font-family: inherit;
}
.idea-form textarea:focus {
outline: none;
border-color: #4f46e5;
}
.capture-btn {
width: 100%;
margin-top: 0.75rem;
padding: 1rem;
border: none;
border-radius: 8px;
background: #4f46e5;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
.capture-btn:hover {
background: #4338ca;
}
.capture-btn:disabled {
background: #333;
cursor: not-allowed;
}
.saved-message {
text-align: center;
color: #22c55e;
margin-top: 0.5rem;
font-size: 0.9rem;
}
What do these styles do?
The textarea takes full width with generous padding so it is easy to tap on mobile. The capture-btn is also full-width with 1rem padding, making it a large touch target.
The disabled state turns the button grey during saves. The saved-message class shows a green confirmation that fades in briefly after a successful capture.
- Save src/App.css.
Form not appearing in the browser?
Make sure you are signed in. The form only renders when a user is authenticated. If you see the auth screen, sign in with the account you created earlier.
If the IdeaList import error prevents the page from loading, create a temporary placeholder file at src/components/IdeaList.jsx that exports an empty component. You will replace it in the next step.
help me fix my IdeaForm not showing up after signing in
✔️ Awesome, I've got everything!
Great. Double check you have saved both files before moving on.
ⓧ I'd like to double check the full code
Here is the complete IdeaForm.jsx file:
import { useState } from "react";
import { collection, addDoc, serverTimestamp } from "firebase/firestore";
import { db } from "../firebase";
function IdeaForm({ userId }) {
const [text, setText] = useState("");
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!text.trim()) return;
setSaving(true);
try {
await addDoc(collection(db, "ideas"), {
text: text.trim(),
userId: userId,
createdAt: serverTimestamp(),
});
setText("");
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} catch (err) {
console.error("Error saving idea:", err);
}
setSaving(false);
};
return (
<form onSubmit={handleSubmit} className="idea-form">
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="What's your idea?"
rows={3}
required
/>
<button type="submit" disabled={saving} className="capture-btn">
{saving ? "Saving..." : "Capture Idea"}
</button>
{saved && <p className="saved-message">Idea captured!</p>}
</form>
);
}
export default IdeaForm;
Here is the complete App.css file with all styles so far:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #0f0f0f;
color: #f5f5f5;
min-height: 100vh;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 1.2rem;
color: #888;
}
/* Auth Styles */
.auth-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1.5rem;
}
.auth-container h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: #fff;
}
.auth-tagline {
color: #888;
margin-bottom: 2rem;
text-align: center;
}
.auth-form {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.auth-form input {
padding: 1rem;
border: 1px solid #333;
border-radius: 8px;
background: #1a1a1a;
color: #f5f5f5;
font-size: 1rem;
}
.auth-btn {
padding: 1rem;
border: none;
border-radius: 8px;
background: #4f46e5;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
.auth-btn:hover {
background: #4338ca;
}
.toggle-auth-btn {
margin-top: 1rem;
background: none;
border: none;
color: #4f46e5;
cursor: pointer;
font-size: 0.9rem;
}
.error-message {
color: #ef4444;
font-size: 0.9rem;
}
/* App Layout */
.app {
max-width: 600px;
margin: 0 auto;
padding: 1rem;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
margin-bottom: 1rem;
}
.app-header h1 {
font-size: 1.5rem;
}
.sign-out-btn {
padding: 0.5rem 1rem;
border: 1px solid #333;
border-radius: 6px;
background: none;
color: #888;
cursor: pointer;
font-size: 0.85rem;
}
.sign-out-btn:hover {
border-color: #ef4444;
color: #ef4444;
}
/* Idea Form */
.idea-form {
margin-bottom: 2rem;
}
.idea-form textarea {
width: 100%;
padding: 1rem;
border: 1px solid #333;
border-radius: 8px;
background: #1a1a1a;
color: #f5f5f5;
font-size: 1rem;
resize: vertical;
min-height: 80px;
font-family: inherit;
}
.idea-form textarea:focus {
outline: none;
border-color: #4f46e5;
}
.capture-btn {
width: 100%;
margin-top: 0.75rem;
padding: 1rem;
border: none;
border-radius: 8px;
background: #4f46e5;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
.capture-btn:hover {
background: #4338ca;
}
.capture-btn:disabled {
background: #333;
cursor: not-allowed;
}
.saved-message {
text-align: center;
color: #22c55e;
margin-top: 0.5rem;
font-size: 0.9rem;
}
Save ideas to Firestore with server timestamps
The form is built. Now test it by capturing your first idea and verifying it lands in Firestore.
- Sign in to Originly in your browser (or refresh if you are already signed in).
- Type a test idea into the textarea, for example: My first idea captured on Originly.
- Click the Capture Idea button.
You should see the button text change to "Saving..." briefly, then the form clears and a green "Idea captured!" message appears for 2 seconds.
- Return to the Firebase Console in your browser.
- In the left panel, navigate to Databases & Storage then click Firestore.
- Click the Data tab.
- You should see an ideas collection with a document inside it containing your idea text, your user ID, and a createdAt timestamp.
Why serverTimestamp instead of new Date()?
If you used new Date() in JavaScript, the timestamp would come from the user's device. A user could change their device clock and fake when an idea was captured.
serverTimestamp() tells Firebase to set the time on its servers. The user has no way to manipulate this value. This creates a verifiable record of when the idea was saved.
Not seeing your idea in Firestore?
Check your browser's developer console for errors. If you see a permission denied error, your Firestore is no longer in test mode. Re-enable test mode temporarily from the Rules tab (you will set proper rules in the next substep).
If you see no errors but no data appears, make sure you clicked inside the ideas collection in the Firestore console. Documents are nested inside collections.
help me debug why my idea is not saving to Firestore
Set up Firestore security rules
Your database is currently in test mode, which means anyone on the internet could read or write data. You need to replace these open rules with rules that only allow authenticated users to access their own ideas.
- In the Firebase Console, navigate to Databases & Storage then click Firestore.
- Click the Rules tab.
- Select all the existing rules in the editor and delete them.
- Paste the following security rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /ideas/{ideaId} {
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
allow read, update, delete: if request.auth != null
&& resource.data.userId == request.auth.uid;
}
}
}
What do these rules do?
These rules enforce two things:
- allow create checks that the user is logged in AND that the userId field in the new document matches their auth UID. This prevents users from creating ideas under someone else's account.
- allow read, update, delete checks that the existing document's userId field matches the requesting user's UID. Only the idea's owner can see or modify it.
- Click Publish to deploy the rules.
- Return to your app in the browser, sign in, and capture another test idea.
- Verify the idea still saves successfully. The green "Idea captured!" message confirms the rules allow your write.
Getting a permission denied error after publishing rules?
Double check that the userId field you write in IdeaForm.jsx matches exactly what the rules expect. The component passes userId: userId which comes from user.uid in App.jsx. If these do not match, the create rule will reject the write.
Also confirm you copied the rules exactly as shown, including rules_version = '2'; at the top.
help me fix a Firestore permission denied error after updating security rules
Your idea capture form is working and your database is secured. Next up, you will build the vault view that queries and displays all your saved ideas in real time.
Build the Vault View
You can capture ideas into Firestore, but there is no way to see them yet.
In this step, you will build the vault view that pulls ideas from Firestore in real-time and displays them in a searchable, mobile-friendly card layout.
In this step, get ready to:
- Query Firestore for your ideas with real-time updates.
- Display ideas as cards with formatted timestamps and a search filter.
- Style the vault with mobile-first CSS.
Query and display ideas
The vault needs to fetch only the current user's ideas, ordered newest-first, and update instantly when a new idea is saved.
- Create a new file at src/components/IdeaList.jsx.
- Add the full component code by pasting the following:
import { useState, useEffect } from "react";
import {
collection,
query,
where,
orderBy,
onSnapshot,
} from "firebase/firestore";
import { db } from "../firebase";
function IdeaList({ userId }) {
// Store the array of idea objects from Firestore
const [ideas, setIdeas] = useState([]);
// Store the current search filter text
const [search, setSearch] = useState("");
useEffect(() => {
// Build a query: only this user's ideas, newest first
const q = query(
collection(db, "ideas"),
where("userId", "==", userId),
orderBy("createdAt", "desc")
);
// Subscribe to real-time updates from Firestore
const unsubscribe = onSnapshot(q, (snapshot) => {
const ideasData = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setIdeas(ideasData);
});
// Cleanup listener when component unmounts
return () => unsubscribe();
}, [userId]);
// Filter ideas case-insensitively based on search text
const filteredIdeas = ideas.filter((idea) =>
idea.text.toLowerCase().includes(search.toLowerCase())
);
// Convert a Firestore Timestamp into a human-readable string
const formatDate = (timestamp) => {
if (!timestamp) return "Just now";
const date = timestamp.toDate();
return date.toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
};
return (
<div className="idea-list">
<div className="vault-header">
<h2>Your Vault</h2>
<span className="idea-count">{ideas.length} ideas</span>
</div>
<div className="search-container">
<input
type="text"
placeholder="Search your ideas..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="search-input"
/>
{search && (
<button onClick={() => setSearch("")} className="clear-search-btn">
Clear
</button>
)}
</div>
{ideas.length === 0 && (
<p className="empty-state">
No ideas yet. Capture your first idea above!
</p>
)}
{ideas.length > 0 && filteredIdeas.length === 0 && (
<p className="empty-state">No ideas found matching "{search}"</p>
)}
<div className="ideas-grid">
{filteredIdeas.map((idea) => (
<div key={idea.id} className="idea-card">
<p className="idea-text">{idea.text}</p>
<span className="idea-timestamp">
{formatDate(idea.createdAt)}
</span>
</div>
))}
</div>
</div>
);
}
export default IdeaList;
What does this code do?
The query function builds a Firestore query that filters ideas to only those matching the current user's userId and sorts them by createdAt descending (newest first).
The onSnapshot function creates a real-time listener. Every time the ideas collection changes, the callback fires and updates your component state automatically. No page refresh needed.
The filteredIdeas variable filters the full ideas array case-insensitively based on whatever the user types in the search input. The formatDate function converts a Firestore Timestamp into a readable string like "Jun 30, 2026, 3:45 PM".
- Save src/components/IdeaList.jsx.
- Switch to your browser. You should see the "Your Vault" heading with "0 ideas" and the empty state message "No ideas yet. Capture your first idea above!" below the idea form.
Seeing a blank page or console error about an index?
Firestore requires a composite index for queries that combine where and orderBy on different fields. Open your browser's developer console. If you see an error containing a link that starts with https://console.firebase.google.com, click it. It opens the Firebase Console with the index pre-configured. Click Create and wait a minute or two for the index to build.
If you see a different error, double-check that your import paths match the file structure (../firebase with two dots).
Why is my vault not loading any ideas?
Style the vault for mobile
The vault structure is rendering, but it needs card-based styling and mobile-friendly spacing to feel like a real app.
- Open src/App.css and add the following styles at the bottom of the file, after the existing .saved-message rule:
/* Vault */
.vault-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.vault-header h2 {
font-size: 1.25rem;
}
.idea-count {
color: #888;
font-size: 0.85rem;
}
/* Search */
.search-container {
position: relative;
margin-bottom: 1rem;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #333;
border-radius: 8px;
background: #1a1a1a;
color: #f5f5f5;
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: #4f46e5;
}
.clear-search-btn {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #888;
cursor: pointer;
font-size: 0.85rem;
}
/* Ideas Grid */
.empty-state {
text-align: center;
color: #888;
padding: 2rem;
}
.ideas-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.idea-card {
padding: 1rem;
border: 1px solid #222;
border-radius: 8px;
background: #1a1a1a;
}
.idea-text {
margin-bottom: 0.5rem;
line-height: 1.5;
white-space: pre-wrap;
}
.idea-timestamp {
font-size: 0.8rem;
color: #666;
}
What do these styles do?
The vault header uses flexbox to push the title and idea count to opposite sides. The search input gets the same dark styling as the rest of the app, with a purple focus ring matching the brand color.
The .clear-search-btn is absolutely positioned inside the search container so it sits on the right edge of the input. Each .idea-card has a subtle border, rounded corners, and a dark background to create visual separation between ideas.
The white-space: pre-wrap on .idea-text preserves line breaks that the user typed into the capture form, so multi-line ideas display correctly.
- Save src/App.css and refresh your browser.
- Try capturing an idea using the form above the vault. You should see a dark card appear instantly in the vault with your idea text and a timestamp like "Jun 30, 2026, 3:45 PM".
- Type a word from your idea into the search bar. The list should filter instantly. Click Clear to reset.
Ideas not appearing as styled cards?
Make sure your new CSS is placed after the .saved-message block, not inside an existing rule. An extra closing brace can silently break all styles that follow it.
If the search works but timestamps show "Just now" for every idea, the serverTimestamp() value has not resolved yet. Refresh the page once and the server-set time will appear.
Help me fix my vault styling or timestamp display.
✔️ Awesome, I've got everything!
Great. Double check you have saved both src/components/IdeaList.jsx and src/App.css.
ⓧ I'd like to double check the full code
Here is the complete src/components/IdeaList.jsx:
import { useState, useEffect } from "react";
import {
collection,
query,
where,
orderBy,
onSnapshot,
} from "firebase/firestore";
import { db } from "../firebase";
function IdeaList({ userId }) {
const [ideas, setIdeas] = useState([]);
const [search, setSearch] = useState("");
useEffect(() => {
const q = query(
collection(db, "ideas"),
where("userId", "==", userId),
orderBy("createdAt", "desc")
);
const unsubscribe = onSnapshot(q, (snapshot) => {
const ideasData = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data(),
}));
setIdeas(ideasData);
});
return () => unsubscribe();
}, [userId]);
const filteredIdeas = ideas.filter((idea) =>
idea.text.toLowerCase().includes(search.toLowerCase())
);
const formatDate = (timestamp) => {
if (!timestamp) return "Just now";
const date = timestamp.toDate();
return date.toLocaleString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
};
return (
<div className="idea-list">
<div className="vault-header">
<h2>Your Vault</h2>
<span className="idea-count">{ideas.length} ideas</span>
</div>
<div className="search-container">
<input
type="text"
placeholder="Search your ideas..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="search-input"
/>
{search && (
<button onClick={() => setSearch("")} className="clear-search-btn">
Clear
</button>
)}
</div>
{ideas.length === 0 && (
<p className="empty-state">
No ideas yet. Capture your first idea above!
</p>
)}
{ideas.length > 0 && filteredIdeas.length === 0 && (
<p className="empty-state">No ideas found matching "{search}"</p>
)}
<div className="ideas-grid">
{filteredIdeas.map((idea) => (
<div key={idea.id} className="idea-card">
<p className="idea-text">{idea.text}</p>
<span className="idea-timestamp">
{formatDate(idea.createdAt)}
</span>
</div>
))}
</div>
</div>
);
}
export default IdeaList;
Here is the complete src/App.css:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #0f0f0f;
color: #f5f5f5;
min-height: 100vh;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
font-size: 1.2rem;
color: #888;
}
/* Auth Styles */
.auth-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1.5rem;
}
.auth-container h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: #fff;
}
.auth-tagline {
color: #888;
margin-bottom: 2rem;
text-align: center;
}
.auth-form {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 1rem;
}
.auth-form input {
padding: 1rem;
border: 1px solid #333;
border-radius: 8px;
background: #1a1a1a;
color: #f5f5f5;
font-size: 1rem;
}
.auth-btn {
padding: 1rem;
border: none;
border-radius: 8px;
background: #4f46e5;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
.auth-btn:hover {
background: #4338ca;
}
.toggle-auth-btn {
margin-top: 1rem;
background: none;
border: none;
color: #4f46e5;
cursor: pointer;
font-size: 0.9rem;
}
.error-message {
color: #ef4444;
font-size: 0.9rem;
}
/* App Layout */
.app {
max-width: 600px;
margin: 0 auto;
padding: 1rem;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 0;
margin-bottom: 1rem;
}
.app-header h1 {
font-size: 1.5rem;
}
.sign-out-btn {
padding: 0.5rem 1rem;
border: 1px solid #333;
border-radius: 6px;
background: none;
color: #888;
cursor: pointer;
font-size: 0.85rem;
}
.sign-out-btn:hover {
border-color: #ef4444;
color: #ef4444;
}
/* Idea Form */
.idea-form {
margin-bottom: 2rem;
}
.idea-form textarea {
width: 100%;
padding: 1rem;
border: 1px solid #333;
border-radius: 8px;
background: #1a1a1a;
color: #f5f5f5;
font-size: 1rem;
resize: vertical;
min-height: 80px;
font-family: inherit;
}
.idea-form textarea:focus {
outline: none;
border-color: #4f46e5;
}
.capture-btn {
width: 100%;
margin-top: 0.75rem;
padding: 1rem;
border: none;
border-radius: 8px;
background: #4f46e5;
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
}
.capture-btn:hover {
background: #4338ca;
}
.capture-btn:disabled {
background: #333;
cursor: not-allowed;
}
.saved-message {
text-align: center;
color: #22c55e;
margin-top: 0.5rem;
font-size: 0.9rem;
}
/* Vault */
.vault-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.vault-header h2 {
font-size: 1.25rem;
}
.idea-count {
color: #888;
font-size: 0.85rem;
}
/* Search */
.search-container {
position: relative;
margin-bottom: 1rem;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid #333;
border-radius: 8px;
background: #1a1a1a;
color: #f5f5f5;
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: #4f46e5;
}
.clear-search-btn {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #888;
cursor: pointer;
font-size: 0.85rem;
}
/* Ideas Grid */
.empty-state {
text-align: center;
color: #888;
padding: 2rem;
}
.ideas-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.idea-card {
padding: 1rem;
border: 1px solid #222;
border-radius: 8px;
background: #1a1a1a;
}
.idea-text {
margin-bottom: 0.5rem;
line-height: 1.5;
white-space: pre-wrap;
}
.idea-timestamp {
font-size: 0.8rem;
color: #666;
}
Your vault is fully functional. You can capture ideas, see them appear instantly, search through them, and each one carries a server-verified timestamp. Next up, you will add search polish and empty state handling to make the experience feel complete.
Secret mission
Add View-Only Sharing Links
Your vault is private, but what if you want to show someone a single idea without exposing everything else? In this mission, you'll add a Share button that generates a public link to one idea while keeping the rest of your vault locked down.
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 Firebase's free Spark plan, so there are no ongoing costs.
Resources you used:
- Firebase project (Originly) with Authentication and Firestore database.
- Firestore ideas collection containing your saved ideas.
- Local originly project folder on your Desktop.
Keep everything running
No action needed. Choose this if you're still actively building on Originly or want to keep capturing ideas.
- Your Firebase project stays active on the free Spark plan with no charges.
- Your local originly folder remains on your Desktop.
- The Vite dev server only runs when you start it manually, so it consumes no resources when stopped.
Pause - I'll come back to this later
Stop the dev server to free up your terminal. Your Firebase project and local files stay intact for when you return.
- In your terminal, press Ctrl+C to stop the Vite dev server if it is still running.
- Your Firebase project remains active on the Spark plan at no cost.
- Your local files stay on your Desktop. Run npm run dev from the originly folder to resume development.
Delete - I don't want to use this again
Remove all project resources and start fresh. This deletes your Firebase project, all stored ideas, and your local files permanently.
- In your terminal, press Ctrl+C to stop the Vite dev server if it is still running.
- Go to the Firebase Console at console.firebase.google.com.
- Select your Originly project.
- Click the gear icon, then click Project settings.
- Scroll to the bottom of the page and click Delete project.
- Check each confirmation box and click Delete project to confirm.
Delete your local project files by running these commands:
macOS
cd ~/Desktop
rm -rf originly
Windows
cd ~\Desktop
rmdir /s /q originly
Nice Work!
Nice Work!
Nice work! You just built Originly, a full-stack idea capture app with authentication, real-time database storage, and a mobile-first interface using React and Firebase.
You've learned how to:
- Build a React + Vite web application with Firebase Authentication for email/password sign-up and sign-in.
- Create a Firestore-powered idea vault with tamper-proof server timestamps and real-time updates using onSnapshot.
- Implement client-side search filtering and a mobile-first UI for fast idea retrieval.
- Secret Mission: Added view-only sharing with conditional Firestore security rules so creators can share a single idea without exposing their entire vault.
Ready to quiz yourself?