diff --git a/src/components/Comment/CommentElement.jsx b/src/components/Comment/CommentElement.jsx new file mode 100644 index 00000000..d9f2c44c --- /dev/null +++ b/src/components/Comment/CommentElement.jsx @@ -0,0 +1,82 @@ +import React, { useState } from "react"; + +const CommentElement = ({ + id, + content, + createdAt, + handleCommentDelete, + handleCommentEdit, +}) => { + const [isEditing, setIsEditing] = useState(false); + const [editedContent, setEditedContent] = useState(content); + + const date = new Date(createdAt); + const year = date.getFullYear(); + let month = date.getMonth() + 1; + month = month < 10 ? `0${month}` : month; + let day = date.getDate(); + day = day < 10 ? `0${day}` : day; + + const handleEditComment = () => { + if (editedContent.trim() !== "") { + handleCommentEdit(id, editedContent); + setIsEditing(false); + alert("댓글 수정 완료"); + } else { + alert("댓글 내용을 입력해주세요."); + } + }; + + return ( +
+
+ {isEditing ? ( + setEditedContent(e.target.value)} + className="flex-grow border p-2 rounded mr-2 text-black" + /> + ) : ( +
+

{content}

+
+ )} + + {isEditing ? ( + <> + + + + ) : ( +
+ + +
+ )} +
+
+ {year}.{month}.{day} +
+
+ ); +}; + +export default CommentElement; diff --git a/src/components/Comment/index.jsx b/src/components/Comment/index.jsx new file mode 100644 index 00000000..41023bcc --- /dev/null +++ b/src/components/Comment/index.jsx @@ -0,0 +1,87 @@ +import React, { useState } from "react"; +import commentsData from "../../data/comments"; +import CommentElement from "./CommentElement"; + +const Comment = ({ postId }) => { + // 댓글들을 저장하기 위한 state + const [comments, setComments] = useState(commentsData); + // 새로운 댓글을 추가하기 위한 state + const [newComment, setNewComment] = useState(""); + + const handleCommentSubmit = (e) => { + e.preventDefault(); + // 현재 날짜와 시간을 포함하는 새 댓글 객체 생성 + const now = new Date(); + const createdAt = now.toISOString().split("T")[0]; + + const commentToAdd = { + id: comments.length + 1, + content: newComment, + created_at: createdAt, + author: { id: "1", username: "currentUser" }, + }; + setComments([...comments, commentToAdd]); + setNewComment(""); + alert("댓글 작성"); + }; + + const handleCommentEdit = (id, editedContent) => { + setComments( + comments.map((comment) => { + if (comment.id === id) { + return { ...comment, content: editedContent }; + } + return comment; + }) + ); + }; + + const handleCommentDelete = (commentId) => { + const updatedComments = comments.filter( + (comment) => comment.id !== commentId + ); + setComments(updatedComments); + alert("댓글 삭제"); + }; + + return ( +
+

Comments

+ {comments.map((comment) => ( + + ))} + +
+
+ setNewComment(e.target.value)} + className="border p-2 rounded flex-grow text-black" + /> + +
+
+
+ ); +}; + +export default Comment; diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index 99e87e1b..951c8bbc 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -1,8 +1,18 @@ +import { useState, useEffect } from "react"; import { Link } from "react-router-dom"; import lion from "../../assets/images/lion.jpeg"; const Header = () => { + const [isUserLoggedIn, setIsUserLoggedIn] = useState(false); // 로그인 여부 상태, 우선 false로 초기화 + + useEffect(() => { + // TODO: 이후 api 연결 시 유효한 token이 있다면 setIsUserLoggedIn(true)로 상태 변경하는 코드 작성 + }, []); + + const handleSignOut = () => { + // TODO: 이후 api 연결 시 token 제거 + }; return (
@@ -10,12 +20,20 @@ const Header = () => {
SNULION BLOG
- - sign in - - - sign up - + {isUserLoggedIn ? ( + + sign out + + ) : ( + <> + + sign in + + + sign up + + + )}
); diff --git a/src/components/Posts/index.jsx b/src/components/Posts/index.jsx index 07686dcc..506a468e 100644 --- a/src/components/Posts/index.jsx +++ b/src/components/Posts/index.jsx @@ -1,6 +1,12 @@ import { Link } from "react-router-dom"; export const SmallPost = ({ post }) => { + const onClickLike = (e) => { + e.preventDefault(); + alert("나도 좋아!"); + // add api call for liking post here + }; + return ( { ))} - + {post.like_users.length > 0 && `❤️ ${post.like_users.length}`} @@ -23,6 +29,10 @@ export const SmallPost = ({ post }) => { }; export const BigPost = ({ post }) => { + const onClickLike = () => { + alert("나도 좋아!"); + // add api call for liking post here + }; return (
@@ -44,7 +54,10 @@ export const BigPost = ({ post }) => { ))}
- + ❤️ {post.like_users.length > 0 ? post.like_users.length : "0"}
diff --git a/src/data/comments.js b/src/data/comments.js new file mode 100644 index 00000000..f95a23e4 --- /dev/null +++ b/src/data/comments.js @@ -0,0 +1,35 @@ +// dummy data +const comments = [ + { + id: 1, + content: "새해 복 많이 받으세요^^", + created_at: "2024-01-01T15:09:43Z", + post: 1, + author: { + id: 2, + username: "user2", + }, + }, + { + id: 2, + content: "축구 2대 0;;;", + created_at: "2024-02-07T15:09:43Z", + post: 1, + author: { + id: 3, + username: "user3", + }, + }, + { + id: 3, + content: "망할 개강이야...", + created_at: "2024-03-02T15:09:43Z", + post: 1, + author: { + id: 4, + username: "user4", + }, + }, +]; + +export default comments; diff --git a/src/routes/HomePage.jsx b/src/routes/HomePage.jsx index 027eae34..b97e8010 100644 --- a/src/routes/HomePage.jsx +++ b/src/routes/HomePage.jsx @@ -1,11 +1,43 @@ +import { useState, useEffect } from "react"; import { SmallPost } from "../components/Posts"; import { Link } from "react-router-dom"; import posts from "../data/posts"; const HomePage = () => { - const postList = posts; + const [postList, setPostList] = useState(posts); + const [tags, setTags] = useState([]); + const [searchTags, setSearchTags] = useState([]); + const [searchValue, setSearchValue] = useState(""); - const handleChange = (e) => {}; + useEffect(() => { + const tagList = posts.reduce((acc, post) => { + for (let tag of post.tags) { + acc.add(tag.content); + } + return acc; + }, new Set()); + setTags([...tagList]); + setSearchTags([...tagList]); + }, []); + const handleTagFilter = (e) => { + const { innerText } = e.target; + if (searchValue === innerText.substring(1)) { + setSearchValue(""); + setPostList(posts); + } else { + const activeTag = innerText.substring(1); + setSearchValue(activeTag); + const newPosts = posts.filter((post) => + post.tags.find((tag) => tag.content === activeTag) + ); + setPostList(newPosts); + } + }; + const handleChange = (e) => { + const { value } = e.target; + const newTags = tags.filter((tag) => tag.includes(value)); + setSearchTags(newTags); + }; return (
@@ -20,6 +52,19 @@ const HomePage = () => { className="border border-orange-400 outline-none rounded-2xl text-center py-2 px-20 text-orange-400 bg-transparent" />
+
+ {searchTags.map((tag) => { + return ( + + ); + })} +
{postList.map((post) => ( diff --git a/src/routes/PostCreatePage.jsx b/src/routes/PostCreatePage.jsx index a33c7af4..1dd42922 100644 --- a/src/routes/PostCreatePage.jsx +++ b/src/routes/PostCreatePage.jsx @@ -1,7 +1,13 @@ +import { useState, useEffect } from "react"; import posts from "../data/posts"; +import { BigPost } from "../components/Posts"; const PostCreatePage = () => { - const post = { + const [isSubmitted, setIsSubmitted] = useState(false); + const [tags, setTags] = useState([]); + const [tagInputValue, setTagInputValue] = useState(""); + const [autoCompletes, setAutoCompletes] = useState([]); + const [post, setPost] = useState({ id: posts.length, title: "", content: "", @@ -9,14 +15,82 @@ const PostCreatePage = () => { tags: [], like_users: [], created_at: "2024-02-04T07:42:50.658501Z", + }); + useEffect(() => { + const duplicatedTagList = posts.reduce((acc, post) => { + for (let tag of post.tags) { + acc.add(tag.content); + } + return acc; + }, new Set()); + const tagList = [...duplicatedTagList]; + setTags([...tagList]); + }, []); + + const handleAutoCompletes = (autoComplete) => { + const selectedTag = tags.find((tag) => tag === autoComplete); + if (post.tags.includes(selectedTag)) return; // 입력한 내용이 이미 등록된 태그면 그냥 등록 안됨 + setPost({ + ...post, + tags: [...post.tags, selectedTag], + }); + setTagInputValue(""); + setAutoCompletes([]); + }; + + const handleTag = (e) => { + setTagInputValue(e.target.value); + if (e.target.value) { + const autoCompleteData = tags.filter((tag) => + tag.includes(e.target.value) + ); + setAutoCompletes(autoCompleteData); + } else { + setAutoCompletes([]); + } + }; + const handleChange = (e) => { + setPost({ ...post, [e.target.id]: e.target.value }); }; const onSubmit = (e) => { + e.preventDefault(); + const createdPost = { + ...post, + like_users: [], + tags: post.tags.map((tag, idx) => { + return { id: idx + 1, content: tag }; + }), + }; + setPost(createdPost); + setIsSubmitted(true); alert("게시글을 등록합니다."); //TODO : api connect(create post) }; - return ( + const addTag = (e) => { + e.preventDefault(); + if (post.tags.find((tag) => tag === tagInputValue)) return; + setPost({ + ...post, + tags: [...post.tags, tagInputValue], + }); + setTagInputValue(""); + setAutoCompletes([]); + }; + + const deleteTag = (tag) => { + setPost({ + ...post, + tags: post.tags.filter((t) => t !== tag), + }); + }; + + return isSubmitted ? ( +
+ +
+ ) : (

게시글 작성

@@ -28,7 +102,9 @@ const PostCreatePage = () => { placeholder="제목을 입력하세요" defaultValue={post.title} id="title" + value={post.title} className="input" + onChange={handleChange} required />
+
+ {autoCompletes && + autoCompletes.map((autoComplete) => ( + + ))} +
{post.tags && (
{post.tags.map((tag) => ( @@ -66,7 +161,10 @@ const PostCreatePage = () => {

#{tag}

-
))} diff --git a/src/routes/PostDetailPage.jsx b/src/routes/PostDetailPage.jsx index 079fd151..8839134b 100644 --- a/src/routes/PostDetailPage.jsx +++ b/src/routes/PostDetailPage.jsx @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; -import { useParams, Link } from "react-router-dom"; +import { useParams, Link, useNavigate } from "react-router-dom"; import { BigPost } from "../components/Posts"; +import Comment from "../components/Comment"; import posts from "../data/posts"; const PostDetailPage = () => { @@ -10,16 +11,18 @@ const PostDetailPage = () => { const post = posts.find((post) => post.id === parseInt(postId)); setPost(post); }, [postId]); - + const navigate = useNavigate(); const onClickDelete = () => { - alert("삭제"); - //TODO : api connect(delete post) + alert("게시물을 삭제합니다."); + navigate("/"); + // add api call for deleting post }; return ( post && (
+
diff --git a/src/routes/PostEditPage.jsx b/src/routes/PostEditPage.jsx index ff86e811..b28fbea4 100644 --- a/src/routes/PostEditPage.jsx +++ b/src/routes/PostEditPage.jsx @@ -1,24 +1,104 @@ -import { useEffect, useState } from "react"; +import { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; import posts from "../data/posts"; +import { BigPost } from "../components/Posts"; const PostEditPage = () => { const { postId } = useParams(); - const [post, setPost] = useState({}); + const [isSubmitted, setIsSubmitted] = useState(false); + const [tagInputValue, setTagInputValue] = useState(""); + const [autoCompletes, setAutoCompletes] = useState([]); + const [tags, setTags] = useState([]); + const [post, setPost] = useState({ + id: posts.length, + title: "", + content: "", + author: { id: posts.length, username: "아기사자" }, + tags: [], + like_users: [], + created_at: "2024-02-04T07:42:50.658501Z", + }); + + useEffect(() => { + const duplicatedTagList = posts.reduce((acc, post) => { + for (let tag of post.tags) { + acc.add(tag.content); + } + return acc; + }, new Set()); + const tagList = [...duplicatedTagList]; + setTags([...tagList]); + }, []); - // 기존 게시글 불러오기 useEffect(() => { const post = posts.find((post) => post.id === parseInt(postId)); const originalPost = { ...post, tags: post.tags.map((tag) => tag.content) }; setPost(originalPost); }, [postId]); + const handleChange = (e) => { + setPost({ ...post, [e.target.id]: e.target.value }); + }; + + const handleTag = (e) => { + setTagInputValue(e.target.value); + if (e.target.value) { + const autoCompleteData = tags.filter((tag) => + tag.includes(e.target.value) + ); + setAutoCompletes(autoCompleteData); + } + }; + + const handleAutoCompletes = (autoComplete) => { + const selectedTag = tags.find((tag) => tag === autoComplete); + if (post.tags.includes(selectedTag)) return; + setPost({ + ...post, + tags: [...post.tags, selectedTag], + }); + setTagInputValue(""); + setAutoCompletes([]); + }; + + const addTag = (e) => { + e.preventDefault(); + if (post.tags.find((tag) => tag === tagInputValue)) return; + setPost({ + ...post, + tags: [...post.tags, tagInputValue], + }); + setTagInputValue(""); + setAutoCompletes([]); + }; + + const deleteTag = (tag) => { + setPost({ + ...post, + tags: post.tags.filter((t) => t !== tag), + }); + }; + const onSubmit = (e) => { + e.preventDefault(); + const editedPost = { + ...post, + like_users: [], + tags: post.tags.map((tag, idx) => { + return { id: idx + 1, content: tag }; + }), + }; + setPost(editedPost); + setIsSubmitted(true); alert("게시글을 수정합니다."); - // TODO : api connect(edit post) + //TODO : api connect }; - return ( + return isSubmitted ? ( +
+ +
+ ) : (

게시글 수정

@@ -29,8 +109,9 @@ const PostEditPage = () => { type="text" placeholder="제목을 입력하세요" id="title" - defaultValue={post.title} + value={post.title} className="input" + onChange={handleChange} required />
+
+ {autoCompletes && + autoCompletes.map((autoComplete) => ( + + ))} +
{post.tags && (
{post.tags.map((tag) => ( @@ -69,8 +165,8 @@ const PostEditPage = () => {

#{tag}

))} diff --git a/src/routes/SignInPage.jsx b/src/routes/SignInPage.jsx index 537272d3..1a17be90 100644 --- a/src/routes/SignInPage.jsx +++ b/src/routes/SignInPage.jsx @@ -1,25 +1,61 @@ +import { useState } from "react"; + const SignInPage = () => { - const handleSignInSubmit = () => { - alert("로그인 하기"); // TODO: add api call for sign in - }; + const [signInData, setSignInData] = useState({ + username: "", + password: "", + }); + + const handleSignInData = (e) => { + const { id, value } = e.target; + setSignInData({ ...signInData, [id]: value }); + }; + + const handleSignInSubmit = (e) => { + e.preventDefault(); // to prevent reloading the page + console.log(signInData); + alert("로그인 하기"); // TODO: add api call for sign in + }; + + return ( +
+

로그인

+ + + + + + - return ( -
-

로그인

- - - - - - - -
- - -
- -
- ); +
+ + +
+ +
+ ); }; export default SignInPage; diff --git a/src/routes/SignUpPage.jsx b/src/routes/SignUpPage.jsx index 9b87106b..dc91ac8a 100644 --- a/src/routes/SignUpPage.jsx +++ b/src/routes/SignUpPage.jsx @@ -1,5 +1,22 @@ +import { useState } from "react"; + const SignUpPage = () => { - const handleSignUpSubmit = () => { + const [signUpData, setSignUpData] = useState({ + email: "", + password: "", + confirm_password: "", + username: "", + college: "", + major: "", + }); + + const handleSignUpData = (e) => { + const { id, value } = e.target; + setSignUpData({ ...signUpData, [id]: value }); + }; + const handleSignUpSubmit = (e) => { + e.preventDefault(); // to prevent reloading the page + console.log(signUpData); alert("회원가입 하기"); // TODO: add api call for sign up }; @@ -10,17 +27,38 @@ const SignUpPage = () => { - + - + - +