Skip to content
Merged
49 changes: 49 additions & 0 deletions apps/re-member/src/app/layout/CommunityLayout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Header
left={<Title>커뮤니티</Title>}
className="absolute left-0 right-0 top-0 bg-white"
/>

<div className="pt-header-height scrollbar-hide gap-3xl pb-bottom-padding flex h-full w-full flex-col overflow-y-auto">
<Tabs>
<Tabs.Item
icon={<BsFillClipboardFill />}
label="공지"
href={ROUTE.COMMUNITY_NOTICE}
/>
<Tabs.Item
icon={<BsFillEmojiSmileFill />}
label="자유"
href={ROUTE.COMMUNITY_FREE}
/>
<Tabs.Item
icon={<BsFillQuestionCircleFill />}
label="질문"
href={ROUTE.COMMUNITY_QUESTION}
/>
<Tabs.Item
icon={<BsFillInfoSquareFill />}
label="정보"
href={ROUTE.COMMUNITY_INFORMATION}
/>
</Tabs>
<Outlet />
</div>
<BottomNavbar />
</>
);
}
15 changes: 14 additions & 1 deletion apps/re-member/src/app/route/config.tsx
Original file line number Diff line number Diff line change
@@ -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([
{
Expand All @@ -19,6 +26,12 @@ export const router = createBrowserRouter([
path: `${ROUTE.ACTIVITY}/:id`,
children: [{ index: true, element: <StudyDetailPage /> }],
},
{
path: ROUTE.COMMUNITY,
element: <CommunityLayout />,
children: [{ index: true, element: <CommunityPage /> }],
},
{ path: `${ROUTE.COMMUNITY}/:id`, element: <CommunityDetailPage /> },
],
},
]);
35 changes: 35 additions & 0 deletions apps/re-member/src/app/types/community.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions apps/re-member/src/components/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
16 changes: 16 additions & 0 deletions apps/re-member/src/components/common/plus-button/PlusButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { IoAdd } from "react-icons/io5";

interface PlusButtonProps {
onClick?: () => void;
}

export default function PlusButton({ onClick }: PlusButtonProps) {
return (
<button
className="bg-primary absolute bottom-32 right-4 flex h-14 w-14 items-center justify-center rounded-full text-4xl text-white shadow-2xl"
onClick={onClick}
>
<IoAdd />
</button>
);
}
166 changes: 166 additions & 0 deletions apps/re-member/src/components/community/CommunityFilter.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>(true);

const onClick = () => {
setIsLatest(!isLatest);
};
return (
<div className="px-gutter gap-md flex items-center">
<div className="scrollbar-hide min-w-0 flex-1 overflow-x-auto">
{tab === CATEGORY.INFORMATION && <InformationFilterOptions />}
{tab === CATEGORY.QUESTION && <QuestionFilterOptions />}
</div>
<Button color="text" size="small" className="shrink-0" onClick={onClick}>
{isLatest ? "최신순" : "인기순"}
</Button>
</div>
);
}

const INFORMATION_FILTERS: string[] = ["전체", "IT 소식", "채용 정보"];

function InformationFilterOptions() {
const [activeFilter, setActiveFilter] = useState(INFORMATION_FILTERS[0]);

return (
<div className="gap-lg flex overflow-auto">
{INFORMATION_FILTERS.map((filter) => (
<Button
key={filter}
color={activeFilter === filter ? "outlineActive" : "ghost"}
size="small"
onClick={() => setActiveFilter(filter)}
>
{filter}
</Button>
))}
</div>
);
}

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<string>("all");
const [field, setField] = useState<string>("all");
const [tech, setTech] = useState<string>("all");

const getSelectedLabel = (options: FilterOption[], value: string) => {
return (
options.find((opt) => opt.value === value)?.label ?? options[0].label
);
};

return (
<div className="gap-md scrollbar-hide flex overflow-y-hidden overflow-x-scroll">
<Dropdown
trigger={
<Button
color={language === "all" ? "outlineDisabled" : "outlineActive"}
size="small"
className="shrink-0 whitespace-nowrap"
>
{getSelectedLabel(LANGUAGE_OPTIONS, language)}
<IoChevronDown className="ml-xs" />
</Button>
}
align="start"
>
{LANGUAGE_OPTIONS.map((option) => (
<Dropdown.Item
key={option.value}
onSelect={() => setLanguage(option.value)}
>
{option.label}
</Dropdown.Item>
))}
</Dropdown>

<Dropdown
trigger={
<Button
color={field === "all" ? "outlineDisabled" : "outlineActive"}
size="small"
className="shrink-0 whitespace-nowrap"
>
{getSelectedLabel(FIELD_OPTIONS, field)}
<IoChevronDown className="ml-xs" />
</Button>
}
align="start"
>
{FIELD_OPTIONS.map((option) => (
<Dropdown.Item
key={option.value}
onSelect={() => setField(option.value)}
>
{option.label}
</Dropdown.Item>
))}
</Dropdown>

<Dropdown
trigger={
<Button
color={tech === "all" ? "outlineDisabled" : "outlineActive"}
size="small"
className="shrink-0 whitespace-nowrap"
>
{getSelectedLabel(TECH_OPTIONS, tech)}
<IoChevronDown className="ml-xs" />
</Button>
}
align="start"
>
{TECH_OPTIONS.map((option) => (
<Dropdown.Item
key={option.value}
onSelect={() => setTech(option.value)}
>
{option.label}
</Dropdown.Item>
))}
</Dropdown>
</div>
);
}
52 changes: 52 additions & 0 deletions apps/re-member/src/components/community/CommunityPostItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className="border-b-gray-2 gap-sm px-gutter py-xl flex cursor-pointer flex-col border-b"
role="button"
onClick={() => navigate(`${ROUTE.COMMUNITY}/${id}`)}
>
<div className="gap-md flex items-center">
{chipLabel && <Chip>{chipLabel}</Chip>}
<span className="text-16-medium truncate text-black">{title}</span>
</div>
<div className="text-12-regular text-gray-4 flex items-center justify-between">
<div className="gap-xs flex items-center">
<span>{createdAt}</span>
<span>|</span>
<span>
{author}({generation})
</span>
</div>
<div className="gap-md flex items-center">
<div className="gap-xs flex items-center">
<IoHeartOutline className="size-4" />
<span>{likeCount}</span>
</div>
<div className="gap-xs flex items-center">
<IoChatbubbleOutline className="size-4" />
<span>{commentCount}</span>
</div>
</div>
</div>
</div>
);
}
Loading