-
Notifications
You must be signed in to change notification settings - Fork 30
Happy thoughts - Tilde #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
be10ac2
e6e31a8
d20f84a
e6c632f
62b6952
1ef5edd
f25f5da
7da754b
52659b0
53cebc7
87e97c0
fd8e9cf
27b50b0
f2da62b
0efc2c2
1de7a82
4c1805f
ee29461
bb01e32
ecb5968
5021a40
ce625de
3e3823a
3509abf
c189082
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# ✅ TODO: Connect Frontend & Backend for Happy Thoughts | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
}); | ||
} | ||
}; |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" }); | ||
} | ||
}; |
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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!