From 196a04802231626d92478212f87be7a9a8347a9d Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 18 Feb 2026 12:32:18 +0100
Subject: [PATCH 01/67] set up the basic folder structure frontend and backend
---
backend/.env.example | 0
backend/db/connectDB.js | 0
backend/middleware/auth.js | 0
backend/middleware/errorHandler.js | 0
backend/models/Place.js | 0
backend/models/Review.js | 0
backend/models/User.js | 0
backend/routes/auth.js | 0
backend/routes/index.js | 0
backend/routes/places.js | 0
backend/routes/reviews.js | 0
backend/seed/seed.js | 0
backend/src/app.js | 0
backend/{ => src}/server.js | 0
backend/utils/recalcRating.js | 0
backend/utils/validators.js | 0
frontend/.env.example | 0
frontend/components/Filters.jsx | 0
frontend/components/MapView.jsx | 0
frontend/components/Navbar.jsx | 0
frontend/components/PlaceCard.jsx | 0
frontend/hooks/useAuth.jsx | 0
frontend/pages/AddPlace.jsx | 0
frontend/pages/Home.jsx | 0
frontend/pages/Login.jsx | 0
frontend/pages/PlaceDetails.jsx | 1 +
frontend/pages/Signup.jsx | 0
frontend/src/api/auth.js | 0
frontend/src/api/client.js | 0
frontend/src/api/places.js | 0
frontend/src/api/reviews.js | 0
frontend/styles/global.css | 0
32 files changed, 1 insertion(+)
create mode 100644 backend/.env.example
create mode 100644 backend/db/connectDB.js
create mode 100644 backend/middleware/auth.js
create mode 100644 backend/middleware/errorHandler.js
create mode 100644 backend/models/Place.js
create mode 100644 backend/models/Review.js
create mode 100644 backend/models/User.js
create mode 100644 backend/routes/auth.js
create mode 100644 backend/routes/index.js
create mode 100644 backend/routes/places.js
create mode 100644 backend/routes/reviews.js
create mode 100644 backend/seed/seed.js
create mode 100644 backend/src/app.js
rename backend/{ => src}/server.js (100%)
create mode 100644 backend/utils/recalcRating.js
create mode 100644 backend/utils/validators.js
create mode 100644 frontend/.env.example
create mode 100644 frontend/components/Filters.jsx
create mode 100644 frontend/components/MapView.jsx
create mode 100644 frontend/components/Navbar.jsx
create mode 100644 frontend/components/PlaceCard.jsx
create mode 100644 frontend/hooks/useAuth.jsx
create mode 100644 frontend/pages/AddPlace.jsx
create mode 100644 frontend/pages/Home.jsx
create mode 100644 frontend/pages/Login.jsx
create mode 100644 frontend/pages/PlaceDetails.jsx
create mode 100644 frontend/pages/Signup.jsx
create mode 100644 frontend/src/api/auth.js
create mode 100644 frontend/src/api/client.js
create mode 100644 frontend/src/api/places.js
create mode 100644 frontend/src/api/reviews.js
create mode 100644 frontend/styles/global.css
diff --git a/backend/.env.example b/backend/.env.example
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/db/connectDB.js b/backend/db/connectDB.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/middleware/errorHandler.js b/backend/middleware/errorHandler.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/models/Place.js b/backend/models/Place.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/models/Review.js b/backend/models/Review.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/models/User.js b/backend/models/User.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/routes/auth.js b/backend/routes/auth.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/routes/index.js b/backend/routes/index.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/routes/places.js b/backend/routes/places.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/routes/reviews.js b/backend/routes/reviews.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/seed/seed.js b/backend/seed/seed.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/src/app.js b/backend/src/app.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/server.js b/backend/src/server.js
similarity index 100%
rename from backend/server.js
rename to backend/src/server.js
diff --git a/backend/utils/recalcRating.js b/backend/utils/recalcRating.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/utils/validators.js b/backend/utils/validators.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/.env.example b/frontend/.env.example
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/components/Filters.jsx b/frontend/components/Filters.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/components/MapView.jsx b/frontend/components/MapView.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/components/Navbar.jsx b/frontend/components/Navbar.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/components/PlaceCard.jsx b/frontend/components/PlaceCard.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/hooks/useAuth.jsx b/frontend/hooks/useAuth.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/pages/AddPlace.jsx b/frontend/pages/AddPlace.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/pages/Home.jsx b/frontend/pages/Home.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/pages/Login.jsx b/frontend/pages/Login.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/pages/PlaceDetails.jsx b/frontend/pages/PlaceDetails.jsx
new file mode 100644
index 000000000..c1b0730e0
--- /dev/null
+++ b/frontend/pages/PlaceDetails.jsx
@@ -0,0 +1 @@
+x
\ No newline at end of file
diff --git a/frontend/pages/Signup.jsx b/frontend/pages/Signup.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/api/places.js b/frontend/src/api/places.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/src/api/reviews.js b/frontend/src/api/reviews.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/frontend/styles/global.css b/frontend/styles/global.css
new file mode 100644
index 000000000..e69de29bb
From 735f720640266d4d7d0f1b539474143a0e5b4962 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 18 Feb 2026 12:43:00 +0100
Subject: [PATCH 02/67] updated the folder structure
---
backend/package.json | 4 ++--
backend/routes/places.js | 0
backend/src/app.js | 2 ++
backend/{ => src}/db/connectDB.js | 0
backend/{ => src}/middleware/auth.js | 0
backend/{ => src}/middleware/errorHandler.js | 0
backend/{ => src}/models/Place.js | 0
backend/{ => src}/models/Review.js | 0
backend/{ => src}/models/User.js | 0
backend/{ => src}/routes/auth.js | 0
backend/{ => src}/routes/index.js | 0
backend/src/routes/places.js | 1 +
backend/{ => src}/routes/reviews.js | 0
backend/{ => src}/seed/seed.js | 0
14 files changed, 5 insertions(+), 2 deletions(-)
delete mode 100644 backend/routes/places.js
rename backend/{ => src}/db/connectDB.js (100%)
rename backend/{ => src}/middleware/auth.js (100%)
rename backend/{ => src}/middleware/errorHandler.js (100%)
rename backend/{ => src}/models/Place.js (100%)
rename backend/{ => src}/models/Review.js (100%)
rename backend/{ => src}/models/User.js (100%)
rename backend/{ => src}/routes/auth.js (100%)
rename backend/{ => src}/routes/index.js (100%)
create mode 100644 backend/src/routes/places.js
rename backend/{ => src}/routes/reviews.js (100%)
rename backend/{ => src}/seed/seed.js (100%)
diff --git a/backend/package.json b/backend/package.json
index 08f29f244..370d27cda 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -3,8 +3,8 @@
"version": "1.0.0",
"description": "Server part of final project",
"scripts": {
- "start": "babel-node server.js",
- "dev": "nodemon server.js --exec babel-node"
+ "start": "babel-node src/server.js",
+ "dev": "nodemon src/server.js --exec babel-node"
},
"author": "",
"license": "ISC",
diff --git a/backend/routes/places.js b/backend/routes/places.js
deleted file mode 100644
index e69de29bb..000000000
diff --git a/backend/src/app.js b/backend/src/app.js
index e69de29bb..3b96cc947 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -0,0 +1,2 @@
+import { connectDB } from "./db/connectDB.js";
+import routes from "./routes/index.js";
diff --git a/backend/db/connectDB.js b/backend/src/db/connectDB.js
similarity index 100%
rename from backend/db/connectDB.js
rename to backend/src/db/connectDB.js
diff --git a/backend/middleware/auth.js b/backend/src/middleware/auth.js
similarity index 100%
rename from backend/middleware/auth.js
rename to backend/src/middleware/auth.js
diff --git a/backend/middleware/errorHandler.js b/backend/src/middleware/errorHandler.js
similarity index 100%
rename from backend/middleware/errorHandler.js
rename to backend/src/middleware/errorHandler.js
diff --git a/backend/models/Place.js b/backend/src/models/Place.js
similarity index 100%
rename from backend/models/Place.js
rename to backend/src/models/Place.js
diff --git a/backend/models/Review.js b/backend/src/models/Review.js
similarity index 100%
rename from backend/models/Review.js
rename to backend/src/models/Review.js
diff --git a/backend/models/User.js b/backend/src/models/User.js
similarity index 100%
rename from backend/models/User.js
rename to backend/src/models/User.js
diff --git a/backend/routes/auth.js b/backend/src/routes/auth.js
similarity index 100%
rename from backend/routes/auth.js
rename to backend/src/routes/auth.js
diff --git a/backend/routes/index.js b/backend/src/routes/index.js
similarity index 100%
rename from backend/routes/index.js
rename to backend/src/routes/index.js
diff --git a/backend/src/routes/places.js b/backend/src/routes/places.js
new file mode 100644
index 000000000..37343cd8d
--- /dev/null
+++ b/backend/src/routes/places.js
@@ -0,0 +1 @@
+import Place from "../models/Place.js";
diff --git a/backend/routes/reviews.js b/backend/src/routes/reviews.js
similarity index 100%
rename from backend/routes/reviews.js
rename to backend/src/routes/reviews.js
diff --git a/backend/seed/seed.js b/backend/src/seed/seed.js
similarity index 100%
rename from backend/seed/seed.js
rename to backend/src/seed/seed.js
From b7585fa077d9c33eca9c1c68f25fcaf22e691858 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Mon, 23 Feb 2026 14:09:16 +0100
Subject: [PATCH 03/67] updated errorhandling
---
backend/src/app.js | 28 ++++++++++++++++++++++++++
backend/src/db/connectDB.js | 13 ++++++++++++
backend/src/middleware/errorHandler.js | 9 +++++++++
package.json | 14 ++++++++++++-
4 files changed, 63 insertions(+), 1 deletion(-)
diff --git a/backend/src/app.js b/backend/src/app.js
index 3b96cc947..66368f7cd 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -1,2 +1,30 @@
+import "dotenv/config";
+import express from "express";
+import cors from "cors";
+import listEndpoints from "express-list-endpoints";
+
import { connectDB } from "./db/connectDB.js";
import routes from "./routes/index.js";
+import { errorHandler } from "./middleware/errorHandler.js";
+
+const app = express();
+
+app.use(cors());
+app.use(express.json());
+
+connectDB();
+
+// API docs
+app.get("/", (req, res) => {
+ res.json({
+ message: "Baby Changing Places API",
+ endpoints: listEndpoints(app),
+ });
+});
+
+app.use(routes);
+
+// Error handler LAST
+app.use(errorHandler);
+
+export default app;
diff --git a/backend/src/db/connectDB.js b/backend/src/db/connectDB.js
index e69de29bb..3ce2571ca 100644
--- a/backend/src/db/connectDB.js
+++ b/backend/src/db/connectDB.js
@@ -0,0 +1,13 @@
+import mongoose from "mongoose";
+
+export const connectDB = async () => {
+ const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/babyplaces";
+
+ try {
+ await mongoose.connect(mongoUrl);
+ console.log("✅ Connected to MongoDB");
+ } catch (err) {
+ console.error("❌ MongoDB connection error:", err.message);
+ process.exit(1);
+ }
+};
diff --git a/backend/src/middleware/errorHandler.js b/backend/src/middleware/errorHandler.js
index e69de29bb..c517ceb93 100644
--- a/backend/src/middleware/errorHandler.js
+++ b/backend/src/middleware/errorHandler.js
@@ -0,0 +1,9 @@
+export const errorHandler = (err, req, res, next) => {
+ console.error(err);
+
+ const status = err.status || 500;
+ res.status(status).json({
+ success: false,
+ message: err.message || "Something went wrong",
+ });
+};
diff --git a/package.json b/package.json
index 680d19077..b6e0010d0 100644
--- a/package.json
+++ b/package.json
@@ -3,5 +3,17 @@
"version": "1.0.0",
"scripts": {
"postinstall": "npm install --prefix backend"
+ },
+ "dependencies": {
+ "bcrypt": "^6.0.0",
+ "cors": "^2.8.6",
+ "dotenv": "^17.3.1",
+ "express": "^5.2.1",
+ "express-list-endpoints": "^7.1.1",
+ "jsonwebtoken": "^9.0.3",
+ "mongoose": "^9.2.1"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.11"
}
-}
\ No newline at end of file
+}
From d78320fa8e548049ab446b3ff64153eb5bceb6ea Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Tue, 24 Feb 2026 10:29:25 +0100
Subject: [PATCH 04/67] Implement user authentication and review functionality
with JWT
---
backend/src/app.js | 2 +-
backend/src/middleware/auth.js | 18 +++++++++
backend/src/models/Place.js | 38 +++++++++++++++++++
backend/src/models/User.js | 12 ++++++
backend/src/routes/auth.js | 69 ++++++++++++++++++++++++++++++++++
backend/src/routes/index.js | 12 ++++++
backend/src/routes/places.js | 59 ++++++++++++++++++++++++++++-
backend/src/routes/reviews.js | 45 ++++++++++++++++++++++
backend/utils/recalcRating.js | 23 ++++++++++++
9 files changed, 276 insertions(+), 2 deletions(-)
diff --git a/backend/src/app.js b/backend/src/app.js
index 66368f7cd..038be19e3 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -24,7 +24,7 @@ app.get("/", (req, res) => {
app.use(routes);
-// Error handler LAST
+// Error handler
app.use(errorHandler);
export default app;
diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js
index e69de29bb..f0692b6a6 100644
--- a/backend/src/middleware/auth.js
+++ b/backend/src/middleware/auth.js
@@ -0,0 +1,18 @@
+import jwt from "jsonwebtoken";
+
+export const authenticateUser = (req, res, next) => {
+ try {
+ const header = req.headers.authorization;
+ if (!header?.startsWith("Bearer ")) {
+ return res.status(401).json({ success: false, message: "Missing or invalid token" });
+ }
+
+ const token = header.replace("Bearer ", "");
+ const payload = jwt.verify(token, process.env.JWT_SECRET);
+
+ req.user = { userId: payload.userId, email: payload.email };
+ next();
+ } catch (err) {
+ return res.status(401).json({ success: false, message: "Invalid token" });
+ }
+};
\ No newline at end of file
diff --git a/backend/src/models/Place.js b/backend/src/models/Place.js
index e69de29bb..1fd404c8b 100644
--- a/backend/src/models/Place.js
+++ b/backend/src/models/Place.js
@@ -0,0 +1,38 @@
+import mongoose from "mongoose";
+
+const placeSchema = new mongoose.Schema(
+ {
+ name: { type: String, required: true, trim: true, minlength: 2 },
+ category: {
+ type: String,
+ enum: ["cafe", "restaurant", "mall", "public", "other"],
+ default: "other",
+ },
+ address: { type: String, trim: true },
+ city: { type: String, required: true, trim: true },
+
+ // GeoJSON Point (Mongo stores [lng, lat])
+ location: {
+ type: { type: String, enum: ["Point"], default: "Point" },
+ coordinates: { type: [Number], required: true }, // [lng, lat]
+ },
+
+ features: {
+ changingTable: { type: Boolean, default: true },
+ privateRoom: { type: Boolean, default: false },
+ strollerAccess: { type: Boolean, default: false },
+ accessible: { type: Boolean, default: false },
+ clean: { type: Boolean, default: false },
+ },
+
+ avgRating: { type: Number, default: 0 },
+ reviewCount: { type: Number, default: 0 },
+
+ createdBy: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
+ },
+ { timestamps: true }
+);
+
+placeSchema.index({ location: "2dsphere" });
+
+export const Place = mongoose.model("Place", placeSchema);
\ No newline at end of file
diff --git a/backend/src/models/User.js b/backend/src/models/User.js
index e69de29bb..64527f4e8 100644
--- a/backend/src/models/User.js
+++ b/backend/src/models/User.js
@@ -0,0 +1,12 @@
+import mongoose from "mongoose";
+
+const userSchema = new mongoose.Schema(
+ {
+ email: { type: String, required: true, unique: true, lowercase: true, trim: true },
+ username: { type: String, trim: true },
+ passwordHash: { type: String, required: true },
+ },
+ { timestamps: true }
+);
+
+export const User = mongoose.model("User", userSchema);
\ No newline at end of file
diff --git a/backend/src/routes/auth.js b/backend/src/routes/auth.js
index e69de29bb..a7c779059 100644
--- a/backend/src/routes/auth.js
+++ b/backend/src/routes/auth.js
@@ -0,0 +1,69 @@
+import express from "express";
+import bcrypt from "bcrypt";
+import jwt from "jsonwebtoken";
+import { User } from "../models/User.js";
+
+const router = express.Router();
+
+router.post("/signup", async (req, res, next) => {
+ try {
+ const { email, password, username } = req.body;
+
+ if (!email || !password) {
+ return res.status(400).json({ success: false, message: "Email and password are required" });
+ }
+ if (password.length < 6) {
+ return res.status(400).json({ success: false, message: "Password must be at least 6 characters" });
+ }
+
+ const existing = await User.findOne({ email: email.toLowerCase() });
+ if (existing) {
+ return res.status(409).json({ success: false, message: "Email already in use" });
+ }
+
+ const passwordHash = await bcrypt.hash(password, 12);
+ const user = await User.create({ email, username, passwordHash });
+
+ const accessToken = jwt.sign(
+ { userId: user._id, email: user.email },
+ process.env.JWT_SECRET,
+ { expiresIn: "7d" }
+ );
+
+ res.status(201).json({
+ success: true,
+ accessToken,
+ user: { userId: user._id, email: user.email, username: user.username || "" },
+ });
+ } catch (err) {
+ next(err);
+ }
+});
+
+router.post("/login", async (req, res, next) => {
+ try {
+ const { email, password } = req.body;
+
+ const user = await User.findOne({ email: email?.toLowerCase() });
+ if (!user) return res.status(401).json({ success: false, message: "Invalid credentials" });
+
+ const ok = await bcrypt.compare(password, user.passwordHash);
+ if (!ok) return res.status(401).json({ success: false, message: "Invalid credentials" });
+
+ const accessToken = jwt.sign(
+ { userId: user._id, email: user.email },
+ process.env.JWT_SECRET,
+ { expiresIn: "7d" }
+ );
+
+ res.json({
+ success: true,
+ accessToken,
+ user: { userId: user._id, email: user.email, username: user.username || "" },
+ });
+ } catch (err) {
+ next(err);
+ }
+});
+
+export default router;
\ No newline at end of file
diff --git a/backend/src/routes/index.js b/backend/src/routes/index.js
index e69de29bb..1f45b51f9 100644
--- a/backend/src/routes/index.js
+++ b/backend/src/routes/index.js
@@ -0,0 +1,12 @@
+import express from "express";
+import authRoutes from "./auth.js";
+import placesRoutes from "./places.js";
+import reviewsRoutes from "./reviews.js";
+
+const router = express.Router();
+
+router.use("/auth", authRoutes);
+router.use("/places", placesRoutes);
+router.use("/", reviewsRoutes); // reviews are nested like /places/:id/reviews
+
+export default router;
\ No newline at end of file
diff --git a/backend/src/routes/places.js b/backend/src/routes/places.js
index 37343cd8d..68c9baef9 100644
--- a/backend/src/routes/places.js
+++ b/backend/src/routes/places.js
@@ -1 +1,58 @@
-import Place from "../models/Place.js";
+import express from "express";
+import { Place } from "../models/Place.js";
+import { authenticateUser } from "../middleware/auth.js";
+
+const router = express.Router();
+
+// GET /places?city=Stockholm&category=cafe
+router.get("/", async (req, res, next) => {
+ try {
+ const { city, category } = req.query;
+ const filter = {};
+ if (city) filter.city = city;
+ if (category) filter.category = category;
+
+ const places = await Place.find(filter).sort({ createdAt: -1 }).limit(200);
+ res.json({ success: true, places });
+ } catch (err) {
+ next(err);
+ }
+});
+
+// GET /places/:id
+router.get("/:id", async (req, res, next) => {
+ try {
+ const place = await Place.findById(req.params.id);
+ if (!place) return res.status(404).json({ success: false, message: "Place not found" });
+ res.json({ success: true, place });
+ } catch (err) {
+ next(err);
+ }
+});
+
+// POST /places (protected)
+router.post("/", authenticateUser, async (req, res, next) => {
+ try {
+ const { name, city, category, address, lat, lng, features } = req.body;
+
+ if (!name || !city || lat == null || lng == null) {
+ return res.status(400).json({ success: false, message: "name, city, lat, lng are required" });
+ }
+
+ const place = await Place.create({
+ name,
+ city,
+ category,
+ address,
+ features,
+ location: { type: "Point", coordinates: [Number(lng), Number(lat)] },
+ createdBy: req.user.userId,
+ });
+
+ res.status(201).json({ success: true, place });
+ } catch (err) {
+ next(err);
+ }
+});
+
+export default router;
\ No newline at end of file
diff --git a/backend/src/routes/reviews.js b/backend/src/routes/reviews.js
index e69de29bb..01c3d6586 100644
--- a/backend/src/routes/reviews.js
+++ b/backend/src/routes/reviews.js
@@ -0,0 +1,45 @@
+import express from "express";
+import mongoose from "mongoose";
+import { Review } from "../models/Review.js";
+import { authenticateUser } from "../middleware/auth.js";
+import { recalcRatingForPlace } from "../utils/recalcRating.js";
+
+const router = express.Router();
+
+// GET /places/:id/reviews
+router.get("/places/:id/reviews", async (req, res, next) => {
+ try {
+ const placeId = new mongoose.Types.ObjectId(req.params.id);
+ const reviews = await Review.find({ placeId }).sort({ createdAt: -1 }).limit(50);
+ res.json({ success: true, reviews });
+ } catch (err) {
+ next(err);
+ }
+});
+
+// POST /places/:id/reviews (protected)
+router.post("/places/:id/reviews", authenticateUser, async (req, res, next) => {
+ try {
+ const placeId = new mongoose.Types.ObjectId(req.params.id);
+ const { rating, comment } = req.body;
+
+ const review = await Review.create({
+ placeId,
+ userId: req.user.userId,
+ rating,
+ comment,
+ });
+
+ await recalcRatingForPlace(placeId);
+
+ res.status(201).json({ success: true, review });
+ } catch (err) {
+ // handle unique index (one review per user per place)
+ if (err.code === 11000) {
+ return res.status(409).json({ success: false, message: "You already reviewed this place" });
+ }
+ next(err);
+ }
+});
+
+export default router;
\ No newline at end of file
diff --git a/backend/utils/recalcRating.js b/backend/utils/recalcRating.js
index e69de29bb..71f89e022 100644
--- a/backend/utils/recalcRating.js
+++ b/backend/utils/recalcRating.js
@@ -0,0 +1,23 @@
+import { Review } from "../models/Review.js";
+import { Place } from "../models/Place.js";
+
+export const recalcRatingForPlace = async (placeId) => {
+ const agg = await Review.aggregate([
+ { $match: { placeId } },
+ {
+ $group: {
+ _id: "$placeId",
+ avgRating: { $avg: "$rating" },
+ reviewCount: { $sum: 1 },
+ },
+ },
+ ]);
+
+ const avgRating = agg[0]?.avgRating || 0;
+ const reviewCount = agg[0]?.reviewCount || 0;
+
+ await Place.findByIdAndUpdate(placeId, {
+ avgRating: Math.round(avgRating * 10) / 10,
+ reviewCount,
+ });
+};
\ No newline at end of file
From 9e807b61f28ff02050c1a9464c68e9d5add8170d Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Tue, 24 Feb 2026 10:32:10 +0100
Subject: [PATCH 05/67] Refactor error handler comment and ensure consistent
newline at end of files
---
backend/src/app.js | 4 ++--
backend/src/db/connectDB.js | 2 +-
backend/src/middleware/errorHandler.js | 2 +-
backend/src/models/Review.js | 16 ++++++++++++++++
4 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/backend/src/app.js b/backend/src/app.js
index 038be19e3..55e433cc4 100644
--- a/backend/src/app.js
+++ b/backend/src/app.js
@@ -24,7 +24,7 @@ app.get("/", (req, res) => {
app.use(routes);
-// Error handler
+// Error handler LAST
app.use(errorHandler);
-export default app;
+export default app;
\ No newline at end of file
diff --git a/backend/src/db/connectDB.js b/backend/src/db/connectDB.js
index 3ce2571ca..fd984fb4d 100644
--- a/backend/src/db/connectDB.js
+++ b/backend/src/db/connectDB.js
@@ -10,4 +10,4 @@ export const connectDB = async () => {
console.error("❌ MongoDB connection error:", err.message);
process.exit(1);
}
-};
+};
\ No newline at end of file
diff --git a/backend/src/middleware/errorHandler.js b/backend/src/middleware/errorHandler.js
index c517ceb93..bf9ae1934 100644
--- a/backend/src/middleware/errorHandler.js
+++ b/backend/src/middleware/errorHandler.js
@@ -6,4 +6,4 @@ export const errorHandler = (err, req, res, next) => {
success: false,
message: err.message || "Something went wrong",
});
-};
+};
\ No newline at end of file
diff --git a/backend/src/models/Review.js b/backend/src/models/Review.js
index e69de29bb..b24a930e6 100644
--- a/backend/src/models/Review.js
+++ b/backend/src/models/Review.js
@@ -0,0 +1,16 @@
+import mongoose from "mongoose";
+
+const reviewSchema = new mongoose.Schema(
+ {
+ placeId: { type: mongoose.Schema.Types.ObjectId, ref: "Place", required: true },
+ userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true },
+ rating: { type: Number, required: true, min: 1, max: 5 },
+ comment: { type: String, trim: true, maxlength: 500 },
+ },
+ { timestamps: true }
+);
+
+// One review per user and per place
+reviewSchema.index({ placeId: 1, userId: 1 }, { unique: true });
+
+export const Review = mongoose.model("Review", reviewSchema);
\ No newline at end of file
From 4859c8b318bdb118f645e34badec0a0ee173c4dd Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Tue, 24 Feb 2026 10:36:02 +0100
Subject: [PATCH 06/67] Refactor server setup by removing unused imports and
simplifying code structure
---
backend/src/server.js | 21 +++------------------
1 file changed, 3 insertions(+), 18 deletions(-)
diff --git a/backend/src/server.js b/backend/src/server.js
index 070c87518..8ce08c001 100644
--- a/backend/src/server.js
+++ b/backend/src/server.js
@@ -1,22 +1,7 @@
-import express from "express";
-import cors from "cors";
-import mongoose from "mongoose";
-
-const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/final-project";
-mongoose.connect(mongoUrl);
-mongoose.Promise = Promise;
+import app from "./app.js";
const port = process.env.PORT || 8080;
-const app = express();
-
-app.use(cors());
-app.use(express.json());
-
-app.get("/", (req, res) => {
- res.send("Hello Technigo!");
-});
-// Start the server
app.listen(port, () => {
- console.log(`Server running on http://localhost:${port}`);
-});
+ console.log(`✅ Server running on http://localhost:${port}`);
+});
\ No newline at end of file
From 41dde08596d907bf07ce7b9242ea0591682dd89b Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Tue, 24 Feb 2026 10:40:58 +0100
Subject: [PATCH 07/67] Reintroduce recalcRating utility and add validators
file
---
backend/{ => src}/utils/recalcRating.js | 0
backend/{ => src}/utils/validators.js | 0
2 files changed, 0 insertions(+), 0 deletions(-)
rename backend/{ => src}/utils/recalcRating.js (100%)
rename backend/{ => src}/utils/validators.js (100%)
diff --git a/backend/utils/recalcRating.js b/backend/src/utils/recalcRating.js
similarity index 100%
rename from backend/utils/recalcRating.js
rename to backend/src/utils/recalcRating.js
diff --git a/backend/utils/validators.js b/backend/src/utils/validators.js
similarity index 100%
rename from backend/utils/validators.js
rename to backend/src/utils/validators.js
From e2a7ec779a3fe08c482da689e096cbb9dffd9a24 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 25 Feb 2026 11:38:32 +0100
Subject: [PATCH 08/67] Add Home component and implement API calls for places;
enhance error handling in places API
---
frontend/pages/Home.jsx | 24 ++++++++++++++++++++++++
frontend/src/api/auth.js | 31 +++++++++++++++++++++++++++++++
frontend/src/api/client.js | 0
frontend/src/api/places.js | 32 ++++++++++++++++++++++++++++++++
frontend/src/api/reviews.js | 25 +++++++++++++++++++++++++
5 files changed, 112 insertions(+)
delete mode 100644 frontend/src/api/client.js
diff --git a/frontend/pages/Home.jsx b/frontend/pages/Home.jsx
index e69de29bb..c4e594ed0 100644
--- a/frontend/pages/Home.jsx
+++ b/frontend/pages/Home.jsx
@@ -0,0 +1,24 @@
+import { useEffect, useState } from "react";
+import { api } from "../api";
+import PlaceCard from "../components/PlaceCard";
+
+export default function Home() {
+ const [places, setPlaces] = useState([]);
+ const [error, setError] = useState("");
+
+ useEffect(() => {
+ api("/places")
+ .then((data) => setPlaces(data.places || []))
+ .catch((err) => setError(err.message));
+ }, []);
+
+ if (error) return {error}
;
+
+ return (
+
+ {places.map((p) => (
+
+ ))}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/api/auth.js b/frontend/src/api/auth.js
index e69de29bb..199ce7a8f 100644
--- a/frontend/src/api/auth.js
+++ b/frontend/src/api/auth.js
@@ -0,0 +1,31 @@
+const BASE = import.meta.env.VITE_API_URL || "http://localhost:8081";
+
+export async function signup(email, password) {
+ const res = await fetch(`${BASE}/auth/signup`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, password }),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) throw new Error(data.message);
+
+ localStorage.setItem("token", data.accessToken);
+ return data;
+}
+
+export async function login(email, password) {
+ const res = await fetch(`${BASE}/auth/login`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, password }),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) throw new Error(data.message);
+
+ localStorage.setItem("token", data.accessToken);
+ return data;
+}
\ No newline at end of file
diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js
deleted file mode 100644
index e69de29bb..000000000
diff --git a/frontend/src/api/places.js b/frontend/src/api/places.js
index e69de29bb..c2c80711d 100644
--- a/frontend/src/api/places.js
+++ b/frontend/src/api/places.js
@@ -0,0 +1,32 @@
+const BASE = import.meta.env.VITE_API_URL || "http://localhost:8081";
+
+export async function getPlaces() {
+ const res = await fetch(`${BASE}/places`);
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.message);
+ return data;
+}
+
+export async function getPlace(id) {
+ const res = await fetch(`${BASE}/places/${id}`);
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.message);
+ return data;
+}
+
+export async function createPlace(place) {
+ const token = localStorage.getItem("token");
+
+ const res = await fetch(`${BASE}/places`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify(place),
+ });
+
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.message);
+ return data;
+}
\ No newline at end of file
diff --git a/frontend/src/api/reviews.js b/frontend/src/api/reviews.js
index e69de29bb..e16150500 100644
--- a/frontend/src/api/reviews.js
+++ b/frontend/src/api/reviews.js
@@ -0,0 +1,25 @@
+const BASE = import.meta.env.VITE_API_URL || "http://localhost:8081";
+
+export async function getReviews(placeId) {
+ const res = await fetch(`${BASE}/places/${placeId}/reviews`);
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.message);
+ return data;
+}
+
+export async function createReview(placeId, review) {
+ const token = localStorage.getItem("token");
+
+ const res = await fetch(`${BASE}/places/${placeId}/reviews`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify(review),
+ });
+
+ const data = await res.json();
+ if (!res.ok) throw new Error(data.message);
+ return data;
+}
\ No newline at end of file
From d9ee5929a8b5a962723955ddde26ff8dee5bfec3 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 25 Feb 2026 11:52:08 +0100
Subject: [PATCH 09/67] Remove global.css, update package.json to include
styled-components, and add GlobalStyles.js
---
frontend/README.md | 53 ++++++++++++++++++-
.../styles/{global.css => GlobalStyles.js} | 0
package.json | 3 +-
3 files changed, 54 insertions(+), 2 deletions(-)
rename frontend/styles/{global.css => GlobalStyles.js} (100%)
diff --git a/frontend/README.md b/frontend/README.md
index 5cdb1d9cf..8973a5063 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -5,4 +5,55 @@ This boilerplate is designed to give you a head start in your React projects, wi
## Getting Started
1. Install the required dependencies using `npm install`.
-2. Start the development server using `npm run dev`.
\ No newline at end of file
+2. Start the development server using `npm run dev`.
+
+Baby Changing Places — Full-Stack App
+
+A full-stack web application for finding and reviewing baby changing facilities in public places.
+Users can browse locations, see details and reviews, and (when logged in) add new places or leave a review.
+
+This project was built as a final bootcamp project using a React frontend and a Node/Express/MongoDB backend.
+
+
+Features
+
+* View a list of baby changing places
+* View detailed information for each place
+* See reviews and average ratings
+* Create an account and log in
+* Add new places (authenticated)
+* Add one review per place (authenticated)
+* Prevent duplicate reviews from the same user
+
+
+Tech Stack
+
+Frontend
+
+Built using React
+
+Navigation handled with React Router
+
+Global state managed using Context API / Zustand (depending on implementation)
+
+Backend
+
+Node.js with Express
+
+MongoDB database with Mongoose models
+
+Authentication
+
+JWT-based authentication implemented
+
+Protected routes require valid token
+
+External Libraries
+
+
+
+
+
+
+
+
diff --git a/frontend/styles/global.css b/frontend/styles/GlobalStyles.js
similarity index 100%
rename from frontend/styles/global.css
rename to frontend/styles/GlobalStyles.js
diff --git a/package.json b/package.json
index b6e0010d0..01fb29fc4 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,8 @@
"express": "^5.2.1",
"express-list-endpoints": "^7.1.1",
"jsonwebtoken": "^9.0.3",
- "mongoose": "^9.2.1"
+ "mongoose": "^9.2.1",
+ "styled-components": "^6.3.11"
},
"devDependencies": {
"nodemon": "^3.1.11"
From bb3e75e50e3e2ad04f9344454e258c54f58132c9 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 25 Feb 2026 11:56:40 +0100
Subject: [PATCH 10/67] Add Navbar component with styled components for
navigation and logout functionality
---
frontend/components/Navbar.jsx | 64 +++++++++++++++++++++++++++++++++
frontend/styles/GlobalStyles.js | 57 +++++++++++++++++++++++++++++
2 files changed, 121 insertions(+)
diff --git a/frontend/components/Navbar.jsx b/frontend/components/Navbar.jsx
index e69de29bb..d94d56cf5 100644
--- a/frontend/components/Navbar.jsx
+++ b/frontend/components/Navbar.jsx
@@ -0,0 +1,64 @@
+import { Link, NavLink } from "react-router-dom";
+import styled from "styled-components";
+
+export default function Navbar({ isLoggedIn, onLogout }) {
+ return (
+
+ Baby Changing Places
+
+
+
+ );
+}
+
+/* styled components */
+
+const Header = styled.header`
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+ border-bottom: 1px solid #eee;
+`;
+
+const Brand = styled(Link)`
+ font-weight: 700;
+ text-decoration: none;
+ color: inherit;
+`;
+
+const Nav = styled.nav`
+ display: flex;
+ gap: 12px;
+ align-items: center;
+`;
+
+const NavItem = styled(NavLink)`
+ text-decoration: none;
+ color: inherit;
+
+ &.active {
+ font-weight: 600;
+ }
+`;
+
+const LogoutButton = styled.button`
+ padding: 8px 10px;
+ border: 1px solid #ddd;
+ background: white;
+ border-radius: 8px;
+ cursor: pointer;
+
+ &:hover {
+ background: #f6f6f6;
+ }
+`;
\ No newline at end of file
diff --git a/frontend/styles/GlobalStyles.js b/frontend/styles/GlobalStyles.js
index e69de29bb..e8fc4d40c 100644
--- a/frontend/styles/GlobalStyles.js
+++ b/frontend/styles/GlobalStyles.js
@@ -0,0 +1,57 @@
+import { createGlobalStyle } from "styled-components";
+
+export const GlobalStyles = createGlobalStyle`
+ :root{
+ /* Grebban-ish: neutral, high contrast, lots of whitespace */
+ --bg: #ffffff;
+ --text: #111111;
+ --muted: #666666;
+ --border: #e7e7e7;
+ --surface: #f7f7f7;
+
+ --radius: 16px;
+
+ /* spacing scale */
+ --s1: 6px;
+ --s2: 10px;
+ --s3: 16px;
+ --s4: 24px;
+ --s5: 40px;
+ --s6: 64px;
+
+ /* layout */
+ --max: 1120px;
+ }
+
+ * { box-sizing: border-box; }
+ html, body { height: 100%; }
+
+ body {
+ margin: 0;
+ background: var(--bg);
+ color: var(--text);
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
+ line-height: 1.35;
+ letter-spacing: -0.01em;
+ }
+
+ a { color: inherit; text-decoration: none; }
+ a:hover { text-decoration: underline; }
+
+ img { max-width: 100%; display: block; }
+
+ /* accessible focus */
+ :focus-visible {
+ outline: 3px solid rgba(17,17,17,0.25);
+ outline-offset: 2px;
+ border-radius: 8px;
+ }
+
+ /* Typography scale: editorial */
+ h1, h2, h3, h4 { margin: 0; letter-spacing: -0.03em; }
+ h1 { font-size: clamp(32px, 4vw, 56px); line-height: 1.05; }
+ h2 { font-size: clamp(22px, 2.4vw, 32px); line-height: 1.1; }
+ h3 { font-size: 18px; line-height: 1.2; }
+ p { margin: 0; color: var(--text); }
+ .muted { color: var(--muted); }
+`;
\ No newline at end of file
From d332f0c69515b014c57dd0841f65c72a2c905be4 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 25 Feb 2026 12:07:48 +0100
Subject: [PATCH 11/67] Add PlaceCard component with styled components for
showing place details and features
---
frontend/components/Filters.jsx | 116 +++++++++++++++++++++++++++++
frontend/components/PlaceCard.jsx | 118 ++++++++++++++++++++++++++++++
2 files changed, 234 insertions(+)
diff --git a/frontend/components/Filters.jsx b/frontend/components/Filters.jsx
index e69de29bb..79c09fe19 100644
--- a/frontend/components/Filters.jsx
+++ b/frontend/components/Filters.jsx
@@ -0,0 +1,116 @@
+import styled from "styled-components";
+
+export default function Filters({ value, onChange }) {
+ const { city = "", category = "", onlyChangingTable = false } = value || {};
+
+ function update(next) {
+ onChange({ ...(value || {}), ...next });
+ }
+
+ return (
+
+
+
+ update({ city: e.target.value })}
+ placeholder="e.g. Solna"
+ autoComplete="address-level2"
+ />
+
+
+
+
+
+
+
+
+ update({ onlyChangingTable: e.target.checked })}
+ />
+
+ Only show places with changing table
+
+
+
+ );
+}
+
+/* styled components */
+
+const Wrap = styled.section`
+ border: 1px solid #e7e7e7;
+ border-radius: 16px;
+ padding: 20px;
+ display: grid;
+ gap: 16px;
+ background: white;
+`;
+
+const Field = styled.div`
+ display: grid;
+ gap: 6px;
+`;
+
+const Label = styled.label`
+ font-size: 13px;
+ color: #666;
+`;
+
+const Input = styled.input`
+ border: 1px solid #e7e7e7;
+ border-radius: 12px;
+ padding: 12px 14px;
+ font-size: 15px;
+
+ &:focus {
+ outline: none;
+ border-color: #bbb;
+ }
+`;
+
+const Select = styled.select`
+ border: 1px solid #e7e7e7;
+ border-radius: 12px;
+ padding: 12px 14px;
+ font-size: 15px;
+ background: white;
+
+ &:focus {
+ outline: none;
+ border-color: #bbb;
+ }
+`;
+
+const CheckRow = styled.div`
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ padding: 12px;
+ border-radius: 12px;
+ background: #f7f7f7;
+`;
+
+const Checkbox = styled.input`
+ width: 16px;
+ height: 16px;
+`;
+
+const CheckLabel = styled.label`
+ font-size: 14px;
+ color: #111;
+`;
\ No newline at end of file
diff --git a/frontend/components/PlaceCard.jsx b/frontend/components/PlaceCard.jsx
index e69de29bb..c6ea49a7c 100644
--- a/frontend/components/PlaceCard.jsx
+++ b/frontend/components/PlaceCard.jsx
@@ -0,0 +1,118 @@
+import { Link } from "react-router-dom";
+import styled from "styled-components";
+
+export default function PlaceCard({ place }) {
+ const {
+ _id,
+ name,
+ city,
+ category,
+ avgRating,
+ reviewCount,
+ features = {},
+ } = place;
+
+ return (
+
+
+
+ {name}
+
+
+ {category && {category}}
+
+
+ {city}
+
+
+ ⭐ {avgRating ?? 0}
+ ({reviewCount ?? 0})
+
+
+
+ {features.changingTable && Changing table}
+ {features.privateRoom && Private room}
+ {features.strollerAccess && Stroller access}
+ {features.accessible && Accessible}
+
+
+ );
+}
+
+/* styled components */
+
+const Card = styled.article`
+ border: 1px solid #e7e7e7;
+ border-radius: 16px;
+ padding: 18px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ background: white;
+ transition: transform 0.15s ease, border-color 0.15s ease;
+
+ &:hover {
+ transform: translateY(-3px);
+ border-color: #cfcfcf;
+ }
+`;
+
+const Top = styled.div`
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ align-items: flex-start;
+`;
+
+const Title = styled.h3`
+ margin: 0;
+ font-size: 18px;
+ line-height: 1.2;
+`;
+
+const StyledLink = styled(Link)`
+ text-decoration: none;
+ color: #111;
+
+ &:hover {
+ text-decoration: underline;
+ }
+`;
+
+const Pill = styled.span`
+ border: 1px solid #e7e7e7;
+ border-radius: 999px;
+ padding: 4px 10px;
+ font-size: 12px;
+ color: #666;
+ white-space: nowrap;
+`;
+
+const City = styled.p`
+ margin: 0;
+ color: #555;
+`;
+
+const Rating = styled.div`
+ font-size: 14px;
+ font-weight: 600;
+
+ span {
+ font-weight: 400;
+ color: #777;
+ margin-left: 4px;
+ }
+`;
+
+const Features = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+`;
+
+const Feature = styled.span`
+ background: #f6f6f6;
+ padding: 4px 10px;
+ border-radius: 999px;
+ font-size: 12px;
+`;
\ No newline at end of file
From 465d15e3238eb159cf48df42dfd7fbecdc1ae4c4 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Thu, 26 Feb 2026 09:33:37 +0100
Subject: [PATCH 12/67] Refactor Home component to improve loading and error
handling; enhance layout with styled components
---
frontend/pages/Home.jsx | 84 ++++++++++++++++++++++++++++++++++++-----
1 file changed, 75 insertions(+), 9 deletions(-)
diff --git a/frontend/pages/Home.jsx b/frontend/pages/Home.jsx
index c4e594ed0..5d17ead82 100644
--- a/frontend/pages/Home.jsx
+++ b/frontend/pages/Home.jsx
@@ -1,24 +1,90 @@
import { useEffect, useState } from "react";
+import styled from "styled-components";
import { api } from "../api";
import PlaceCard from "../components/PlaceCard";
export default function Home() {
const [places, setPlaces] = useState([]);
const [error, setError] = useState("");
+ const [loading, setLoading] = useState(true);
useEffect(() => {
api("/places")
.then((data) => setPlaces(data.places || []))
- .catch((err) => setError(err.message));
+ .catch((err) => setError(err.message))
+ .finally(() => setLoading(false));
}, []);
- if (error) return {error}
;
-
return (
-
- {places.map((p) => (
-
- ))}
-
+
+
+ Find baby changing facilities near you
+
+ Discover clean, accessible and family-friendly places.
+
+
+
+
+ {loading && Loading places…
}
+ {error && {error}}
+
+ {!loading && !error && places.length === 0 && (
+ No places added yet.
+ )}
+
+
+ {places.map((p) => (
+
+ ))}
+
+
+
);
-}
\ No newline at end of file
+}
+
+/*styled components */
+
+const Wrapper = styled.main`
+ padding: 40px 16px;
+ max-width: 1120px;
+ margin: 0 auto;
+`;
+
+const Hero = styled.section`
+ margin-bottom: 48px;
+
+ h1 {
+ margin: 0;
+ font-size: clamp(28px, 4vw, 48px);
+ line-height: 1.1;
+ letter-spacing: -0.03em;
+ }
+
+ p {
+ margin-top: 12px;
+ font-size: 16px;
+ color: #666;
+ }
+`;
+
+const Section = styled.section`
+ border-top: 1px solid #e7e7e7;
+ padding-top: 32px;
+`;
+
+const Grid = styled.div`
+ display: grid;
+ gap: 20px;
+
+ @media (min-width: 640px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ @media (min-width: 1024px) {
+ grid-template-columns: repeat(3, 1fr);
+ }
+`;
+
+const ErrorText = styled.p`
+ color: crimson;
+`;
\ No newline at end of file
From 460c4d2080bd1a6db709004d510ee710b622dee7 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Thu, 26 Feb 2026 09:42:52 +0100
Subject: [PATCH 13/67] Implement PlaceDetails page with API integration for
fetching place and reviews; enhance loading and error handling
---
frontend/pages/PlaceDetails.jsx | 129 +++++++++++++++++++++++++++++++-
frontend/src/App.jsx | 19 +++--
2 files changed, 142 insertions(+), 6 deletions(-)
diff --git a/frontend/pages/PlaceDetails.jsx b/frontend/pages/PlaceDetails.jsx
index c1b0730e0..af0fc1c54 100644
--- a/frontend/pages/PlaceDetails.jsx
+++ b/frontend/pages/PlaceDetails.jsx
@@ -1 +1,128 @@
-x
\ No newline at end of file
+import { useEffect, useState } from "react";
+import { useParams, Link } from "react-router-dom";
+import styled from "styled-components";
+import { api } from "../api";
+
+export default function PlaceDetails() {
+ const { id } = useParams();
+
+ const [place, setPlace] = useState(null);
+ const [reviews, setReviews] = useState([]);
+ const [error, setError] = useState("");
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ async function fetchData() {
+ try {
+ const placeData = await api(`/places/${id}`);
+ const reviewData = await api(`/places/${id}/reviews`);
+
+ setPlace(placeData.place);
+ setReviews(reviewData.reviews || []);
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ fetchData();
+ }, [id]);
+
+ if (loading) return Loading…;
+ if (error) return {error};
+ if (!place) return Place not found.;
+
+ const features = place.features || {};
+
+ return (
+
+ Back
+
+ {place.name}
+ {place.city}
+
+
+ ⭐ {place.avgRating ?? 0} ({place.reviewCount ?? 0} reviews)
+
+
+
+ Features
+
+ - Changing table: {features.changingTable ? "Yes" : "No"}
+ - Private room: {features.privateRoom ? "Yes" : "No"}
+ - Stroller access: {features.strollerAccess ? "Yes" : "No"}
+ - Accessible: {features.accessible ? "Yes" : "No"}
+
+
+
+
+ Reviews
+
+ {reviews.length === 0 ? (
+ No reviews yet.
+ ) : (
+ reviews.map((r) => (
+
+ ⭐ {r.rating}
+ {r.comment}
+
+ ))
+ )}
+
+
+ );
+}
+
+/* styled components */
+
+const Wrapper = styled.main`
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 40px 16px;
+`;
+
+const BackLink = styled(Link)`
+ display: inline-block;
+ margin-bottom: 16px;
+ color: #666;
+ text-decoration: none;
+`;
+
+const Title = styled.h1`
+ margin: 0;
+ font-size: 32px;
+`;
+
+const City = styled.p`
+ margin: 8px 0;
+ color: #666;
+`;
+
+const Rating = styled.p`
+ margin: 12px 0 24px;
+ font-weight: 600;
+`;
+
+const Section = styled.section`
+ margin-top: 24px;
+
+ h2 {
+ margin-bottom: 8px;
+ }
+
+ ul {
+ padding-left: 20px;
+ }
+`;
+
+const ReviewCard = styled.div`
+ border: 1px solid #e7e7e7;
+ border-radius: 12px;
+ padding: 12px;
+ margin-bottom: 12px;
+`;
+
+const ErrorText = styled.p`
+ color: red;
+`;
\ No newline at end of file
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 0a24275e6..6194143bc 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,8 +1,17 @@
-export const App = () => {
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+import Navbar from "./components/Navbar";
+import Home from "./pages/Home";
+import PlaceDetails from "./pages/PlaceDetails";
+export const App = () => {
return (
- <>
- Welcome to Final Project!
- >
+
+
+
+
+ } />
+ } />
+
+
);
-};
+};
\ No newline at end of file
From c9107f22c6386af54860a3fe003a63cb75c97239 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Thu, 26 Feb 2026 09:47:24 +0100
Subject: [PATCH 14/67] Rename Brand in Navbar component
---
frontend/components/Navbar.jsx | 2 +-
package.json | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/frontend/components/Navbar.jsx b/frontend/components/Navbar.jsx
index d94d56cf5..fe5a2c08b 100644
--- a/frontend/components/Navbar.jsx
+++ b/frontend/components/Navbar.jsx
@@ -4,7 +4,7 @@ import styled from "styled-components";
export default function Navbar({ isLoggedIn, onLogout }) {
return (
- Baby Changing Places
+ MiniStops
) : (
-
)}
@@ -165,8 +167,6 @@ export default function PlaceDetails() {
);
}
-/* styled components */
-
const Wrapper = styled.main`
max-width: 800px;
margin: 0 auto;
@@ -217,38 +217,34 @@ const ReviewCard = styled.div`
const Form = styled.form`
display: grid;
gap: 12px;
- max-width: 520px;
+ max-width: 500px;
label {
display: grid;
gap: 6px;
- font-size: 14px;
}
select,
textarea {
- padding: 10px 12px;
- border-radius: 10px;
+ padding: 10px;
border: 1px solid #ddd;
+ border-radius: 8px;
font-family: inherit;
- font-size: 14px;
}
button {
- padding: 10px 12px;
- border-radius: 10px;
+ padding: 10px;
border: 1px solid #ddd;
+ border-radius: 8px;
background: white;
cursor: pointer;
}
-
- button:disabled {
- opacity: 0.7;
- cursor: not-allowed;
- }
`;
const ErrorText = styled.p`
color: red;
- margin: 0;
+`;
+
+const SuccessText = styled.p`
+ color: green;
`;
\ No newline at end of file
diff --git a/frontend/src/store/authStore.js b/frontend/src/store/authStore.js
index 8dc4eaedf..b35a173d2 100644
--- a/frontend/src/store/authStore.js
+++ b/frontend/src/store/authStore.js
@@ -6,20 +6,27 @@ const USER_KEY = "ministops_user";
export const useAuthStore = create((set) => ({
accessToken: localStorage.getItem(TOKEN_KEY),
user: JSON.parse(localStorage.getItem(USER_KEY) || "null"),
-
- get isLoggedIn() {
- return !!localStorage.getItem(TOKEN_KEY);
- },
+ isLoggedIn: !!localStorage.getItem(TOKEN_KEY),
login: ({ accessToken, user }) => {
localStorage.setItem(TOKEN_KEY, accessToken);
localStorage.setItem(USER_KEY, JSON.stringify(user));
- set({ accessToken, user });
+
+ set({
+ accessToken,
+ user,
+ isLoggedIn: true,
+ });
},
logout: () => {
localStorage.removeItem(TOKEN_KEY);
localStorage.removeItem(USER_KEY);
- set({ accessToken: null, user: null });
+
+ set({
+ accessToken: null,
+ user: null,
+ isLoggedIn: false,
+ });
},
}));
\ No newline at end of file
From be1712f41b0a283a1da268513a7b97d381228aec Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Tue, 10 Mar 2026 10:25:09 +0100
Subject: [PATCH 32/67] updated scripts in package.json
---
frontend/src/components/Navbar.jsx | 6 ++----
package.json | 31 +++++++++++++++++-------------
2 files changed, 20 insertions(+), 17 deletions(-)
diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx
index 1300bc3db..5e9515d99 100644
--- a/frontend/src/components/Navbar.jsx
+++ b/frontend/src/components/Navbar.jsx
@@ -3,10 +3,8 @@ import styled from "styled-components";
import { useAuthStore } from "../store/authStore";
export default function Navbar() {
- const accessToken = useAuthStore((s) => s.accessToken);
- const logout = useAuthStore((s) => s.logout);
-
- const isLoggedIn = !!accessToken;
+ const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
+ const logout = useAuthStore((state) => state.logout);
return (
diff --git a/package.json b/package.json
index df388eed5..1a4707688 100644
--- a/package.json
+++ b/package.json
@@ -1,22 +1,27 @@
{
- "name": "project-final-parent",
+ "name": "project-final-frontend",
+ "description": "Client part of final project",
"version": "1.0.0",
+ "type": "module",
"scripts": {
- "postinstall": "npm install --prefix backend"
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
},
"dependencies": {
- "bcrypt": "^6.0.0",
- "cors": "^2.8.6",
- "dotenv": "^17.3.1",
- "express": "^5.2.1",
- "express-list-endpoints": "^7.1.1",
- "jsonwebtoken": "^9.0.3",
- "mongoose": "^9.2.1",
- "react-router-dom": "^7.13.1",
- "styled-components": "^6.3.11",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
"zustand": "^5.0.11"
},
"devDependencies": {
- "nodemon": "^3.1.11"
+ "@types/react": "^18.2.15",
+ "@types/react-dom": "^18.2.7",
+ "@vitejs/plugin-react": "^4.0.3",
+ "eslint": "^8.45.0",
+ "eslint-plugin-react": "^7.32.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.3",
+ "vite": "^6.3.5"
}
-}
+}
\ No newline at end of file
From 11b0e825bc1bc782802ee7e55a32484eb2c77528 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Tue, 10 Mar 2026 10:31:40 +0100
Subject: [PATCH 33/67] package.json dependencies and import leaflet CSS in
main.jsx
---
frontend/package.json | 4 ++++
frontend/src/main.jsx | 3 ++-
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/frontend/package.json b/frontend/package.json
index 0635c82ca..0f9a6a5d0 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,8 +10,12 @@
"preview": "vite preview"
},
"dependencies": {
+ "leaflet": "^1.9.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-leaflet": "^4.2.1",
+ "react-router-dom": "^7.13.1",
+ "styled-components": "^6.3.11",
"zustand": "^5.0.11"
},
"devDependencies": {
diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx
index 51294f399..45f9bd69e 100644
--- a/frontend/src/main.jsx
+++ b/frontend/src/main.jsx
@@ -2,9 +2,10 @@ import React from "react";
import ReactDOM from "react-dom/client";
import { App } from "./App.jsx";
import "./index.css";
+import "leaflet/dist/leaflet.css";
ReactDOM.createRoot(document.getElementById("root")).render(
-);
+);
\ No newline at end of file
From 86d2d561940718aef74e41ff1bd974bbfea9001a Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Tue, 10 Mar 2026 15:13:42 +0100
Subject: [PATCH 34/67] added test Maps to see it will work
---
frontend/src/components/MapTest.jsx | 16 ++++++++
frontend/src/pages/Home.jsx | 61 +++++++++++------------------
2 files changed, 38 insertions(+), 39 deletions(-)
create mode 100644 frontend/src/components/MapTest.jsx
diff --git a/frontend/src/components/MapTest.jsx b/frontend/src/components/MapTest.jsx
new file mode 100644
index 000000000..b3b9cf6b3
--- /dev/null
+++ b/frontend/src/components/MapTest.jsx
@@ -0,0 +1,16 @@
+import { MapContainer, TileLayer } from "react-leaflet";
+
+export default function MapTest() {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx
index a05b7fec3..ca1270909 100644
--- a/frontend/src/pages/Home.jsx
+++ b/frontend/src/pages/Home.jsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
import styled from "styled-components";
import { getPlaces } from "../api/places";
import PlaceCard from "../components/PlaceCard";
+import MapTest from "../components/MapTest";
export default function Home() {
const [places, setPlaces] = useState([]);
@@ -9,14 +10,14 @@ export default function Home() {
const [loading, setLoading] = useState(true);
useEffect(() => {
- setLoading(true);
- setError("");
+ setLoading(true);
+ setError("");
- getPlaces()
- .then((data) => setPlaces(data.places || []))
- .catch((err) => setError(err.message || "Failed to load places"))
- .finally(() => setLoading(false));
-}, []);
+ getPlaces()
+ .then((data) => setPlaces(data.places || []))
+ .catch((err) => setError(err.message || "Failed to load places"))
+ .finally(() => setLoading(false));
+ }, []);
return (
@@ -27,6 +28,8 @@ export default function Home() {
+
+
{loading && Loading places…
}
{error && {error}}
@@ -45,52 +48,32 @@ export default function Home() {
);
}
-/*styled components */
-
const Wrapper = styled.main`
- padding: 40px 16px;
- max-width: 1120px;
+ padding: 24px 16px 40px;
+ max-width: 1200px;
margin: 0 auto;
`;
const Hero = styled.section`
- margin: 0 auto 64px;
- text-align: center;
- max-width: 900px;
+ margin-bottom: 24px;
- h1 {
- margin: 0;
- font-family: 'Inter', sans-serif;
- font-weight: 400;
- font-size: clamp(40px, 6vw, 72px);
- line-height: 1.1;
- letter-spacing: -0.02em;
-}
- p {
- margin-top: 20px;
- font-size: 18px;
- color: #555;
- font-weight: 400;
+ .muted {
+ color: #666;
}
`;
+
const Section = styled.section`
- border-top: 1px solid #e7e7e7;
- padding-top: 32px;
+ display: grid;
+ gap: 16px;
`;
const Grid = styled.div`
display: grid;
- gap: 20px;
-
- @media (min-width: 640px) {
- grid-template-columns: repeat(2, 1fr);
- }
-
- @media (min-width: 1024px) {
- grid-template-columns: repeat(3, 1fr);
- }
+ gap: 16px;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
`;
const ErrorText = styled.p`
- color: crimson;
+ color: red;
+ margin: 0;
`;
\ No newline at end of file
From a8911320252dff41a4e498562122913959787f6e Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 11 Mar 2026 12:48:34 +0100
Subject: [PATCH 35/67] replaced MapTest with PlacesMap for real mapping
---
frontend/src/components/MapTest.jsx | 16 -----------
frontend/src/components/PlacesMap.jsx | 39 +++++++++++++++++++++++++++
frontend/src/pages/Home.jsx | 4 +--
3 files changed, 41 insertions(+), 18 deletions(-)
delete mode 100644 frontend/src/components/MapTest.jsx
create mode 100644 frontend/src/components/PlacesMap.jsx
diff --git a/frontend/src/components/MapTest.jsx b/frontend/src/components/MapTest.jsx
deleted file mode 100644
index b3b9cf6b3..000000000
--- a/frontend/src/components/MapTest.jsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { MapContainer, TileLayer } from "react-leaflet";
-
-export default function MapTest() {
- return (
-
-
-
- );
-}
\ No newline at end of file
diff --git a/frontend/src/components/PlacesMap.jsx b/frontend/src/components/PlacesMap.jsx
new file mode 100644
index 000000000..904aceafc
--- /dev/null
+++ b/frontend/src/components/PlacesMap.jsx
@@ -0,0 +1,39 @@
+import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
+import { Link } from "react-router-dom";
+
+export default function PlacesMap({ places }) {
+ const defaultCenter = [59.3293, 18.0686];
+
+ return (
+
+
+
+ {places.map((place) => {
+ const coordinates = place.location?.coordinates;
+
+ if (!coordinates || coordinates.length < 2) return null;
+
+ const [lng, lat] = coordinates;
+
+ return (
+
+
+ {place.name}
+
+ {place.city}
+
+ View details
+
+
+ );
+ })}
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx
index ca1270909..1cfa144ed 100644
--- a/frontend/src/pages/Home.jsx
+++ b/frontend/src/pages/Home.jsx
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import styled from "styled-components";
import { getPlaces } from "../api/places";
import PlaceCard from "../components/PlaceCard";
-import MapTest from "../components/MapTest";
+import PlacesMap from "../components/PlacesMap";
export default function Home() {
const [places, setPlaces] = useState([]);
@@ -28,7 +28,7 @@ export default function Home() {
-
+
{loading && Loading places…
}
From 4d8adce7f63a686c94bb75a206df45d66c26e8af Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 11 Mar 2026 13:00:42 +0100
Subject: [PATCH 36/67] Added Leaflet imports and LocationPicker
---
frontend/src/pages/AddPlace.jsx | 48 ++++++++++++++++++++++++++++++---
1 file changed, 45 insertions(+), 3 deletions(-)
diff --git a/frontend/src/pages/AddPlace.jsx b/frontend/src/pages/AddPlace.jsx
index 80563458c..0fb769d52 100644
--- a/frontend/src/pages/AddPlace.jsx
+++ b/frontend/src/pages/AddPlace.jsx
@@ -1,10 +1,25 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
+import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet";
import { useAuthStore } from "../store/authStore";
const API_BASE_URL =
- import.meta.env.VITE_API_BASE_URL || "http://localhost:8081";
+ import.meta.env.VITE_API_URL || "http://localhost:8081";
+
+function LocationPicker({ lat, lng, setLat, setLng }) {
+ useMapEvents({
+ click(event) {
+ const { lat, lng } = event.latlng;
+ setLat(lat.toFixed(6));
+ setLng(lng.toFixed(6));
+ },
+ });
+
+ if (!lat || !lng) return null;
+
+ return ;
+}
export default function AddPlace() {
const navigate = useNavigate();
@@ -35,7 +50,7 @@ export default function AddPlace() {
}
if (isNaN(latNumber) || isNaN(lngNumber)) {
- setError("Please enter valid numbers for latitude and longitude.");
+ setError("Please select a valid location on the map.");
return;
}
@@ -68,6 +83,8 @@ export default function AddPlace() {
}
};
+ const defaultCenter = lat && lng ? [Number(lat), Number(lng)] : [59.3293, 18.0686];
+
return (
Add a new place
@@ -105,6 +122,21 @@ export default function AddPlace() {
+
+ Pick location on map *
+
+
+
+
+
+
- Pick location on map *
+ Pick the location on the map *
+
@@ -159,6 +179,60 @@ export default function AddPlace() {
+
+ Features
+
+
+
+ Changing table
+
+
+
+
+ Private room
+
+
+
+
+ Stroller access
+
+
+
+
+ Accessible
+
+
+
+
+ Cleanliness
+
+
+
{error && {error}}
@@ -175,7 +249,7 @@ const Wrapper = styled.main`
const Form = styled.form`
display: grid;
- gap: 12px;
+ gap: 16px;
label {
display: grid;
@@ -204,7 +278,7 @@ const MapSection = styled.div`
gap: 8px;
`;
-const MapLabel = styled.p`
+const MapText = styled.p`
margin: 0;
font-size: 14px;
`;
@@ -215,6 +289,23 @@ const Row = styled.div`
grid-template-columns: 1fr 1fr;
`;
+const FeatureSection = styled.div`
+ display: grid;
+ gap: 10px;
+`;
+
+const FeatureHeading = styled.h2`
+ margin: 0;
+ font-size: 18px;
+`;
+
+const CheckboxLabel = styled.label`
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+`;
+
const Error = styled.p`
color: red;
margin: 0;
diff --git a/frontend/src/pages/PlaceDetails.jsx b/frontend/src/pages/PlaceDetails.jsx
index d0fa3c7fc..d512f554a 100644
--- a/frontend/src/pages/PlaceDetails.jsx
+++ b/frontend/src/pages/PlaceDetails.jsx
@@ -108,6 +108,7 @@ export default function PlaceDetails() {
Private room: {place.features?.privateRoom ? "Yes" : "No"}
Stroller access: {place.features?.strollerAccess ? "Yes" : "No"}
Accessible: {place.features?.accessible ? "Yes" : "No"}
+ Cleanliness: {place.features?.clean ? "Yes" : "No"}
From b2d93e8c0312cd16800384f766226d478243c6c0 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 11 Mar 2026 13:43:13 +0100
Subject: [PATCH 38/67] Added additional features to AddPlace
---
frontend/src/pages/AddPlace.jsx | 35 ++++++++++++++++++++++++-----
frontend/src/pages/PlaceDetails.jsx | 10 +++++----
2 files changed, 35 insertions(+), 10 deletions(-)
diff --git a/frontend/src/pages/AddPlace.jsx b/frontend/src/pages/AddPlace.jsx
index e46047ee9..0a1ae49a8 100644
--- a/frontend/src/pages/AddPlace.jsx
+++ b/frontend/src/pages/AddPlace.jsx
@@ -38,6 +38,8 @@ export default function AddPlace() {
privateRoom: false,
strollerAccess: false,
accessible: false,
+ disposableMats: false,
+ diaperBags: false,
clean: false,
});
@@ -180,7 +182,7 @@ export default function AddPlace() {
- Features
+ Features
- Changing table
+ Changing table available
- Private room
+ Baby lounge available
@@ -209,7 +211,7 @@ export default function AddPlace() {
checked={features.strollerAccess}
onChange={handleFeatureChange}
/>
- Stroller access
+ Stroller friendly
@@ -221,6 +223,27 @@ export default function AddPlace() {
/>
Accessible
+
+
+
+ Disposable changing mats available
+
+
+
+
+
+ Diaper disposal bags available
+
Features
- - Changing table: {place.features?.changingTable ? "Yes" : "No"}
- - Private room: {place.features?.privateRoom ? "Yes" : "No"}
- - Stroller access: {place.features?.strollerAccess ? "Yes" : "No"}
- - Accessible: {place.features?.accessible ? "Yes" : "No"}
+ - Changing table available: {place.features?.changingTable ? "Yes" : "No"}
+ - Baby lounge available: {place.features?.babyLounge ? "Yes" : "No"}
+ - Stroller friendly: {place.features?.strollerAccess ? "Yes" : "No"}
+ - Wheelchair accessible: {place.features?.accessible ? "Yes" : "No"}
+ - Disposable changing mats available: {place.features?.disposableMats ? "Yes" : "No"}
+ - Diaper disposal bags available: {place.features?.diaperBags ? "Yes" : "No"}
- Cleanliness: {place.features?.clean ? "Yes" : "No"}
From 1efc99662ead0112b1cb584f6f3ae673315f4082 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 11 Mar 2026 13:46:51 +0100
Subject: [PATCH 39/67] Added additional features to Place in backend
---
backend/src/models/Place.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/backend/src/models/Place.js b/backend/src/models/Place.js
index 18be5f728..f4939e2e0 100644
--- a/backend/src/models/Place.js
+++ b/backend/src/models/Place.js
@@ -37,9 +37,11 @@ const placeSchema = new mongoose.Schema(
features: {
changingTable: { type: Boolean, default: true },
- privateRoom: { type: Boolean, default: false },
+ babyLounge: { type: Boolean, default: false },
strollerAccess: { type: Boolean, default: false },
accessible: { type: Boolean, default: false },
+ disposableMats: { type: Boolean, default: false },
+ diaperBags: { type: Boolean, default: false },
clean: { type: Boolean, default: false },
},
From 0aa4a37d9173640c018df21842cac1c22578d227 Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 11 Mar 2026 14:04:59 +0100
Subject: [PATCH 40/67] fixed checkboxes in Addplace
---
frontend/src/pages/AddPlace.jsx | 58 ++++++++++++++++++++++-----------
1 file changed, 39 insertions(+), 19 deletions(-)
diff --git a/frontend/src/pages/AddPlace.jsx b/frontend/src/pages/AddPlace.jsx
index 0a1ae49a8..727724eac 100644
--- a/frontend/src/pages/AddPlace.jsx
+++ b/frontend/src/pages/AddPlace.jsx
@@ -35,7 +35,7 @@ export default function AddPlace() {
const [features, setFeatures] = useState({
changingTable: false,
- privateRoom: false,
+ babyLounge: false,
strollerAccess: false,
accessible: false,
disposableMats: false,
@@ -104,7 +104,8 @@ export default function AddPlace() {
}
};
- const mapCenter = lat && lng ? [Number(lat), Number(lng)] : [59.3293, 18.0686];
+ const mapCenter =
+ lat && lng ? [Number(lat), Number(lng)] : [59.3293, 18.0686];
return (
@@ -155,7 +156,12 @@ export default function AddPlace() {
attribution="© OpenStreetMap contributors"
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
-
+
@@ -182,77 +188,76 @@ export default function AddPlace() {
- Features
+ Features
+ Changing table available
- Changing table available
+ Baby lounge available
- Baby lounge available
+ Stroller friendly
- Stroller friendly
+ Wheelchair accessible
- Accessible
-
+
+ Disposable changing mats available
- Disposable changing mats available
-
+ Diaper disposal bags available
- Diaper disposal bags available
+ onChange={handleFeatureChange}
+ />
+ Cleanliness
- Cleanliness
@@ -280,7 +285,7 @@ const Form = styled.form`
font-size: 14px;
}
- input,
+ input[type="text"],
select {
padding: 8px 10px;
border-radius: 8px;
@@ -323,10 +328,25 @@ const FeatureHeading = styled.h2`
`;
const CheckboxLabel = styled.label`
- display: flex;
+ display: flex !important;
+ justify-content: space-between;
align-items: center;
- gap: 8px;
- font-size: 14px;
+ width: 100%;
+ font-size: 15px;
+ cursor: pointer;
+ padding: 6px 0;
+
+ span {
+ flex: 1;
+ }
+
+ input[type="checkbox"] {
+ width: 16px;
+ height: 16px;
+ margin: 0;
+ padding: 0;
+ flex-shrink: 0;
+ }
`;
const Error = styled.p`
From 6c35195d0c46a1d04a038f7bd6a79a30b3d9fdce Mon Sep 17 00:00:00 2001
From: Iris de Gracia Zhang
Date: Wed, 11 Mar 2026 14:36:21 +0100
Subject: [PATCH 41/67] improved styling and removed log in Navbar
---
frontend/src/components/Navbar.jsx | 11 +++--
frontend/src/pages/AddPlace.jsx | 78 ++++++++++++++++++++----------
2 files changed, 60 insertions(+), 29 deletions(-)
diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx
index 5e9515d99..441eba1de 100644
--- a/frontend/src/components/Navbar.jsx
+++ b/frontend/src/components/Navbar.jsx
@@ -12,14 +12,14 @@ export default function Navbar() {
{isLoggedIn && Home}
- {isLoggedIn && Add place}
+ {isLoggedIn && Add a place}
{isLoggedIn ? (
Log out
) : (
- Log in
+
)}
@@ -30,14 +30,17 @@ const Header = styled.header`
display: flex;
align-items: center;
justify-content: space-between;
- padding: 12px 16px;
+
+ padding: 20px 40px;
border-bottom: 1px solid #eee;
`;
const Brand = styled(Link)`
+ font-size: 28px;
font-weight: 700;
text-decoration: none;
- color: inherit;
+ color: black;
+ letter-spacing: -0.5px;
`;
const Nav = styled.nav`
diff --git a/frontend/src/pages/AddPlace.jsx b/frontend/src/pages/AddPlace.jsx
index 727724eac..55c888b71 100644
--- a/frontend/src/pages/AddPlace.jsx
+++ b/frontend/src/pages/AddPlace.jsx
@@ -109,28 +109,28 @@ export default function AddPlace() {
return (
- Add a new place
+ Add a new place