├── .babelrc ├── .codeclimate.yml ├── .dockerignore ├── .editorconfig ├── .env.sample ├── .eslintrc.js ├── .github ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── lint.yml │ ├── misspell.yml │ ├── publish-dockerhub.yml │ ├── publish-ecr.yml │ ├── release-drafter.yml │ ├── source-map.yml │ └── test.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .node-version ├── .prettierrc.json ├── Dockerfile ├── LICENSE ├── README.md ├── __tests__ ├── __helpers__ │ └── test-id.js ├── __setups__ │ └── config.js ├── components │ ├── atoms │ │ ├── bar-chart.spec.js │ │ ├── base-input.spec.js │ │ ├── coach-tooltip.spec.js │ │ ├── color-scheme.spec.js │ │ ├── doughnut-chart.spec.js │ │ ├── drag-drop.spec.js │ │ ├── dragger.spec.js │ │ ├── login-guard.spec.js │ │ ├── resizer.spec.js │ │ ├── ticker.spec.js │ │ └── window-scroll.spec.js │ ├── molecules │ │ ├── base-select.spec.js │ │ ├── color-select.spec.js │ │ ├── datetime-picker.spec.js │ │ ├── delighted.spec.js │ │ ├── delta.spec.js │ │ ├── locale-select.spec.js │ │ ├── swipe-menu.spec.js │ │ ├── tabs.spec.js │ │ └── toast.spec.js │ └── organisms │ │ ├── activity-editor-description.spec.js │ │ ├── activity-editor.spec.js │ │ ├── calendar-activity.spec.js │ │ ├── calendar-content.spec.js │ │ ├── calendar-day.spec.js │ │ ├── calendar-ruler.spec.js │ │ ├── loop-slider.spec.js │ │ ├── nav-modal.spec.js │ │ ├── project-editor.spec.js │ │ ├── project-list.spec.js │ │ ├── pwa-popover.spec.js │ │ ├── setting-delete-account-button.spec.js │ │ ├── setting-email-editor.spec.js │ │ ├── setting-logout-button.spec.js │ │ ├── setting-password-editor.spec.js │ │ ├── setting-start-day-select.spec.js │ │ ├── setting-time-zone-select.spec.js │ │ ├── suggestion-list.spec.js │ │ └── timer-form.spec.js ├── layouts │ └── default.spec.js ├── pages │ ├── auth.spec.js │ ├── calendar.spec.js │ ├── oauth │ │ └── authorize.spec.js │ ├── password-reset │ │ ├── edit.spec.js │ │ └── index.spec.js │ ├── reports │ │ ├── csv.spec.js │ │ ├── index.spec.js │ │ └── pdf.spec.js │ ├── settings.spec.js │ └── settings │ │ ├── applications.spec.js │ │ └── integrations.spec.js ├── plugins │ └── api.spec.js └── store │ ├── activities │ ├── actions.spec.js │ └── getters.spec.js │ ├── activity-calendar │ ├── actions.spec.js │ ├── getters.spec.js │ └── mutations.spec.js │ ├── applications │ ├── actions.spec.js │ └── getters.spec.js │ ├── auth │ ├── actions.spec.js │ ├── getters.spec.js │ └── mutations.spec.js │ ├── entities │ ├── actions.spec.js │ ├── getters.spec.js │ └── mutations.spec.js │ ├── oauth │ ├── actions.spec.js │ ├── getters.spec.js │ └── mutations.spec.js │ ├── projects │ ├── actions.spec.js │ └── getters.spec.js │ ├── reports │ ├── actions.spec.js │ ├── getters.spec.js │ └── mutations.spec.js │ ├── suggestions │ ├── actions.spec.js │ ├── getters.spec.js │ └── mutations.spec.js │ ├── toast │ ├── actions.spec.js │ ├── getters.spec.js │ └── mutations.spec.js │ └── users │ ├── actions.spec.js │ ├── getters.spec.js │ └── mutations.spec.js ├── assets ├── README.md ├── images │ └── logo.svg ├── locales │ ├── common │ │ └── scopes.json │ ├── components │ │ └── organisms │ │ │ ├── activity-day-group.json │ │ │ ├── activity-editor-description.json │ │ │ ├── activity-editor.json │ │ │ ├── activity-item.json │ │ │ ├── calendar-activity.json │ │ │ ├── calendar-day-header.json │ │ │ ├── date-header.json │ │ │ ├── eol-notification.json │ │ │ ├── project-editor.json │ │ │ ├── project-list.json │ │ │ ├── pwa-popover.json │ │ │ ├── report-content.json │ │ │ ├── setting-delete-account-button.json │ │ │ ├── setting-email-editor.json │ │ │ ├── setting-locale-select.json │ │ │ ├── setting-logout-button.json │ │ │ ├── setting-password-editor.json │ │ │ ├── setting-start-day-select.json │ │ │ ├── setting-time-zone-select.json │ │ │ └── timer-form.json │ └── pages │ │ ├── auth.json │ │ ├── calendar.json │ │ ├── index.json │ │ ├── oauth │ │ └── authorize.json │ │ ├── password-reset │ │ ├── edit.json │ │ └── index.json │ │ ├── reports │ │ └── index.json │ │ ├── settings.json │ │ └── settings │ │ ├── applications.json │ │ ├── integrations.json │ │ └── notification.json └── scss │ ├── _animations.scss │ ├── _reset.scss │ ├── _tooltip.scss │ ├── main.scss │ └── modules │ ├── _mixins.scss │ └── _variables.scss ├── components ├── README.md ├── atoms │ ├── animate-duration.vue │ ├── bar-chart.vue │ ├── base-button.vue │ ├── base-input.vue │ ├── calendar-event.vue │ ├── coach-tooltip.vue │ ├── color-scheme.vue │ ├── dot.vue │ ├── doughnut-chart.vue │ ├── drag-drop.vue │ ├── dragger.vue │ ├── heading.vue │ ├── highlight.vue │ ├── icon-button.vue │ ├── icon.vue │ ├── indicator.vue │ ├── login-guard.vue │ ├── resizer.vue │ ├── ticker.vue │ └── window-scroll.vue ├── molecules │ ├── activity-name.vue │ ├── base-select.vue │ ├── color-select.vue │ ├── date-heading.vue │ ├── datetime-picker.vue │ ├── delighted.vue │ ├── delta.vue │ ├── dot-text.vue │ ├── loading.vue │ ├── locale-select.vue │ ├── modal-footer.vue │ ├── modal-header.vue │ ├── modal-item.vue │ ├── project-name.vue │ ├── setting-box.vue │ ├── swipe-menu.vue │ ├── tabs.vue │ └── toast.vue └── organisms │ ├── activity-day-group.vue │ ├── activity-editor-description.vue │ ├── activity-editor.vue │ ├── activity-item.vue │ ├── base-modal.vue │ ├── calendar-activity.vue │ ├── calendar-content.vue │ ├── calendar-day-header.vue │ ├── calendar-day.vue │ ├── calendar-hours.vue │ ├── calendar-ruler.vue │ ├── content-header.vue │ ├── date-header.vue │ ├── eol-notification.vue │ ├── loop-slider.vue │ ├── nav-modal.vue │ ├── project-editor.vue │ ├── project-list.vue │ ├── pwa-popover.vue │ ├── report-content-item.vue │ ├── report-content.vue │ ├── setting-delete-account-button.vue │ ├── setting-email-editor.vue │ ├── setting-locale-select.vue │ ├── setting-logout-button.vue │ ├── setting-password-editor.vue │ ├── setting-start-day-select.vue │ ├── setting-time-zone-select.vue │ ├── side-bar.vue │ ├── suggestion-list.vue │ └── timer-form.vue ├── dip.yml ├── docker-compose.dev.yml ├── docker-compose.prod.yml ├── docker-compose.yml ├── jest.config.js ├── layouts ├── README.md ├── auth.vue ├── default.vue └── none.vue ├── middleware └── README.md ├── nuxt.config.js ├── package.json ├── pages ├── README.md ├── auth.vue ├── calendar.vue ├── index.vue ├── oauth │ ├── authorize.vue │ └── callback.vue ├── password-reset │ ├── edit.vue │ └── index.vue ├── reports │ ├── csv.vue │ ├── index.vue │ └── pdf.vue ├── settings.vue └── settings │ ├── applications.vue │ ├── index.vue │ ├── integrations.vue │ └── notifications.vue ├── plugins ├── README.md ├── api.js ├── customs │ ├── mezr.js │ ├── platform.js │ └── px-min.js ├── load-script.js ├── logrocket.js ├── mixpanel.js ├── persist-state.js ├── v-scroll-lock.js ├── v-tooltip.js └── vue-timers.js ├── schemas.js ├── static ├── README.md ├── apple-splash-1125-2436.png ├── apple-splash-1170-2532.png ├── apple-splash-1242-2208.png ├── apple-splash-1242-2688.png ├── apple-splash-1284-2778.png ├── apple-splash-1536-2048.png ├── apple-splash-1620-2160.png ├── apple-splash-1668-2224.png ├── apple-splash-1668-2388.png ├── apple-splash-2048-2732.png ├── apple-splash-640-1136.png ├── apple-splash-750-1334.png ├── apple-splash-828-1792.png ├── apple-touch-icon.png ├── favicon.ico └── icon.png ├── store ├── README.md ├── activities.js ├── activity-calendar.js ├── applications.js ├── auth.js ├── entities.js ├── oauth.js ├── projects.js ├── reports.js ├── suggestions.js ├── toast.js └── user.js ├── stylelint.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": "defaults" 7 | } 8 | ] 9 | ], 10 | "env": { 11 | "test": { 12 | "presets": [ 13 | [ 14 | "@babel/preset-env", 15 | { 16 | "targets": { 17 | "node": "current" 18 | } 19 | } 20 | ] 21 | ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | file-lines: 3 | enabled: false 4 | similar-code: 5 | enabled: false 6 | method-lines: 7 | enabled: false 8 | exclude_paths: 9 | - '__tests__/**/*' 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Nuxtjs 2 | .nuxt 3 | node_modules 4 | coverage 5 | yarn-error.log 6 | 7 | # Dotenv 8 | .env* 9 | 10 | # PWA 11 | sw.* 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # Hackaru 2 | HACKARU_API_URL=http://localhost:3000 3 | HACKARU_API_TIMEOUT=0 4 | HACKARU_TOS_AND_PRIVACY_URL= 5 | 6 | # Google Analytics 7 | # GOOGLE_ANALYTICS_ID= 8 | 9 | # Sentry 10 | # SENTRY_DSN= 11 | # SENTRY_ENVIRONMENT= 12 | # SENTRY_PROJECT= 13 | # SENTRY_RELEASE= 14 | # SENTRY_CSP_REPORT_URI= 15 | # SENTRY_EXPECT_CT_REPORT_URI= 16 | 17 | # Delighted 18 | # DELIGHTED_TOKEN= 19 | 20 | # LogRocket 21 | # LOGROCKET_ID= 22 | # LOGROCKET_RELEASE= 23 | 24 | # Mixpanel 25 | # MIXPANEL_PROJECT_TOKEN= 26 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | jest: true, 6 | }, 7 | globals: { 8 | delighted: true, 9 | }, 10 | extends: [ 11 | 'plugin:vue/essential', 12 | 'plugin:vue/strongly-recommended', 13 | 'plugin:vue/recommended', 14 | 'prettier', 15 | ], 16 | rules: { 17 | 'vue/multi-word-component-names': 'off', 18 | 'no-unused-vars': [ 19 | 'error', 20 | { 21 | args: 'all', 22 | argsIgnorePattern: '^_', 23 | }, 24 | ], 25 | camelcase: [ 26 | 'error', 27 | { 28 | properties: 'never', 29 | }, 30 | ], 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: weekly 7 | time: '11:00' 8 | ignore: 9 | - dependency-name: 'color' 10 | versions: ['4.x'] 11 | - dependency-name: 'vue-plugin-load-script' 12 | versions: ['2.x'] 13 | open-pull-requests-limit: 10 14 | labels: 15 | - 'type: dependencies' 16 | - package-ecosystem: docker 17 | directory: '/' 18 | schedule: 19 | interval: weekly 20 | time: '11:00' 21 | open-pull-requests-limit: 10 22 | labels: 23 | - 'type: dependencies' 24 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | template: | 4 | $CHANGES 5 | categories: 6 | - title: '🚀 Features' 7 | labels: 8 | - 'type: feature' 9 | - title: '🐛 Bug Fixes' 10 | labels: 11 | - 'type: bug' 12 | - title: '🛠️ Improvements' 13 | labels: 14 | - 'type: improvement' 15 | - title: '📦 Dependency Updates' 16 | labels: 17 | - 'type: dependencies' 18 | 19 | version-resolver: 20 | minor: 21 | labels: 22 | - 'type: feature' 23 | patch: 24 | labels: 25 | - 'type: bug' 26 | - 'type: improvement' 27 | - 'type: dependencies' 28 | 29 | exclude-labels: 30 | - 'skip-changelog' 31 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f 15 | 16 | - name: Set up QEMU 17 | uses: docker/setup-qemu-action@c308fdd69d26ed66f4506ebd74b180abe5362145 18 | 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@0d135e0c2fc0dba0729c1a47ecfcf5a3c7f8579e 21 | 22 | - name: Build 23 | uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f 24 | with: 25 | push: false 26 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: code-ql 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | schedule: 10 | - cron: '0 3 * * 4' 11 | jobs: 12 | analyze: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | language: [javascript] 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f 21 | 22 | - name: Initialize CodeQL 23 | uses: github/codeql-action/init@21830ef0c1f0f06f2d82ba3f1f07cb3ffe543ed7 24 | with: 25 | languages: ${{ matrix.language }} 26 | 27 | - name: Perform CodeQL analysis 28 | uses: github/codeql-action/analyze@21830ef0c1f0f06f2d82ba3f1f07cb3ffe543ed7 29 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | run-lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f 15 | 16 | - name: Get yarn cache directory path 17 | id: yarn-cache-dir-path 18 | run: echo "::set-output name=dir::$(yarn cache dir)" 19 | 20 | - uses: actions/cache@1a9e2138d905efd099035b49d8b7a3888c653ca8 21 | with: 22 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 23 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-yarn- 26 | 27 | - name: Install dependencies 28 | run: yarn --frozen-lockfile 29 | 30 | - name: Run eslint 31 | run: yarn eslint . 32 | 33 | - name: Run stylelint 34 | run: yarn stylelint 35 | 36 | - name: Run prettier 37 | run: yarn prettier --check . 38 | -------------------------------------------------------------------------------- /.github/workflows/misspell.yml: -------------------------------------------------------------------------------- 1 | name: misspell 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | run-misspell: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f 15 | 16 | - name: Install misspell 17 | run: curl -L https://git.io/misspell | bash 18 | 19 | - name: Run misspell 20 | run: bin/misspell -error . 21 | -------------------------------------------------------------------------------- /.github/workflows/publish-dockerhub.yml: -------------------------------------------------------------------------------- 1 | name: publish-dockerhub 2 | on: 3 | release: 4 | types: 5 | - published 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f 12 | 13 | - name: Get tags 14 | id: get-tags 15 | run: | 16 | IMAGE_NAME=${DOCKERHUB_REGISTRY}/${DOCKERHUB_REPO_NAME} 17 | VERSION=${GITHUB_REF/refs\/tags\//} 18 | TAGS=${IMAGE_NAME}:latest 19 | TAGS=${TAGS},${IMAGE_NAME}:$(echo "$VERSION" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1/') 20 | TAGS=${TAGS},${IMAGE_NAME}:$(echo "$VERSION" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1.\2/') 21 | TAGS=${TAGS},${IMAGE_NAME}:$(echo "$VERSION" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1.\2.\3/') 22 | echo ::set-output name=tags::${TAGS} 23 | env: 24 | DOCKERHUB_REGISTRY: ${{ secrets.DOCKERHUB_REGISTRY }} 25 | DOCKERHUB_REPO_NAME: ${{ secrets.DOCKERHUB_REPO_NAME }} 26 | 27 | - name: Set up QEMU 28 | uses: docker/setup-qemu-action@c308fdd69d26ed66f4506ebd74b180abe5362145 29 | 30 | - name: Set up Docker buildx 31 | uses: docker/setup-buildx-action@0d135e0c2fc0dba0729c1a47ecfcf5a3c7f8579e 32 | 33 | - name: Login to DockerHub 34 | uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c 35 | with: 36 | username: ${{ secrets.DOCKERHUB_USERNAME }} 37 | password: ${{ secrets.DOCKERHUB_TOKEN }} 38 | 39 | - name: Build and push 40 | uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f 41 | with: 42 | push: true 43 | tags: ${{ steps.get-tags.outputs.tags }} 44 | -------------------------------------------------------------------------------- /.github/workflows/publish-ecr.yml: -------------------------------------------------------------------------------- 1 | name: publish-ecr 2 | on: 3 | release: 4 | types: 5 | - published 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | id-token: write 11 | contents: read 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f 15 | 16 | - name: Get tags 17 | id: get-tags 18 | run: | 19 | IMAGE_NAME=${AWS_ECR_REGISTRY}/${AWS_ECR_REPO_NAME} 20 | VERSION=${GITHUB_REF/refs\/tags\//} 21 | TAGS=${IMAGE_NAME}:latest 22 | TAGS=${TAGS},${IMAGE_NAME}:$(echo "$VERSION" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1/') 23 | TAGS=${TAGS},${IMAGE_NAME}:$(echo "$VERSION" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1.\2/') 24 | TAGS=${TAGS},${IMAGE_NAME}:$(echo "$VERSION" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)$/\1.\2.\3/') 25 | echo ::set-output name=tags::${TAGS} 26 | env: 27 | AWS_ECR_REGISTRY: ${{ secrets.AWS_ECR_REGISTRY }} 28 | AWS_ECR_REPO_NAME: ${{ secrets.AWS_ECR_REPO_NAME }} 29 | 30 | - name: Set up QEMU 31 | uses: docker/setup-qemu-action@c308fdd69d26ed66f4506ebd74b180abe5362145 32 | 33 | - name: Set up Docker buildx 34 | uses: docker/setup-buildx-action@0d135e0c2fc0dba0729c1a47ecfcf5a3c7f8579e 35 | 36 | - name: Configure AWS credentials 37 | uses: aws-actions/configure-aws-credentials@05b148adc31e091bafbaf404f745055d4d3bc9d2 38 | with: 39 | role-to-assume: ${{ secrets.AWS_ECR_ROLE_ARN }} 40 | aws-region: ${{ secrets.AWS_REGION }} 41 | 42 | - name: Login to ECR 43 | uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c 44 | with: 45 | registry: ${{ secrets.AWS_ECR_REGISTRY }} 46 | 47 | - name: Build and push 48 | uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f 49 | with: 50 | push: true 51 | tags: ${{ steps.get-tags.outputs.tags }} 52 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: release-drafter 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release-draft: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Release draft 11 | uses: release-drafter/release-drafter@fe52e97d262833ae07d05efaf1a239df3f1b5cd4 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/source-map.yml: -------------------------------------------------------------------------------- 1 | name: source-map 2 | on: 3 | release: 4 | types: 5 | - published 6 | jobs: 7 | upload: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f 12 | 13 | - name: Get yarn cache directory path 14 | id: yarn-cache-dir-path 15 | run: echo "::set-output name=dir::$(yarn cache dir)" 16 | 17 | - uses: actions/cache@1a9e2138d905efd099035b49d8b7a3888c653ca8 18 | with: 19 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 20 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 21 | restore-keys: | 22 | ${{ runner.os }}-yarn- 23 | 24 | - name: Install dependencies 25 | run: yarn --frozen-lockfile 26 | 27 | - name: Get version 28 | id: get-version 29 | run: echo ::set-output name=version::${GITHUB_REF/refs\/tags\/v/} 30 | 31 | - name: Upload source map 32 | run: yarn build 33 | env: 34 | SENTRY_PUBLISH_RELEASE: true 35 | SENTRY_AUTO_ATTACH_COMMITS: true 36 | SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} 37 | SENTRY_ORG: ${{ secrets.SENTRY_ORG }} 38 | SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} 39 | SENTRY_RELEASE: ${{ steps.get-version.outputs.version }} 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | run-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f 15 | 16 | - name: Get yarn cache directory path 17 | id: yarn-cache-dir-path 18 | run: echo "::set-output name=dir::$(yarn cache dir)" 19 | 20 | - uses: actions/cache@1a9e2138d905efd099035b49d8b7a3888c653ca8 21 | with: 22 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 23 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 24 | restore-keys: | 25 | ${{ runner.os }}-yarn- 26 | 27 | - name: Install dependencies 28 | run: yarn --frozen-lockfile 29 | 30 | - name: Run test 31 | run: yarn test 32 | 33 | - name: Publish code coverage 34 | uses: paambaati/codeclimate-action@7bcf9e73c0ee77d178e72c0ec69f1a99c1afc1f3 35 | if: ${{ env.CC_TEST_REPORTER_ID != '' }} 36 | env: 37 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | npm-debug.log 6 | 7 | # Nuxt build 8 | .nuxt 9 | 10 | # Nuxt generate 11 | dist 12 | 13 | # Dotenv 14 | /.env* 15 | !.env.sample 16 | 17 | # PWA 18 | sw.* 19 | 20 | # Yarn 21 | yarn-error.log 22 | 23 | # Jest 24 | coverage 25 | 26 | # MacOS 27 | .DS_Store 28 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16.11.1 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.12.0-alpine as builder 2 | ENV WEB_DIR /hackaru 3 | WORKDIR $WEB_DIR 4 | COPY package.json yarn.lock $WEB_DIR/ 5 | RUN apk add --update --no-cache python3 make g++ git && yarn install 6 | COPY . $WEB_DIR 7 | RUN yarn build 8 | 9 | FROM node:16.12.0-alpine 10 | ENV WEB_DIR /hackaru 11 | WORKDIR $WEB_DIR 12 | RUN addgroup hackaru \ 13 | && adduser -s /bin/sh -D -G hackaru hackaru \ 14 | && chown hackaru:hackaru $WEB_DIR 15 | COPY --chown=hackaru:hackaru . $WEB_DIR 16 | COPY --chown=hackaru:hackaru \ 17 | --from=builder \ 18 | $WEB_DIR/node_modules \ 19 | $WEB_DIR/node_modules 20 | COPY --chown=hackaru:hackaru \ 21 | --from=builder \ 22 | $WEB_DIR/.nuxt/dist \ 23 | $WEB_DIR/.nuxt/dist 24 | USER hackaru 25 | CMD ["yarn", "start"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ktmouk 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hackaru-web 2 | 3 | The web server for Hackaru. 4 | An open source and simple time tracking app. 5 | 6 | [](https://codeclimate.com/github/hackaru-app/hackaru-web/maintainability) 7 | [](https://codeclimate.com/github/hackaru-app/hackaru-web/test_coverage) 8 | [](https://opensource.org/licenses/MIT) 9 | 10 | ## Features 11 | 12 | Want to know that you can do with Hackaru? 13 | For more information on the app, please see the main repository [README](https://github.com/hackaru-app/hackaru). 14 | 15 | ## Roles 16 | 17 | The web server provides the UI to a browser. 18 | It also sends user actions to the API server via REST API. 19 | 20 | ## Feedback 21 | 22 | Do you find a bug or would like to submit feature requests? 23 | Please let us know via [Issues](https://github.com/hackaru-app/hackaru/issues). 😉 24 | 25 | ## Quickstart 26 | 27 | You can run Hackaru on your local easily using [docker-compose](https://docs.docker.com/compose/install). 28 | 29 | It's also necessary to run the web server if you want to login to Hackaru on your browser. 30 | Please see the API server [README](https://github.com/hackaru-app/hackaru-api). 31 | 32 | ```sh 33 | # Clone this repository. 34 | git clone git@github.com:hackaru-app/hackaru-web.git 35 | cd hackaru-web 36 | 37 | # Copy and rename env file. 38 | cp .env.sample .env.development 39 | 40 | # Try accessing http://localhost:3333 after execution. 41 | docker-compose -f docker-compose.yml -f docker-compose.dev.yml up 42 | ``` 43 | 44 | ## License 45 | 46 | - [MIT](./LICENSE) 47 | -------------------------------------------------------------------------------- /__tests__/__helpers__/test-id.js: -------------------------------------------------------------------------------- 1 | export default function testId(id) { 2 | return `[data-test-id="${id}"]`; 3 | } 4 | -------------------------------------------------------------------------------- /__tests__/__setups__/config.js: -------------------------------------------------------------------------------- 1 | import { config, RouterLinkStub } from '@vue/test-utils'; 2 | 3 | config.mocks['$t'] = () => ''; 4 | config.mocks['$route'] = { fullPath: '' }; 5 | config.mocks['$toPx'] = (min) => min; 6 | config.mocks['$toMin'] = (px) => px; 7 | config.mocks['localePath'] = (path) => `/en/${path}`; 8 | 9 | config.stubs['NuxtLink'] = RouterLinkStub; 10 | config.stubs['NuxtChild'] = { render: (h) => h }; 11 | config.stubs['ClientOnly'] = { render: (h) => h }; 12 | config.stubs['I18n'] = { render: (h) => h }; 13 | -------------------------------------------------------------------------------- /__tests__/components/atoms/bar-chart.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import BarChart from '~/components/atoms/bar-chart'; 3 | 4 | describe('BarChart', () => { 5 | const $mixpanel = { track: jest.fn() }; 6 | 7 | const factory = () => 8 | shallowMount(BarChart, { 9 | mocks: { 10 | $mixpanel, 11 | }, 12 | propsData: { 13 | isDark: false, 14 | chartData: { 15 | labels: ['Jan', 'Feb', 'Mar'], 16 | datasets: [ 17 | { 18 | label: 'Development', 19 | backgroundColor: '#ff0', 20 | data: [10, 10, 10], 21 | }, 22 | { 23 | label: 'Review', 24 | backgroundColor: '#ff0', 25 | data: [10, 10, 10], 26 | }, 27 | ], 28 | }, 29 | }, 30 | }); 31 | 32 | it('shows tooltip label correctly', () => { 33 | const wrapper = factory(); 34 | const label = wrapper.vm.options.tooltips.callbacks.label; 35 | expect(label({ yLabel: 10 })).toBe('00:10'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__tests__/components/atoms/base-input.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import BaseInput from '~/components/atoms/base-input'; 3 | 4 | describe('BaseInput', () => { 5 | let wrapper; 6 | 7 | const factory = () => shallowMount(BaseInput); 8 | 9 | describe('when enter text', () => { 10 | beforeEach(() => { 11 | wrapper = factory(); 12 | wrapper.setValue('foo'); 13 | }); 14 | 15 | it('emits input', () => { 16 | expect(wrapper.emitted('input')[0][0]).toBe('foo'); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/components/atoms/coach-tooltip.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, createLocalVue } from '@vue/test-utils'; 2 | import CoachTooltip from '~/components/atoms/coach-tooltip'; 3 | 4 | describe('CoachTooltip', () => { 5 | let wrapper; 6 | 7 | jest.useFakeTimers(); 8 | 9 | const localVue = createLocalVue(); 10 | localVue.directive('tooltip', () => {}); 11 | 12 | const factory = () => 13 | shallowMount(CoachTooltip, { 14 | localVue, 15 | propsData: { 16 | name: 'example', 17 | }, 18 | }); 19 | 20 | beforeEach(() => { 21 | localStorage.clear(); 22 | }); 23 | 24 | describe('when user already seen', () => { 25 | beforeEach(() => { 26 | localStorage.setItem('coachTooltip/example', true); 27 | wrapper = factory(); 28 | }); 29 | 30 | it('does not show tooltip', () => { 31 | expect(wrapper.vm.params.show).toBe(false); 32 | }); 33 | }); 34 | 35 | describe('when user does not seen', () => { 36 | beforeEach(() => { 37 | wrapper = factory(); 38 | jest.runOnlyPendingTimers(); 39 | }); 40 | 41 | it('shows tooltip', () => { 42 | expect(wrapper.vm.params.show).toBe(true); 43 | }); 44 | }); 45 | 46 | describe('when hide', () => { 47 | beforeEach(() => { 48 | wrapper = factory(); 49 | wrapper.vm.hide(); 50 | }); 51 | 52 | it('hides tooltip', () => { 53 | expect(wrapper.vm.params.show).toBe(false); 54 | }); 55 | 56 | it('stores hidden flag to local-storage', () => { 57 | expect(localStorage.setItem).toHaveBeenCalledWith( 58 | 'coachTooltip/example', 59 | true 60 | ); 61 | }); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /__tests__/components/atoms/color-scheme.spec.js: -------------------------------------------------------------------------------- 1 | import MatchMediaMock from 'match-media-mock'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import ColorScheme from '~/components/atoms/color-scheme'; 4 | 5 | describe('ColorScheme', () => { 6 | let wrapper; 7 | 8 | const factory = () => shallowMount(ColorScheme); 9 | 10 | beforeEach(() => { 11 | window.matchMedia = MatchMediaMock.create(); 12 | }); 13 | 14 | describe('when scheme changes to dark', () => { 15 | beforeEach(() => { 16 | window.matchMedia.setConfig({ 'prefers-color-scheme': 'light' }); 17 | wrapper = factory(); 18 | window.matchMedia.setConfig({ 'prefers-color-scheme': 'dark' }); 19 | }); 20 | 21 | it('sets isDark to true', () => { 22 | expect(wrapper.vm.isDark).toBe(true); 23 | }); 24 | }); 25 | 26 | describe('when scheme changes to light', () => { 27 | beforeEach(() => { 28 | window.matchMedia.setConfig({ 'prefers-color-scheme': 'dark' }); 29 | wrapper = factory(); 30 | window.matchMedia.setConfig({ 'prefers-color-scheme': 'light' }); 31 | }); 32 | 33 | it('sets isDark to false', () => { 34 | expect(wrapper.vm.isDark).toBe(false); 35 | }); 36 | }); 37 | 38 | describe('when scheme changes to no-preference', () => { 39 | beforeEach(() => { 40 | window.matchMedia.setConfig({ 'prefers-color-scheme': 'dark' }); 41 | wrapper = factory(); 42 | window.matchMedia.setConfig({ 'prefers-color-scheme': 'no-preference' }); 43 | }); 44 | 45 | it('sets isDark to false', () => { 46 | expect(wrapper.vm.isDark).toBe(false); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/components/atoms/doughnut-chart.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import DoughnutChart from '~/components/atoms/doughnut-chart'; 3 | 4 | describe('DoughnutChart', () => { 5 | const $mixpanel = { track: jest.fn() }; 6 | 7 | const factory = () => 8 | shallowMount(DoughnutChart, { 9 | mocks: { 10 | $mixpanel, 11 | }, 12 | propsData: { 13 | chartData: { 14 | labels: ['Review', 'Development'], 15 | datasets: [{ data: [50, 50], backgroundColor: ['#ff0', '#0ff'] }], 16 | }, 17 | }, 18 | }); 19 | 20 | it('shows tooltip label correctly', () => { 21 | const wrapper = factory(); 22 | const label = wrapper.vm.options.tooltips.callbacks.label; 23 | expect(label({ index: 0 }, { datasets: [{ data: [60] }] })).toBe('01:00'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /__tests__/components/atoms/dragger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import Dragger from '~/components/atoms/dragger'; 3 | import testId from '~/__tests__/__helpers__/test-id'; 4 | 5 | describe('Dragger', () => { 6 | let wrapper; 7 | 8 | const factory = () => shallowMount(Dragger); 9 | const dragEvent = (x, y) => ({ 10 | e: { preventDefault: () => {} }, 11 | distance: { x, y }, 12 | }); 13 | 14 | describe('when drag', () => { 15 | beforeEach(() => { 16 | wrapper = factory(); 17 | wrapper.find(testId('drag-drop')).vm.$emit('start', dragEvent()); 18 | }); 19 | 20 | it('emits start', () => { 21 | expect(wrapper.emitted('start')).toBeTruthy(); 22 | }); 23 | }); 24 | 25 | describe('when drag', () => { 26 | beforeEach(() => { 27 | wrapper = factory(); 28 | wrapper.find(testId('drag-drop')).vm.$emit('start', dragEvent()); 29 | wrapper.find(testId('drag-drop')).vm.$emit('move', dragEvent(70, 80)); 30 | }); 31 | 32 | it('emits moving', () => { 33 | expect(wrapper.emitted('moving')).toBeTruthy(); 34 | }); 35 | 36 | it('emits update:left', () => { 37 | expect(wrapper.emitted('update:left')[0][0]).toBe(70); 38 | }); 39 | 40 | it('emits update:top', () => { 41 | expect(wrapper.emitted('update:top')[0][0]).toBe(80); 42 | }); 43 | }); 44 | 45 | describe('when drop', () => { 46 | beforeEach(() => { 47 | wrapper = factory(); 48 | wrapper.find(testId('drag-drop')).vm.$emit('start', dragEvent()); 49 | wrapper.find(testId('drag-drop')).vm.$emit('move', dragEvent(70, 80)); 50 | wrapper.find(testId('drag-drop')).vm.$emit('end', dragEvent()); 51 | }); 52 | 53 | it('emits end', () => { 54 | expect(wrapper.emitted('end')).toBeTruthy(); 55 | }); 56 | }); 57 | 58 | describe('when drop but not moved', () => { 59 | beforeEach(() => { 60 | wrapper = factory(); 61 | wrapper.find(testId('drag-drop')).vm.$emit('start', dragEvent(0, 0)); 62 | wrapper.find(testId('drag-drop')).vm.$emit('end', dragEvent(0, 0)); 63 | }); 64 | 65 | it('emits cancel', () => { 66 | expect(wrapper.emitted('cancel')).toBeTruthy(); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /__tests__/components/atoms/resizer.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import Resizer from '~/components/atoms/resizer'; 3 | 4 | describe('Resizer', () => { 5 | let wrapper; 6 | 7 | const factory = () => shallowMount(Resizer); 8 | const dragEvent = (x, y) => ({ 9 | e: { preventDefault: () => {} }, 10 | distance: { x, y }, 11 | }); 12 | 13 | describe('when drag', () => { 14 | beforeEach(() => { 15 | wrapper = factory(); 16 | wrapper 17 | .findComponent({ ref: 'drag-drop' }) 18 | .vm.$emit('start', dragEvent()); 19 | }); 20 | 21 | it('emits start', () => { 22 | expect(wrapper.emitted('start')).toBeTruthy(); 23 | }); 24 | }); 25 | 26 | describe('when drag', () => { 27 | beforeEach(() => { 28 | wrapper = factory(); 29 | wrapper 30 | .findComponent({ ref: 'drag-drop' }) 31 | .vm.$emit('start', dragEvent()); 32 | wrapper 33 | .findComponent({ ref: 'drag-drop' }) 34 | .vm.$emit('move', dragEvent(0, 80)); 35 | }); 36 | 37 | it('emits resizing', () => { 38 | expect(wrapper.emitted('resizing')).toBeTruthy(); 39 | }); 40 | 41 | it('emits update:height', () => { 42 | expect(wrapper.emitted('update:height')[0][0]).toBe(80); 43 | }); 44 | }); 45 | 46 | describe('when drop', () => { 47 | beforeEach(() => { 48 | wrapper = factory(); 49 | wrapper 50 | .findComponent({ ref: 'drag-drop' }) 51 | .vm.$emit('start', dragEvent()); 52 | wrapper 53 | .findComponent({ ref: 'drag-drop' }) 54 | .vm.$emit('move', dragEvent(70, 80)); 55 | wrapper.findComponent({ ref: 'drag-drop' }).vm.$emit('end', dragEvent()); 56 | }); 57 | 58 | it('emits end', () => { 59 | expect(wrapper.emitted('end')).toBeTruthy(); 60 | }); 61 | }); 62 | 63 | describe('when drop but not moved', () => { 64 | beforeEach(() => { 65 | wrapper = factory(); 66 | wrapper 67 | .findComponent({ ref: 'drag-drop' }) 68 | .vm.$emit('start', dragEvent(0, 0)); 69 | wrapper 70 | .findComponent({ ref: 'drag-drop' }) 71 | .vm.$emit('end', dragEvent(0, 0)); 72 | }); 73 | 74 | it('emits cancel', () => { 75 | expect(wrapper.emitted('cancel')).toBeTruthy(); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /__tests__/components/atoms/ticker.spec.js: -------------------------------------------------------------------------------- 1 | import MockDate from 'mockdate'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import Ticker from '~/components/atoms/ticker'; 4 | 5 | describe('Ticker', () => { 6 | let wrapper; 7 | 8 | MockDate.set('2019-01-31T01:23:45'); 9 | 10 | const $timer = { stop: jest.fn() }; 11 | const factory = () => 12 | shallowMount(Ticker, { 13 | propsData: { startedAt: '2019-01-31T02:23:45' }, 14 | mocks: { $timer }, 15 | }); 16 | 17 | describe('when stoppedAt is empty', () => { 18 | beforeEach(async () => { 19 | wrapper = factory(); 20 | await wrapper.setProps({ 21 | startedAt: '2019-01-31T00:23:45', 22 | stoppedAt: undefined, 23 | }); 24 | wrapper.vm.updateDuration(); 25 | }); 26 | 27 | it('sets duration correctly', () => { 28 | expect(wrapper.find('time').text()).toBe('01:00:00'); 29 | }); 30 | }); 31 | 32 | describe('when stoppedAt is defined', () => { 33 | beforeEach(async () => { 34 | wrapper = factory(); 35 | await wrapper.setProps({ 36 | startedAt: '2019-01-31T02:23:45', 37 | stoppedAt: '2019-01-31T04:23:45', 38 | }); 39 | wrapper.vm.updateDuration(); 40 | }); 41 | 42 | it('sets duration correctly', () => { 43 | expect(wrapper.find('time').text()).toBe('02:00:00'); 44 | }); 45 | 46 | it('stop timer', () => { 47 | expect($timer.stop).toHaveBeenCalledWith('updateDuration'); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /__tests__/components/atoms/window-scroll.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import WindowScroll from '~/components/atoms/window-scroll'; 3 | 4 | describe('WindowScroll', () => { 5 | let wrapper; 6 | 7 | jest.useFakeTimers(); 8 | 9 | const factory = () => mount(WindowScroll, { attachTo: document.body }); 10 | 11 | describe('when scroll', () => { 12 | beforeEach(() => { 13 | wrapper = factory(); 14 | window.dispatchEvent(new CustomEvent('scroll')); 15 | window.dispatchEvent(new CustomEvent('scroll')); 16 | window.dispatchEvent(new CustomEvent('scroll')); 17 | }); 18 | 19 | it('emits start only once', () => { 20 | expect(wrapper.emitted('start').length).toBe(1); 21 | }); 22 | 23 | it('emits scroll', () => { 24 | expect(wrapper.emitted('scroll').length).toBe(3); 25 | }); 26 | }); 27 | 28 | describe('when stop scrolling', () => { 29 | beforeEach(() => { 30 | wrapper = factory(); 31 | window.dispatchEvent(new CustomEvent('scroll')); 32 | jest.runOnlyPendingTimers(); 33 | }); 34 | 35 | it('emits end', () => { 36 | expect(wrapper.emitted('end')).toBeTruthy(); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /__tests__/components/molecules/base-select.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import BaseSelect from '~/components/molecules/base-select'; 3 | 4 | describe('BaseSelect', () => { 5 | let wrapper; 6 | 7 | const factory = () => 8 | shallowMount(BaseSelect, { 9 | propsData: { 10 | value: 'apple', 11 | }, 12 | slots: { 13 | default: ` 14 | 15 | 16 | 17 | `, 18 | }, 19 | }); 20 | 21 | describe('when select', () => { 22 | beforeEach(() => { 23 | wrapper = factory(); 24 | wrapper.findAll('option').at(1).setSelected(); 25 | }); 26 | 27 | it('emits change', () => { 28 | expect(wrapper.emitted('change')[0][0]).toBe('orange'); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /__tests__/components/molecules/color-select.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import ColorSelect from '~/components/molecules/color-select'; 3 | 4 | describe('ColorSelect', () => { 5 | let wrapper; 6 | 7 | const factory = () => shallowMount(ColorSelect); 8 | 9 | describe('when select color', () => { 10 | beforeEach(async () => { 11 | wrapper = factory(); 12 | await wrapper.setProps({ colors: ['#ff0', '#f00', '#0ff'] }); 13 | wrapper.findAll('button').at(2).trigger('click'); 14 | }); 15 | 16 | it('emits update:value', () => { 17 | expect(wrapper.emitted('update:value')[0]).toEqual(['#0ff']); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/components/molecules/datetime-picker.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import DatetimePicker from '~/components/molecules/datetime-picker'; 3 | import { formatISO, parseISO } from 'date-fns'; 4 | import testId from '~/__tests__/__helpers__/test-id'; 5 | 6 | describe('DatetimePicker', () => { 7 | let wrapper; 8 | 9 | const factory = () => shallowMount(DatetimePicker); 10 | 11 | describe('when input value is invalid', () => { 12 | beforeEach(() => { 13 | wrapper = factory(); 14 | wrapper.find(testId('date')).setValue('foo'); 15 | wrapper.find(testId('time')).setValue('bar'); 16 | }); 17 | 18 | it('emits input with undefined', () => { 19 | expect(wrapper.emitted('input')[0]).toEqual([undefined]); 20 | expect(wrapper.emitted('input')[1]).toEqual([undefined]); 21 | }); 22 | }); 23 | 24 | describe('when input date is valid', () => { 25 | beforeEach(async () => { 26 | wrapper = factory(); 27 | await wrapper.setProps({ value: '2019-03-03T11:22:33' }); 28 | wrapper.find(testId('date')).setValue('2019-01-01'); 29 | }); 30 | 31 | it('emits input with datetime', () => { 32 | expect(wrapper.emitted('input')[0]).toEqual([ 33 | formatISO(parseISO('2019-01-01T11:22:33')), 34 | ]); 35 | }); 36 | }); 37 | 38 | describe('when input time is valid', () => { 39 | beforeEach(async () => { 40 | wrapper = factory(); 41 | await wrapper.setProps({ value: '2019-03-03T11:22:33' }); 42 | wrapper.find(testId('time')).setValue('22:33:44'); 43 | }); 44 | 45 | it('emits input with datetime', () => { 46 | expect(wrapper.emitted('input')[0]).toEqual([ 47 | formatISO(parseISO('2019-03-03T22:33:44')), 48 | ]); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /__tests__/components/molecules/delighted.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import Delighted from '~/components/molecules/delighted'; 4 | 5 | describe('Delighted', () => { 6 | const $store = new Store({}); 7 | const $config = {}; 8 | 9 | beforeEach(() => { 10 | window.delighted = { 11 | survey: jest.fn(), 12 | }; 13 | }); 14 | 15 | const factory = () => 16 | shallowMount(Delighted, { 17 | mocks: { 18 | $config, 19 | $store, 20 | $i18n: { 21 | locale: 'en', 22 | }, 23 | $loadScript: () => {}, 24 | }, 25 | }); 26 | 27 | describe('when config has delightedToken', () => { 28 | beforeEach(() => { 29 | $store.getters['auth/userId'] = 1; 30 | $config.delightedToken = 'token'; 31 | factory(); 32 | }); 33 | 34 | it('shows survey', () => { 35 | expect(window.delighted.survey).toHaveBeenCalledWith({ 36 | name: 1, 37 | email: '1@hackaru.app', 38 | properties: { 39 | locale: 'en', 40 | }, 41 | }); 42 | }); 43 | }); 44 | 45 | describe('when config does not have delightedToken', () => { 46 | beforeEach(() => { 47 | $config.delightedToken = undefined; 48 | factory(); 49 | }); 50 | 51 | it('does not show survey', () => { 52 | expect(window.delighted.survey).not.toHaveBeenCalled(); 53 | }); 54 | }); 55 | 56 | describe('when window.delighted is undefined', () => { 57 | let wrapper; 58 | 59 | beforeEach(() => { 60 | window.delighted = undefined; 61 | $config.delightedToken = 'token'; 62 | wrapper = factory(); 63 | }); 64 | 65 | it('does not throw error', async () => { 66 | expect(wrapper.vm.survey).not.toThrow(); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /__tests__/components/molecules/locale-select.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import LocaleSelect from '~/components/molecules/locale-select'; 4 | 5 | describe('LocaleSelect', () => { 6 | let factory; 7 | let wrapper; 8 | 9 | const $store = new Store({}); 10 | const setLocale = jest.fn(); 11 | 12 | beforeEach(() => { 13 | factory = () => 14 | shallowMount(LocaleSelect, { 15 | mocks: { 16 | $store, 17 | $i18n: { 18 | setLocale, 19 | locale: 'en', 20 | locales: [ 21 | { 22 | code: 'en', 23 | name: 'English', 24 | }, 25 | { 26 | code: 'ja', 27 | name: '日本語', 28 | }, 29 | ], 30 | }, 31 | }, 32 | }); 33 | }); 34 | 35 | it('has selected value correctly', () => { 36 | expect(factory().findComponent({ ref: 'base-select' }).vm.value).toEqual( 37 | 'English' 38 | ); 39 | }); 40 | 41 | describe('when select and user is logged in', () => { 42 | beforeEach(() => { 43 | $store.getters['auth/loggedIn'] = true; 44 | wrapper = factory(); 45 | wrapper.findComponent({ ref: 'base-select' }).vm.$emit('change', 'ja'); 46 | }); 47 | 48 | it('dispatches user/update', () => { 49 | expect($store.dispatch).toHaveBeenCalledWith('user/update', { 50 | locale: 'ja', 51 | }); 52 | }); 53 | }); 54 | 55 | describe('when select and user is not logged in', () => { 56 | beforeEach(() => { 57 | $store.getters['auth/loggedIn'] = false; 58 | wrapper = factory(); 59 | wrapper.findComponent({ ref: 'base-select' }).vm.$emit('change', 'ja'); 60 | }); 61 | 62 | it('sets locale', () => { 63 | expect(setLocale).toHaveBeenCalledWith('ja'); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /__tests__/components/molecules/tabs.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import Tabs from '~/components/molecules/tabs'; 3 | 4 | describe('Tabs', () => { 5 | let wrapper; 6 | 7 | const factory = () => 8 | shallowMount(Tabs, { 9 | propsData: { 10 | items: ['Home', 'Reports', 'Calendar'], 11 | }, 12 | }); 13 | 14 | describe('when click item', () => { 15 | beforeEach(() => { 16 | wrapper = factory(); 17 | wrapper.findAll('li').at(1).trigger('click'); 18 | }); 19 | 20 | it('emits change', () => { 21 | expect(wrapper.emitted('change')[0][0]).toBe(1); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/components/molecules/toast.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import { Store } from 'vuex-mock-store'; 3 | import Toast from '~/components/molecules/toast'; 4 | import testId from '~/__tests__/__helpers__/test-id'; 5 | 6 | describe('Toast', () => { 7 | let wrapper; 8 | 9 | jest.useFakeTimers(); 10 | 11 | const $store = new Store({ 12 | getters: { 13 | 'toast/message': { 14 | text: 'message', 15 | type: 'success', 16 | rand: 12345, 17 | duration: 500, 18 | }, 19 | }, 20 | }); 21 | 22 | const factory = () => 23 | shallowMount(Toast, { 24 | mocks: { 25 | $store, 26 | }, 27 | }); 28 | 29 | describe('when change message', () => { 30 | beforeEach(() => { 31 | wrapper = factory(); 32 | $store.getters['toast/message'] = { 33 | text: 'new message', 34 | type: 'success', 35 | rand: 456789, 36 | duration: 500, 37 | }; 38 | }); 39 | 40 | it('shows new message', async () => { 41 | await wrapper.vm.$nextTick(); 42 | expect(wrapper.find(testId('content')).text()).toBe('new message'); 43 | expect(wrapper.find(testId('content')).exists()).toBe(true); 44 | }); 45 | 46 | it('hides toast delayed', async () => { 47 | await jest.runOnlyPendingTimers(); 48 | expect(wrapper.find(testId('content')).exists()).toBe(false); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /__tests__/components/organisms/calendar-ruler.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import CalendarRuler from '~/components/organisms/calendar-ruler'; 3 | 4 | describe('CalendarRuler', () => { 5 | let wrapper; 6 | 7 | const factory = () => 8 | shallowMount(CalendarRuler, { 9 | propsData: { 10 | top: 90, 11 | color: '#ff0', 12 | }, 13 | }); 14 | 15 | describe('when showTime is true', () => { 16 | beforeEach(() => { 17 | wrapper = factory(); 18 | wrapper.setProps({ showTime: true }); 19 | }); 20 | 21 | it('shows time correcly', () => { 22 | expect(wrapper.find('time').text()).toBe('01:30'); 23 | }); 24 | }); 25 | 26 | describe('when showTime is false', () => { 27 | beforeEach(() => { 28 | wrapper = factory(); 29 | wrapper.setProps({ showTime: false }); 30 | }); 31 | 32 | it('hides time', () => { 33 | expect(wrapper.find('time').exists()).toBe(false); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /__tests__/components/organisms/loop-slider.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import LoopSlider from '~/components/organisms/loop-slider'; 3 | import testId from '~/__tests__/__helpers__/test-id'; 4 | 5 | describe('LoopSlider', () => { 6 | let wrapper; 7 | 8 | jest.useFakeTimers(); 9 | 10 | const dragEvent = (x, y) => ({ 11 | e: { preventDefault: () => {} }, 12 | distance: { x, y }, 13 | }); 14 | 15 | const factory = () => 16 | shallowMount(LoopSlider, { 17 | slots: { 18 | default: '
Content
', 19 | }, 20 | }); 21 | 22 | describe('when scroll window', () => { 23 | beforeEach(() => { 24 | wrapper = factory(); 25 | wrapper.find(testId('window-scroll')).vm.$emit('scroll'); 26 | }); 27 | 28 | it('disable slider', () => { 29 | expect(wrapper.findComponent({ ref: 'drag-drop' }).props().enabled).toBe( 30 | false 31 | ); 32 | }); 33 | }); 34 | 35 | describe('when scroll window', () => { 36 | beforeEach(() => { 37 | wrapper = factory(); 38 | wrapper.find(testId('window-scroll')).vm.$emit('end'); 39 | }); 40 | 41 | it('enable slider', () => { 42 | expect(wrapper.findComponent({ ref: 'drag-drop' }).props().enabled).toBe( 43 | true 44 | ); 45 | }); 46 | }); 47 | 48 | describe('when left-swiped', () => { 49 | beforeEach(() => { 50 | wrapper = factory(); 51 | wrapper 52 | .findComponent({ ref: 'drag-drop' }) 53 | .vm.$emit('end', dragEvent(200, 0)); 54 | jest.runOnlyPendingTimers(); 55 | }); 56 | 57 | it('emits slide-left', () => { 58 | expect(wrapper.emitted('slide-left')).toBeTruthy(); 59 | }); 60 | }); 61 | 62 | describe('when right-swiped', () => { 63 | beforeEach(() => { 64 | wrapper = factory(); 65 | wrapper 66 | .findComponent({ ref: 'drag-drop' }) 67 | .vm.$emit('end', dragEvent(-200, 0)); 68 | jest.runOnlyPendingTimers(); 69 | }); 70 | 71 | it('emits slide-right', () => { 72 | expect(wrapper.emitted('slide-right')).toBeTruthy(); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /__tests__/components/organisms/pwa-popover.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import PwaPopover from '~/components/organisms/pwa-popover'; 3 | import testId from '~/__tests__/__helpers__/test-id'; 4 | 5 | describe('PwaPopover', () => { 6 | let wrapper; 7 | 8 | const $platform = { 9 | isIOS: () => false, 10 | isPWA: () => false, 11 | }; 12 | 13 | const factory = () => 14 | shallowMount(PwaPopover, { 15 | stubs: ['i18n', 'v-popover'], 16 | mocks: { $platform }, 17 | }); 18 | 19 | beforeEach(() => { 20 | localStorage.clear(); 21 | }); 22 | 23 | describe('when user platform is iOS', () => { 24 | beforeEach(() => { 25 | $platform.isIOS = () => true; 26 | $platform.isPWA = () => false; 27 | wrapper = factory(); 28 | }); 29 | 30 | it('shows popover', () => { 31 | expect(wrapper.find(testId('popover')).attributes('open')).toBeTruthy(); 32 | }); 33 | }); 34 | 35 | describe('when user platform is not iOS', () => { 36 | beforeEach(() => { 37 | $platform.isIOS = () => false; 38 | $platform.isPWA = () => false; 39 | wrapper = factory(); 40 | }); 41 | 42 | it('shows popover', () => { 43 | expect(wrapper.find(testId('popover')).attributes('open')).toBeFalsy(); 44 | }); 45 | }); 46 | 47 | describe('when user platform is iOS but already use PWA', () => { 48 | beforeEach(() => { 49 | $platform.isIOS = () => true; 50 | $platform.isPWA = () => true; 51 | wrapper = factory(); 52 | }); 53 | 54 | it('hides popover', () => { 55 | expect(wrapper.find(testId('popover')).attributes('open')).toBeFalsy(); 56 | }); 57 | }); 58 | 59 | describe('when platform is iOS but popover already closed', () => { 60 | beforeEach(() => { 61 | $platform.isIOS = () => true; 62 | $platform.isPWA = () => false; 63 | localStorage.setItem('pwaPopover', true); 64 | wrapper = factory(); 65 | }); 66 | 67 | it('hides popover', () => { 68 | expect(wrapper.find(testId('popover')).attributes('open')).toBeFalsy(); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /__tests__/components/organisms/setting-email-editor.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import SettingEmailEditor from '~/components/organisms/setting-email-editor'; 4 | import testId from '~/__tests__/__helpers__/test-id'; 5 | 6 | describe('SettingEmailEditor', () => { 7 | let wrapper; 8 | 9 | const $store = new Store({}); 10 | const factory = () => 11 | shallowMount(SettingEmailEditor, { 12 | mocks: { 13 | $store, 14 | }, 15 | }); 16 | 17 | beforeEach(() => { 18 | $store.reset(); 19 | }); 20 | 21 | describe('when clicks submit-button', () => { 22 | beforeEach(() => { 23 | global.confirm = () => true; 24 | wrapper = factory(); 25 | wrapper.find(testId('email')).vm.$emit('input', 'example@example.com'); 26 | wrapper.find(testId('current-password')).vm.$emit('input', 'password'); 27 | wrapper.find('form').trigger('submit.prevent'); 28 | }); 29 | 30 | it('dispatches auth/changeEmail', () => { 31 | expect($store.dispatch).toHaveBeenCalledWith('auth/changeEmail', { 32 | email: 'example@example.com', 33 | currentPassword: 'password', 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__tests__/components/organisms/setting-logout-button.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import SettingLogoutButton from '~/components/organisms/setting-logout-button'; 4 | import testId from '~/__tests__/__helpers__/test-id'; 5 | 6 | describe('SettingLogoutButton', () => { 7 | let wrapper; 8 | 9 | const $ga = { event: jest.fn() }; 10 | const $mixpanel = { reset: jest.fn() }; 11 | const $store = new Store({}); 12 | 13 | const factory = () => 14 | shallowMount(SettingLogoutButton, { 15 | mocks: { 16 | $ga, 17 | $mixpanel, 18 | $store, 19 | }, 20 | }); 21 | 22 | beforeEach(() => { 23 | $store.reset(); 24 | }); 25 | 26 | describe('when click logout-button', () => { 27 | beforeEach(() => { 28 | global.confirm = () => true; 29 | wrapper = factory(); 30 | wrapper.find(testId('logout-button')).vm.$emit('click'); 31 | }); 32 | 33 | it('dispatches auth/logout', () => { 34 | expect($store.dispatch).toHaveBeenCalledWith('auth/logout'); 35 | }); 36 | 37 | it('resets mixpanel properties', () => { 38 | expect($mixpanel.reset).toHaveBeenCalled(); 39 | }); 40 | 41 | it('sends ga event', () => { 42 | expect($ga.event).toHaveBeenCalledWith({ 43 | eventCategory: 'Accounts', 44 | eventAction: 'logout', 45 | }); 46 | }); 47 | }); 48 | 49 | describe('when click logout-button but confirm is false', () => { 50 | beforeEach(() => { 51 | global.confirm = () => false; 52 | wrapper = factory(); 53 | wrapper.find(testId('logout-button')).vm.$emit('click'); 54 | }); 55 | 56 | it('does not dispatch ', () => { 57 | expect($store.dispatch).not.toHaveBeenCalled(); 58 | }); 59 | 60 | it('does not reset mixpanel properties', () => { 61 | expect($mixpanel.reset).not.toHaveBeenCalled(); 62 | }); 63 | 64 | it('does not send ga event', () => { 65 | expect($ga.event).not.toHaveBeenCalledWith(); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /__tests__/components/organisms/setting-password-editor.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import SettingPasswordEditor from '~/components/organisms/setting-password-editor'; 4 | import testId from '~/__tests__/__helpers__/test-id'; 5 | 6 | describe('SettingPasswordEditor', () => { 7 | let wrapper; 8 | 9 | const $store = new Store({}); 10 | const factory = () => 11 | shallowMount(SettingPasswordEditor, { 12 | mocks: { 13 | $store, 14 | }, 15 | }); 16 | 17 | beforeEach(() => { 18 | $store.reset(); 19 | }); 20 | 21 | describe('click submit-button', () => { 22 | beforeEach(() => { 23 | global.confirm = () => true; 24 | wrapper = factory(); 25 | wrapper.find(testId('current-password')).vm.$emit('input', 'current'); 26 | wrapper.find(testId('password')).vm.$emit('input', 'password'); 27 | wrapper 28 | .find(testId('password-confirmation')) 29 | .vm.$emit('input', 'confirmation'); 30 | wrapper.find('form').trigger('submit.prevent'); 31 | }); 32 | 33 | it('dispatches auth/changePassword', () => { 34 | expect($store.dispatch).toHaveBeenCalledWith('auth/changePassword', { 35 | password: 'password', 36 | currentPassword: 'current', 37 | passwordConfirmation: 'confirmation', 38 | }); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /__tests__/components/organisms/setting-start-day-select.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import SettingStartDaySelect from '~/components/organisms/setting-start-day-select'; 4 | import testId from '~/__tests__/__helpers__/test-id'; 5 | 6 | describe('SettingstartDaySelect', () => { 7 | let wrapper; 8 | 9 | const $store = new Store({ 10 | getters: { 11 | 'user/startDay': 0, 12 | }, 13 | }); 14 | 15 | const factory = () => 16 | shallowMount(SettingStartDaySelect, { 17 | mocks: { 18 | $store, 19 | }, 20 | }); 21 | 22 | beforeEach(() => { 23 | $store.reset(); 24 | }); 25 | 26 | describe('when select', () => { 27 | beforeEach(() => { 28 | wrapper = factory(); 29 | wrapper.find(testId('base-select')).vm.$emit('change', 1); 30 | }); 31 | 32 | it('changes user/update', () => { 33 | expect($store.dispatch).toHaveBeenCalledWith('user/update', { 34 | startDay: 1, 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/components/organisms/setting-time-zone-select.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import SettingTimeZoneSelect from '~/components/organisms/setting-time-zone-select'; 4 | import testId from '~/__tests__/__helpers__/test-id'; 5 | 6 | describe('SettingTimeZoneSelect', () => { 7 | let wrapper; 8 | 9 | const $store = new Store({ 10 | getters: { 11 | 'user/timeZone': 'Etc/UTC', 12 | }, 13 | }); 14 | 15 | const factory = () => 16 | shallowMount(SettingTimeZoneSelect, { 17 | mocks: { 18 | $store, 19 | }, 20 | }); 21 | 22 | beforeEach(() => { 23 | $store.reset(); 24 | }); 25 | 26 | describe('when select', () => { 27 | beforeEach(() => { 28 | wrapper = factory(); 29 | wrapper.find(testId('base-select')).vm.$emit('change', 'Asia/Tokyo'); 30 | }); 31 | 32 | it('changes user/update', () => { 33 | expect($store.dispatch).toHaveBeenCalledWith('user/update', { 34 | timeZone: 'Asia/Tokyo', 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/layouts/default.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import MockDate from 'mockdate'; 4 | import Default from '~/layouts/default'; 5 | 6 | describe('Default', () => { 7 | MockDate.set('2019-01-31T01:23:45'); 8 | 9 | const $store = new Store({ 10 | getters: { 11 | 'activities/getWorkingActivities': () => [], 12 | }, 13 | }); 14 | 15 | const factory = () => 16 | shallowMount(Default, { 17 | stubs: ['nuxt', 'client-only'], 18 | mocks: { $store }, 19 | }); 20 | 21 | beforeEach(() => { 22 | $store.reset(); 23 | }); 24 | 25 | it('dispatches user/fetch', () => { 26 | factory(); 27 | expect($store.dispatch).toHaveBeenCalledWith('user/fetch'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/pages/password-reset/edit.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import Edit from '~/pages/password-reset/edit'; 4 | import testId from '~/__tests__/__helpers__/test-id'; 5 | 6 | describe('Edit', () => { 7 | let wrapper; 8 | 9 | const $store = new Store(); 10 | const factory = () => 11 | shallowMount(Edit, { 12 | mocks: { 13 | $store, 14 | $route: { 15 | query: { 16 | user_id: 1, 17 | token: 'token', 18 | }, 19 | }, 20 | }, 21 | }); 22 | 23 | describe('when click submit-button', () => { 24 | beforeEach(() => { 25 | wrapper = factory(); 26 | wrapper.find(testId('password')).vm.$emit('input', 'password'); 27 | wrapper 28 | .find(testId('password-confirmation')) 29 | .vm.$emit('input', 'confirmation'); 30 | wrapper.find('form').trigger('submit.prevent'); 31 | }); 32 | 33 | it('dispatches auth/resetPassword', () => { 34 | expect($store.dispatch).toHaveBeenCalledWith('auth/resetPassword', { 35 | id: 1, 36 | password: 'password', 37 | passwordConfirmation: 'confirmation', 38 | token: 'token', 39 | }); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /__tests__/pages/password-reset/index.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import Index from '~/pages/password-reset/index'; 4 | import testId from '~/__tests__/__helpers__/test-id'; 5 | 6 | describe('Index', () => { 7 | let wrapper; 8 | 9 | const $store = new Store({}); 10 | const factory = () => 11 | shallowMount(Index, { 12 | mocks: { 13 | $store, 14 | }, 15 | }); 16 | 17 | beforeEach(() => { 18 | $store.reset(); 19 | }); 20 | 21 | describe('when click submit-button', () => { 22 | beforeEach(() => { 23 | wrapper = factory(); 24 | wrapper.find(testId('email')).vm.$emit('input', 'example@example.com'); 25 | wrapper.find('form').trigger('submit.prevent'); 26 | }); 27 | 28 | it('dispatches auth/sendPasswordResetEmail', () => { 29 | expect($store.dispatch).toHaveBeenCalledWith( 30 | 'auth/sendPasswordResetEmail', 31 | { 32 | email: 'example@example.com', 33 | redirectUrl: 'http://localhost/en/password-reset/edit', 34 | } 35 | ); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /__tests__/pages/reports/csv.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import Csv from '~/pages/reports/csv'; 4 | import { parseISO } from 'date-fns'; 5 | import fileSaver from 'file-saver'; 6 | 7 | describe('Csv', () => { 8 | const $ga = { event: jest.fn() }; 9 | const $store = new Store(); 10 | 11 | const factory = () => 12 | shallowMount(Csv, { 13 | mocks: { 14 | $ga, 15 | $store, 16 | $route: { 17 | query: { 18 | start: parseISO('2019-01-31T00:00:00'), 19 | end: parseISO('2019-01-31T23:59:59.999'), 20 | }, 21 | }, 22 | }, 23 | }); 24 | 25 | beforeEach(() => { 26 | $store.reset(); 27 | }); 28 | 29 | describe('when mounted', () => { 30 | beforeEach(() => { 31 | $store.dispatch.mockReturnValue('example,example'); 32 | fileSaver.saveAs = jest.fn(); 33 | factory(); 34 | }); 35 | 36 | it('dispatches reports/fetchCsv', () => { 37 | expect($store.dispatch).toHaveBeenLastCalledWith('reports/fetchCsv', { 38 | start: parseISO('2019-01-31T00:00:00'), 39 | end: parseISO('2019-01-31T23:59:59.999'), 40 | }); 41 | }); 42 | 43 | it('saves csv', () => { 44 | expect(fileSaver.saveAs).toHaveBeenCalledWith(new Blob(), 'report.csv'); 45 | }); 46 | 47 | it('sends ga event', () => { 48 | expect($ga.event).toHaveBeenCalledWith({ 49 | eventCategory: 'CsvReports', 50 | eventAction: 'export', 51 | }); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /__tests__/pages/reports/pdf.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import Pdf from '~/pages/reports/pdf'; 4 | import { parseISO } from 'date-fns'; 5 | import fileSaver from 'file-saver'; 6 | 7 | describe('Pdf', () => { 8 | const $ga = { event: jest.fn() }; 9 | const $store = new Store(); 10 | 11 | const factory = () => 12 | shallowMount(Pdf, { 13 | mocks: { 14 | $ga, 15 | $store, 16 | $route: { 17 | query: { 18 | start: parseISO('2019-01-31T00:00:00'), 19 | end: parseISO('2019-01-31T23:59:59.999'), 20 | }, 21 | }, 22 | }, 23 | }); 24 | 25 | beforeEach(() => { 26 | $store.reset(); 27 | }); 28 | 29 | describe('when mounted', () => { 30 | beforeEach(() => { 31 | $store.dispatch.mockReturnValue('%PDF-'); 32 | fileSaver.saveAs = jest.fn(); 33 | factory(); 34 | }); 35 | 36 | it('dispatches reports/fetchPdf', () => { 37 | expect($store.dispatch).toHaveBeenLastCalledWith('reports/fetchPdf', { 38 | start: parseISO('2019-01-31T00:00:00'), 39 | end: parseISO('2019-01-31T23:59:59.999'), 40 | }); 41 | }); 42 | 43 | it('saves pdf', () => { 44 | expect(fileSaver.saveAs).toHaveBeenCalledWith(new Blob(), 'report.pdf'); 45 | }); 46 | 47 | it('sends ga event', () => { 48 | expect($ga.event).toHaveBeenCalledWith({ 49 | eventCategory: 'PdfReports', 50 | eventAction: 'export', 51 | }); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /__tests__/pages/settings.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import Settings from '~/pages/settings'; 3 | import testId from '~/__tests__/__helpers__/test-id'; 4 | 5 | describe('Settings', () => { 6 | let wrapper; 7 | 8 | const $router = { push: jest.fn() }; 9 | const factory = () => 10 | shallowMount(Settings, { 11 | mocks: { 12 | $router, 13 | $route: { path: '/' }, 14 | }, 15 | }); 16 | 17 | describe('when change selected tab', () => { 18 | beforeEach(() => { 19 | wrapper = factory(); 20 | wrapper.find(testId('tabs')).vm.$emit('change', 1); 21 | }); 22 | 23 | it('moves to selected page', () => { 24 | expect($router.push).toHaveBeenCalledWith('./integrations'); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/pages/settings/integrations.spec.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'vuex-mock-store'; 2 | import { shallowMount } from '@vue/test-utils'; 3 | import Integrations from '~/pages/settings/integrations'; 4 | import testId from '~/__tests__/__helpers__/test-id'; 5 | 6 | describe('Integrations', () => { 7 | let wrapper; 8 | 9 | delete window.location; 10 | window.location = { assign: jest.fn() }; 11 | 12 | const $ga = { event: jest.fn() }; 13 | const $mixpanel = { track: jest.fn() }; 14 | const $store = new Store({ 15 | getters: { 16 | 'activity-calendar/calendarUrl': 'https://example.com', 17 | }, 18 | }); 19 | 20 | const factory = () => 21 | shallowMount(Integrations, { 22 | mocks: { 23 | $ga, 24 | $mixpanel, 25 | $store, 26 | }, 27 | }); 28 | 29 | beforeEach(() => { 30 | $store.reset(); 31 | }); 32 | 33 | describe('when click calendar button', () => { 34 | beforeEach(() => { 35 | $store.dispatch.mockReturnValue(true); 36 | wrapper = factory(); 37 | wrapper.find(testId('other-calendar-button')).vm.$emit('click'); 38 | }); 39 | 40 | it('dispatches activity-calendar/createUrl', () => { 41 | expect($store.dispatch).toHaveBeenCalledWith( 42 | 'activity-calendar/createUrl' 43 | ); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /__tests__/store/activity-calendar/actions.spec.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | import { actions } from '~/store/activity-calendar'; 4 | 5 | describe('Actions', () => { 6 | const mock = new MockAdapter(axios); 7 | 8 | beforeEach(() => { 9 | mock.reset(); 10 | actions.$api = axios; 11 | }); 12 | 13 | describe('when dispatch createUrl', () => { 14 | const commit = jest.fn(); 15 | 16 | beforeEach(() => { 17 | mock.onPut('/v1/activity_calendar').replyOnce(200, {}); 18 | actions.createUrl({ commit }); 19 | }); 20 | 21 | it('commits SET_TOKEN_AND_USER_ID', () => { 22 | expect(commit).toHaveBeenCalledWith('SET_TOKEN_AND_USER_ID', {}); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /__tests__/store/activity-calendar/getters.spec.js: -------------------------------------------------------------------------------- 1 | import { getters } from '~/store/activity-calendar'; 2 | 3 | describe('Getters', () => { 4 | let result; 5 | 6 | describe('when call calendarUrl', () => { 7 | const state = { 8 | baseUrl: 'https://localhost', 9 | token: 'token', 10 | userId: 1, 11 | }; 12 | 13 | beforeEach(() => { 14 | result = getters.calendarUrl(state); 15 | }); 16 | 17 | it('returns url', () => { 18 | expect(result).toBe( 19 | 'https://localhost/v1/activity_calendar?token=token&user_id=1' 20 | ); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/store/activity-calendar/mutations.spec.js: -------------------------------------------------------------------------------- 1 | import { mutations } from '~/store/activity-calendar'; 2 | 3 | describe('Mutations', () => { 4 | describe('when commit SET_TOKEN_AND_USER_ID', () => { 5 | const state = { 6 | baseUrl: '', 7 | token: '', 8 | userId: '', 9 | }; 10 | 11 | beforeEach(() => { 12 | mutations.$config = { 13 | axios: { 14 | browserBaseURL: 'https://localhost', 15 | }, 16 | }; 17 | mutations['SET_TOKEN_AND_USER_ID'](state, { 18 | token: 'token', 19 | userId: 1, 20 | }); 21 | }); 22 | 23 | it('sets token', () => { 24 | expect(state.token).toBe('token'); 25 | }); 26 | 27 | it('sets userId', () => { 28 | expect(state.userId).toBe(1); 29 | }); 30 | 31 | it('sets baseUrl', () => { 32 | expect(state.baseUrl).toBe('https://localhost'); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /__tests__/store/applications/actions.spec.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | import { actions } from '~/store/applications'; 4 | import { application } from '~/schemas'; 5 | 6 | describe('Actions', () => { 7 | const mock = new MockAdapter(axios); 8 | 9 | beforeEach(() => { 10 | mock.reset(); 11 | actions.$api = axios; 12 | }); 13 | 14 | describe('when dispatch fetch', () => { 15 | const dispatch = jest.fn(); 16 | 17 | beforeEach(() => { 18 | mock.onGet('/oauth/authorized_applications').replyOnce(200, {}); 19 | actions.fetch({ dispatch }); 20 | }); 21 | 22 | it('dispatches entities/merge', () => { 23 | expect(dispatch).toHaveBeenCalledWith( 24 | 'entities/merge', 25 | { 26 | json: {}, 27 | schema: [application], 28 | }, 29 | { root: true } 30 | ); 31 | }); 32 | }); 33 | 34 | describe('when dispatch delete', () => { 35 | const dispatch = jest.fn(); 36 | 37 | beforeEach(() => { 38 | mock.onDelete('/oauth/authorized_applications/1').replyOnce(200); 39 | actions.delete({ dispatch }, 1); 40 | }); 41 | 42 | it('dispatches entities/delete', () => { 43 | expect(dispatch).toHaveBeenCalledWith( 44 | 'entities/delete', 45 | { name: 'applications', id: 1 }, 46 | { root: true } 47 | ); 48 | }); 49 | 50 | it('requests api', () => { 51 | expect(mock.history.delete.length).toBe(1); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /__tests__/store/applications/getters.spec.js: -------------------------------------------------------------------------------- 1 | import { getters } from '~/store/applications'; 2 | 3 | describe('Getters', () => { 4 | let result; 5 | 6 | describe('when call all', () => { 7 | const rootGetters = { 8 | 'entities/getEntities': () => [], 9 | }; 10 | 11 | beforeEach(() => { 12 | result = getters.all({}, {}, {}, rootGetters); 13 | }); 14 | 15 | it('returns result', () => { 16 | expect(result).toEqual([]); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/store/auth/getters.spec.js: -------------------------------------------------------------------------------- 1 | import MockDate from 'mockdate'; 2 | import { getters } from '~/store/auth'; 3 | 4 | describe('Getters', () => { 5 | let result; 6 | 7 | MockDate.set('2019-01-31T01:23:45'); 8 | 9 | beforeEach(() => { 10 | localStorage.clear(); 11 | }); 12 | 13 | describe('when call email', () => { 14 | const state = { email: 'example@example.com' }; 15 | 16 | beforeEach(() => { 17 | result = getters.email(state); 18 | }); 19 | 20 | it('returns email', () => { 21 | expect(result).toBe('example@example.com'); 22 | }); 23 | }); 24 | 25 | describe('when call userId', () => { 26 | const state = { id: 1 }; 27 | 28 | beforeEach(() => { 29 | result = getters.userId(state); 30 | }); 31 | 32 | it('returns id', () => { 33 | expect(result).toEqual(1); 34 | }); 35 | }); 36 | 37 | describe('when call loggedIn', () => { 38 | const state = { loggedIn: true }; 39 | 40 | beforeEach(() => { 41 | result = getters.loggedIn(state); 42 | }); 43 | 44 | it('returns true', () => { 45 | expect(result).toBe(true); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /__tests__/store/auth/mutations.spec.js: -------------------------------------------------------------------------------- 1 | import { mutations } from '~/store/auth'; 2 | 3 | describe('Mutations', () => { 4 | describe('when commit LOGIN', () => { 5 | const state = { id: undefined, email: '' }; 6 | 7 | beforeEach(() => { 8 | mutations['LOGIN'](state, { 9 | id: 1, 10 | email: 'example@example.com', 11 | }); 12 | }); 13 | 14 | it('sets userId', () => { 15 | expect(state.id).toBe(1); 16 | }); 17 | 18 | it('sets email', () => { 19 | expect(state.email).toBe('example@example.com'); 20 | }); 21 | 22 | it('sets loggedIn to true', () => { 23 | expect(state.loggedIn).toBe(true); 24 | }); 25 | }); 26 | 27 | describe('when commit LOGOUT', () => { 28 | const state = { 29 | id: 1, 30 | email: 'example@example.com', 31 | loggedIn: true, 32 | }; 33 | 34 | beforeEach(() => { 35 | mutations['LOGOUT'](state); 36 | }); 37 | 38 | it('clears userId', () => { 39 | expect(state.id).toBeUndefined(); 40 | }); 41 | 42 | it('clears email', () => { 43 | expect(state.email).toBe(''); 44 | }); 45 | 46 | it('sets loggedIn to false', () => { 47 | expect(state.loggedIn).toBe(false); 48 | }); 49 | }); 50 | 51 | describe('when commit SET_EMAIL', () => { 52 | const state = { 53 | email: '', 54 | }; 55 | 56 | beforeEach(() => { 57 | mutations['SET_EMAIL'](state, 'example@example.com'); 58 | }); 59 | 60 | it('sets email', () => { 61 | expect(state.email).toBe('example@example.com'); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /__tests__/store/entities/getters.spec.js: -------------------------------------------------------------------------------- 1 | import { getters } from '~/store/entities'; 2 | import { schema } from 'normalizr'; 3 | 4 | describe('Getters', () => { 5 | let result; 6 | 7 | const user = new schema.Entity('users', { 8 | comment: new schema.Entity('comments'), 9 | }); 10 | 11 | describe('when call getEntities', () => { 12 | const state = { 13 | data: { 14 | users: { 15 | 1: { 16 | comment: 2, 17 | id: 1, 18 | name: 'John', 19 | }, 20 | }, 21 | comments: { 22 | 2: { 23 | id: 2, 24 | description: 'Hello', 25 | }, 26 | }, 27 | }, 28 | }; 29 | 30 | beforeEach(() => { 31 | result = getters.getEntities(state)('users', [user]); 32 | }); 33 | 34 | it('returns getEntities entities', () => { 35 | expect(result).toEqual([ 36 | { 37 | id: 1, 38 | name: 'John', 39 | comment: { 40 | id: 2, 41 | description: 'Hello', 42 | }, 43 | }, 44 | ]); 45 | }); 46 | }); 47 | 48 | describe('when call getEntities but entities is empty', () => { 49 | const state = { 50 | data: {}, 51 | }; 52 | 53 | beforeEach(() => { 54 | result = getters.getEntities(state)('users', [user]); 55 | }); 56 | 57 | it('returns empty array', () => { 58 | expect(result).toEqual([]); 59 | }); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /__tests__/store/entities/mutations.spec.js: -------------------------------------------------------------------------------- 1 | import { mutations } from '~/store/entities'; 2 | 3 | describe('Mutations', () => { 4 | describe('when commit MERGE_ENTITIES', () => { 5 | const entities = { 6 | users: { 7 | 1: { 8 | id: 1, 9 | name: 'Jonh', 10 | tags: ['orange'], 11 | }, 12 | }, 13 | }; 14 | 15 | const state = { entities }; 16 | 17 | beforeEach(() => { 18 | mutations['MERGE_ENTITIES'](state, { 19 | users: { 20 | 1: { 21 | name: 'John', 22 | age: 20, 23 | tags: ['apple'], 24 | }, 25 | 2: { 26 | id: 2, 27 | name: 'Bob', 28 | }, 29 | }, 30 | }); 31 | }); 32 | 33 | it('overwrite existing array', () => { 34 | expect(state.data.users[1].tags).toEqual(['apple']); 35 | }); 36 | 37 | it('overwrite existing property', () => { 38 | expect(state.data.users[1].name).toBe('John'); 39 | }); 40 | 41 | it('add new property', () => { 42 | expect(state.data.users[1].age).toBe(20); 43 | }); 44 | 45 | it('add new user', () => { 46 | expect(state.data.users[2]).toEqual({ id: 2, name: 'Bob' }); 47 | }); 48 | 49 | it('is not shallow copy', () => { 50 | expect(entities).not.toBe(state.data); 51 | }); 52 | }); 53 | 54 | describe('when commit DELETE_ENTITY', () => { 55 | const state = { 56 | data: { 57 | users: { 58 | 1: { 59 | id: 1, 60 | name: 'Jonh', 61 | tags: ['orange'], 62 | }, 63 | }, 64 | }, 65 | }; 66 | 67 | beforeEach(() => { 68 | mutations['DELETE_ENTITY'](state, { name: 'users', id: 1 }); 69 | }); 70 | 71 | it('remove entity', () => { 72 | expect(state.data.users).toEqual({}); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /__tests__/store/oauth/getters.spec.js: -------------------------------------------------------------------------------- 1 | import { getters } from '~/store/oauth'; 2 | 3 | describe('Getters', () => { 4 | let result; 5 | 6 | describe('when call client', () => { 7 | const state = { client: {} }; 8 | 9 | beforeEach(() => { 10 | result = getters.client(state); 11 | }); 12 | 13 | it('returns client', () => { 14 | expect(result).toEqual({}); 15 | }); 16 | }); 17 | 18 | describe('when call decided', () => { 19 | const state = { decided: false }; 20 | 21 | beforeEach(() => { 22 | result = getters.decided(state); 23 | }); 24 | 25 | it('returns decided', () => { 26 | expect(result).toEqual(false); 27 | }); 28 | }); 29 | 30 | describe('when call redirectUri', () => { 31 | const state = { redirectUri: '' }; 32 | 33 | beforeEach(() => { 34 | result = getters.redirectUri(state); 35 | }); 36 | 37 | it('returns redirectUri', () => { 38 | expect(result).toEqual(''); 39 | }); 40 | }); 41 | 42 | describe('when call redirectQuery', () => { 43 | const state = { redirectQuery: {} }; 44 | 45 | beforeEach(() => { 46 | result = getters.redirectQuery(state); 47 | }); 48 | 49 | it('returns redirectQuery', () => { 50 | expect(result).toEqual({}); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /__tests__/store/projects/getters.spec.js: -------------------------------------------------------------------------------- 1 | import { getters } from '~/store/projects'; 2 | 3 | describe('Getters', () => { 4 | let result; 5 | 6 | describe('when call all', () => { 7 | const rootGetters = { 8 | 'entities/getEntities': () => [ 9 | { name: 'B' }, 10 | { name: 'C' }, 11 | { name: 'A' }, 12 | ], 13 | }; 14 | 15 | beforeEach(() => { 16 | result = getters.all({}, {}, {}, rootGetters); 17 | }); 18 | 19 | it('returns projects sorted by name', () => { 20 | expect(result).toEqual([{ name: 'A' }, { name: 'B' }, { name: 'C' }]); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /__tests__/store/suggestions/actions.spec.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | import { actions } from '~/store/suggestions'; 4 | 5 | describe('Actions', () => { 6 | const mock = new MockAdapter(axios); 7 | 8 | beforeEach(() => { 9 | mock.reset(); 10 | actions.$api = axios; 11 | }); 12 | 13 | describe('when dispatch fetch', () => { 14 | const commit = jest.fn(); 15 | 16 | beforeEach(() => { 17 | mock 18 | .onGet('/v1/suggestions', { 19 | params: { 20 | q: 'query', 21 | limit: 30, 22 | }, 23 | }) 24 | .replyOnce(200, {}); 25 | actions.fetch({ commit }, 'query'); 26 | }); 27 | 28 | it('commits SET_SUGGESTIONS', () => { 29 | expect(commit).toHaveBeenCalledWith('SET_SUGGESTIONS', {}); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /__tests__/store/suggestions/getters.spec.js: -------------------------------------------------------------------------------- 1 | import { getters } from '~/store/suggestions'; 2 | 3 | describe('Getters', () => { 4 | let result; 5 | 6 | describe('when call all', () => { 7 | const state = { 8 | data: {}, 9 | }; 10 | 11 | beforeEach(() => { 12 | result = getters.all(state); 13 | }); 14 | 15 | it('returns result', () => { 16 | expect(result).toEqual({}); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/store/suggestions/mutations.spec.js: -------------------------------------------------------------------------------- 1 | import { mutations } from '~/store/suggestions'; 2 | 3 | describe('Mutations', () => { 4 | describe('when commit SET_SUGGESTIONS', () => { 5 | const state = { data: [] }; 6 | 7 | beforeEach(() => { 8 | mutations['SET_SUGGESTIONS'](state, [ 9 | { project: undefined, description: 'Review' }, 10 | ]); 11 | }); 12 | 13 | it('sets suggestions', () => { 14 | expect(state.data).toEqual([ 15 | { project: undefined, description: 'Review' }, 16 | ]); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/store/toast/actions.spec.js: -------------------------------------------------------------------------------- 1 | import { actions } from '~/store/toast'; 2 | 3 | describe('Actions', () => { 4 | const commit = jest.fn(); 5 | 6 | describe('when dispatch success', () => { 7 | beforeEach(() => { 8 | actions.success({ commit }, 'message'); 9 | }); 10 | 11 | it('shows toast', () => { 12 | expect(commit).toHaveBeenCalledWith('SHOW_SUCCESS', 'message'); 13 | }); 14 | }); 15 | 16 | describe('when error has message in error_description', () => { 17 | beforeEach(() => { 18 | actions.error( 19 | { commit }, 20 | { 21 | response: { 22 | data: { 23 | error_description: 'message', 24 | }, 25 | }, 26 | } 27 | ); 28 | }); 29 | 30 | it('shows response.data.error-description', () => { 31 | expect(commit).toHaveBeenCalledWith('SHOW_ERROR', 'message'); 32 | }); 33 | }); 34 | 35 | describe('when error has message in data.message', () => { 36 | beforeEach(() => { 37 | actions.error( 38 | { commit }, 39 | { 40 | response: { 41 | data: { 42 | message: 'message', 43 | }, 44 | }, 45 | } 46 | ); 47 | }); 48 | 49 | it('shows response.data.message', () => { 50 | expect(commit).toHaveBeenCalledWith('SHOW_ERROR', 'message'); 51 | }); 52 | }); 53 | 54 | describe('when error has message', () => { 55 | beforeEach(() => { 56 | actions.error({ commit }, { message: 'message' }); 57 | }); 58 | 59 | it('shows message', () => { 60 | expect(commit).toHaveBeenCalledWith('SHOW_ERROR', 'message'); 61 | }); 62 | }); 63 | 64 | describe('when error does not have message', () => { 65 | beforeEach(() => { 66 | actions.error({ commit }, undefined); 67 | }); 68 | 69 | it('does not show toast', () => { 70 | expect(commit).not.toHaveBeenCalled(); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /__tests__/store/toast/getters.spec.js: -------------------------------------------------------------------------------- 1 | import { getters } from '~/store/toast'; 2 | 3 | describe('Getters', () => { 4 | let result; 5 | 6 | describe('when call message', () => { 7 | const state = { 8 | text: 'text', 9 | type: 'success', 10 | rand: 123, 11 | duration: 3000, 12 | }; 13 | 14 | beforeEach(() => { 15 | result = getters.message(state); 16 | }); 17 | 18 | it('returns correctly', () => { 19 | expect(result).toEqual({ 20 | text: 'text', 21 | type: 'success', 22 | rand: 123, 23 | duration: 3000, 24 | }); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /__tests__/store/toast/mutations.spec.js: -------------------------------------------------------------------------------- 1 | import { mutations } from '~/store/toast'; 2 | 3 | describe('Mutations', () => { 4 | describe('when commit SHOW_ERROR', () => { 5 | const state = { rand: 0 }; 6 | 7 | beforeEach(() => { 8 | mutations['SHOW_ERROR'](state, 'description'); 9 | }); 10 | 11 | it('sets text', () => { 12 | expect(state.text).toBe('description'); 13 | }); 14 | 15 | it('sets type', () => { 16 | expect(state.type).toBe('error'); 17 | }); 18 | 19 | it('sets rand', () => { 20 | expect(state.rand).not.toBe(0); 21 | }); 22 | 23 | it('sets duration', () => { 24 | expect(state.duration).toBe(5000); 25 | }); 26 | }); 27 | 28 | describe('when commit SHOW_SUCCESS', () => { 29 | const state = { rand: 0 }; 30 | 31 | beforeEach(() => { 32 | mutations['SHOW_SUCCESS'](state, 'description'); 33 | }); 34 | 35 | it('sets text', () => { 36 | expect(state.text).toBe('description'); 37 | }); 38 | 39 | it('sets type', () => { 40 | expect(state.type).toBe('success'); 41 | }); 42 | 43 | it('sets rand', () => { 44 | expect(state.rand).not.toBe(0); 45 | }); 46 | 47 | it('sets duration', () => { 48 | expect(state.duration).toBe(3000); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /__tests__/store/users/actions.spec.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import MockAdapter from 'axios-mock-adapter'; 3 | import { actions } from '~/store/user'; 4 | 5 | describe('Actions', () => { 6 | let result; 7 | 8 | const mock = new MockAdapter(axios); 9 | 10 | beforeEach(() => { 11 | mock.reset(); 12 | actions.$api = axios; 13 | }); 14 | 15 | describe('when dispatch fetch', () => { 16 | const commit = jest.fn(); 17 | 18 | beforeEach(async () => { 19 | mock.onGet('/v1/user').replyOnce(200, { 20 | timeZone: 'Asia/Tokyo', 21 | receiveWeekReport: true, 22 | receiveMonthReport: true, 23 | }); 24 | result = await actions.fetch({ commit }); 25 | }); 26 | 27 | it('commits SET_USER', () => { 28 | expect(commit).toHaveBeenCalledWith('SET_USER', { 29 | timeZone: 'Asia/Tokyo', 30 | receiveWeekReport: true, 31 | receiveMonthReport: true, 32 | }); 33 | }); 34 | 35 | it('returns true', () => { 36 | expect(result).toBe(true); 37 | }); 38 | }); 39 | 40 | describe('when dispatch update', () => { 41 | const commit = jest.fn(); 42 | 43 | beforeEach(async () => { 44 | mock 45 | .onPut('/v1/user', { 46 | user: { 47 | timeZone: 'Asia/Tokyo', 48 | receiveWeekReport: true, 49 | receiveMonthReport: true, 50 | }, 51 | }) 52 | .replyOnce(200, { 53 | timeZone: 'Asia/Tokyo', 54 | receiveWeekReport: true, 55 | receiveMonthReport: true, 56 | }); 57 | result = await actions.update( 58 | { commit }, 59 | { 60 | timeZone: 'Asia/Tokyo', 61 | receiveWeekReport: true, 62 | receiveMonthReport: true, 63 | } 64 | ); 65 | }); 66 | 67 | it('commits SET_USER', () => { 68 | expect(commit).toHaveBeenCalledWith('SET_USER', { 69 | timeZone: 'Asia/Tokyo', 70 | receiveWeekReport: true, 71 | receiveMonthReport: true, 72 | }); 73 | }); 74 | 75 | it('returns true', () => { 76 | expect(result).toBe(true); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /__tests__/store/users/getters.spec.js: -------------------------------------------------------------------------------- 1 | import { getters } from '~/store/user'; 2 | 3 | describe('Getters', () => { 4 | let result; 5 | 6 | describe('when call timeZone', () => { 7 | const state = { timeZone: 'Asia/Tokyo' }; 8 | 9 | beforeEach(() => { 10 | result = getters.timeZone(state); 11 | }); 12 | 13 | it('returns time zone', () => { 14 | expect(result).toBe('Asia/Tokyo'); 15 | }); 16 | }); 17 | 18 | describe('when call receiveWeekReport', () => { 19 | const state = { receiveWeekReport: true }; 20 | 21 | beforeEach(() => { 22 | result = getters.receiveWeekReport(state); 23 | }); 24 | 25 | it('returns receiveWeekReport', () => { 26 | expect(result).toBe(true); 27 | }); 28 | }); 29 | 30 | describe('when call receiveMonthReport', () => { 31 | const state = { receiveMonthReport: true }; 32 | 33 | beforeEach(() => { 34 | result = getters.receiveMonthReport(state); 35 | }); 36 | 37 | it('returns receiveMonthReport', () => { 38 | expect(result).toBe(true); 39 | }); 40 | }); 41 | 42 | describe('when call startDay', () => { 43 | const state = { startDay: 0 }; 44 | 45 | beforeEach(() => { 46 | result = getters.startDay(state); 47 | }); 48 | 49 | it('returns startDay', () => { 50 | expect(result).toBe(0); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /__tests__/store/users/mutations.spec.js: -------------------------------------------------------------------------------- 1 | import { mutations } from '~/store/user'; 2 | 3 | describe('Mutations', () => { 4 | describe('when commit SET_USER', () => { 5 | const state = { 6 | timeZone: 'Etc/UTC', 7 | receiveWeekReport: false, 8 | receiveMonthReport: false, 9 | startDay: 0, 10 | }; 11 | 12 | beforeEach(() => { 13 | mutations.$i18n = { 14 | setLocale: jest.fn(), 15 | }; 16 | mutations['SET_USER'](state, { 17 | timeZone: 'Asia/Tokyo', 18 | receiveWeekReport: true, 19 | receiveMonthReport: true, 20 | locale: 'ja', 21 | startDay: 1, 22 | }); 23 | }); 24 | 25 | it('sets user data', () => { 26 | expect(state.timeZone).toBe('Asia/Tokyo'); 27 | expect(state.receiveWeekReport).toBe(true); 28 | expect(state.receiveMonthReport).toBe(true); 29 | expect(state.startDay).toBe(1); 30 | }); 31 | 32 | it('sets locale', () => { 33 | expect(mutations.$i18n.setLocale).toHaveBeenCalledWith('ja'); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 4 | 5 | More information about the usage of this directory in the documentation: 6 | https://nuxtjs.org/guide/assets#webpacked 7 | 8 | **This directory is not required, you can delete it if you don't want to use it.** 9 | -------------------------------------------------------------------------------- /assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/locales/common/scopes.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "scopes": { 4 | "activities:read": "Read your activities", 5 | "activities:write": "Update your activities", 6 | "projects:read": "Read your projects", 7 | "projects:write": "Update your projects", 8 | "suggestions:read": "Read your suggestions", 9 | "user:read": "Read user information excluding authentication" 10 | } 11 | }, 12 | "ja": { 13 | "scopes": { 14 | "activities:read": "アクティビティの読み取り", 15 | "activities:write": "アクティビティの変更", 16 | "projects:read": "プロジェクトの読み取り", 17 | "projects:write": "プロジェクトの変更", 18 | "suggestions:read": "検索候補の読み取り", 19 | "user:read": "認証情報を除くユーザ情報の読み取り" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/activity-day-group.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "today": "Today", 4 | "yesterday": "Yesterday", 5 | "ago": " days", 6 | "later": " days later", 7 | "weeks": [ 8 | "Monday", 9 | "Tuesday", 10 | "Wednesday", 11 | "Thursday", 12 | "Friday", 13 | "Saturday", 14 | "Sunday" 15 | ] 16 | }, 17 | "ja": { 18 | "today": "今日", 19 | "yesterday": "昨日", 20 | "ago": "日前", 21 | "later": "日後", 22 | "weeks": [ 23 | "月曜日", 24 | "火曜日", 25 | "水曜日", 26 | "木曜日", 27 | "金曜日", 28 | "土曜日", 29 | "日曜日" 30 | ] 31 | }, 32 | "zh-CN": { 33 | "today": "今天", 34 | "yesterday": "昨天", 35 | "ago": " 天前", 36 | "later": " 天后", 37 | "weeks": [ 38 | "星期一", 39 | "星期二", 40 | "星期三", 41 | "星期四", 42 | "星期五", 43 | "星期六", 44 | "星期天" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/activity-editor-description.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "description": "Description" 4 | }, 5 | "ja": { 6 | "description": "作業内容や補足を入力" 7 | }, 8 | "zh-CN": { 9 | "description": "描述" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/activity-editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Update Timer", 4 | "project": "Project", 5 | "startedAt": "Started at", 6 | "stoppedAt": "Stopped at", 7 | "update": "Update", 8 | "start": "Start", 9 | "saved": "Timer updated", 10 | "started": "Timer started", 11 | "deleted": "Timer deleted", 12 | "confirms": { 13 | "delete": "Are you sure you want to delete the timer?" 14 | } 15 | }, 16 | "ja": { 17 | "title": "計測を編集", 18 | "startedAt": "開始時間", 19 | "stoppedAt": "終了時間", 20 | "update": "更新", 21 | "start": "開始", 22 | "saved": "計測を更新しました", 23 | "started": "計測を開始しました", 24 | "deleted": "計測を削除しました", 25 | "confirms": { 26 | "delete": "計測を削除しますか?" 27 | } 28 | }, 29 | "zh-CN": { 30 | "title": "更新计时", 31 | "project": "项目", 32 | "startedAt": "开始于", 33 | "stoppedAt": "结束于", 34 | "update": "更新", 35 | "start": "开始", 36 | "saved": "计时已更新", 37 | "started": "计时已开始", 38 | "deleted": "计时已删除", 39 | "confirms": { 40 | "delete": "你确定要删除计时吗?" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/activity-item.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "duplicated": "Timer duplicated", 4 | "duplicate": "Duplicate", 5 | "deleted": "Timer deleted", 6 | "duplicate": "Start timer with same content", 7 | "confirms": { 8 | "delete": "Are you sure you want to delete the timer?" 9 | } 10 | }, 11 | "ja": { 12 | "duplicated": "計測を複製しました", 13 | "duplicate": "複製する", 14 | "deleted": "計測を削除しました", 15 | "duplicate": "同じ内容で開始", 16 | "confirms": { 17 | "delete": "計測を削除しますか?" 18 | } 19 | }, 20 | "zh-CN": { 21 | "duplicated": "计时已重复", 22 | "duplicate": "重复", 23 | "deleted": "计时已删除", 24 | "duplicate": "使用相同内容开始计时", 25 | "confirms": { 26 | "delete": "你确定要删除计时吗?" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/calendar-activity.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "moveByLongPress": "Move by long press" 4 | }, 5 | "ja": { 6 | "moveByLongPress": "長押しで移動" 7 | }, 8 | "zh-CN": { 9 | "moveByLongPress": "长按以移动" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/calendar-day-header.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "weeks": ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] 4 | }, 5 | "ja": { 6 | "weeks": ["月", "火", "水", "木", "金", "土", "日"] 7 | }, 8 | "zh-CN": { 9 | "weeks": ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/date-header.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "day": { 4 | "label": "Day" 5 | }, 6 | "week": { 7 | "label": "Week" 8 | }, 9 | "month": { 10 | "label": "Month" 11 | }, 12 | "year": { 13 | "label": "Year" 14 | } 15 | }, 16 | "ja": { 17 | "day": { 18 | "label": "日" 19 | }, 20 | "week": { 21 | "label": "週" 22 | }, 23 | "month": { 24 | "label": "月" 25 | }, 26 | "year": { 27 | "label": "年" 28 | } 29 | }, 30 | "zh-CN": { 31 | "day": { 32 | "label": "日" 33 | }, 34 | "week": { 35 | "label": "周" 36 | }, 37 | "month": { 38 | "label": "月" 39 | }, 40 | "year": { 41 | "label": "年" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/eol-notification.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "message": "Hackaru service will be terminated on 2022-11-12." 4 | }, 5 | "ja": { 6 | "message": "2022-11-12 をもってHackaruのサービスを終了します。" 7 | }, 8 | "zh-CN": { 9 | "message": "Hackaru 服务会在2022年11月12日终止。" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/project-editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "titles": { 4 | "update": "Edit Project", 5 | "add": "Add project" 6 | }, 7 | "name": "Name", 8 | "color": "Color", 9 | "add": "Add", 10 | "update": "Update", 11 | "confirms": { 12 | "delete": "Are you sure you want to delete the project?" 13 | }, 14 | "added": "Project created", 15 | "updated": "Project updated", 16 | "deleted": "Project deleted" 17 | }, 18 | "ja": { 19 | "titles": { 20 | "update": "プロジェクトを編集", 21 | "add": "プロジェクトを追加" 22 | }, 23 | "name": "名前", 24 | "color": "色", 25 | "add": "追加", 26 | "update": "更新", 27 | "confirms": { 28 | "delete": "プロジェクトを削除しますか?" 29 | }, 30 | "added": "プロジェクトを追加しました", 31 | "updated": "プロジェクトを更新しました", 32 | "deleted": "プロジェクトを削除しました" 33 | }, 34 | "zh-CN": { 35 | "titles": { 36 | "update": "修改项目", 37 | "add": "新增项目" 38 | }, 39 | "name": "名字", 40 | "color": "颜色", 41 | "add": "新增", 42 | "update": "修改", 43 | "confirms": { 44 | "delete": "你确定要删除项目吗?" 45 | }, 46 | "added": "项目已创建", 47 | "updated": "项目已更新", 48 | "deleted": "项目已删除" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/project-list.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Select Project" 4 | }, 5 | "ja": { 6 | "title": "プロジェクトを選択" 7 | }, 8 | "zh-CN": { 9 | "title": "选择项目" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/pwa-popover.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "PWA Ready", 4 | "about": "Tap {0} then 'Add to Home Screen.'" 5 | }, 6 | "ja": { 7 | "title": "PWA を体験してみよう。", 8 | "about": "{0} から \"ホーム画面に追加\" をタップ" 9 | }, 10 | "zh-CN": { 11 | "title": "PWA 已就绪", 12 | "about": "点击 {0} 然后 “添加至主屏幕”" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/report-content.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "total": "Total" 4 | }, 5 | "ja": { 6 | "total": "合計" 7 | }, 8 | "zh-CN": { 9 | "total": "总计" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/setting-delete-account-button.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "modal": { 4 | "title": "Delete Account", 5 | "warning": "Delete your account, activities, and projects. You can't cancel after executed.", 6 | "delete": "Delete", 7 | "currentPassword": "Enter your current password" 8 | }, 9 | "title": "Delete Account", 10 | "deleteButton": "Delete", 11 | "confirms": "Are you sure you want to delete your account?" 12 | }, 13 | "ja": { 14 | "modal": { 15 | "title": "アカウントを削除", 16 | "warning": "アカウント情報、計測記録、プロジェクト等がすべて削除されます。 この操作は実行後、取り消すことはできません。", 17 | "delete": "削除する", 18 | "currentPassword": "続行するには、現在のパスワードを入力" 19 | }, 20 | "title": "アカウントを削除", 21 | "deleteButton": "削除する", 22 | "confirms": "本当にアカウントを削除してよろしいですか?" 23 | }, 24 | "zh-CN": { 25 | "modal": { 26 | "title": "删除账号", 27 | "warning": "删除你的账号、活动和项目。你将不能恢复这些数据。", 28 | "delete": "删除", 29 | "currentPassword": "输入密码" 30 | }, 31 | "title": "删除账号", 32 | "deleteButton": "删除", 33 | "confirms": "你确定想要删除账号吗?" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/setting-email-editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Change Email address", 4 | "email": "Enter new email address", 5 | "password": "Enter password", 6 | "update": "Update", 7 | "changed": "Email address changed" 8 | }, 9 | "ja": { 10 | "title": "Eメールを変更", 11 | "email": "変更後のメールアドレスを入力", 12 | "password": "現在のパスワードを入力", 13 | "update": "変更", 14 | "changed": "メールアドレスを変更しました" 15 | }, 16 | "zh-CN": { 17 | "title": "修改邮箱", 18 | "email": "输入新邮箱", 19 | "password": "输入密码", 20 | "update": "更新", 21 | "changed": "邮箱已修改" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/setting-locale-select.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Locale" 4 | }, 5 | "ja": { 6 | "title": "言語を設定" 7 | }, 8 | "zh-CN": { 9 | "title": "选择语言" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/setting-logout-button.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Logout", 4 | "confirms": "Logout?" 5 | }, 6 | "ja": { 7 | "title": "ログアウト", 8 | "confirms": "ログアウトしますか?" 9 | }, 10 | "zh-CN": { 11 | "title": "登出", 12 | "confirms": "登出?" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/setting-password-editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Change Password", 4 | "currentPassword": "Enter current password", 5 | "password": "Enter new password", 6 | "passwordConfirmation": "Confirm new password", 7 | "update": "Update", 8 | "changed": "Password changed" 9 | }, 10 | "ja": { 11 | "title": "パスワードを変更", 12 | "currentPassword": "現在のパスワードを入力", 13 | "password": "新しいパスワードを入力", 14 | "passwordConfirmation": "新しいパスワードを再入力", 15 | "update": "変更", 16 | "changed": "パスワードを変更しました" 17 | }, 18 | "zh-CN": { 19 | "title": "修改密码", 20 | "currentPassword": "输入当前密码", 21 | "password": "输入新密码", 22 | "passwordConfirmation": "重复新密码", 23 | "update": "更新", 24 | "changed": "密码已更新" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/setting-start-day-select.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "First day of the week", 4 | "changed": "First day of the week changed", 5 | "startDays": [ 6 | "Sunday", 7 | "Monday", 8 | "Tuesday", 9 | "Wednesday", 10 | "Thursday", 11 | "Friday", 12 | "Saturday" 13 | ] 14 | }, 15 | "ja": { 16 | "title": "週の始まり", 17 | "changed": "週の始まりを変更しました", 18 | "startDays": [ 19 | "日曜日", 20 | "月曜日", 21 | "火曜日", 22 | "水曜日", 23 | "木曜日", 24 | "金曜日", 25 | "土曜日" 26 | ] 27 | }, 28 | "zh-CN": { 29 | "title": "每周的第一天", 30 | "changed": "每周的第一天已修改", 31 | "startDays": [ 32 | "星期一", 33 | "星期二", 34 | "星期三", 35 | "星期四", 36 | "星期五", 37 | "星期六", 38 | "星期天" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/setting-time-zone-select.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Timezone", 4 | "changed": "Timezone changed" 5 | }, 6 | "ja": { 7 | "title": "タイムゾーン", 8 | "changed": "タイムゾーンを変更しました" 9 | }, 10 | "zh-CN": { 11 | "title": "时区", 12 | "changed": "修改时区" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /assets/locales/components/organisms/timer-form.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "start": "Start timer", 4 | "stop": "Stop timer", 5 | "description": "What are you working on?", 6 | "updated": "Timer updated!", 7 | "started": "Timer started! Let's start your work!", 8 | "selectProject": "Select a project from here", 9 | "stopped": ["Timer stopped! Nice work! 🎉", "Timer stopped! Great! 🎉"] 10 | }, 11 | "ja": { 12 | "start": "計測を開始", 13 | "stop": "計測を停止", 14 | "description": "何を始めますか? 作業内容や補足を入力できます。", 15 | "updated": "計測中の情報を更新しました!", 16 | "started": "計測を開始しました。作業を始めましょう!", 17 | "selectProject": "ここからプロジェクトを選択", 18 | "stopped": [ 19 | "計測を完了しました!いい感じですね!🎉", 20 | "計測を完了しました!お疲れ様です!🎉" 21 | ] 22 | }, 23 | "zh-CN": { 24 | "start": "开始计时", 25 | "stop": "停止计时", 26 | "description": "你在做什么?", 27 | "updated": "计时已更新!", 28 | "started": "计时已开始! 开始工作吧!", 29 | "selectProject": "在这里选择一个项目", 30 | "stopped": ["计时已结束!🎉", "计时已结束!🎉"] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /assets/locales/pages/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "titles": { 4 | "login": "Log in", 5 | "signUp": "Sign up" 6 | }, 7 | "email": "Email", 8 | "password": "Password", 9 | "passwordConfirmation": "Confirm password", 10 | "forgot": "Forgot your password?", 11 | "login": "Login", 12 | "signUp": "Create", 13 | "or": { 14 | "signUp": "Sign-up", 15 | "login": "Login" 16 | }, 17 | "agreement": "I agree to {0}.", 18 | "termOfServiceAndPrivacyPolicy": "Terms and Policy" 19 | }, 20 | "ja": { 21 | "titles": { 22 | "login": "ログイン", 23 | "signUp": "新規登録" 24 | }, 25 | "email": "メールアドレス", 26 | "password": "パスワード", 27 | "passwordConfirmation": "パスワードを再入力", 28 | "forgot": "パスワードを忘れましたか?", 29 | "login": "ログイン", 30 | "signUp": "登録", 31 | "or": { 32 | "signUp": "アカウントを登録", 33 | "login": "ログイン" 34 | }, 35 | "agreement": "{0} に同意。", 36 | "termOfServiceAndPrivacyPolicy": "利用規約とプライバシーポリシー" 37 | }, 38 | "zh-CN": { 39 | "titles": { 40 | "login": "登录", 41 | "signUp": "注册" 42 | }, 43 | "email": "邮箱", 44 | "password": "密码", 45 | "passwordConfirmation": "重复密码", 46 | "forgot": "忘记密码?", 47 | "login": "登录", 48 | "signUp": "注册", 49 | "or": { 50 | "signUp": "注册", 51 | "login": "登录" 52 | }, 53 | "agreement": "我同意 {0}.", 54 | "termOfServiceAndPrivacyPolicy": "使用条款和隐私政策" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /assets/locales/pages/calendar.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "format": "MM/yyyy" 4 | }, 5 | "ja": { 6 | "format": "yyyy/MM" 7 | }, 8 | "zh-CN": { 9 | "format": "yyyy/MM" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/locales/pages/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "empty": "Welcome. There is no activity this week. Let's start the timer!" 4 | }, 5 | "ja": { 6 | "empty": "ようこそ。過去一週間の計測はまだありません。計測を始めましょう!" 7 | }, 8 | "zh-CN": { 9 | "empty": "欢迎。过去一周没有活动记录。开始记录活动把!" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /assets/locales/pages/oauth/authorize.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Authorize {name}", 4 | "logged-in": "Your account is {email}.", 5 | "decide": "Authorize {name} to use your account?", 6 | "empty-scope": "This application has no required controls.", 7 | "allow": "Accept", 8 | "deny": "Reject" 9 | }, 10 | "ja": { 11 | "title": "{name} の許可", 12 | "logged-in": "{email} のアカウントでログイン中です。", 13 | "decide": "{name} に以下の操作を許可しますか?", 14 | "empty-scope": "このアプリが必要としている操作はありません。", 15 | "allow": "許可", 16 | "deny": "拒否" 17 | }, 18 | "zh-CN": { 19 | "title": "授权 {name}", 20 | "logged-in": "你的账号是 {email}.", 21 | "decide": "授权 {name} 使用你的账号?", 22 | "empty-scope": "该应用没有请求权限。", 23 | "allow": "允许", 24 | "deny": "拒绝" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /assets/locales/pages/password-reset/edit.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Reset your password", 4 | "password": "New password", 5 | "passwordConfirmation": "Confirm new password", 6 | "reset": "Reset", 7 | "success": "Password reset successfully" 8 | }, 9 | "ja": { 10 | "title": "パスワードを再設定", 11 | "password": "新しいパスワードを入力", 12 | "passwordConfirmation": "新しいパスワードを再入力", 13 | "reset": "再設定", 14 | "success": "パスワードの再設定が完了しました" 15 | }, 16 | "zh-CN": { 17 | "title": "重置密码", 18 | "password": "新密码", 19 | "passwordConfirmation": "重复密码", 20 | "reset": "重置", 21 | "success": "密码重置成功" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /assets/locales/pages/password-reset/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Reset your password", 4 | "about": "We will send you a mail to reset your password.", 5 | "email": "Enter your email address", 6 | "send": "Send", 7 | "sent": "We sent you email to reset your password" 8 | }, 9 | "ja": { 10 | "title": "パスワードを再設定", 11 | "about": "パスワードを再設定するための案内メールを送信します。", 12 | "email": "登録時のメールアドレスを入力", 13 | "send": "送信", 14 | "sent": "再設定のためのメールを送信しました" 15 | }, 16 | "zh-CN": { 17 | "title": "重置密码", 18 | "about": "我们会发送一封邮件来重置密码。", 19 | "email": "输入邮箱", 20 | "send": "发送", 21 | "sent": "邮件已发送" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /assets/locales/pages/reports/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "day": { 4 | "format": "dd/MM/yyyy" 5 | }, 6 | "week": { 7 | "format": "MM/yyyy" 8 | }, 9 | "month": { 10 | "format": "MM/yyyy" 11 | }, 12 | "year": { 13 | "format": "yyyy" 14 | }, 15 | "moveToNextPage": "Move to the next period by swiping" 16 | }, 17 | "ja": { 18 | "day": { 19 | "format": "yyyy/MM/dd" 20 | }, 21 | "week": { 22 | "format": "yyyy/MM" 23 | }, 24 | "month": { 25 | "format": "yyyy/MM" 26 | }, 27 | "year": { 28 | "format": "yyyy" 29 | }, 30 | "moveToNextPage": "左右にスワイプで別の日付へ" 31 | }, 32 | "zh-CN": { 33 | "day": { 34 | "format": "yyyy/MM/dd" 35 | }, 36 | "week": { 37 | "format": "yyyy/MM" 38 | }, 39 | "month": { 40 | "format": "yyyy/MM" 41 | }, 42 | "year": { 43 | "format": "yyyy" 44 | }, 45 | "moveToNextPage": "左右滑动以调整日期" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /assets/locales/pages/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "account": "Account", 4 | "notifications": "Notifications", 5 | "integrations": "Integrations", 6 | "applications": "Authorized apps" 7 | }, 8 | "ja": { 9 | "account": "アカウント設定", 10 | "notifications": "配信設定", 11 | "integrations": "連携機能", 12 | "applications": "認証済みアプリ" 13 | }, 14 | "zh-CN": { 15 | "account": "账号", 16 | "notifications": "通知", 17 | "integrations": "集成", 18 | "applications": "授权应用" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /assets/locales/pages/settings/applications.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Apps", 4 | "confirms": { 5 | "delete": "Are you sure you want to delete the application?" 6 | }, 7 | "labels": { 8 | "scopes": "This application can" 9 | }, 10 | "deleted": "Application deleted", 11 | "empty": "No applications." 12 | }, 13 | "ja": { 14 | "title": "認証済みアプリ", 15 | "confirms": { 16 | "delete": "アプリケーションの認証を取り消しますか?" 17 | }, 18 | "labels": { 19 | "scopes": "使用可能な操作" 20 | }, 21 | "deleted": "削除しました", 22 | "empty": "認証済みのアプリはありません" 23 | }, 24 | "zh-CN": { 25 | "title": "应用", 26 | "confirms": { 27 | "delete": "你确定要该删除应用吗?" 28 | }, 29 | "labels": { 30 | "scopes": "该应用可以" 31 | }, 32 | "deleted": "应用已删除", 33 | "empty": "暂无应用。" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /assets/locales/pages/settings/integrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Link with Calendar", 4 | "description": "You can check timers on your calendar.\nSynchronization can take up to a day.", 5 | "create": "Link with Google calendar or Other calendar", 6 | "modal": { 7 | "howTo": "Add the above URL to your calendar application. Please refer to the help of each calendar application for how to register. (Please be careful not to share the URL to other persons.)", 8 | "title": "Link with calendar", 9 | "example": "If you want to use Google calendar, Please open ", 10 | "help": "Help page ", 11 | "link": "and refer to \"Use a link to add a public calendar\" section." 12 | } 13 | }, 14 | "ja": { 15 | "title": "カレンダーと連携", 16 | "description": "計測した記録をお使いのカレンダー上に表示できます。同期まで最大1日ほど時間がかかる場合があります。", 17 | "create": "Googleカレンダー / 他のカレンダーと連携", 18 | "modal": { 19 | "howTo": "上記のURLをお使いのカレンダーにご登録ください。登録方法は各カレンダーアプリのヘルプ等を参照ください。(URLは他人に共有しないようご注意ください)", 20 | "example": "例: Googleカレンダーの場合、", 21 | "help": "ヘルプページ", 22 | "link": "の「リンクを使用して一般公開のカレンダーを追加する」をご参考ください。", 23 | "title": "カレンダーと連携" 24 | } 25 | }, 26 | "zh-CN": { 27 | "title": "链接日历", 28 | "description": "你可以在你的日历上检查定时器。\n同步可能需要一天时间。", 29 | "create": "与 Google Calender 或其他日历链接", 30 | "modal": { 31 | "howTo": "将上述 URL 添加到你的日历应用程序中。关于如何注册,请参考每个日历应用程序的帮助。(请注意不要将该网址分享给其他人)", 32 | "example": "如果你想要使用 Google Calender,请打开 ", 33 | "help": "帮助页面", 34 | "link": "并参考“使用链接添加公开日历”小节。", 35 | "title": "链接日历" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /assets/locales/pages/settings/notification.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "title": "Notification", 4 | "receiveWeekReport": "Receive weekly report", 5 | "receiveMonthReport": "Receive monthly report", 6 | "updated": "Notification updated" 7 | }, 8 | "ja": { 9 | "title": "メール配信を設定", 10 | "receiveWeekReport": "週次レポートを受信する", 11 | "receiveMonthReport": "月次レポートを受信する", 12 | "updated": "設定を更新しました" 13 | }, 14 | "zh-CN": { 15 | "title": "通知设置", 16 | "receiveWeekReport": "接收周报", 17 | "receiveMonthReport": "接收日报", 18 | "updated": "已更新" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /assets/scss/_animations.scss: -------------------------------------------------------------------------------- 1 | // stylelint-disable-next-line scss/dollar-variable-pattern 2 | $animationDuration: $animation-duration; 3 | 4 | @import '../../node_modules/vue2-animate/src/sass/vue2-animate'; 5 | -------------------------------------------------------------------------------- /assets/scss/_reset.scss: -------------------------------------------------------------------------------- 1 | html { 2 | -webkit-tap-highlight-color: rgb(0 0 0 / 0%); 3 | user-select: none; 4 | } 5 | 6 | body, 7 | select, 8 | input, 9 | textarea, 10 | button { 11 | color: $text; 12 | } 13 | 14 | body { 15 | background-color: $background; 16 | font-display: auto; 17 | font-family: $font-family; 18 | font-size: $font-size; 19 | line-height: $line-height; 20 | outline: none; 21 | 22 | &::before { 23 | background: $background; 24 | content: ''; 25 | display: flex; 26 | height: 100vh; 27 | position: fixed; 28 | width: 100vw; 29 | z-index: -1; 30 | } 31 | } 32 | 33 | input[type='text'], 34 | input[type='time'], 35 | input[type='date'], 36 | input[type='email'], 37 | input[type='password'], 38 | textarea { 39 | appearance: none; 40 | box-sizing: border-box; 41 | outline: none; 42 | } 43 | 44 | input::placeholder { 45 | color: $grey-ccc; 46 | opacity: 1; 47 | padding-top: 2px; 48 | } 49 | -------------------------------------------------------------------------------- /assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import './reset'; 2 | @import './tooltip'; 3 | @import './animations'; 4 | -------------------------------------------------------------------------------- /assets/scss/modules/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin mq($breakpoint: small) { 2 | @media screen and (max-width: #{map-get($breakpoints, $breakpoint)}) { 3 | @content; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /components/README.md: -------------------------------------------------------------------------------- 1 | # COMPONENTS 2 | 3 | The components directory contains your Vue.js Components. 4 | Nuxt.js doesn't supercharge these components. 5 | 6 | **This directory is not required, you can delete it if you don't want to use it.** 7 | -------------------------------------------------------------------------------- /components/atoms/animate-duration.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 60 | -------------------------------------------------------------------------------- /components/atoms/bar-chart.vue: -------------------------------------------------------------------------------- 1 | 100 | -------------------------------------------------------------------------------- /components/atoms/base-button.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 73 | -------------------------------------------------------------------------------- /components/atoms/base-input.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 25 | 26 | 70 | -------------------------------------------------------------------------------- /components/atoms/calendar-event.vue: -------------------------------------------------------------------------------- 1 | 2 |{{ text }}
5 |
8 |
{{ $t('about') }}
9 | 25 |