Here before we start this, watch how we actually built the app from scratch in Build with Sagar Kakkala series
In the last session, we have integrated Firebase authentication for our application but we had discussed limitation of exposed credentials
while we are aware of issue, since our app still on basic level, we will continue with firebase as of now and we can later take necessary actions or plans thinking about migration and that can add up our learning as we will learn on Database migration
In this session, we will learn about adding post login pages,
here our idea of app is to be used by Gym owner and Gym user both
For those who created account as Gym owner, they must be routed to Different Dashboard where they can register their Gym
And for Gym users, our application must display nearby Gyms available in area that our user is searching for
So since, this is completely based on data as user registering as Gym owner must fill details and Gym user must able to get that details
Let us first actually create a collection in our database which makes our things easier
Go to Firebase console > Firestore Database
if you have not setup Firestore Database, watch session2 on knowing how to set it up
click on start collection
and Add gyms
once Done, Now go for rules and update our firewall rules in following way
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
match /gyms/{gymId} {
allow create: if request.auth != null
&& get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == "owner";
allow read: if true;
allow update, delete: if request.auth != null
&& resource.data.ownerId == request.auth.uid;
}
}
}
And the above rules are where only Gym owners can create or update data in gyms collection
we will understand this better after we update our code
Also take firebase config and fill it in firebase.js file in code, which we have discussed in Day2 of these sessions
Now take code from Gym-per-Day-Day2
let us take it step by step, let us add owner dashboard
import { useState } from "react";
import { Dialog } from "@headlessui/react";
import { Dumbbell, Plus } from "lucide-react";
import { addDoc, collection } from "firebase/firestore";
import { auth, db } from "../firebase";
import { useNavigate } from "react-router-dom";
export default function OwnerDashboard() {
const [open, setOpen] = useState(false);
const navigate = useNavigate();
const [form, setForm] = useState({
gymName: "",
city: "",
gmapLocation: "",
openHours: "",
perDayCost: "",
holidays: "",
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSubmit = async () => {
try {
const user = auth.currentUser;
if (!user) return;
await addDoc(collection(db, "gyms"), {
...form,
ownerId: user.uid,
createdAt: new Date(),
});
setOpen(false);
navigate("/my-gyms");
} catch (error) {
console.error("Error saving gym:", error);
alert("Failed to save gym");
}
};
return (
<div className="min-h-screen bg-slate-50 p-8">
<div className="max-w-6xl mx-auto">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-2">
<Dumbbell className="w-7 h-7 text-emerald-600" />
<h1 className="text-2xl font-bold">Owner Dashboard</h1>
</div>
<button
onClick={() => setOpen(true)}
className="flex items-center gap-2 px-5 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700"
>
<Plus className="w-4 h-4" />
Add Gym
</button>
</div>
<div className="bg-white rounded-xl shadow p-10 text-center text-slate-600">
No gyms registered yet. Click <b>Add Gym</b> to get started.
</div>
</div>
<Dialog open={open} onClose={() => setOpen(false)} className="relative z-50">
<div className="fixed inset-0 bg-black/30" />
<div className="fixed inset-0 flex items-center justify-center">
<Dialog.Panel className="bg-white rounded-2xl w-full max-w-lg p-6 space-y-4">
<Dialog.Title className="text-xl font-bold">
Register Your Gym
</Dialog.Title>
{[
{ name: "gymName", label: "Gym Name" },
{ name: "city", label: "City" },
{ name: "gmapLocation", label: "Google Maps Location URL" },
{ name: "openHours", label: "Open Hours (eg: 6AM - 10PM)" },
{ name: "perDayCost", label: "Per Day Cost (₹ / $)" },
{ name: "holidays", label: "Holidays (eg: Sunday)" },
].map((field) => (
<input
key={field.name}
name={field.name}
placeholder={field.label}
value={(form as any)[field.name]}
onChange={handleChange}
className="w-full border px-4 py-3 rounded-lg focus:ring-2 focus:ring-emerald-500"
/>
))}
<div className="flex justify-end gap-3 pt-4">
<button
onClick={() => setOpen(false)}
className="px-4 py-2 rounded-lg border"
>
Cancel
</button>
<button
onClick={handleSubmit}
className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700"
>
Save Gym
</button>
</div>
</Dialog.Panel>
</div>
</Dialog>
</div>
);
}
and also since we need new react package now install headlessui/react
npm install @headlessui/react
We will understand that our owner dashboard displays buttons to Add Gym and pops up a form to add details which we will understand when we will implement code
and we will create another page that displays all gyms our gym owner has regiatered
import { useEffect, useState } from "react";
import { collection, query, where, getDocs } from "firebase/firestore";
import { db, auth } from "../firebase";
import { Dumbbell, MapPin } from "lucide-react";
type Gym = {
id: string;
gymName: string;
city: string;
gmapLocation?: string;
openHours: string;
perDayCost: string;
holidays: string;
};
export default function MyGyms() {
const [gyms, setGyms] = useState<Gym[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchGyms = async () => {
const user = auth.currentUser;
if (!user) return;
const q = query(
collection(db, "gyms"),
where("ownerId", "==", user.uid)
);
const snapshot = await getDocs(q);
const gymsList: Gym[] = snapshot.docs.map((doc) => ({
id: doc.id,
...(doc.data() as Omit<Gym, "id">),
}));
setGyms(gymsList);
setLoading(false);
};
fetchGyms();
}, []);
if (loading)
return <div className="text-center mt-20">Loading your gyms...</div>;
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-8">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="flex items-center gap-3 mb-8">
<Dumbbell className="w-7 h-7 text-emerald-600" />
<h1 className="text-3xl font-bold text-slate-900">
My Registered Gyms
</h1>
</div>
{gyms.length === 0 ? (
<div className="bg-white rounded-xl shadow p-10 text-center text-slate-600">
You haven’t registered any gyms yet.
</div>
) : (
<div className="grid md:grid-cols-2 gap-6">
{gyms.map((gym) => (
<div
key={gym.id}
className="bg-white rounded-2xl shadow-sm hover:shadow-md transition p-6"
>
<h2 className="text-xl font-semibold text-emerald-600 mb-1">
{gym.gymName}
</h2>
<p className="text-slate-700 mb-1">
<span className="font-medium">City:</span> {gym.city}
</p>
<p className="text-slate-700 mb-1">
<span className="font-medium">Open Hours:</span>{" "}
{gym.openHours}
</p>
<p className="text-slate-700 mb-1">
<span className="font-medium">Per Day Cost:</span>{" "}
{gym.perDayCost}
</p>
<p className="text-slate-700 mb-3">
<span className="font-medium">Holidays:</span>{" "}
{gym.holidays}
</p>
{gym.gmapLocation && (
<a
href={gym.gmapLocation}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-emerald-600 font-medium hover:underline"
>
<MapPin className="w-4 h-4" />
View on Google Maps
</a>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}
And now we need user to have different dashboard which actually navigates to UserDashboard to display gyms nearby
import { useState } from "react";
import { Dialog } from "@headlessui/react";
import { Dumbbell, Search, MapPin } from "lucide-react";
import {
collection,
getDocs,
query,
where,
} from "firebase/firestore";
import { db } from "../firebase";
type Gym = {
id: string;
gymName: string;
city: string;
gmapLocation: string;
openHours: string;
perDayCost: string;
holidays: string;
};
export default function UserDashboard() {
const [open, setOpen] = useState(false);
const [city, setCity] = useState("");
const [gyms, setGyms] = useState<Gym[]>([]);
const [loading, setLoading] = useState(false);
const handleSearch = async () => {
if (!city.trim()) {
alert("Please enter a city");
return;
}
try {
setLoading(true);
setOpen(false);
const q = query(
collection(db, "gyms"),
where("city", "==", city)
);
const snapshot = await getDocs(q);
const results: Gym[] = snapshot.docs.map((doc) => ({
id: doc.id,
...(doc.data() as Omit<Gym, "id">),
}));
setGyms(results);
} catch (error) {
console.error("Error fetching gyms:", error);
alert("Failed to fetch gyms");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-8">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="text-center mb-10">
<div className="flex justify-center items-center gap-2 mb-4">
<Dumbbell className="w-8 h-8 text-emerald-600" />
<h1 className="text-3xl font-bold">
Welcome to Gym per Day
</h1>
</div>
<p className="text-slate-600 text-lg">
Train anywhere, anytime — no subscriptions, no commitments.
</p>
<button
onClick={() => setOpen(true)}
className="mt-6 inline-flex items-center gap-2 px-8 py-4 bg-emerald-600 text-white rounded-xl font-semibold hover:bg-emerald-700 shadow-lg"
>
<Search className="w-5 h-5" />
Book Your Gym for a Day
</button>
</div>
{/* Results */}
{loading && (
<p className="text-center text-slate-600">
Searching gyms...
</p>
)}
{!loading && gyms.length > 0 && (
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{gyms.map((gym) => (
<div
key={gym.id}
className="bg-white rounded-xl shadow p-6 space-y-3"
>
<h3 className="text-xl font-bold text-slate-900">
{gym.gymName}
</h3>
<p className="text-slate-600">
📍 {gym.city}
</p>
<p className="text-slate-600">
🕒 {gym.openHours}
</p>
<p className="text-slate-600">
💰 {gym.perDayCost} / day
</p>
<p className="text-slate-500 text-sm">
Holidays: {gym.holidays}
</p>
<div className="flex items-center justify-between pt-3">
<a
href={gym.gmapLocation}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-emerald-600 hover:underline"
>
<MapPin className="w-4 h-4" />
View on Map
</a>
<button
className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700"
>
Book Now
</button>
</div>
</div>
))}
</div>
)}
{!loading && gyms.length === 0 && (
<p className="text-center text-slate-600 mt-10">
No gyms found for this city.
</p>
)}
</div>
{/* Dialog */}
<Dialog open={open} onClose={() => setOpen(false)} className="relative z-50">
<div className="fixed inset-0 bg-black/30" />
<div className="fixed inset-0 flex items-center justify-center px-4">
<Dialog.Panel className="bg-white rounded-2xl w-full max-w-md p-6 space-y-5">
<Dialog.Title className="text-xl font-bold">
Find Gyms Near You
</Dialog.Title>
<div>
<label className="block text-sm font-medium mb-2">
Which city are you looking to work out in today?
</label>
<input
value={city}
onChange={(e) => setCity(e.target.value)}
placeholder="Eg: Bangalore, Mumbai"
className="w-full border px-4 py-3 rounded-lg focus:ring-2 focus:ring-emerald-500"
/>
<p className="text-xs text-slate-500 mt-2">
We’ll show gyms available for daily access.
</p>
</div>
<div className="flex justify-end gap-3 pt-4">
<button
onClick={() => setOpen(false)}
className="px-4 py-2 border rounded-lg"
>
Cancel
</button>
<button
onClick={handleSearch}
className="px-5 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700"
>
Find Gyms
</button>
</div>
</Dialog.Panel>
</div>
</Dialog>
</div>
);
}
And now let us update App.tsx file
import { BrowserRouter, Routes, Route } from "react-router-dom";
import LandingPage from "./components/LandingPage";
import Login from "./components/Login";
import Signup from "./components/Signup";
import ForgotPassword from "./components/ForgotPassword";
import OwnerDashboard from "./components/OwnerDashboard";
import MyGyms from "./components/MyGyms";
import UserDashboard from "./components/UserDashboard";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />} />
<Route path="/landingpage" element={<LandingPage />} />
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/owner" element={<OwnerDashboard />} />
<Route path="/my-gyms" element={<MyGyms />} />
<Route path="/user" element={<UserDashboard />} />
</Routes>
</BrowserRouter>
);
}
export default App;
Code will be explained in video
And now since user dashboard and owner dashboard is ready, we need to navigate our users to user dashboard and owners to owner dashboard post login
update handle login in Login.tsx file
import { useState } from "react";
import { useNavigate, Link } from "react-router-dom";
import { signInWithEmailAndPassword } from "firebase/auth";
import { auth } from "../firebase";
import { Dumbbell, AlertCircle } from "lucide-react";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
try {
// 🔐 Firebase Auth login
const userCredential = await signInWithEmailAndPassword(auth, email, password);
const uid = userCredential.user.uid;
// 🔍 Fetch user role from Firestore
const userSnap = await getDoc(doc(db, "users", uid));
if (!userSnap.exists()) {
throw new Error("User data not found");
}
const role = userSnap.data().role;
// 🚦 Role-based navigation
if (role === "owner") {
navigate("/owner");
} else {
navigate("/user");
}
} catch (err: any) {
setError(err.message || "Invalid email or password");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 flex items-center justify-center px-4">
<div className="w-full max-w-md">
<div className="bg-white rounded-2xl shadow-lg p-8">
<div className="flex items-center justify-center gap-3 mb-8">
<Dumbbell className="w-8 h-8 text-emerald-600" />
<h1 className="text-2xl font-bold text-slate-900">Gym per Day</h1>
</div>
<h2 className="text-3xl font-bold text-slate-900 mb-2">
Welcome Back
</h2>
<p className="text-slate-600 mb-8">
Log in to continue your fitness journey
</p>
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
<p className="text-red-700 text-sm">{error}</p>
</div>
)}
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Email Address
</label>
<input
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Password
</label>
<input
type="password"
placeholder="Enter your password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500"
required
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-3 bg-emerald-600 text-white font-semibold rounded-lg hover:bg-emerald-700 disabled:opacity-50"
>
{loading ? "Logging in..." : "Log In"}
</button>
</form>
<div className="mt-6 text-center">
<p className="text-slate-600">
New here?{" "}
<Link
to="/signup"
className="text-emerald-600 font-semibold hover:text-emerald-700"
>
Sign Up
</Link>
</p>
</div>
<div className="mt-4 text-center">
<Link
to="/forgot-password"
className="text-sm text-slate-600 hover:text-slate-900"
>
Forgot Password?
</Link>
</div>
</div>
</div>
</div>
);
}
npm run dev
Now let us create a two accounts one as Gym owner and other as Gym user
Now let us login as Gym owner
Now click on Add gym
Fill details and click save
And now we can actually see list of Gyms owner registered
And now let us login as user by creating an account
And now, if you let us login as Gym user
click on Book Your Gym for today
and now this fetches all data available in our DB that is under city Porto
And now we have successfully created owner dashboard and user dashboard
Comments
Post a Comment