Deployed app here Doodler
Create an account and login to see the full features!
*Note - no extra work has been done to this apart from deployment. This is to give an accurate representation of what can be accomplished in a set time frame.
Doodler is a social site for users to create and share their doodles.
- Make sure mongoDB is running
mongod --dbpath ~/data/db
- Server directory
cd backend
yarn
yarn seed
yarn serve
- Front end
cd client
- Install front-end dependencies:
yarn
- Start front-end server:
yarn start
- Build a full-stack application by making your own backend and your own front-end
- Use a MongoDB and Node.js API using express framework to serve your data from a MongoDB database
- Consume your API with a separate front-end built with React
- Be a complete product
- Be deployed online so itβs publicly accessible
- MongoDB/Mongoose
- Express
- React.js (hooks)
- Node.js
- SCSS
- Bulma
- Axios
- Nodemon
- HTTP-proxy-middleware
- Bcrypt
- Body-parser
- jsonwebtoken
- Git/GitHub
While discussing ideas we found a canvas component that allows users to make a doodle. We knew we wanted this as the focus of our app and got to work.
We met and had some wireframes drawn up of how we want the app to look like.
We used Trello to track our progress and raise issues.
The doodles are referred as artwork in our schematics. They contain both embedded and reference relationships.
const commentSchema = new mongoose.Schema({
commentText: { type: String, required: false }, //! Sami change to false for star rating
rating: { type: Number, required: false, min: 1, max: 5 },
owner: { type: mongoose.Schema.ObjectId, ref: 'User', required: false },
username: { type: String , required: false }
},
{ timestamps: true
})
const favouriteSchema = new mongoose.Schema({
owner: { type: mongoose.Schema.ObjectId, ref: 'User', required: false }
},
{ timestamps: true }
)
const artworkSchema = new mongoose.Schema({
title: { type: String, required: true, maxlength: 60 },
description: { type: String, required: false, maxlength: 500 },
doodleData: { type: String, required: true },
owner: { type: mongoose.Schema.ObjectId, ref: 'User', required: true },
comments: [commentSchema],
favourites: [ favouriteSchema ]
},
{ timestamps: true }
)
Example of controller for liking / adding to favourites.
export const addLike = async (req, res) => {
// const currentUser = req.currentUser._id
try {
const { id } = req.params
const artworkToLike = await Artwork.findById(id)
if (!artworkToLike) {
throw new Error('π₯ no artwork found to like π₯ ')
}
const newLike = { owner: req.currentUser }
const favouritesArray = artworkToLike.favourites
const currentUserLoggedInId = JSON.stringify(req.currentUser._id)
const hasUserLikedBefore = await favouritesArray.find(item => JSON.stringify(item.owner._id) === currentUserLoggedInId)
console.log('π ~ file: artworkController.js ~ line 107 ~ hasUserLikedBefore', hasUserLikedBefore)
if (hasUserLikedBefore) {
const likeToRemove = await artworkToLike.favourites.id(hasUserLikedBefore._id)
await likeToRemove.remove()
await artworkToLike.save()
res.status(200).json( { message: 'Dis-liked!' })
} else if (!hasUserLikedBefore) {
artworkToLike.favourites.push(newLike)
await artworkToLike.save()
res.status(200).json( { message: 'liked!' })
}
} catch (err) {
console.log('π ~ file: artworkController.js ~ line 128 ~ error', err)
res.status(500).json( { message: err.message })
}
}
- We ran into a problem often with the payload size but managed to overcome this! By using lz-string: everything to make it work for you - pieroxy.net
- this changed the data from an object with line coordinates to a single string .
The like button was something I focused on, front and back-end. This will check if the user has liked previously and dislike if so, or add a like if not.
export const addLike = async (req, res) => {
// const currentUser = req.currentUser._id
try {
const { id } = req.params
const artworkToLike = await Artwork.findById(id)
if (!artworkToLike) {
throw new Error('π₯ no artwork found to like π₯ ')
}
const newLike = { owner: req.currentUser }
const favouritesArray = artworkToLike.favourites
const currentUserLoggedInId = JSON.stringify(req.currentUser._id)
const hasUserLikedBefore = await favouritesArray.find(item => JSON.stringify(item.owner._id) === currentUserLoggedInId)
console.log('π ~ file: artworkController.js ~ line 107 ~ hasUserLikedBefore', hasUserLikedBefore)
if (hasUserLikedBefore) {
const likeToRemove = await artworkToLike.favourites.id(hasUserLikedBefore._id)
await likeToRemove.remove()
await artworkToLike.save()
res.status(200).json( { message: 'Dis-liked!' })
} else if (!hasUserLikedBefore) {
artworkToLike.favourites.push(newLike)
await artworkToLike.save()
res.status(200).json( { message: 'liked!' })
}
} catch (err) {
console.log('π ~ file: artworkController.js ~ line 128 ~ error', err)
res.status(500).json( { message: err.message })
}
}
My focus was on the gallery page, user authentication and the like and comment features.
The comment feed component uses the doodle ID to get all its comments and formats the timestamp to display a comment feed. Using an interval this refreshes every five seconds.
const CommentFeed = ({ _id }) => {
const [commentsArray, setCommentsArray] = useState([])
useEffect(() => {
getComments()
const interval = setInterval(getComments, 5000)
return () => {
clearInterval(interval)
}
}, [])
const getComments = async () =>{
const response = await axios.get(`/api/artwork/${_id}/getComments`)
const newCommentsArray = response.data
setCommentsArray(newCommentsArray)
}
const formattedTimestamp = (timestamp) =>{
const date = new Date(timestamp)
const toString = date.toString()
const dateSlice = toString.slice(4,10)
const timeSlice = toString.slice(15,21)
return `${dateSlice} at ${timeSlice}`
}
To add comments - this will also take a user rating, between 1-5 and add this to the doodle.
const CommentForm = ({ _id }) => {
const [userComment, setUserComment] = useState({
commentText: '',
rating: 1
})
const [userRating, setUserRating] = useState(null)
const [isThereComment, setIsThereComment] = useState(null)
const [isThereRating, setIsThereRating] = useState(null)
console.log('π ~ file: CommentForm.js ~ line 25 ~ isThereRating', isThereRating)
const handleCommentChange = (event) => {
//?get the value of what's being typed in the form and updating state
const newUserComment = { ...userComment, [event.target.name]: event.target.value }
// console.log('π ~ file: Login.js ~ line 25 ~ event', event)
setUserComment(newUserComment)
}
const handleRatingChange = (event) => {
console.log('π ~ file: CommentForm.js ~ line 65 ~ event', event)
setIsThereRating(true)
setUserRating(event)
}
const handleCommentPost = async(event) => {
event.preventDefault()
const isThereComment = !!userComment.commentText
if (!userIsAuthenticated()) {
userNeedsToLogin('Please login to review and comment!βΊοΈ')
}
if (!isThereComment) {
setIsThereComment(false)
console.log('NO COMMENT')
commentPopup(0)
return 0
} if (!isThereRating) {
ratingPopup(false)
return 0
}
try {
const commentToAdd = { ...userComment, rating: userRating }
await axios.post(`/api/${_id}/comment`, commentToAdd, { headers: { Authorization: `Bearer ${getTokenFromLocalStorage()}` } } )
console.log('π ~ file: CommentForm.js ~ line 23 ~ commentToAdd', commentToAdd)
commentPopup(true)
setUserComment({ commentText: '' })
} catch (err) {
console.log('π΄ ~ file: CommentForm.js ~ line 24 ~ err', err)
}
}
I really Like how the gallery page turned out with the cards and getting the filter feature to work.
- Managing and splitting work proved challenging initially!
- Merging and solving conflicts was something we had to learn on the fly and practice.
- Working as a team to create a really cool interactive app!
- happy to get a working like button which updates correctly
- Using GitHub with others on a project.
- Splitting workload with team.
- Using SVGs.