Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
196a048
set up the basic folder structure frontend and backend
irisdgz Feb 18, 2026
735f720
updated the folder structure
irisdgz Feb 18, 2026
b7585fa
updated errorhandling
irisdgz Feb 23, 2026
d78320f
Implement user authentication and review functionality with JWT
irisdgz Feb 24, 2026
9e807b6
Refactor error handler comment and ensure consistent newline at end o…
irisdgz Feb 24, 2026
4859c8b
Refactor server setup by removing unused imports and simplifying code…
irisdgz Feb 24, 2026
41dde08
Reintroduce recalcRating utility and add validators file
irisdgz Feb 24, 2026
e2a7ec7
Add Home component and implement API calls for places; enhance error …
irisdgz Feb 25, 2026
d9ee592
Remove global.css, update package.json to include styled-components, …
irisdgz Feb 25, 2026
bb3e75e
Add Navbar component with styled components for navigation and logout…
irisdgz Feb 25, 2026
d332f0c
Add PlaceCard component with styled components for showing place deta…
irisdgz Feb 25, 2026
465d15e
Refactor Home component to improve loading and error handling; enhanc…
irisdgz Feb 26, 2026
460c4d2
Implement PlaceDetails page with API integration for fetching place a…
irisdgz Feb 26, 2026
c9107f2
Rename Brand in Navbar component
irisdgz Feb 26, 2026
d8e62a4
Add core components and pages for the application, including Filters,…
irisdgz Feb 26, 2026
24114a4
Remove unused pages: AddPlace, Home, Login, PlaceDetails, and Signup
irisdgz Feb 26, 2026
588a4a6
Refactor Home component
irisdgz Feb 26, 2026
830cca9
Update server port, add Google Fonts, and restructure pages; remove u…
irisdgz Feb 27, 2026
4e49b58
Implement authentication middleware and update Place model routes for…
irisdgz Feb 27, 2026
3ff66e1
same structure, cleaned up typos
irisdgz Feb 27, 2026
a1e7ab3
updated place.js
irisdgz Feb 27, 2026
ef0edfe
Updated Place and User models for improved schema structure
irisdgz Feb 27, 2026
bf84ea5
Implement login functionality and update Navbar for authentication state
irisdgz Feb 27, 2026
39dab48
Added authentication store and update routing for login and signup pages
irisdgz Feb 27, 2026
8c6e538
Add ProtectedRoute component for route protection based on authentica…
irisdgz Feb 27, 2026
6d19cfd
Add ProtectedRoute to app.jsx
irisdgz Feb 27, 2026
d356449
update navbar, hide home and add place
irisdgz Feb 27, 2026
d42636d
Added ProtectedRoute for AddPlace component
irisdgz Feb 27, 2026
74f26d6
integrate review function in PlaceDetails
irisdgz Feb 27, 2026
cb2435b
fix: remove commented-out code in AddPlace component
irisdgz Mar 9, 2026
e44abe6
improved state management and error handling in AddPlace and PlaceDet…
irisdgz Mar 10, 2026
be1712f
updated scripts in package.json
irisdgz Mar 10, 2026
11b0e82
package.json dependencies and import leaflet CSS in main.jsx
irisdgz Mar 10, 2026
86d2d56
added test Maps to see it will work
irisdgz Mar 10, 2026
a891132
replaced MapTest with PlacesMap for real mapping
irisdgz Mar 11, 2026
4d8adce
Added Leaflet imports and LocationPicker
irisdgz Mar 11, 2026
95a1b9f
Added feature checkboxes to AddPlace
irisdgz Mar 11, 2026
b2d93e8
Added additional features to AddPlace
irisdgz Mar 11, 2026
1efc996
Added additional features to Place in backend
irisdgz Mar 11, 2026
0aa4a37
fixed checkboxes in Addplace
irisdgz Mar 11, 2026
6c35195
improved styling and removed log in Navbar
irisdgz Mar 11, 2026
287d82e
improved global styles, styleing of home, updated main, fixed typos i…
irisdgz Mar 11, 2026
028c468
improved responsiveness in Signup and Login pages
irisdgz Mar 12, 2026
daa55b9
adjusted Title styling for better consistency on AddPlace page
irisdgz Mar 12, 2026
c5fe811
added new features to PlaceCard to match backend and addplace
irisdgz Mar 12, 2026
24a85c8
improved responsiveness in navbar
irisdgz Mar 12, 2026
8668d47
update README with project details and features; modify Home page tit…
irisdgz Mar 12, 2026
54b6865
removed unused files
irisdgz Mar 12, 2026
ae1f6fd
updated the comments i various files
irisdgz Mar 13, 2026
6fb012e
updated comments
irisdgz Mar 13, 2026
e8dc55b
improvee comments for clarity in places.js and AddPlace.jsx
irisdgz Mar 13, 2026
35abf81
Add dotenv to backend dependencies
irisdgz Mar 13, 2026
be4bde6
Add express-list-endpoints dependency
irisdgz Mar 13, 2026
3c10809
updated serverjs and app js backend
irisdgz Mar 13, 2026
bf37f55
Remove Babel and use native Node ESM
irisdgz Mar 13, 2026
4e42e04
Add auth dependencies
irisdgz Mar 13, 2026
3aecd00
Fix Leaflet marker icons in production
irisdgz Mar 13, 2026
2ec566d
remove demo comments
irisdgz Mar 25, 2026
0396ce7
Refactor API_BASE_URL assignment in auth, places, and reviews modules
irisdgz Mar 25, 2026
8c0cbdf
Fix API_BASE_URL usage and improve error handling in auth and places …
irisdgz Mar 25, 2026
218f773
Implement filters for places in Home component and clean up code
irisdgz Mar 26, 2026
67b5eb7
Add VITE_API_URL to .env.example for environment configuration
irisdgz Mar 26, 2026
ecc956c
Fix title formatting and update meta description in index.html
irisdgz Mar 26, 2026
4224171
Enhance PlacesMap accessibility and improve button styles in GlobalSt…
irisdgz Mar 26, 2026
3bb3113
Fix typo in signup route and implement login route with error handling
irisdgz Mar 26, 2026
7c7746f
fixed export before imports, typos and double closing
irisdgz Mar 26, 2026
deddd67
Finalizing Readme
irisdgz Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
# Final Project
# MiniStops

