├── .eslintrc.cjs ├── .github └── workflows │ ├── ci.yml │ ├── regression.yml │ └── update-regression.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc ├── .node-version ├── .npmrc ├── .prettierignore ├── .storybook ├── main.ts └── preview.ts ├── CODEOWNERS ├── Dockerfile ├── docker-compose.yml ├── env.d.ts ├── functions └── _middleware.ts ├── generate-sdk.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public ├── apple-touch-icon.png ├── diploma-mock.jpg ├── favicon.ico ├── icon-192.png ├── icon-512.png ├── icon.svg ├── logo-text.svg ├── logo.svg ├── manifest.webmanifest └── robots.txt ├── readme.md ├── src ├── App.vue ├── api │ ├── auth.ts │ ├── generated-api.ts │ ├── index.ts │ ├── onRequestFulfilled.test.ts │ ├── onRequestFulfilled.ts │ ├── onResponseFulfilled.test.ts │ ├── onResponseFulfilled.ts │ ├── onResponseRejected.test.ts │ ├── onResponseRejected.ts │ ├── requestCaseMiddleware.test.ts │ ├── requestCaseMiddleware.ts │ ├── responseCaseMiddleware.test.ts │ └── responseCaseMiddleware.ts ├── assets │ └── fonts │ │ ├── subset-PTRootUI-Bold.woff │ │ ├── subset-PTRootUI-Bold.woff2 │ │ ├── subset-PTRootUI-Light.woff │ │ ├── subset-PTRootUI-Light.woff2 │ │ ├── subset-PTRootUI-Medium.woff │ │ ├── subset-PTRootUI-Medium.woff2 │ │ ├── subset-PTRootUI-Regular.woff │ │ └── subset-PTRootUI-Regular.woff2 ├── components │ ├── VAnswer │ │ ├── VAnswer.stories.ts │ │ ├── VAnswer.test.ts │ │ ├── VAnswer.vue │ │ ├── VAnswerProvider.vue │ │ └── index.ts │ ├── VAnswerActions │ │ ├── VAnswerActions.stories.ts │ │ ├── VAnswerActions.test.ts │ │ └── VAnswerActions.vue │ ├── VAnswerActionsDesktop │ │ ├── VAnswerActionsDesktop.stories.ts │ │ ├── VAnswerActionsDesktop.test.ts │ │ └── VAnswerActionsDesktop.vue │ ├── VAnswerActionsMobile │ │ ├── VAnswerActionsMobile.stories.ts │ │ ├── VAnswerActionsMobile.test.ts │ │ └── VAnswerActionsMobile.vue │ ├── VAvatar │ │ ├── VAvatar.stories.ts │ │ ├── VAvatar.test.ts │ │ └── VAvatar.vue │ ├── VAvatarSettings │ │ ├── VAvatarSettings.stories.ts │ │ └── VAvatarSettings.vue │ ├── VBreadcrumbs │ │ └── VBreadcrumbs.vue │ ├── VButton │ │ ├── VButton.stories.ts │ │ ├── VButton.test.ts │ │ └── VButton.vue │ ├── VCard │ │ ├── VCard.stories.ts │ │ ├── VCard.test.ts │ │ └── VCard.vue │ ├── VCertificate │ │ ├── VCertificate.stories.ts │ │ ├── VCertificate.test.ts │ │ └── VCertificate.vue │ ├── VCertificateCard │ │ ├── VCertificateCard.stories.ts │ │ ├── VCertificateCard.test.ts │ │ └── VCertificateCard.vue │ ├── VCertificateSettings │ │ ├── VCertificateSettings.stories.ts │ │ └── VCertificateSettings.vue │ ├── VCover │ │ ├── VCover.stories.ts │ │ ├── VCover.test.ts │ │ └── VCover.vue │ ├── VCreateAnswer │ │ └── VCreateAnswer.vue │ ├── VCrossChecks │ │ └── VCrossChecks.vue │ ├── VDetails │ │ └── VDetails.vue │ ├── VExistingAnswer │ │ ├── VExistingAnswer.vue │ │ ├── VExistingAnswerProvider.vue │ │ └── index.ts │ ├── VFeedbackGuide │ │ ├── VFeedbackGuide.stories.ts │ │ └── VFeedbackGuide.vue │ ├── VFloat │ │ ├── VFloat.stories.ts │ │ └── VFloat.vue │ ├── VHeader │ │ ├── VHeader.stories.ts │ │ ├── VHeader.test.ts │ │ └── VHeader.vue │ ├── VHeading │ │ ├── VHeading.stories.ts │ │ ├── VHeading.test.ts │ │ └── VHeading.vue │ ├── VHtmlContent │ │ ├── VHtmlContent.stories.ts │ │ ├── VHtmlContent.test.ts │ │ └── VHtmlContent.vue │ ├── VLessonCard │ │ ├── VLessonCard.stories.ts │ │ └── VLessonCard.vue │ ├── VLinksSettings │ │ ├── VLinksSettings.stories.ts │ │ └── VLinksSettings.vue │ ├── VLoader │ │ ├── VLoader.stories.ts │ │ └── VLoader.vue │ ├── VLoginLink │ │ └── VLoginLink.vue │ ├── VLoginPassword │ │ └── VLoginPassword.vue │ ├── VLogo │ │ ├── VLogo.stories.ts │ │ └── VLogo.vue │ ├── VModuleCard │ │ ├── VModuleCard.stories.ts │ │ └── VModuleCard.vue │ ├── VNotionRenderer │ │ └── VNotionRenderer.vue │ ├── VPasswordSettings │ │ ├── VPasswordSettings.stories.ts │ │ ├── VPasswordSettings.test.ts │ │ └── VPasswordSettings.vue │ ├── VPill │ │ ├── VPill.vue │ │ └── VPillItem.vue │ ├── VPillHomework │ │ ├── VPillHomework.stories.ts │ │ └── VPillHomework.vue │ ├── VPreferencesSettings │ │ └── VPreferencesSettings.vue │ ├── VPreloader │ │ ├── VPreloader.stories.ts │ │ └── VPreloader.vue │ ├── VProfileMenu │ │ ├── VProfileMenu.stories.ts │ │ ├── VProfileMenu.test.ts │ │ └── VProfileMenu.vue │ ├── VRadioSwitch │ │ ├── VRadioSwitch.stories.ts │ │ ├── VRadioSwitch.test.ts │ │ └── VRadioSwitch.vue │ ├── VReactions │ │ ├── VReactions.stories.ts │ │ ├── VReactions.vue │ │ └── components │ │ │ └── VReaction │ │ │ ├── VReaction.stories.ts │ │ │ ├── VReaction.test.ts │ │ │ └── VReaction.vue │ ├── VTag │ │ └── VTag.vue │ ├── VTextEditor │ │ ├── VTextEditor.stories.ts │ │ └── VTextEditor.vue │ ├── VTextInput │ │ ├── VTextInput.stories.ts │ │ ├── VTextInput.test.ts │ │ └── VTextInput.vue │ ├── VThread │ │ ├── VThread.vue │ │ ├── VThreadProvider.vue │ │ └── index.ts │ ├── VToast │ │ ├── VToast.stories.ts │ │ ├── VToast.test.ts │ │ └── VToast.vue │ └── VToastFeed │ │ ├── VToastFeed.stories.ts │ │ ├── VToastFeed.test.ts │ │ └── VToastFeed.vue ├── fonts.css ├── hooks │ └── useChatra.ts ├── layouts │ ├── VBaseLayout │ │ └── VBaseLayout.vue │ ├── VLoggedLayout │ │ └── VLoggedLayout.vue │ └── VPublicLayout │ │ └── VPublicLayout.vue ├── main.ts ├── mocks │ ├── VTransparentComponent.vue │ ├── mockAnswer.ts │ ├── mockBreadcrumbs.ts │ ├── mockCase.ts │ ├── mockContent.ts │ ├── mockCourse.ts │ ├── mockDiploma.ts │ ├── mockEmoji.ts │ ├── mockLMSCourse.ts │ ├── mockLessonPlain.ts │ ├── mockLocale.ts │ ├── mockMaterialSerializer.ts │ ├── mockModule.ts │ ├── mockQuestion.ts │ ├── mockReactionDetailed.ts │ ├── mockUserId.ts │ └── mockUserSafe.ts ├── query.ts ├── router │ ├── index.ts │ ├── loginById.test.ts │ ├── loginById.ts │ ├── loginByToken.test.ts │ └── loginByToken.ts ├── stores │ ├── auth.test.ts │ ├── auth.ts │ ├── toasts.test.ts │ └── toasts.ts ├── style.css ├── types │ ├── index.ts │ └── users.ts ├── utils │ ├── date.ts │ ├── filterDictionary.ts │ ├── getCertificateLink.ts │ ├── getName.test.ts │ ├── getName.ts │ ├── getNotionTitle.test.ts │ ├── getNotionTitle.ts │ ├── handleError.test.ts │ ├── handleError.ts │ ├── htmlToMarkdown.ts │ ├── idToUuid.test.ts │ ├── idToUuid.ts │ ├── layoutDecorator.ts │ ├── makeStatic.ts │ └── uuid.ts └── views │ ├── VCertificatesView │ ├── VCertificatesView.stories.ts │ ├── VCertificatesView.test.ts │ └── VCertificatesView.vue │ ├── VCoursesView │ ├── VCoursesView.stories.ts │ ├── VCoursesView.test.ts │ └── VCoursesView.vue │ ├── VHomeworkView │ ├── VHomeworkAnswerView.vue │ ├── VHomeworkQuestionView.vue │ ├── VHomeworkView.vue │ └── useHomeworkBreadcrumbs.ts │ ├── VLessonsView │ └── VLessonsView.vue │ ├── VLoadingView │ ├── VLoadingView.stories.ts │ └── VLoadingView.vue │ ├── VLoginChangeView │ ├── VLoginChangeView.stories.ts │ ├── VLoginChangeView.test.ts │ └── VLoginChangeView.vue │ ├── VLoginResetView │ ├── VLoginResetView.stories.ts │ ├── VLoginResetView.test.ts │ └── VLoginResetView.vue │ ├── VLoginView │ ├── VLoginView.stories.ts │ ├── VLoginView.test.ts │ └── VLoginView.vue │ ├── VMailSentView │ ├── VMailSentView.stories.ts │ ├── VMailSentView.test.ts │ └── VMailSentView.vue │ ├── VModulesView │ └── VModulesView.vue │ ├── VNotionView │ ├── VNotionView.stories.ts │ └── VNotionView.vue │ └── VSettingsView │ ├── VSettingsView.stories.ts │ ├── VSettingsView.test.ts │ └── VSettingsView.vue ├── tailwind.config.ts ├── tests ├── __image_snapshots__ │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-1440-900-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-1440-900-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-320-560-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-320-560-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-390-840-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-390-840-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-768-1024-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-768-1024-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-1440-900-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-1440-900-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-320-560-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-320-560-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-390-840-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-390-840-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-768-1024-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-768-1024-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-1440-900-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-1440-900-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-320-560-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-320-560-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-390-840-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-390-840-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-768-1024-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-768-1024-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-1440-900-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-1440-900-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-320-560-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-320-560-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-390-840-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-390-840-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-768-1024-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-768-1024-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-1440-900-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-1440-900-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-320-560-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-320-560-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-390-840-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-390-840-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-768-1024-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-768-1024-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-1440-900-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-1440-900-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-320-560-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-320-560-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-390-840-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-390-840-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-768-1024-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-768-1024-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-1440-900-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-768-1024-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-1440-900-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-1440-900-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-320-560-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-320-560-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-390-840-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-390-840-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-768-1024-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-768-1024-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-1440-900-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-1440-900-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-320-560-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-320-560-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-390-840-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-390-840-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-768-1024-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-768-1024-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-1440-900-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-1440-900-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-320-560-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-320-560-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-390-840-dark-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-390-840-light-1-snap.png │ ├── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-768-1024-dark-1-snap.png │ └── regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-768-1024-light-1-snap.png ├── regression.config.ts ├── regression.test.ts └── unit.config.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.vitest.json ├── vite.config.ts └── vue-avatar-cropper.d.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution'); 3 | 4 | module.exports = { 5 | root: true, 6 | env: { 7 | node: true, 8 | }, 9 | extends: [ 10 | 'plugin:vue/vue3-recommended', 11 | 'eslint:recommended', 12 | '@vue/eslint-config-typescript', 13 | '@vue/eslint-config-prettier', 14 | 'plugin:storybook/recommended', 15 | ], 16 | parserOptions: { 17 | ecmaVersion: 'latest', 18 | }, 19 | rules: { 20 | 'no-empty': ['error', { allowEmptyCatch: true }], 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/regression.yml: -------------------------------------------------------------------------------- 1 | name: Regression tests 2 | on: [push] 3 | 4 | jobs: 5 | regression: 6 | runs-on: ubuntu-24.04 7 | 8 | steps: 9 | - name: Checkout 🛎 10 | uses: actions/checkout@v4 11 | 12 | - uses: pnpm/action-setup@v3 13 | with: 14 | version: 9 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version-file: '.node-version' 20 | cache: 'pnpm' 21 | 22 | - name: Install dependencies 23 | run: pnpm install 24 | 25 | - name: Install playwright browsers 26 | run: pnpm playwright install chromium 27 | 28 | - name: Build storybook 29 | run: pnpm run storybook:build 30 | 31 | - name: Run storybook 32 | run: pnpm run storybook:preview & 33 | 34 | - name: Run visual regression tests 35 | run: pnpm run test:regression 36 | 37 | - name: Saving diffs 📥 38 | if: failure() 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: diff 42 | path: tests/__image_snapshots__/__diff_output__ 43 | retention-days: 14 44 | 45 | - name: Saving failure 📥 46 | if: failure() 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: received 50 | path: tests/__image_snapshots__/__received_output__ 51 | retention-days: 14 -------------------------------------------------------------------------------- /.github/workflows/update-regression.yml: -------------------------------------------------------------------------------- 1 | name: Update regression tests snapshots 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | regression: 6 | runs-on: ubuntu-24.04 7 | 8 | steps: 9 | - name: Checkout 🛎 10 | uses: actions/checkout@v4 11 | 12 | - uses: pnpm/action-setup@v3 13 | with: 14 | version: 9 15 | 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version-file: '.node-version' 20 | cache: 'pnpm' 21 | 22 | - name: Install dependencies 23 | run: pnpm install 24 | 25 | - name: Install playwright browsers 26 | run: pnpm playwright install chromium 27 | 28 | - name: Build storybook 29 | run: pnpm run storybook:build 30 | 31 | - name: Run storybook 32 | run: pnpm run storybook:preview & 33 | 34 | - name: Run regression tests 35 | run: pnpm run test:regression:update 36 | 37 | - name: Saving screenshots 📥 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: updated-screenshots 41 | path: tests/__image_snapshots__/ 42 | retention-days: 14 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | .env 30 | 31 | storybook-static 32 | src/__image_snapshots__/__diff_output__ 33 | 34 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged --concurrent false 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*": ["prettier --write --ignore-unknown"], 3 | "*.(ts|tsx|js|jsx|vue)": ["eslint --fix"] 4 | } 5 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 22 -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | strict-peer-dependencies=false 3 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | dist/ 3 | coverage/ 4 | docker-compose.yml 5 | .husky/ 6 | *.ejs.t 7 | .npmrc 8 | *.png 9 | *.svg 10 | *.jpg 11 | .* -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/vue3-vite'; 2 | 3 | const config: StorybookConfig = { 4 | stories: [ 5 | '../src/**/*.mdx', 6 | '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)', 7 | ], 8 | addons: [ 9 | "@storybook/addon-docs", 10 | ], 11 | framework: { 12 | name: '@storybook/vue3-vite', 13 | options: {}, 14 | }, 15 | core: { 16 | disableTelemetry: true, 17 | } 18 | }; 19 | export default config; -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import '../src/fonts.css'; 2 | import '../src/style.css'; 3 | import { setup } from '@storybook/vue3-vite'; 4 | import { createPinia } from 'pinia'; 5 | import FloatingVue from 'floating-vue'; 6 | import 'floating-vue/dist/style.css'; 7 | import { VueQueryPlugin } from '@tanstack/vue-query'; 8 | import { vueQueryConfig } from '../src/main'; 9 | import type { Preview } from '@storybook/vue3-vite'; 10 | 11 | const preview: Preview = { 12 | parameters: { 13 | actions: { argTypesRegex: '^on[A-Z].*' }, 14 | controls: { 15 | matchers: { 16 | color: /(background|color)$/i, 17 | date: /Date$/, 18 | }, 19 | }, 20 | viewport: { 21 | viewports: [ 22 | { 23 | name: 'fluid', 24 | styles: { width: '100%', height: '100%' }, 25 | type: 'desktop', 26 | }, 27 | { 28 | name: 'desktop', 29 | styles: { width: '1440px', height: '100%' }, 30 | type: 'desktop', 31 | }, 32 | { 33 | name: 'tablet', 34 | styles: { width: '768px', height: '100%' }, 35 | type: 'tablet', 36 | }, 37 | { 38 | name: 'mobile', 39 | styles: { width: '320px', height: '100%' }, 40 | type: 'mobile', 41 | }, 42 | ], 43 | defaultViewport: 'fluid', 44 | }, 45 | } 46 | }; 47 | 48 | setup((app) => { 49 | app.use(FloatingVue); 50 | app.use(createPinia()); 51 | app.use(VueQueryPlugin, vueQueryConfig); 52 | }); 53 | 54 | 55 | export default preview; -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @brachkow 2 | .* @brachkow 3 | !docker-compose.yml @brachkow 4 | !Dockerfile @brachkow 5 | 6 | .github/workflows/ci.yml @f213 7 | docker-compose.yml @f213 8 | Dockerfile @f213 9 | src/api/* @f213 10 | src/axios/* @f213 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/tough-dev-school/frontend-static:0.0.2 2 | 3 | ADD dist /dist 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | backend: 3 | # please login to ghcr.io 4 | image: ghcr.io/tough-dev-school/dev-backend 5 | command: ["wait-for-it", "postgres:5432", "--", "/bin/sh", "-c", "python ./manage.py migrate && ./manage.py create_dev_admin_user && uwsgi --master --http :8000 --module core.wsgi --workers 2"] 6 | environment: 7 | - DEBUG=Off 8 | - AXES_ENABLED=Off 9 | - NO_CACHE=On 10 | - DISABLE_THROTTLING=On 11 | - CELERY_ALWAYS_EAGER=On 12 | - DATABASE_URL=postgres://postgres:secret@postgres/postgres 13 | - REDISCLOUD_URL=redis://redis 14 | - EMAIL_BACKEND=apps.mailing.backends.ConsoleEmailBackend 15 | - EMAIL_FROM=dev@localhost 16 | - EMAIL_ENABLED=On 17 | - FRONTEND_URL=http://localhost:3000/ 18 | - DEFAULT_FILE_STORAGE=core.storages.ProdReadOnlyStorage 19 | - AWS_S3_CUSTOM_DOMAIN=cdn.tough-dev.school 20 | - AWS_ACCESS_KEY_ID=root 21 | - AWS_SECRET_ACCESS_KEY=ibcxJ8Du 22 | - AWS_STORAGE_BUCKET_NAME=dev 23 | - NOTION_CACHE_ONLY=On 24 | 25 | ports: 26 | - 8000:8000 27 | links: 28 | - postgres 29 | - redis 30 | 31 | postgres: 32 | ports: 33 | - 5432:5432 34 | image: ghcr.io/tough-dev-school/dev-db 35 | volumes: 36 | - db-data:/var/lib/postgresql/data 37 | 38 | redis: 39 | image: redis:6-alpine 40 | 41 | volumes: 42 | db-data: 43 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /generate-sdk.ts: -------------------------------------------------------------------------------- 1 | import { rmSync, writeFileSync, readFileSync } from 'node:fs'; 2 | import { resolve } from 'node:path'; 3 | import { generateApi } from 'swagger-typescript-api'; 4 | 5 | const OPENAPI_URL = 'http://localhost:8000/api/v2/docs/schema/?format=json'; 6 | const SWAGGER_FILE = resolve(process.cwd(), './swagger.json'); 7 | const GENERATED_API_FILE = resolve(process.cwd(), './src/api/generated-api.ts'); 8 | 9 | const openapiData = await fetch(OPENAPI_URL).then((res) => res.json()); 10 | 11 | writeFileSync(SWAGGER_FILE, JSON.stringify(openapiData, null, 2)); 12 | 13 | await generateApi({ 14 | input: SWAGGER_FILE, 15 | output: resolve(process.cwd(), './src/api/'), 16 | httpClientType: 'axios', 17 | singleHttpClient: true, 18 | extractEnums: true, 19 | extractRequestParams: true, 20 | unwrapResponseData: true, 21 | fileName: 'generated-api', 22 | }); 23 | 24 | const content = readFileSync(GENERATED_API_FILE, 'utf-8'); 25 | 26 | const updatedContent = content.replace( 27 | /descendants?\?: Answer\[\]/g, 28 | 'descendants: AnswerTree[]', 29 | ); 30 | writeFileSync(GENERATED_API_FILE, updatedContent); 31 | 32 | rmSync(SWAGGER_FILE); 33 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | 'tailwindcss/nesting': {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | tabWidth: 2, 3 | useTabs: false, 4 | semi: true, 5 | singleQuote: true, 6 | quoteProps: 'as-needed', 7 | jsxSingleQuote: false, 8 | trailingComma: 'all', 9 | bracketSpacing: true, 10 | bracketSameLine: true, 11 | arrowParens: 'always', 12 | htmlWhitespaceSensitivity: 'css', 13 | vueIndentScriptAndStyle: true, 14 | }; 15 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/diploma-mock.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/public/diploma-mock.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/public/icon-192.png -------------------------------------------------------------------------------- /public/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/public/icon-512.png -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "icons": [ 3 | { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" }, 4 | { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" } 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # lms-frontend [![ci](https://github.com/tough-dev-school/lms-frontend-v2/actions/workflows/ci.yml/badge.svg)](https://github.com/tough-dev-school/lms-frontend-v2/actions/workflows/ci.yml) 2 | 3 | - `dev` — dev server 4 | - `build` — build project 5 | - `test` — run tests 6 | - `lint` — run eslint 7 | - `storybook` — open storybook 8 | 9 | ## Backend setup 10 | 11 | ```bash 12 | docker compose up 13 | ``` 14 | 15 | Так же нужно будет войти в [GitHub Container registry](https://docs.github.com/en/packages/guides/configuring-docker-for-use-with-github-packages#authenticating-with-a-personal-access-token), обратите внимание что нужно войти в ` https://ghcr.io`, а не в `https://docker.pkg.github.com`. 16 | 17 | ## Тесты на визульную регрессию 18 | 19 | Если ветка содержит визуальные изменения, тест на регрессию не пройдет. Чтобы исправить тест, выполните следующие шаги: 20 | 21 | 1. Перейдите на вкладку “Summary” неудачного задания и скачайте файл `diff.zip` внизу. 22 | 2. В скачанном архиве проверьте различия. Если различия неожиданны — исправьте их. Если различия ожидаемы, вам нужно сгенерировать новые скриншоты, включая эти изменения. 23 | 3. Перейдите к [экшену обновления](https://github.com/tough-dev-school/lms-frontend-v2/actions/workflows/update-regression.yml) и запустите его для вашей ветки. 24 | 4. Перейдите на вкладку “Summary” задания обновления и скачайте файл `updated-screenshots.zip` внизу. 25 | 5. На вашем компьютере замените изображения в `tests/image_snapshots/` на изображения из `updated-screenshots.zip` и зафиксируйте эти изменения в вашей ветке. 26 | 6. CI будет исправлен. 27 | 28 | Если вам нужно добавить новые тесты, добавьте новый маршрут в массив pages и выполните шаги 3-5. 29 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import onResponseRejected from './onResponseRejected'; 2 | import { Api, HttpClient } from './generated-api'; 3 | import onResponseFulfilled from './onResponseFulfilled'; 4 | import onRequestFulfilled from './onRequestFulfilled'; 5 | import { merge } from 'lodash-es'; 6 | 7 | export interface CustomAxiosInstanceConfig { 8 | useResponseCaseMiddleware: boolean; 9 | useRequestCaseMiddleware: boolean; 10 | } 11 | 12 | export const createHttpClient = ( 13 | customConfig: Partial = {}, 14 | ) => { 15 | // #FIXME Case middleware must be removed after migration to autofg 16 | const defaultConfig = { 17 | useResponseCaseMiddleware: false, 18 | useRequestCaseMiddleware: false, 19 | }; 20 | 21 | const config = merge( 22 | {}, 23 | defaultConfig, 24 | customConfig, 25 | ) as CustomAxiosInstanceConfig; 26 | 27 | const { useResponseCaseMiddleware, useRequestCaseMiddleware } = config; 28 | 29 | const httpClient = new HttpClient({ 30 | baseURL: import.meta.env.VITE_API_URL, 31 | }); 32 | 33 | httpClient.instance.interceptors.request.use((value) => 34 | onRequestFulfilled(value, useRequestCaseMiddleware), 35 | ); 36 | 37 | httpClient.instance.interceptors.response.use( 38 | (value) => onResponseFulfilled(value, useResponseCaseMiddleware), 39 | (value) => onResponseRejected(value, useResponseCaseMiddleware), 40 | ); 41 | 42 | return httpClient; 43 | }; 44 | 45 | /** 46 | * @deprecated This is a temporary solution to support handwritten API client. 47 | */ 48 | const backwardCompatibleHttpClient = createHttpClient({ 49 | useResponseCaseMiddleware: true, 50 | useRequestCaseMiddleware: true, 51 | }); 52 | 53 | /** 54 | * @deprecated Direct usage of axios is deprecated. Use api instead. 55 | */ 56 | export const axios = backwardCompatibleHttpClient.instance; 57 | 58 | const modernHttpClient = createHttpClient(); 59 | export const { api } = new Api(modernHttpClient); 60 | export default axios; 61 | -------------------------------------------------------------------------------- /src/api/onRequestFulfilled.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, beforeEach, expect, test } from 'vitest'; 2 | import onRequestFulfilled from './onRequestFulfilled'; 3 | import requestCaseMiddleware from './requestCaseMiddleware'; 4 | import type { InternalAxiosRequestConfig } from 'axios'; 5 | import { faker } from '@faker-js/faker'; 6 | import { createApp } from 'vue'; 7 | import { setActivePinia } from 'pinia'; 8 | import { createTestingPinia } from '@pinia/testing'; 9 | import useAuth from '@/stores/auth'; 10 | 11 | vi.mock('./requestCaseMiddleware'); 12 | 13 | const input = { data: {}, headers: {} } as InternalAxiosRequestConfig; 14 | 15 | describe('onRequestFulfilled', () => { 16 | let authStore: any; 17 | 18 | beforeEach(() => { 19 | const app = createApp({}); 20 | const pinia = createTestingPinia({ createSpy: vi.fn }); 21 | app.use(pinia); 22 | setActivePinia(pinia); 23 | 24 | authStore = useAuth(); 25 | }); 26 | 27 | test('request data is converted to snake_case', () => { 28 | onRequestFulfilled(input, true); 29 | 30 | expect(requestCaseMiddleware).toHaveBeenCalledTimes(1); 31 | expect(requestCaseMiddleware).toHaveBeenCalledWith(input.data, true); 32 | }); 33 | 34 | test('request data coversion can be disabled', () => { 35 | onRequestFulfilled(input, false); 36 | 37 | expect(requestCaseMiddleware).toHaveBeenCalledTimes(1); 38 | expect(requestCaseMiddleware).toHaveBeenCalledWith(input.data, false); 39 | }); 40 | 41 | test('auth token added to request headers if exist', () => { 42 | authStore.token = faker.string.uuid(); 43 | 44 | const request = onRequestFulfilled(input, true); 45 | 46 | expect(request?.headers?.Authorization).toBe(`Bearer ${authStore.token}`); 47 | }); 48 | 49 | test('auth token is not added to request headers if not exist', () => { 50 | authStore.token = undefined; 51 | 52 | const request = onRequestFulfilled(input, true); 53 | 54 | expect(request?.headers?.Authorization).toBe(undefined); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/api/onRequestFulfilled.ts: -------------------------------------------------------------------------------- 1 | import useAuth from '@/stores/auth'; 2 | import type { InternalAxiosRequestConfig } from 'axios'; 3 | import requestCaseMiddleware from './requestCaseMiddleware'; 4 | import { cloneDeep } from 'lodash-es'; 5 | 6 | const onRequestFulfilled = ( 7 | config: InternalAxiosRequestConfig, 8 | enableCaseMiddleware = true, 9 | ) => { 10 | const auth = useAuth(); 11 | 12 | config = cloneDeep(config); 13 | 14 | config.headers = config.headers || {}; 15 | 16 | // Manage authorization via pinia 17 | if (auth.token) { 18 | config.headers.Authorization = `Bearer ${auth.token}`; 19 | config.headers.frkn = '1'; 20 | } 21 | 22 | // Convert data keys to target case 23 | if (!(config.data instanceof FormData)) { 24 | config.data = requestCaseMiddleware(config.data, enableCaseMiddleware); 25 | } 26 | 27 | if (config.params !== undefined) { 28 | Object.keys(config.params).forEach((paramName) => { 29 | if ( 30 | Array.isArray(config.params[paramName]) && 31 | config.params[paramName].length > 0 32 | ) { 33 | config.params[paramName] = config.params[paramName].join(','); 34 | } 35 | }); 36 | } 37 | 38 | return config; 39 | }; 40 | 41 | export default onRequestFulfilled; 42 | -------------------------------------------------------------------------------- /src/api/onResponseFulfilled.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, beforeEach, expect, test } from 'vitest'; 2 | import onResponseFulfilled from './onResponseFulfilled'; 3 | import responseCaseMiddleware from './responseCaseMiddleware'; 4 | import { createApp } from 'vue'; 5 | import { setActivePinia } from 'pinia'; 6 | import { createTestingPinia } from '@pinia/testing'; 7 | import type { AxiosResponse } from 'axios'; 8 | import { cloneDeep } from 'lodash-es'; 9 | 10 | vi.mock('./responseCaseMiddleware'); 11 | 12 | const input = { data: {} } as AxiosResponse; 13 | 14 | describe('onResponseFulfilled', () => { 15 | beforeEach(() => { 16 | const app = createApp({}); 17 | const pinia = createTestingPinia({ createSpy: vi.fn }); 18 | app.use(pinia); 19 | setActivePinia(pinia); 20 | }); 21 | 22 | test('request data is converted to CamelCase', () => { 23 | onResponseFulfilled(cloneDeep(input), true); 24 | 25 | expect(responseCaseMiddleware).toHaveBeenCalledTimes(1); 26 | expect(responseCaseMiddleware).toHaveBeenCalledWith( 27 | cloneDeep(input).data, 28 | true, 29 | ); 30 | }); 31 | 32 | test('request data coversion can be disabled', () => { 33 | onResponseFulfilled(cloneDeep(input), false); 34 | 35 | expect(responseCaseMiddleware).toHaveBeenCalledTimes(1); 36 | expect(responseCaseMiddleware).toHaveBeenCalledWith( 37 | cloneDeep(input).data, 38 | false, 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/api/onResponseFulfilled.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosResponse } from 'axios'; 2 | import responseCaseMiddleware from './responseCaseMiddleware'; 3 | 4 | const onResponseFulfilled = ( 5 | response: AxiosResponse, 6 | enableCaseMiddleware: boolean, 7 | ) => { 8 | // Convert data keys to target case 9 | if (response.data) { 10 | response.data = responseCaseMiddleware(response.data, enableCaseMiddleware); 11 | } 12 | 13 | return response; 14 | }; 15 | 16 | export default onResponseFulfilled; 17 | -------------------------------------------------------------------------------- /src/api/onResponseRejected.ts: -------------------------------------------------------------------------------- 1 | import useAuth from '@/stores/auth'; 2 | import type { AxiosError } from 'axios'; 3 | import handleError from '@/utils/handleError'; 4 | import responseCaseMiddleware from './responseCaseMiddleware'; 5 | 6 | const onResponseRejected = ( 7 | error: AxiosError, 8 | enableCaseMiddleware: boolean, 9 | ) => { 10 | const auth = useAuth(); 11 | 12 | if (error.response) { 13 | if (error.response.status === 401 && auth.token) auth.removeToken(); 14 | 15 | if (error.response.status !== 401) { 16 | // Convert data keys to target case 17 | if (error.response.data) { 18 | error.response.data = responseCaseMiddleware( 19 | error.response.data as any, 20 | enableCaseMiddleware, 21 | ); 22 | } 23 | 24 | // Handle error with default or custom message 25 | const isJson = 26 | error.response.headers['content-type'] === 27 | 'application/json; charset=utf-8'; 28 | 29 | isJson ? handleError(error) : handleError(); 30 | } 31 | } 32 | 33 | return Promise.reject(error); 34 | }; 35 | 36 | export default onResponseRejected; 37 | -------------------------------------------------------------------------------- /src/api/requestCaseMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, expect, test } from 'vitest'; 2 | import requestCaseMiddleware from './requestCaseMiddleware'; 3 | import decamelizeKeys from 'decamelize-keys'; 4 | import { 5 | STATIC_CAMEL_CASE_EXAMPLE, 6 | STATIC_SNAKE_CASE_EXAMPLE, 7 | } from '@/mocks/mockCase'; 8 | 9 | vi.mock('decamelize-keys'); 10 | 11 | const data = STATIC_CAMEL_CASE_EXAMPLE; 12 | 13 | describe('requestCaseMiddleware', () => { 14 | test('run decamelizeKeys when enabled', () => { 15 | (decamelizeKeys as ReturnType).mockReturnValue( 16 | STATIC_SNAKE_CASE_EXAMPLE, 17 | ); 18 | const result = requestCaseMiddleware(data, true); 19 | 20 | expect(decamelizeKeys).toHaveBeenCalledTimes(1); 21 | expect(decamelizeKeys).toHaveBeenCalledWith(data, { deep: true }); 22 | expect(result).toStrictEqual(decamelizeKeys(data)); 23 | }); 24 | 25 | test('dont run decamelizeKeys when not enabled', () => { 26 | const result = requestCaseMiddleware(data, false); 27 | 28 | expect(decamelizeKeys).toHaveReturnedTimes(0); 29 | expect(result).toStrictEqual(data); 30 | }); 31 | 32 | test('return data when enabled', () => { 33 | (decamelizeKeys as ReturnType).mockReturnValue(data); 34 | 35 | expect(requestCaseMiddleware(data, true)).toBe(data); 36 | }); 37 | 38 | test('return data when not enabled', () => { 39 | expect(requestCaseMiddleware(data, false)).toBe(data); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/api/requestCaseMiddleware.ts: -------------------------------------------------------------------------------- 1 | import decamelizeKeys from 'decamelize-keys'; 2 | 3 | /** 4 | * @deprecated since transition to generated api we don't need this middleware as it breaks types 5 | */ 6 | const requestCaseMiddleware = (data: object | object[], enable = true) => 7 | enable ? decamelizeKeys(data, { deep: true }) : data; 8 | 9 | export default requestCaseMiddleware; 10 | -------------------------------------------------------------------------------- /src/api/responseCaseMiddleware.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, expect, test } from 'vitest'; 2 | import responseCaseMiddleware from './responseCaseMiddleware'; 3 | import camelcaseKeys from 'camelcase-keys'; 4 | import { 5 | STATIC_CAMEL_CASE_EXAMPLE, 6 | STATIC_SNAKE_CASE_EXAMPLE, 7 | } from '@/mocks/mockCase'; 8 | 9 | vi.mock('camelcase-keys'); 10 | 11 | const data = STATIC_SNAKE_CASE_EXAMPLE; 12 | 13 | describe('responseCaseMiddleware', () => { 14 | test('run camelcaseKeys when enabled', () => { 15 | (camelcaseKeys as ReturnType).mockReturnValue( 16 | STATIC_CAMEL_CASE_EXAMPLE, 17 | ); 18 | const result = responseCaseMiddleware(data, true); 19 | 20 | expect(camelcaseKeys).toHaveBeenCalledTimes(1); 21 | expect(camelcaseKeys).toHaveBeenCalledWith(data, { deep: true }); 22 | expect(result).toStrictEqual(camelcaseKeys(data)); 23 | }); 24 | 25 | test('dont run camelcaseKeys when not enabled', () => { 26 | const result = responseCaseMiddleware(data, false); 27 | 28 | expect(camelcaseKeys).toHaveReturnedTimes(0); 29 | expect(result).toStrictEqual(data); 30 | }); 31 | 32 | test('return data when enabled', () => { 33 | (camelcaseKeys as ReturnType).mockReturnValue(data); 34 | 35 | expect(responseCaseMiddleware(data, true)).toBe(data); 36 | }); 37 | 38 | test('return data when not enabled', () => { 39 | expect(responseCaseMiddleware(data, false)).toBe(data); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/api/responseCaseMiddleware.ts: -------------------------------------------------------------------------------- 1 | import camelcaseKeys from 'camelcase-keys'; 2 | 3 | /** 4 | * @deprecated since transition to generated api we don't need this middleware as it breaks types 5 | */ 6 | const responseCaseMiddleware = ( 7 | data: Record | any[], 8 | enable = true, 9 | ) => (enable ? camelcaseKeys(data, { deep: true }) : data); 10 | 11 | export default responseCaseMiddleware; 12 | -------------------------------------------------------------------------------- /src/assets/fonts/subset-PTRootUI-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/src/assets/fonts/subset-PTRootUI-Bold.woff -------------------------------------------------------------------------------- /src/assets/fonts/subset-PTRootUI-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/src/assets/fonts/subset-PTRootUI-Bold.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/subset-PTRootUI-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/src/assets/fonts/subset-PTRootUI-Light.woff -------------------------------------------------------------------------------- /src/assets/fonts/subset-PTRootUI-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/src/assets/fonts/subset-PTRootUI-Light.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/subset-PTRootUI-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/src/assets/fonts/subset-PTRootUI-Medium.woff -------------------------------------------------------------------------------- /src/assets/fonts/subset-PTRootUI-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/src/assets/fonts/subset-PTRootUI-Medium.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/subset-PTRootUI-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/src/assets/fonts/subset-PTRootUI-Regular.woff -------------------------------------------------------------------------------- /src/assets/fonts/subset-PTRootUI-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/src/assets/fonts/subset-PTRootUI-Regular.woff2 -------------------------------------------------------------------------------- /src/components/VAnswer/VAnswer.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VAnswer from '@/components/VAnswer/VAnswer.vue'; 3 | import { mockAnswer } from '@/mocks/mockAnswer'; 4 | import { mockUserSafe, STATIC_AUTHOR_1 } from '@/mocks/mockUserSafe'; 5 | 6 | export default { 7 | title: 'Answer/VAnswer', 8 | component: VAnswer, 9 | } as Meta; 10 | 11 | const answer = mockAnswer(); 12 | const ownAnswer = { ...answer, author: STATIC_AUTHOR_1 }; 13 | 14 | const Template: StoryFn = (args) => ({ 15 | components: { VAnswer }, 16 | setup() { 17 | return { args }; 18 | }, 19 | template: '', 20 | }); 21 | 22 | export const Default = { 23 | render: Template, 24 | 25 | args: { 26 | answer, 27 | user: mockUserSafe(), 28 | }, 29 | }; 30 | 31 | export const Own = { 32 | render: Template, 33 | 34 | args: { 35 | answer: ownAnswer, 36 | user: STATIC_AUTHOR_1, 37 | }, 38 | }; 39 | 40 | // #TODO Add stories for OWN answers (=disabled reactions) 41 | -------------------------------------------------------------------------------- /src/components/VAnswer/VAnswerProvider.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 18 | -------------------------------------------------------------------------------- /src/components/VAnswer/index.ts: -------------------------------------------------------------------------------- 1 | import VAnswerProvider from './VAnswerProvider.vue'; 2 | 3 | const VAnswer = VAnswerProvider; 4 | 5 | export default VAnswer; 6 | -------------------------------------------------------------------------------- /src/components/VAnswerActions/VAnswerActions.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VAnswerActions from '@/components/VAnswerActions/VAnswerActions.vue'; 3 | import dayjs from 'dayjs'; 4 | 5 | export default { 6 | title: 'AnswerActions/VAnswerActions', 7 | component: VAnswerActions, 8 | } as Meta; 9 | 10 | const Template: StoryFn = (args) => ({ 11 | components: { VAnswerActions }, 12 | setup() { 13 | return { args }; 14 | }, 15 | template: '', 16 | }); 17 | 18 | export const Default = { 19 | render: Template, 20 | 21 | args: { 22 | created: dayjs().toISOString(), 23 | deleteTime: 10, 24 | editTime: 30, 25 | }, 26 | }; 27 | 28 | export const DeletePassed = { 29 | render: Template, 30 | 31 | args: { 32 | created: dayjs().toISOString(), 33 | deleteTime: 0, 34 | editTime: 30, 35 | }, 36 | }; 37 | 38 | export const AllPassed = { 39 | render: Template, 40 | 41 | args: { 42 | created: dayjs().toISOString(), 43 | deleteTime: 0, 44 | editTime: 0, 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/components/VAnswerActions/VAnswerActions.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import type { VueWrapper } from '@vue/test-utils'; 4 | import VAnswerActions from './VAnswerActions.vue'; 5 | import type VAnswerActionsDesktop from '@/components/VAnswerActionsDesktop/VAnswerActionsDesktop.vue'; 6 | import type VAnswerActionsMobile from '@/components/VAnswerActionsMobile/VAnswerActionsMobile.vue'; 7 | 8 | describe('VAnswerActions', () => { 9 | let wrapper: VueWrapper>; 10 | 11 | beforeEach(() => { 12 | wrapper = mount(VAnswerActions, { shallow: true }); 13 | }); 14 | 15 | const getMobileActions = () => { 16 | return wrapper.findComponent( 17 | '[data-testid="mobile"]', 18 | ); 19 | }; 20 | const getDesktopActions = () => { 21 | return wrapper.findComponent( 22 | '[data-testid="desktop"]', 23 | ); 24 | }; 25 | 26 | test('mobile actions emit edit on edit', () => { 27 | getMobileActions().vm.$emit('edit'); 28 | 29 | expect(wrapper.emitted('edit')).toBeTruthy(); 30 | }); 31 | 32 | test('mobile actions emit delete on delete', () => { 33 | getMobileActions().vm.$emit('delete'); 34 | 35 | expect(wrapper.emitted('delete')).toBeTruthy(); 36 | }); 37 | 38 | test('desktop actions emit edit on edit', () => { 39 | getDesktopActions().vm.$emit('edit'); 40 | 41 | expect(wrapper.emitted('edit')).toBeTruthy(); 42 | }); 43 | 44 | test('desktop actions emit delete on delete', () => { 45 | getDesktopActions().vm.$emit('delete'); 46 | 47 | expect(wrapper.emitted('delete')).toBeTruthy(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/VAnswerActions/VAnswerActions.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | -------------------------------------------------------------------------------- /src/components/VAnswerActionsDesktop/VAnswerActionsDesktop.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VAnswerActionsDesktop from '@/components/VAnswerActionsDesktop/VAnswerActionsDesktop.vue'; 3 | 4 | export default { 5 | title: 'AnswerActions/VAnswerActionsDesktop', 6 | component: VAnswerActionsDesktop, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VAnswerActionsDesktop }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | 20 | args: { 21 | allowDelete: true, 22 | allowEdit: true, 23 | deleteTime: 10, 24 | editTime: 30, 25 | }, 26 | }; 27 | 28 | export const DeletePassed = { 29 | render: Template, 30 | 31 | args: { 32 | allowDelete: false, 33 | allowEdit: true, 34 | deleteTime: 0, 35 | editTime: 30, 36 | }, 37 | }; 38 | 39 | export const AllPassed = { 40 | render: Template, 41 | 42 | args: { 43 | allowDelete: false, 44 | allowEdit: false, 45 | deleteTime: 10, 46 | editTime: 30, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/VAnswerActionsDesktop/VAnswerActionsDesktop.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import type { VueWrapper } from '@vue/test-utils'; 4 | import VAnswerActionsDesktop from './VAnswerActionsDesktop.vue'; 5 | 6 | describe('VAnswerActionsDesktop', () => { 7 | let wrapper: VueWrapper>; 8 | 9 | beforeEach(() => { 10 | wrapper = mount(VAnswerActionsDesktop, { 11 | shallow: true, 12 | global: { 13 | stubs: { 14 | VFloat: false, 15 | }, 16 | }, 17 | }); 18 | }); 19 | 20 | const getDeleteWrapper = () => { 21 | return wrapper.find('[data-testid="delete"]'); 22 | }; 23 | 24 | const getEditWrapper = () => { 25 | return wrapper.find('[data-testid="edit"]'); 26 | }; 27 | 28 | test('delete emits delete on click', async () => { 29 | await getDeleteWrapper().trigger('click'); 30 | 31 | expect(wrapper.emitted('delete')).toBeTruthy(); 32 | }); 33 | 34 | test('edit emits edit on click', async () => { 35 | await getEditWrapper().trigger('click'); 36 | 37 | expect(wrapper.emitted('edit')).toBeTruthy(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/components/VAnswerActionsDesktop/VAnswerActionsDesktop.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | -------------------------------------------------------------------------------- /src/components/VAnswerActionsMobile/VAnswerActionsMobile.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VAnswerActionsMobile from '@/components/VAnswerActionsMobile/VAnswerActionsMobile.vue'; 3 | 4 | export default { 5 | title: 'AnswerActions/VAnswerActionsMobile', 6 | component: VAnswerActionsMobile, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VAnswerActionsMobile }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | 20 | args: { 21 | allowDelete: true, 22 | allowEdit: true, 23 | deleteTime: 10, 24 | editTime: 30, 25 | }, 26 | }; 27 | 28 | export const DeletePassed = { 29 | render: Template, 30 | 31 | args: { 32 | allowDelete: false, 33 | allowEdit: true, 34 | deleteTime: 0, 35 | editTime: 30, 36 | }, 37 | }; 38 | 39 | export const AllPassed = { 40 | render: Template, 41 | 42 | args: { 43 | allowDelete: false, 44 | allowEdit: false, 45 | deleteTime: 10, 46 | editTime: 30, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /src/components/VAnswerActionsMobile/VAnswerActionsMobile.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import type { VueWrapper } from '@vue/test-utils'; 4 | import VAnswerActionsMobile from './VAnswerActionsMobile.vue'; 5 | import VCard from '@/components/VCard/VCard.vue'; 6 | 7 | describe('VAnswerActionsMobile', () => { 8 | let wrapper: VueWrapper>; 9 | 10 | beforeEach(() => { 11 | wrapper = mount(VAnswerActionsMobile, { 12 | shallow: true, 13 | global: { 14 | stubs: { 15 | VFloat: VCard, 16 | }, 17 | }, 18 | }); 19 | }); 20 | 21 | const getDeleteWrapper = () => { 22 | return wrapper.find('[data-testid="delete"]'); 23 | }; 24 | 25 | const getEditWrapper = () => { 26 | return wrapper.find('[data-testid="edit"]'); 27 | }; 28 | 29 | test('delete emits delete on click', async () => { 30 | await getDeleteWrapper().trigger('click'); 31 | 32 | expect(wrapper.emitted('delete')).toBeTruthy(); 33 | }); 34 | 35 | test('edit emits edit on click', async () => { 36 | await getEditWrapper().trigger('click'); 37 | 38 | expect(wrapper.emitted('edit')).toBeTruthy(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/components/VAnswerActionsMobile/VAnswerActionsMobile.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | -------------------------------------------------------------------------------- /src/components/VAvatar/VAvatar.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VAvatar from '@/components/VAvatar/VAvatar.vue'; 3 | import { faker } from '@faker-js/faker'; 4 | 5 | export default { 6 | title: 'UI/VAvatar', 7 | component: VAvatar, 8 | } as Meta; 9 | 10 | const Template: StoryFn = (args) => ({ 11 | components: { VAvatar }, 12 | setup() { 13 | return { args }; 14 | }, 15 | template: '', 16 | }); 17 | 18 | export const Default = { 19 | render: Template, 20 | 21 | args: { 22 | userId: faker.string.uuid(), 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/VAvatar/VAvatar.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import VAvatar from '@/components/VAvatar/VAvatar.vue'; 3 | import { mount, VueWrapper } from '@vue/test-utils'; 4 | import { faker } from '@faker-js/faker'; 5 | 6 | const defaultProps = { 7 | userId: faker.string.uuid(), 8 | }; 9 | 10 | describe('VAvatar', () => { 11 | let wrapper: VueWrapper; 12 | 13 | beforeEach(() => { 14 | wrapper = mount(VAvatar, { shallow: true, props: defaultProps }); 15 | }); 16 | 17 | test('Has image source', () => { 18 | expect(wrapper.attributes('src')).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/VAvatar/VAvatar.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 38 | 39 | 44 | -------------------------------------------------------------------------------- /src/components/VAvatarSettings/VAvatarSettings.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VAvatarSettings from '@/components/VAvatarSettings/VAvatarSettings.vue'; 3 | 4 | export default { 5 | title: 'Settings/VAvatarSettings', 6 | component: VAvatarSettings, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VAvatarSettings }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/VBreadcrumbs/VBreadcrumbs.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 36 | -------------------------------------------------------------------------------- /src/components/VButton/VButton.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VButton from '@/components/VButton/VButton.vue'; 3 | 4 | export default { 5 | title: 'Forms/VButton', 6 | component: VButton, 7 | argTypes: { 8 | appearance: { 9 | control: 'select', 10 | options: ['primary', 'secondary', 'link'], 11 | }, 12 | type: { 13 | control: 'select', 14 | options: ['big', 'inline'], 15 | }, 16 | }, 17 | } as Meta; 18 | 19 | const Template: StoryFn = (args) => ({ 20 | components: { VButton }, 21 | setup() { 22 | return { args }; 23 | }, 24 | template: 'Press me', 25 | }); 26 | 27 | export const Primary = { 28 | render: Template, 29 | args: { 30 | appearance: 'primary', 31 | type: 'big', 32 | }, 33 | }; 34 | 35 | export const Secondary = { 36 | render: Template, 37 | args: { 38 | appearance: 'secondary', 39 | type: 'big', 40 | }, 41 | }; 42 | 43 | export const Link = { 44 | render: Template, 45 | args: { 46 | appearance: 'link', 47 | type: 'big', 48 | }, 49 | }; 50 | 51 | export const Inline = { 52 | render: Template, 53 | args: { 54 | appearance: 'primary', 55 | type: 'inline', 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/VButton/VButton.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import VButton from '@/components/VButton/VButton.vue'; 3 | import { mount, VueWrapper } from '@vue/test-utils'; 4 | 5 | describe('VButton', () => { 6 | let wrapper: VueWrapper; 7 | 8 | beforeEach(() => { 9 | wrapper = mount(VButton, { shallow: true, props: {} }); 10 | }); 11 | 12 | test('button visual can be link', () => { 13 | wrapper = mount(VButton, { shallow: true, props: { appearance: 'link' } }); 14 | 15 | expect(wrapper.classes('Button_Appearance_Link')).toBe(true); 16 | }); 17 | 18 | test('button visual can be primary', () => { 19 | wrapper = mount(VButton, { 20 | shallow: true, 21 | props: { appearance: 'primary' }, 22 | }); 23 | 24 | expect(wrapper.classes('Button_Appearance_Primary')).toBe(true); 25 | }); 26 | 27 | test('button visual can be secondary', () => { 28 | wrapper = mount(VButton, { 29 | shallow: true, 30 | props: { appearance: 'secondary' }, 31 | }); 32 | 33 | expect(wrapper.classes('Button_Appearance_Secondary')).toBe(true); 34 | }); 35 | 36 | test('button has primary appearance by default', () => { 37 | wrapper = mount(VButton, { shallow: true }); 38 | 39 | expect(wrapper.classes('Button_Appearance_Primary')).toBe(true); 40 | }); 41 | 42 | test('can render as a different tag', () => { 43 | wrapper = mount(VButton, { shallow: true, props: { tag: 'a' } }); 44 | 45 | expect(wrapper.element.tagName).toBe('A'); 46 | }); 47 | 48 | test('renders as button tag by default', () => { 49 | wrapper = mount(VButton, { shallow: true }); 50 | 51 | expect(wrapper.element.tagName).toBe('BUTTON'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/components/VButton/VButton.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 21 | 22 | 36 | 37 | 64 | -------------------------------------------------------------------------------- /src/components/VCard/VCard.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VCard from '@/components/VCard/VCard.vue'; 3 | 4 | export default { 5 | title: 'UI/VCard', 6 | component: VCard, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VCard }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: 'This is card', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/VCard/VCard.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | 31 | 37 | -------------------------------------------------------------------------------- /src/components/VCertificate/VCertificate.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VCertificate from '@/components/VCertificate/VCertificate.vue'; 3 | import { mockDiplomaData } from '@/mocks/mockDiploma'; 4 | 5 | export default { 6 | title: 'UI/VCertificate', 7 | component: VCertificate, 8 | } as Meta; 9 | 10 | const Template: StoryFn = (args) => ({ 11 | components: { VCertificate }, 12 | setup() { 13 | return { args }; 14 | }, 15 | template: '', 16 | }); 17 | 18 | export const En = { 19 | render: Template, 20 | 21 | args: { 22 | certificate: { ...mockDiplomaData(), language: 'EN' }, 23 | }, 24 | }; 25 | 26 | export const Ru = { 27 | render: Template, 28 | 29 | args: { 30 | certificate: { ...mockDiplomaData(), language: 'RU' }, 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/VCertificate/VCertificate.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 41 | -------------------------------------------------------------------------------- /src/components/VCertificateCard/VCertificateCard.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VCertificateCard from '@/components/VCertificateCard/VCertificateCard.vue'; 3 | import { mockDiplomaData, mockDiplomaSet } from '@/mocks/mockDiploma'; 4 | 5 | export default { 6 | title: 'UI/VCertificateCard', 7 | component: VCertificateCard, 8 | } as Meta; 9 | 10 | const Template: StoryFn = (args) => ({ 11 | components: { VCertificateCard }, 12 | setup() { 13 | return { args }; 14 | }, 15 | template: 'This is card', 16 | }); 17 | 18 | const diplomas = mockDiplomaSet(mockDiplomaData()); 19 | 20 | export const Default = { 21 | render: Template, 22 | 23 | args: { 24 | course: diplomas[0].course.name, 25 | certificates: diplomas.map((diploma) => { 26 | diploma.image = 'https://picsum.photos/1480/1048'; 27 | return diploma; 28 | }), 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/VCertificateCard/VCertificateCard.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import type { VueWrapper } from '@vue/test-utils'; 4 | import VCertificateCard from './VCertificateCard.vue'; 5 | import type VCertificate from '@/components/VCertificate/VCertificate.vue'; 6 | import { mockDiplomaData, mockDiplomaSet } from '@/mocks/mockDiploma'; 7 | import VCard from '@/components/VCard/VCard.vue'; 8 | 9 | const diplomas = mockDiplomaSet(mockDiplomaData()); 10 | 11 | const defaultProps = { 12 | course: diplomas[0].course.name, 13 | certificates: diplomas, 14 | }; 15 | 16 | describe('VCertificateCard', () => { 17 | let wrapper: VueWrapper>; 18 | 19 | beforeEach(() => { 20 | wrapper = mount(VCertificateCard, { 21 | global: { 22 | renderStubDefaultSlot: true, 23 | }, 24 | shallow: true, 25 | props: defaultProps, 26 | }); 27 | }); 28 | 29 | const getContainerWrapper = () => { 30 | return wrapper.findComponent('[data-testid="container"]'); 31 | }; 32 | 33 | const getCertificatesWrappers = () => { 34 | return wrapper.findAll('[data-testid="certificate"]'); 35 | }; 36 | 37 | const getCertificateWrapper = () => { 38 | return wrapper.findComponent( 39 | '[data-testid="certificate"]', 40 | ); 41 | }; 42 | 43 | test('has course name', () => { 44 | expect(getContainerWrapper().attributes('title')).toContain( 45 | defaultProps.course, 46 | ); 47 | }); 48 | 49 | test('passes props to VCertificate', () => { 50 | expect(getCertificateWrapper().props()).toStrictEqual({ 51 | certificate: defaultProps.certificates[0], 52 | }); 53 | }); 54 | 55 | test('displays certificates', () => { 56 | expect(getCertificatesWrappers()).toHaveLength( 57 | defaultProps.certificates.length, 58 | ); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/components/VCertificateCard/VCertificateCard.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | -------------------------------------------------------------------------------- /src/components/VCertificateSettings/VCertificateSettings.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VCertificateSettings from '@/components/VCertificateSettings/VCertificateSettings.vue'; 3 | import { userKeys } from '@/query'; 4 | import { useQueryClient } from '@tanstack/vue-query'; 5 | import { mockUserSafe } from '@/mocks/mockUserSafe'; 6 | 7 | export default { 8 | title: 'Settings/VCertificateSettings', 9 | component: VCertificateSettings, 10 | } as Meta; 11 | 12 | const Template: StoryFn = (args) => ({ 13 | components: { VCertificateSettings }, 14 | setup() { 15 | const queryClient = useQueryClient(); 16 | 17 | queryClient.setQueryData(userKeys.me(), mockUserSafe()); 18 | 19 | return { args }; 20 | }, 21 | template: '', 22 | }); 23 | 24 | export const Default = { 25 | render: Template, 26 | args: {}, 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/VCover/VCover.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | 3 | import VCover from '@/components/VCover/VCover.vue'; 4 | 5 | export default { 6 | title: 'UI/VCover', 7 | component: VCover, 8 | } as Meta; 9 | 10 | const Template: StoryFn = (args) => ({ 11 | components: { VCover }, 12 | setup() { 13 | return { args }; 14 | }, 15 | template: '', 16 | }); 17 | 18 | export const AutoCover = { 19 | render: Template, 20 | 21 | args: { 22 | name: 'Cool Course', 23 | }, 24 | }; 25 | 26 | export const RealCover = { 27 | render: Template, 28 | 29 | args: { 30 | name: 'Cool Course', 31 | image: 'https://picsum.photos/1500/600', 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/VCover/VCover.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import type { VueWrapper } from '@vue/test-utils'; 4 | import VCover from './VCover.vue'; 5 | import { faker } from '@faker-js/faker'; 6 | 7 | const studyName = faker.commerce.productName(); 8 | const studyAdditionalInfo = faker.commerce.productAdjective(); 9 | 10 | const name = `${studyName} (${studyAdditionalInfo})`; 11 | 12 | const defaultProps = { 13 | name, 14 | }; 15 | 16 | describe('VCover', () => { 17 | let wrapper: VueWrapper>; 18 | 19 | beforeEach(() => { 20 | wrapper = mount(VCover, { 21 | shallow: true, 22 | props: defaultProps, 23 | global: { stubs: { VHeading: false } }, 24 | }); 25 | }); 26 | 27 | const getAutoCoverWrapper = () => { 28 | return wrapper.find('[data-testid="auto-cover"]'); 29 | }; 30 | const getImageCoverWrapper = () => { 31 | return wrapper.find('[data-testid="image-cover"]'); 32 | }; 33 | 34 | test('auto cover has name without additional info', () => { 35 | expect(getAutoCoverWrapper().text()).toBe(studyName); 36 | }); 37 | 38 | test('has real cover if defined', () => { 39 | wrapper = mount(VCover, { 40 | shallow: true, 41 | props: { ...defaultProps, image: 'image' }, 42 | }); 43 | 44 | expect(getImageCoverWrapper().exists()).toBe(true); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/VCover/VCover.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 46 | -------------------------------------------------------------------------------- /src/components/VCreateAnswer/VCreateAnswer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | 28 | 40 | -------------------------------------------------------------------------------- /src/components/VCrossChecks/VCrossChecks.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 61 | -------------------------------------------------------------------------------- /src/components/VDetails/VDetails.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 37 | -------------------------------------------------------------------------------- /src/components/VExistingAnswer/VExistingAnswerProvider.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /src/components/VExistingAnswer/index.ts: -------------------------------------------------------------------------------- 1 | import VExistingAnswerProvider from './VExistingAnswerProvider.vue'; 2 | 3 | const VExistingAnswer = VExistingAnswerProvider; 4 | 5 | export default VExistingAnswer; 6 | -------------------------------------------------------------------------------- /src/components/VFeedbackGuide/VFeedbackGuide.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VFeedbackGuide from '@/components/VFeedbackGuide/VFeedbackGuide.vue'; 3 | 4 | export default { 5 | title: 'UI/VFeedbackGuide', 6 | component: VFeedbackGuide, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VFeedbackGuide }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/VFeedbackGuide/VFeedbackGuide.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /src/components/VFloat/VFloat.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VFloat from '@/components/VFloat/VFloat.vue'; 3 | 4 | export default { 5 | title: 'UI/VFloat', 6 | component: VFloat, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VFloat }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: 'Floating tip!', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/VFloat/VFloat.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /src/components/VHeader/VHeader.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VHeader from './VHeader.vue'; 3 | 4 | export default { 5 | title: 'VHeader', 6 | component: VHeader, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VHeader }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/VHeader/VHeader.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import { mount, RouterLinkStub, VueWrapper } from '@vue/test-utils'; 3 | import VHeader from './VHeader.vue'; 4 | 5 | describe('VHeader', () => { 6 | let wrapper: VueWrapper>; 7 | 8 | const mountComponent = () => { 9 | wrapper = mount(VHeader, { 10 | shallow: true, 11 | global: { 12 | stubs: { 13 | RouterLink: RouterLinkStub, 14 | }, 15 | }, 16 | }); 17 | }; 18 | 19 | beforeEach(() => { 20 | mountComponent(); 21 | }); 22 | 23 | const getLogoWrapper = () => wrapper.findComponent(RouterLinkStub); 24 | 25 | test('has logo that leads to home page', () => { 26 | expect(getLogoWrapper().exists()).toBe(true); 27 | expect(getLogoWrapper().props().to).toBe('/'); 28 | }); 29 | 30 | test('renders slot content', () => { 31 | const slotContent = '
Test Slot
'; 32 | wrapper = mount(VHeader, { 33 | shallow: true, 34 | slots: { 35 | default: slotContent, 36 | }, 37 | global: { 38 | stubs: { 39 | RouterLink: RouterLinkStub, 40 | }, 41 | }, 42 | }); 43 | 44 | expect(wrapper.html()).toContain(slotContent); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/VHeader/VHeader.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/components/VHeading/VHeading.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VHeading from '@/components/VHeading/VHeading.vue'; 3 | 4 | export default { 5 | title: 'UI/VHeading', 6 | component: VHeading, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VHeading }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: 'VHeading', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: {}, 20 | }; 21 | 22 | export const H1 = { 23 | render: Template, 24 | 25 | args: { 26 | tag: 'h1', 27 | }, 28 | }; 29 | 30 | export const H2 = { 31 | render: Template, 32 | 33 | args: { 34 | tag: 'h2', 35 | }, 36 | }; 37 | 38 | export const H3 = { 39 | render: Template, 40 | 41 | args: { 42 | tag: 'h3', 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/VHeading/VHeading.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import type { VueWrapper } from '@vue/test-utils'; 4 | import VHeading from './VHeading.vue'; 5 | import { faker } from '@faker-js/faker'; 6 | 7 | const defaultProps = {}; 8 | 9 | describe('VHeading', () => { 10 | let wrapper: VueWrapper>; 11 | 12 | beforeEach(() => { 13 | wrapper = mount(VHeading, { shallow: true, props: defaultProps }); 14 | }); 15 | 16 | test('element is h2 by default', () => { 17 | expect(wrapper.element.tagName.toLowerCase()).toBe('h2'); 18 | }); 19 | 20 | test('element can be changed using props', () => { 21 | wrapper = mount(VHeading, { shallow: true, props: { tag: 'h1' } }); 22 | 23 | expect(wrapper.element.tagName.toLowerCase()).toBe('h1'); 24 | }); 25 | 26 | test('heading has slot', () => { 27 | const content = faker.finance.accountNumber(); 28 | 29 | wrapper = mount(VHeading, { 30 | shallow: true, 31 | slots: { default: content }, 32 | props: defaultProps, 33 | }); 34 | 35 | expect(wrapper.text()).toBe(content); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/VHeading/VHeading.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | -------------------------------------------------------------------------------- /src/components/VHtmlContent/VHtmlContent.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VHtmlContent from '@/components/VHtmlContent/VHtmlContent.vue'; 3 | import { mockContent } from '@/mocks/mockContent'; 4 | 5 | export default { 6 | title: 'Answer/VHtmlContent', 7 | component: VHtmlContent, 8 | } as Meta; 9 | 10 | const Template: StoryFn = (args) => ({ 11 | components: { VHtmlContent }, 12 | setup() { 13 | return { args }; 14 | }, 15 | template: '', 16 | }); 17 | 18 | export const Default = { 19 | render: Template, 20 | 21 | args: { 22 | content: mockContent(), 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/components/VHtmlContent/VHtmlContent.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import type { VueWrapper } from '@vue/test-utils'; 4 | import VHtmlContent from './VHtmlContent.vue'; 5 | import { faker } from '@faker-js/faker'; 6 | 7 | const defaultProps = { 8 | content: `

${faker.lorem.words(10)}

`, 9 | }; 10 | 11 | describe('VHtmlContent', () => { 12 | let wrapper: VueWrapper>; 13 | 14 | beforeEach(() => { 15 | wrapper = mount(VHtmlContent, { shallow: true, props: defaultProps }); 16 | }); 17 | 18 | test('renders html from props', () => { 19 | expect(wrapper.html()).toContain(defaultProps.content); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/VHtmlContent/VHtmlContent.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /src/components/VLinksSettings/VLinksSettings.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VLinksSettings from '@/components/VLinksSettings/VLinksSettings.vue'; 3 | import { useQueryClient } from '@tanstack/vue-query'; 4 | import { userKeys } from '@/query'; 5 | import { mockUserSafe } from '@/mocks/mockUserSafe'; 6 | 7 | export default { 8 | title: 'Settings/VLinksSettings', 9 | component: VLinksSettings, 10 | } as Meta; 11 | 12 | const Template: StoryFn = (args) => ({ 13 | components: { VLinksSettings }, 14 | setup() { 15 | const queryClient = useQueryClient(); 16 | 17 | queryClient.setQueryData(userKeys.me(), mockUserSafe()); 18 | 19 | return { args }; 20 | }, 21 | template: '', 22 | }); 23 | 24 | export const Default = { 25 | render: Template, 26 | args: {}, 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/VLinksSettings/VLinksSettings.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 61 | -------------------------------------------------------------------------------- /src/components/VLoader/VLoader.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VLoader from '@/components/VLoader/VLoader.vue'; 3 | 4 | export default { 5 | title: 'UI/VLoader', 6 | component: VLoader, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VLoader }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/VLoader/VLoader.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | 33 | 49 | -------------------------------------------------------------------------------- /src/components/VLoginLink/VLoginLink.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 50 | -------------------------------------------------------------------------------- /src/components/VLogo/VLogo.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VLogo from './VLogo.vue'; 3 | 4 | export default { 5 | title: 'UI/VLogo', 6 | component: VLogo, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VLogo }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/VModuleCard/VModuleCard.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /src/components/VPasswordSettings/VPasswordSettings.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VPasswordSettings from '@/components/VPasswordSettings/VPasswordSettings.vue'; 3 | 4 | export default { 5 | title: 'Settings/VPasswordSettings', 6 | component: VPasswordSettings, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VPasswordSettings }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: {}, 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/VPasswordSettings/VPasswordSettings.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 52 | -------------------------------------------------------------------------------- /src/components/VPill/VPill.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/components/VPill/VPillItem.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/components/VPillHomework/VPillHomework.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 67 | -------------------------------------------------------------------------------- /src/components/VPreferencesSettings/VPreferencesSettings.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 43 | -------------------------------------------------------------------------------- /src/components/VPreloader/VPreloader.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VPreloader from '@/components/VPreloader/VPreloader.vue'; 3 | 4 | export default { 5 | title: 'UI/VPreloader', 6 | component: VPreloader, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VPreloader }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/VPreloader/VPreloader.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/components/VProfileMenu/VProfileMenu.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VProfileMenu from '@/components/VProfileMenu/VProfileMenu.vue'; 3 | import { faker } from '@faker-js/faker'; 4 | import { userKeys } from '@/query'; 5 | import { useQueryClient } from '@tanstack/vue-query'; 6 | 7 | export default { 8 | title: 'UI/VProfileMenu', 9 | component: VProfileMenu, 10 | } as Meta; 11 | 12 | const Template: StoryFn = (args) => ({ 13 | components: { VProfileMenu }, 14 | setup() { 15 | const queryClient = useQueryClient(); 16 | queryClient.setQueryData(userKeys.me(), { 17 | username: faker.internet.email(), 18 | firstName: faker.person.firstName(), 19 | lastName: faker.person.lastName(), 20 | uuid: faker.string.uuid(), 21 | }); 22 | return { args }; 23 | }, 24 | template: '', 25 | }); 26 | 27 | export const Default = { 28 | render: Template, 29 | args: {}, 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/VRadioSwitch/VRadioSwitch.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VRadioSwitch from './VRadioSwitch.vue'; 3 | 4 | export default { 5 | title: 'UI/VRadioSwitch', 6 | component: VRadioSwitch, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VRadioSwitch }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: { 20 | options: [ 21 | { value: 'option1', label: 'Option 1', icon: 'IconComponent1' }, 22 | { value: 'option2', label: 'Option 2', icon: 'IconComponent2' }, 23 | ], 24 | modelValue: 'option1', 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/VRadioSwitch/VRadioSwitch.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | 40 | 41 | 49 | -------------------------------------------------------------------------------- /src/components/VReactions/VReactions.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VReactions from './VReactions.vue'; 3 | import VCard from '@/components/VCard/VCard.vue'; 4 | import { faker } from '@faker-js/faker'; 5 | import { mockReactionDetailed } from '@/mocks/mockReactionDetailed'; 6 | 7 | export default { 8 | title: 'Reactions/VReactions', 9 | component: VReactions, 10 | } as Meta; 11 | 12 | const Template: StoryFn = (args) => ({ 13 | components: { VReactions, VCard }, 14 | setup() { 15 | return { args }; 16 | }, 17 | argTypes: { 18 | reactions: { control: 'object' }, 19 | open: { control: 'boolean' }, 20 | disabled: { control: 'boolean' }, 21 | }, 22 | template: 23 | '', 24 | }); 25 | 26 | export const Default = { 27 | render: Template, 28 | args: { 29 | reactions: faker.helpers.multiple(mockReactionDetailed, { count: 15 }), 30 | open: false, 31 | disabled: false, 32 | }, 33 | }; 34 | 35 | export const Open = { 36 | render: Template, 37 | args: { 38 | reactions: faker.helpers.multiple(mockReactionDetailed, { count: 15 }), 39 | open: true, 40 | disabled: false, 41 | }, 42 | }; 43 | 44 | export const Disabled = { 45 | render: Template, 46 | args: { 47 | reactions: faker.helpers.multiple(mockReactionDetailed, { count: 15 }), 48 | open: true, 49 | disabled: true, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /src/components/VReactions/components/VReaction/VReaction.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VReaction from './VReaction.vue'; 3 | import VCard from '@/components/VCard/VCard.vue'; 4 | import { faker } from '@faker-js/faker'; 5 | import { ALLOWED_REACTIONS } from '@/components/VReactions/VReactions.vue'; 6 | import { mockReactionDetailed } from '@/mocks/mockReactionDetailed'; 7 | import { times } from 'lodash-es'; 8 | 9 | const userId = faker.string.uuid(); 10 | const emoji = faker.helpers.arrayElement(ALLOWED_REACTIONS); 11 | 12 | export default { 13 | title: 'Reactions/VReaction', 14 | component: VReaction, 15 | } as Meta; 16 | 17 | const Template: StoryFn = (args) => ({ 18 | components: { VReaction, VCard }, 19 | setup() { 20 | return { args }; 21 | }, 22 | template: '', 23 | }); 24 | 25 | export const Default = { 26 | render: Template, 27 | args: { 28 | userId, 29 | emoji, 30 | reactions: times(faker.number.int({ min: 1, max: 10 }), () => 31 | mockReactionDetailed(), 32 | ), 33 | }, 34 | }; 35 | 36 | export const Own = { 37 | render: Template, 38 | args: { 39 | userId, 40 | emoji, 41 | reactions: [ 42 | ...times(1, () => mockReactionDetailed({ author: { uuid: userId } })), 43 | ...times(3, () => mockReactionDetailed()), 44 | ], 45 | }, 46 | }; 47 | 48 | export const Disabled = { 49 | render: Template, 50 | args: { 51 | userId, 52 | emoji, 53 | reactions: times(faker.number.int({ min: 1, max: 10 }), () => 54 | mockReactionDetailed(), 55 | ), 56 | disabled: true, 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/VTag/VTag.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/VTextEditor/VTextEditor.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VTextEditor from '@/components/VTextEditor/VTextEditor.vue'; 3 | 4 | export default { 5 | title: 'Forms/VTextEditor', 6 | component: VTextEditor, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VTextEditor }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: {}, 20 | }; 21 | 22 | export const Placeholder = { 23 | render: Template, 24 | 25 | args: { 26 | placeholder: 'You can change placeholder text', 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/VTextInput/VTextInput.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VTextInput from '@/components/VTextInput/VTextInput.vue'; 3 | 4 | export default { 5 | title: 'Forms/VTextInput', 6 | component: VTextInput, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VTextInput }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | 20 | args: { 21 | tip: 'This is a tooltip for text', 22 | label: 'Basic text input', 23 | }, 24 | }; 25 | 26 | export const ErrorWithMessage = { 27 | render: Template, 28 | 29 | args: { 30 | error: 'This is error', 31 | }, 32 | }; 33 | 34 | export const ErrorWithoutMessage = { 35 | render: Template, 36 | 37 | args: { 38 | error: true, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/VThread/VThreadProvider.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 38 | -------------------------------------------------------------------------------- /src/components/VThread/index.ts: -------------------------------------------------------------------------------- 1 | import VThreadProvider from './VThreadProvider.vue'; 2 | 3 | export default VThreadProvider; 4 | -------------------------------------------------------------------------------- /src/components/VToast/VToast.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VToast from '@/components/VToast/VToast.vue'; 3 | 4 | export default { 5 | title: 'Toasts/VToast', 6 | component: VToast, 7 | } as Meta; 8 | 9 | const Template: StoryFn = (args) => ({ 10 | components: { VToast }, 11 | setup() { 12 | return { args }; 13 | }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | args: { text: 'This is a message!' }, 20 | }; 21 | 22 | export const Error = { 23 | render: Template, 24 | args: { text: 'This is a error!', type: 'error' }, 25 | }; 26 | 27 | export const Success = { 28 | render: Template, 29 | args: { text: 'This is a success!', type: 'success' }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/VToast/VToast.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import { mount, VueWrapper } from '@vue/test-utils'; 3 | import { faker } from '@faker-js/faker'; 4 | import VToast from '@/components/VToast/VToast.vue'; 5 | import { uuid } from '@/utils/uuid'; 6 | 7 | const defaultProps = { 8 | text: faker.lorem.sentence(), 9 | id: uuid(), 10 | lifetime: 1000, 11 | }; 12 | 13 | describe('VToast', () => { 14 | let wrapper: VueWrapper>; 15 | 16 | beforeEach(() => { 17 | wrapper = mount(VToast, { shallow: true, props: defaultProps }); 18 | }); 19 | 20 | test('displays correct message', () => { 21 | expect(wrapper.text()).toBe(defaultProps.text); 22 | }); 23 | 24 | test('emits delete with id on click', async () => { 25 | await wrapper.trigger('click'); 26 | 27 | expect(wrapper.emitted('delete')).toStrictEqual([[defaultProps.id]]); 28 | }); 29 | 30 | test.todo('emits delete after liftime end'); 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/VToast/VToast.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 47 | -------------------------------------------------------------------------------- /src/components/VToastFeed/VToastFeed.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VToastFeed from '@/components/VToastFeed/VToastFeed.vue'; 3 | import useToasts from '@/stores/toasts'; 4 | import { faker } from '@faker-js/faker'; 5 | 6 | export default { 7 | title: 'Toasts/VToastFeed', 8 | component: VToastFeed, 9 | } as Meta; 10 | 11 | const Template: StoryFn = (args) => ({ 12 | components: { VToastFeed }, 13 | setup() { 14 | const toasts = useToasts(); 15 | toasts.$reset(); 16 | [...Array(10)].forEach(() => toasts.addMessage(faker.lorem.sentence())); 17 | 18 | return { args }; 19 | }, 20 | template: '', 21 | }); 22 | 23 | export const Default = { 24 | render: Template, 25 | args: {}, 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/VToastFeed/VToastFeed.test.ts: -------------------------------------------------------------------------------- 1 | import { mount, VueWrapper } from '@vue/test-utils'; 2 | import VToastFeed from '@/components/VToastFeed/VToastFeed.vue'; 3 | import { faker } from '@faker-js/faker'; 4 | import useToasts, { VToastMessage } from '@/stores/toasts'; 5 | import type VToast from '@/components/VToast/VToast.vue'; 6 | import { createTestingPinia } from '@pinia/testing'; 7 | import { vi, describe, beforeEach, expect, test } from 'vitest'; 8 | 9 | const MESSAGES = 10; 10 | 11 | describe('VToastFeed', () => { 12 | let wrapper: VueWrapper; 13 | 14 | beforeEach(async () => { 15 | const messages = [...Array(MESSAGES)].map(() => { 16 | return new VToastMessage(faker.lorem.sentence(), 'success', 10000); 17 | }); 18 | 19 | wrapper = mount(VToastFeed, { 20 | shallow: true, 21 | global: { 22 | plugins: [ 23 | createTestingPinia({ 24 | initialState: { 25 | toasts: { 26 | messages, 27 | }, 28 | }, 29 | createSpy: vi.fn, 30 | }), 31 | ], 32 | }, 33 | }); 34 | }); 35 | 36 | const getAllToasts = () => { 37 | return wrapper.findAll('[data-testid="toast"]'); 38 | }; 39 | 40 | const getFirstToast = () => { 41 | return wrapper.findComponent('[data-testid="toast"]'); 42 | }; 43 | 44 | test('VToast delete event calls removeMessage with correct id', async () => { 45 | const toasts = useToasts(); 46 | 47 | (getFirstToast() as VueWrapper).vm.$emit('delete', toasts.messages[0].id); 48 | 49 | expect(toasts.removeMessage).toHaveBeenCalledTimes(1); 50 | expect(toasts.removeMessage).toHaveBeenCalledWith(toasts.messages[0].id); 51 | }); 52 | 53 | test('Feed has correct ammount of toasts', async () => { 54 | expect(getAllToasts()).toHaveLength(MESSAGES); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/components/VToastFeed/VToastFeed.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | 28 | 40 | -------------------------------------------------------------------------------- /src/fonts.css: -------------------------------------------------------------------------------- 1 | /* Google fonts go straight to the head */ 2 | 3 | @font-face { 4 | font-family: 'PT Root UI'; 5 | src: 6 | url('assets/fonts/subset-PTRootUI-Medium.woff2') format('woff2'), 7 | url('assets/fonts/subset-PTRootUI-Medium.woff') format('woff'); 8 | font-weight: 500; 9 | font-style: normal; 10 | font-display: swap; 11 | } 12 | 13 | @font-face { 14 | font-family: 'PT Root UI'; 15 | src: 16 | url('assets/fonts/subset-PTRootUI-Light.woff2') format('woff2'), 17 | url('assets/fonts/subset-PTRootUI-Light.woff') format('woff'); 18 | font-weight: 300; 19 | font-style: normal; 20 | font-display: swap; 21 | } 22 | 23 | @font-face { 24 | font-family: 'PT Root UI'; 25 | src: 26 | url('assets/fonts/subset-PTRootUI-Bold.woff2') format('woff2'), 27 | url('assets/fonts/subset-PTRootUI-Bold.woff') format('woff'); 28 | font-weight: bold; 29 | font-style: normal; 30 | font-display: swap; 31 | } 32 | 33 | @font-face { 34 | font-family: 'PT Root UI'; 35 | src: 36 | url('assets/fonts/subset-PTRootUI-Regular.woff2') format('woff2'), 37 | url('assets/fonts/subset-PTRootUI-Regular.woff') format('woff'); 38 | font-weight: normal; 39 | font-style: normal; 40 | font-display: swap; 41 | } 42 | -------------------------------------------------------------------------------- /src/hooks/useChatra.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, ref } from 'vue'; 2 | 3 | type CustomWindow = typeof window & { Chatra: any }; 4 | 5 | export const useChatra = () => { 6 | const chatra: any = ref(undefined); 7 | 8 | onMounted(() => { 9 | chatra.value = (window as CustomWindow).Chatra; 10 | }); 11 | 12 | return { chatra }; 13 | }; 14 | -------------------------------------------------------------------------------- /src/layouts/VBaseLayout/VBaseLayout.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /src/layouts/VLoggedLayout/VLoggedLayout.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 48 | -------------------------------------------------------------------------------- /src/layouts/VPublicLayout/VPublicLayout.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './fonts.css'; 2 | import './style.css'; 3 | import { createApp } from 'vue'; 4 | import { createPinia } from 'pinia'; 5 | import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; 6 | import App from './App.vue'; 7 | import router from './router'; 8 | import FloatingVue from 'floating-vue'; 9 | import 'floating-vue/dist/style.css'; 10 | import * as Sentry from '@sentry/vue'; 11 | import AvatarCropper from 'vue-avatar-cropper'; 12 | import { VueQueryPlugin } from '@tanstack/vue-query'; 13 | 14 | const app = createApp(App); 15 | 16 | if (process.env.NODE_ENV === 'production') { 17 | Sentry.init({ 18 | app, 19 | dsn: 'https://c7d16bbe0bff41929238c6fefc3c6da0@app.glitchtip.com/10541', 20 | integrations: [ 21 | Sentry.browserTracingIntegration({ router }), 22 | Sentry.captureConsoleIntegration(), 23 | ], 24 | tracesSampleRate: 0.5, 25 | tracePropagationTargets: [ 26 | 'localhost', 27 | /^https:\/\/lms\.tough-dev\.school\/api\//, 28 | /^http:\/\/127\.0\.0\.1:8000\//, 29 | ], 30 | replaysSessionSampleRate: 0, 31 | replaysOnErrorSampleRate: 0.8, 32 | }); 33 | } 34 | 35 | export const vueQueryConfig = { 36 | enableDevtoolsV6Plugin: true, 37 | queryClientConfig: { 38 | defaultOptions: { 39 | queries: { 40 | refetchOnWindowFocus: false, 41 | staleTime: Infinity, 42 | retry: false, 43 | }, 44 | }, 45 | }, 46 | }; 47 | 48 | app.use(VueQueryPlugin, vueQueryConfig); 49 | 50 | app.use(FloatingVue); 51 | app.use(AvatarCropper); 52 | const pinia = createPinia(); 53 | pinia.use(piniaPluginPersistedstate); 54 | app.use(pinia); 55 | 56 | app.use(router); 57 | 58 | app.mount('#app'); 59 | -------------------------------------------------------------------------------- /src/mocks/VTransparentComponent.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/mocks/mockAnswer.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import { faker } from '@faker-js/faker'; 3 | import { mockUserSafe } from './mockUserSafe'; 4 | import htmlToMarkdown from '@/utils/htmlToMarkdown'; 5 | import { LOREM_CONTENT, mockContent } from './mockContent'; 6 | import type { AnswerTree } from '@/api/generated-api'; 7 | 8 | export const mockAnswer = ( 9 | payload: Partial = {}, 10 | ): Required => { 11 | const text = mockContent(LOREM_CONTENT); 12 | return { 13 | created: dayjs().toISOString(), 14 | modified: dayjs().toISOString(), 15 | slug: faker.string.uuid(), 16 | question: faker.string.uuid(), 17 | author: mockUserSafe(), 18 | text, 19 | src: htmlToMarkdown(text), 20 | has_descendants: false, 21 | reactions: [], 22 | is_editable: true, 23 | descendants: [], 24 | parent: faker.string.uuid(), 25 | ...payload, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/mocks/mockBreadcrumbs.ts: -------------------------------------------------------------------------------- 1 | import type { Breadcrumbs } from '@/api/generated-api'; 2 | import { mockModule } from './mockModule'; 3 | import { mockLMSCourse } from './mockLMSCourse'; 4 | import { mockLessonPlain } from './mockLessonPlain'; 5 | 6 | export const mockBreadcrumbs = (): Breadcrumbs => { 7 | return { 8 | module: mockModule(), 9 | course: mockLMSCourse(), 10 | lesson: mockLessonPlain(), 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/mocks/mockCase.ts: -------------------------------------------------------------------------------- 1 | export const STATIC_CAMEL_CASE_EXAMPLE = { 2 | basicExample: true, 3 | arrayExample: [ 4 | { arrayChildExample: 1 }, 5 | { arrayChildExample: 2 }, 6 | { arrayChildExample: 3 }, 7 | ], 8 | deepExample: { 9 | objectChildExample: true, 10 | }, 11 | }; 12 | 13 | export const STATIC_SNAKE_CASE_EXAMPLE = { 14 | basic_example: true, 15 | array_example: [ 16 | { array_child_example: 1 }, 17 | { array_child_example: 2 }, 18 | { array_child_example: 3 }, 19 | ], 20 | deep_example: { 21 | object_child_example: true, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /src/mocks/mockContent.ts: -------------------------------------------------------------------------------- 1 | export const HTML_CONTENT = 2 | '

Heading 1

Heading 2

Heading 3

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut varius justo, vitae accumsan ipsum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Curabitur felis metus, laoreet in scelerisque ac, faucibus at nulla. Sed sit amet vulputate urna. Praesent euismod non diam in luctus. Duis volutpat massa sed est auctor, at tincidunt odio facilisis. Integer placerat libero sit amet consectetur consectetur. Suspendisse ultrices nec erat eu porta. Pellentesque sed augue congue, tempus erat vitae, feugiat orci. Ut faucibus massa sollicitudin diam scelerisque efficitur. Suspendisse eget sapien vel purus scelerisque varius nec non sem. Ut ornare lobortis ultricies. Morbi ut iaculis orci. Phasellus sed massa vitae massa tincidunt mattis. Ut posuere facilisis lorem, rhoncus varius orci malesuada eu.

Suspendisse eget sapien vel purus scelerisque varius nec non sem. Ut ornare lobortis ultricies. Morbi ut iaculis orci. Phasellus sed massa vitae massa tincidunt mattis. Ut posuere facilisis lorem, rhoncus varius orci malesuada eu.

  1. Option 1

  2. Option 2

  3. Option 3

  • Option 1

  • Option 2

  • Option 3

'; 3 | export const LOREM_CONTENT = 4 | '

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

'; 5 | 6 | export const mockContent = (payload?: string) => 7 | payload ? payload : LOREM_CONTENT; 8 | -------------------------------------------------------------------------------- /src/mocks/mockCourse.ts: -------------------------------------------------------------------------------- 1 | import { type Course } from '@/api/generated-api'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | export const mockCourse = (payload: Partial = {}): Required => { 5 | return { 6 | cover: faker.image.url(), 7 | id: faker.number.int(), 8 | slug: faker.string.uuid(), 9 | name: faker.finance.accountName(), 10 | chat: faker.internet.url(), 11 | calendar_ios: faker.internet.url(), 12 | calendar_google: faker.internet.url(), 13 | home_page_slug: faker.string.uuid(), 14 | links: [ 15 | { 16 | name: 'test', 17 | url: faker.internet.url(), 18 | }, 19 | ], 20 | ...payload, 21 | }; 22 | }; 23 | 24 | export const STATIC_STUDY = { 25 | id: 32, 26 | slug: 'popug-4-vip', 27 | name: 'Асинхронная архитектура (всё включено)', 28 | homePageSlug: '23fe5823cd0c44e5a56fc7aa2e2439a4', 29 | }; 30 | -------------------------------------------------------------------------------- /src/mocks/mockDiploma.ts: -------------------------------------------------------------------------------- 1 | // #FIXME Split into separate modules 2 | 3 | import { faker } from '@faker-js/faker'; 4 | import { type Diploma, LanguageEnum } from '@/api/generated-api'; 5 | import { mockLocale } from './mockLocale'; 6 | import { mockUserSafe, STATIC_AUTHOR_1 } from './mockUserSafe'; 7 | 8 | export const mockDiplomaData = (): Diploma => { 9 | return { 10 | course: { 11 | name: faker.commerce.productName(), 12 | }, 13 | slug: faker.string.uuid(), 14 | language: mockLocale(), 15 | image: '/diploma-mock.jpg', 16 | student: mockUserSafe(), 17 | url: faker.internet.url(), 18 | }; 19 | }; 20 | 21 | export const mockDiplomaSet = (payload: Diploma): Diploma[] => { 22 | return Object.values(LanguageEnum).map((locale) => { 23 | return { 24 | ...payload, 25 | language: locale, 26 | course: { name: payload.course.name }, 27 | }; 28 | }); 29 | }; 30 | 31 | export const STATIC_DIPLOMA_1: Diploma = { 32 | ...mockDiplomaData(), 33 | course: { 34 | name: 'Amazing Course', 35 | }, 36 | student: STATIC_AUTHOR_1, 37 | }; 38 | 39 | export const STATIC_DIPLOMA_2: Diploma = { 40 | ...mockDiplomaData(), 41 | course: { 42 | name: 'Cool Course', 43 | }, 44 | student: STATIC_AUTHOR_1, 45 | }; 46 | 47 | export const STATIC_DIPLOMA_3: Diploma = { 48 | ...mockDiplomaData(), 49 | course: { 50 | name: 'Pro Course', 51 | }, 52 | student: STATIC_AUTHOR_1, 53 | }; 54 | 55 | export const STATIC_DIPLOMAS = [ 56 | STATIC_DIPLOMA_1, 57 | STATIC_DIPLOMA_2, 58 | STATIC_DIPLOMA_3, 59 | ]; 60 | -------------------------------------------------------------------------------- /src/mocks/mockEmoji.ts: -------------------------------------------------------------------------------- 1 | import { ALLOWED_REACTIONS } from '@/components/VReactions/VReactions.vue'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | export const mockEmoji = () => faker.helpers.arrayElement(ALLOWED_REACTIONS); 5 | -------------------------------------------------------------------------------- /src/mocks/mockLMSCourse.ts: -------------------------------------------------------------------------------- 1 | import type { LMSCourse } from '@/api/generated-api'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | export const mockLMSCourse = ( 5 | payload: Partial = {}, 6 | ): Required => { 7 | return { 8 | id: faker.number.int(), 9 | name: faker.lorem.words(3), 10 | slug: faker.string.uuid(), 11 | cover: faker.image.url(), 12 | chat: faker.internet.url(), 13 | calendar_ios: faker.internet.url(), 14 | calendar_google: faker.internet.url(), 15 | ...payload, 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/mocks/mockLessonPlain.ts: -------------------------------------------------------------------------------- 1 | import type { LessonPlain } from '@/api/generated-api'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | export const mockLessonPlain = ( 5 | payload: Partial = {}, 6 | ): Required => { 7 | return { 8 | id: faker.number.int(), 9 | ...payload, 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/mocks/mockLocale.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { LanguageEnum } from '@/api/generated-api'; 3 | 4 | export const mockLocale = () => 5 | faker.helpers.arrayElement(Object.values(LanguageEnum)); 6 | -------------------------------------------------------------------------------- /src/mocks/mockModule.ts: -------------------------------------------------------------------------------- 1 | import type { Module } from '@/api/generated-api'; 2 | import { faker } from '@faker-js/faker'; 3 | 4 | export const mockModule = (payload: Partial = {}): Required => { 5 | return { 6 | id: faker.number.int(), 7 | name: faker.lorem.words(3), 8 | description: faker.lorem.words(3), 9 | text: faker.lorem.words(3), 10 | ...payload, 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /src/mocks/mockQuestion.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { LOREM_CONTENT, mockContent } from './mockContent'; 3 | import type { Question } from '@/api/generated-api'; 4 | 5 | export const mockQuestion = ( 6 | payload: Partial = {}, 7 | ): Required => { 8 | return { 9 | deadline: faker.date.future().toISOString(), 10 | slug: faker.string.uuid(), 11 | name: faker.lorem.words(3), 12 | text: mockContent(LOREM_CONTENT), 13 | ...payload, 14 | }; 15 | }; 16 | 17 | export const STATIC_QUESTION = { 18 | slug: 'd89180cd-1bba-4fb9-873f-7b6ad6e3865e', 19 | name: 'Just a static question', 20 | text: mockContent(LOREM_CONTENT), 21 | }; 22 | -------------------------------------------------------------------------------- /src/mocks/mockReactionDetailed.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { mockEmoji } from './mockEmoji'; 3 | import { mockUserSafe, STATIC_AUTHOR_1, STATIC_AUTHOR_2 } from './mockUserSafe'; 4 | import type { ReactionDetailed } from '@/api/generated-api'; 5 | import { ReactionEmoji } from '@/components/VReactions/VReactions.vue'; 6 | 7 | export const mockReactionDetailed = ( 8 | payload: Partial = {}, 9 | ): ReactionDetailed => ({ 10 | slug: faker.string.uuid(), 11 | emoji: mockEmoji(), 12 | author: mockUserSafe(), 13 | answer: faker.string.uuid(), 14 | ...payload, 15 | }); 16 | 17 | export const STATIC_REACTION_1 = mockReactionDetailed({ 18 | author: STATIC_AUTHOR_2, 19 | emoji: ReactionEmoji.HAPPY, 20 | }); 21 | 22 | export const STATIC_REACTION_2 = mockReactionDetailed({ 23 | author: STATIC_AUTHOR_1, 24 | emoji: ReactionEmoji.HAPPY, 25 | }); 26 | 27 | export const STATIC_REACTION_3 = mockReactionDetailed({ 28 | author: STATIC_AUTHOR_1, 29 | emoji: ReactionEmoji.LIKE, 30 | }); 31 | 32 | export const STATIC_REACTIONS = [ 33 | STATIC_REACTION_1, 34 | STATIC_REACTION_2, 35 | STATIC_REACTION_3, 36 | ]; 37 | -------------------------------------------------------------------------------- /src/mocks/mockUserId.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | 3 | export const mockUserId = (payload?: string): string => 4 | payload ? payload : faker.string.uuid(); 5 | 6 | export const USER_1 = '72fd1310-89fa-4756-a88d-d60a60838aac'; 7 | export const USER_2 = '996efe59-8c77-40a6-9d0d-528f20c9580f'; 8 | export const USER_3 = '9697e414-3dba-4a44-8ea5-c83c0a11dcdf'; 9 | -------------------------------------------------------------------------------- /src/mocks/mockUserSafe.ts: -------------------------------------------------------------------------------- 1 | import type { UserSafe } from '@/api/generated-api'; 2 | import responseCaseMiddleware from '@/api/responseCaseMiddleware'; 3 | import { fakerRU as faker } from '@faker-js/faker'; 4 | 5 | export const mockUserSafe = ({ 6 | seed, 7 | payload, 8 | }: { 9 | seed?: number; 10 | payload?: Partial; 11 | } = {}): Required => { 12 | if (seed) faker.seed(seed); 13 | 14 | const data: Required = { 15 | uuid: faker.string.uuid(), 16 | first_name: faker.person.firstName(), 17 | last_name: faker.person.lastName(), 18 | first_name_en: faker.person.firstName(), 19 | last_name_en: faker.person.lastName(), 20 | avatar: faker.image.url(), 21 | ...payload, 22 | }; 23 | 24 | return responseCaseMiddleware(data) as Required; 25 | }; 26 | 27 | export const STATIC_AUTHOR_1: UserSafe = mockUserSafe({ seed: 1 }); 28 | 29 | export const STATIC_AUTHOR_2: UserSafe = mockUserSafe({ seed: 2 }); 30 | 31 | export const STATIC_AUTHOR_3: UserSafe = mockUserSafe({ seed: 3 }); 32 | -------------------------------------------------------------------------------- /src/router/loginById.test.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { vi, describe, beforeEach, expect, test } from 'vitest'; 3 | import loginById from './loginById'; 4 | import useAuth from '@/stores/auth'; 5 | import type { RouteLocationNormalized } from 'vue-router'; 6 | import { createApp } from 'vue'; 7 | import { createTestingPinia } from '@pinia/testing'; 8 | import { setActivePinia } from 'pinia'; 9 | 10 | const userId = faker.string.uuid(); 11 | 12 | const to: Partial = { 13 | params: { 14 | userId, 15 | }, 16 | }; 17 | 18 | describe('loginById', () => { 19 | let auth: ReturnType; 20 | 21 | beforeEach(() => { 22 | const app = createApp({}); 23 | const pinia = createTestingPinia({ createSpy: vi.fn }); 24 | app.use(pinia); 25 | setActivePinia(pinia); 26 | 27 | auth = useAuth(); 28 | }); 29 | 30 | test('should call loginWithUserId', async () => { 31 | await loginById(to as RouteLocationNormalized); 32 | 33 | expect(auth.loginWithUserId).toHaveBeenCalled(); 34 | expect(auth.loginWithUserId).toHaveBeenCalledWith(userId); 35 | }); 36 | 37 | test('should return directions to home', async () => { 38 | expect(await loginById(to as RouteLocationNormalized)).toStrictEqual({ 39 | name: 'home', 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/router/loginById.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized } from 'vue-router'; 2 | import useAuth from '@/stores/auth'; 3 | 4 | const loginByToken = async (to: RouteLocationNormalized) => { 5 | const auth = useAuth(); 6 | await auth.loginWithUserId(String(to.params.userId)); 7 | 8 | return { name: 'home' }; 9 | }; 10 | 11 | export default loginByToken; 12 | -------------------------------------------------------------------------------- /src/router/loginByToken.test.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { vi, describe, beforeEach, expect, test } from 'vitest'; 3 | import loginByToken from './loginByToken'; 4 | import useAuth from '@/stores/auth'; 5 | import type { RouteLocationNormalized } from 'vue-router'; 6 | import { createApp } from 'vue'; 7 | import { createTestingPinia } from '@pinia/testing'; 8 | import { setActivePinia } from 'pinia'; 9 | 10 | const passwordlessToken = faker.string.uuid(); 11 | 12 | const to: Partial = { 13 | params: { 14 | passwordlessToken, 15 | }, 16 | }; 17 | 18 | describe('loginByToken', () => { 19 | let auth: ReturnType; 20 | 21 | beforeEach(() => { 22 | const app = createApp({}); 23 | const pinia = createTestingPinia({ createSpy: vi.fn }); 24 | app.use(pinia); 25 | setActivePinia(pinia); 26 | 27 | auth = useAuth(); 28 | }); 29 | 30 | test('should call exchangeTokens', async () => { 31 | await loginByToken(to as RouteLocationNormalized); 32 | 33 | expect(auth.exchangeTokens).toHaveBeenCalled(); 34 | expect(auth.exchangeTokens).toHaveBeenCalledWith(passwordlessToken); 35 | }); 36 | 37 | test('should return directions to home', async () => { 38 | expect(await loginByToken(to as RouteLocationNormalized)).toStrictEqual({ 39 | name: 'home', 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/router/loginByToken.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized } from 'vue-router'; 2 | import useAuth from '@/stores/auth'; 3 | 4 | const loginByToken = async (to: RouteLocationNormalized) => { 5 | const auth = useAuth(); 6 | await auth.exchangeTokens(String(to.params.passwordlessToken)); 7 | return { name: 'home' }; 8 | }; 9 | 10 | export default loginByToken; 11 | -------------------------------------------------------------------------------- /src/stores/toasts.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from '@/utils/uuid'; 2 | import { defineStore } from 'pinia'; 3 | 4 | type VToastType = 'error' | 'success' | undefined; 5 | 6 | export class VToastMessage { 7 | text: string; 8 | id: string; 9 | lifetime: number; 10 | type: VToastType; 11 | 12 | constructor(text: string, type: VToastType = undefined, lifetime = 5000) { 13 | this.text = text; 14 | this.type = type; 15 | this.id = uuid(); 16 | this.lifetime = lifetime; 17 | } 18 | } 19 | 20 | interface State { 21 | messages: VToastMessage[]; 22 | disabled: boolean; 23 | } 24 | 25 | const useToasts = defineStore('toasts', { 26 | state: (): State => { 27 | return { messages: [], disabled: false }; 28 | }, 29 | actions: { 30 | addMessage(text: string, type: VToastType = undefined) { 31 | if (this.disabled) return; 32 | this.messages = [...this.messages, new VToastMessage(text, type)]; 33 | }, 34 | removeMessage(id: string) { 35 | this.messages = this.messages.filter((message) => message.id !== id); 36 | }, 37 | disable() { 38 | this.messages = []; 39 | this.disabled = true; 40 | }, 41 | enable() { 42 | this.disabled = false; 43 | }, 44 | }, 45 | }); 46 | 47 | export default useToasts; 48 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export type BlockMap = { 2 | [key: string]: any; 3 | }; 4 | 5 | export type AuthToken = string; 6 | -------------------------------------------------------------------------------- /src/types/users.ts: -------------------------------------------------------------------------------- 1 | export type Gender = 'female' | 'male' | undefined; 2 | 3 | export interface User { 4 | id: string; 5 | uuid: string; 6 | username: string; 7 | firstName: string; 8 | lastName: string; 9 | firstNameEn: string; 10 | lastNameEn: string; 11 | gender: Gender; 12 | linkedinUsername: string; 13 | githubUsername: string; 14 | telegramUsername: string; 15 | avatar?: string; 16 | } 17 | 18 | type EditableUserDataProperties = 19 | | 'firstName' 20 | | 'lastName' 21 | | 'firstNameEn' 22 | | 'lastNameEn' 23 | | 'gender' 24 | | 'linkedinUsername' 25 | | 'githubUsername' 26 | | 'telegramUsername'; 27 | 28 | export type EditableUserData = Partial>; 29 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs'; 2 | import relativeTime from 'dayjs/plugin/relativeTime'; 3 | import 'dayjs/locale/ru'; 4 | 5 | dayjs.extend(relativeTime); 6 | dayjs.locale('ru'); 7 | 8 | export const relativeDate = (date: dayjs.ConfigType) => { 9 | return dayjs().to(date); 10 | }; 11 | 12 | export const formatDate = ( 13 | date: dayjs.ConfigType, 14 | format: string = 'DD.MM.YYYY', 15 | ) => { 16 | return dayjs(date).format(format); 17 | }; 18 | 19 | export const formatDateTime = (date: string) => { 20 | return dayjs(date).format('DD.MM.YYYY HH:mm'); 21 | }; 22 | -------------------------------------------------------------------------------- /src/utils/filterDictionary.ts: -------------------------------------------------------------------------------- 1 | export interface Dictionary { 2 | [k: string]: T; 3 | } 4 | 5 | export default function filterDictionary( 6 | inputDictionary: Dictionary, 7 | filterFunction: (value: T, key: string) => boolean, 8 | ): Dictionary { 9 | const outDict: Dictionary = {}; 10 | for (const k of Object.keys(inputDictionary)) { 11 | const thisValue = inputDictionary[k]; 12 | if (filterFunction(thisValue, k)) outDict[k] = thisValue; 13 | } 14 | return outDict; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/getCertificateLink.ts: -------------------------------------------------------------------------------- 1 | const getCertificateLink = (slug: string): string => 2 | `https://cert.tough-dev.school/${slug}/en`; 3 | 4 | export default getCertificateLink; 5 | -------------------------------------------------------------------------------- /src/utils/getName.test.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { describe, expect, test } from 'vitest'; 3 | import { getName } from './getName'; 4 | 5 | const firstName = faker.person.firstName(); 6 | const lastName = faker.person.lastName(); 7 | 8 | describe('getName', () => { 9 | test('returns correct name when first and last names defined', () => { 10 | expect(getName(firstName, lastName)).toBe(`${firstName} ${lastName}`); 11 | }); 12 | 13 | test('returns correct name when only first name defined', () => { 14 | expect(getName(firstName, '')).toBe(`${firstName}`); 15 | }); 16 | 17 | test('returns correct name when only last name defined', () => { 18 | expect(getName('', lastName)).toBe(`${lastName}`); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/utils/getName.ts: -------------------------------------------------------------------------------- 1 | export const getName = ( 2 | firstName: string | undefined, 3 | lastName: string | undefined, 4 | ) => { 5 | return [firstName, lastName].filter(Boolean).join(' ').trim(); 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/getNotionTitle.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | // #FIXME Remove ts-nocheck 3 | import { faker } from '@faker-js/faker'; 4 | import { describe, expect, test } from 'vitest'; 5 | import getNotionTitle from './getNotionTitle'; 6 | 7 | const title = faker.lorem.sentence(); 8 | const id = faker.string.uuid(); 9 | 10 | const example = { 11 | [id]: { 12 | role: 'reader', 13 | value: { 14 | properties: { 15 | title: [[title]], 16 | }, 17 | }, 18 | }, 19 | }; 20 | 21 | describe('getNotionTitle', () => { 22 | test('return undefined if has no title', () => { 23 | expect(getNotionTitle(id, {})).toBe(undefined); 24 | expect(getNotionTitle(id, { [id]: {} })).toBe(undefined); 25 | expect(getNotionTitle(id, { [id]: { value: {} } })).toBe(undefined); 26 | expect(getNotionTitle(id, { [id]: { value: { properties: {} } } })).toBe( 27 | undefined, 28 | ); 29 | }); 30 | 31 | test('return title if has proper title', () => { 32 | expect(getNotionTitle(id, example)).toBe(title); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/utils/getNotionTitle.ts: -------------------------------------------------------------------------------- 1 | import type { BlockMap } from '@/types'; 2 | import idToUuid from './idToUuid'; 3 | 4 | const getNotionTitle = (materialId: string, material: BlockMap) => { 5 | // as api is unofficial and poorly typed we just assume title value is a nested array and then flatten it with the magic number to convert it into string 6 | const getBlockTitle = (block: any): string | undefined => 7 | block?.value?.properties?.title?.flat(100).join(''); 8 | 9 | const blockId = idToUuid(materialId); 10 | const firstBlockId = Object.keys(material)[0]; 11 | 12 | if (getBlockTitle(material[blockId])) { 13 | return getBlockTitle(material[blockId]); 14 | } else if (material[firstBlockId]?.value?.type === 'page') { 15 | return getBlockTitle(material[firstBlockId]); // using title of the first block as fallback title 16 | } 17 | return undefined; 18 | }; 19 | 20 | export default getNotionTitle; 21 | -------------------------------------------------------------------------------- /src/utils/handleError.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, beforeEach, expect, test } from 'vitest'; 2 | import handleError, { DEFAULT_ERROR_MESSAGE } from '@/utils/handleError'; 3 | import { faker } from '@faker-js/faker'; 4 | import { createApp } from 'vue'; 5 | import { setActivePinia } from 'pinia'; 6 | import { createTestingPinia } from '@pinia/testing'; 7 | import useToasts from '@/stores/toasts'; 8 | 9 | const createError = (data: object = {}) => { 10 | return { 11 | response: { 12 | data, 13 | }, 14 | }; 15 | }; 16 | 17 | describe('handleError', () => { 18 | let toasts: ReturnType; 19 | 20 | beforeEach(() => { 21 | const app = createApp({}); 22 | const pinia = createTestingPinia({ createSpy: vi.fn }); 23 | app.use(pinia); 24 | setActivePinia(pinia); 25 | toasts = useToasts(); 26 | }); 27 | 28 | test('calls toast with default message for empty error', () => { 29 | handleError(); 30 | 31 | expect(toasts.addMessage).toHaveBeenCalledTimes(1); 32 | expect(toasts.addMessage).toHaveBeenCalledWith( 33 | DEFAULT_ERROR_MESSAGE, 34 | 'error', 35 | ); 36 | }); 37 | 38 | test('calls toast for text error', () => { 39 | const message = faker.lorem.sentence(); 40 | 41 | handleError(message); 42 | 43 | expect(toasts.addMessage).toHaveBeenCalledTimes(1); 44 | expect(toasts.addMessage).toHaveBeenCalledWith(message, 'error'); 45 | }); 46 | 47 | test('calls toast for error', () => { 48 | const message = faker.lorem.sentence(); 49 | const error = createError({ 50 | details: message, 51 | }); 52 | 53 | handleError(error); 54 | 55 | expect(toasts.addMessage).toHaveBeenCalledTimes(1); 56 | expect(toasts.addMessage).toHaveBeenCalledWith(message, 'error'); 57 | }); 58 | 59 | test('calls toast for each error in array', () => { 60 | const messages = [...Array(10)].map(() => faker.lorem.sentence()); 61 | const error = createError({ 62 | details: messages, 63 | }); 64 | 65 | handleError(error); 66 | 67 | expect(toasts.addMessage).toBeCalledTimes(messages.length); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/utils/handleError.ts: -------------------------------------------------------------------------------- 1 | import useToasts from '@/stores/toasts'; 2 | 3 | export const DEFAULT_ERROR_MESSAGE = 'Ошибка!'; 4 | 5 | const handleError = (error: any = DEFAULT_ERROR_MESSAGE) => { 6 | const toasts = useToasts(); 7 | 8 | if (typeof error === 'string') { 9 | toasts.addMessage(error, 'error'); 10 | } else { 11 | Object.keys(error.response.data).forEach((key) => { 12 | const field = error.response.data[key]; 13 | const fields = Array.isArray(field) ? field : [field]; 14 | fields.forEach((error: string) => { 15 | toasts.addMessage(error, 'error'); 16 | }); 17 | }); 18 | } 19 | }; 20 | 21 | export default handleError; 22 | -------------------------------------------------------------------------------- /src/utils/htmlToMarkdown.ts: -------------------------------------------------------------------------------- 1 | import TurndownService from 'turndown'; 2 | 3 | // De-escape links, see: 4 | // https://github.com/mixmark-io/turndown#escaping-markdown-characters 5 | // https://github.com/mixmark-io/turndown/blob/97e4535ca76bb2e70d9caa2aa4d4686956b06d44/src/turndown.js 6 | 7 | const escapes: [RegExp, string][] = [ 8 | [/\\/g, '\\\\'], 9 | [/\*/g, '\\*'], 10 | [/^-/g, '\\-'], 11 | [/^\+ /g, '\\+ '], 12 | [/^(=+)/g, '\\$1'], 13 | [/^(#{1,6}) /g, '\\$1 '], 14 | [/`/g, '\\`'], 15 | [/^~~~/g, '\\~~~'], 16 | [/\[/g, '\\['], 17 | [/\]/g, '\\]'], 18 | [/^>/g, '\\>'], 19 | [/_/g, '\\_'], 20 | [/^(\d+)\. /g, '$1\\. '], 21 | ]; 22 | 23 | const URL_REGEX = 24 | /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape 25 | const regex = new RegExp(URL_REGEX); 26 | 27 | TurndownService.prototype.escape = function (string) { 28 | if (string.match(regex)) return string; 29 | return escapes.reduce(function (accumulator, escape) { 30 | return accumulator.replace(escape[0], escape[1]); 31 | }, string); 32 | }; 33 | 34 | const turndownService = new TurndownService(); 35 | 36 | const htmlToMarkdown = (html: string) => turndownService.turndown(html); 37 | 38 | export default htmlToMarkdown; 39 | -------------------------------------------------------------------------------- /src/utils/idToUuid.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import idToUuid from './idToUuid'; 3 | 4 | describe('idToUuid', () => { 5 | test('returns predicted uuid', () => { 6 | expect(idToUuid('b3d1f8c33e2d4c0a9e8a3e7f2a2e2c7f')).toBe( 7 | 'b3d1f8c3-3e2d-4c0a-9e8a-3e7f2a2e2c7f', 8 | ); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/utils/idToUuid.ts: -------------------------------------------------------------------------------- 1 | const idToUuid = (id: string) => { 2 | return id 3 | .replace(/-/g, '') 4 | .replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5'); 5 | }; 6 | 7 | export default idToUuid; 8 | -------------------------------------------------------------------------------- /src/utils/layoutDecorator.ts: -------------------------------------------------------------------------------- 1 | import { mockUserId, USER_1 } from '@/mocks/mockUserId'; 2 | import useToasts from '@/stores/toasts'; 3 | import VTransparentComponent from '@/mocks/VTransparentComponent.vue'; 4 | import { useQueryClient } from '@tanstack/vue-query'; 5 | import { userKeys } from '@/query'; 6 | 7 | const layoutDecorator = (story: any, layout: any) => ({ 8 | components: { layout, story }, 9 | template: '', 10 | setup() { 11 | const toasts = useToasts(); 12 | toasts.disable(); 13 | 14 | const queryClient = useQueryClient(); 15 | queryClient.setQueryData(userKeys.me(), { 16 | id: '', 17 | uuid: mockUserId(USER_1), 18 | username: 'johndoe@demo.com', 19 | firstName: 'Иван', 20 | lastName: 'Иванов', 21 | firstNameEn: 'John', 22 | lastNameEn: 'Doe', 23 | gender: 'male', 24 | linkedinUsername: 'johndoe', 25 | githubUsername: 'johndoe', 26 | telegramUsername: 'johndoe', 27 | }); 28 | }, 29 | }); 30 | 31 | const defaultLayoutDecorator = (story: any) => 32 | layoutDecorator(story, VTransparentComponent); 33 | 34 | export { defaultLayoutDecorator }; 35 | -------------------------------------------------------------------------------- /src/utils/makeStatic.ts: -------------------------------------------------------------------------------- 1 | // This function is used to make static mocks for visual regression tests 2 | import { merge } from 'lodash-es'; 3 | 4 | const makeStatic = (dynamic: Type, edits: Partial) => { 5 | return Object.freeze(merge({}, dynamic, edits)); 6 | }; 7 | 8 | export default makeStatic; 9 | -------------------------------------------------------------------------------- /src/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from 'uuid'; 2 | 3 | export const uuid = () => v4(); 4 | -------------------------------------------------------------------------------- /src/views/VCertificatesView/VCertificatesView.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VCertificatesView from './VCertificatesView.vue'; 3 | import { defaultLayoutDecorator } from '@/utils/layoutDecorator'; 4 | import { mockDiplomaSet, STATIC_DIPLOMAS } from '@/mocks/mockDiploma'; 5 | import { flatten } from 'lodash-es'; 6 | import { diplomasKeys } from '@/query'; 7 | import { useQueryClient } from '@tanstack/vue-query'; 8 | 9 | export default { 10 | title: 'App/VCertificatesView', 11 | component: VCertificatesView, 12 | decorators: [defaultLayoutDecorator], 13 | parameters: { layout: 'fullscreen' }, 14 | } as Meta; 15 | 16 | const Template: StoryFn = (args) => ({ 17 | components: { VCertificatesView }, 18 | setup() { 19 | return { args }; 20 | }, 21 | template: '', 22 | }); 23 | 24 | export const Default = { 25 | render: Template, 26 | decorators: [ 27 | () => ({ 28 | setup() { 29 | const queryClient = useQueryClient(); 30 | 31 | queryClient.setQueryData(diplomasKeys.lists(), () => 32 | flatten(STATIC_DIPLOMAS.map((diploma) => mockDiplomaSet(diploma))), 33 | ); 34 | }, 35 | template: '', 36 | }), 37 | ], 38 | }; 39 | 40 | export const Empty = { 41 | render: Template, 42 | decorators: [ 43 | () => ({ 44 | setup() { 45 | const queryClient = useQueryClient(); 46 | 47 | queryClient.setQueryData(diplomasKeys.lists(), () => []); 48 | }, 49 | template: '', 50 | }), 51 | ], 52 | }; 53 | -------------------------------------------------------------------------------- /src/views/VCertificatesView/VCertificatesView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 38 | -------------------------------------------------------------------------------- /src/views/VCoursesView/VCoursesView.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VCoursesView from './VCoursesView.vue'; 3 | import { defaultLayoutDecorator } from '@/utils/layoutDecorator'; 4 | import { STATIC_STUDY } from '@/mocks/mockCourse'; 5 | import { studiesKeys } from '@/query'; 6 | import { useQueryClient } from '@tanstack/vue-query'; 7 | 8 | export default { 9 | title: 'App/VCoursesView', 10 | component: VCoursesView, 11 | decorators: [defaultLayoutDecorator], 12 | parameters: { layout: 'fullscreen' }, 13 | } as Meta; 14 | 15 | const Template: StoryFn = (args) => ({ 16 | components: { VCoursesView }, 17 | setup() { 18 | return { args }; 19 | }, 20 | template: '', 21 | }); 22 | 23 | export const Default = { 24 | render: Template, 25 | decorators: [ 26 | () => ({ 27 | setup() { 28 | const queryClient = useQueryClient(); 29 | queryClient.setQueryData(studiesKeys.lists(), [STATIC_STUDY]); 30 | }, 31 | template: '', 32 | }), 33 | ], 34 | }; 35 | 36 | export const Empty = { 37 | render: Template, 38 | decorators: [ 39 | () => ({ 40 | setup() { 41 | const queryClient = useQueryClient(); 42 | queryClient.setQueryData(studiesKeys.lists(), []); 43 | }, 44 | template: '', 45 | }), 46 | ], 47 | }; 48 | -------------------------------------------------------------------------------- /src/views/VCoursesView/VCoursesView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 41 | -------------------------------------------------------------------------------- /src/views/VHomeworkView/VHomeworkView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /src/views/VHomeworkView/useHomeworkBreadcrumbs.ts: -------------------------------------------------------------------------------- 1 | import { fetchHomeworkQuestion } from '@/query'; 2 | import { onMounted, ref, toValue, type MaybeRefOrGetter } from 'vue'; 3 | import type { Breadcrumb } from '@/components/VBreadcrumbs/VBreadcrumbs.vue'; 4 | import { useQueryClient } from '@tanstack/vue-query'; 5 | 6 | export const useHomeworkBreadcrumbs = ( 7 | questionId: MaybeRefOrGetter, 8 | ) => { 9 | const queryClient = useQueryClient(); 10 | 11 | const breadcrumbs = ref(); 12 | 13 | onMounted(async () => { 14 | const result: Breadcrumb[] = [{ name: 'Мои курсы', to: { name: 'home' } }]; 15 | 16 | const question = await fetchHomeworkQuestion(queryClient, { 17 | questionId: toValue(questionId), 18 | }); 19 | 20 | if (!question) return undefined; 21 | 22 | if (question.breadcrumbs.course) { 23 | result.push({ 24 | name: question.breadcrumbs.course.name, 25 | to: { 26 | name: 'modules', 27 | params: { 28 | courseId: question.breadcrumbs.course.id, 29 | }, 30 | }, 31 | }); 32 | } else { 33 | return undefined; 34 | } 35 | 36 | if (question.breadcrumbs.module) { 37 | result.push({ 38 | name: question.breadcrumbs.module.name, 39 | to: { 40 | name: 'lessons', 41 | params: { 42 | courseId: question.breadcrumbs.course.id, 43 | moduleId: question.breadcrumbs.module.id, 44 | }, 45 | }, 46 | }); 47 | } else { 48 | return undefined; 49 | } 50 | 51 | result.push({ 52 | name: question.name, 53 | to: { 54 | name: 'homework', 55 | params: { 56 | questionId: toValue(questionId), 57 | }, 58 | }, 59 | }); 60 | 61 | breadcrumbs.value = result; 62 | }); 63 | 64 | return { 65 | breadcrumbs, 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /src/views/VLoadingView/VLoadingView.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VLoadingView from './VLoadingView.vue'; 3 | import { defaultLayoutDecorator } from '@/utils/layoutDecorator'; 4 | 5 | export default { 6 | title: 'App/VLoadingView', 7 | component: VLoadingView, 8 | decorators: [defaultLayoutDecorator], 9 | parameters: { layout: 'fullscreen' }, 10 | } as Meta; 11 | 12 | const Template: StoryFn = (args) => ({ 13 | components: { VLoadingView }, 14 | setup() { 15 | return { args }; 16 | }, 17 | template: '', 18 | }); 19 | 20 | export const Default = { 21 | render: Template, 22 | }; 23 | -------------------------------------------------------------------------------- /src/views/VLoadingView/VLoadingView.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/views/VLoginChangeView/VLoginChangeView.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VLoginChangeView from './VLoginChangeView.vue'; 3 | import { defaultLayoutDecorator } from '@/utils/layoutDecorator'; 4 | 5 | export default { 6 | title: 'App/VLoginChangeView', 7 | component: VLoginChangeView, 8 | decorators: [defaultLayoutDecorator], 9 | parameters: { layout: 'fullscreen' }, 10 | } as Meta; 11 | 12 | const Template: StoryFn = (args) => ({ 13 | components: { VLoginChangeView }, 14 | setup() { 15 | return { args }; 16 | }, 17 | template: '', 18 | }); 19 | 20 | export const Default = { 21 | render: Template, 22 | }; 23 | -------------------------------------------------------------------------------- /src/views/VLoginChangeView/VLoginChangeView.test.ts: -------------------------------------------------------------------------------- 1 | import { vi, describe, beforeEach, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import type { VueWrapper } from '@vue/test-utils'; 4 | import VLoginChangeView from './VLoginChangeView.vue'; 5 | import VPasswordSettings from '@/components/VPasswordSettings/VPasswordSettings.vue'; 6 | import { faker } from '@faker-js/faker'; 7 | 8 | const uid = faker.string.uuid(); 9 | const token = faker.string.uuid(); 10 | 11 | const defaultProps = { 12 | uid, 13 | token, 14 | }; 15 | 16 | const routerPushMock = vi.fn(); 17 | 18 | vi.mock('vue-router', () => ({ 19 | useRouter: () => ({ 20 | push: routerPushMock, 21 | }), 22 | })); 23 | 24 | describe('VLoginChangeView', () => { 25 | let wrapper: VueWrapper>; 26 | 27 | beforeEach(() => { 28 | wrapper = mount(VLoginChangeView, { 29 | shallow: true, 30 | props: defaultProps, 31 | global: { 32 | renderStubDefaultSlot: true, 33 | }, 34 | }); 35 | }); 36 | 37 | const getPasswordSettingsWrapper = () => 38 | wrapper.findComponent(VPasswordSettings); 39 | 40 | test('has proper data in props', () => { 41 | expect(getPasswordSettingsWrapper().props()).toStrictEqual({ uid, token }); 42 | }); 43 | 44 | test('navigates to /login on save', () => { 45 | getPasswordSettingsWrapper().vm.$emit('save'); 46 | 47 | expect(routerPushMock).toHaveBeenCalledTimes(1); 48 | expect(routerPushMock).toHaveBeenCalledWith({ name: 'login' }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/views/VLoginChangeView/VLoginChangeView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 22 | -------------------------------------------------------------------------------- /src/views/VLoginResetView/VLoginResetView.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VLoginResetView from './VLoginResetView.vue'; 3 | import { defaultLayoutDecorator } from '@/utils/layoutDecorator'; 4 | 5 | export default { 6 | title: 'App/VLoginResetView', 7 | component: VLoginResetView, 8 | decorators: [defaultLayoutDecorator], 9 | parameters: { layout: 'fullscreen' }, 10 | } as Meta; 11 | 12 | const Template: StoryFn = (args) => ({ 13 | components: { VLoginResetView }, 14 | setup() { 15 | return { args }; 16 | }, 17 | template: '', 18 | }); 19 | 20 | export const Default = { 21 | render: Template, 22 | }; 23 | -------------------------------------------------------------------------------- /src/views/VLoginResetView/VLoginResetView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 50 | -------------------------------------------------------------------------------- /src/views/VLoginView/VLoginView.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VLoginView from './VLoginView.vue'; 3 | import { defaultLayoutDecorator } from '@/utils/layoutDecorator'; 4 | 5 | export default { 6 | title: 'App/VLoginView', 7 | component: VLoginView, 8 | decorators: [defaultLayoutDecorator], 9 | parameters: { layout: 'fullscreen' }, 10 | } as Meta; 11 | 12 | const Template: StoryFn = (args) => ({ 13 | components: { VLoginView }, 14 | setup() { 15 | return { args }; 16 | }, 17 | template: '', 18 | }); 19 | 20 | export const Default = { 21 | render: Template, 22 | }; 23 | -------------------------------------------------------------------------------- /src/views/VLoginView/VLoginView.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, beforeEach, expect, test } from 'vitest'; 2 | import { mount } from '@vue/test-utils'; 3 | import type { VueWrapper } from '@vue/test-utils'; 4 | import VLoginView from './VLoginView.vue'; 5 | import type VLoginLink from '@/components/VLoginLink/VLoginLink.vue'; 6 | import type VLoginPassword from '@/components/VLoginPassword/VLoginPassword.vue'; 7 | 8 | describe('VLoginView', () => { 9 | let wrapper: VueWrapper>; 10 | 11 | beforeEach(() => { 12 | wrapper = mount(VLoginView, { 13 | shallow: true, 14 | global: { 15 | renderStubDefaultSlot: true, 16 | }, 17 | }); 18 | }); 19 | 20 | const getLoginLinkWrapper = () => { 21 | return wrapper.findComponent( 22 | '[data-testid="login-link"]', 23 | ); 24 | }; 25 | const getLoginPasswordWrapper = () => { 26 | return wrapper.findComponent( 27 | '[data-testid="login-password"]', 28 | ); 29 | }; 30 | 31 | test('should render link login by default', () => { 32 | expect(getLoginLinkWrapper().exists()).toBe(true); 33 | }); 34 | 35 | test('should render password login if mode changed to password', async () => { 36 | await getLoginLinkWrapper().trigger('change'); 37 | 38 | expect(getLoginPasswordWrapper().exists()).toBe(true); 39 | }); 40 | 41 | test('should render link login if mode changed to link', async () => { 42 | await getLoginLinkWrapper().trigger('change'); 43 | await getLoginPasswordWrapper().trigger('change'); 44 | 45 | expect(getLoginLinkWrapper().exists()).toBe(true); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/views/VLoginView/VLoginView.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | -------------------------------------------------------------------------------- /src/views/VMailSentView/VMailSentView.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VMailSentView from './VMailSentView.vue'; 3 | import { defaultLayoutDecorator } from '@/utils/layoutDecorator'; 4 | 5 | export default { 6 | title: 'App/VMailSentView', 7 | component: VMailSentView, 8 | decorators: [defaultLayoutDecorator], 9 | parameters: { layout: 'fullscreen' }, 10 | } as Meta; 11 | 12 | const Template: StoryFn = (args) => ({ 13 | components: { VMailSentView }, 14 | setup() { 15 | return { args }; 16 | }, 17 | template: '', 18 | }); 19 | 20 | export const Default = { 21 | render: Template, 22 | args: { 23 | email: 'test@test.com', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/views/VMailSentView/VMailSentView.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 41 | -------------------------------------------------------------------------------- /src/views/VNotionView/VNotionView.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VNotionView from './VNotionView.vue'; 3 | import { defaultLayoutDecorator } from '@/utils/layoutDecorator'; 4 | import { mockMaterial } from '@/mocks/mockMaterialSerializer'; 5 | import { materialsKeys } from '@/query'; 6 | import { useQueryClient } from '@tanstack/vue-query'; 7 | 8 | export default { 9 | title: 'App/VNotionView', 10 | component: VNotionView, 11 | decorators: [defaultLayoutDecorator], 12 | parameters: { layout: 'fullscreen' }, 13 | } as Meta; 14 | 15 | const MOCK_MATERIAL_ID = 'cf1379bf-bf5a-41f9-942f-31dd4253c178'; 16 | const NOT_FOUND_MATERIAL_ID = 'not-found-material-id'; 17 | 18 | const Template: StoryFn = (args) => ({ 19 | components: { VNotionView }, 20 | setup() { 21 | const queryClient = useQueryClient(); 22 | 23 | if (args.materialId === MOCK_MATERIAL_ID) { 24 | queryClient.setQueryData( 25 | materialsKeys.materials(MOCK_MATERIAL_ID), 26 | mockMaterial(), 27 | ); 28 | } else { 29 | queryClient.setQueryData(materialsKeys.materials(args.materialId), null); 30 | } 31 | 32 | return { args }; 33 | }, 34 | template: '', 35 | }); 36 | 37 | export const Default = { 38 | render: Template, 39 | args: { 40 | materialId: MOCK_MATERIAL_ID, 41 | }, 42 | }; 43 | 44 | export const Empty = { 45 | render: Template, 46 | args: { 47 | materialId: NOT_FOUND_MATERIAL_ID, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /src/views/VSettingsView/VSettingsView.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryFn } from '@storybook/vue3-vite'; 2 | import VSettingsView from './VSettingsView.vue'; 3 | import { defaultLayoutDecorator } from '@/utils/layoutDecorator'; 4 | 5 | export default { 6 | title: 'App/VSettingsView', 7 | component: VSettingsView, 8 | decorators: [defaultLayoutDecorator], 9 | parameters: { layout: 'fullscreen' }, 10 | } as Meta; 11 | 12 | const Template: StoryFn = () => ({ 13 | components: { VSettingsView }, 14 | template: '', 15 | }); 16 | 17 | export const Default = { 18 | render: Template, 19 | }; 20 | -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-1440-900-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-1440-900-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-1440-900-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-1440-900-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-320-560-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-320-560-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-320-560-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-320-560-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-390-840-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-390-840-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-390-840-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-390-840-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-768-1024-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-768-1024-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-768-1024-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-certificates-768-1024-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-1440-900-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-1440-900-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-1440-900-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-1440-900-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-320-560-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-320-560-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-320-560-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-320-560-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-390-840-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-390-840-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-390-840-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-390-840-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-768-1024-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-768-1024-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-768-1024-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-change-password-768-1024-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-1440-900-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-1440-900-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-1440-900-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-1440-900-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-320-560-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-320-560-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-320-560-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-320-560-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-390-840-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-390-840-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-390-840-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-390-840-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-768-1024-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-768-1024-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-768-1024-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-email-login-768-1024-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-1440-900-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-1440-900-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-1440-900-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-1440-900-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-320-560-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-320-560-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-320-560-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-320-560-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-390-840-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-390-840-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-390-840-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-390-840-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-768-1024-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-768-1024-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-768-1024-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-mail-sent-768-1024-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-1440-900-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-1440-900-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-1440-900-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-1440-900-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-320-560-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-320-560-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-320-560-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-320-560-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-390-840-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-390-840-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-390-840-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-390-840-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-768-1024-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-768-1024-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-768-1024-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-password-login-768-1024-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-1440-900-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-1440-900-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-1440-900-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-1440-900-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-320-560-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-320-560-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-320-560-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-320-560-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-390-840-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-390-840-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-390-840-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-390-840-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-768-1024-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-768-1024-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-768-1024-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-login-reset-password-768-1024-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-1440-900-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-1440-900-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-768-1024-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-768-1024-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-1440-900-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-1440-900-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-1440-900-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-1440-900-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-320-560-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-320-560-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-320-560-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-320-560-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-390-840-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-390-840-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-390-840-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-390-840-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-768-1024-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-768-1024-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-768-1024-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-materials-missing-768-1024-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-1440-900-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-1440-900-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-1440-900-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-1440-900-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-320-560-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-320-560-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-320-560-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-320-560-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-390-840-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-390-840-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-390-840-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-390-840-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-768-1024-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-768-1024-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-768-1024-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-no-certificates-768-1024-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-1440-900-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-1440-900-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-1440-900-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-1440-900-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-320-560-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-320-560-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-320-560-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-320-560-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-390-840-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-390-840-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-390-840-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-390-840-light-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-768-1024-dark-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-768-1024-dark-1-snap.png -------------------------------------------------------------------------------- /tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-768-1024-light-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tough-dev-school/lms-frontend-v2/4734f70385ddb937b919d14c7b44c1704b076875/tests/__image_snapshots__/regression-test-ts-tests-regression-test-ts-visual-regression-test-for-settings-768-1024-light-1-snap.png -------------------------------------------------------------------------------- /tests/regression.config.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig } from 'vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | import viteConfig from '../vite.config'; 4 | 5 | export default mergeConfig( 6 | viteConfig, 7 | defineConfig({ 8 | test: { 9 | globals: true, 10 | include: ['./tests/regression.test.ts'], 11 | testTimeout: 180000, 12 | }, 13 | }), 14 | ); 15 | -------------------------------------------------------------------------------- /tests/unit.config.ts: -------------------------------------------------------------------------------- 1 | import { mergeConfig } from 'vite'; 2 | import { configDefaults, defineConfig } from 'vitest/config'; 3 | import viteConfig from '../vite.config'; 4 | 5 | export default mergeConfig( 6 | viteConfig, 7 | defineConfig({ 8 | test: { 9 | globals: true, 10 | mockReset: true, 11 | environment: 'jsdom', 12 | exclude: [...configDefaults.exclude, './tests/regression.test.ts'], 13 | coverage: { 14 | provider: 'istanbul', 15 | extension: ['.ts', '.js', '.vue'], 16 | include: ['**/*.ts', '**/*.vue'], 17 | exclude: ['**/*.stories.ts'], 18 | all: true, 19 | reporter: 'lcov', 20 | }, 21 | }, 22 | }), 23 | ); 24 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node22/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "types": ["node"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "lib": [], 7 | "types": ["node", "jsdom"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { fileURLToPath, URL } from 'node:url'; 3 | 4 | import { defineConfig } from 'vite'; 5 | import vue from '@vitejs/plugin-vue'; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | server: { 10 | host: true, 11 | port: 3000, 12 | proxy: { 13 | '/api': 'http://127.0.0.1:8000', 14 | }, 15 | }, 16 | plugins: [ 17 | vue({ 18 | script: { 19 | defineModel: true, 20 | }, 21 | }), 22 | ], 23 | resolve: { 24 | alias: { 25 | '@': fileURLToPath(new URL('./src', import.meta.url)), 26 | }, 27 | }, 28 | // test: { 29 | // go to ./tests/*.config.ts 30 | // }, 31 | }); 32 | -------------------------------------------------------------------------------- /vue-avatar-cropper.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue-avatar-cropper'; 2 | --------------------------------------------------------------------------------