diff --git a/apps/re-member/src/app/layout/CommunityLayout.tsx b/apps/re-member/src/app/layout/CommunityLayout.tsx new file mode 100644 index 00000000..6f067b3f --- /dev/null +++ b/apps/re-member/src/app/layout/CommunityLayout.tsx @@ -0,0 +1,49 @@ +import { Header, Tabs, Title } from "@/components/common"; +import { BottomNavbar } from "@/components/menu"; + +import { ROUTE } from "@/shared/config/route"; +import { + BsFillClipboardFill, + BsFillEmojiSmileFill, + BsFillInfoSquareFill, + BsFillQuestionCircleFill, +} from "react-icons/bs"; +import { Outlet } from "react-router"; + +export default function CommunityLayout() { + return ( + <> +
커뮤니티} + className="absolute left-0 right-0 top-0 bg-white" + /> + +
+ + } + label="공지" + href={ROUTE.COMMUNITY_NOTICE} + /> + } + label="자유" + href={ROUTE.COMMUNITY_FREE} + /> + } + label="질문" + href={ROUTE.COMMUNITY_QUESTION} + /> + } + label="정보" + href={ROUTE.COMMUNITY_INFORMATION} + /> + + +
+ + + ); +} diff --git a/apps/re-member/src/app/route/config.tsx b/apps/re-member/src/app/route/config.tsx index 9c00513c..9b154393 100644 --- a/apps/re-member/src/app/route/config.tsx +++ b/apps/re-member/src/app/route/config.tsx @@ -1,8 +1,15 @@ import { createBrowserRouter } from "react-router"; import RootLayout from "../layout/RootLayout"; -import { ActivityHomePage, HomePage, StudyDetailPage } from "@/pages"; +import { + ActivityHomePage, + HomePage, + StudyDetailPage, + CommunityDetailPage, + CommunityPage, +} from "@/pages"; import { ROUTE } from "@/shared/config/route"; import ActivityLayout from "../layout/ActivityLayout"; +import CommunityLayout from "../layout/CommunityLayout"; export const router = createBrowserRouter([ { @@ -19,6 +26,12 @@ export const router = createBrowserRouter([ path: `${ROUTE.ACTIVITY}/:id`, children: [{ index: true, element: }], }, + { + path: ROUTE.COMMUNITY, + element: , + children: [{ index: true, element: }], + }, + { path: `${ROUTE.COMMUNITY}/:id`, element: }, ], }, ]); diff --git a/apps/re-member/src/app/types/community.ts b/apps/re-member/src/app/types/community.ts new file mode 100644 index 00000000..8ff87f83 --- /dev/null +++ b/apps/re-member/src/app/types/community.ts @@ -0,0 +1,35 @@ +export const CATEGORY = { + NOTICE: "notice", + FREE: "free", + QUESTION: "question", + INFORMATION: "information", +}; + +export interface PostData { + id: number; + title: string; + createdAt: string; + author: string; + generation: number; + likeCount: number; + commentCount: number; +} + +export interface PostDetailData { + author: string; + generation: number; + createdAt: string; + content: string; + likeCount: number; + commentCount: number; +} + +export interface PostDetailCommentData { + id: number; + author: string; + generation: number; + createdAt: string; + content: string; + likeCount: number; + isAuthor?: boolean; +} diff --git a/apps/re-member/src/components/common/button/button.css.ts b/apps/re-member/src/components/common/button/button.css.ts index 7b96d37d..708b3e20 100644 --- a/apps/re-member/src/components/common/button/button.css.ts +++ b/apps/re-member/src/components/common/button/button.css.ts @@ -17,6 +17,7 @@ export const buttonVariant = cva( outlineDisabled: "border border-gray-3 text-gray-3 bg-gray-1 active:brightness-100", ghost: "bg-transparent text-gray-3 ", + text: "bg-transparent text-black ", }, }, defaultVariants: { diff --git a/apps/re-member/src/components/common/index.ts b/apps/re-member/src/components/common/index.ts index 082277a4..c569fab1 100644 --- a/apps/re-member/src/components/common/index.ts +++ b/apps/re-member/src/components/common/index.ts @@ -7,3 +7,4 @@ export { default as Scrollable } from "./scrollable/Scrollable"; export { default as Section } from "./section/Section"; export { default as Tabs } from "./tabs/Tabs"; export { default as Title } from "./title/Title"; +export { default as PlusButton } from "./plus-button/PlusButton"; diff --git a/apps/re-member/src/components/common/plus-button/PlusButton.tsx b/apps/re-member/src/components/common/plus-button/PlusButton.tsx new file mode 100644 index 00000000..2d3ea80f --- /dev/null +++ b/apps/re-member/src/components/common/plus-button/PlusButton.tsx @@ -0,0 +1,16 @@ +import { IoAdd } from "react-icons/io5"; + +interface PlusButtonProps { + onClick?: () => void; +} + +export default function PlusButton({ onClick }: PlusButtonProps) { + return ( + + ); +} diff --git a/apps/re-member/src/components/community/CommunityFilter.tsx b/apps/re-member/src/components/community/CommunityFilter.tsx new file mode 100644 index 00000000..a1bbd6f7 --- /dev/null +++ b/apps/re-member/src/components/community/CommunityFilter.tsx @@ -0,0 +1,166 @@ +import { useState } from "react"; +import { IoChevronDown } from "react-icons/io5"; +import { CATEGORY } from "@/app/types/community"; +import { Button, Dropdown } from "../common"; + +interface CommunityFilterProps { + tab: string; +} + +export default function CommunityFilter({ tab }: CommunityFilterProps) { + const [isLatest, setIsLatest] = useState(true); + + const onClick = () => { + setIsLatest(!isLatest); + }; + return ( +
+
+ {tab === CATEGORY.INFORMATION && } + {tab === CATEGORY.QUESTION && } +
+ +
+ ); +} + +const INFORMATION_FILTERS: string[] = ["전체", "IT 소식", "채용 정보"]; + +function InformationFilterOptions() { + const [activeFilter, setActiveFilter] = useState(INFORMATION_FILTERS[0]); + + return ( +
+ {INFORMATION_FILTERS.map((filter) => ( + + ))} +
+ ); +} + +interface FilterOption { + label: string; + value: string; +} + +const LANGUAGE_OPTIONS: FilterOption[] = [ + { label: "언어", value: "all" }, + { label: "JavaScript", value: "javascript" }, + { label: "TypeScript", value: "typescript" }, + { label: "Python", value: "python" }, + { label: "Java", value: "java" }, + { label: "C/C++", value: "cpp" }, + { label: "Go", value: "go" }, +]; + +const FIELD_OPTIONS: FilterOption[] = [ + { label: "분야", value: "all" }, + { label: "프론트엔드", value: "frontend" }, + { label: "백엔드", value: "backend" }, + { label: "모바일", value: "mobile" }, + { label: "데브옵스", value: "devops" }, + { label: "데이터", value: "data" }, +]; + +const TECH_OPTIONS: FilterOption[] = [ + { label: "기술", value: "all" }, + { label: "React", value: "react" }, + { label: "Next.js", value: "nextjs" }, + { label: "Node.js", value: "nodejs" }, + { label: "Spring", value: "spring" }, + { label: "AWS", value: "aws" }, +]; + +function QuestionFilterOptions() { + const [language, setLanguage] = useState("all"); + const [field, setField] = useState("all"); + const [tech, setTech] = useState("all"); + + const getSelectedLabel = (options: FilterOption[], value: string) => { + return ( + options.find((opt) => opt.value === value)?.label ?? options[0].label + ); + }; + + return ( +
+ + {getSelectedLabel(LANGUAGE_OPTIONS, language)} + + + } + align="start" + > + {LANGUAGE_OPTIONS.map((option) => ( + setLanguage(option.value)} + > + {option.label} + + ))} + + + + {getSelectedLabel(FIELD_OPTIONS, field)} + + + } + align="start" + > + {FIELD_OPTIONS.map((option) => ( + setField(option.value)} + > + {option.label} + + ))} + + + + {getSelectedLabel(TECH_OPTIONS, tech)} + + + } + align="start" + > + {TECH_OPTIONS.map((option) => ( + setTech(option.value)} + > + {option.label} + + ))} + +
+ ); +} diff --git a/apps/re-member/src/components/community/CommunityPostItem.tsx b/apps/re-member/src/components/community/CommunityPostItem.tsx new file mode 100644 index 00000000..c22c3bf7 --- /dev/null +++ b/apps/re-member/src/components/community/CommunityPostItem.tsx @@ -0,0 +1,52 @@ +import { IoHeartOutline, IoChatbubbleOutline } from "react-icons/io5"; +import { useNavigate } from "react-router"; +import { Chip } from "../common"; +import { ROUTE } from "@/shared/config/route"; +import type { PostData } from "@/app/types/community"; + +interface CommunityPostItemProps { + postData: PostData; + chipLabel?: string; +} + +export default function CommunityPostItem({ + postData, + chipLabel, +}: CommunityPostItemProps) { + const { id, title, createdAt, author, generation, likeCount, commentCount } = + postData; + + const navigate = useNavigate(); + + return ( +
navigate(`${ROUTE.COMMUNITY}/${id}`)} + > +
+ {chipLabel && {chipLabel}} + {title} +
+
+
+ {createdAt} + | + + {author}({generation}) + +
+
+
+ + {likeCount} +
+
+ + {commentCount} +
+
+
+
+ ); +} diff --git a/apps/re-member/src/components/community/CommunityPostList.tsx b/apps/re-member/src/components/community/CommunityPostList.tsx new file mode 100644 index 00000000..09aff70b --- /dev/null +++ b/apps/re-member/src/components/community/CommunityPostList.tsx @@ -0,0 +1,71 @@ +import type { PostData } from "@/app/types/community"; +import CommunityPostItem from "./CommunityPostItem"; + +interface PostItem { + postData: PostData; + chipLabel?: string; +} + +const MOCK_POSTS: PostItem[] = [ + { + postData: { + id: 1, + title: "송년회 참석 투표에 참여해주세요!", + createdAt: "3분 전", + author: "한유진", + generation: 23, + likeCount: 12, + commentCount: 12, + }, + chipLabel: "공지", + }, + { + postData: { + id: 2, + title: "아자스입니다", + createdAt: "12분 전", + author: "한유진", + generation: 23, + likeCount: 12, + commentCount: 12, + }, + chipLabel: "공지", + }, + { + postData: { + id: 3, + title: "아자스입니다", + createdAt: "28분 전", + author: "한유진", + generation: 23, + likeCount: 12, + commentCount: 12, + }, + chipLabel: "공지", + }, + { + postData: { + id: 4, + title: "아자스입니다", + createdAt: "30분 전", + author: "한유진", + generation: 23, + likeCount: 12, + commentCount: 12, + }, + }, +]; + +export default function CommunityPostList() { + return ( +
+ {MOCK_POSTS.map((post) => ( + + ))} +
+ ); +} diff --git a/apps/re-member/src/components/community/communityPostDetail/PostDetailCommentInput.tsx b/apps/re-member/src/components/community/communityPostDetail/PostDetailCommentInput.tsx new file mode 100644 index 00000000..1f0a4117 --- /dev/null +++ b/apps/re-member/src/components/community/communityPostDetail/PostDetailCommentInput.tsx @@ -0,0 +1,42 @@ +import { useState } from "react"; +import { IoSend } from "react-icons/io5"; + +interface PostDetailCommentInputProps { + onSubmit?: (comment: string) => void; +} + +export default function PostDetailCommentInput({ + onSubmit, +}: PostDetailCommentInputProps) { + const [comment, setComment] = useState(""); + + const handleSubmit = () => { + if (comment.trim() && onSubmit) { + onSubmit(comment); + setComment(""); + } + }; + + return ( +
+
+
+