Express.js
Node.js - Twitter Clone Coding // Replying to posts
selene park
2021. 4. 1. 00:06
getbootstrap.com/docs/4.0/components/modal/
Modal
Use Bootstrap’s JavaScript modal plugin to add dialogs to your site for lightboxes, user notifications, or completely custom content.
getbootstrap.com
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Modal title</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
...
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
// Modal
#exampleModal.modal.fade(tabindex='-1' role='dialog' aria-labelledby='exampleModalLabel' aria-hidden='true')
.modal-dialog(role='document')
.modal-content
.modal-header
h5#exampleModalLabel.modal-title Modal title
button.close(type='button' data-dismiss='modal' aria-label='Close')
span(aria-hidden='true') ×
.modal-body
| ...
.modal-footer
button.btn.btn-secondary(type='button' data-dismiss='modal') Close
button.btn.btn-primary(type='button') Save changes
common.js
// $(document).ready(()=>{//from main-layout.png!!!
// //alert("hola"); //파일에 js 연결 잘 됐는지 확인
// })
//버튼 활성화 시키기
$("#postTextarea, #replyTextarea").keyup(event =>{//여기서 파라미터가 1개면 () 필요없이 그냥 사용가능
var textbox = $(event.target);
var value=textbox.val().trim();
//console.log(value);
//message
var isModal = textbox.parents(".modal").length==1;
var submitButton = isModal ? $("#submitReplyButton") : $("#submitPostButton");//# 중요해//# 중요해
//==랑 ===차이점 @@@
if(submitButton.lengh == 0) return alert("No submit button found");
if (value==""){
submitButton.prop("disabled", true);
return;
}
submitButton.prop("disabled", false);
})
//작성한 글 & message 저장
$("#submitPostButton, #submitReplyButton").click(()=>{
var button = $(event.target);
var isModal = button.parents(".modal").length==1;
var textbox = isModal ? $("#replyTextarea") : $("#postTextarea") ;
var data={
//object
content:textbox.val()
}
//reply message
if(isModal){
var id = button.data().id;
if(id==null) return alert("Button id is null!");
data.replyTo = id;
}
//ajax만들기(request, which will send the data to the server without us having to reload the page)
$.post("/api/posts", data, postData => { //()=>{} : callback 함수
// 여기에서 data를 "/api/post"로 요청 req 하고 끝나면 ()=>{} 함수로 돌아오겠다, callback 해라는 이야기
//alert(postData);
//console.log(postData);
if(postData.replyTo){//it means it was reply
location.reload();
}else{
var html = createPostHtml(postData);
//prepend(top) & apend(end)
$(".postsContainer").prepend(html);
textbox.val("");//remove the textbox
button.prop("disabled", true);
}
})
})
// #replyModal 열렸다고 알려주기
$("#replyModal").on("show.bs.modal", (event)=>{
//modal open했을때
//console.log("hello");
var button = $(event.relatedTarget);// have to get postId (need to html 모드 들어가서 debug)
var postId = getPostIdFromElement(button);
//message
$("#submitReplyButton").data("id", postId);
//have to handle endpoint!
$.get("/api/posts/" + postId, results => {
//console.log(results);
outputPosts(results, $("#originalPostContainer"));//2번째 파라미터 값은 populate(참조)
})
})
$("#replyModal").on("hidden.bs.modal", ()=> $("#originalPostContainer").html(""));
//like를 위해서 @@@@important@@@@@
//document 전체페이지가 로딩된 후 버튼 이벤트를 통해서 실행
$(document).on("click", ".likeButton", (event)=>{
//alert("button clicked");
var button = $(event.target);
var postId = getPostIdFromElement(button);
//console.log(postId);
//중요 : ajax로 put request(create@@@) 하기 (but 한번 사용했던 이름으로 $.post or $.get으로는 재사용 불가능)
if(postId === undefined) return;
$.ajax({
url : `/api/posts/${postId}/like`,
type : "PUT", // PUT 이나 POST 나 둘 다 작동함
//callback (arrow function)
success: (postData)=>{//if on success they are gonna be return postData
// console.log(postData.likes.length);//크롬콘솔에 찍힐예정 -> like :1 , unlike :0
//버튼 누를시 새로고침 없이 숫자 나타내기
button.find("span").text(postData.likes.length || "");
if(postData.likes.includes(userLoggedIn._id)){// .id는 user 테이블에 자동 pk id number값
button.addClass("active");
}else{
button.removeClass("active");
}
}
})
});
//리트윗을 위해서
$(document).on("click", ".retweetButton", (event)=>{
var button = $(event.target);
var postId = getPostIdFromElement(button);
if(postId === undefined) return;
$.ajax({
url : `/api/posts/${postId}/retweet`,
type : "POST", //PUT->POST
success: (postData)=>{
//console.log(postData);
button.find("span").text(postData.retweetUsers.length || "");
if(postData.retweetUsers.includes(userLoggedIn._id)){
button.addClass("active");
}else{
button.removeClass("active");
}
}
})
});
//각 포스팅된 것들의 pk 넘버 찾는것
function getPostIdFromElement(element){
var isRoot = element.hasClass("post");
var rootElement = isRoot == true ? element : element.closest(".post");
var postId = rootElement.data().id;//data().id 의미 : 밑에있는 data-id
if(postId === undefined) return alert("Post id undefined");
return postId;
}
function createPostHtml(postData){
//return postData.content;
if(postData == null) return alert("post object is null");
//retweet
var isRetweet = postData.retweetData !== undefined;
var retweetedBy = isRetweet ? postData.postedBy.userName : null;
postData = isRetweet ? postData.retweetData : postData;
console.log(isRetweet);
//html 작업하기
var postedBy = postData.postedBy;
//Have to Populated!!
if(postedBy._id === undefined){
return console.log("User object not populated");
}
var displayName=postedBy.firstName + " " + postedBy.lastName;
//시간 변경하기
var timestamps = timeDifference(new Date(), new Date(postData.createdAt));
//showing correct button colour when page loads
var likeButtonActiveClass = postData.likes.includes(userLoggedIn._id) ? "active" : "";
//page reloading 했을때 리트윗이 있으면 색 그대로 유지하기
var retweetButtonActiveClass = postData.retweetUsers.includes(userLoggedIn._id) ? "active" : "";
//retweet
var retweetedText = '';
if(isRetweet){
retweetedText = `<span>
<i class='fas fa-retweet '></i>
Retweeted by <a href ='/profile/${retweetedBy}'>@${retweetedBy}</a>
</span>`
}
//message need to populate with POST!
var replyFlag ="";
if(postData.replyTo){
if(!postData.replyTo._id){
return alert("Reply to is not populated with post table!");
}else if(!postData.replyTo.postedBy._id){
return alert("Posted by is not populated with post table!");
}
var replyToUsername = postData.replyTo.postedBy.userName
replyFlag = `<div class='replyFlag'>
Replying to <a href='/profile/${replyToUsername}'>@${replyToUsername}</a>
</div>`
}
return `<div class='post' data-id='${postData._id}'>
<div class='postActionContainer'>
${retweetedText}
</div>
<div class='mainContentContainer'>
<div class='userImageContainer'>
<img src='${postedBy.profilePic}'>
</div>
<div class='postContentContainer'>
<div class='header'>
<a href='/profile/${postedBy.userName}' class='displayName'>${displayName}</a>
<span class='userName'>@${postedBy.userName}</span>
<span class='date'>${timestamps}</span>
</div>
${replyFlag}
<div class='postBody'>
<span>${postData.content}</span>
</div>
<div class='postFooter'>
<div class='postButtonContainer'>
<button data-toggle='modal' data-target='#replyModal'>
<i class='far fa-comment'></i>
</button>
</div>
<div class='postButtonContainer green'>
<button class='retweetButton ${retweetButtonActiveClass}'>
<i class='fas fa-retweet '></i>
<span>${postData.retweetUsers.length || ""}</span>
</button>
</div>
<div class='postButtonContainer red'>
<button class='likeButton ${likeButtonActiveClass}'>
<i class='far fa-heart'></i>
<span>${postData.likes.length || ""}</span>
</button>
</div>
</div>
</div>
</div>
</div>`;
}
//posting 한 시간을 시간그대로 나타내는게 아니라 ㅇㅇ전이라고 표시하기
function timeDifference(current, previous) {
var msPerMinute = 60 * 1000;
var msPerHour = msPerMinute * 60;
var msPerDay = msPerHour * 24;
var msPerMonth = msPerDay * 30;
var msPerYear = msPerDay * 365;
var elapsed = current - previous;
if (elapsed < msPerMinute) {
if(elapsed/1000 < 30) return "Just Now"
return Math.round(elapsed/1000) + ' seconds ago';
}
else if (elapsed < msPerHour) {
return Math.round(elapsed/msPerMinute) + ' minutes ago';
}
else if (elapsed < msPerDay ) {
return Math.round(elapsed/msPerHour ) + ' hours ago';
}
else if (elapsed < msPerMonth) {
return Math.round(elapsed/msPerDay) + ' days ago';
}
else if (elapsed < msPerYear) {
return Math.round(elapsed/msPerMonth) + ' months ago';
}
else {
return Math.round(elapsed/msPerYear ) + ' years ago';
}
}
//message
function outputPosts(results, container){//현재 이 함수는 배열로 만들어짐
container.html("");//have to clear contents
if(!Array.isArray(results)){//배열 아닌것을 배열로 리턴해라
results = [results]
; }
results.forEach(result =>{ //every single item takes result one by one
var html = createPostHtml(result)
container.append(html);
});
// == & === 차이점
if(results.length == 0){
container.append("<span class='noResults'>Nothing to show...</span>")
}
}
main.css
:root{
--blue: #1FA2F1;
--blueLight:#9BD1F9;
--buttonHoverBg:#d4edff;
--lightGrey:rgb(230, 236, 240);
--spacing:15px;
--greyText:rgb(101,110,134);
--greyButtonText:rgba(0,0,0,0.34);
--red:rgb(226, 34,94);
--redBackground:rgba(226,34,94,0.1);
--green:rgb(23, 1991,99);
--greenBackground:rgba(23,191,99,0.1);
}
*{
outline:none imp !important;
}
a{
color: inherit;
}
a:hover{
color:inherit;
text-decoration: none;
}
h1{
font-size: 19px;
font-weight: 800;
margin: 0;
}
nav a:hover{
background-color: var(--buttonHoverBg);
color: var(--blue);
border-radius: 50%;
}
nav{
display:flex;
flex-direction:column;
align-items : flex-end;
height:100%
}
nav a{
padding: 10px;
font-size: 30px;
width: 55px;
height: 55px;
display: flex;
align-items: center;
justify-content: center;
}
nav a.blue{
color: var(--blue);
}
button{
background-color: transparent;
border: none;
color:var(--greyButtonText);
}
button i,
button span{
pointer-events: none;
}
.mainSectionContainer{
padding: 0;
border-left: 1px solid var(--lightGrey);
border-right: 1px solid var(--lightGrey);
display : flex;
flex-direction: column;
}
.titleContainer {
height : 53px;
padding : 0 var(--spacing);
display: flex;
align-items: center;
border-bottom: 1px solid var(--lightGrey);
flex-shrink: 0;
}
.titleContainer h1{
flex:1;
}
.postFormContainer{
display: flex;
padding : var(--spacing);
border-bottom: 10px solid rgb(230, 236, 240);
flex-shrink: 0;
}
.modal .postFormContainer{
border: none;
padding: 0;
padding-top: var(--spacing);
}
.modal .post {
padding: 0 0 var(--spacing) 0;
}
.userImageContainer{
width: 50px;
height: 50px;
}
.userImageContainer img{
width: 100%;
border-radius: 50%;
background-color: white;
}
.textareaContainer{
flex:1;
padding-left: var(--spacing);
}
.textareaContainer textarea{
width: 100%;
border: none;
resize:none;
font-size: 25px;
}
#submitPostButton{
background-color: var(--blue);
color: white;
border: none;
border-radius: 40px;
padding: 7px 15px;
}
#submitPostButton:disabled{
background-color: var(--blueLight);
}
.post{
display: flex;
flex-direction: column;
padding:var(--spacing);
cursor: pointer;
border-bottom: 1px solid var(--lightGrey);
flex-shrink: 0;
}
.mainContentContainer{
flex:1;
display: flex;
}
.postContentContainer{
padding-left: var(--spacing);
display: flex;
flex-direction: column;
flex: 1;
}
.userName,
.date{
color:var(--greyText)
}
.displayName{
font-weight:bold;
}
.postFooter{
display: flex;
align-items: center;
}
.postFooter .postButtonContainer{
flex:1;
display: flex;
}
.postFooter .postButtonContainer button{
padding: 2px 7px;
}
.header a:hover{
text-decoration: underline;
}
.header a,
.header span{
padding-right: 5px;
}
.postButtonContainer button:hover{
background-color: #d4eeff;
color: var(--blue);
border-radius: 50%;
}
.postButtonContainer.red button.active{
color: var(--red);
}
.postButtonContainer.red button:hover{
color: var(--red);
background-color: var(--redBackground);
}
.postButtonContainer.green button.active{
color: var(--green);
}
.postButtonContainer.green button:hover{
color: var(--green);
background-color: var(--greenBackground);
}
.postActionContainer{
padding-left: 35px;
padding-bottom: 10px;
font-size: 13px;
font-weight:bold;
color: var(--greyText);
}
.replyFlag{
margin-bottom: 5px;
}
.replyFlag a {
color: var(--blue);
}
post.js
const express = require('express');
const app = express();
const router = express.Router();
const bodyParser = require("body-parser");
const User = require('../../schemas/UserSchema');
const Post = require('../../schemas/PostSchema');
app.use(bodyParser.urlencoded({ extended:false}));
router.get("/", async (req, res, next)=>{//<pending> 된 상태 해결
//message
var results = await getPosts({});//<pending> 된 상태 해결
//console.log(results);//<pending> 된 상태였음
res.status(200).send(results);
})
//message
//Getting a single post by ID
router.get("/:id", async (req, res, next)=>{
//message 버튼 눌렀을때 반응하는지 크롬 콘솔 확인
//return res.status(200).send("this is test");
var postId = req.params.id;
//console.log(results);
var results = await getPosts({ _id: postId});//여기에서 _id는 Post 테이블의 고유 넘버값
results = results[0];
res.status(200).send(results);
})
router.post("/", async (req, res, next)=>{
//message check
// if(req.body.replyTo){
// console.log(req.body.replyTo);
// return res.sendStatus(400);
// }
//session 을 통해 누가 로그인 했는지 알 수 있다.
if(!req.body.content) {
console.log("Content param not sent with request");
return res.sendStatus(400);
}
var postData ={
content : req.body.content,
postedBy : req.session.user
}
//message
if(req.body.replyTo) {
postData.replyTo = req.body.replyTo;
}
//callback ??인지??
Post.create(postData)
.then( async newPost =>{//201 : Created!, 200 :Success!
newPost = await User.populate(newPost, { path : "postedBy"})
res.status(201).send(newPost);
})
.catch( error =>{
console.log(error);
res.sendStatus(400);
})
//res.status(200).send("it worked");
})
//like를 위해 추가
//url : `/api/posts/${postId}/like`
router.put("/:id/like", async (req, res, next)=>{//:id말고 다른 아이디 네임 작성해도 괜찮음
//console.log(req.params.id);// :id라고 적어서 id라고 작성함
var postId = req.params.id;
var userId = req.session.user._id;//session을 통해서 누군지 알 수 있음
// have to check same person already pressed like or not
// 이미 좋아요 누른 경우
var isLiked = req.session.user.likes && req.session.user.likes.includes(postId);
//console.log("Is liked: " + isLiked);
var option = isLiked ? "$pull" : "$addToSet"; //mongoDB에서 쓰는..
console.log("is liked : " + isLiked);
console.log("option : " + option);
console.log("User Id : " + userId);
//Insert User Table like (need to update)
//here have to put await !! 비동기 시스템이라 안하면 user table에 like 필드가 안나타난다.
//option을 valuable 하게 사용하려고 [] 씀 & like가 아니라 ! User테이블의 likes 필드명
//unlike 가 실행이 안됐던 이유 : findByIdAndUpdate 가 실행되면서 새롭게 업데이트 된 document를 리턴하지 않아서
req.session.user = await User.findByIdAndUpdate(userId, { [option] : { likes: postId} }, { new : true})
.catch(error => {
console.log(error);
res.sendStatus(400);
})
//Insert Post Table like (need to update)
var post= await Post.findByIdAndUpdate(postId, { [option] : { likes: userId} }, { new : true})
.catch(error => {
console.log(error);
res.sendStatus(400);
})
res.status(200).send(post);
})
//retweet를 위해 추가
router.post("/:id/retweet", async (req, res, next)=>{
//return res.status(200).send("response test") // retweet 버튼반응테스트
var postId = req.params.id;
var userId = req.session.user._id;
//Try and delete retweet
var deletedPost = await Post.findOneAndDelete({ postedBy : userId, retweetData : postId }) // retweetData
.catch(error => {
console.log(error);
res.sendStatus(400);
})
var option = deletedPost !=null ? "$pull" : "$addToSet";
//return res.status(200).send(option);
//NOTE : REPOST HAS A VALUE!!! @@@@@
var repost = deletedPost;
if(repost == null){
repost = await Post.create({ postedBy : userId, retweetData: postId })// retweetData
.catch(error => {
console.log(error);
res.sendStatus(400);
})
}
//NOTE : Retweet 에서 id는 Post 테이블의 postId가 될 수 없다. repost의 고유한 id가 되어야 한다. @@@@@@
//User Table
req.session.user = await User.findByIdAndUpdate(userId, { [option] : { retweets: repost._id} }, { new : true})
.catch(error => {
console.log(error);
res.sendStatus(400);
})
//Post Table
var post= await Post.findByIdAndUpdate(postId, { [option] : { retweetUsers: userId} }, { new : true})
.catch(error => {
console.log(error);
res.sendStatus(400);
})
res.status(200).send(post);
})
//message
async function getPosts(filter){
var results = await Post.find(filter)//Post.fineOne 의미 : (query에서 1 result만 추출)
.populate("postedBy")
.populate("retweetData")
.populate("replyTo")
.sort({ "createdAt" : -1})
.catch(error => console.log(error))
results = await User.populate(results, { path : "replyTo.postedBy"})
return await User.populate(results, { path : "retweetData.postedBy"})
}
module.exports = router;
PostSchema.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const PostSchema = new Schema({
content : { type: String, trim : true },
postedBy : { type: Schema.Types.ObjectId, ref: 'User'}, // Schema.Types.ObjectId -> mongodb가 자동으로 populate 할 수 있다.
pinned : Boolean, // 고정하기
//likes도 마찬가지로 User의 object이다@@
likes:[{ type: Schema.Types.ObjectId, ref: 'User'}],
//retweet
retweetUsers : [{ type: Schema.Types.ObjectId, ref: 'User'}], // [] ->every tweet uses
//the root of retweetData is Post, and the root of Post can be identified by postedBy@@@@@
retweetData : { type: Schema.Types.ObjectId, ref: 'Post'},//retweet 하는 just id of the post every retwitting...
replyTo : { type: Schema.Types.ObjectId, ref: 'Post'}
}, {timestamps:true});
var Post = mongoose.model('Post', PostSchema);
module.exports = Post;