Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
53 changes: 53 additions & 0 deletions TODO.md

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to do list! I need to start getting more organised like this even when working solo!

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# ✅ TODO: Connect Frontend & Backend for Happy Thoughts

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⭐ Great way to organise your work.


## 🥇 Step 1: Finalize backend using MongoDB & Mongoose

- [x] Create a `Thought` model in `models/Thought.js`
- Fields:
[x]`message` (string, required, min/max length),
[x]`hearts`,
[x]`createdAt`
- [ ] Add update thought in frontend and backend
- [x] Seed the database with sample thoughts
- [x] Create route: `GET /thoughts` → return latest (e.g. 20)
- [x] Create route: `POST /thoughts` → save new thought
- [x] Create route: `PATCH /thoughts/:id/like` → increment hearts
- [x] Create route: `DELETE /thoughts/:id` → delete a thought
- [x] BONUS: `GET /thoughts?page=2` → use `.skip().limit()` for pagination
- [ ] BONUS: `GET /thoughts?category=joy` → filter with query params

---

## 🥈 Step 2: Add validation & error handling

- [x] Add Mongoose validation in the model (e.g. min/max message length)
- [x] Use `try/catch` in all routes
- [x] Return proper status codes (400, 404, 500) with `.status().json()
- [x] Return useful error messages for the frontend

---

## 🥉 Step 3: Connect frontend to the API

- [x] Change API URL in `happy-thoughts` frontend
- Update fetch requests:
- [x] GET /thoughts → display the thought list
- [x] POST /thoughts → send a new thought
- [x] PATCH /thoughts/:id/like → like a thought
- [x] DELETE /thoughts/:id → delete a thought
- [x] Show errors and loading states in the UI

## 🏁 Step 4: Deploy & manage environments

- [x] Deploy backend to Render
- [x] Deploy database to MongoDB Atlas
- [x] Add .env on Render with MONGO_URL
- [x] Update the frontend to point to deployed backend

## 🌈 Step 5: Stretch goals – once core functionality is working

- [ ] Add filtering using query parameters (/thoughts?tag=joy)
- [ ] Implement pagination using .skip() and .limit()
- [ ] Add infinite scroll to the frontend
- [ ] Sort thoughts with .sort() – by newest or most liked
- [ ] Group thoughts by category (if using categories)
154 changes: 154 additions & 0 deletions controllers/thoughtsController.js

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Only thing that could be nice is to add the different consts into a different util component to make it easier to find the right one. Sort of like you did in your routes file.

Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// controllers/thoughtsController.js
import { Thought } from "../models/Thought.js";

// GET /thoughts
export const getAllThoughts = async (req, res) => {
const {
minHearts,
after,
sortBy = "createdAt",
order = "desc",
page = 1,
limit = 10,
} = req.query;

const query = {};
if (minHearts) {
query.hearts = { $gte: parseInt(minHearts) };
}
if (after) {
query.createdAt = { $gt: new Date(after) };
}

const sortOrder = order === "desc" ? -1 : 1;
const skip = (parseInt(page) - 1) * parseInt(limit);

try {
const totalThoughts = await Thought.countDocuments(query);
const thoughts = await Thought.find(query)
.sort({ [sortBy]: sortOrder })
.skip(skip)
.limit(parseInt(limit));

res.json({
totalThoughts,
totalPages: Math.ceil(totalThoughts / limit),
currentPage: parseInt(page),
thoughts,
});
} catch (err) {
res.status(500).json({
success: false,
message: "Failed to get thoughts",
error: err.message,
});
}
};

// GET /thoughts/:id
export const getThoughtById = async (req, res) => {
try {
const thought = await Thought.findById(req.params.id);
if (!thought) return res.status(404).json({ message: "Thought not found" });
res.json(thought);
} catch (err) {
res.status(400).json({ message: "Invalid ID", error: err.message });
}
};

// POST /thoughts
export const createThought = async (req, res) => {
const { message } = req.body;

try {
const newThought = new Thought({
message,
createdBy: req.user.id,
});
const saved = await newThought.save();
res.status(201).json(saved);
} catch (err) {
res.status(400).json({
success: false,
message: "Failed to create thought",
error: err.message,
});
}
};

// PATCH /thoughts/:id/like

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great comments so that it is easy to look up!

export const likeThought = async (req, res) => {
try {
const updated = await Thought.findByIdAndUpdate(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering myself. Is update a good name? At first a thought no but then when I thought about it that it is in a function maybe it is overkill to have like updateThought or whatever. So I think it is a good name!

req.params.id,
{ $inc: { hearts: 1 } },
{ new: true }
);
if (!updated) return res.status(404).json({ message: "Thought not found" });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice error handling! Like all the rest you did!

res.json(updated);
} catch (err) {
res.status(400).json({
success: false,
message: "Failed to like thought",
error: err.message,
});
}
};

// PATCH /thoughts/:id – Update the message of a thought
export const updateThought = async (req, res) => {
const { id } = req.params;
const { message } = req.body;

try {
const thought = await Thought.findById(id);

if (!thought) {
return res.status(404).json({ message: "Thought not found" });
}

if (thought.createdBy.toString() !== req.user.id) {
return res
.status(403)
.json({ message: "Not authorized to update this thought" });
}

thought.message = message || thought.message;
const updated = await thought.save();
res.json(updated);
} catch (err) {
res.status(400).json({
success: false,
message: "Failed to update thought",
error: err.message,
});
}
};

// DELETE /thoughts/:id
export const deleteThought = async (req, res) => {
const { id } = req.params;

try {
const thought = await Thought.findById(id);

if (!thought) {
return res.status(404).json({ message: "Thought not found" });
}

if (thought.createdBy.toString() !== req.user.id) {
return res
.status(403)
.json({ message: "Not authorized to delete this thought" });
}

await thought.deleteOne();
res.json({ success: true, message: "Thought deleted", id });
} catch (err) {
res.status(400).json({
success: false,
message: "Failed to delete thought",
error: err.message,
});
}
};
12 changes: 6 additions & 6 deletions data.json → data/data.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
[
{
{
"_id": "682bab8c12155b00101732ce",
"message": "Berlin baby",
"hearts": 37,
"createdAt": "2025-05-19T22:07:08.999Z",
"__v": 0
},
{
"_id": "682e53cc4fddf50010bbe739",
"_id": "682e53cc4fddf50010bbe739",
"message": "My family!",
"hearts": 0,
"createdAt": "2025-05-22T22:29:32.232Z",
Expand All @@ -25,7 +25,7 @@
"message": "Newly washed bedlinen, kids that sleeps through the night.. FINGERS CROSSED 🤞🏼\n",
"hearts": 6,
"createdAt": "2025-05-21T21:42:23.862Z",
"__v": 0
"__v": 0
},
{
"_id": "682e45804fddf50010bbe736",
Expand Down Expand Up @@ -53,7 +53,7 @@
"message": "A god joke: \nWhy did the scarecrow win an award?\nBecause he was outstanding in his field!",
"hearts": 12,
"createdAt": "2025-05-20T20:54:51.082Z",
"__v": 0
"__v": 0
},
{
"_id": "682cebbe17487d0010a298b5",
Expand All @@ -74,7 +74,7 @@
"message": "Summer is coming...",
"hearts": 2,
"createdAt": "2025-05-20T15:03:22.379Z",
"__v": 0
"__v": 0
},
{
"_id": "682c706c951f7a0017130024",
Expand Down Expand Up @@ -118,4 +118,4 @@
"createdAt": "2025-05-19T22:07:08.999Z",
"__v": 0
}
]
]
21 changes: 21 additions & 0 deletions middleware/auth.js

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I think you did a bit different then I but it looks good!

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import jwt from "jsonwebtoken";

const JWT_SECRET = process.env.JWT_SECRET || "secret-key";

export const authenticateUser = (req, res, next) => {
const authHeader = req.headers.authorization;

if (!authHeader) {
return res.status(401).json({ message: "Access token missing" });
}

const token = authHeader.replace("Bearer ", "");

try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded; // du kan nu använda req.user.id i skyddade routes
next();
} catch (err) {
res.status(401).json({ message: "Invalid token" });
}
};
27 changes: 27 additions & 0 deletions models/Thought.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import mongoose from "mongoose";

const ThoughtSchema = new mongoose.Schema({
message: {
type: String,
required: [true, "A message is required"],
minlength: [5, "A message must be at least 5 characters long"],
maxlength: [140, "A message can't be longer than 140 characters"],
trim: true,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice with the trim!

},
hearts: {
type: Number,
default: 0,
min: [0, "Hearts cannot be negative"],
},
createdAt: {
type: Date,
default: Date.now,
},
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
});

export const Thought = mongoose.model("Thought", ThoughtSchema);
19 changes: 19 additions & 0 deletions models/User.js

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import mongoose from "mongoose";
import bcrypt from "bcryptjs";

const UserSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
},

password: { type: String, required: true },
username: { type: String },
});

UserSchema.pre("save", async function () {
this.password = await bcrypt.hash(this.password, 10);
});

export const User = mongoose.model("User", UserSchema);
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@
"@babel/core": "^7.17.9",
"@babel/node": "^7.16.8",
"@babel/preset-env": "^7.16.11",
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^4.17.3",
"express-list-endpoints": "^7.1.1",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.15.1",
"nodemon": "^3.0.1"
}
}
33 changes: 33 additions & 0 deletions routes/thoughts.js

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like how clear and gathered this is with all the routes

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// routes/thoughts.js
import express from "express";
import { authenticateUser } from "../middleware/auth.js";
import {
getAllThoughts,
getThoughtById,
createThought,
likeThought,
deleteThought,
updateThought,
} from "../controllers/thoughtsController.js";

const router = express.Router();

// GET /thoughts – List all thoughts
router.get("/", getAllThoughts);

// GET /thoughts/:id – Get one thought
router.get("/:id", getThoughtById);

// POST /thoughts – Create a new thought
router.post("/", authenticateUser, createThought);

// PATCH /thoughts/:id/like – Like a thought
router.patch("/:id/like", likeThought);

// PATCH /thoughts/:id – Update a thought
router.patch("/:id", authenticateUser, updateThought);

// DELETE /thoughts/:id – Delete a thought
router.delete("/:id", authenticateUser, deleteThought);

export default router;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you like export default more? I prefer the export const. Nothing wrong just different ways of doing it

Loading