Mi Lugarcito

Node.js - Twitter Clone Coding // Deleting posts 본문

Express.js

Node.js - Twitter Clone Coding // Deleting posts

selene park 2021. 4. 1. 19:52

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);
        //SO, RESULTS CONTAINE OBJECT WITH  3 DIFFERENT THING (postData>replyTo>replies)
        outputPosts(results.postData, $("#originalPostContainer"));//2번째 파라미터 값은 populate(참조)
    })
})


$("#replyModal").on("hidden.bs.modal", ()=> $("#originalPostContainer").html(""));


//delete
$("#deletePostModal").on("show.bs.modal", (event)=>{
    //alert("opend");
    var button = $(event.relatedTarget);
    var postId = getPostIdFromElement(button);

    $("#deletePostButton").data("id", postId);

    //console.log($("#deletePostButton").data().id);
})

//delete
//여기 삭제버튼은 mixins.pug 페이지에 바로 있어서 (같은 한개의 페이지에 다 있어서)따로 페이지를 로딩할 필요가 없어서 앞에 document 붙여도 되고 안붙여도 작동함
$("#deletePostButton").click((event)=>{
    //alert("hi");
    var postId =$(event.target).data("id");


    //delete ajax
    $.ajax({
        url : `/api/posts/${postId}`,
        type : "DELETE", 
        success: ()=>{
            location.reload();
        }
    })
})



//like를 위해서 @@@@important@@@@@
//document 전체페이지가 로딩된 후 버튼 이벤트를 통해서 실행
$(document).on("click", ".likeButton", (event)=>{//중요! 여기서 document 를 붙이는 이유는 해당 page를 load해야해서 
    
    //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)=>{//중요! 여기서 document 를 붙이는 이유는 해당 page를 load해야해서 
    
    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");
           }

        }
    })


});


//post page on click and show all
$(document).on("click", ".post", (event)=> {
    var element = $(event.target);
    var postId = getPostIdFromElement(element);

    if(postId!==undefined && !element.is("button")){
        window.location.href='/posts/' + postId;
    }
});



//각 포스팅된 것들의 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, largeFont = false){
    //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;
    var largeFontClass = largeFont ? "largeFont" : "";

    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 && postData.replyTo._id){//댓글에 대댓글 방지하기 위해서
        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>`


    }

    //delete
    var buttons ="";
    if(postData.postedBy._id == userLoggedIn._id){
        buttons = `<button data-id="${postData._id}" data-toggle="modal" data-target = "#deletePostModal"><i class='fas fa-times'></i></button>`;
    }

    return `<div class='post ${largeFontClass}' 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>
                            ${buttons}
                        </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>")
    }
}

//postPage.pug
function outputPostsWithReplies(results, container){
    container.html("");
    //1번째 받은값 넘기기
    if(results.replyTo !== undefined && results.replyTo._id !==undefined){
        var html = createPostHtml(results.replyTo)
        container.append(html);
    }
    //2번째 받은값 넘기기
    //main post!!
    var mainpostHtml = createPostHtml(results.postData, true)// function createPostHtml(postData, largeFont = false) 에서 largeFont => true
    container.append(mainpostHtml);

    //3번째
    results.replies.forEach(result =>{ 
        var html = createPostHtml(result)
        container.append(html);
    });

}

posts.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);
    //post -> post+message
    var postData = await getPosts({ _id: postId});//여기에서 _id는 Post 테이블의 고유 넘버값
    postData = postData[0];// have to change all@


     //SO, RESULTS CONTAINE OBJECT WITH  3 DIFFERENT THING (postData>replyTo>replies)
    //now results has a postData
    var results ={
        //object : value @@@@@@
        postData : postData
    }
    //results has also replyTo
    if(postData.replyTo !== undefined){
        results.replyTo = postData.replyTo;
    }


    //have to get all reply from all post
    results.replies = await getPosts({ replyTo : postId });



    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);
})

//delete
router.delete("/:id", (req, res, next) => {
    Post.findByIdAndDelete(req.params.id)
    .then(() => res.sendStatus(202))
    .catch(error=>{
        console.log(error);
        res.sendStatus(400);
    })
})

//message
async function getPosts(filter){//filter -> can do search on
    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;

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.largeFont .postBody,
.post.largeFont .postFooter{
    font-size: 28px;
}

.postContentContainer .header{
    display: flex;
}

.postContentContainer .header .date{
    flex:1;
}

mixins.pug

mixin createPostForm(userLoggedIn)
    .postFormContainer 
        .userImageContainer 
            img(src=userLoggedIn.profilePic, alt="User's profile picture")
        .textareaContainer 
            textarea#postTextarea(placeholder="What's happening?")
            .buttonsContainer 
                button#submitPostButton(disabled="") Post 


mixin creatReplyModal(userLoggedIn)
    #replyModal.modal.fade(tabindex='-1' role='dialog' aria-labelledby='replyModalLabel' aria-hidden='true')
        .modal-dialog(role='document')
            .modal-content
                .modal-header
                    h5#replyModalLabel.modal-title Reply
                    button.close(type='button' data-dismiss='modal' aria-label='Close')
                    span(aria-hidden='true') &times;
                .modal-body
                      #originalPostContainer
                      .postFormContainer 
                        .userImageContainer 
                            img(src=userLoggedIn.profilePic, alt="User's profile picture")
                        .textareaContainer 
                            textarea#replyTextarea(placeholder="What's happening?")
                .modal-footer
                    button.btn.btn-secondary(type='button' data-dismiss='modal') Close
                    button#submitReplyButton.btn.btn-primary(type='button', disabled="") Reply



mixin creatDeletePostModal()
    #deletePostModal.modal.fade(tabindex='-1' role='dialog' aria-labelledby='deletePostModalLabel' aria-hidden='true')
        .modal-dialog(role='document')
            .modal-content
                .modal-header
                    h5#deletePostModalLabel.modal-title Delete the post?
                    button.close(type='button' data-dismiss='modal' aria-label='Close')
                    span(aria-hidden='true') &times;
                .modal-body
                     p You won't be able to delete this.
                .modal-footer
                    button.btn.btn-secondary(type='button' data-dismiss='modal') Close
                    button#deletePostButton.btn.btn-primary(type='button') Delete



mixin createPostModals(userLoggedIn)
    +creatReplyModal(userLoggedIn)
    +creatDeletePostModal()

home.pug

extends layouts/main-layout.pug

block content
    +createPostForm(userLoggedIn)
    .postsContainer

    +createPostModals(userLoggedIn)
    
block scripts 
    script(src="/js/home.js") 

postPage.pug

extends layouts/main-layout.pug

block content
    script. 
        var postId = '!{postId}'
    .postsContainer

    +createPostModals(userLoggedIn)
    
    
block scripts 
    script(src="/js/postPage.js")