├── .babelrc ├── .dockerignore ├── .env ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature-request.md ├── pull_request_template.md └── workflows │ ├── dev-build-image.yaml │ ├── dev-deploy.yaml │ ├── prod-build-deploy-s3.yaml │ ├── prod-build-image.yaml │ └── prod-deploy.yaml ├── .gitignore ├── .husky └── pre-commit ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .storybook ├── addons.js ├── config.js └── preview-head.html ├── .stylelintrc ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── docs ├── Upload_Grid_Layout.gif ├── Zabo_Upload_Process.gif ├── storybook.png └── storybook_cli.png ├── netlify.toml ├── package.json ├── public ├── icons │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png ├── images │ ├── icons-192.png │ ├── icons-512.png │ └── og_image.jpg ├── index.html ├── main.js └── manifest.json ├── src ├── App.styled.js ├── App.test.js ├── App.tsx ├── boot.js ├── components │ ├── ErrorBoundary.tsx │ ├── atoms │ │ ├── Button │ │ │ ├── Button.js │ │ │ ├── Button.styled.js │ │ │ └── index.js │ │ ├── Category │ │ │ ├── Category.js │ │ │ └── index.js │ │ ├── Container │ │ │ ├── Container.tsx │ │ │ └── index.js │ │ ├── DueDate │ │ │ ├── DueDate.tsx │ │ │ └── index.ts │ │ ├── InputBase │ │ │ ├── InputBase.js │ │ │ └── index.js │ │ ├── Link │ │ │ ├── Link.container.js │ │ │ ├── Link.js │ │ │ ├── Link.stories.js │ │ │ ├── Link.styled.js │ │ │ └── index.js │ │ ├── Modal │ │ │ ├── Modal.js │ │ │ └── index.js │ │ ├── SVG │ │ │ ├── SVG.js │ │ │ ├── SVG.stories.js │ │ │ └── index.js │ │ ├── Span │ │ │ └── index.js │ │ ├── SuperTooltip │ │ │ ├── SuperTooltip.js │ │ │ └── index.js │ │ ├── TagList │ │ │ ├── TagList.js │ │ │ └── index.js │ │ ├── Todo │ │ │ ├── Todo.js │ │ │ ├── Todo.stories.js │ │ │ ├── Todo.styled.js │ │ │ └── index.js │ │ ├── ToggleButton │ │ │ ├── ToggleButton.js │ │ │ └── index.js │ │ └── TwoCol │ │ │ ├── TwoCol.js │ │ │ └── index.js │ ├── containers │ │ ├── ChannelTalk.tsx │ │ ├── ScrollToTop.tsx │ │ └── WindowResizeListener.tsx │ ├── molecules │ │ ├── OwnerInfo │ │ │ ├── OwnerInfo.js │ │ │ └── index.js │ │ ├── ScrollBtn │ │ │ ├── ScrollBtn.tsx │ │ │ └── index.ts │ │ ├── Select │ │ │ ├── Select.js │ │ │ └── index.js │ │ ├── SimpleSelect │ │ │ ├── SimpleSelect.js │ │ │ └── index.js │ │ └── StatBox │ │ │ ├── StatBox.js │ │ │ └── index.js │ ├── organisms │ │ ├── AuthCallback │ │ │ ├── AuthCallback.tsx │ │ │ └── index.ts │ │ ├── GroupBox │ │ │ ├── GroupBox.styled.tsx │ │ │ ├── GroupBox.tsx │ │ │ ├── GroupBoxA.tsx │ │ │ ├── GroupBoxP.tsx │ │ │ ├── GroupBoxS.tsx │ │ │ └── index.ts │ │ ├── GroupList │ │ │ ├── GroupList.tsx │ │ │ └── index.ts │ │ ├── List │ │ │ ├── List.js │ │ │ └── index.js │ │ ├── MemberItem │ │ │ ├── MemberItem.js │ │ │ └── index.js │ │ ├── ProfileStats │ │ │ ├── ProfileStats.js │ │ │ ├── ProfileStats.styled.js │ │ │ └── index.js │ │ ├── SearchSelect │ │ │ ├── SearchSelect.js │ │ │ └── index.js │ │ ├── StyledQuill.js │ │ └── ZaboCard │ │ │ ├── ZaboCard.js │ │ │ ├── ZaboCard.stories.js │ │ │ ├── ZaboCard.styled.js │ │ │ ├── ZaboCardL.js │ │ │ ├── ZaboCardM.js │ │ │ └── index.js │ ├── pages │ │ ├── ApiPage │ │ │ ├── ApiPage.js │ │ │ └── index.js │ │ ├── AuthPage │ │ │ ├── AuthPage.container.js │ │ │ ├── AuthPage.js │ │ │ ├── AuthPage.stories.js │ │ │ ├── AuthPage.styled.js │ │ │ └── index.js │ │ ├── HomePage │ │ │ ├── HomePage.container.js │ │ │ ├── HomePage.js │ │ │ ├── HomePage.stories.js │ │ │ ├── HomePage.styled.js │ │ │ └── index.js │ │ ├── LandingPage │ │ │ ├── LandingPage.js │ │ │ ├── LandingPage.styled.js │ │ │ └── index.js │ │ ├── LoginPage │ │ │ ├── LoginPage.container.js │ │ │ ├── LoginPage.js │ │ │ ├── LoginPage.stories.js │ │ │ └── index.js │ │ ├── NotFound │ │ │ ├── NotFound.js │ │ │ ├── NotFound.styled.js │ │ │ └── index.js │ │ ├── ProfilePage │ │ │ ├── GroupProfilePage.js │ │ │ ├── Main.js │ │ │ ├── Profile.styled.js │ │ │ ├── ProfilePage.js │ │ │ ├── UserProfilePage.js │ │ │ └── index.js │ │ ├── Reload.tsx │ │ ├── SearchPage │ │ │ ├── SearchPage.js │ │ │ ├── SearchPage.styled.js │ │ │ └── index.js │ │ ├── SettingsPage │ │ │ ├── GroupApply.js │ │ │ ├── GroupMembersSetting.js │ │ │ ├── GroupProfileSetting.js │ │ │ ├── Main.js │ │ │ ├── ProfileSetting.js │ │ │ ├── Setting.styled.js │ │ │ ├── index.js │ │ │ └── withGroupProfile.tsx │ │ ├── Zabo │ │ │ └── index.js │ │ ├── ZaboPage │ │ │ ├── Main.js │ │ │ ├── ZaboDetailPage.js │ │ │ ├── ZaboEditPage.js │ │ │ ├── ZaboPage.styled.js │ │ │ └── index.js │ │ ├── ZaboUploadPage │ │ │ ├── ZaboUploadPage.js │ │ │ ├── ZaboUploadPage.styled.js │ │ │ └── index.js │ │ └── index.ts │ └── templates │ │ ├── FloatingNavigator │ │ ├── FloatingNavigator.js │ │ ├── FloatingNavigator.stories.js │ │ ├── FloatingNavigator.styled.js │ │ └── index.js │ │ ├── Footer │ │ ├── Footer.styled.tsx │ │ ├── Footer.tsx │ │ └── index.ts │ │ ├── Header │ │ ├── Header.stories.js │ │ ├── Header.styled.tsx │ │ ├── Header.tsx │ │ └── index.ts │ │ ├── Loading │ │ ├── Loading.js │ │ ├── Loading.stories.js │ │ ├── Loading.styled.js │ │ └── index.js │ │ ├── Modal │ │ ├── Modal.js │ │ ├── Modal.stories.js │ │ ├── Modal.styled.js │ │ ├── index.js │ │ └── modalMethods.js │ │ ├── PWAPrompt │ │ ├── PWAPrompt.js │ │ ├── PWAPrompt.stories.js │ │ ├── PWAPrompt.styled.js │ │ └── index.js │ │ ├── SavedPosters │ │ ├── SavedPosters.container.js │ │ ├── SavedPosters.js │ │ ├── SavedPosters.stories.js │ │ ├── SavedPosters.styled.js │ │ └── index.js │ │ ├── SearchBar │ │ ├── SearchBar.stories.js │ │ ├── SearchBar.styled.tsx │ │ ├── SearchBar.tsx │ │ └── index.js │ │ ├── ZaboList │ │ ├── ZaboList.container.js │ │ ├── ZaboList.js │ │ ├── ZaboList.stories.js │ │ ├── ZaboList.styled.js │ │ ├── index.js │ │ └── withStackMaster.js │ │ └── ZaboUpload │ │ ├── InfoForm.js │ │ ├── InfoForm.styled.js │ │ ├── LeavingAlert.js │ │ ├── Loading.js │ │ ├── SelectGroup.js │ │ ├── UploadImages.js │ │ ├── UploadProcess.js │ │ ├── ZaboUpload.styled.js │ │ ├── grid-layout.scss │ │ └── index.js ├── hoc │ ├── AuthRoutes.tsx │ ├── paramsToProps.js │ ├── withLog.js │ └── withZabo.js ├── hooks │ ├── useInputs.js │ └── useSetState.js ├── index.scss ├── index.tsx ├── lib │ ├── api │ │ ├── auth.ts │ │ ├── group.js │ │ ├── profile.js │ │ ├── search.js │ │ ├── user.js │ │ └── zabo.js │ ├── axios.ts │ ├── channel_io.js │ ├── fonts │ │ ├── NanumSquareBold.eot │ │ ├── NanumSquareBold.svg │ │ ├── NanumSquareBold.ttf │ │ ├── NanumSquareBold.woff │ │ ├── NanumSquareBold.woff2 │ │ ├── NanumSquareExtraBold.eot │ │ ├── NanumSquareExtraBold.svg │ │ ├── NanumSquareExtraBold.ttf │ │ ├── NanumSquareExtraBold.woff │ │ ├── NanumSquareExtraBold.woff2 │ │ ├── NanumSquareLight.eot │ │ ├── NanumSquareLight.svg │ │ ├── NanumSquareLight.ttf │ │ ├── NanumSquareLight.woff │ │ ├── NanumSquareLight.woff2 │ │ ├── NanumSquareRegular.eot │ │ ├── NanumSquareRegular.svg │ │ ├── NanumSquareRegular.ttf │ │ ├── NanumSquareRegular.woff │ │ ├── NanumSquareRegular.woff2 │ │ └── stylesheet.css │ ├── i18n │ │ ├── i18next-react-li-postprocessor.js │ │ ├── i18next-react-react-postprocessor.js │ │ └── index.js │ ├── immutable.js │ ├── interface │ │ ├── group.ts │ │ ├── index.ts │ │ ├── schemas │ │ │ ├── board.ts │ │ │ ├── group.ts │ │ │ ├── index.ts │ │ │ ├── user.ts │ │ │ └── zabo.ts │ │ ├── user.ts │ │ ├── utils │ │ │ └── mongo.ts │ │ └── zabo.ts │ ├── mixins.js │ ├── propTypes.js │ ├── storage.js │ ├── theme.ts │ ├── utils │ │ ├── index.ts │ │ ├── selector.ts │ │ └── style.ts │ └── variables.js ├── locales │ ├── en │ │ ├── Home.json │ │ └── translation.json │ └── ko │ │ ├── Home.json │ │ └── translation.json ├── react-app-env.d.ts ├── serviceWorker.ts ├── setupProxy.js ├── static │ ├── hd │ │ ├── zhangjiajie-landscape.jpg │ │ ├── zhangjiajie-portrait.jpg │ │ └── zhangjiajie-snow.jpg │ ├── icon │ │ ├── arrow.svg │ │ ├── baseline-expand_more-24px.svg │ │ ├── category │ │ │ ├── all.svg │ │ │ ├── contest.svg │ │ │ ├── demoday.svg │ │ │ ├── education.svg │ │ │ ├── event.svg │ │ │ ├── exhibition.svg │ │ │ ├── festival.svg │ │ │ ├── hangsa.svg │ │ │ ├── hire.svg │ │ │ ├── meeting.svg │ │ │ ├── notice.svg │ │ │ ├── openclub.svg │ │ │ ├── performance.svg │ │ │ ├── recruit.svg │ │ │ ├── seminar.svg │ │ │ └── volunteer.svg │ │ ├── help.svg │ │ ├── person_add-24px.svg │ │ ├── remove-24px.svg │ │ └── rightArrow.svg │ ├── images │ │ ├── add.svg │ │ ├── add_gray.svg │ │ ├── banner_poster.jpg │ │ ├── banner_poster.webp │ │ ├── banner_sparcs.jpg │ │ ├── banner_sparcs.webp │ │ ├── bookmark.svg │ │ ├── bookmarkEmpty.svg │ │ ├── calendar.svg │ │ ├── cancel.png │ │ ├── check_gray.png │ │ ├── chevron_home.svg │ │ ├── chevron_left.svg │ │ ├── chevron_right.svg │ │ ├── circle.svg │ │ ├── cross.svg │ │ ├── defaultProfile.png │ │ ├── delete.svg │ │ ├── group.svg │ │ ├── groupDefaultProfile.png │ │ ├── landing_background.jpg │ │ ├── leftArrow-navy.png │ │ ├── leftArrow.png │ │ ├── leftScroll.png │ │ ├── like.svg │ │ ├── likeEmpty.svg │ │ ├── main.jpg │ │ ├── notFoundImage.jpg │ │ ├── recruitAscii.js │ │ ├── rightArrow.png │ │ ├── rightArrowForward.png │ │ ├── rightGrayArrow.png │ │ ├── rightScroll.png │ │ ├── search-icon-navy.png │ │ ├── search-icon-white.png │ │ ├── uploadImg.png │ │ ├── user.svg │ │ ├── whiteBookmakrEmpty.svg │ │ ├── whiteBookmark.svg │ │ ├── whiteLike.svg │ │ ├── whiteLikeEmpty.svg │ │ └── whiteShare.svg │ └── logo │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── logo_white.svg │ │ ├── sparcs.png │ │ └── sparcs.svg ├── store │ ├── example.ts │ ├── index.js │ ├── persist.ts │ └── reducers │ │ ├── admin.ts │ │ ├── app.ts │ │ ├── auth.ts │ │ ├── index.ts │ │ ├── profile.ts │ │ ├── upload.ts │ │ └── zabo.ts ├── stories │ └── index.js ├── theme │ └── index.js └── types │ ├── index.d.ts │ └── store.d.ts ├── tools ├── generate-component.py ├── modules │ ├── colors.inc.sh │ └── verbose.inc.sh ├── moveBuildFolder.sh └── updateStories.sh ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["babel-plugin-styled-components"] 3 | } 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .git 3 | .gitignore 4 | .dockerignore 5 | Dockerfile 6 | *.md 7 | *.sh 8 | node_modules 9 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_PATH=src:src/components 2 | 3 | # This allows the use of .eslintignore 4 | # https://github.com/facebook/create-react-app/blob/master/docusaurus/docs/advanced-configuration.md 5 | EXTEND_ESLINT=true 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/lib/react-grid-layout/ 2 | src/lib/channel_io.js 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "react": { 4 | "version": "detect" 5 | } 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true, 10 | "jest": true 11 | }, 12 | "extends": [ 13 | "eslint:recommended", 14 | "plugin:react/recommended", 15 | "plugin:react-hooks/recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:import/typescript", 18 | "prettier" 19 | ], 20 | "rules": { 21 | "react/prop-types": "warn" 22 | }, 23 | "parser": "@typescript-eslint/parser", 24 | "root": true, 25 | "parserOptions": { 26 | "project": "./tsconfig.json" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/lib/fonts/**/*.svg binary 2 | 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] Bug title" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Describe the bug 11 | 12 | 13 | 14 | A clear and concise description of what the bug is. 15 | 16 | # To Reproduce 17 | 18 | 19 | 20 | Steps to reproduce the behavior: 21 | 22 | 1. Go to '...' 23 | 2. Click on '....' 24 | 3. Scroll down to '....' 25 | 4. See error 26 | 27 | # Screenshots 28 | 29 | 30 | 31 | If applicable, add screenshots to help explain your problem. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: issue title 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Summary 11 | 12 | It implements ... 13 | 14 | # Tasks 15 | 16 | - [ ] Task 1 17 | - [ ] Task 2 18 | - [ ] Task 3 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | It closes #issue_number 4 | 5 | # Extra info 6 | 7 | # Images or Screenshots 8 | 9 | # Further Work 10 | 11 | - Do something... 12 | -------------------------------------------------------------------------------- /.github/workflows/dev-build-image.yaml: -------------------------------------------------------------------------------- 1 | name: Build docker image on push to develop 2 | 3 | on: 4 | push: 5 | branches: ["develop"] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Docker Buildx 13 | uses: docker/setup-buildx-action@v3 14 | - name: Cache Docker layers 15 | uses: actions/cache@v3 16 | with: 17 | path: /tmp/.buildx-cache 18 | key: ${{ runner.os }}-buildx-${{ github.sha }} 19 | restore-keys: | 20 | ${{ runner.os }}-buildx- 21 | - name: Log in to GitHub Container Registry 22 | run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u USERNAME --password-stdin 23 | - name: Build and push Image 24 | id: docker-build 25 | uses: docker/build-push-action@v5 26 | env: 27 | IMAGE_TAG: dev 28 | with: 29 | push: true 30 | tags: "ghcr.io/sparcs-kaist/zabo-front:${{ env.IMAGE_TAG }}" 31 | cache-from: type=local,src=/tmp/.buildx-cache 32 | cache-to: type=local,dest=/tmp/.buildx-cache-new 33 | - name: Remove old cache 34 | run: | 35 | rm -rf /tmp/.buildx-cache 36 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 37 | -------------------------------------------------------------------------------- /.github/workflows/dev-deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to dev server 2 | 3 | # when dev image build action is completed, run this action 4 | on: 5 | workflow_run: 6 | workflows: ["Build docker image on push to develop"] 7 | types: 8 | - completed 9 | 10 | jobs: 11 | if_workflow_success: 12 | name: Deploy to dev server 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 15 | 16 | steps: 17 | - name: pull the image and restart the container 18 | uses: appleboy/ssh-action@v1.0.0 19 | with: 20 | host: ${{ secrets.DEV_HOST }} 21 | port: ${{ secrets.DEV_PORT }} 22 | username: ${{ secrets.DEV_USERNAME }} 23 | password: ${{ secrets.DEV_PASSWORD }} 24 | proxy_host: ${{ secrets.DEV_PROXY_HOST }} 25 | proxy_port: ${{ secrets.DEV_PROXY_PORT }} 26 | proxy_username: ${{ secrets.DEV_PROXY_USERNAME }} 27 | proxy_password: ${{ secrets.DEV_PROXY_PASSWORD }} 28 | script_stop: true # stop script if any command has failed 29 | script: | 30 | docker pull ghcr.io/sparcs-kaist/zabo-front:dev 31 | docker rm -f zabo-front 32 | docker run --restart always -d -p ${{ secrets.DEV_CONTAINER_PORT }}:80 --name zabo-front ghcr.io/sparcs-kaist/zabo-front:dev 33 | -------------------------------------------------------------------------------- /.github/workflows/prod-build-deploy-s3.yaml: -------------------------------------------------------------------------------- 1 | name: Build src on release tag created and deploy to s3(cloudfront) 2 | 3 | on: 4 | # push: 5 | # tags: 6 | # - "**" 7 | push: 8 | branches: 9 | - "develop" 10 | 11 | jobs: 12 | build: 13 | name: Build and push static to s3 cloudfront 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Cache node modules # node modules 캐싱 18 | uses: actions/cache@v1 19 | id: yarn-cache 20 | with: 21 | path: node_modules 22 | key: ${{ runner.OS }}-master-build-${{ hashFiles('**/yarn.lock') }} 23 | restore-keys: | 24 | ${{ runner.OS }}-build- 25 | ${{ runner.OS }}- 26 | - if: steps.yarn-cache.outputs.cache-hit == 'true' 27 | run: echo 'yarn cache hit!' 28 | - if: steps.yarn-cache.outputs.cache-hit != 'true' 29 | run: echo 'yarn cache missed!' 30 | 31 | - name: Install Dependencies 32 | run: yarn 33 | 34 | - name: Build 35 | run: CI=false yarn build 36 | env: 37 | REACT_APP_API_HOST: ${{ vars.REACT_APP_API_HOST }} 38 | CI: ${{ vars.CI }} 39 | 40 | - name: Deploy 41 | env: 42 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 43 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 44 | run: | 45 | aws s3 cp \ 46 | --recursive \ 47 | --region ap-northeast-2 \ 48 | build s3://zabo.staging.sparcs.org -------------------------------------------------------------------------------- /.github/workflows/prod-build-image.yaml: -------------------------------------------------------------------------------- 1 | name: Build docker image on release tag created 2 | 3 | on: 4 | push: 5 | tags: 6 | - "**" 7 | 8 | jobs: 9 | build: 10 | name: Build and push Image to GitHub Container Registry 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up Docker Buildx 15 | uses: docker/setup-buildx-action@v3 16 | - name: Cache Docker layers 17 | uses: actions/cache@v3 18 | with: 19 | path: /tmp/.buildx-cache 20 | key: ${{ runner.os }}-buildx-${{ github.sha }} 21 | restore-keys: | 22 | ${{ runner.os }}-buildx- 23 | - name: Log in to GitHub Container Registry 24 | run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u USERNAME --password-stdin 25 | - name: Build and push Image 26 | id: docker-build 27 | uses: docker/build-push-action@v5 28 | env: 29 | IMAGE_TAG: ${{github.ref_name}} 30 | with: 31 | push: true 32 | tags: | 33 | "ghcr.io/sparcs-kaist/zabo-front:${{ env.IMAGE_TAG }}" 34 | "ghcr.io/sparcs-kaist/zabo-front:latest" 35 | cache-from: type=local,src=/tmp/.buildx-cache 36 | cache-to: type=local,dest=/tmp/.buildx-cache-new 37 | - name: Remove old cache 38 | run: | 39 | rm -rf /tmp/.buildx-cache 40 | mv /tmp/.buildx-cache-new /tmp/.buildx-cache 41 | -------------------------------------------------------------------------------- /.github/workflows/prod-deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy to prod server 2 | 3 | # when prod image build action is completed, run this action 4 | on: 5 | workflow_run: 6 | workflows: ["Build docker image on release tag created"] 7 | types: 8 | - completed 9 | 10 | jobs: 11 | if_workflow_success: 12 | name: Deploy to prod server 13 | runs-on: ubuntu-latest 14 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 15 | 16 | steps: 17 | - name: pull the image and restart the container 18 | uses: appleboy/ssh-action@v1.0.0 19 | with: 20 | host: ${{ secrets.PROD_HOST }} 21 | port: ${{ secrets.PROD_PORT }} 22 | username: ${{ secrets.PROD_USERNAME }} 23 | password: ${{ secrets.PROD_PASSWORD }} 24 | proxy_host: ${{ secrets.PROD_PROXY_HOST }} 25 | proxy_port: ${{ secrets.PROD_PROXY_PORT }} 26 | proxy_username: ${{ secrets.PROD_PROXY_USERNAME }} 27 | proxy_password: ${{ secrets.PROD_PROXY_PASSWORD }} 28 | script_stop: true # stop script if any command has failed 29 | script: | 30 | docker pull ghcr.io/sparcs-kaist/zabo-front:latest 31 | docker rm -f zabo-front 32 | docker run --restart always -d -p ${{ secrets.PROD_CONTAINER_PORT }}:80 --name zabo-front ghcr.io/sparcs-kaist/zabo-front:latest 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .idea/ 26 | .vscode/ 27 | /tmp 28 | /deploy 29 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.15.1 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | deploy 3 | .husky 4 | .storybook 5 | docs 6 | public 7 | yarn.lock 8 | package.json 9 | tools 10 | .eslintrc 11 | .stylelintrc 12 | *.md 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "all", 8 | "jsxSingleQuote": false 9 | } 10 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-knobs/register' 2 | import '@storybook/addon-viewport/register' 3 | import '@storybook/addon-notes/register' 4 | import '@storybook/addon-actions/register' 5 | import '@storybook/addon-links/register' 6 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { addDecorator, configure } from '@storybook/react' 2 | import { withNotes } from '@storybook/addon-notes' 3 | import '@storybook/addon-console' 4 | //import { withInfo } from "@storybook/addon-info" 5 | import { withKnobs } from '@storybook/addon-knobs' 6 | import { withConsole } from '@storybook/addon-console' 7 | 8 | import requireContext from 'require-context.macro' 9 | 10 | import React from "react" 11 | 12 | import { Provider } from "react-redux" 13 | import { BrowserRouter as Router } from "react-router-dom" 14 | import { ThemeProvider } from "styled-components" 15 | 16 | import store from "../src/store" 17 | 18 | import '../src/index.scss' 19 | 20 | const Decorator = (storyFn) => ( 21 | 22 | 23 | 24 | {storyFn()} 25 | 26 | 27 | 28 | ) 29 | 30 | 31 | addDecorator(Decorator) 32 | addDecorator(withNotes) 33 | addDecorator((storyFn, context) => withConsole()(storyFn)(context)) 34 | //addDecorator(withInfo) 35 | addDecorator(withKnobs) 36 | 37 | //addParameters({ 38 | // chromatic: { viewports: [1320, 992, 768, 576] }, 39 | //}) 40 | 41 | const req = requireContext('../src/components', true, /\.stories\.js$/) 42 | 43 | function loadStories() { 44 | req.keys().forEach(filename => req(filename)) 45 | } 46 | 47 | configure(loadStories, module) 48 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": [ 3 | "stylelint-processor-styled-components" 4 | ], 5 | "extends": [ 6 | "stylelint-config-recommended", 7 | "stylelint-config-styled-components" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine AS builder 2 | WORKDIR '/app' 3 | 4 | COPY package.json . 5 | COPY yarn.lock . 6 | 7 | RUN yarn 8 | 9 | COPY . . 10 | RUN yarn build 11 | 12 | FROM node:16-alpine 13 | 14 | WORKDIR '/usr/src/app' 15 | 16 | COPY --from=builder /app/build ./build 17 | 18 | # Install requirements 19 | RUN npm install serve -g 20 | 21 | # Run container 22 | EXPOSE 80 23 | CMD ["sh", "-c", "serve -s build -l 80"] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 SPARCS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/Upload_Grid_Layout.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/docs/Upload_Grid_Layout.gif -------------------------------------------------------------------------------- /docs/Zabo_Upload_Process.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/docs/Zabo_Upload_Process.gif -------------------------------------------------------------------------------- /docs/storybook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/docs/storybook.png -------------------------------------------------------------------------------- /docs/storybook_cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/docs/storybook_cli.png -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/api/*" 3 | to = "https://zabo.dev.sparcs.org/api/:splat" 4 | status = 200 5 | -------------------------------------------------------------------------------- /public/icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /public/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /public/icons/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/android-icon-36x36.png -------------------------------------------------------------------------------- /public/icons/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/android-icon-48x48.png -------------------------------------------------------------------------------- /public/icons/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/android-icon-72x72.png -------------------------------------------------------------------------------- /public/icons/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/android-icon-96x96.png -------------------------------------------------------------------------------- /public/icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/icons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/icons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/apple-icon.png -------------------------------------------------------------------------------- /public/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | #ffffff 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/favicon-96x96.png -------------------------------------------------------------------------------- /public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/favicon.ico -------------------------------------------------------------------------------- /public/icons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /public/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/icons/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/icons/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/images/icons-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/images/icons-192.png -------------------------------------------------------------------------------- /public/images/icons-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/images/icons-512.png -------------------------------------------------------------------------------- /public/images/og_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/public/images/og_image.jpg -------------------------------------------------------------------------------- /public/main.js: -------------------------------------------------------------------------------- 1 | // Modules to control application life and create native browser window 2 | const {app, BrowserWindow} = require('electron') 3 | const path = require('path') 4 | 5 | function createWindow () { 6 | // Create the browser window. 7 | const mainWindow = new BrowserWindow({ 8 | width: 800, 9 | height: 600, 10 | webPreferences: { 11 | preload: path.join(__dirname, 'preload.js') 12 | }, 13 | frame: false, 14 | }) 15 | 16 | // and load the index.html of the app. 17 | mainWindow.loadFile(`file://${path.join (__dirname, '../deploy/index.html')}`) 18 | 19 | // Open the DevTools. 20 | // mainWindow.webContents.openDevTools() 21 | } 22 | 23 | // This method will be called when Electron has finished 24 | // initialization and is ready to create browser windows. 25 | // Some APIs can only be used after this event occurs. 26 | app.whenReady().then(() => { 27 | createWindow() 28 | 29 | app.on('activate', function () { 30 | // On macOS it's common to re-create a window in the app when the 31 | // dock icon is clicked and there are no other windows open. 32 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 33 | }) 34 | }) 35 | 36 | // Quit when all windows are closed. 37 | app.on('window-all-closed', function () { 38 | // On macOS it is common for applications and their menu bar 39 | // to stay active until the user quits explicitly with Cmd + Q 40 | if (process.platform !== 'darwin') app.quit() 41 | }) 42 | 43 | // In this file you can include the rest of your app's specific main process 44 | // code. You can also put them in separate files and require them here. 45 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "자보", 3 | "name": "ZABO - SPARCS KAIST", 4 | "icons": [ 5 | { 6 | "src": "/images/icons-192.png", 7 | "type": "image/png", 8 | "sizes": "192x192" 9 | }, 10 | { 11 | "src": "/images/icons-512.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "start_url": "/?utm_source=pwa", 17 | "display": "standalone", 18 | "theme_color": "#12397d", 19 | "background_color": "#fff" 20 | } 21 | -------------------------------------------------------------------------------- /src/App.styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export default styled.div``; 4 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch } from "react-router-dom"; 3 | import { Helmet } from "react-helmet"; 4 | 5 | import ChannelTalk from "components/containers/ChannelTalk"; 6 | import ScrollToTop from "components/containers/ScrollToTop"; 7 | import WindowResizeListener from "components/containers/WindowResizeListener"; 8 | import ErrorBoundary from "components/ErrorBoundary"; 9 | import AuthCallback from "components/organisms/AuthCallback"; 10 | import { 11 | ApiPage, 12 | AuthPage, 13 | HomePage, 14 | LandingPage, 15 | NotFound, 16 | ProfilePage, 17 | SearchPage, 18 | SettingsPage, 19 | ZaboPage, 20 | ZaboUploadPage, 21 | } from "components/pages"; 22 | 23 | import PWAPrompt from "components/templates/PWAPrompt"; 24 | 25 | import { PrivateRoute, PublicRoute } from "hoc/AuthRoutes"; 26 | import pToP from "hoc/paramsToProps"; 27 | 28 | import AppWrapper from "./App.styled"; 29 | import Reload from "./components/pages/Reload"; 30 | 31 | const App = () => ( 32 | 33 | 34 | ZABO - 자보 35 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ); 68 | 69 | export default App; 70 | -------------------------------------------------------------------------------- /src/boot.js: -------------------------------------------------------------------------------- 1 | import "devtools-detect"; 2 | 3 | import moment from "moment"; 4 | 5 | import store from "store"; 6 | import { checkAuth } from "store/reducers/auth"; 7 | import { initialize } from "store/reducers/upload"; 8 | import axios from "lib/axios"; 9 | import storage from "lib/storage"; 10 | 11 | import { parseJSON } from "./lib/utils"; 12 | 13 | const pwaInstallPromptListener = () => { 14 | window.addEventListener("beforeinstallprompt", (e) => { 15 | // Prevent Chrome 67 and earlier from automatically showing the prompt 16 | e.preventDefault(); 17 | // Stash the event so it can be triggered later. 18 | window.deferredPrompt = e; 19 | // Update UI notify the user they can add to home screen 20 | window.pwaPromptActive = true; 21 | document.body.classList.add("pwa-prompt-active"); 22 | if (window.onPWAPromptActive) { 23 | window.onPWAPromptActive(); 24 | } 25 | }); 26 | }; 27 | 28 | export default () => { 29 | (() => { 30 | const throttle = (type, name, givenObj) => { 31 | const obj = givenObj || window; 32 | let running = false; 33 | const func = () => { 34 | if (running) { 35 | return; 36 | } 37 | running = true; 38 | requestAnimationFrame(() => { 39 | obj.dispatchEvent(new CustomEvent(name)); 40 | running = false; 41 | }); 42 | }; 43 | obj.addEventListener(type, func); 44 | }; 45 | 46 | /* init - you can init any event */ 47 | throttle("resize", "optimizedResize"); 48 | throttle("scroll", "optimizedScroll"); 49 | })(); 50 | 51 | pwaInstallPromptListener(); 52 | 53 | const uploadPersist = storage.getItem("uploadPersist"); 54 | if (uploadPersist) { 55 | if (uploadPersist.date) { 56 | store.dispatch(initialize(uploadPersist)); 57 | } else { 58 | storage.removeItem("uploadPersist"); 59 | } 60 | } 61 | 62 | const token = storage.getItem("token"); 63 | axios.updateToken(token); 64 | 65 | if (token) { 66 | store.dispatch(checkAuth(token)); 67 | } 68 | }; 69 | 70 | window.addEventListener("devtoolschange", () => { 71 | if (process.env.NODE_ENV !== "production") return; 72 | import("static/images/recruitAscii").then((asciiArt) => { 73 | // eslint-disable-next-line no-console 74 | console.log(asciiArt.default); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React, { ErrorInfo, ReactNode } from "react"; 2 | 3 | interface Props { 4 | children?: ReactNode; 5 | } 6 | 7 | interface State { 8 | hasError: boolean; 9 | } 10 | 11 | class ErrorBoundary extends React.Component { 12 | state = { hasError: false }; 13 | 14 | static getDerivedStateFromError(error: Error) { 15 | // Update state so the next render will show the fallback UI. 16 | return { error, hasError: true }; 17 | } 18 | 19 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 20 | console.error("Uncaught error:", error, errorInfo); 21 | // You can also log the error to an error reporting service 22 | // logErrorToMyService(error, errorInfo); 23 | } 24 | 25 | render() { 26 | if (this.state.hasError) { 27 | // You can render any custom fallback UI 28 | return