Replace this readme with your own information about your project.
A web application that helps parents quickly find baby-changing facilities nearby.

Start by briefly describing the assignment in a sentence or two. Keep it short and to the point.
## View it live
- Frontend: https://project-final-irisdgz-1.onrender.com
- Backend API: https://project-final-irisdgz.onrender.com

## The problem
As a parent of a toddler, finding a clean and accessible place to change a diaper can be tricky. MiniStops makes this easier by showing nearby changing facilities and allowing parents to share information about them.

Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next?
## Features
Users can:
- Sign up and log in
- Add new places with features and a map-picked location
- Filter places by city, category, or amenities
- View all places on an interactive map
- Read and leave reviews with ratings

## View it live
## Tech stack
**Frontend:** React, React Router, Zustand, Styled-components, React Leaflet

**Backend:** Node.js, Express, MongoDB with Mongoose, JWT authentication

## How I built it
I started by defining the core features (authentication, map, reviews, adding places), built the backend API first, then connected the frontend to it.

One interesting challenge was using React Leaflet's `useMapEvents` hook to let users click directly on the map to pick a location for a new place.

Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about.
## If I had more time
- Allow browsing and viewing places without needing to log in
- Show the user's current location on the map
- Allow editing and deleting reviews
- Add photos for places
- Push notifications for new reviews
Empty file added backend/.env.example
Empty file.
16 changes: 8 additions & 8 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
"name": "project-final-backend",
"version": "1.0.0",
"description": "Server part of final project",
"type": "module",
"scripts": {
"start": "babel-node server.js",
"dev": "nodemon server.js --exec babel-node"
"start": "node src/server.js",
"dev": "nodemon src/server.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"bcrypt": "^6.0.0",
"cors": "^2.8.5",
"dotenv": "^17.3.1",
"express": "^4.17.3",
"express-list-endpoints": "^7.1.0",
"jsonwebtoken": "^9.0.3",
"mongoose": "^8.4.0",
"nodemon": "^3.0.1"
}
}
}
22 changes: 0 additions & 22 deletions backend/server.js

This file was deleted.

24 changes: 24 additions & 0 deletions backend/src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import "dotenv/config";
import express from "express";
import cors from "cors";
import listEndpoints from "express-list-endpoints";

import routes from "./routes/index.js";
import { errorHandler } from "./middleware/errorHandler.js";

const app = express();

app.use(cors());
app.use(express.json());

app.get("/", (req, res) => {
res.json({
message: "Baby Changing Places API",
endpoints: listEndpoints(app),
});
});

app.use(routes);
app.use(errorHandler);

export default app;
13 changes: 13 additions & 0 deletions backend/src/db/connectDB.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
29 changes: 29 additions & 0 deletions backend/src/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import jwt from "jsonwebtoken";

export const authenticateUser = (req, res, next) => {
try {
const header = req.headers.authorization || "";
const [type, token] = header.split(" ");

if (type !== "Bearer" || !token) {
return res.status(401).json({
success: false,
message: "Missing or invalid Authorization header",
});
}

if (!process.env.JWT_SECRET) {
return res.status(500).json({
success: false,
message: "Server misconfigured (JWT_SECRET missing)",
});
}

const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload;

next();
} catch (err) {
return res.status(401).json({ success: false, message: "Invalid or expired token" });
}
};
9 changes: 9 additions & 0 deletions backend/src/middleware/errorHandler.js
Original file line number Diff line number Diff line change
@@ -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",
});
};
58 changes: 58 additions & 0 deletions backend/src/models/Place.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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 },


location: {
type: {
type: String,
enum: ["Point"],
default: "Point",
required: true,
},
coordinates: {
type: [Number],
required: true,
validate: {
validator: (arr) =>
Array.isArray(arr) &&
arr.length === 2 &&
arr.every((n) => Number.isFinite(n)),
message: "location.coordinates must be [lng, lat] (two numbers)",
},
},
},

features: {
changingTable: { type: Boolean, default: true },
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 },
},

avgRating: { type: Number, default: 0, min: 0, max: 5 },
reviewCount: { type: Number, default: 0, min: 0 },

createdBy: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
},
{ timestamps: true }
);

placeSchema.index({ location: "2dsphere" });

export const Place = mongoose.model("Place", placeSchema);
16 changes: 16 additions & 0 deletions backend/src/models/Review.js
Original file line number Diff line number Diff line change
@@ -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 }
);


reviewSchema.index({ placeId: 1, userId: 1 }, { unique: true });

export const Review = mongoose.model("Review", reviewSchema);
30 changes: 30 additions & 0 deletions backend/src/models/User.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import mongoose from "mongoose";

const userSchema = new mongoose.Schema(
{
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
match: /.+\@.+\..+/,
},

username: {
type: String,
trim: true,
maxlength: 30,
},

passwordHash: {
type: String,
required: true,
},
},
{ timestamps: true }
);

userSchema.index({ email: 1 }, { unique: true });

export const User = mongoose.model("User", userSchema);
99 changes: 99 additions & 0 deletions backend/src/routes/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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;
const normalizedEmail = email?.toLowerCase().trim();

if (!normalizedEmail || !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: normalizedEmail });

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: normalizedEmail,
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;
Loading