Showing
17 changed files
with
431 additions
and
0 deletions
1 | +//@ts-nocheck | ||
2 | + | ||
3 | +import React from 'react'; | ||
4 | +import { Table as TableContainer, TableProps } from 'antd'; | ||
5 | +import { useWindowSize } from '@src/hooks'; | ||
6 | +import TableHeader from './TableHeader'; | ||
7 | + | ||
8 | +interface NewTableProps extends TableProps<any> { | ||
9 | + title: string; | ||
10 | + columns: Object[]; | ||
11 | + data: Object[]; | ||
12 | +} | ||
13 | + | ||
14 | +export default function Table({ | ||
15 | + title, | ||
16 | + columns, | ||
17 | + data, | ||
18 | + ...rest | ||
19 | +}: NewTableProps) { | ||
20 | + const [width, height] = useWindowSize(); | ||
21 | + | ||
22 | + const size = width > 1000 ? 'middle' : 'small'; | ||
23 | + const cellHeight = size === 'small' ? 39 : 55; | ||
24 | + const pageSize = Math.ceil((height * 2) / 3 / cellHeight); | ||
25 | + | ||
26 | + const emptyRowNum = pageSize - (data.length % pageSize); | ||
27 | + const emptyRow = columns.reduce( | ||
28 | + (acc, curr) => ((acc[curr['dataIndex']] = '-'), acc), | ||
29 | + {}, | ||
30 | + ); | ||
31 | + | ||
32 | + const body = [...data, ...Array(emptyRowNum || 0).fill(emptyRow)]; | ||
33 | + | ||
34 | + return ( | ||
35 | + <TableContainer | ||
36 | + size={size} | ||
37 | + title={() => <TableHeader title={title} />} | ||
38 | + columns={columns} | ||
39 | + dataSource={body} | ||
40 | + pagination={{ | ||
41 | + responsive: true, | ||
42 | + showLessItems: true, | ||
43 | + pageSize, | ||
44 | + }} | ||
45 | + {...rest} | ||
46 | + /> | ||
47 | + ); | ||
48 | +} |
1 | +import React from 'react'; | ||
2 | +import { Button } from 'antd'; | ||
3 | +import Link from 'next/link'; | ||
4 | +import { useRouter } from 'next/router'; | ||
5 | + | ||
6 | +export default function TableHeader({ title }) { | ||
7 | + const { query } = useRouter(); | ||
8 | + | ||
9 | + return ( | ||
10 | + <div className={'ant-table-title'}> | ||
11 | + <h2>{title} 게시판</h2> | ||
12 | + <Button> | ||
13 | + <Link href={`/${query.name}/create`}>{'글쓰기'}</Link> | ||
14 | + </Button> | ||
15 | + </div> | ||
16 | + ); | ||
17 | +} |
1 | +import Table from './Table'; | ||
2 | +import { | ||
3 | + makeArticleURLWithNumber, | ||
4 | + makeCategoryTableBody, | ||
5 | +} from '@shared/functions'; | ||
6 | +import { useRouter } from 'next/router'; | ||
7 | + | ||
8 | +export default function Category({ category, articleList }) { | ||
9 | + const router = useRouter(); | ||
10 | + | ||
11 | + // example | ||
12 | + const newData = makeCategoryTableBody(articleList); | ||
13 | + | ||
14 | + const handleRoute = ({ id }) => (e) => { | ||
15 | + e.preventDefault(); | ||
16 | + | ||
17 | + if (id === '-') return; | ||
18 | + | ||
19 | + const { | ||
20 | + query: { name: categoryName }, | ||
21 | + } = router; | ||
22 | + const URL = makeArticleURLWithNumber(categoryName as string, id); | ||
23 | + | ||
24 | + router.push(URL); | ||
25 | + }; | ||
26 | + | ||
27 | + return ( | ||
28 | + <div className={'outer-container category-table-container'}> | ||
29 | + <Table | ||
30 | + title={category} | ||
31 | + columns={[ | ||
32 | + { | ||
33 | + key: '1', | ||
34 | + title: 'No.', | ||
35 | + dataIndex: 'id', | ||
36 | + align: 'center', | ||
37 | + }, | ||
38 | + { key: '2', title: '제목', dataIndex: 'title', width: '70%' }, | ||
39 | + { | ||
40 | + key: '3', | ||
41 | + title: '작성자', | ||
42 | + dataIndex: 'author', | ||
43 | + align: 'center', | ||
44 | + }, | ||
45 | + { | ||
46 | + key: '4', | ||
47 | + title: '등록일', | ||
48 | + dataIndex: 'created_date', | ||
49 | + align: 'center', | ||
50 | + }, | ||
51 | + ]} | ||
52 | + data={newData} | ||
53 | + onRow={(record) => ({ | ||
54 | + onClick: handleRoute(record), | ||
55 | + })} | ||
56 | + /> | ||
57 | + </div> | ||
58 | + ); | ||
59 | +} |
1 | +import React from 'react'; | ||
2 | +import { Form } from 'antd'; | ||
3 | + | ||
4 | +export default function Inputs({ forms }) { | ||
5 | + return ( | ||
6 | + <> | ||
7 | + {forms.map(({ form, input: { Item, value, ...rest } }) => ( | ||
8 | + <Form.Item {...form} required key={value}> | ||
9 | + <Item placeholder={value} name={value} {...rest} /> | ||
10 | + </Form.Item> | ||
11 | + ))} | ||
12 | + </> | ||
13 | + ); | ||
14 | +} |
project/packages/web/src/views/Main/Card.tsx
0 → 100644
1 | +import Link from 'next/link'; | ||
2 | +import { Card as CardItem } from 'antd'; | ||
3 | +import { Row } from './Row'; | ||
4 | + | ||
5 | +function MoreButton(category) { | ||
6 | + return <Link href={`/category/${category}`}>더보기</Link>; | ||
7 | +} | ||
8 | + | ||
9 | +export default function Card({ category, posts, ...rest }) { | ||
10 | + const postsNum = posts.length; | ||
11 | + const emptyRows = postsNum < 5 ? Array(5 - postsNum).fill(null) : []; | ||
12 | + const sliced = postsNum > 5 ? posts.slice(postsNum - 5, postsNum) : posts; | ||
13 | + | ||
14 | + return ( | ||
15 | + <CardItem title={category} extra={MoreButton(category)} {...rest}> | ||
16 | + {sliced.map(({ title, id }) => ( | ||
17 | + <Row key={title} category={category} title={title} id={id} /> | ||
18 | + ))} | ||
19 | + {emptyRows.map((_, idx) => ( | ||
20 | + <div className={'card-row'} key={idx} /> | ||
21 | + ))} | ||
22 | + </CardItem> | ||
23 | + ); | ||
24 | +} |
project/packages/web/src/views/Main/Row.tsx
0 → 100644
1 | +import { useRouter } from 'next/router'; | ||
2 | +import { makeArticleURLWithNumber } from '@src/shared/functions'; | ||
3 | + | ||
4 | +export const Row = ({ category, title, id }) => { | ||
5 | + const router = useRouter(); | ||
6 | + const sliced = title.length > 20 ? title.substr(0, 20) + '...' : title; | ||
7 | + | ||
8 | + const handleClickArticle = () => { | ||
9 | + const URL = makeArticleURLWithNumber(category, id); | ||
10 | + router.push(URL); | ||
11 | + }; | ||
12 | + | ||
13 | + return ( | ||
14 | + <div className={'card-row has-content'} onClick={handleClickArticle}> | ||
15 | + <h2 className={'card-row-title'}>{sliced}</h2> | ||
16 | + <span className={'card-row-recomment'}>{'?'}</span> | ||
17 | + </div> | ||
18 | + ); | ||
19 | +}; |
1 | +import Card from '@src/views/Main/Card'; | ||
2 | + | ||
3 | +export default function Main({ categories, posts }) { | ||
4 | + return ( | ||
5 | + <div className={'outer-container main-card-container'}> | ||
6 | + {categories?.map((category) => { | ||
7 | + const filtered = posts.filter((post) => post.category === category); | ||
8 | + return ( | ||
9 | + <Card | ||
10 | + key={category} | ||
11 | + category={category} | ||
12 | + posts={filtered} | ||
13 | + className={'main-card'} | ||
14 | + /> | ||
15 | + ); | ||
16 | + })} | ||
17 | + </div> | ||
18 | + ); | ||
19 | +} |
1 | +import React, { useState } from 'react'; | ||
2 | +import { Comment as CommentItem, Divider } from 'antd'; | ||
3 | + | ||
4 | +import Profile from './Profile'; | ||
5 | +import Datetime from './Datetime'; | ||
6 | +import Like from './Like'; | ||
7 | +import Dislike from './Dislike'; | ||
8 | + | ||
9 | +export default function Comment({ | ||
10 | + author, | ||
11 | + content, | ||
12 | + created_date, | ||
13 | + idx, | ||
14 | + commentsNum, | ||
15 | +}) { | ||
16 | + const [likes, setLikes] = useState(0); | ||
17 | + const [dislikes, setDislikes] = useState(0); | ||
18 | + const [action, setAction] = useState(null); | ||
19 | + | ||
20 | + const like = () => { | ||
21 | + setLikes(1); | ||
22 | + setDislikes(0); | ||
23 | + setAction('liked'); | ||
24 | + }; | ||
25 | + | ||
26 | + const dislike = () => { | ||
27 | + setLikes(0); | ||
28 | + setDislikes(1); | ||
29 | + setAction('disliked'); | ||
30 | + }; | ||
31 | + | ||
32 | + return ( | ||
33 | + <> | ||
34 | + <CommentItem | ||
35 | + actions={[ | ||
36 | + Like({ action, like, likes }), | ||
37 | + Dislike({ | ||
38 | + action, | ||
39 | + dislike, | ||
40 | + dislikes, | ||
41 | + }), | ||
42 | + ]} | ||
43 | + author={<a>{author}</a>} | ||
44 | + avatar={Profile({ src: '', author })} | ||
45 | + content={<div dangerouslySetInnerHTML={{ __html: content }} />} | ||
46 | + datetime={Datetime({ created_date })} | ||
47 | + /> | ||
48 | + {commentsNum - 1 !== idx && <Divider />} | ||
49 | + </> | ||
50 | + ); | ||
51 | +} |
1 | +import React from 'react'; | ||
2 | +import { Tooltip } from 'antd'; | ||
3 | +import moment from 'moment'; | ||
4 | + | ||
5 | +export default function Datetime({ created_date }) { | ||
6 | + return ( | ||
7 | + <Tooltip title={moment(created_date).format('YYYY-MM-DD HH:mm:ss')}> | ||
8 | + <span>{moment(created_date).fromNow()}</span> | ||
9 | + </Tooltip> | ||
10 | + ); | ||
11 | +} |
1 | +import React, { createElement } from 'react'; | ||
2 | +import { Tooltip } from 'antd'; | ||
3 | +import { DislikeOutlined, DislikeFilled } from '@ant-design/icons'; | ||
4 | + | ||
5 | +export default function Dislike({ dislike, action, dislikes }) { | ||
6 | + return ( | ||
7 | + <Tooltip key="comment-basic-dislike" title="Dislike"> | ||
8 | + <span onClick={dislike}> | ||
9 | + {createElement(action === 'disliked' ? DislikeFilled : DislikeOutlined)} | ||
10 | + <span className="comment-action">{dislikes}</span> | ||
11 | + </span> | ||
12 | + </Tooltip> | ||
13 | + ); | ||
14 | +} |
1 | +import React, { createElement } from 'react'; | ||
2 | +import { Tooltip } from 'antd'; | ||
3 | +import { LikeOutlined, LikeFilled } from '@ant-design/icons'; | ||
4 | + | ||
5 | +export default function Like({ action, like, likes }) { | ||
6 | + return ( | ||
7 | + <Tooltip key="comment-basic-like" title="Like"> | ||
8 | + <span onClick={like}> | ||
9 | + {createElement(action === 'liked' ? LikeFilled : LikeOutlined)} | ||
10 | + <span className="comment-action">{likes}</span> | ||
11 | + </span> | ||
12 | + </Tooltip> | ||
13 | + ); | ||
14 | +} |
1 | +import React from 'react'; | ||
2 | +import TextArea from 'antd/lib/input/TextArea'; | ||
3 | +import { Button } from 'antd'; | ||
4 | +import { useMutation } from '@apollo/client'; | ||
5 | +import { CREATE_COMMENT } from '@src/gql/create-comment'; | ||
6 | + | ||
7 | +export default function Submit({ title, postId, addCommentList }) { | ||
8 | + const [content, setContent] = React.useState(''); | ||
9 | + const [createComment] = useMutation(CREATE_COMMENT); | ||
10 | + | ||
11 | + const handleChange = (e) => { | ||
12 | + setContent(e.target.value); | ||
13 | + }; | ||
14 | + | ||
15 | + const handleSubmit = async () => { | ||
16 | + const { data } = await createComment({ | ||
17 | + variables: { input: { content, post_id: postId } }, | ||
18 | + }); | ||
19 | + setContent(''); | ||
20 | + data && addCommentList(data.createComment); | ||
21 | + }; | ||
22 | + | ||
23 | + return ( | ||
24 | + <> | ||
25 | + <TextArea | ||
26 | + value={content} | ||
27 | + onChange={handleChange} | ||
28 | + placeholder={'댓글을 입력하세요'} | ||
29 | + autoSize={{ minRows: 3, maxRows: 5 }} | ||
30 | + className={'comments-textarea'} | ||
31 | + /> | ||
32 | + <Button | ||
33 | + type={'primary'} | ||
34 | + size={'large'} | ||
35 | + onClick={handleSubmit} | ||
36 | + className={'comments-submit-button'} | ||
37 | + > | ||
38 | + {title} | ||
39 | + </Button> | ||
40 | + </> | ||
41 | + ); | ||
42 | +} |
1 | +import React, { useState } from 'react'; | ||
2 | +import { Card } from 'antd'; | ||
3 | + | ||
4 | +import Submit from './Submit'; | ||
5 | +import Comment from './Comment'; | ||
6 | + | ||
7 | +export default function CommentsContainer({ comments, postId }) { | ||
8 | + const [tempComments, setTempComments] = useState(comments); | ||
9 | + const commentsNum = tempComments?.length; | ||
10 | + | ||
11 | + const addCommentList = (data) => { | ||
12 | + setTempComments((prev) => [...prev, data]); | ||
13 | + }; | ||
14 | + | ||
15 | + React.useEffect(() => { | ||
16 | + comments && setTempComments(comments); | ||
17 | + }, [comments]); | ||
18 | + | ||
19 | + return ( | ||
20 | + <Card className={'post-comments'}> | ||
21 | + <h1 className={'post-comments-num'}>{`COMMENTS (${commentsNum})`}</h1> | ||
22 | + {tempComments?.map(({ author, content, created_date }, idx) => ( | ||
23 | + <Comment | ||
24 | + key={created_date} | ||
25 | + author={author} | ||
26 | + content={content} | ||
27 | + created_date={created_date} | ||
28 | + idx={idx} | ||
29 | + commentsNum={commentsNum} | ||
30 | + /> | ||
31 | + ))} | ||
32 | + <Submit title={'작성'} postId={postId} addCommentList={addCommentList} /> | ||
33 | + </Card> | ||
34 | + ); | ||
35 | +} |
1 | +import React from 'react'; | ||
2 | +import { Card, Descriptions } from 'antd'; | ||
3 | +import moment from 'moment'; | ||
4 | + | ||
5 | +export default function Content({ | ||
6 | + id, | ||
7 | + author, | ||
8 | + category, | ||
9 | + created_date, | ||
10 | + title, | ||
11 | + content, | ||
12 | +}) { | ||
13 | + return ( | ||
14 | + <Card className={'post-content'}> | ||
15 | + <Descriptions title={title} layout={'horizontal'} column={3}> | ||
16 | + <Descriptions.Item label={'작성자'} span={3}> | ||
17 | + {author} | ||
18 | + </Descriptions.Item> | ||
19 | + <Descriptions.Item | ||
20 | + label={'작성일'} | ||
21 | + span={3} | ||
22 | + style={{ textAlign: 'right' }} | ||
23 | + > | ||
24 | + {moment(created_date).format('YYYY.MM.DD HH:mm:ss')} | ||
25 | + </Descriptions.Item> | ||
26 | + <Descriptions.Item label={'게시글'} span={3}> | ||
27 | + <br /> | ||
28 | + <div dangerouslySetInnerHTML={{ __html: content }} /> | ||
29 | + </Descriptions.Item> | ||
30 | + </Descriptions> | ||
31 | + </Card> | ||
32 | + ); | ||
33 | +} |
-
Please register or login to post a comment