Something went wrong.

; 29 | } 30 | 31 | return this.props.children; 32 | } 33 | } 34 | 35 | export default ErrorBoundary; 36 | -------------------------------------------------------------------------------- /src/components/atoms/Button/Button.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { withRouter } from "react-router-dom"; 4 | 5 | import StyledButton, { ButtonGroup } from "./Button.styled"; 6 | 7 | const Button = ({ history, to, onClick, ...props }) => ( 8 | { 11 | if (typeof onClick === "function") onClick(event); 12 | if (to) history.push(to); 13 | }} 14 | /> 15 | ); 16 | 17 | Button.propTypes = { 18 | ...StyledButton.propTypes, 19 | className: PropTypes.string, 20 | to: PropTypes.string, 21 | type: PropTypes.string, 22 | }; 23 | 24 | Button.defaultProps = { 25 | ...StyledButton.defaultProps, 26 | className: "", 27 | to: "", 28 | type: "button", 29 | }; 30 | 31 | Button.defaultProps = {}; 32 | 33 | Button.Group = ButtonGroup; 34 | 35 | export default withRouter(Button); 36 | -------------------------------------------------------------------------------- /src/components/atoms/Button/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Button"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/Category/Category.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | import * as mixins from "lib/mixins"; 4 | import { media } from "lib/utils/style"; 5 | 6 | export const CategoryW = styled.div` 7 | ${mixins.flexCenter}; 8 | height: 19px; 9 | padding: 3px 6px; 10 | background: ${(props) => props.theme.gray5}; 11 | border-radius: 2px; 12 | font-size: 10px; 13 | line-height: 11px; 14 | color: ${(props) => props.theme.gray60}; 15 | cursor: default; 16 | ${media.tablet(css` 17 | font-size: 12px; 18 | line-height: 14px; 19 | height: 20px; 20 | `)}; 21 | `; 22 | 23 | export const CategoryListW = styled.div` 24 | display: flex; 25 | ${CategoryW} { 26 | margin-right: 4px; 27 | &:last-child { 28 | margin-right: 0; 29 | } 30 | } 31 | `; 32 | -------------------------------------------------------------------------------- /src/components/atoms/Category/index.js: -------------------------------------------------------------------------------- 1 | export * from "./Category"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | import { colors } from "lib/theme"; 4 | import { media } from "lib/utils/style"; 5 | 6 | const ContainerComponent = styled.div<{ 7 | ownStyle?: ReturnType; 8 | background?: keyof typeof colors; 9 | }>` 10 | width: 100%; 11 | margin: 0 auto; 12 | padding: 0 24px; 13 | display: flex; 14 | overflow: scroll; 15 | 16 | &::-webkit-scrollbar { 17 | width: 0 !important; 18 | } 19 | 20 | -ms-overflow-style: none; 21 | ${media.tablet(css` 22 | padding: 0 18px; 23 | `)}; 24 | ${(props) => props.ownStyle || ""}; 25 | background: ${(props) => (props.background ? props.theme[props.background] : "transparent")}; 26 | `; 27 | 28 | const PadComponent = styled.div<{ width?: number }>` 29 | padding-left: ${({ width = "24px" }) => width}; 30 | `; 31 | 32 | const Container = Object.assign(ContainerComponent, { Pad: PadComponent }); 33 | 34 | export default Container; 35 | -------------------------------------------------------------------------------- /src/components/atoms/Container/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Container"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/DueDate/DueDate.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | import moment from "moment"; 4 | 5 | import { to2Digits } from "lib/utils"; 6 | 7 | const DueDateW = styled.div` 8 | position: absolute; 9 | right: 12px; 10 | top: 12px; 11 | display: flex; 12 | flex-shrink: 0; 13 | justify-content: center; 14 | align-items: center; 15 | width: 40px; 16 | height: 20px; 17 | font-size: 11px; 18 | line-height: 18px; 19 | border-radius: 2px; 20 | background: ${(props) => props.theme.main}; 21 | font-style: normal; 22 | font-weight: bold; 23 | color: ${(props) => props.theme.white}; 24 | `; 25 | 26 | const DDayW = styled(DueDateW)` 27 | background: ${(props) => props.theme.red50}; 28 | `; 29 | 30 | const DueDateLW = styled.div` 31 | margin-top: 4px; 32 | display: inline-block; 33 | height: 26px; 34 | padding: 4px 10px; 35 | border-radius: 4px; 36 | background: #143441; 37 | color: white; 38 | font-size: 16px; 39 | font-weight: bold; 40 | vertical-align: middle; 41 | @media (max-width: 640px) { 42 | margin-top: 5px; 43 | height: 20px; 44 | font-size: 12px; 45 | padding: 3px 4px; 46 | border-radius: 2px; 47 | } 48 | `; 49 | 50 | const DDayLW = styled(DueDateLW)` 51 | background: ${(props) => props.theme.red50}; 52 | `; 53 | 54 | interface Props { 55 | schedule: string; 56 | large?: boolean; 57 | } 58 | 59 | const DueDate: React.FC = ({ schedule, large }) => { 60 | const due = schedule ? moment(schedule).diff(moment(), "days") : -1; 61 | if (large) { 62 | if (due > 0) return D{to2Digits(-due, true)}; 63 | if (due === 0) return D-Day; 64 | } else { 65 | if (due > 0) return D{to2Digits(-due, true)}; 66 | if (due === 0) return D-Day; 67 | } 68 | return null; 69 | }; 70 | 71 | export default DueDate; 72 | -------------------------------------------------------------------------------- /src/components/atoms/DueDate/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./DueDate"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/InputBase/InputBase.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MInputBase from "@material-ui/core/InputBase"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | 5 | const useStyles = makeStyles({ 6 | root: { 7 | width: "100%", 8 | minHeight: "48px", 9 | margin: "8px 0 24px 0", 10 | padding: "9px 10px", 11 | borderRadius: "4px", 12 | backgroundColor: "#f4f4f4", 13 | fontFailiy: "NanumSquare", 14 | }, 15 | }); 16 | 17 | const InputBase = (props) => { 18 | const classes = useStyles(); 19 | return ; 20 | }; 21 | 22 | export default InputBase; 23 | -------------------------------------------------------------------------------- /src/components/atoms/InputBase/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./InputBase"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/Link/Link.container.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { connect } from "react-redux"; 3 | 4 | import Link from "./Link"; 5 | 6 | class LinkContainer extends PureComponent { 7 | render() { 8 | return ; 9 | } 10 | } 11 | 12 | const mapStateToProps = (state) => ({}); 13 | 14 | const mapDispatchToProps = (dispatch) => ({}); 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(LinkContainer); 17 | -------------------------------------------------------------------------------- /src/components/atoms/Link/Link.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import LinkWrapper from "./Link.styled"; 5 | 6 | class Link extends PureComponent { 7 | render() { 8 | return ( 9 | 10 | {this.props.children} 11 | Link 12 | 13 | ); 14 | } 15 | } 16 | 17 | Link.propTypes = {}; 18 | 19 | Link.defaultProps = {}; 20 | 21 | export default Link; 22 | -------------------------------------------------------------------------------- /src/components/atoms/Link/Link.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import Link from "./index"; 5 | 6 | storiesOf("atoms/Link", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/atoms/Link/Link.styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const LinkWrapper = styled.div``; 4 | 5 | export default LinkWrapper; 6 | -------------------------------------------------------------------------------- /src/components/atoms/Link/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Link.container"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/Modal/Modal.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import styled from "styled-components"; 4 | 5 | const modalRoot = document.getElementById("modal-root"); 6 | 7 | class Portal extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | this.el = document.createElement("div"); 11 | } 12 | 13 | componentDidMount() { 14 | modalRoot.appendChild(this.el); 15 | } 16 | 17 | componentWillUnmount() { 18 | modalRoot.removeChild(this.el); 19 | } 20 | 21 | render() { 22 | return ReactDOM.createPortal(this.props.children, this.el); 23 | } 24 | } 25 | 26 | const ModalWrapper = styled.div` 27 | background-color: rgba(0, 0, 0, 0.5); 28 | position: fixed; 29 | height: 100%; 30 | width: 100%; 31 | top: 0; 32 | left: 0; 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | `; 37 | 38 | const Modal = (props) => ( 39 | 40 | {props.children} 41 | 42 | ); 43 | 44 | export default Modal; 45 | -------------------------------------------------------------------------------- /src/components/atoms/Modal/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Modal"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/SVG/SVG.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { 4 | faArrowRight, 5 | faCoffee, 6 | faPlus, 7 | faQuestionCircle, 8 | faUser, 9 | } from "@fortawesome/free-solid-svg-icons"; 10 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 11 | 12 | import arrow from "static/icon/arrow.svg"; 13 | 14 | export const faIcons = { 15 | coffee: faCoffee, 16 | user: faUser, 17 | plus: faPlus, 18 | arrowRight: faArrowRight, 19 | questionCircle: faQuestionCircle, 20 | }; 21 | const faIconList = Object.keys(faIcons); 22 | 23 | const icons = { 24 | arrow, 25 | }; 26 | 27 | const SVG = ({ icon, ...props }) => { 28 | if (faIconList.some((faIcon) => icon === faIcon)) 29 | return ; 30 | return {icon}; 31 | }; 32 | 33 | SVG.propTypes = { 34 | ...FontAwesomeIcon.propTypes, 35 | icon: PropTypes.oneOf(Object.keys(faIcons)).isRequired, 36 | }; 37 | 38 | SVG.defaultProps = {}; 39 | 40 | export default SVG; 41 | -------------------------------------------------------------------------------- /src/components/atoms/SVG/SVG.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import SVG from "./index"; 5 | import { faIcons } from "./SVG"; 6 | 7 | Object.keys(faIcons).forEach((icon) => { 8 | storiesOf("atoms/SVG", module).add(icon, () => , { 9 | notes: "", 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/atoms/SVG/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SVG"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/Span/index.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Light = styled.span` 4 | font-weight: 300; 5 | `; 6 | -------------------------------------------------------------------------------- /src/components/atoms/SuperTooltip/SuperTooltip.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Tooltip from "@material-ui/core/Tooltip"; 4 | 5 | const SuperTooltip = ({ title, hide, children, ...props }) => 6 | hide ? ( 7 | children 8 | ) : ( 9 | 10 | {children} 11 | 12 | ); 13 | 14 | SuperTooltip.propTypes = { 15 | ...Tooltip.propTypes, 16 | hide: PropTypes.bool, 17 | }; 18 | 19 | SuperTooltip.defaultProps = { 20 | ...Tooltip.defaultProps, 21 | hide: false, 22 | }; 23 | 24 | export default SuperTooltip; 25 | -------------------------------------------------------------------------------- /src/components/atoms/SuperTooltip/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SuperTooltip"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/TagList/TagList.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useState } from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled, { css } from "styled-components"; 4 | 5 | import { ZABO_CATEGORIES } from "lib/variables"; 6 | 7 | export const TagListWrapper = styled.div` 8 | margin-top: 20px; 9 | display: flex; 10 | flex-wrap: wrap; 11 | 12 | button { 13 | font-size: 16px; 14 | line-height: 16px; 15 | padding: 10px 14px; 16 | margin: 0 12px 10px 0; 17 | border: 1px solid #143441; 18 | border-radius: 4px; 19 | &:hover, 20 | &:focus, 21 | &.clicked { 22 | color: white; 23 | background-color: #143441; 24 | } 25 | &.unclicked { 26 | color: #143441; 27 | background-color: white; 28 | } 29 | } 30 | ${(props) => 31 | props.type === "search" 32 | ? css` 33 | width: 100%; 34 | scroll-behavior: smooth; 35 | overflow-x: scroll; 36 | white-space: nowrap; 37 | /* hide scroll bar */ 38 | /* -webkit- (Chrome, Safari, newer versions of Opera) */ 39 | &::-webkit-scrollbar { 40 | width: 0 !important; 41 | } 42 | /* Firefox */ 43 | scrollbar-width: none; 44 | /* -ms- (Internet Explorer +10) */ 45 | -ms-overflow-style: none; 46 | 47 | button { 48 | font-size: 14px; 49 | padding: 8px 10px; 50 | margin-right: 8px; 51 | &:last-child { 52 | margin-right: 0; 53 | } 54 | } 55 | ` 56 | : css``} 57 | `; 58 | 59 | const TagList = ({ type, onTagClick, clickedTags }) => { 60 | const handleClick = (e) => { 61 | const { value: category } = e.target; 62 | onTagClick(category); 63 | }; 64 | 65 | const tagList = useMemo( 66 | () => 67 | ZABO_CATEGORIES.map((tag, idx) => { 68 | const cName = clickedTags.includes(tag) ? "clicked" : "unclicked"; 69 | return ( 70 | 73 | ); 74 | }), 75 | [clickedTags, onTagClick], 76 | ); 77 | 78 | return {tagList}; 79 | }; 80 | 81 | TagList.propTypes = { 82 | type: PropTypes.string, 83 | onTagClick: PropTypes.func.isRequired, 84 | clickedTags: PropTypes.arrayOf(PropTypes.string), 85 | }; 86 | 87 | TagList.defaultProps = { 88 | type: "", 89 | clickedTags: [], 90 | }; 91 | 92 | export default TagList; 93 | -------------------------------------------------------------------------------- /src/components/atoms/TagList/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./TagList"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/Todo/Todo.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import TodoWrapper from "./Todo.styled"; 5 | 6 | const Todo = ({ onClick, completed, text }) => ( 7 | 13 | {text} 14 | 15 | ); 16 | 17 | Todo.propTypes = { 18 | onClick: PropTypes.func.isRequired, 19 | completed: PropTypes.bool.isRequired, 20 | text: PropTypes.string.isRequired, 21 | }; 22 | 23 | Todo.defaultProps = {}; 24 | 25 | export default Todo; 26 | -------------------------------------------------------------------------------- /src/components/atoms/Todo/Todo.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import Todo from "./index"; 5 | 6 | storiesOf("atoms/Todo", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/atoms/Todo/Todo.styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const TodoWrapper = styled.li``; 4 | 5 | export default TodoWrapper; 6 | -------------------------------------------------------------------------------- /src/components/atoms/Todo/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Todo"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/ToggleButton/ToggleButton.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | 5 | const ToggleButtonWrapper = styled.div` 6 | display: inline-block; 7 | /* The switch - the box around the slider */ 8 | .switch { 9 | position: relative; 10 | display: inline-block; 11 | width: 60px; 12 | height: 30px; 13 | } 14 | 15 | /* Hide default HTML checkbox */ 16 | .switch input { 17 | opacity: 0; 18 | width: 0; 19 | height: 0; 20 | } 21 | 22 | /* The slider */ 23 | .slider { 24 | position: absolute; 25 | cursor: pointer; 26 | top: 0; 27 | left: 0; 28 | right: 0; 29 | bottom: 0; 30 | background-color: #bcbcbc; 31 | transition: 0.4s; 32 | } 33 | 34 | .slider:before { 35 | position: absolute; 36 | content: ""; 37 | height: 22px; 38 | width: 22px; 39 | left: 4px; 40 | bottom: 4px; 41 | background-color: white; 42 | transition: 0.4s; 43 | } 44 | 45 | input:checked + .slider { 46 | background-color: #143441; 47 | } 48 | 49 | input:focus + .slider { 50 | box-shadow: 0 0 1px #143441; 51 | } 52 | 53 | input:checked + .slider:before { 54 | transform: translateX(29px); 55 | } 56 | 57 | /* Rounded sliders */ 58 | .slider.round { 59 | border-radius: 34px; 60 | } 61 | 62 | .slider.round:before { 63 | border-radius: 50%; 64 | } 65 | `; 66 | 67 | const ToggleButton = ({ onChange, checked }) => ( 68 | 69 | 73 | 74 | ); 75 | 76 | ToggleButton.propTypes = { 77 | onChange: PropTypes.func.isRequired, 78 | checked: PropTypes.bool, 79 | }; 80 | 81 | ToggleButton.defaultProps = { 82 | checked: false, 83 | }; 84 | 85 | export default ToggleButton; 86 | -------------------------------------------------------------------------------- /src/components/atoms/ToggleButton/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ToggleButton"; 2 | -------------------------------------------------------------------------------- /src/components/atoms/TwoCol/TwoCol.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import styled, { css } from "styled-components"; 3 | 4 | import { media } from "lib/utils/style"; 5 | 6 | const colMixin = css` 7 | display: flex; 8 | flex-direction: column; 9 | flex: ${(props) => props.flex} 0 0; 10 | flex-shrink: 0; 11 | flex-basis: auto; 12 | min-width: 0; 13 | ${media.tablet(css` 14 | flex: ${(props) => props.flex}; 15 | `)}; 16 | `; 17 | 18 | const Left = styled.section` 19 | ${colMixin}; 20 | `; 21 | Left.propTypes = { 22 | flex: PropTypes.number, 23 | }; 24 | Left.defaultProps = { 25 | flex: 1, 26 | }; 27 | 28 | const Right = styled.section` 29 | ${colMixin}; 30 | `; 31 | Right.propTypes = { 32 | flex: PropTypes.number, 33 | }; 34 | Right.defaultProps = { 35 | flex: 1, 36 | }; 37 | 38 | const TwoCol = styled.section` 39 | display: flex; 40 | width: 100%; 41 | flex-wrap: wrap; 42 | flex-shrink: 0; 43 | flex-basis: auto; 44 | ${(props) => 45 | props.mobileWrap 46 | ? css` 47 | flex-direction: column; 48 | ` 49 | : ""}; 50 | 51 | ${media.tablet(css` 52 | flex-direction: row; 53 | flex-wrap: nowrap; 54 | ${(props) => 55 | props.divider 56 | ? css` 57 | ${Left} { 58 | padding-right: 24px; 59 | border-right: 1px solid ${(props) => props.theme.gray10}; 60 | } 61 | ${Right} { 62 | padding-left: 24px; 63 | } 64 | ` 65 | : ""}; 66 | `)}; 67 | `; 68 | 69 | TwoCol.propTypes = { 70 | mobileWrap: PropTypes.bool, 71 | }; 72 | TwoCol.defaultProps = { 73 | mobileWrap: true, 74 | }; 75 | 76 | TwoCol.Left = Left; 77 | TwoCol.Right = Right; 78 | 79 | TwoCol.DLeft = styled.div` 80 | display: flex; 81 | flex-direction: column; 82 | flex: 1; 83 | `; 84 | TwoCol.DRight = styled.div` 85 | flex: 1; 86 | `; 87 | 88 | export default TwoCol; 89 | -------------------------------------------------------------------------------- /src/components/atoms/TwoCol/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./TwoCol"; 2 | -------------------------------------------------------------------------------- /src/components/containers/ChannelTalk.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import { RouteComponentProps, useLocation } from "react-router-dom"; 3 | import { useSelector } from "react-redux"; 4 | import { get } from "lodash"; 5 | import queryString from "query-string"; 6 | import { IState } from "types/store.d"; 7 | 8 | import ChannelService from "lib/channel_io"; 9 | import { isAuthedSelector } from "lib/utils"; 10 | 11 | const ChannelTalk = ({ match }: RouteComponentProps<{ top: string }>) => { 12 | const { search, pathname } = useLocation(); 13 | const { top } = match.params; 14 | const isAuthenticated = useSelector(isAuthedSelector); 15 | const info = useSelector((state: IState) => get(state, ["auth", "info"])); 16 | 17 | useEffect(() => { 18 | if (process.env.NODE_ENV !== "production") return; 19 | if (top === "settings") { 20 | ChannelService.shutdown(); 21 | return; 22 | } 23 | if (pathname === "/zabo/upload") { 24 | ChannelService.shutdown(); 25 | return; 26 | } 27 | const { code, state } = queryString.parse(search); 28 | if (code && state) return; 29 | const settings = { 30 | pluginKey: "5fe8c634-bcbd-4499-ba99-967191a2ef77", 31 | }; 32 | if (isAuthenticated) { 33 | if (!info) return; 34 | const { _id, username, koreanName, groups, flags, email, profilePhoto } = info; 35 | Object.assign(settings, { 36 | userId: _id, 37 | profile: { 38 | name: username, 39 | koreanName, 40 | avatarUrl: profilePhoto, 41 | groups: groups.map((group) => group.name).join(", "), 42 | flags: flags.join(", "), 43 | email, 44 | }, 45 | }); 46 | } 47 | ChannelService.boot(settings); 48 | }, [search, isAuthenticated, info, top, pathname]); 49 | return null; 50 | }; 51 | 52 | export default ChannelTalk; 53 | -------------------------------------------------------------------------------- /src/components/containers/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useLocation, useParams } from "react-router-dom"; 4 | 5 | interface ScrollToTopProps { 6 | updateWithPath?: boolean; 7 | } 8 | 9 | const ScrollToTop: React.FC = ({ updateWithPath }) => { 10 | // const { route } = useParams(); 11 | const { pathname } = useLocation(); 12 | // useEffect(() => { 13 | // window.scrollTo(0, 0); 14 | // }, [route]); 15 | useEffect(() => { 16 | if (updateWithPath) window.scrollTo(0, 0); 17 | }, [updateWithPath, pathname]); 18 | return null; 19 | }; 20 | 21 | ScrollToTop.propTypes = { 22 | updateWithPath: PropTypes.bool, 23 | }; 24 | 25 | ScrollToTop.defaultProps = { 26 | updateWithPath: false, 27 | }; 28 | 29 | export default ScrollToTop; 30 | -------------------------------------------------------------------------------- /src/components/containers/WindowResizeListener.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | 4 | import { setWindowSize } from "store/reducers/app"; 5 | 6 | function getDimensions() { 7 | const width = window.innerWidth; 8 | const height = window.innerHeight; 9 | return { width, height }; 10 | } 11 | 12 | const WindowResizeListener = () => { 13 | const dispatch = useDispatch(); 14 | const handleResize = useCallback(() => { 15 | dispatch(setWindowSize(getDimensions())); 16 | }, []); 17 | useEffect(() => { 18 | window.addEventListener("optimizedResize", handleResize); 19 | return () => { 20 | window.removeEventListener("optimizedResize", handleResize); 21 | }; 22 | }, []); 23 | return null; 24 | }; 25 | 26 | export default WindowResizeListener; 27 | -------------------------------------------------------------------------------- /src/components/molecules/OwnerInfo/OwnerInfo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled, { css } from "styled-components"; 4 | 5 | import { ZaboType } from "lib/propTypes"; 6 | import { media } from "lib/utils/style"; 7 | 8 | import defaultProfile from "static/images/defaultProfile.png"; 9 | 10 | export const OwnerInfoW = styled.div` 11 | display: flex; 12 | `; 13 | 14 | const ProfileW = styled.img` 15 | width: auto; 16 | `; 17 | 18 | const WritingsW = styled.div` 19 | color: ${(props) => props.theme.gray100}; 20 | `; 21 | WritingsW.Name = styled.div` 22 | font-size: 14px; 23 | line-height: 16px; 24 | ${media.tablet(css` 25 | font-size: 14px; 26 | line-height: 16px; 27 | `)}; 28 | `; 29 | WritingsW.Sub = styled.div` 30 | font-size: 12px; 31 | line-height: 14px; 32 | `; 33 | 34 | const OwnerInfo = ({ info, showProfile, showSub, styles, style }) => { 35 | const { name, profilePhoto, subtitle } = info; 36 | const safeUrl = profilePhoto || defaultProfile; 37 | return ( 38 | 39 | {showProfile && } 40 | 41 | {name || ""} 42 | {showSub && {subtitle || ""}} 43 | 44 | 45 | ); 46 | }; 47 | 48 | OwnerInfo.propTypes = { 49 | info: ZaboType.owner, 50 | style: PropTypes.shape({}), 51 | styles: PropTypes.shape({ 52 | writings: PropTypes.shape(), 53 | name: PropTypes.shape(), 54 | subtitle: PropTypes.shape(), 55 | }), 56 | showProfile: PropTypes.bool, 57 | showSub: PropTypes.bool, 58 | }; 59 | 60 | OwnerInfo.defaultProps = { 61 | info: {}, 62 | style: {}, 63 | styles: { 64 | name: {}, 65 | subtitle: {}, 66 | }, 67 | showProfile: false, 68 | showSub: false, 69 | }; 70 | 71 | export default OwnerInfo; 72 | -------------------------------------------------------------------------------- /src/components/molecules/OwnerInfo/index.js: -------------------------------------------------------------------------------- 1 | export { default, OwnerInfoW } from "./OwnerInfo"; 2 | -------------------------------------------------------------------------------- /src/components/molecules/ScrollBtn/ScrollBtn.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from "react"; 2 | import styled, { css } from "styled-components"; 3 | 4 | import leftScroll from "static/images/leftScroll.png"; 5 | import rightScroll from "static/images/rightScroll.png"; 6 | 7 | const ScrollBtnW = styled.div<{ show?: boolean }>` 8 | ${(props) => 9 | props.show 10 | ? css`` 11 | : css` 12 | display: none; 13 | `}; 14 | @media (max-width: 640px) { 15 | display: none; 16 | } 17 | float: right; 18 | 19 | img { 20 | width: 30px; 21 | height: 30px; 22 | margin-left: 3px; 23 | border-radius: 50%; 24 | cursor: pointer; 25 | 26 | &:hover { 27 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); 28 | } 29 | } 30 | 31 | margin: 0 auto; 32 | `; 33 | 34 | interface Props { 35 | elemId: string; 36 | show?: boolean; 37 | scrollSize?: number; 38 | left?: boolean; 39 | right?: boolean; 40 | } 41 | 42 | const ScrollBtn: React.FC = ({ 43 | elemId, 44 | show, 45 | scrollSize = 622, 46 | left = true, 47 | right = true, 48 | }) => { 49 | const scroll = useCallback( 50 | (offset: number) => { 51 | const elem = document.getElementById(elemId); 52 | if (elem) elem.scrollLeft += offset; 53 | }, 54 | [elemId], 55 | ); 56 | 57 | const leftScrollClick = useCallback(() => scroll(-scrollSize), [scroll, scrollSize]); 58 | const rightScrollClick = useCallback(() => scroll(scrollSize), [scroll, scrollSize]); 59 | return ( 60 | 61 | {left && left scroll button} 62 | {right && right scroll button} 63 | 64 | ); 65 | }; 66 | 67 | export default ScrollBtn; 68 | -------------------------------------------------------------------------------- /src/components/molecules/ScrollBtn/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./ScrollBtn"; 2 | -------------------------------------------------------------------------------- /src/components/molecules/Select/Select.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import ReactSelect from "react-select"; 4 | import ReactAsyncSelect from "react-select/async"; 5 | 6 | const Select = ({ async, ...props }) => { 7 | let Component = ReactSelect; 8 | if (async) Component = ReactAsyncSelect; 9 | return ; 10 | }; 11 | 12 | Select.propTypes = { 13 | ...ReactSelect.propTypes, 14 | ...ReactAsyncSelect.propTypes, 15 | async: PropTypes.bool, 16 | }; 17 | 18 | Select.defaultProps = { 19 | async: false, 20 | }; 21 | 22 | export default Select; 23 | -------------------------------------------------------------------------------- /src/components/molecules/Select/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Select"; 2 | -------------------------------------------------------------------------------- /src/components/molecules/SimpleSelect/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SimpleSelect"; 2 | -------------------------------------------------------------------------------- /src/components/molecules/StatBox/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./StatBox"; 2 | -------------------------------------------------------------------------------- /src/components/organisms/AuthCallback/AuthCallback.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import queryString from "query-string"; 4 | import type { RouteComponentProps } from "react-router-dom"; 5 | 6 | import { loginCallback } from "store/reducers/auth"; 7 | import storage from "lib/storage"; 8 | import type { Action } from "redux-actions"; 9 | 10 | // TODO 11 | // This is a temporary fix to avoid type errors 12 | // We should find a way to infer types from redux middlewares 13 | interface Dispatch { 14 |

