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 ( + + + + <StyledLink to={`/places/${_id}`}>{name}</StyledLink> + + + {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