Merge branch 'allinone' into 'master'
Check k8s setting and Add Allinone Page See merge request !4
Showing
44 changed files
with
1182 additions
and
52 deletions
... | @@ -28,7 +28,7 @@ export class PostService { | ... | @@ -28,7 +28,7 @@ export class PostService { |
28 | async findSome(input: Partial<GetPostInput>): Promise<Post[]> { | 28 | async findSome(input: Partial<GetPostInput>): Promise<Post[]> { |
29 | return this.postRepository | 29 | return this.postRepository |
30 | .createQueryBuilder('post') | 30 | .createQueryBuilder('post') |
31 | - .where('post.id like :id', { id: input.id }) | 31 | + .orWhere('post.id = :id', { id: input.id }) |
32 | .orWhere('post.author like :author', { author: input.author }) | 32 | .orWhere('post.author like :author', { author: input.author }) |
33 | .orWhere('post.category like :category', { category: input.category }) | 33 | .orWhere('post.category like :category', { category: input.category }) |
34 | .getMany() | 34 | .getMany() | ... | ... |
project/packages/web/assets/styles/app.scss
0 → 100644
1 | +@import './shared.scss'; | ||
2 | + | ||
3 | +html, | ||
4 | +body { | ||
5 | + height: 100%; | ||
6 | + width: 100%; | ||
7 | +} | ||
8 | + | ||
9 | +* { | ||
10 | + margin: 0; | ||
11 | + padding: 0; | ||
12 | + text-decoration: none; | ||
13 | +} | ||
14 | + | ||
15 | +#__next { | ||
16 | + height: 100%; | ||
17 | + min-width: 236px; | ||
18 | +} | ||
19 | + | ||
20 | +.app-layout { | ||
21 | + min-height: 100vh; | ||
22 | + height: auto; | ||
23 | + display: block; | ||
24 | +} | ||
25 | + | ||
26 | +.app-header { | ||
27 | + @media screen and (max-width: $medium_tablet_width) { | ||
28 | + padding: 0px; | ||
29 | + position: fixed; | ||
30 | + width: 100%; | ||
31 | + z-index: 1; | ||
32 | + | ||
33 | + .ant-menu-submenu-horizontal { | ||
34 | + float: right; | ||
35 | + } | ||
36 | + } | ||
37 | +} | ||
38 | + | ||
39 | +.outer-container { | ||
40 | + width: 100%; | ||
41 | + max-width: $window_max_width; | ||
42 | + min-height: calc(100vh - 64px); | ||
43 | + padding: 12px 50px; | ||
44 | + margin: 0 auto; | ||
45 | + | ||
46 | + overflow-y: scroll; | ||
47 | + | ||
48 | + @media screen and (max-width: $medium_tablet_width) { | ||
49 | + padding: 76px 16px 12px 16px; | ||
50 | + min-height: 100vh; | ||
51 | + } | ||
52 | + | ||
53 | + @media screen and (max-width: $small_smart_phone_width) { | ||
54 | + padding: 10px 8px; | ||
55 | + } | ||
56 | +} |
1 | +@import './shared.scss'; | ||
2 | + | ||
3 | +.category-table-container { | ||
4 | + display: flex; | ||
5 | + flex-direction: column; | ||
6 | + align-items: stretch; | ||
7 | + justify-content: center; | ||
8 | + | ||
9 | + .ant-table { | ||
10 | + border-radius: $border_radius; | ||
11 | + overflow: hidden; | ||
12 | + | ||
13 | + .ant-table-title { | ||
14 | + width: 100%; | ||
15 | + display: flex; | ||
16 | + justify-content: space-between; | ||
17 | + align-items: center; | ||
18 | + | ||
19 | + margin: 0 4px; | ||
20 | + @media screen and (max-width: $medium_smart_phone_width) { | ||
21 | + h2 { | ||
22 | + font-size: 14px; | ||
23 | + } | ||
24 | + } | ||
25 | + } | ||
26 | + | ||
27 | + tr { | ||
28 | + @media screen and (max-width: $small_tablet_width) { | ||
29 | + .ant-table-cell:nth-child(3) { | ||
30 | + display: none; | ||
31 | + } | ||
32 | + } | ||
33 | + | ||
34 | + @media screen and (max-width: $medium_smart_phone_width) { | ||
35 | + .ant-table-cell:nth-child(1) { | ||
36 | + display: none; | ||
37 | + } | ||
38 | + | ||
39 | + .ant-table-cell:nth-child(2) { | ||
40 | + width: 100%; | ||
41 | + } | ||
42 | + } | ||
43 | + | ||
44 | + @media screen and (max-width: $small_smart_phone_width) { | ||
45 | + .ant-table-cell:nth-child(2) { | ||
46 | + font-size: 12px; | ||
47 | + } | ||
48 | + .ant-table-cell:nth-child(4) { | ||
49 | + font-size: 11px; | ||
50 | + } | ||
51 | + } | ||
52 | + } | ||
53 | + } | ||
54 | +} |
1 | +@import './shared.scss'; | ||
2 | + | ||
3 | +.create-container { | ||
4 | + display: flex; | ||
5 | + flex-direction: column; | ||
6 | + justify-content: center; | ||
7 | + align-items: stretch; | ||
8 | + | ||
9 | + .form-container { | ||
10 | + border-radius: $border_radius; | ||
11 | + background-color: white; | ||
12 | + padding: 24px; | ||
13 | + | ||
14 | + form { | ||
15 | + height: 100%; | ||
16 | + | ||
17 | + .form-button { | ||
18 | + float: right; | ||
19 | + | ||
20 | + &.cancel-button { | ||
21 | + margin-right: 8px; | ||
22 | + } | ||
23 | + } | ||
24 | + | ||
25 | + #form-textarea { | ||
26 | + height: 50vh; | ||
27 | + } | ||
28 | + } | ||
29 | + } | ||
30 | +} |
project/packages/web/assets/styles/main.scss
0 → 100644
1 | +@import './shared.scss'; | ||
2 | + | ||
3 | +.main-card-container { | ||
4 | + display: flex; | ||
5 | + flex-wrap: wrap; | ||
6 | + justify-content: space-evenly; | ||
7 | + align-items: center; | ||
8 | + | ||
9 | + @media screen and (max-width: $medium_tablet_width) { | ||
10 | + display: block; | ||
11 | + } | ||
12 | + | ||
13 | + .main-card { | ||
14 | + width: 47%; | ||
15 | + height: 48%; | ||
16 | + | ||
17 | + border-radius: $border_radius; | ||
18 | + | ||
19 | + @media screen and (max-width: $medium_tablet_width) { | ||
20 | + width: 100%; | ||
21 | + height: 300px; | ||
22 | + margin-bottom: 12px; | ||
23 | + } | ||
24 | + | ||
25 | + @media screen and (max-width: $large_smart_phone_width) { | ||
26 | + height: 240px; | ||
27 | + } | ||
28 | + | ||
29 | + @media screen and (max-width: $medium_smart_phone_width) { | ||
30 | + height: 220px; | ||
31 | + } | ||
32 | + | ||
33 | + .ant-card-body { | ||
34 | + height: calc(100% - 58px); | ||
35 | + | ||
36 | + display: flex; | ||
37 | + flex-direction: column; | ||
38 | + justify-content: space-evenly; | ||
39 | + align-items: stretch; | ||
40 | + | ||
41 | + padding: 18px; | ||
42 | + | ||
43 | + .card-row { | ||
44 | + display: flex; | ||
45 | + justify-content: space-between; | ||
46 | + align-items: center; | ||
47 | + border-radius: $border_radius; | ||
48 | + padding: 5px 18px; | ||
49 | + height: 45px; | ||
50 | + | ||
51 | + @media screen and (max-width: $medium_tablet_width) { | ||
52 | + &:nth-child(4), | ||
53 | + &:nth-child(5) { | ||
54 | + display: none; | ||
55 | + } | ||
56 | + } | ||
57 | + | ||
58 | + &.has-content { | ||
59 | + transition: background-color 0.3s; | ||
60 | + padding: 2px 12px; | ||
61 | + | ||
62 | + &:hover { | ||
63 | + background-color: rgba(26, 144, 255, 0.2); | ||
64 | + transition: background-color 0.3s; | ||
65 | + } | ||
66 | + } | ||
67 | + | ||
68 | + .card-row-title { | ||
69 | + margin: 0; | ||
70 | + font-size: 17px; | ||
71 | + font-weight: 400; | ||
72 | + | ||
73 | + @media screen and (max-width: $large_smart_phone_width) { | ||
74 | + font-size: 14px; | ||
75 | + } | ||
76 | + } | ||
77 | + | ||
78 | + .card-row-recomment { | ||
79 | + color: $red; | ||
80 | + font-weight: 600; | ||
81 | + margin-left: 16px; | ||
82 | + | ||
83 | + @media screen and (max-width: $large_smart_phone_width) { | ||
84 | + font-size: 13px; | ||
85 | + } | ||
86 | + | ||
87 | + @media screen and (max-width: $medium_smart_phone_width) { | ||
88 | + display: none; | ||
89 | + } | ||
90 | + } | ||
91 | + } | ||
92 | + } | ||
93 | + } | ||
94 | +} |
project/packages/web/assets/styles/post.scss
0 → 100644
1 | +@import './shared.scss'; | ||
2 | + | ||
3 | +.post-container { | ||
4 | + display: flex; | ||
5 | + flex-direction: column; | ||
6 | + justify-content: space-between; | ||
7 | + align-items: stretch; | ||
8 | + | ||
9 | + height: 100%; | ||
10 | + | ||
11 | + .post-content { | ||
12 | + flex: 1; | ||
13 | + border-radius: $border_radius; | ||
14 | + margin-bottom: 24px; | ||
15 | + } | ||
16 | + | ||
17 | + .post-comments { | ||
18 | + min-height: $comment_height; | ||
19 | + border-radius: $border_radius; | ||
20 | + overflow-y: scroll; | ||
21 | + | ||
22 | + .post-comments-num { | ||
23 | + font-size: 14px; | ||
24 | + color: #888; | ||
25 | + } | ||
26 | + } | ||
27 | +} | ||
28 | + | ||
29 | +.comments-textarea { | ||
30 | + margin-top: 12px; | ||
31 | +} | ||
32 | + | ||
33 | +.comments-submit-button { | ||
34 | + float: right; | ||
35 | + margin-top: 8px; | ||
36 | +} |
1 | +$window_max_width: 1240px; | ||
2 | +$header_height: 64px; | ||
3 | +$border_radius: 10px; | ||
4 | + | ||
5 | +$comment_height: 158px; | ||
6 | + | ||
7 | +// color | ||
8 | +$black: #333; | ||
9 | +$red: #eb4d4b; | ||
10 | + | ||
11 | +// media query | ||
12 | +$large_tablet_width: 1024px; | ||
13 | +$medium_tablet_width: 768px; | ||
14 | +$small_tablet_width: 500px; | ||
15 | +$large_smart_phone_width: 444px; | ||
16 | +$medium_smart_phone_width: 370px; | ||
17 | +$small_smart_phone_width: 300px; |
... | @@ -20,7 +20,8 @@ | ... | @@ -20,7 +20,8 @@ |
20 | "graphql": "15.3.0", | 20 | "graphql": "15.3.0", |
21 | "next": "latest", | 21 | "next": "latest", |
22 | "react": "^16.13.1", | 22 | "react": "^16.13.1", |
23 | - "react-dom": "^16.13.1" | 23 | + "react-dom": "^16.13.1", |
24 | + "sass": "^1.34.1" | ||
24 | }, | 25 | }, |
25 | "devDependencies": { | 26 | "devDependencies": { |
26 | "@graphql-codegen/cli": "^1.17.8", | 27 | "@graphql-codegen/cli": "^1.17.8", | ... | ... |
1 | +import React from 'react'; | ||
2 | +import { useRouter } from 'next/router'; | ||
3 | +import { useQuery } from '@apollo/client'; | ||
4 | +import { GET_POST_WITH_COMMENTS } from '@src/gql/post-with-comments'; | ||
5 | +import Content from '@src/views/Post/Content'; | ||
6 | +import Comments from '@src/views/Post/Comment'; | ||
7 | + | ||
8 | +export default function ArticlePage() { | ||
9 | + const { query } = useRouter(); | ||
10 | + | ||
11 | + if (!query.num) { | ||
12 | + return null; // or redirect 404 ? | ||
13 | + } | ||
14 | + | ||
15 | + const { error, data } = useQuery(GET_POST_WITH_COMMENTS, { | ||
16 | + variables: { | ||
17 | + post_id: Number(query.num), | ||
18 | + inputComment: { post_id: Number(query.num) }, | ||
19 | + }, | ||
20 | + }); | ||
21 | + if (error) console.log(JSON.stringify(error, null, 2)); | ||
22 | + | ||
23 | + const post = data?.getPost || {}; | ||
24 | + | ||
25 | + return ( | ||
26 | + <div className={'outer-container post-container'}> | ||
27 | + <Content {...post} /> | ||
28 | + <Comments comments={data?.getSomeComments || []} postId={post.id} /> | ||
29 | + </div> | ||
30 | + ); | ||
31 | +} |
project/packages/web/pages/[name]/create.tsx
0 → 100644
1 | +import React from 'react'; | ||
2 | +import { useRouter } from 'next/router'; | ||
3 | +import { useMutation } from '@apollo/client'; | ||
4 | +import { CREATE_POST } from '@src/gql/create-post'; | ||
5 | +import { Form, Input } from 'antd'; | ||
6 | +import TextArea from 'antd/lib/input/TextArea'; | ||
7 | + | ||
8 | +import { CreateButtons, CreateInputs } from '@src/views/Create'; | ||
9 | + | ||
10 | +export default function CreatePage() { | ||
11 | + const router = useRouter(); | ||
12 | + const [createPost] = useMutation(CREATE_POST); | ||
13 | + const [form] = Form.useForm(); | ||
14 | + const [contents, setContents] = React.useState({ | ||
15 | + title: '', | ||
16 | + description: '', | ||
17 | + }); | ||
18 | + | ||
19 | + const handleChange = (e) => { | ||
20 | + const { | ||
21 | + target: { name, value }, | ||
22 | + } = e; | ||
23 | + | ||
24 | + setContents({ | ||
25 | + ...contents, | ||
26 | + [name]: value, | ||
27 | + }); | ||
28 | + }; | ||
29 | + | ||
30 | + const handleSubmit = async (e) => { | ||
31 | + e.preventDefault(); | ||
32 | + const { title, description } = contents; | ||
33 | + const { | ||
34 | + query: { name }, | ||
35 | + } = router; | ||
36 | + | ||
37 | + if (!(title && description)) { | ||
38 | + alert('필수항목을 모두 입력해주세요'); | ||
39 | + return; | ||
40 | + } | ||
41 | + | ||
42 | + const { data } = await createPost({ | ||
43 | + variables: { | ||
44 | + input: { | ||
45 | + category: name, | ||
46 | + content: description, | ||
47 | + title, | ||
48 | + }, | ||
49 | + }, | ||
50 | + }); | ||
51 | + | ||
52 | + router.push(`/${name}/article?num=${data.createPost.id}`); | ||
53 | + }; | ||
54 | + | ||
55 | + const handleCancel = () => { | ||
56 | + router.back(); | ||
57 | + }; | ||
58 | + | ||
59 | + return ( | ||
60 | + <div className={'outer-container create-container'}> | ||
61 | + <div className={'form-container'}> | ||
62 | + <Form form={form} layout={'vertical'}> | ||
63 | + <CreateInputs | ||
64 | + forms={[ | ||
65 | + { | ||
66 | + form: { | ||
67 | + label: '글제목', | ||
68 | + tooltip: '게시글 제목은 필수항목입니다', | ||
69 | + }, | ||
70 | + input: { | ||
71 | + Item: Input, | ||
72 | + value: 'title', | ||
73 | + onChange: handleChange, | ||
74 | + }, | ||
75 | + }, | ||
76 | + { | ||
77 | + form: { | ||
78 | + label: '글내용', | ||
79 | + tooltip: '게시글의 내용은 필수항목입니다', | ||
80 | + }, | ||
81 | + input: { | ||
82 | + Item: TextArea, | ||
83 | + value: 'description', | ||
84 | + id: 'form-textarea', | ||
85 | + onChange: handleChange, | ||
86 | + }, | ||
87 | + }, | ||
88 | + ]} | ||
89 | + /> | ||
90 | + <CreateButtons | ||
91 | + buttons={[ | ||
92 | + { | ||
93 | + title: 'Submit', | ||
94 | + onClick: handleSubmit, | ||
95 | + type: 'primary', | ||
96 | + className: 'form-button submit-button', | ||
97 | + }, | ||
98 | + { | ||
99 | + title: 'Cancel', | ||
100 | + onClick: handleCancel, | ||
101 | + className: 'form-button cancel-button', | ||
102 | + }, | ||
103 | + ]} | ||
104 | + /> | ||
105 | + </Form> | ||
106 | + </div> | ||
107 | + </div> | ||
108 | + ); | ||
109 | +} |
1 | -import { AppProps } from "next/app"; | 1 | +import Router from 'next/router'; |
2 | -import { ApolloProvider } from "@apollo/client"; | 2 | +import { AppProps } from 'next/app'; |
3 | -import { useApollo } from "../lib/apollo"; | 3 | +import Head from 'next/head'; |
4 | -import "antd/dist/antd.css"; | 4 | + |
5 | +import { ApolloProvider } from '@apollo/client'; | ||
6 | +import { useApollo } from '../lib/apollo'; | ||
7 | + | ||
8 | +import 'antd/dist/antd.css'; | ||
9 | +import '../assets/styles/app.scss'; | ||
10 | +import '../assets/styles/main.scss'; | ||
11 | +import '../assets/styles/category.scss'; | ||
12 | +import '../assets/styles/post.scss'; | ||
13 | +import '../assets/styles/create.scss'; | ||
14 | + | ||
15 | +import Layout from 'antd/lib/layout/layout'; | ||
16 | +import Header from '@components/Header'; | ||
17 | +import Loader, { startLoading, finishLoading } from '@components/Loader'; | ||
18 | + | ||
19 | +Router.events.on('routeChangeStart', startLoading); | ||
20 | +Router.events.on('routeChangeComplete', finishLoading); | ||
21 | +Router.events.on('routeChangeError', finishLoading); | ||
5 | 22 | ||
6 | export default function App({ Component, pageProps }: AppProps) { | 23 | export default function App({ Component, pageProps }: AppProps) { |
7 | const apolloClient = useApollo(pageProps); | 24 | const apolloClient = useApollo(pageProps); |
8 | 25 | ||
9 | return ( | 26 | return ( |
27 | + <> | ||
28 | + <Head> | ||
29 | + <meta | ||
30 | + name={'viewport'} | ||
31 | + content={ | ||
32 | + 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' | ||
33 | + } | ||
34 | + /> | ||
35 | + </Head> | ||
36 | + <Loader /> | ||
37 | + <Layout className={'app-layout'}> | ||
10 | <ApolloProvider client={apolloClient}> | 38 | <ApolloProvider client={apolloClient}> |
39 | + <Header /> | ||
11 | <Component {...pageProps} /> | 40 | <Component {...pageProps} /> |
12 | </ApolloProvider> | 41 | </ApolloProvider> |
42 | + </Layout> | ||
43 | + </> | ||
13 | ); | 44 | ); |
14 | } | 45 | } | ... | ... |
project/packages/web/pages/about.tsx
deleted
100644 → 0
1 | +import { useRouter } from 'next/router'; | ||
2 | +import { useQuery } from '@apollo/client'; | ||
3 | + | ||
4 | +import { GET_SOME_POSTS } from '@src/gql/get-some-posts'; | ||
5 | + | ||
6 | +import Category from '@src/views/Category'; | ||
7 | + | ||
8 | +export default function CategoryPage() { | ||
9 | + const { | ||
10 | + query: { name }, | ||
11 | + } = useRouter(); | ||
12 | + | ||
13 | + const { error, data } = useQuery(GET_SOME_POSTS, { | ||
14 | + variables: { | ||
15 | + input: { category: name }, | ||
16 | + }, | ||
17 | + }); | ||
18 | + | ||
19 | + if (error) console.log(JSON.stringify(error, null, 2)); | ||
20 | + | ||
21 | + const getCategoryPosts = data?.getSomePosts || []; | ||
22 | + const articleList = getCategoryPosts.filter((post) => post.category === name); | ||
23 | + | ||
24 | + return <Category category={name} articleList={articleList} />; | ||
25 | +} |
1 | -import { GetPostInput, Post } from "@graphql-community/shared"; | 1 | +import { useQuery } from '@apollo/client'; |
2 | -import { useQuery, gql } from "@apollo/client"; | ||
3 | -import { message } from "antd"; | ||
4 | 2 | ||
5 | -const GET_SOME_POST_QUERY = gql` | 3 | +import { GET_ALL_POSTS } from '@src/gql/get-all-posts'; |
6 | - query GetSomePosts($getSomePostInput: GetPostInput!) { | ||
7 | - getSomePosts(input: $getSomePostInput) { | ||
8 | - author | ||
9 | - category | ||
10 | - } | ||
11 | - } | ||
12 | -`; | ||
13 | 4 | ||
14 | -const Index = () => { | 5 | +import Main from '@views/Main'; |
15 | - const { data, error } = useQuery< | 6 | + |
16 | - { getSomePosts: Post[] }, | 7 | +export default function IndexPage() { |
17 | - { getSomePostInput: GetPostInput } | 8 | + const { error, data } = useQuery(GET_ALL_POSTS); |
18 | - >(GET_SOME_POST_QUERY, { | ||
19 | - variables: { | ||
20 | - getSomePostInput: { | ||
21 | - id: 1, | ||
22 | - }, | ||
23 | - }, | ||
24 | - }); | ||
25 | if (error) console.log(JSON.stringify(error, null, 2)); | 9 | if (error) console.log(JSON.stringify(error, null, 2)); |
26 | 10 | ||
27 | - return ( | 11 | + let categories = []; |
28 | - <> | 12 | + let getAllPosts = data?.getAllPosts || []; |
29 | - <div onClick={() => message.success("hi")}>index </div> | 13 | + |
30 | - <div>{data?.getSomePosts[0].author}</div> | 14 | + getAllPosts.forEach((post) => { |
31 | - <div>{data?.getSomePosts[0].category}</div> | 15 | + if (!categories.find((category) => post.category === category)) { |
32 | - </> | 16 | + categories.push(post.category); |
33 | - ); | 17 | + } |
34 | -}; | 18 | + }); |
35 | 19 | ||
36 | -export default Index; | 20 | + return <Main categories={categories} posts={getAllPosts} />; |
21 | +} | ... | ... |
1 | +import { useEffect, useState } from 'react'; | ||
2 | +import Link from 'next/link'; | ||
3 | +import { useRouter } from 'next/dist/client/router'; | ||
4 | +import { Layout, Menu } from 'antd'; | ||
5 | +import { MenuOutlined } from '@ant-design/icons'; | ||
6 | +import { useQuery } from '@apollo/client'; | ||
7 | +import { GET_ALL_POSTS } from '@src/gql/get-all-posts'; | ||
8 | + | ||
9 | +const { Header: HeaderContainer } = Layout; | ||
10 | + | ||
11 | +export default function Header() { | ||
12 | + const [selected, setSelected] = useState(null); | ||
13 | + const { query } = useRouter(); | ||
14 | + | ||
15 | + const { error, data } = useQuery(GET_ALL_POSTS); | ||
16 | + if (error) console.log(JSON.stringify(error, null, 2)); | ||
17 | + | ||
18 | + let list = []; | ||
19 | + let getAllPosts = data?.getAllPosts || []; | ||
20 | + | ||
21 | + getAllPosts.forEach((post) => { | ||
22 | + if (!list.find((category) => post.category === category)) { | ||
23 | + list.push(post.category); | ||
24 | + } | ||
25 | + }); | ||
26 | + | ||
27 | + useEffect(() => { | ||
28 | + setSelected(query.name || '/'); | ||
29 | + }, [query]); | ||
30 | + | ||
31 | + return ( | ||
32 | + <HeaderContainer className={'app-header'}> | ||
33 | + <Menu | ||
34 | + overflowedIndicator={<MenuOutlined className={'header-hbg-menu'} />} | ||
35 | + theme={'dark'} | ||
36 | + mode={'horizontal'} | ||
37 | + selectedKeys={[selected]} | ||
38 | + > | ||
39 | + {/* logo */} | ||
40 | + <Menu.Item key={'/'}> | ||
41 | + <Link href={'/'}> | ||
42 | + <a | ||
43 | + style={{ | ||
44 | + float: 'left', | ||
45 | + width: '120px', | ||
46 | + height: '31px', | ||
47 | + margin: '16px 24px 16px 0', | ||
48 | + background: 'rgba(255, 255, 255, 0.3)', | ||
49 | + }} | ||
50 | + /> | ||
51 | + </Link> | ||
52 | + </Menu.Item> | ||
53 | + {list.map((item) => ( | ||
54 | + <Menu.Item key={item} className={'header-item'}> | ||
55 | + <Link href={`/category/${item}`}> | ||
56 | + <a>{item}</a> | ||
57 | + </Link> | ||
58 | + </Menu.Item> | ||
59 | + ))} | ||
60 | + </Menu> | ||
61 | + </HeaderContainer> | ||
62 | + ); | ||
63 | +} |
1 | +import { Spin, Space } from 'antd'; | ||
2 | + | ||
3 | +export const startLoading = () => { | ||
4 | + const element = document.getElementById('app-loader'); | ||
5 | + | ||
6 | + element.style.display = 'flex'; | ||
7 | +}; | ||
8 | + | ||
9 | +export const finishLoading = () => { | ||
10 | + const element = document.getElementById('app-loader'); | ||
11 | + | ||
12 | + element.style.display = 'none'; | ||
13 | +}; | ||
14 | + | ||
15 | +export default function Loader() { | ||
16 | + return ( | ||
17 | + <Space | ||
18 | + id={'app-loader'} | ||
19 | + size="middle" | ||
20 | + style={{ | ||
21 | + position: 'fixed', | ||
22 | + top: 0, | ||
23 | + left: 0, | ||
24 | + width: '100vw', | ||
25 | + height: '100vh', | ||
26 | + display: 'none', | ||
27 | + alignItems: 'center', | ||
28 | + justifyContent: 'center', | ||
29 | + zIndex: 2, | ||
30 | + }} | ||
31 | + > | ||
32 | + <Spin size="large" /> | ||
33 | + </Space> | ||
34 | + ); | ||
35 | +} |
project/packages/web/src/config/URI.ts
0 → 100644
project/packages/web/src/gql/create-post.ts
0 → 100644
1 | +import gql from 'graphql-tag'; | ||
2 | + | ||
3 | +export const GET_POST_WITH_COMMENTS = gql` | ||
4 | + query GetPostWithComments($post_id: Float!, $inputComment: GetCommentInput!) { | ||
5 | + getPost(id: $post_id) { | ||
6 | + id | ||
7 | + author | ||
8 | + category | ||
9 | + created_date | ||
10 | + title | ||
11 | + content | ||
12 | + } | ||
13 | + getSomeComments(input: $inputComment) { | ||
14 | + id | ||
15 | + author | ||
16 | + content | ||
17 | + created_date | ||
18 | + } | ||
19 | + } | ||
20 | +`; |
project/packages/web/src/hooks/index.ts
0 → 100644
1 | +export { default as useWindowSize } from './useWindowSize'; |
1 | +import { useLayoutEffect, useState } from 'react'; | ||
2 | + | ||
3 | +export default function useWindowSize() { | ||
4 | + const [size, setSize] = useState([0, 0]); | ||
5 | + | ||
6 | + useLayoutEffect(() => { | ||
7 | + function updateSize() { | ||
8 | + setSize([window.innerWidth, window.innerHeight]); | ||
9 | + } | ||
10 | + window.addEventListener('resize', updateSize); | ||
11 | + updateSize(); | ||
12 | + return () => window.removeEventListener('resize', updateSize); | ||
13 | + }, []); | ||
14 | + | ||
15 | + return size; | ||
16 | +} |
project/packages/web/src/shared/functions.ts
0 → 100644
1 | +import moment from 'moment'; | ||
2 | +import { useWindowSize } from '@src/hooks'; | ||
3 | + | ||
4 | +export const getMaxLengthByWidth = (width: number) => { | ||
5 | + if (width > 590) return 30; | ||
6 | + if (width > 474) return 20; | ||
7 | + else return 13; | ||
8 | +}; | ||
9 | + | ||
10 | +export const sliceTextByLength = (text: string, length: number) => | ||
11 | + text.length > length ? text.substr(0, length) + '...' : text; | ||
12 | + | ||
13 | +export const makeArticleURLWithNumber = (name: string, id: number) => { | ||
14 | + return `${window.location.origin}/${name}/article?num=${id}`; | ||
15 | +}; | ||
16 | + | ||
17 | +export const makeDateForDayOrTime = (date) => | ||
18 | + moment().diff(moment(date), 'days') > 1 | ||
19 | + ? moment(date).format('YY.MM.DD') | ||
20 | + : moment(date).format('HH:mm:ss'); | ||
21 | + | ||
22 | +export const makeCategoryTableBody = (list) => { | ||
23 | + const [width] = useWindowSize(); | ||
24 | + const newData = list.map(({ title, id, created_date, ...rest }) => { | ||
25 | + const textMaxLength = getMaxLengthByWidth(width); | ||
26 | + const newTitle = sliceTextByLength(title, textMaxLength); | ||
27 | + const createdDate = makeDateForDayOrTime(created_date); | ||
28 | + | ||
29 | + return { | ||
30 | + ...rest, | ||
31 | + id, | ||
32 | + key: String(id), | ||
33 | + title: newTitle, | ||
34 | + created_date: createdDate, | ||
35 | + }; | ||
36 | + }); | ||
37 | + | ||
38 | + newData.sort((a, b) => Number(b.id) - Number(a.id)); | ||
39 | + | ||
40 | + return newData; | ||
41 | +}; |
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 | +} |
... | @@ -18,7 +18,19 @@ | ... | @@ -18,7 +18,19 @@ |
18 | "resolveJsonModule": true, | 18 | "resolveJsonModule": true, |
19 | "skipLibCheck": true, | 19 | "skipLibCheck": true, |
20 | "target": "esnext", | 20 | "target": "esnext", |
21 | - "strict": false | 21 | + "strict": false, |
22 | + "baseUrl": ".", | ||
23 | + "rootDir": ".", | ||
24 | + "paths": { | ||
25 | + "@src/*": ["src/*"], | ||
26 | + "@components/*": ["src/components/*"], | ||
27 | + "@config/*": ["src/config/*"], | ||
28 | + "@constants/*": ["src/constants/*"], | ||
29 | + "@hooks/*": ["src/hooks/*"], | ||
30 | + "@gql/*": ["src/gql/*"], | ||
31 | + "@views/*": ["src/views/*"], | ||
32 | + "@shared/*": ["src/shared/*"] | ||
33 | + } | ||
22 | }, | 34 | }, |
23 | "exclude": [ | 35 | "exclude": [ |
24 | "node_modules" | 36 | "node_modules" | ... | ... |
... | @@ -5007,7 +5007,7 @@ chardet@^0.7.0: | ... | @@ -5007,7 +5007,7 @@ chardet@^0.7.0: |
5007 | resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" | 5007 | resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" |
5008 | integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== | 5008 | integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== |
5009 | 5009 | ||
5010 | -chokidar@3.5.1, chokidar@^3.4.2, chokidar@^3.5.1: | 5010 | +chokidar@3.5.1, "chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.1: |
5011 | version "3.5.1" | 5011 | version "3.5.1" |
5012 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" | 5012 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" |
5013 | integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== | 5013 | integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== |
... | @@ -12191,6 +12191,13 @@ sane@^4.0.3: | ... | @@ -12191,6 +12191,13 @@ sane@^4.0.3: |
12191 | minimist "^1.1.1" | 12191 | minimist "^1.1.1" |
12192 | walker "~1.0.5" | 12192 | walker "~1.0.5" |
12193 | 12193 | ||
12194 | +sass@^1.34.1: | ||
12195 | + version "1.34.1" | ||
12196 | + resolved "https://registry.yarnpkg.com/sass/-/sass-1.34.1.tgz#30f45c606c483d47b634f1e7371e13ff773c96ef" | ||
12197 | + integrity sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ== | ||
12198 | + dependencies: | ||
12199 | + chokidar ">=3.0.0 <4.0.0" | ||
12200 | + | ||
12194 | sax@>=0.6.0, sax@^1.2.4: | 12201 | sax@>=0.6.0, sax@^1.2.4: |
12195 | version "1.2.4" | 12202 | version "1.2.4" |
12196 | resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" | 12203 | resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" | ... | ... |
-
Please register or login to post a comment