(action: Action

): P; 15 | } 16 | 17 | const AuthCallback: React.FC = ({ location, history }) => { 18 | const dispatch = useDispatch(); 19 | useEffect(() => { 20 | const { code, state } = queryString.parse(location.search); 21 | if (typeof code === "string" && typeof state === "string") { 22 | dispatch(loginCallback(code, state)) 23 | .then((res) => { 24 | const referrer = storage.getItem("referrer"); 25 | if (referrer) { 26 | storage.removeItem("referrer"); 27 | history.replace(referrer); 28 | return; 29 | } 30 | history.replace(`/${res.user.username}`); 31 | }) 32 | .catch((error) => { 33 | alert(error.message); 34 | history.replace("/"); 35 | }); 36 | } 37 | }, [dispatch, history, location.search]); 38 | return null; 39 | }; 40 | 41 | export default AuthCallback; 42 | -------------------------------------------------------------------------------- /src/components/organisms/AuthCallback/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./AuthCallback"; 2 | -------------------------------------------------------------------------------- /src/components/organisms/GroupBox/GroupBox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import GroupBoxA from "./GroupBoxA"; 4 | import GroupBoxP from "./GroupBoxP"; 5 | import GroupBoxS from "./GroupBoxS"; 6 | import type { Group } from "lib/interface"; 7 | 8 | interface Props { 9 | type?: "profile" | "simple" | "apply"; 10 | group?: Group; 11 | } 12 | 13 | const GroupBox: React.FC = ({ type = "profile", group = null, ...props }) => { 14 | switch (type) { 15 | case "profile": 16 | return group && ; 17 | case "simple": 18 | return group && ; 19 | case "apply": 20 | return ; 21 | default: 22 | return null; 23 | } 24 | }; 25 | 26 | export default GroupBox; 27 | -------------------------------------------------------------------------------- /src/components/organisms/GroupBox/GroupBoxA.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import addGray from "static/images/add_gray.svg"; 4 | 5 | import { GroupAW } from "./GroupBox.styled"; 6 | 7 | const GroupBox: React.FC = () => ( 8 | 9 | add icon 10 |

새로운 그룹 신청하기

