Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • ludda756/TDDD27
1 result
Show changes
Commits on Source (2)
Showing
with 362 additions and 246 deletions
Ludvig Damberg (ludda756)
Ludvig Hillert (ludhi842)
We've created a social media applicaiton that is reddit-like. This is meant to have similar functionality to reddit, built with the MERN stack together with some usefull libraries, managed through NodeJS. Ofcourse this wont be an equal to reddit but the purpose is to create something in acceptable quality with similar basic functionality.
During the project we have updated through this gitlab, ofcourse. But also through github since we both think its smoother and are more "used" and comfortable using it. So to get a little better detail over some of the commits/pushes and commenting, please check out the github-repo which will be linked below.
Github-repo: https://github.com/ludvigdamberg/TDDD27
......@@ -3,7 +3,6 @@ const express = require('express')
const cors = require('cors')
require('dotenv').config()
const Routes = require('./routes/routes')
const { errorHandler, notFound } = require('./middlewares/errorMiddleware')
const checkUserExists = require('./middlewares/MulterMiddleware')
......@@ -14,8 +13,6 @@ app.use(express.json())
app.use(express.static("public"))
app.use(cors())
app.use(Routes)
app.use(notFound)
app.use(errorHandler)
const PORT = process.env.PORT || 5000
......
/*const jwt = require('jsonwebtoken');
const config = require('config');
require('dotenv').config()
const auth = (req, res, next) => {
// Get token from header
const token = req.headers['Authorization'];
console.log(token)
// Check if not token
if (!token) {
return res.status(401).json({ msg: 'No token, authorization denied' });
}
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Add user from payload
req.user = decoded.user;
next();
} catch (err) {
res.status(401).json({ msg: 'Token is not valid' });
}
}
module.exports = auth;
*/
\ No newline at end of file
const notFound = (req,res,next) => {
const error = new Error(`Not found ${req.originalUrl}`)
res.status(400)
next(error)
}
const errorHandler = (err,req,res,next) => {
const statusCode = res.statusCode === 200 ? 500 : res.statusCode
res.status(statusCode)
res.json({
message:err.message,
stack: process.env.NODE.ENV === "production" ? null : err.stack,
})
}
module.exports = {notFound,errorHandler}
\ No newline at end of file
const mongoose = require('mongoose')
const jwt = require('jsonwebtoken')
require('dotenv').config()
const passwordComplexity = require('joi-password-complexity')
const bcrypt = require('bcrypt')
const userSchema = new mongoose.Schema({
......
backend/public/uploads/043edabc-bff2-4ccf-b2bd-401e86ccc6a5_.jpg

1.19 MiB

backend/public/uploads/2520fee2-e0e4-4188-842f-42b725765aef_.jpg

1.05 MiB

......@@ -7,7 +7,6 @@ const multer = require('multer')
const generateToken = require('../utils/generateToken')
const asyncHandler = require('express-async-handler')
const router = Router()
const auth = require('../middlewares/auth')
const jwt = require('jsonwebtoken');
const fs = require('fs')
const commentModel = require('../models/comment')
......@@ -17,7 +16,7 @@ require('dotenv').config()
//Log in
//Logga in
router.post('/login', async (req, res) => {
const { email, password } = req.body
......@@ -40,7 +39,8 @@ router.post('/login', async (req, res) => {
}
})
//Register
//Registrering
router.post("/register", uploadMiddleware.single("photo"), asyncHandler(async (req, res) => {
const email = req.body.email
......@@ -72,13 +72,11 @@ router.post("/register", uploadMiddleware.single("photo"), asyncHandler(async (r
}
}))
//Create post
//Skapa inlägg
router.post("/post", uploadMiddleware.single("photo"), (req, res) => {
const photo = req.file.filename
const post = new postModel(
{
name: req.body.name,
......@@ -96,20 +94,20 @@ router.post("/post", uploadMiddleware.single("photo"), (req, res) => {
})
//Get posts
//Hämta inlägg
router.get('/posts', async (req, res) => {
const loads = req.headers.loads
console.log(loads)
console.log(loads)
if (req.headers.search.length == 0) {
const posts = await postModel.find().limit(loads*2).exec()
const posts = await postModel.find().limit(loads * 2).exec()
return res.send(posts)
} else if(req.headers.search.length > 0){
} else if (req.headers.search.length > 0) {
const search = req.headers.search; // Get the search query from the request query parameters
const search = req.headers.search;
console.log(search)
const posts = await postModel.find({
......@@ -118,36 +116,37 @@ console.log(loads)
{ recipe: { $regex: search, $options: "i" } }
// add as many components as needed
],
}).limit(loads*2).exec()
}).limit(loads * 2).exec()
return res.send(posts)
}
})
//Hämta resusltat i databasen utefter en sökning
router.get('/search', async (req, res) => {
const search = req.headers.search; // Get the search query from the request query parameters
console.log(search)
const posts = await postModel.find({
$or: [
{ name: { $regex: searchString, $options: "i" } },
{ recipe: { $regex: searchString, $options: "i" } }
// add as many components as needed
],
})
return res.send(posts)
const search = req.headers.search;
const posts = await postModel.find({
$or: [
{ name: { $regex: searchString, $options: "i" } },
{ recipe: { $regex: searchString, $options: "i" } }
],
})
return res.send(posts)
})
//get profile posts
//Hämta profilinlägg
router.get('/profilePosts', async (req, res) => {
const userId = req.headers.author
......@@ -156,23 +155,21 @@ router.get('/profilePosts', async (req, res) => {
.populate('author')
.exec()
.then(posts => {
res.json(posts)
res.send(posts)
})
})
//fetch profile
//Hämta specifik profil
router.get('/profile', (req, res) => {
// Get the user's token from local storage
const token = req.headers.authorization.split(' ')[1];
// Use jwt.verify to decode the token and get the user ID
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
// If the token is invalid, send an error response
return res.status(401).json({ message: 'Invalid token' });
}
const userId = decoded.userId;
// If the token is valid, use the user ID to fetch the user data from the database
userModel.findById(userId)
......@@ -185,6 +182,8 @@ router.get('/profile', (req, res) => {
});
});
//Ta bort ett inlägg och radera bild ur databasen
router.post('/deletePost', async (req, res) => {
......@@ -202,7 +201,7 @@ router.post('/deletePost', async (req, res) => {
})
// adding user id to upvote list
// Upvote, lägger till en användares id i en array
router.put('/upvote', async (req, res) => {
console.log(req.body)
......@@ -222,27 +221,17 @@ router.put('/upvote', async (req, res) => {
post.upvotes.splice(index, 1)
await post.save()
return res.json("upvote deleted")
}else{
post.upvotes.push(req.body.upvote)
await post.save()
res.send(post)
} else {
post.upvotes.push(req.body.upvote)
await post.save()
res.send(post)
}
})
router.get("/api/posts/search", (req, res) => {
const search = req.headers.search; // Get the search query from the request query parameters
postModel.find({ $text: { $search: search } }, (err, posts) => {
if (err) {
console.log(err);
return res.status(500).send("Internal server error");
}
return res.json(posts); // Send the matching posts back to the frontend
});
});
})
//kommentera ett inlägg
router.post("/comment", (req, res) => {
console.log(req.body)
......@@ -255,10 +244,11 @@ router.post("/comment", (req, res) => {
})
comment.save()
.then(console.log("comment saved successfully"))
res.json(comment)
res.json(comment)
})
//spara kommentaren i inläggsmodellen
router.put('/savecomment', async (req, res) => {
console.log(req.body)
......@@ -269,7 +259,7 @@ router.put('/savecomment', async (req, res) => {
if (!post) {
return res.json("Not a valid post")
}else{
} else {
const comment = req.body.comment_id
......@@ -282,20 +272,34 @@ router.put('/savecomment', async (req, res) => {
res.send("Success")
}
})
//hämta kommentarer för ett specifikt inlägg
router.get('/getcomments', async (req, res) => {
const postId = req.headers.author
postModel.find({ author: postId })
.populate('comments')
.exec()
.then(comments => {
res.json(comments)
const postId = req.headers.postid
})
})
const post = await postModel.findById(postId);
if (!post) {
return res.send("Somethings up bud!");
}
const commentIds = post.comments
console.log(commentIds)
const comments = []
for (const commentId of commentIds) {
const comment = await commentModel.findById(commentId);
if (comment) {
comments.push(comment);
}
}
res.send(comments)
})
......
......@@ -10,14 +10,14 @@ import buttons from '../styles/buttons.module.css'
const Posts = () => {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(false)
const [loading, setLoading] = useState(true)
const [drinks, setDrinks] = useState([])
const [loads, setLoads] = useState(1)
const [search, setSearch] = useState("")
const [searchChange, setSearchChange] = useState()
const [profileData, setProfileData] = useState()
const [comment, setComment] = useState("")
const [commentData, setCommentData] = useState()
const [commentData, setCommentData] = useState([])
const [showComments, setShowComments] = useState(false)
const get_profile = () => {
......@@ -34,18 +34,18 @@ const Posts = () => {
setProfileData(res.data)
})
setLoading(false)
setTimeout(() => {
setLoading(false)
}, 1000)
}
const loadPosts = () => {
setLoading(true)
console.log(loads)
axios.get("http://localhost:5000/posts", { headers: { loads: loads, search: search } })
.then((res) => {
setPosts(res.data)
setLoading(false)
})
setLoads(loads + 1)
}
......@@ -55,7 +55,7 @@ const Posts = () => {
get_profile()
}, [])
const handleChange = (e) => {
......@@ -83,42 +83,43 @@ const Posts = () => {
axios.post("http://localhost:5000/comment", { profile_name: profileData.username, profile_picture: profileData.photo, comment: comment })
.then((res) => {
console.log(res.data._id)
axios.put("http://localhost:5000/savecomment", {post_id: id, comment_id: res.data._id })
.then((res) => {
console.log(res.data)
}).catch(err => console.log(err))
console.log(res.data._id)
axios.put("http://localhost:5000/savecomment", { post_id: id, comment_id: res.data._id })
.then((res) => {
console.log(res.data)
}).catch(err => console.log(err))
}).catch(err => console.log(err))
}
const save_comment = (id) => {
const get_comments = (id) => {
console.log(id)
setLoading(true)
console.log(commentData)
/* axios.put("http://localhost:5000/savecomment", {post_id: id, comment_id: comment_id._id })
axios.get("http://localhost:5000/getcomments", { headers: { postId: id } })
.then((res) => {
console.log(res.data)
}).catch(err => console.log(err))*/
setCommentData(res.data)
setShowComments(true)
})
setTimeout(() => {
setLoading(false)
}, 1000)
}
if (loading) {
return (
<div>
<Loading />
</div>
)
} else
return (
return (
<div>
<input className={styles.searchbar}
......@@ -156,32 +157,49 @@ const Posts = () => {
))}
</ul>
</div>
</div>
</div>
<div>
<div className={styles.description}><h3>Description:</h3>{post.description} </div>
</div>
</div>
</div>
<div>
<input type='text' className={styles.comment_input} onChange={(e) => setComment(e.target.value)} />
<button onClick={() => handleComment(post._id)} className={styles.comment_button} >Comment</button>
<div className={styles.comments}>
{showComments ? (
<div >HELLO
<button onClick={() => setShowComments(false)}>hide comments</button>
</div>
<div>
<input type='text' className={styles.comment_input} onChange={(e) => setComment(e.target.value)} />
<button onClick={() => handleComment(post._id)} className={styles.comment_button} >Comment</button>
<div className={styles.comments}>
{showComments ? (
<div >
<button className={buttons.button3} onClick={() => setShowComments(false)}>hide comments</button>
{loading ? (<></>) : commentData.map((comment) => {
if (post.comments.includes(comment._id)) {
return(
<>
<div className={styles.comment_header}>
<img className={styles.posts_header_img} src={`http://localhost:5000/uploads/${comment.profile_picture}`} />
<p>{comment.profile_name}</p>
</div>
<div className={styles.comment}>
<p>{comment.comment}</p>
</div>
</>
)}})}
</div>
) : (
<> <button onClick={() => setShowComments(true)}>show comments</button></>
<> <button className={buttons.button3} onClick={() => get_comments(post._id)}>show comments</button></>
)}
</div>
</div>
</div>
</div>
</div>
)
})}</div>
......
......@@ -29,10 +29,10 @@ const ProfilePosts = () => {
)
.then((res) => {
setPosts(res.data);
}).catch(err => console.log(err))
setLoading(false)
setTimeout(() => {
setLoading(false)
}, 1000)
}
const deletePost = async (image, id) => {
......@@ -48,26 +48,26 @@ const ProfilePosts = () => {
}
useEffect(() => {
setLoading(true)
loadPosts()
}, [])
if (loading) {
return (
<div>
<Loading />
<></>
</div>
)
} else
return (
<div className={styles.container}>
<div>{posts.map((post) => {
<div>{posts.map((post,index) => {
return (
<div className={styles.wrapper}>
<div className={styles.posts_container} key={post._id}>
<div className={styles.post_img_container}>
<img className={styles.img} src={`http://localhost:5000/uploads/${post.photo}`} />
</div>
<div className={styles.wrapper} key={post._id}>
<div className={styles.posts_container} >
<img className={styles.post_img_container2} src={`http://localhost:5000/uploads/${post.photo}`} />
<div>
<h1>{post.name}</h1>
<div >
......
......@@ -8,6 +8,7 @@ import { FaArrowLeft } from 'react-icons/fa';
import { Link } from 'react-router-dom'
import axios from 'axios'
import { useNavigate } from 'react-router-dom'
import Loading from '../components/Loading'
const Feed = () => {
const [profileData, setProfileData] = useState()
......@@ -21,7 +22,7 @@ const Feed = () => {
localStorage.removeItem("token")
setIsLoggedIn(false)
navigate('/')
}
}
const fetchProfile = async () => {
......@@ -35,27 +36,30 @@ const Feed = () => {
}
}).then((res) => {
setProfileData(res.data);
setLoading(false)
}).catch(err => console.log(err))
setTimeout(() => {
setLoading(false)
}, 1000)
}
const checkLoggedIn = async () => {
const token = localStorage.getItem('token');
if (!token) {
logout()
return;
logout()
return;
}
const response = await axios.get('http://localhost:5000/profile', {
headers: { Authorization: `Bearer ${token}` },
headers: { Authorization: `Bearer ${token}` },
}).then((res) => {
setIsLoggedIn(true);
setIsLoggedIn(true);
}).catch(err => {
localStorage.removeItem("token")
logout()
localStorage.removeItem("token")
logout()
})
}
}
useEffect(() => {
......@@ -73,10 +77,14 @@ const Feed = () => {
}
}
if (loading == false) {
if (loading === true) {
return (
<Loading />
)
}
else {
return (
<div className={styles.feed}>
<div className={styles.header}>
......@@ -99,9 +107,9 @@ const Feed = () => {
</div>
)
} else {
return (<div>loading</div>)
}
}
export default Feed
\ No newline at end of file
......@@ -8,13 +8,42 @@ import ProfilePosts from '../components/ProfilePosts'
import Loading from '../components/Loading';
import { FaArrowLeft } from 'react-icons/fa';
import buttons from '../styles/buttons.module.css'
import jwt_decode from 'jwt-decode';
const Profile = () => {
const [profileData, setProfileData] = useState()
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [isLoggedIn, setIsLoggedIn] = useState(false);
const navigate = useNavigate();
const [posts, setPosts] = useState()
let upvotes = 0
const loadPosts = async () => {
const token = localStorage.getItem('token');
const decodedToken = jwt_decode(token);
const author = decodedToken.userId;
console.log(decodedToken.userId)
await axios.get('http://localhost:5000/profilePosts', { headers: { author: `${author}` } },
)
.then((res) => {
setPosts(res.data);
}).catch(err => console.log(err))
setTimeout(() => {
setLoading(false)
}, 1000)
}
const checkLoggedIn = async () => {
......@@ -35,10 +64,8 @@ const Profile = () => {
const fetchProfile = async () => {
setLoading(true)
const token = localStorage.getItem('token');
console.log(token)
axios.get('http://localhost:5000/profile', {
headers: {
......@@ -49,7 +76,6 @@ const Profile = () => {
}).catch(err => console.log(err))
setLoading(false)
}
const logout = () => {
......@@ -61,55 +87,79 @@ const Profile = () => {
useEffect(() => {
checkLoggedIn()
fetchProfile()
loadPosts()
}, [])
if (!profileData) {
const count_upvotes = () => {
posts.map((post) => {
upvotes += post.upvotes.length
console.log(upvotes)
})
}
if (loading === true) {
return (
<Loading/>
<Loading />
)
} else if (profileData) {
console.log(profileData)
} else if (loading === false) {
count_upvotes()
return (
<>
<div className={styles.header}>
<div className={buttons.arrow}><Link to='/'><FaArrowLeft /> Home</Link></div>
<h1>Welcome to your profile page</h1>
<p>Redirect through these links</p>
<h1>Profile Page</h1>
<div className={styles.text_container}>
<Link style={{ textDecoration:'none'}} to='/feed'>
<Link style={{ textDecoration: 'none' }} to='/feed'>
<div className={buttons.button}>discover</div>
</Link>
<h3>|</h3>
<Link style={{ textDecoration:'none'}} to='/'>
<Link style={{ textDecoration: 'none' }} to='/'>
<div className={buttons.button}>home</div>
</Link>
<h3>|</h3>
<div className={buttons.button} onClick={logout}>log out</div>
</div>
</div>
<div className={buttons.arrow}><Link to='/'><FaArrowLeft /> Home</Link></div>
<div className={styles.profile}>
<img className={styles.picture} src={`http://localhost:5000/uploads/${profileData.photo}`} />
<div className={styles.info_container}>
<div className={styles.info_slot}>
<div> <h1>{posts.length}</h1></div>
<div> <h3>Posts</h3></div>
</div>
<div className={styles.info_slot}>
<div> <h1>{upvotes}</h1></div>
<div> <h3>Upvotes</h3></div>
</div>
</div>
</div>
<div className={styles.profile2}>
<h2>{profileData.username}</h2>
<p>{profileData.email}</p>
<div className={styles.text_container}>
<h3>Posts: 0</h3>
<h3>Upvotes: 0</h3>
</div>
</div>
<div>
</div>
<div>
</div> <h1>Your Posts:</h1>
<ProfilePosts />
</div>
<div className={styles.your}> <h1>Your posts</h1></div>
<ProfilePosts />
</>
)
}
......
.button {
font-size: 18px;
color: #000000;
color: #ffffff;
font-family: inherit;
font-weight: 800;
cursor: pointer;
......@@ -11,12 +11,13 @@
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1);
transition-duration: 400ms;
transition-property: color;
margin: 20px;
}
.button:focus,
.button:hover {
color: #7f9ec5;
color: #fff597;
}
.button:focus:after,
......@@ -33,7 +34,7 @@
position: absolute;
width: 0%;
height: 2px;
background-color: #7f9ec5;
background-color: #fff597;
transition-timing-function: cubic-bezier(0.25, 0.8, 0.25, 1);
transition-duration: 400ms;
transition-property: width, left;
......@@ -114,7 +115,8 @@
font-size: 18px;
line-height: normal;
height: 40px;
width: 100px;
width: 150px;
margin: 5px;
outline: none;
text-align: center;
transition: all 300ms cubic-bezier(.23, 1, 0.32, 1);
......
......@@ -237,6 +237,8 @@ outline: none;
border: solid 1px rgba(128, 128, 128, 0.644);
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
box-sizing: border-box;
padding: 10px;
}
.delete {
......@@ -272,7 +274,30 @@ outline: none;
font-family: 'Outfit', sans-serif;
}
.comment_header{
height: 60px;
display: flex;
align-items: center;
border: solid 0.5px grey;
width: 90%;
margin: auto;
box-sizing: border-box;
padding: 5px;
}
.comment{
display: flex;
padding: 5px;
box-sizing: border-box;
margin: auto;
width: 90%;
box-shadow: rgba(0, 0, 0, 0.25) 0 8px 15px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
background-color: rgba(209, 209, 209, 0.342);
}
.posts_container {
min-height: 300px;
width: 900px;
......@@ -313,6 +338,13 @@ outline: none;
margin-right: 10%;
}
.post_img_container2 {
width: 310px;
/* set width of image container */
height: 310px;
overflow: hidden;
object-fit: cover;
}
.post_img_container img {
......
......@@ -10,7 +10,7 @@ align-items: center;
.button {
appearance: none;
background-color: #ffffff;
background-color: #ffffffa1;
border: 0.125em solid #000000;
border-radius: 5px;
box-sizing: border-box;
......
......@@ -3,6 +3,7 @@
height: 100vh;
position: fixed;
background-color: rgba(0, 0, 0, 0.342);
}
.loadingspinner {
--square: 26px;
......
h1{
font-size: 50px;
text-align: center;
h1 {
font-size: 40px;
margin: 0;
}
h2{
h2 {
margin: 0;
}
h3{
h3 {
margin: 20px;
}
.picture {
width: 250px;
height: 250px;
border-radius: 50%;
width: 300px;
height: 300px;
border-radius: 5px;
object-fit: cover;
display: block;
overflow: hidden;
margin: 20px auto;
border: 1px solid rgb(156, 156, 156);
border: solid 1px rgb(139, 139, 139);
box-shadow: 0 2px 10px 0 rgba(107, 107, 107, 0.774);
}
.profile {
width: 100vw;
min-height: 30vh;
margin-top: 100px;
margin-left: 20px;
color: rgb(107, 107, 107);
display: flex;
}
.profile{
background: rgb(255, 255, 255);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-radius: 5px;
border: 1px solid rgb(156, 156, 156);
box-shadow: 0 8px 23px 0 rgba(37, 37, 37, 0.562);
text-align: center;
width: 40vw;
margin: 100px auto;
min-height: 50vh;
.profile2 {
width: 100vw;
min-height: 10vh;
margin-top: 20px;
margin-left: 20px;
color: rgb(107, 107, 107);
display: flex;
flex-direction: column;
}
.header{
.header {
max-width: 100vw;
min-height: 10vh;
background: #5cbbd8;
color: white;
height: 10vh;
margin: 0;
background-color: rgb(255, 255, 255);
box-sizing: border-box;
text-align: center;
display: flex;
align-items: center;
padding: 10px;
box-shadow: 0 2px 10px 0 rgba(107, 107, 107, 0.774);
}
.text_container{
.text_container {
display: flex;
flex-direction: row;
justify-content: center;
text-align: center;
justify-content: center;
align-items: center;
text-decoration: none;
margin-left: auto;
margin-right: 40px;
}
.text_container Link{
text-decoration: none;
.info_container{
display: flex;
height: 30vh;
}
.info_slot{
margin-left: 200px;
width: 500px;
height: 300px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: end;
box-sizing: border-box;
padding: 80px;
border: solid 1px rgb(173, 173, 173);
box-shadow: 0 2px 10px 0 rgba(107, 107, 107, 0.774);
border-radius: 5px;
}
.your{
text-align: center;
width: 100vw;
height: 5vh;
margin-top: 100px;
color: rgb(107, 107, 107);
}