11 | 12 | ); 13 | 14 | export default GroupBox; 15 | -------------------------------------------------------------------------------- /src/components/organisms/GroupBox/GroupBoxP.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Tooltip from "@material-ui/core/Tooltip"; 3 | 4 | import { getLabeledTimeDiff } from "lib/utils"; 5 | 6 | import groupDefaultProfile from "static/images/groupDefaultProfile.png"; 7 | 8 | import { GroupW, NameW, ProfileStatsW, WritingsW } from "./GroupBox.styled"; 9 | import type { Group } from "lib/interface"; 10 | 11 | interface Props { 12 | group: Group; 13 | } 14 | 15 | const GroupBox: React.FC = ({ group, ...props }) => { 16 | const timePast = group.recentUpload 17 | ? getLabeledTimeDiff(group.recentUpload, 60, 60, 24, 7, 5, 12) 18 | : "없음"; 19 | const stats = [ 20 | { 21 | name: "올린 자보", 22 | value: group.zabosCount, 23 | }, 24 | { 25 | name: "팔로워", 26 | value: group.followersCount, 27 | }, 28 | { 29 | name: "최근 업로드", 30 | value: timePast, 31 | }, 32 | ]; 33 | 34 | return ( 35 | { 40 | if (group.isPending) e.preventDefault(); 41 | }} 42 | {...props} 43 | > 44 | {group.profilePhoto ? ( 45 | group profile photo 46 | ) : ( 47 | default group profile photo 48 | )} 49 | 50 | 51 | {group.name} 52 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | 59 | export default GroupBox; 60 | -------------------------------------------------------------------------------- /src/components/organisms/GroupBox/GroupBoxS.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import get from "lodash.get"; 4 | 5 | import SuperTooltip from "components/atoms/SuperTooltip"; 6 | 7 | import { isElemWidthOverflown } from "lib/utils"; 8 | 9 | import groupDefaultProfile from "static/images/groupDefaultProfile.png"; 10 | 11 | import { GroupSW, NameW, SubtitleW, WritingsW } from "./GroupBox.styled"; 12 | import { Group } from "lib/interface"; 13 | 14 | interface Props { 15 | group: Group; 16 | } 17 | 18 | const GroupBoxS: React.FC = ({ group, ...props }) => { 19 | const width = useSelector((state) => get(state, ["app", "windowSize", "width"])); 20 | const nameRef = useRef(null); 21 | const [showTooltip, setShowTooltip] = useState(false); 22 | useEffect(() => { 23 | nameRef.current && setShowTooltip(isElemWidthOverflown(nameRef.current)); 24 | }, [nameRef, width]); 25 | 26 | return ( 27 | 28 |
29 | {group.profilePhoto ? ( 30 | group profile 31 | ) : ( 32 | default group profile 33 | )} 34 |
35 | 36 | 37 | {group.name} 38 | 39 | {group.subtitle || "한 줄 소개 없음"} 40 | 41 |
42 | ); 43 | }; 44 | 45 | export default GroupBoxS; 46 | -------------------------------------------------------------------------------- /src/components/organisms/GroupBox/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./GroupBox"; 2 | -------------------------------------------------------------------------------- /src/components/organisms/GroupList/GroupList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | import ScrollBtn from "../../molecules/ScrollBtn"; 5 | import GroupBox from "../GroupBox"; 6 | import type { Group } from "lib/interface/schemas"; 7 | 8 | const GroupsComponent = styled.section` 9 | width: 1032px; 10 | 11 | h1 { 12 | display: inline-block; 13 | font-size: 22px; 14 | color: #363636; 15 | margin: 0 0 16px; 16 | font-weight: 800; 17 | } 18 | 19 | @media (max-width: 640px) { 20 | width: 100%; 21 | /* padding: 0 16px; */ 22 | h1 { 23 | padding: 0 16px; 24 | font-size: 18px; 25 | margin-bottom: 12px; 26 | } 27 | } 28 | `; 29 | 30 | const GroupsListComponent = styled.div` 31 | display: flex; 32 | scroll-behavior: smooth; 33 | width: 100%; 34 | padding: 3px; 35 | margin-top: 16px; 36 | overflow-x: scroll; 37 | /* overflow-y: visible; */ 38 | white-space: nowrap; 39 | 40 | /* hide scroll bar */ 41 | /* -webkit- (Chrome, Safari, newer versions of Opera) */ 42 | 43 | &::-webkit-scrollbar { 44 | width: 0 !important; 45 | } 46 | 47 | /* Firefox */ 48 | scrollbar-width: none; 49 | /* -ms- (Internet Explorer +10) */ 50 | -ms-overflow-style: none; 51 | @media (max-width: 640px) { 52 | margin-top: 12px; 53 | padding: 3px 16px; 54 | } 55 | `; 56 | 57 | export const Groups = Object.assign(GroupsComponent, { List: GroupsListComponent }); 58 | 59 | const text = { 60 | profile: "소속 그룹", 61 | search: "그룹 검색 결과", 62 | } as const; 63 | 64 | interface Props { 65 | type: keyof typeof text; 66 | groups: Group[]; 67 | hasApplyBox?: boolean; 68 | isMyProfile?: boolean; 69 | } 70 | 71 | const GroupList: React.FC = ({ type, groups, isMyProfile }) => ( 72 | 73 |

{text[type]}

74 | 3} /> 75 | 76 | {groups.map((group) => ( 77 | 78 | ))} 79 | {isMyProfile && } 80 | <>  81 | 82 |
83 | ); 84 | 85 | export default GroupList; 86 | -------------------------------------------------------------------------------- /src/components/organisms/GroupList/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./GroupList"; 2 | -------------------------------------------------------------------------------- /src/components/organisms/List/List.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import styled from "styled-components"; 4 | 5 | const StyledList = styled.div` 6 | width: 100%; 7 | `; 8 | 9 | const List = ({ dataSource, renderItem }) => {dataSource.map(renderItem)}; 10 | 11 | List.propTypes = { 12 | dataSource: PropTypes.arrayOf(PropTypes.shape({})).isRequired, 13 | renderItem: PropTypes.func, 14 | }; 15 | 16 | List.defaultProps = { 17 | renderItem: (item) =>
{item}
, 18 | }; 19 | 20 | export default List; 21 | -------------------------------------------------------------------------------- /src/components/organisms/List/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./List"; 2 | -------------------------------------------------------------------------------- /src/components/organisms/MemberItem/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./MemberItem"; 2 | -------------------------------------------------------------------------------- /src/components/organisms/ProfileStats/ProfileStats.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { Stats } from "./ProfileStats.styled"; 5 | 6 | const ProfileStats = ({ stats, smallV, ...props }) => ( 7 | 8 | {stats.map(({ name, value }) => ( 9 | 10 |

{value || 0}

11 |
{name}
12 |
13 | ))} 14 |
15 | ); 16 | 17 | ProfileStats.propTypes = { 18 | stats: PropTypes.arrayOf( 19 | PropTypes.shape({ 20 | name: PropTypes.string.isRequired, 21 | value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, 22 | }), 23 | ).isRequired, 24 | smallV: PropTypes.bool, 25 | }; 26 | 27 | ProfileStats.defaultProps = { 28 | smallV: false, 29 | }; 30 | 31 | export default ProfileStats; 32 | -------------------------------------------------------------------------------- /src/components/organisms/ProfileStats/ProfileStats.styled.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | export const Stats = styled.section` 4 | display: inline-block; 5 | `; 6 | 7 | Stats.elem = styled.div` 8 | display: inline-block; 9 | border-right: 1px solid #e9e9e9; 10 | padding: 0 18px; 11 | 12 | h3 { 13 | font-size: 22px; 14 | font-weight: 800; 15 | color: #143441; 16 | text-align: center; 17 | margin: 0 0 6px 0; 18 | } 19 | div { 20 | font-size: 14px; 21 | color: #8f8f8f; 22 | text-align: center; 23 | } 24 | 25 | ${(props) => 26 | props.small 27 | ? css` 28 | padding: 0 14px; 29 | h3 { 30 | font-size: 16px; 31 | margin-bottom: 4px; 32 | font-weight: bold; 33 | } 34 | div { 35 | font-size: 12px; 36 | } 37 | @media (max-width: 640px) { 38 | padding: 0 11px; 39 | h3 { 40 | font-size: 14px; 41 | margin-bottom: 3px; 42 | } 43 | div { 44 | font-size: 10px; 45 | } 46 | } 47 | ` 48 | : css``}; 49 | 50 | &:nth-child(1) { 51 | padding-left: 0; 52 | } 53 | &:nth-child(3) { 54 | padding-right: 0; 55 | border-right: none; 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /src/components/organisms/ProfileStats/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ProfileStats"; 2 | -------------------------------------------------------------------------------- /src/components/organisms/SearchSelect/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SearchSelect"; 2 | -------------------------------------------------------------------------------- /src/components/organisms/ZaboCard/ZaboCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import withZabo from "hoc/withZabo"; 5 | import { ZaboType } from "lib/propTypes"; 6 | 7 | import ZaboCardL from "./ZaboCardL"; 8 | import ZaboCardM from "./ZaboCardM"; 9 | 10 | const ZaboCard = ({ zabo, size }) => { 11 | if (size === "large") return ; 12 | return ; 13 | }; 14 | 15 | ZaboCard.propTypes = { 16 | zabo: ZaboType.isRequired, 17 | size: PropTypes.oneOf(["medium", "large"]), 18 | }; 19 | 20 | ZaboCard.defaultProps = { 21 | size: "medium", 22 | }; 23 | 24 | export default withZabo(ZaboCard, false, false); 25 | -------------------------------------------------------------------------------- /src/components/organisms/ZaboCard/ZaboCard.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import ZaboCard from "./index"; 5 | 6 | storiesOf("organisms/ZaboCard", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/organisms/ZaboCard/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ZaboCard"; 2 | -------------------------------------------------------------------------------- /src/components/pages/ApiPage/ApiPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Switch, useRouteMatch } from "react-router-dom"; 3 | 4 | import { PublicRoute } from "hoc/AuthRoutes"; 5 | import axios from "lib/axios"; 6 | 7 | const Login = () => { 8 | useEffect(() => { 9 | axios 10 | .get("/auth/loginApi") 11 | .then((data) => { 12 | window.location.href = data.url; 13 | }) 14 | .catch((error) => { 15 | console.error(error); 16 | alert("로그인에 실패하였습니다."); 17 | }); 18 | }, []); 19 | return null; 20 | }; 21 | 22 | const ApiPage = () => { 23 | const { path } = useRouteMatch(); 24 | return ( 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | ApiPage.propTypes = {}; 32 | 33 | ApiPage.defaultProps = {}; 34 | 35 | export default ApiPage; 36 | -------------------------------------------------------------------------------- /src/components/pages/ApiPage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ApiPage"; 2 | -------------------------------------------------------------------------------- /src/components/pages/AuthPage/AuthPage.container.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { connect } from "react-redux"; 3 | 4 | import AuthPage from "./AuthPage"; 5 | 6 | class AuthPageContainer extends PureComponent { 7 | render() { 8 | return ; 9 | } 10 | } 11 | 12 | const mapStateToProps = (state) => ({}); 13 | 14 | const mapDispatchToProps = (dispatch) => ({}); 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(AuthPageContainer); 17 | -------------------------------------------------------------------------------- /src/components/pages/AuthPage/AuthPage.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { Redirect, Route, Switch } from "react-router-dom"; 3 | 4 | import { LoginPage } from "components/pages"; 5 | 6 | import AuthPageWrapper from "./AuthPage.styled"; 7 | 8 | class AuthPage extends PureComponent { 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | AuthPage.propTypes = {}; 22 | 23 | AuthPage.defaultProps = {}; 24 | 25 | export default AuthPage; 26 | -------------------------------------------------------------------------------- /src/components/pages/AuthPage/AuthPage.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import AuthPage from "./index"; 5 | 6 | storiesOf("pages/AuthPage", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/pages/AuthPage/AuthPage.styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const AuthPageWrapper = styled.div``; 4 | 5 | export default AuthPageWrapper; 6 | -------------------------------------------------------------------------------- /src/components/pages/AuthPage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./AuthPage.container"; 2 | -------------------------------------------------------------------------------- /src/components/pages/HomePage/HomePage.container.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { connect } from "react-redux"; 3 | import get from "lodash.get"; 4 | 5 | import { getZaboList } from "store/reducers/zabo"; 6 | 7 | import HomePage from "./HomePage"; 8 | 9 | // deliver states(Redux) as props to the HomePage component 10 | class HomePageContainer extends PureComponent { 11 | render() { 12 | return ; 13 | } 14 | } 15 | 16 | // Subscribe to the Redux "state" 17 | const mapStateToProps = (state) => ({ 18 | zaboList: get(state, ["zabo", "zaboList"]), 19 | }); 20 | 21 | // HomePage 에서 변경 사항이 생긴다면 널리 알려라. 22 | const mapDispatchToProps = { 23 | getZaboList, 24 | }; 25 | 26 | // index.js 가 HomePage 를 import 하는 것이 아닌, HomePage 27 | export default connect(mapStateToProps, mapDispatchToProps)(HomePageContainer); 28 | -------------------------------------------------------------------------------- /src/components/pages/HomePage/HomePage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Header from "components/templates/Header"; 4 | import ZaboList from "components/templates/ZaboList"; 5 | 6 | import HomePageWrapper from "./HomePage.styled"; 7 | 8 | const HomePage = () => ( 9 | 10 |
11 |
12 | 13 |
14 | 15 | ); 16 | 17 | HomePage.propTypes = {}; 18 | 19 | HomePage.defaultProps = {}; 20 | 21 | export default HomePage; 22 | -------------------------------------------------------------------------------- /src/components/pages/HomePage/HomePage.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from "react"; 3 | import { storiesOf } from "@storybook/react"; 4 | 5 | import HomePage from "./index"; 6 | 7 | storiesOf("pages/HomePage", module).add("Default", () => , { 8 | notes: "", 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/pages/HomePage/HomePage.styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | /* ============ Wrapper ============ */ 4 | const HomePageWrapper = styled.div` 5 | transition: 0.4s; 6 | animation-duration: 0.3s; 7 | 8 | /* 9 | * Poster Layout 10 | * poster = 240px, column space = 10px 11 | * .container padding = 0px 20px 12 | * num of posters: total width 13 | * 1: 240px 14 | * 2: 490px => 530 ~ 15 | * 3: 760px => 800 ~ 16 | * 4: 1020px => 1060 ~ 17 | */ 18 | 19 | /* @media (min-width: 0px) and (max-width: 530px) { 20 | !* .container width auto with padding *! 21 | } 22 | @media (min-width: 530px) and (max-width: 800px) { 23 | .container { 24 | width: 530px; 25 | } 26 | } 27 | @media (min-width: 800px) and (max-width: 1060px) { 28 | .container { 29 | width: 800px; 30 | } 31 | } 32 | @media (min-width: 1060px) { 33 | .container { 34 | width: 1060px; 35 | } 36 | }*/ 37 | `; 38 | 39 | export default HomePageWrapper; 40 | 41 | /* ============ Header ============ */ 42 | export const Header = styled.div` 43 | display: flex; 44 | align-items: center; 45 | 46 | .blur { 47 | transition: 0.5s; 48 | position: absolute; 49 | top: 0; 50 | left: 0; 51 | width: 100%; 52 | height: 0; 53 | z-index: 1; 54 | &.show { 55 | border-top: 6px solid rgb(27, 50, 65); 56 | height: 100%; 57 | background-color: rgba(255, 255, 255); 58 | } 59 | } 60 | `; 61 | Header.Search = styled.div` 62 | flex: 1; 63 | margin-right: 12px; 64 | transition: 1s; 65 | z-index: 2; 66 | `; 67 | Header.AddButton = styled.button` 68 | width: 60px; 69 | height: 40px; 70 | background-color: rgb(27, 50, 65); 71 | `; 72 | -------------------------------------------------------------------------------- /src/components/pages/HomePage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./HomePage.container"; 2 | -------------------------------------------------------------------------------- /src/components/pages/LandingPage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./LandingPage"; 2 | -------------------------------------------------------------------------------- /src/components/pages/LoginPage/LoginPage.container.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { connect } from "react-redux"; 3 | 4 | import LoginPage from "./LoginPage"; 5 | 6 | class LoginPageContainer extends PureComponent { 7 | render() { 8 | return ; 9 | } 10 | } 11 | 12 | const mapStateToProps = (state) => ({}); 13 | 14 | const mapDispatchToProps = (dispatch) => ({}); 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(LoginPageContainer); 17 | -------------------------------------------------------------------------------- /src/components/pages/LoginPage/LoginPage.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | import axios from "lib/axios"; 4 | import storage from "lib/storage"; 5 | 6 | const LoginPage = ({ history }) => { 7 | useEffect(() => { 8 | const { state } = history.location; 9 | if (state && state.referrer) { 10 | storage.setItem("referrer", state.referrer); 11 | } 12 | axios 13 | .get("/auth/loginApi") 14 | .then((data) => { 15 | window.location.replace(data.url); 16 | }) 17 | .catch((error) => { 18 | console.error(error); 19 | alert("로그인에 실패하였습니다."); 20 | }); 21 | }, [history]); 22 | return null; 23 | }; 24 | LoginPage.propTypes = {}; 25 | 26 | LoginPage.defaultProps = {}; 27 | 28 | export default LoginPage; 29 | -------------------------------------------------------------------------------- /src/components/pages/LoginPage/LoginPage.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import LoginPage from "./index"; 5 | 6 | storiesOf("pages/LoginPage", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/pages/LoginPage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./LoginPage.container"; 2 | -------------------------------------------------------------------------------- /src/components/pages/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | 4 | import notFoundImage from "static/images/notFoundImage.jpg"; 5 | import logo from "static/logo/logo.svg"; 6 | 7 | import { Page } from "./NotFound.styled"; 8 | 9 | const NotFound = () => ( 10 | 11 | 12 | 13 | logo 14 | 15 | 16 | 17 | 18 | 19 | 요청하신 페이지를 찾을 수 없습니다. 20 |
21 | 메인페이지로 이동하기 22 |
23 |
24 |
25 | ); 26 | 27 | export default NotFound; 28 | -------------------------------------------------------------------------------- /src/components/pages/NotFound/NotFound.styled.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | import { media } from "lib/utils/style"; 4 | 5 | export const Page = styled.section` 6 | width: 100%; 7 | height: 100%; 8 | `; 9 | 10 | Page.Header = styled.div` 11 | position: fixed; 12 | top: 0; 13 | width: 100%; 14 | height: 55px; 15 | border-top: 5px solid ${(props) => props.theme.main}; 16 | padding: 10px 18px; 17 | `; 18 | 19 | Page.Body = styled.div` 20 | width: 90vw; 21 | margin: 240px 5vw 0; 22 | ${media.tablet(css` 23 | width: 34vw; 24 | margin: 280px 33vw 0; 25 | `)}; 26 | `; 27 | 28 | Page.Title = styled.img` 29 | width: 60vw; 30 | margin: 0 15vw; 31 | ${media.tablet(css` 32 | width: 34vw; 33 | margin: 0; 34 | `)}; 35 | `; 36 | 37 | Page.Description = styled.p` 38 | text-align: center; 39 | margin-top: 28px; 40 | line-height: 1.6; 41 | font-size: 16px; 42 | color: ${(props) => props.theme.gray30}; 43 | ${media.tablet(css` 44 | margin-top: 48px; 45 | font-size: 18px; 46 | `)}; 47 | `; 48 | 49 | Page.Description.Link = styled.a` 50 | color: ${(props) => props.theme.main} !important; 51 | font-weight: bold; 52 | text-decoration: underline; 53 | `; 54 | 55 | export default Page; 56 | -------------------------------------------------------------------------------- /src/components/pages/NotFound/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./NotFound"; 2 | -------------------------------------------------------------------------------- /src/components/pages/ProfilePage/Main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch, useRouteMatch } from "react-router-dom"; 3 | import styled from "styled-components"; 4 | 5 | import { NotFound } from "components/pages"; 6 | 7 | import ProfilePage from "./ProfilePage"; 8 | 9 | const ProfilePageWrapper = styled.section``; 10 | 11 | const Main = () => { 12 | const match = useRouteMatch(); 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | Main.propTypes = {}; 25 | 26 | export default Main; 27 | -------------------------------------------------------------------------------- /src/components/pages/ProfilePage/ProfilePage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | import { shallowEqual, useDispatch, useSelector } from "react-redux"; 4 | import get from "lodash.get"; 5 | 6 | import { NotFound } from "components/pages"; 7 | 8 | import { getProfile } from "store/reducers/profile"; 9 | 10 | import GroupProfile from "./GroupProfilePage"; 11 | import UserProfile from "./UserProfilePage"; 12 | 13 | const ProfilePage = () => { 14 | const { name } = useParams(); 15 | const dispatch = useDispatch(); 16 | useEffect(() => { 17 | dispatch(getProfile(name)).catch((error) => undefined); 18 | }, [name]); 19 | const profile = useSelector((state) => get(state, ["profile", "profiles", name])); 20 | if (!profile) return null; 21 | if (profile.error) return ; 22 | if (profile.username === name) return ; 23 | if (profile.name === name) return ; 24 | return null; 25 | }; 26 | 27 | ProfilePage.propTypes = {}; 28 | 29 | ProfilePage.defaultProps = {}; 30 | 31 | export default ProfilePage; 32 | -------------------------------------------------------------------------------- /src/components/pages/ProfilePage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Main"; 2 | -------------------------------------------------------------------------------- /src/components/pages/Reload.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | const Reload: React.FC = () => { 4 | useEffect(() => { 5 | location.reload(); 6 | }, []); 7 | 8 | return <>; 9 | }; 10 | 11 | export default Reload; 12 | -------------------------------------------------------------------------------- /src/components/pages/SearchPage/SearchPage.styled.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | import { media } from "lib/utils/style"; 4 | 5 | export const Page = styled.div` 6 | min-width: 1072px; 7 | padding: 48px 0; 8 | @media (max-width: 640px) { 9 | min-width: 100%; 10 | padding: 28px 0; 11 | } 12 | 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | `; 17 | 18 | Page.Body = styled.div` 19 | width: 1032px; 20 | h1 { 21 | display: inline-block; 22 | font-size: 22px; 23 | font-weight: 800; 24 | color: #363636; 25 | margin: 0; 26 | } 27 | .emptySpace { 28 | margin-top: 8px; 29 | } 30 | 31 | @media (max-width: 640px) { 32 | width: 100%; 33 | margin-top: 12px; 34 | padding: 0 16px; 35 | h1 { 36 | font-size: 18px; 37 | } 38 | .emptySpace { 39 | margin-top: 0; 40 | } 41 | } 42 | `; 43 | 44 | export const EmptyResultW = styled.section` 45 | text-align: center; 46 | font-size: 16px; 47 | margin-top: ${(props) => (props.isZaboEmpty ? "90px" : "225px")}; 48 | img { 49 | width: 29.15px; 50 | height: 29.15px; 51 | } 52 | div.empty-text { 53 | color: #202020; 54 | margin-bottom: 40px; 55 | .empty-query { 56 | display: inline-block; 57 | font-weight: 800; 58 | } 59 | } 60 | .search-icon { 61 | margin-bottom: 22px; 62 | } 63 | .empty-text { 64 | margin-bottom: 44px; 65 | } 66 | p { 67 | color: #bcbcbc; 68 | line-height: 28px; 69 | margin: 0; 70 | } 71 | 72 | @media (max-width: 640px) { 73 | margin-top: 97px; 74 | } 75 | ${media.tablet(css` 76 | img { 77 | margin-bottom: 34px; 78 | } 79 | `)}; 80 | `; 81 | 82 | // TODO: Refactor dups 83 | export const ZaboResultW = styled.section` 84 | width: 100%; 85 | margin-top: ${(props) => (props.isGroupEmpty ? "0" : "68px")}; 86 | ${media.tablet(css` 87 | width: 1032px; 88 | `)}; 89 | `; 90 | 91 | export const GroupResultW = styled.section` 92 | @media (max-width: 640px) { 93 | margin: 0 -16px; 94 | } 95 | `; 96 | -------------------------------------------------------------------------------- /src/components/pages/SearchPage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SearchPage"; 2 | -------------------------------------------------------------------------------- /src/components/pages/SettingsPage/Main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect, Route, Switch, useRouteMatch } from "react-router-dom"; 3 | import styled from "styled-components"; 4 | 5 | import { NotFound } from "components/pages"; 6 | 7 | import pToP from "../../../hoc/paramsToProps"; 8 | import GroupApply from "./GroupApply"; 9 | import GroupMembersSetting from "./GroupMembersSetting"; 10 | import GroupProfileSetting from "./GroupProfileSetting"; 11 | import ProfileSetting from "./ProfileSetting"; 12 | 13 | const SettingsWrapper = styled.section``; 14 | 15 | const Main = () => { 16 | const { path } = useRouteMatch(); 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default Main; 32 | -------------------------------------------------------------------------------- /src/components/pages/SettingsPage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Main"; 2 | -------------------------------------------------------------------------------- /src/components/pages/SettingsPage/withGroupProfile.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import get from "lodash.get"; 4 | 5 | import { NotFound } from "components/pages"; 6 | 7 | import { getProfile } from "store/reducers/profile"; 8 | 9 | const withGroupProfile = ( 10 | // TODO: add profile types 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | WrappedComponent: React.FC

, 13 | isPrivate = false, 14 | ) => { 15 | const ComponentWithGroupProfile: React.FC

= (props) => { 16 | const dispatch = useDispatch(); 17 | 18 | useEffect(() => { 19 | dispatch(getProfile(props.groupName)); 20 | }, [props.groupName, dispatch]); 21 | 22 | const profile = useSelector((state) => get(state, ["profile", "profiles", props.groupName])); 23 | if (!profile) return null; 24 | if (profile.error) return ; 25 | if (isPrivate && !profile.myRole) return ; 26 | return ; 27 | }; 28 | 29 | return ComponentWithGroupProfile; 30 | }; 31 | 32 | export default withGroupProfile; 33 | -------------------------------------------------------------------------------- /src/components/pages/Zabo/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/components/pages/Zabo/index.js -------------------------------------------------------------------------------- /src/components/pages/ZaboPage/Main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, Switch, useRouteMatch } from "react-router-dom"; 3 | 4 | import { NotFound, ZaboDetailPage, ZaboEditPage } from "components/pages"; 5 | import Header from "components/templates/Header"; 6 | 7 | import paramsToProps from "../../../hoc/paramsToProps"; 8 | import { ZaboPageWrapper } from "./ZaboPage.styled"; 9 | 10 | const Main = () => { 11 | const { path } = useRouteMatch(); 12 | return ( 13 | 14 |

15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default Main; 25 | -------------------------------------------------------------------------------- /src/components/pages/ZaboPage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Main"; 2 | -------------------------------------------------------------------------------- /src/components/pages/ZaboUploadPage/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ZaboUploadPage"; 2 | -------------------------------------------------------------------------------- /src/components/pages/index.ts: -------------------------------------------------------------------------------- 1 | import loadable from "@loadable/component"; 2 | /* 3 | Migrated from react-loadable to loadable-component. 4 | Check out these to find out more 5 | https://loadable-components.com/docs/loadable-vs-react-lazy/#note-about-react-loadable 6 | https://velog.io/@velopert/nomore-react-loadable 7 | */ 8 | 9 | export const HomePage = loadable(/* webpackPrefetch: true */ () => import("./HomePage")); 10 | export const LandingPage = loadable(/* webpackPrefetch: true */ () => import("./LandingPage")); 11 | export const ZaboPage = loadable(/* webpackPrefetch: true */ () => import("./ZaboPage")); 12 | export const ZaboDetailPage = loadable( 13 | /* webpackPrefetch: true */ () => import("./ZaboPage/ZaboDetailPage"), 14 | ); 15 | export const ZaboEditPage = loadable( 16 | /* webpackPrefetch: true */ () => import("./ZaboPage/ZaboEditPage"), 17 | ); 18 | export const ZaboUploadPage = loadable( 19 | /* webpackPrefetch: true */ () => import("./ZaboUploadPage"), 20 | ); 21 | export const SettingsPage = loadable(/* webpackPrefetch: true */ () => import("./SettingsPage")); 22 | export const AuthPage = loadable(/* webpackPrefetch: true */ () => import("./AuthPage")); 23 | export const LoginPage = loadable(/* webpackPrefetch: true */ () => import("./LoginPage")); 24 | export const ProfilePage = loadable(/* webpackPrefetch: true */ () => import("./ProfilePage")); 25 | export const NotFound = loadable(/* webpackPrefetch: true */ () => import("./NotFound")); 26 | // export const AdminPage = loadable(/* webpackPrefetch: true */ () => import("./AdminPage")); 27 | export const SearchPage = loadable(/* webpackPrefetch: true */ () => import("./SearchPage")); 28 | export const ApiPage = loadable(/* webpackPrefetch: true */ () => import("./ApiPage")); 29 | export const ReloadPage = loadable(/* webpackPrefetch: true */ () => import("./Reload")); 30 | -------------------------------------------------------------------------------- /src/components/templates/FloatingNavigator/FloatingNavigator.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | 3 | import add from "static/images/add.svg"; 4 | import calendar from "static/images/calendar.svg"; 5 | import search from "static/images/search-icon-navy.png"; 6 | import user from "static/images/user.svg"; 7 | 8 | import FloatingNavigatorWrapper, { NavItem } from "./FloatingNavigator.styled"; 9 | 10 | class FloatingNavigator extends PureComponent { 11 | state = { scrollY: window.scrollY, show: true }; 12 | 13 | componentDidMount() { 14 | window.addEventListener("optimizedScroll", this.handleScroll); 15 | } 16 | 17 | componentWillUnmount() { 18 | window.removeEventListener("optimizedScroll", this.handleScroll); 19 | } 20 | 21 | handleScroll = () => { 22 | this.setState((prevState) => ({ 23 | scrollY: window.scrollY, 24 | show: prevState.scrollY > window.scrollY, 25 | })); 26 | }; 27 | 28 | render() { 29 | const { show } = this.state; 30 | 31 | return ( 32 | 33 | 34 | 홈 35 | 36 | 37 | 38 | 검색 39 | 40 | 41 | 42 | 업로드 43 | 44 | 45 | 46 | 프로필 47 | 48 | 49 | ); 50 | } 51 | } 52 | 53 | FloatingNavigator.propTypes = {}; 54 | 55 | FloatingNavigator.defaultProps = {}; 56 | 57 | export default FloatingNavigator; 58 | -------------------------------------------------------------------------------- /src/components/templates/FloatingNavigator/FloatingNavigator.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import FloatingNavigator from "./index"; 5 | 6 | storiesOf("templates/FloatingNavigator", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/templates/FloatingNavigator/FloatingNavigator.styled.js: -------------------------------------------------------------------------------- 1 | import { NavLink } from "react-router-dom"; 2 | import styled, { css } from "styled-components"; 3 | 4 | export const NavItem = styled(NavLink)` 5 | flex: 1 0 20%; 6 | margin: 0 2.5%; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | flex-direction: column; 11 | transition: all 0.4s ease; 12 | img { 13 | transition: all 0.4s ease; 14 | width: 20px; 15 | height: 20px; 16 | margin-bottom: 6px; 17 | } 18 | &:hover { 19 | cursor: pointer; 20 | } 21 | &.active { 22 | border-bottom: 1px solid black; 23 | } 24 | `; 25 | 26 | const FloatingNavigatorWrapper = styled.div` 27 | position: fixed; 28 | transition: max-height 0.4s; 29 | bottom: 0; 30 | left: 0; 31 | width: 100%; 32 | height: 60px; 33 | max-height: ${(props) => (props.show ? "60px" : 0)}; 34 | overflow: hidden; 35 | border-top: 1px solid black; 36 | background-color: white; 37 | z-index: 1000; 38 | display: flex; 39 | justify-content: space-between; 40 | 41 | ${NavItem} { 42 | ${(props) => 43 | props.show 44 | ? css`` 45 | : css` 46 | img { 47 | width: 0; 48 | height: 0; 49 | } 50 | font-size: 0; 51 | `}; 52 | @media (min-width: 560px) { 53 | display: none; 54 | } 55 | } 56 | `; 57 | 58 | export default FloatingNavigatorWrapper; 59 | -------------------------------------------------------------------------------- /src/components/templates/FloatingNavigator/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./FloatingNavigator"; 2 | -------------------------------------------------------------------------------- /src/components/templates/Footer/Footer.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled, { type css } from "styled-components"; 2 | 3 | const FooterWrapper = styled.footer<{ 4 | ownStyle?: ReturnType; 5 | }>` 6 | position: fixed; 7 | bottom: 0; 8 | left: 0; 9 | width: 100%; 10 | height: 74px; 11 | background: white; 12 | z-index: 1000; 13 | 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: center; 17 | box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); 18 | @media (max-width: 640px) { 19 | height: 60px; 20 | } 21 | ${(props) => props.ownStyle || ""}; 22 | `; 23 | 24 | export default FooterWrapper; 25 | -------------------------------------------------------------------------------- /src/components/templates/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React, { type PropsWithChildren, useEffect, useState } from "react"; 2 | import { css } from "styled-components"; 3 | 4 | import Container from "components/atoms/Container"; 5 | 6 | import FooterWrapper from "./Footer.styled"; 7 | 8 | const containerStyle = (props: { horizontalScroll?: boolean }) => css` 9 | position: absolute; 10 | justify-content: space-between; 11 | align-items: center; 12 | 13 | ${props.horizontalScroll && 14 | css` 15 | @media (min-width: 640px) { 16 | min-width: 1072px; 17 | } 18 | `} 19 | `; 20 | 21 | interface Props extends PropsWithChildren { 22 | ownStyle?: ReturnType; 23 | scrollFooter?: boolean; 24 | } 25 | 26 | const Footer: React.FC = ({ ownStyle, scrollFooter, children }) => { 27 | const [left, setLeft] = useState(0); 28 | useEffect(() => { 29 | const listener = () => setLeft(-window.pageXOffset); 30 | window.addEventListener("optimizedScroll", listener); 31 | return () => window.removeEventListener("optimizedScroll", listener); 32 | }, []); 33 | const style = { left }; 34 | 35 | return ( 36 | 37 | 38 | {children} 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default Footer; 45 | -------------------------------------------------------------------------------- /src/components/templates/Footer/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Footer"; 2 | -------------------------------------------------------------------------------- /src/components/templates/Header/Header.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from "react"; 3 | import { storiesOf } from "@storybook/react"; 4 | 5 | import Header from "./index"; 6 | 7 | storiesOf("templates/Header", module).add("Default", () =>
, { 8 | notes: "", 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/templates/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from "./Header"; 2 | -------------------------------------------------------------------------------- /src/components/templates/Loading/Loading.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import LoadingWrapper from "./Loading.styled"; 5 | 6 | class Loading extends PureComponent { 7 | render() { 8 | return ( 9 | 10 |
    11 |
  • za
  • 12 |
  • 13 | b 14 | o 15 |
  • 16 |
17 |
18 | ); 19 | } 20 | } 21 | 22 | Loading.propTypes = { 23 | height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 24 | }; 25 | 26 | Loading.defaultProps = { 27 | height: "100vh", 28 | }; 29 | 30 | export default Loading; 31 | -------------------------------------------------------------------------------- /src/components/templates/Loading/Loading.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import Loading from "./index"; 5 | 6 | storiesOf("templates/Loading", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/templates/Loading/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./Loading"; 2 | -------------------------------------------------------------------------------- /src/components/templates/Modal/Modal.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import Modal from "./index"; 5 | 6 | storiesOf("templates/Modal", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/templates/Modal/index.js: -------------------------------------------------------------------------------- 1 | import Modal from "./Modal"; 2 | 3 | export * from "./modalMethods"; 4 | export default Modal; 5 | -------------------------------------------------------------------------------- /src/components/templates/Modal/modalMethods.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import Modal from "./Modal"; 5 | 6 | const { body } = document; 7 | 8 | export const showInstanceModal = ({ content, onCancel, ...props }) => { 9 | const container = document.createElement("div"); 10 | body.appendChild(container); 11 | 12 | const render = (show) => { 13 | ReactDOM.render( 14 | // eslint-disable-next-line no-use-before-define 15 | 16 | {content} 17 | , 18 | container, 19 | ); 20 | }; 21 | 22 | const show = () => render(true); 23 | 24 | const hide = () => render(false); 25 | 26 | const unmount = () => { 27 | ReactDOM.unmountComponentAtNode(container); 28 | body.removeChild(container); 29 | }; 30 | 31 | const _onCancel = () => { 32 | if (typeof onCancel === "function") onCancel(); 33 | hide(); 34 | }; 35 | 36 | show(); 37 | 38 | return hide; 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/templates/PWAPrompt/PWAPrompt.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | 3 | import appIcon from "static/logo/sparcs.svg"; 4 | 5 | import PWAPromptWrapper from "./PWAPrompt.styled"; 6 | 7 | class PWAPrompt extends PureComponent { 8 | state = { active: false }; 9 | 10 | handleScroll = () => { 11 | if (window.scrollY < 10) { 12 | document.body.classList.add("pwa-prompt-active"); 13 | this.setState({ active: true }); 14 | } else { 15 | document.body.classList.remove("pwa-prompt-active"); 16 | setTimeout(() => this.setState({ active: false })); 17 | } 18 | }; 19 | 20 | addListener = () => { 21 | this.setState({ active: true }); 22 | window.addEventListener("optimizedScroll", this.handleScroll); 23 | }; 24 | 25 | deleteListener = () => { 26 | this.setState({ active: false }); 27 | window.removeEventListener("optimizedScroll", this.handleScroll); 28 | }; 29 | 30 | componentDidMount() { 31 | if (window.pwaPromptActive) { 32 | this.addListener(); 33 | } else { 34 | window.onPWAPromptActive = this.addListener; 35 | } 36 | } 37 | 38 | handleOpenClick = () => { 39 | this.deleteListener(); 40 | document.body.classList.remove("pwa-prompt-active"); 41 | 42 | window.deferredPrompt.prompt(); 43 | // Wait for the user to respond to the prompt 44 | window.deferredPrompt.userChoice.then((choiceResult) => { 45 | if (choiceResult.outcome === "accepted") { 46 | console.log("User accepted the A2HS prompt"); // TODO: Statistics 47 | } else { 48 | console.log("User dismissed the A2HS prompt"); 49 | } 50 | window.deferredPrompt = null; 51 | }); 52 | }; 53 | 54 | render() { 55 | const { active } = this.state; 56 | if (!active) return null; 57 | return ( 58 | 59 |
60 | 61 |
62 |
ZABO (자보) : 모든 포스터를 한 곳에서 모아보세요
63 |
ZABO 어플리케이션 설치하기 (데스크탑, 안드로이드 ,iOS)
64 |
65 | 66 |
67 |
68 | ); 69 | } 70 | } 71 | 72 | PWAPrompt.propTypes = {}; 73 | 74 | PWAPrompt.defaultProps = {}; 75 | 76 | export default PWAPrompt; 77 | -------------------------------------------------------------------------------- /src/components/templates/PWAPrompt/PWAPrompt.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import PWAPrompt from "./index"; 5 | 6 | storiesOf("templates/PWAPrompt", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/templates/PWAPrompt/PWAPrompt.styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const PWAPromptWrapper = styled.div` 4 | position: fixed; 5 | top: 0; 6 | width: 100%; 7 | height: 40px; 8 | background-color: #62666a; 9 | z-index: 1000; 10 | transition: max-height 0.4s; 11 | .container { 12 | height: 100%; 13 | flex-direction: row; 14 | align-items: center; 15 | } 16 | 17 | img { 18 | height: 25px; 19 | margin-right: 20px; 20 | } 21 | 22 | .texts { 23 | flex: 1; 24 | overflow: hidden; 25 | 26 | * { 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | white-space: nowrap; 30 | color: white; 31 | } 32 | .title { 33 | font-size: 12px; 34 | } 35 | .desc { 36 | font-size: 10px; 37 | } 38 | } 39 | 40 | button { 41 | background-color: transparent; 42 | border: none; 43 | margin-left: 20px; 44 | height: 25px; 45 | padding: 0; 46 | color: cornflowerblue; 47 | font-size: 14px; 48 | } 49 | `; 50 | 51 | export default PWAPromptWrapper; 52 | -------------------------------------------------------------------------------- /src/components/templates/PWAPrompt/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./PWAPrompt"; 2 | -------------------------------------------------------------------------------- /src/components/templates/SavedPosters/SavedPosters.container.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import { connect } from "react-redux"; 3 | 4 | import SavedPosters from "./SavedPosters"; 5 | 6 | class SavedPostersContainer extends PureComponent { 7 | render() { 8 | return ; 9 | } 10 | } 11 | 12 | const mapStateToProps = (state) => ({}); 13 | 14 | const mapDispatchToProps = (dispatch) => ({}); 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(SavedPostersContainer); 17 | -------------------------------------------------------------------------------- /src/components/templates/SavedPosters/SavedPosters.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import SavedPostersWrapper from "./SavedPosters.styled"; 5 | 6 | class SavedPosters extends PureComponent { 7 | render() { 8 | return ( 9 | 10 | {this.props.children} 11 | poster 12 | 13 | ); 14 | } 15 | } 16 | 17 | SavedPosters.propTypes = {}; 18 | 19 | SavedPosters.defaultProps = {}; 20 | 21 | export default SavedPosters; 22 | -------------------------------------------------------------------------------- /src/components/templates/SavedPosters/SavedPosters.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import SavedPosters from "./SavedPosters"; 5 | 6 | storiesOf("templates/SavedPosters", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/templates/SavedPosters/SavedPosters.styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const SavedPostersWrapper = styled.div``; 4 | 5 | export default SavedPostersWrapper; 6 | -------------------------------------------------------------------------------- /src/components/templates/SavedPosters/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SavedPosters.container"; 2 | -------------------------------------------------------------------------------- /src/components/templates/SearchBar/SearchBar.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import SearchBar from "./index"; 5 | 6 | storiesOf("templates/SearchBar", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/templates/SearchBar/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./SearchBar"; 2 | -------------------------------------------------------------------------------- /src/components/templates/ZaboList/ZaboList.container.js: -------------------------------------------------------------------------------- 1 | import { connect } from "react-redux"; 2 | import { List } from "immutable"; 3 | import get from "lodash.get"; 4 | 5 | import { getGroupZaboList, getPins, getSearchZaboList, getZaboList } from "store/reducers/zabo"; 6 | 7 | import ZaboList from "./ZaboList"; 8 | 9 | const reduxKey = { 10 | main: () => ["zabo", "lists", "main"], 11 | related: (id) => ["zabo", "lists", id], 12 | pins: () => ["zabo", "lists", "pins"], 13 | group: (name) => ["zabo", "lists", name], 14 | search: () => ["zabo", "lists", "search"], 15 | }; 16 | const emptyList = List([]); 17 | const mapStateToProps = (state, ownProps) => { 18 | const { type, query } = ownProps; 19 | const zaboIdList = get(state, reduxKey[type](query)) || emptyList; 20 | return { 21 | zaboIdList, 22 | width: get(state, ["app", "windowSize", "width"]), 23 | }; 24 | }; 25 | 26 | const mapDispatchToProps = { 27 | getPins, 28 | getZaboList, 29 | getGroupZaboList, 30 | getSearchZaboList, 31 | }; 32 | 33 | export default connect(mapStateToProps, mapDispatchToProps)(ZaboList); 34 | -------------------------------------------------------------------------------- /src/components/templates/ZaboList/ZaboList.stories.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react"; 3 | 4 | import ZaboList from "./index"; 5 | 6 | storiesOf("templates/ZaboList", module).add("Default", () => , { 7 | notes: "", 8 | }); 9 | -------------------------------------------------------------------------------- /src/components/templates/ZaboList/ZaboList.styled.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const ZaboListWrapper = styled.div` 4 | width: 100%; 5 | .masonry.masonry-main { 6 | margin: 20px auto 0 auto; 7 | width: 100%; 8 | } 9 | `; 10 | 11 | export default ZaboListWrapper; 12 | -------------------------------------------------------------------------------- /src/components/templates/ZaboList/index.js: -------------------------------------------------------------------------------- 1 | export { default } from "./ZaboList.container"; 2 | -------------------------------------------------------------------------------- /src/components/templates/ZaboList/withStackMaster.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | import storage from "lib/storage"; 5 | 6 | export default (Component) => { 7 | class StackMaster extends React.Component { 8 | state = { stack: [] }; 9 | 10 | componentDidMount() { 11 | const { zaboId } = this.props; 12 | if (!zaboId) { 13 | storage.setItem("zaboStack", []); 14 | } else { 15 | let zaboStack = storage.getItem("zaboStack") || []; 16 | zaboStack = Array.isArray(zaboStack) ? zaboStack : []; 17 | const prev = zaboStack.pop(); 18 | if (!!prev && prev !== zaboId) zaboStack.push(prev); 19 | zaboStack.push(zaboId); 20 | storage.setItem("zaboStack", zaboStack); 21 | this.setState({ stack: zaboStack }); 22 | } 23 | } 24 | 25 | componentDidUpdate(prevProps, prevState, snapshot) { 26 | const { zaboId } = this.props; 27 | if (prevProps.zaboId === zaboId) return; 28 | 29 | if (!zaboId) { 30 | this.setState({ stack: [] }); 31 | storage.setItem("zaboStack"); 32 | return; 33 | } 34 | 35 | this.setState((prevState) => { 36 | const { stack } = prevState; 37 | stack.push(zaboId); 38 | storage.setItem("zaboStack", stack); 39 | return { stack }; 40 | }); 41 | } 42 | 43 | render() { 44 | const { stack } = this.state; 45 | const { zaboId, ...props } = this.props; 46 | 47 | return ; 48 | } 49 | } 50 | 51 | StackMaster.propTypes = { 52 | ...Component.propTypes, 53 | zaboId: PropTypes.string, 54 | }; 55 | 56 | return StackMaster; 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/templates/ZaboUpload/LeavingAlert.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import get from "lodash.get"; 4 | 5 | import { reset, setModal } from "store/reducers/upload"; 6 | 7 | const LeavingAlert = () => { 8 | const dispatch = useDispatch(); 9 | const showModal = useSelector((state) => get(state, ["upload", "showModal"])); 10 | useEffect(() => { 11 | if (showModal) { 12 | alert("데이터를 저장하시겠습니까?"); 13 | dispatch(reset()); 14 | dispatch(setModal(false)); 15 | } 16 | }, [showModal]); 17 | return null; 18 | }; 19 | 20 | export default LeavingAlert; 21 | -------------------------------------------------------------------------------- /src/components/templates/ZaboUpload/Loading.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const Style = styled.div` 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | align-items: center; 9 | background-color: rgba(29, 31, 32, 0.5); 10 | display: flex; 11 | justify-content: center; 12 | width: 100%; 13 | height: 100%; 14 | 15 | .loader { 16 | position: fixed; 17 | left: 50%; 18 | top: 50%; 19 | animation: rotate 1s infinite; 20 | height: 50px; 21 | width: 50px; 22 | } 23 | 24 | .loader:before, 25 | .loader:after { 26 | border-radius: 50%; 27 | content: ""; 28 | display: block; 29 | height: 20px; 30 | width: 20px; 31 | } 32 | .loader:before { 33 | animation: ball1 1s infinite; 34 | background-color: #cb2025; 35 | box-shadow: 30px 0 0 #f8b334; 36 | margin-bottom: 10px; 37 | } 38 | .loader:after { 39 | animation: ball2 1s infinite; 40 | background-color: #00a096; 41 | box-shadow: 30px 0 0 #97bf0d; 42 | } 43 | 44 | @keyframes rotate { 45 | 0% { 46 | transform: rotate(0deg) scale(0.8); 47 | } 48 | 50% { 49 | transform: rotate(360deg) scale(1.2); 50 | } 51 | 100% { 52 | transform: rotate(720deg) scale(0.8); 53 | } 54 | } 55 | 56 | @keyframes ball1 { 57 | 0% { 58 | box-shadow: 30px 0 0 #f8b334; 59 | } 60 | 50% { 61 | box-shadow: 0 0 0 #f8b334; 62 | margin-bottom: 0; 63 | transform: translate(15px, 15px); 64 | } 65 | 100% { 66 | box-shadow: 30px 0 0 #f8b334; 67 | margin-bottom: 10px; 68 | } 69 | } 70 | 71 | @keyframes ball2 { 72 | 0% { 73 | box-shadow: 30px 0 0 #97bf0d; 74 | } 75 | 50% { 76 | box-shadow: 0 0 0 #97bf0d; 77 | margin-top: -20px; 78 | transform: translate(15px, 15px); 79 | } 80 | 100% { 81 | box-shadow: 30px 0 0 #97bf0d; 82 | margin-top: 0; 83 | } 84 | } 85 | `; 86 | 87 | const Loading = () => ( 88 | 91 | ); 92 | 93 | export default Loading; 94 | -------------------------------------------------------------------------------- /src/components/templates/ZaboUpload/ZaboUpload.styled.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/components/templates/ZaboUpload/ZaboUpload.styled.js -------------------------------------------------------------------------------- /src/components/templates/ZaboUpload/index.js: -------------------------------------------------------------------------------- 1 | import InfoForm from "./InfoForm"; 2 | import SelectGroup from "./SelectGroup"; 3 | import UploadImage from "./UploadImages"; 4 | import UploadProcess from "./UploadProcess"; 5 | 6 | const ZaboUpload = { 7 | SelectGroup, 8 | UploadImage, 9 | InfoForm, 10 | UploadProcess, 11 | }; 12 | 13 | export default ZaboUpload; 14 | -------------------------------------------------------------------------------- /src/hoc/paramsToProps.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import get from "lodash.get"; 3 | 4 | // eslint-disable-next-line react/display-name 5 | export default (WrappedComponent) => (props) => { 6 | const params = get(props, ["match", "params"]) || {}; 7 | const downProps = { 8 | ...props, 9 | ...params, 10 | }; 11 | return ; 12 | }; 13 | -------------------------------------------------------------------------------- /src/hoc/withLog.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const withLog = (WrappedComponent) => { 4 | const displayName = WrappedComponent.displayName || WrappedComponent.name; 5 | return class extends React.PureComponent { 6 | constructor(props) { 7 | super(props); 8 | console.log(`%c[${displayName}] : constructor`, "color: #4CAF50;"); 9 | } 10 | 11 | componentDidMount() { 12 | console.log(`%c[${displayName}] : componentDidMount`, "color: #2196F3;", this.props); 13 | } 14 | 15 | componentDidUpdate(prevProps, state) { 16 | console.log( 17 | `%c[${displayName}] : componentDidUpdate`, 18 | "color: #2196F3;", 19 | prevProps, 20 | this.props, 21 | ); 22 | } 23 | 24 | componentWillUnmount() { 25 | console.log(`%c[${displayName}] : componentWillUnmount`, "color: #F44336;"); 26 | } 27 | 28 | render() { 29 | return ; 30 | } 31 | }; 32 | }; 33 | 34 | export default withLog; 35 | -------------------------------------------------------------------------------- /src/hoc/withZabo.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useDispatch, useSelector } from "react-redux"; 4 | import get from "lodash.get"; 5 | 6 | import { getZabo } from "store/reducers/zabo"; 7 | 8 | const NotFound = () =>

Zabo Not Found

; 9 | 10 | const withZabo = (WrappedComponent, isPrivate = false, fetch = true) => { 11 | const Sub = (props) => { 12 | const dispatch = useDispatch(); 13 | const { zaboId } = props; 14 | 15 | useEffect(() => { 16 | if (fetch) dispatch(getZabo(zaboId)); 17 | }, [zaboId]); 18 | 19 | const zabo = useSelector((state) => get(state, ["zabo", "zabos", zaboId])); 20 | if (!zabo) return null; 21 | if (zabo.error) return ; 22 | if (isPrivate && !zabo.isMyZabo) return ; 23 | return ; 24 | }; 25 | Sub.propTypes = { 26 | zaboId: PropTypes.string.isRequired, 27 | }; 28 | return Sub; 29 | }; 30 | 31 | export default withZabo; 32 | -------------------------------------------------------------------------------- /src/hooks/useInputs.js: -------------------------------------------------------------------------------- 1 | import { useReducer } from "react"; 2 | 3 | function reducer(state, action) { 4 | return { 5 | ...state, 6 | [action.name]: action.value, 7 | }; 8 | } 9 | 10 | export default function useInputs(initialForm) { 11 | const [state, dispatch] = useReducer(reducer, initialForm); 12 | const onChange = (e) => { 13 | dispatch(e.target); 14 | }; 15 | return [state, onChange]; 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/useSetState.js: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from "react"; 2 | 3 | export default function useSetState(initialForm) { 4 | const [state, setStateInternal] = useState(initialForm); 5 | const clone = { ...state }; 6 | const setState = (param) => { 7 | if (typeof param === "function") { 8 | setStateInternal((prevState) => ({ ...prevState, ...param(prevState) })); 9 | return; 10 | } 11 | setStateInternal(Object.assign(clone, param)); 12 | }; 13 | const onChange = (e) => { 14 | setState({ 15 | [e.target.name]: e.target.value, 16 | }); 17 | }; 18 | return [state, setState, onChange]; 19 | } 20 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @import "lib/fonts/stylesheet.css"; 2 | 3 | body { 4 | margin: 50px 0 0 0; 5 | padding: 0; 6 | font-family: "NanumSquare", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 7 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 8 | transition: margin-top 0.4s; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | /*IE9*/ 12 | a::selection { 13 | background-color: transparent; 14 | } 15 | a::-moz-selection { 16 | background-color: transparent; 17 | } 18 | a { 19 | -webkit-user-select: none; 20 | -moz-user-select: -moz-none; 21 | /*IE10*/ 22 | -ms-user-select: none; 23 | user-select: none; 24 | 25 | /*You just need this if you are only concerned with android and not desktop browsers.*/ 26 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 27 | } 28 | input[type="text"], 29 | textarea, 30 | [contenteditable] { 31 | -webkit-user-select: text; 32 | -moz-user-select: text; 33 | -ms-user-select: text; 34 | user-select: text; 35 | } 36 | .pwa-prompt { 37 | z-index: -1; 38 | max-height: 0; 39 | } 40 | &.pwa-prompt-active { 41 | margin-top: 95px; 42 | header { 43 | top: 40px; 44 | } 45 | .pwa-prompt { 46 | display: unset; 47 | max-height: 40px; 48 | } 49 | } 50 | } 51 | 52 | code { 53 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 54 | } 55 | 56 | * { 57 | box-sizing: border-box; 58 | } 59 | 60 | ::selection { 61 | background: rgba(184, 203, 236, 0.41); 62 | } 63 | 64 | a, 65 | a:hover, 66 | a:visited, 67 | a:focus { 68 | border: none; 69 | outline: none; 70 | text-decoration: none; 71 | color: inherit; 72 | cursor: pointer; 73 | } 74 | 75 | button, 76 | button:focus, 77 | button:active { 78 | outline: none; 79 | cursor: pointer; 80 | } 81 | 82 | #root { 83 | } 84 | 85 | .container { 86 | width: 100%; 87 | margin: 0 auto; 88 | padding: 0 16px; 89 | display: flex; 90 | flex-direction: column; 91 | } 92 | 93 | #modal-root { 94 | position: relative; 95 | z-index: 9999; 96 | } 97 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import "animate.css"; 2 | import "./index.scss"; 3 | import "slick-carousel/slick/slick.css"; 4 | import "slick-carousel/slick/slick-theme.css"; 5 | 6 | import React from "react"; 7 | import { BrowserRouter as Router } from "react-router-dom"; 8 | import { Provider } from "react-redux"; 9 | import ReactDOM from "react-dom"; 10 | import { ThemeProvider } from "styled-components"; 11 | 12 | import store from "store"; 13 | import { colors } from "lib/theme"; 14 | 15 | import App from "./App"; 16 | import boot from "./boot"; 17 | import * as serviceWorker from "./serviceWorker"; 18 | 19 | boot(); 20 | 21 | ReactDOM.render( 22 | 23 | 24 | 25 | 26 | 27 | 28 | , 29 | document.getElementById("root"), 30 | ); 31 | 32 | // If you want your app to work offline and load faster, you can change 33 | // unregister() to register() below. Note this comes with some pitfalls. 34 | // Learn more about service workers: https://bit.ly/CRA-PWA 35 | serviceWorker.register(); 36 | -------------------------------------------------------------------------------- /src/lib/api/auth.ts: -------------------------------------------------------------------------------- 1 | import axios from "lib/axios"; 2 | 3 | import type { User } from "lib/interface"; 4 | 5 | export const loginCallback = (code: string, state: string, update?: boolean) => 6 | axios.post<{ 7 | token: string; 8 | user: User; 9 | }>("/auth/login/callback", { 10 | code, 11 | state, 12 | update, 13 | }); 14 | 15 | export const checkAuth = () => axios.get("/auth"); 16 | -------------------------------------------------------------------------------- /src/lib/api/group.js: -------------------------------------------------------------------------------- 1 | import axios from "../axios"; 2 | 3 | /* Auth */ 4 | export const updateGroupInfo = ({ curName, name, description, subtitle, category }) => 5 | axios.post(`/group/${curName}`, { 6 | name, 7 | description, 8 | subtitle, 9 | category, 10 | }); 11 | export const updateGroupInfoWithImage = ({ curName, formData }) => 12 | axios.post(`/group/${curName}`, formData); 13 | 14 | /* Profile */ 15 | export const addGroupMember = ({ groupName, userId, role }) => 16 | axios.put(`/group/${groupName}/member`, { userId, role }); 17 | export const updateGroupMember = ({ groupName, userId, role }) => 18 | axios.post(`/group/${groupName}/member`, { userId, role }); 19 | export const removeGroupUser = ({ groupName, userId }) => 20 | axios.delete(`/group/${groupName}/member`, { data: { userId } }); 21 | export const applyNewGroup = (data) => axios.post("/group/apply", data); 22 | 23 | /* Main */ 24 | export const getRecommendedGroups = () => axios.get("/group/recommends"); 25 | -------------------------------------------------------------------------------- /src/lib/api/profile.js: -------------------------------------------------------------------------------- 1 | import axios from "../axios"; 2 | 3 | export const fetchProfile = (name) => axios.get(`/profile/${name}`); 4 | export const validateName = ({ name }) => axios.get(`/profile/${name}/isValid`); 5 | export const followProfile = ({ name }) => axios.post(`/profile/${name}/follow`); 6 | -------------------------------------------------------------------------------- /src/lib/api/search.js: -------------------------------------------------------------------------------- 1 | import queryString from "query-string"; 2 | 3 | import axios from "../axios"; 4 | 5 | export const searchSimpleAPI = ({ query, category }) => { 6 | if (!query && (!category || !category.length)) return Promise.resolve({ zabos: [], groups: [] }); 7 | return axios.get(`/search/simple?${queryString.stringify({ query, category })}`); 8 | }; 9 | 10 | export const searchAPI = ({ query, category, stat }) => { 11 | if (!query && (!category || !category.length)) return Promise.resolve({ zabos: [], groups: [] }); 12 | return axios.get(`/search?${queryString.stringify({ query, category, stat })}`); 13 | }; 14 | 15 | export const searchUsers = (query) => { 16 | if (!query) return Promise.resolve([]); 17 | return axios.get(`/search/user?query=${encodeURIComponent(query)}`); 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/api/user.js: -------------------------------------------------------------------------------- 1 | import axios from "../axios"; 2 | 3 | /* Auth */ 4 | export const updateUserInfo = (data) => axios.post("/user", data); 5 | export const updateUserInfoWithImage = (formData) => axios.post("/user", formData); 6 | export const setCurrentGroup = (groupName) => axios.post(`/user/currentGroup/${groupName}`); 7 | -------------------------------------------------------------------------------- /src/lib/api/zabo.js: -------------------------------------------------------------------------------- 1 | import axios from "../axios"; 2 | 3 | export const uploadZabo = (formData, onUploadProgress = () => undefined) => 4 | axios.post("/zabo", formData, { 5 | headers: { 6 | "Content-Type": "multipart/form-data", 7 | }, 8 | onUploadProgress: (progressEvent) => { 9 | const percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total); 10 | // do whatever you like with the percentage complete 11 | // maybe dispatch an action that will update a progress bar or something 12 | onUploadProgress(percentCompleted); 13 | }, 14 | }); 15 | export const patchZabo = ({ zaboId, data }) => axios.patch(`/zabo/${zaboId}`, data); 16 | export const deleteZabo = ({ zaboId }) => axios.delete(`/zabo/${zaboId}`); 17 | 18 | export const getZabo = (id) => axios.get(`/zabo/${id}`); 19 | export const getHotZaboList = () => axios.get("/zabo/list/hot"); 20 | export const getDeadlineZaboList = () => axios.get("/zabo/list/deadline"); 21 | export const getZaboList = ({ lastSeen, relatedTo }) => 22 | axios 23 | .get("/zabo/list", { params: { lastSeen, relatedTo } }) 24 | .then((data) => data.filter((item) => item.photos[0] !== undefined)); 25 | export const getPins = ({ username, lastSeen }) => 26 | axios 27 | .get(`/user/${username}/pins`, { params: { lastSeen } }) 28 | .then((data) => data.filter((item) => item.photos[0] !== undefined)); 29 | 30 | export const toggleZaboPin = (zaboId) => axios.post(`/zabo/${zaboId}/pin`); 31 | export const toggleZaboLike = (zaboId) => axios.post(`/zabo/${zaboId}/like`); 32 | export const toggleZaboShare = (zaboId) => axios.post(`/zabo/${zaboId}/share`); 33 | 34 | export const getGroupZaboList = ({ groupName, lastSeen }) => 35 | axios.get(`/group/${groupName}/zabo/list`, { params: { lastSeen } }); 36 | export const getSearchZaboList = ({ text, lastSeen }) => { 37 | if (!text) return Promise.resolve({ zabos: [], groups: [] }); 38 | const { query, category } = text; 39 | return axios.get( 40 | `/search/zabo/list?query=${encodeURIComponent(query)}&category=${encodeURIComponent(category)}`, 41 | { params: { lastSeen } }, 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/lib/channel_io.js: -------------------------------------------------------------------------------- 1 | class ChannelService { 2 | constructor() { 3 | this.loaded = false; 4 | this.onloaded = () => {}; 5 | setTimeout(() => { 6 | this.loadScript(); 7 | this.loaded = true; 8 | this.onloaded(); 9 | }, 5000); 10 | } 11 | 12 | loadScript() { 13 | let w = window; 14 | if (w.ChannelIO) { 15 | return (window.console.error || window.console.log || function () {})( 16 | "ChannelIO script included twice.", 17 | ); 18 | } 19 | let d = window.document; 20 | let ch = function () { 21 | ch.c(arguments); 22 | }; 23 | ch.q = []; 24 | ch.c = function (args) { 25 | ch.q.push(args); 26 | }; 27 | w.ChannelIO = ch; 28 | function l() { 29 | if (w.ChannelIOInitialized) { 30 | return; 31 | } 32 | w.ChannelIOInitialized = true; 33 | let s = document.createElement("script"); 34 | s.type = "text/javascript"; 35 | s.async = true; 36 | s.src = "https://cdn.channel.io/plugin/ch-plugin-web.js"; 37 | s.charset = "UTF-8"; 38 | let x = document.getElementsByTagName("script")[0]; 39 | x.parentNode.insertBefore(s, x); 40 | } 41 | if (document.readyState === "complete") { 42 | l(); 43 | } else if (window.attachEvent) { 44 | window.attachEvent("onload", l); 45 | } else { 46 | window.addEventListener("DOMContentLoaded", l, false); 47 | window.addEventListener("load", l, false); 48 | } 49 | } 50 | 51 | boot(settings) { 52 | if (!this.loaded) { 53 | this.onloaded = () => this.boot(settings); 54 | return; 55 | } 56 | window.ChannelIO("boot", settings); 57 | } 58 | 59 | shutdown() { 60 | if (!this.loaded) { 61 | this.onloaded = () => this.shutdown(); 62 | return; 63 | } 64 | window.ChannelIO("shutdown"); 65 | } 66 | } 67 | 68 | export default new ChannelService(); 69 | -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareBold.ttf -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareBold.woff -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareBold.woff2 -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareExtraBold.ttf -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareExtraBold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareExtraBold.woff -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareExtraBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareExtraBold.woff2 -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareLight.ttf -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareLight.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareLight.woff -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareLight.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareLight.woff2 -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareRegular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareRegular.ttf -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareRegular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareRegular.woff -------------------------------------------------------------------------------- /src/lib/fonts/NanumSquareRegular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/lib/fonts/NanumSquareRegular.woff2 -------------------------------------------------------------------------------- /src/lib/fonts/stylesheet.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "NanumSquare Bold"; 3 | src: url('NanumSquareBold.eot?#iefix') format('embedded-opentype'), 4 | url('NanumSquareBold.woff') format('woff'), 5 | url('NanumSquareBold.woff2') format('woff2'), 6 | url('NanumSquareBold.svg') format('svg'), 7 | url('NanumSquareBold.ttf') format('truetype'); 8 | font-weight: 700; 9 | } 10 | 11 | @font-face { 12 | font-family: "NanumSquare ExtraBold"; 13 | src: url('NanumSquareExtraBold.eot?#iefix') format('embedded-opentype'), 14 | url('NanumSquareExtraBold.woff') format('woff'), 15 | url('NanumSquareExtraBold.woff2') format('woff2'), 16 | url('NanumSquareExtraBold.svg') format('svg'), 17 | url('NanumSquareExtraBold.ttf') format('truetype'); 18 | font-weight: 800; 19 | } 20 | 21 | @font-face { 22 | font-family: "NanumSquare Light"; 23 | src: url('NanumSquareLight.eot?#iefix') format('embedded-opentype'), 24 | url('NanumSquareLight.woff') format('woff'), 25 | url('NanumSquareLight.woff2') format('woff2'), 26 | url('NanumSquareLight.svg') format('svg'), 27 | url('NanumSquareLight.ttf') format('truetype'); 28 | font-weight: 300; 29 | } 30 | 31 | @font-face { 32 | font-family: "NanumSquare"; 33 | src: url('NanumSquareRegular.eot?#iefix') format('embedded-opentype'), 34 | url('NanumSquareRegular.woff') format('woff'), 35 | url('NanumSquareRegular.woff2') format('woff2'), 36 | url('NanumSquareRegular.svg') format('svg'), 37 | url('NanumSquareRegular.ttf') format('truetype'); 38 | font-weight: 400; 39 | } -------------------------------------------------------------------------------- /src/lib/i18n/i18next-react-li-postprocessor.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { render, splitReg } from "./i18next-react-react-postprocessor"; 4 | 5 | class Li { 6 | type = "postProcessor"; 7 | 8 | name = "li"; 9 | 10 | /* return manipulated value */ 11 | process = (value, key, options, translator) => 12 | value.split("\n").map((item, index) => ( 13 |
  • 14 | {render(item.split(splitReg), options, "1")} 15 |
  • 16 | )); 17 | } 18 | 19 | export default Li; 20 | -------------------------------------------------------------------------------- /src/lib/i18n/index.js: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import LanguageDetector from "i18next-browser-languagedetector"; 3 | import enTranslation from "locales/en/translation.json"; 4 | import koTranslation from "locales/ko/translation.json"; 5 | 6 | import Li from "./i18next-react-li-postprocessor"; 7 | import ReactPostProcessor from "./i18next-react-react-postprocessor"; 8 | 9 | i18n.on("languageChanged", (lng) => { 10 | if (!lng.split("-")) return; 11 | if (lng.split("-")[0] !== lng) i18n.changeLanguage(lng.split("-")[0]); 12 | }); 13 | 14 | i18n 15 | .use(LanguageDetector) 16 | .use(new Li()) 17 | .use(new ReactPostProcessor()) 18 | .init({ 19 | fallbackLng: "ko", 20 | debug: true, 21 | resources: { 22 | en: { 23 | translation: enTranslation, 24 | }, 25 | ko: { 26 | translation: koTranslation, 27 | }, 28 | }, 29 | whitelist: ["ko", "en"], 30 | nonExplicitWhitelist: true, 31 | updateMissing: true, 32 | ns: ["translation"], 33 | fallbackNS: "translation", 34 | // react i18next special options (optional) 35 | postProcess: ["React"], 36 | interpolation: { 37 | escapeValue: false, // not needed for react!! 38 | format: (value, format, lng) => { 39 | if (format === "uppercase") return value.toUpperCase(); 40 | return value; 41 | }, 42 | formatSeparator: "|", 43 | }, 44 | react: { 45 | wait: true, 46 | bindI18n: "languageChanged loaded", 47 | bindStore: "added removed", 48 | nsMode: "default", 49 | }, 50 | }); 51 | 52 | export default i18n; 53 | -------------------------------------------------------------------------------- /src/lib/immutable.js: -------------------------------------------------------------------------------- 1 | import Immutable from "immutable"; 2 | import Serialize from "remotedev-serialize"; 3 | 4 | const serializer = Serialize.immutable(Immutable); 5 | 6 | export default serializer; 7 | -------------------------------------------------------------------------------- /src/lib/interface/group.ts: -------------------------------------------------------------------------------- 1 | import type { Group as GroupSchema } from "./schemas"; 2 | 3 | export interface Group extends GroupSchema { 4 | zabosCount?: number; 5 | followersCount?: number; 6 | isPending?: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/interface/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./group"; 2 | export * from "./user"; 3 | export * from "./zabo"; 4 | -------------------------------------------------------------------------------- /src/lib/interface/schemas/board.ts: -------------------------------------------------------------------------------- 1 | import type { MongoSchema } from "../utils/mongo"; 2 | 3 | export interface Board extends MongoSchema { 4 | title: string; 5 | /* For further usage */ 6 | description?: string; 7 | category?: string; 8 | isPrivate?: boolean; 9 | pins?: string[]; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/interface/schemas/group.ts: -------------------------------------------------------------------------------- 1 | import type { MongoSchema } from "../utils/mongo"; 2 | import type { User } from "."; 3 | 4 | export interface GroupApply extends MongoSchema { 5 | name: string; 6 | subtitle?: string; 7 | description?: string; 8 | profilePhoto?: string; 9 | backgroundPhoto?: string; 10 | category?: string[]; 11 | members: { 12 | user: User; 13 | role?: "admin" | "editor"; 14 | }[]; 15 | purpose?: string; 16 | isBusiness?: boolean; 17 | } 18 | 19 | export interface Group extends GroupApply { 20 | isPreRegistered?: boolean; 21 | level?: number; 22 | revisionHistory?: RevisionHistory[]; 23 | recentUpload?: string; 24 | followers?: User[] | string[]; 25 | score: number; 26 | } 27 | 28 | interface RevisionHistory extends MongoSchema { 29 | prev?: string; 30 | next?: string; 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/interface/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./board"; 2 | export * from "./group"; 3 | export * from "./user"; 4 | export * from "./zabo"; 5 | -------------------------------------------------------------------------------- /src/lib/interface/schemas/user.ts: -------------------------------------------------------------------------------- 1 | import type { MongoSchema } from "../utils/mongo"; 2 | import type { Board, Group, Zabo } from "./index"; 3 | 4 | export interface User extends MongoSchema { 5 | sso_uid?: string; 6 | sso_sid: string; 7 | email?: string; 8 | username: string; 9 | description?: string; 10 | profilePhoto?: string; 11 | backgroundPhoto?: string; 12 | isAdmin?: boolean; 13 | /* From SSO */ 14 | gender?: string; 15 | birthday?: string; 16 | flags?: string[]; 17 | firstName?: string; 18 | lastName?: string; 19 | koreanName?: string; 20 | kaistId?: string; 21 | sparcsId?: string; 22 | facebookId?: string; 23 | // TODO: check if typo 24 | tweeterId?: string; 25 | studentId?: string; 26 | kaistEmail?: string; 27 | kaistPersonType?: string; 28 | kaistInfoTime?: string; 29 | kaistStatus?: string; 30 | /* From SSO */ 31 | boards?: Board[] | string[]; 32 | groups?: Group[] | string[]; 33 | currentGroup: Group | string; 34 | type?: string; 35 | followings?: ( 36 | | { followee: string | User; onModel: "User" } 37 | | { followee: string | Group; onModel: "Group" } 38 | )[]; 39 | 40 | followers?: User[] | string[]; 41 | recommends?: Zabo[] | string[]; 42 | interests?: Record; 43 | interestMeta?: { 44 | lastCountedDate?: string; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/interface/schemas/zabo.ts: -------------------------------------------------------------------------------- 1 | import type { MongoSchema } from "../utils/mongo"; 2 | import type { User, Group, Board } from "./index"; 3 | 4 | export interface Zabo extends MongoSchema { 5 | createdBy?: User | string; 6 | owner?: Group | string; 7 | photos?: { 8 | url?: string; 9 | width?: number; 10 | height?: number; 11 | }[]; 12 | meta?: { 13 | w?: number; 14 | h?: number; 15 | }; 16 | title: string; 17 | description: string; 18 | category?: string[]; 19 | views?: number; 20 | effectiveViews?: number; 21 | schedules?: { 22 | title?: string; 23 | startAt: string; 24 | endAt?: string; 25 | eventType?: "행사" | "신청"; 26 | }[]; 27 | showBoard: boolean; 28 | 29 | pins?: Board[] | string[]; 30 | likes?: ZaboLike[]; 31 | likesWithTime?: ZaboLike[]; 32 | score?: number; 33 | scoreMeta: { 34 | lastLikeCount?: number; 35 | lastLikeTimeMA?: string; 36 | lastCountedViewDate?: string; 37 | lastViewTimeMA?: string; 38 | }; 39 | } 40 | 41 | export interface ZaboLike extends MongoSchema { 42 | user?: User | string; 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/interface/user.ts: -------------------------------------------------------------------------------- 1 | import type { GroupApply as GroupApplySchema, User as UserSchema } from "./schemas"; 2 | 3 | export interface User extends UserSchema { 4 | pendingGroups?: Pick[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/interface/utils/mongo.ts: -------------------------------------------------------------------------------- 1 | export interface MongoSchema { 2 | _id: string; 3 | createdAt?: string; 4 | updatedAt?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/interface/zabo.ts: -------------------------------------------------------------------------------- 1 | import type { Zabo as ZaboSchema } from "./schemas"; 2 | 3 | // TODO: Write zabo type 4 | export type Zabo = ZaboSchema; 5 | -------------------------------------------------------------------------------- /src/lib/mixins.js: -------------------------------------------------------------------------------- 1 | import { css } from "styled-components"; 2 | 3 | export const flexCenter = css` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | `; 8 | -------------------------------------------------------------------------------- /src/lib/propTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | 3 | export const ZaboType = PropTypes.shape({ 4 | _id: PropTypes.string, 5 | title: PropTypes.string, 6 | owner: PropTypes.shape({ 7 | _id: PropTypes.string, 8 | name: PropTypes.string, 9 | profilePhoto: PropTypes.string, 10 | subtitle: PropTypes.string, 11 | }), 12 | description: PropTypes.string, 13 | category: PropTypes.arrayOf(PropTypes.string), 14 | photos: PropTypes.arrayOf( 15 | PropTypes.shape({ 16 | height: PropTypes.number, 17 | width: PropTypes.number, 18 | url: PropTypes.string, 19 | }), 20 | ), 21 | views: PropTypes.number, 22 | effectiveViews: PropTypes.number, 23 | createdAt: PropTypes.string, 24 | schedules: PropTypes.arrayOf( 25 | PropTypes.shape({ 26 | title: PropTypes.string, 27 | startAt: PropTypes.string, 28 | endAt: PropTypes.string, 29 | type: PropTypes.string, 30 | }), 31 | ), 32 | showBoard: PropTypes.bool, 33 | isLiked: PropTypes.bool, 34 | isPinned: PropTypes.bool, 35 | isMyZabo: PropTypes.bool, 36 | }); 37 | 38 | export const GroupType = PropTypes.shape({ 39 | _id: PropTypes.string, 40 | name: PropTypes.string, 41 | profilePhoto: PropTypes.string, 42 | stats: PropTypes.shape({ 43 | // TODO: Generate stats in server 44 | zaboCount: PropTypes.number, 45 | followerCount: PropTypes.number, 46 | recentUploadDate: PropTypes.string, 47 | }), 48 | myRole: PropTypes.oneOf(["admin", "editor"]), 49 | }); 50 | 51 | export const UserType = PropTypes.shape({ 52 | _id: PropTypes.string, 53 | email: PropTypes.string, 54 | username: PropTypes.string, 55 | description: PropTypes.string, 56 | profilePhoto: PropTypes.string, 57 | backgroundPhoto: PropTypes.string, 58 | birthday: PropTypes.string, 59 | lastName: PropTypes.string, 60 | firstName: PropTypes.string, 61 | studentId: PropTypes.string, 62 | koreanName: PropTypes.string, 63 | boards: PropTypes.arrayOf( 64 | PropTypes.shape({ 65 | pinsCount: PropTypes.number, 66 | pins: PropTypes.array, 67 | }), 68 | ), 69 | currentGroup: PropTypes.oneOfType([GroupType, PropTypes.string]), 70 | groups: PropTypes.arrayOf(PropTypes.oneOfType([GroupType, PropTypes.string])), 71 | stats: PropTypes.shape({ 72 | // TODO: Change shape from server 73 | likesCount: PropTypes.number, 74 | followingsCount: PropTypes.number, 75 | }), 76 | }); 77 | -------------------------------------------------------------------------------- /src/lib/storage.js: -------------------------------------------------------------------------------- 1 | export default (function () { 2 | try { 3 | const st = localStorage || {}; 4 | return { 5 | setItem: (key, object) => { 6 | st[key] = typeof object === "string" ? object : JSON.stringify(object); 7 | }, 8 | getItem: (key, parse = true) => { 9 | if (!st[key]) { 10 | return null; 11 | } 12 | const value = st[key]; 13 | 14 | try { 15 | const parsed = parse ? JSON.parse(value) : value; 16 | return parsed; 17 | } catch (e) { 18 | return value; 19 | } 20 | }, 21 | removeItem: (key) => { 22 | if (localStorage) { 23 | return localStorage.removeItem(key); 24 | } 25 | return delete st[key]; 26 | }, 27 | }; 28 | } catch (err) { 29 | console.warn(err); 30 | setTimeout(() => alert("Cookie disabled"), 1000); 31 | return { 32 | setItem: (key, object) => "", 33 | getItem: (key) => "", 34 | removeItem: (key) => "", 35 | }; 36 | } 37 | })(); 38 | -------------------------------------------------------------------------------- /src/lib/theme.ts: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | main: "#143441", 3 | point: "#ff5d5d", 4 | sub: "#35d48d", 5 | textMain: "#202020", 6 | gray100: "#202020", 7 | gray90: "#363636", 8 | gray80: "#4d4d4d", 9 | gray70: "#636363", 10 | gray60: "#797979", 11 | gray50: "#8f8f8f", 12 | gray40: "#a6a6a6", 13 | gray30: "#bcbcbc", 14 | gray20: "#d2d2d2", 15 | gray10: "#e9e9e9", 16 | gray5: "#f4f4f4", 17 | gray3: "#f8f8f8", 18 | gray2: "#fbfbfb", 19 | gray1: "#fdfdfd", 20 | cobalt: "#194386", 21 | black: "#333333", 22 | red: "#e60909", 23 | red50: "#FA6B6B", 24 | green50: "#35d48d", 25 | green: "#00a97d", 26 | pinkishGrey: "#cccccc", 27 | border: "#cccccc", 28 | pinkishGreyTwo: "#d0d0d0", 29 | deepBlue: "#194386", 30 | white: "white", 31 | teal: "#00a97d", 32 | tealishGreen: "#05d27e", 33 | azure: "#0090ff", 34 | brownishGrey: "#666666", 35 | warmGrey: "#999999", 36 | veryLightBlue: "#dee7f2", 37 | none: "rgba(0,0,0,0)", 38 | } as const; 39 | -------------------------------------------------------------------------------- /src/lib/utils/selector.ts: -------------------------------------------------------------------------------- 1 | import { get } from "lodash"; 2 | import { createSelector, ParametricSelector, Selector } from "reselect"; 3 | import { IGroup, IJwt, IZabo, IZaboMap } from "types/index.d"; 4 | import { IState } from "types/store.d"; 5 | 6 | import { CHECK_AUTH } from "store/reducers/auth"; 7 | 8 | const decodedTokenSelector: Selector = (state: IState) => 9 | get(state, ["auth", "jwt"]); 10 | const isDecodedTokenAlive = (decoded: IJwt | undefined) => { 11 | if (decoded) { 12 | return 1000 * decoded.exp - new Date().getTime() >= 5000; 13 | } 14 | return false; 15 | }; 16 | const decodedTokenLifetime = (decoded: IJwt | undefined): number => { 17 | if (decoded) { 18 | const remain = decoded.exp * 1000 - Date.now(); 19 | return remain > 0 ? remain : 0; 20 | } 21 | return 0; 22 | }; 23 | 24 | export const isAuthedSelector = createSelector(decodedTokenSelector, isDecodedTokenAlive); 25 | export const tokenTimeLeft = createSelector(decodedTokenSelector, decodedTokenLifetime); 26 | 27 | export const isAdminSelector = (state: IState) => get(state, ["auth", "info", "isAdmin"]); 28 | export const authPendingSelector = (state: IState) => state.pender.pending[CHECK_AUTH]; 29 | export const isAdminOrPendingSelector = (state: IState) => [ 30 | isAdminSelector(state), 31 | authPendingSelector(state), 32 | ]; 33 | 34 | const zabosSelector: ParametricSelector = ( 35 | state: IState, 36 | zaboIds: string[], 37 | ): [IZaboMap, string[]] => [get(state, ["zabo", "zabos"]), zaboIds]; 38 | const zabosComputer = ([zabos, zaboIds]: [IZaboMap, string[]]) => 39 | zaboIds.map((id: string) => zabos[id]); 40 | 41 | // TODO: Factory function (due to cache size of 1) 42 | export const zaboListFromIdsSelector = createSelector(zabosSelector, zabosComputer); 43 | 44 | const zaboSelector = (state: IState, zaboId: string) => get(state, ["zabo", "zabos", zaboId]); 45 | const ownGroupsSelector = (state: IState) => get(state, ["auth", "info", "groups"]); 46 | const isMyZabo = (zabo: IZabo, groups: IGroup[]) => 47 | !!groups.find((group: IGroup) => zabo && group._id === get(zabo, ["owner", "_id"])); 48 | export const isMyZaboSelector = createSelector([zaboSelector, ownGroupsSelector], isMyZabo); 49 | -------------------------------------------------------------------------------- /src/lib/utils/style.ts: -------------------------------------------------------------------------------- 1 | import { css } from "styled-components"; 2 | 3 | export const mediaSizes = { 4 | desktop: 992, 5 | tablet: 640, 6 | mobile: 320, 7 | } as const; 8 | 9 | const wrapMedia = 10 | (size: number) => 11 | (...args: ReturnType | Parameters) => 12 | css` 13 | @media (min-width: ${size / 16}em) { 14 | ${Array.isArray(args) && args.length > 1 ? css(...args) : args}; 15 | } 16 | `; 17 | 18 | export const media = { 19 | desktop: wrapMedia(mediaSizes.desktop), 20 | tablet: wrapMedia(mediaSizes.tablet), 21 | mobile: wrapMedia(mediaSizes.mobile), 22 | } as const; 23 | 24 | export const get = 25 |

    , V>(prop: keyof P, defaultValue: V) => 26 | (props: P & { theme: P }) => 27 | props[prop] || props.theme[prop] || defaultValue; 28 | export const getSize = 29 |

    (prop: keyof P) => 30 | (props: P) => 31 | typeof props[prop] === "number" ? `${props[prop]}px` : props[prop]; 32 | 33 | export const cx = (...classNames: (string | Record)[]) => { 34 | let result = ""; 35 | classNames.forEach((className) => { 36 | if (typeof className === "string") result += `${className} `; 37 | if (typeof className === "object") { 38 | for (const key in className) { 39 | // eslint-disable-next-line no-prototype-builtins 40 | if (className.hasOwnProperty(key)) if (className[key]) result += `${key} `; 41 | } 42 | } 43 | }); 44 | return result.trim(); 45 | }; 46 | -------------------------------------------------------------------------------- /src/lib/variables.js: -------------------------------------------------------------------------------- 1 | export const ZABO_CATEGORIES = [ 2 | "행사", 3 | "공연", 4 | "축제", 5 | "세미나", 6 | "교육", 7 | "모임", 8 | "이벤트", 9 | "공모전", 10 | "전시", 11 | "공지", 12 | "모집", 13 | "채용", 14 | "봉사", 15 | "오픈동방", 16 | "데모데이", 17 | ]; 18 | export const GROUP_CATEGORIES = ["학생단체", "동아리", "학교 부서", "스타트업", "기업"]; 19 | export const GROUP_CATEGORIES_2 = [ 20 | "과학생회", 21 | "자치단체", 22 | "총학생회", 23 | "생활문화", 24 | "예술", 25 | "음악", 26 | "종교사회", 27 | "체육", 28 | "학술", 29 | "창업", 30 | ]; 31 | /* eslint-disable */ 32 | export const GROUP_CATEGORY_HIERARCHIY = { 33 | 학생단체: ["과학생회", "자치단체", "총학생회"], 34 | 동아리: ["생활문화", "예술", "음악", "종교사회", "체육", "학술", "창업"], 35 | }; 36 | export const RESERVED_ROUTES_USERNAME_EXCEPTIONS = ["auth", "settings", "admin"]; 37 | 38 | export const alerts = { 39 | login: "로그인이 필요합니다. SSO를 사용해 로그인 하시겠습니까?", 40 | upload: "자보를 게시하시겠습니까?", 41 | edit: "자보를 수정하시겠습니까?", 42 | del: "정말로 자보를 삭제하시겠습니까?", 43 | addMember: "멤버를 추가하시겠습니까?", 44 | updateMember: "멤버 권한을 변경하시겠습니까?", 45 | deleteMember: "정말로 멤버를 삭제하시겠습니까?", 46 | }; 47 | -------------------------------------------------------------------------------- /src/locales/en/Home.json: -------------------------------------------------------------------------------- 1 | { 2 | "zabo": "ZABOasdf

  • asd
  • asdfasdf sadfasdf" 3 | } 4 | -------------------------------------------------------------------------------- /src/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": "Home", 3 | "login": "Log In", 4 | "logout": "Log Out", 5 | "signup": "Sign Up" 6 | } 7 | -------------------------------------------------------------------------------- /src/locales/ko/Home.json: -------------------------------------------------------------------------------- 1 | { 2 | "zabo": "자보" 3 | } 4 | -------------------------------------------------------------------------------- /src/locales/ko/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": "홈", 3 | "login": "로그인", 4 | "logout": "로그아웃", 5 | "signup": "회원가입" 6 | } 7 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare namespace NodeJS { 3 | interface ProcessEnv { 4 | NODE_ENV: "development" | "production" | "test" | "ci"; 5 | PUBLIC_URL: string; 6 | EXTEND_ESLINT: string; 7 | NODE_PATH: string; 8 | } 9 | } 10 | interface Window { 11 | Stripe: any; 12 | } 13 | -------------------------------------------------------------------------------- /src/setupProxy.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const proxy = require("http-proxy-middleware"); 3 | 4 | module.exports = function (app) { 5 | app.use( 6 | "/api", 7 | proxy({ 8 | target: "http://localhost:6001", 9 | changeOrigin: true, 10 | }), 11 | ); 12 | 13 | app.use( 14 | "/admin", 15 | proxy({ 16 | target: "http://localhost:6001", 17 | changeOrigin: true, 18 | }), 19 | ); 20 | 21 | app.use( 22 | "/s", 23 | proxy({ 24 | target: "http://localhost:6001", 25 | pathRewrite: { 26 | "^/s": "/api/s", 27 | }, 28 | }), 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/static/hd/zhangjiajie-landscape.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/hd/zhangjiajie-landscape.jpg -------------------------------------------------------------------------------- /src/static/hd/zhangjiajie-portrait.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/hd/zhangjiajie-portrait.jpg -------------------------------------------------------------------------------- /src/static/hd/zhangjiajie-snow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/hd/zhangjiajie-snow.jpg -------------------------------------------------------------------------------- /src/static/icon/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/icon/baseline-expand_more-24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/icon/category/all.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/static/icon/category/exhibition.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/static/icon/category/notice.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/static/icon/category/recruit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/static/icon/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/static/icon/person_add-24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/icon/remove-24px.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/icon/rightArrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/static/images/add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/images/add_gray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/images/banner_poster.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/banner_poster.jpg -------------------------------------------------------------------------------- /src/static/images/banner_poster.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/banner_poster.webp -------------------------------------------------------------------------------- /src/static/images/banner_sparcs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/banner_sparcs.jpg -------------------------------------------------------------------------------- /src/static/images/banner_sparcs.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/banner_sparcs.webp -------------------------------------------------------------------------------- /src/static/images/bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/images/bookmarkEmpty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/images/calendar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/images/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/cancel.png -------------------------------------------------------------------------------- /src/static/images/check_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/check_gray.png -------------------------------------------------------------------------------- /src/static/images/chevron_home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/images/chevron_left.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/static/images/chevron_right.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/static/images/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Svg Vector Icons : http://www.onlinewebfonts.com/icon 6 | 7 | -------------------------------------------------------------------------------- /src/static/images/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/images/defaultProfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/defaultProfile.png -------------------------------------------------------------------------------- /src/static/images/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/images/group.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/static/images/groupDefaultProfile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/groupDefaultProfile.png -------------------------------------------------------------------------------- /src/static/images/landing_background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/landing_background.jpg -------------------------------------------------------------------------------- /src/static/images/leftArrow-navy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/leftArrow-navy.png -------------------------------------------------------------------------------- /src/static/images/leftArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/leftArrow.png -------------------------------------------------------------------------------- /src/static/images/leftScroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/leftScroll.png -------------------------------------------------------------------------------- /src/static/images/like.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/images/likeEmpty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/images/main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/main.jpg -------------------------------------------------------------------------------- /src/static/images/notFoundImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/notFoundImage.jpg -------------------------------------------------------------------------------- /src/static/images/recruitAscii.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | ........................................................................................................... 3 | ........................................................................................................... 4 | ................','........................................................................................ 5 | ...............,lo;........................................................................................ 6 | ......';,.....,lxxc'....................................................................................... 7 | ......'ll;...,lxxxo;....................................................................................... 8 | ......'ldo;',lxxxxxc.......:oddddddo;..oxxdddddl;....,ldxo;...,ldddodddo:'..':looooool:'.;cooooool;........ 9 | ......'lddoclxxxxxo;......;dxdc;.......oxd;..;dxo;..,ddddo,...,ldo:,,;lxd:',ldoc;,,;cl:',ldoc,,,;;'........ 10 | ......'lddooxxxxd:'.......;oxxdlc;.....oxd;..;dxo;.,ddc:odl'..,ldo:,';ldd:,cddc'........'coooc::;,......... 11 | ......'ldloxxxdc,..........,:coddxxd;..oxxdddddo;.,oxo:;ldd:..,ldddooddoc''cddc'.........';:cloooo:'....... 12 | ......,codxxxxo:;,''............;oxxo;.oxd;......,cxddooodxo;.,lddc:cddo;..;odo:'''';:;'',;,'',codl;....... 13 | ......':oxxxdddddo:,......;dxdooodxdc;.oxo;.....;dxl;'.',:dxl,,ldo;.':odo;..,codollodoc,;loollloooc'....... 14 | .......,;:::ldxdc,..........;ccccc;....oxo:.....'oc:,....';::,';::'...,::;'...,;::::;,'..,;:::::;,'........ 15 | ...........;ooc,........................................................................................... 16 | ..........;l:,............................................................................................. 17 | .........,;'............................................................................................... 18 | ........................................................................................................... 19 | ........................................................................................................... 20 | ......................🎂 개발자 도구를 열어보셨군요. 당신은 개발자, 스팍스에 지원하세요! 🎂............................. 21 | ........................................................................................................... 22 | ........................................................................................................... 23 | `; 24 | -------------------------------------------------------------------------------- /src/static/images/rightArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/rightArrow.png -------------------------------------------------------------------------------- /src/static/images/rightArrowForward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/rightArrowForward.png -------------------------------------------------------------------------------- /src/static/images/rightGrayArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/rightGrayArrow.png -------------------------------------------------------------------------------- /src/static/images/rightScroll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/rightScroll.png -------------------------------------------------------------------------------- /src/static/images/search-icon-navy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/search-icon-navy.png -------------------------------------------------------------------------------- /src/static/images/search-icon-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/search-icon-white.png -------------------------------------------------------------------------------- /src/static/images/uploadImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/images/uploadImg.png -------------------------------------------------------------------------------- /src/static/images/user.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/static/images/whiteBookmakrEmpty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/images/whiteBookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/images/whiteLike.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/images/whiteLikeEmpty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/images/whiteShare.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/logo/logo.png -------------------------------------------------------------------------------- /src/static/logo/sparcs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparcs-kaist/zabo-front-reactjs/d22a0e40efc5c68eea48790b1dd24438aa7a61fd/src/static/logo/sparcs.png -------------------------------------------------------------------------------- /src/static/logo/sparcs.svg: -------------------------------------------------------------------------------- 1 | 2 | Symbol_Zabo 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/store/example.ts: -------------------------------------------------------------------------------- 1 | type State = object; 2 | interface IAction { 3 | type: string; 4 | [key: string]: any; 5 | } 6 | interface IReducer { 7 | (state: State, action: IAction): State; 8 | } 9 | interface IListener { 10 | (state: State): void; 11 | } 12 | 13 | const createStore = (reducer: IReducer) => { 14 | let state: State = {}; 15 | let listeners: IListener[] = []; 16 | 17 | const getState = () => state; 18 | const dispatch = (action: IAction) => { 19 | state = reducer(state, action); 20 | listeners.forEach((listener) => listener(state)); 21 | }; 22 | const subscribe = (listener: IListener) => { 23 | listeners.push(listener); 24 | return () => { 25 | listeners = listeners.filter((l) => l !== listener); 26 | }; 27 | }; 28 | dispatch({ type: "Init" }); 29 | return { 30 | getState, 31 | dispatch, 32 | subscribe, 33 | }; 34 | }; 35 | 36 | const reducer = ( 37 | state: State = { 38 | /* Prev State */ 39 | }, 40 | action: IAction, 41 | ) => ({ 42 | // New State 43 | }); 44 | 45 | export default createStore; 46 | 47 | /* 48 | { 49 | showVisualText: false, 50 | app: { 51 | 52 | }, 53 | todo: { 54 | 55 | } 56 | } 57 | 58 | ADD_TODO : 아이템을 추가 59 | DELETE_TODO : 아이템을 삭제 60 | UPDATE_TODO : 아이템을 업데이트 61 | 62 | SHOW_TEXT: 텍스트를 보이게 한다 63 | HIDE_TEXT: 텍스트를 감춘다. 64 | 65 | */ 66 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from "redux"; 2 | import { composeWithDevTools } from "redux-devtools-extension"; 3 | import penderMiddleware from "redux-pender"; 4 | 5 | import persist from "./persist"; 6 | import rootReducer from "./reducers"; 7 | 8 | const composeEnhancers = composeWithDevTools({ 9 | actionBlacklist: ["@@redux-pender/SUCCESS", "@@redux-pender/FAILURE", "@@redux-pender/PENDING"], 10 | maxAge: 1000, 11 | }); 12 | 13 | const store = createStore( 14 | rootReducer, 15 | {}, // Initial state 16 | composeEnhancers(applyMiddleware(penderMiddleware(), persist)), 17 | ); 18 | 19 | if (rootReducer.hot) { 20 | rootReducer.hot.accept("./reducers", () => { 21 | const nextRootReducer = import("./reducers"); 22 | store.replaceReducer(nextRootReducer); 23 | }); 24 | } 25 | 26 | export default store; 27 | -------------------------------------------------------------------------------- /src/store/persist.ts: -------------------------------------------------------------------------------- 1 | import { Store } from "redux"; 2 | import { Action } from "redux-actions"; 3 | 4 | import storage from "lib/storage"; 5 | 6 | const persistUpload = 7 | (store: Store) => (next: (action: Action) => any) => (action: Action) => { 8 | const result = next(action); 9 | 10 | if (action.type.substring(0, 6) === "upload") { 11 | const { upload } = store.getState(); 12 | const { step, imagesSelected, submitted, ...persisted } = upload; 13 | persisted.images = []; 14 | persisted.date = new Date(); 15 | storage.setItem("uploadPersist", JSON.stringify(persisted)); 16 | } 17 | 18 | return result; // 여기서 반환하는 값은 store.dispatch(ACTION_TYPE) 했을때의 결과로 설정됩니다 19 | }; 20 | 21 | export default persistUpload; 22 | -------------------------------------------------------------------------------- /src/store/reducers/app.ts: -------------------------------------------------------------------------------- 1 | import produce from "immer"; 2 | import { Action, createAction, handleActions } from "redux-actions"; 3 | 4 | // action types 5 | const UPDATE_WINDOW_SIZE = "app/UPDATE_WINDOW_SIZE"; 6 | 7 | // action creators 8 | export const setWindowSize = createAction(UPDATE_WINDOW_SIZE); 9 | 10 | interface IWindowSize { 11 | width: number; 12 | height: number; 13 | } 14 | export interface IAppState { 15 | windowSize: IWindowSize; 16 | } 17 | 18 | // initial state 19 | const initialState: IAppState = { 20 | windowSize: { 21 | width: window.innerWidth, 22 | height: window.innerHeight, 23 | }, 24 | }; 25 | 26 | // reducer 27 | export default handleActions( 28 | { 29 | [UPDATE_WINDOW_SIZE]: (state, action: Action) => 30 | produce(state, (draft: IAppState) => { 31 | draft.windowSize = action.payload; 32 | }), 33 | }, 34 | initialState, 35 | ); 36 | -------------------------------------------------------------------------------- /src/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { penderReducer } from "redux-pender"; 3 | // import immutableTransform from 'redux-persist-transform-immutable'; 4 | // import storage from 'redux-persist/lib/storage'; 5 | // import { persistReducer } from 'redux-persist'; 6 | 7 | import admin from "./admin"; 8 | import app from "./app"; 9 | import auth from "./auth"; 10 | import profile from "./profile"; 11 | import upload from "./upload"; 12 | import zabo from "./zabo"; 13 | 14 | const modules = { 15 | admin, 16 | app, 17 | auth, 18 | profile, 19 | upload, 20 | zabo, 21 | pender: penderReducer, 22 | }; 23 | 24 | // const persistConfig = { 25 | // transforms: [immutableTransform ({ 26 | // blacklist: ['groupSelected', 'imagesSelected', 'infoWritten'], 27 | // })], 28 | // key: 'upload', 29 | // storage, 30 | // }; 31 | // 32 | // modules.upload = persistReducer (persistConfig, modules.upload); 33 | 34 | export default combineReducers(modules); 35 | -------------------------------------------------------------------------------- /src/stories/index.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies:0 */ 2 | import React from "react"; 3 | import { action } from "@storybook/addon-actions"; 4 | import { linkTo } from "@storybook/addon-links"; 5 | import { storiesOf } from "@storybook/react"; 6 | import { Button, Welcome } from "@storybook/react/demo"; 7 | 8 | storiesOf("Welcome", module).add("to Storybook", () => ); 9 | 10 | storiesOf("Button", module) 11 | .add("with text", () => ) 12 | .add("with some emoji", () => ( 13 | 18 | )); 19 | -------------------------------------------------------------------------------- /src/theme/index.js: -------------------------------------------------------------------------------- 1 | import theme from "styled-theming"; 2 | 3 | export default { 4 | mode: "light", 5 | }; 6 | // define background colours for `mode` theme 7 | export const backgroundColor = theme("mode", { 8 | light: "#fafafa", 9 | dark: "#222", 10 | }); 11 | 12 | // define text color for `mode` theme 13 | export const textColor = theme("mode", { 14 | light: "#000", 15 | dark: "#fff", 16 | }); 17 | 18 | export const mainColor = theme("mode", { 19 | light: "#1976d2", 20 | dark: "#12397d", 21 | }); 22 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface IJwt { 2 | _id: string; 3 | sid: string; 4 | email: string; 5 | username: string; 6 | iat: number; 7 | exp: number; 8 | iss: string; 9 | } 10 | 11 | export interface IZabo { 12 | _id: string; 13 | title: string; 14 | owner: { 15 | _id: string; 16 | name: string; 17 | profilePhoto: string; 18 | subtitle: string; 19 | }; 20 | description: string; 21 | category: string[]; 22 | photos: [ 23 | { 24 | height: number; 25 | width: number; 26 | url: string; 27 | }, 28 | ]; 29 | views: number; 30 | effectiveViews: number; 31 | createdAt: string; 32 | schedules: [ 33 | { 34 | title: string; 35 | startAt: string; 36 | endAt: string; 37 | type: string; 38 | }, 39 | ]; 40 | showBoard: boolean; 41 | isLiked: boolean; 42 | isPinned: boolean; 43 | isMyZabo: boolean; 44 | } 45 | 46 | export interface IZaboMap { 47 | [key: string]: IZabo; 48 | } 49 | 50 | export interface IGroup { 51 | _id: string; 52 | name: string; 53 | profilePhoto: string; 54 | stats: { 55 | zaboCount: number; 56 | followerCount: number; 57 | recentUploadDate: string; 58 | }; 59 | myRole: "admin" | "editor"; 60 | } 61 | 62 | export interface IGroupMap { 63 | [key: string]: IGroup; 64 | } 65 | 66 | export interface IUser { 67 | _id: string; 68 | // eslint-disable-next-line camelcase 69 | sso_uid?: string; 70 | // eslint-disable-next-line camelcase 71 | sso_sid?: string; 72 | gender?: string; 73 | email?: string; 74 | kaistPersonType?: string; 75 | isAdmin?: boolean; 76 | flags: string[]; 77 | pendingGroups: IGroup[]; 78 | username: string; 79 | description?: string; 80 | profilePhoto?: string; 81 | backgroundPhoto?: string; 82 | birthday?: string; 83 | lastName?: string; 84 | firstName?: string; 85 | studentId?: string; 86 | koreanName?: string; 87 | boards?: { 88 | pinsCount: number; 89 | pins: any[]; 90 | }[]; 91 | currentGroup?: IGroup | string | null; 92 | groups: IGroup[]; 93 | stats?: { 94 | likesCount: number; 95 | followingsCount: number; 96 | }; 97 | } 98 | 99 | export interface IUserMap { 100 | [key: string]: IGroup; 101 | } 102 | 103 | export type IProfile = IUser | IGroup; 104 | 105 | export interface IProfileMap { 106 | [key: string]: IProfile; 107 | } 108 | -------------------------------------------------------------------------------- /src/types/store.d.ts: -------------------------------------------------------------------------------- 1 | import { PenderState } from "redux-pender"; 2 | 3 | import { IAdminState } from "store/reducers/admin"; 4 | import { IAppState } from "store/reducers/app"; 5 | import { IAuthState } from "store/reducers/auth"; 6 | import { IProfileState } from "store/reducers/profile"; 7 | import { IUploadState } from "store/reducers/upload"; 8 | import { IZaboState } from "store/reducers/zabo"; 9 | 10 | export interface IState { 11 | admin: IAdminState; 12 | app: IAppState; 13 | auth: IAuthState; 14 | profile: IProfileState; 15 | upload: IUploadState; 16 | zabo: IZaboState; 17 | pender: PenderState; 18 | } 19 | -------------------------------------------------------------------------------- /tools/modules/colors.inc.sh: -------------------------------------------------------------------------------- 1 | DEFAULT=$(echo -en '\033[39m') 2 | BLK=$(echo -en '\033[30m') 3 | RED=$(echo -en '\033[31m') 4 | GRN=$(echo -en '\033[32m') 5 | YLW=$(echo -en '\033[33m') 6 | BLU=$(echo -en '\033[34m') 7 | MGT=$(echo -en '\033[35m') 8 | CYN=$(echo -en '\033[36m') 9 | LGRY=$(echo -en '\033[37m') 10 | DGRY=$(echo -en '\033[90m') 11 | LRED=$(echo -en '\033[91m') 12 | LGRN=$(echo -en '\033[92m') 13 | LYLW=$(echo -en '\033[93m') 14 | LBLU=$(echo -en '\033[94m') 15 | LMGT=$(echo -en '\033[95m') 16 | LCYN=$(echo -en '\033[96m') 17 | WHT=$(echo -en '\033[97m') 18 | ORNG=$(echo -en '\033[38;5;166m') 19 | 20 | RST=$(echo -en '\033[00m') 21 | BLD=$(echo -en '\033[1;2m') 22 | RBLD=$(echo -en '\033[21;22m') 23 | UND=$(echo -en '\033[4m') 24 | RUND=$(echo -en '\033[24m') -------------------------------------------------------------------------------- /tools/modules/verbose.inc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | verbose() 3 | { 4 | if $1; then 5 | exec 4>&2 3>&1 6 | IS_VERBOSE=true 7 | else 8 | exec 4>/dev/null 3>/dev/null 9 | IS_VERBOSE=false 10 | fi 11 | } 12 | 13 | to_verboseOut() 14 | { 15 | echo "$1" >&3 16 | } 17 | 18 | to_verboseErr() 19 | { 20 | echo >&2 "$1" 2>&4 21 | } 22 | 23 | if [ -n "$IS_VERBOSE" ]; then 24 | verbose $IS_VERBOSE 25 | else 26 | verbose false 27 | fi -------------------------------------------------------------------------------- /tools/moveBuildFolder.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SECOND=0 4 | 5 | # this causes the current directory to become the parent of this script, 6 | # which is the app's roots 7 | cd "$( dirname "${BASH_SOURCE[0]}" )/.." 8 | . tools/modules/colors.inc.sh 9 | . tools/modules/verbose.inc.sh 10 | 11 | say () { 12 | echo "=> $@" 13 | } 14 | 15 | help_move_build_folder() 16 | { 17 | echo "Usage: $0 -v" 18 | echo "Options: These are optional argument" 19 | echo " -v verbose" 20 | echo " -h help" 21 | echo " -l run only if running in linux machine" 22 | exit 1 23 | } 24 | 25 | while getopts vlh opt 26 | do 27 | case "$opt" in 28 | v) verbose true;; 29 | l) 30 | if [ ! $(uname -s) == "Linux" ]; then 31 | exit 0 32 | fi 33 | ;; 34 | h) help_move_build_folder; exit 1;; 35 | \?) help_move_build_folder; exit 1;; 36 | esac 37 | done 38 | 39 | say "Moving build folder to deploy" 40 | 41 | say "Check if maintenance is on" 42 | # since we're currently in the app's root, we just need to climb up one level 43 | # to find the maintenance file 44 | if [ -f ../maintenance_on.html ]; 45 | then 46 | say "-- Maintenance is on, aborting the process" 47 | exit 48 | else 49 | say "-- Maintenance is off, proceed" 50 | fi 51 | 52 | say "Removing current deploy > $ rm -rf deploy" 53 | rm -rf deploy 54 | 55 | say "Copying build to deploy > $ cp -r build deploy" 56 | cp -r build deploy 57 | 58 | timestamp=`date "+%Y-%m-%d %H:%M:%S"` 59 | min=$(($SECONDS / 60)) 60 | sec=$(($SECONDS % 60)) 61 | 62 | say 63 | say "====== Directory copied successfully ======" 64 | say "+-----------------------------------------+" 65 | say "| Task completed at $timestamp |" 66 | say "| Time elapsed "$min"m and "$sec"s |" 67 | say "+-----------------------------------------+" 68 | -------------------------------------------------------------------------------- /tools/updateStories.sh: -------------------------------------------------------------------------------- 1 | ls src/components | while read line; do 2 | ls src/components/$line | xargs -I% yarn generate $line % 'y' 3 | done 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react" 18 | }, 19 | "includes": ["src"], 20 | } 21 | --------------------------------------------------------------------------------