├── .eslintignore ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature-request.md ├── actions │ └── install-deps │ │ └── action.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── autoMilestone.yml │ ├── base-workflows.yml │ ├── build.yml │ ├── code-style.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.json ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.js ├── preview-head.html └── preview.js ├── .stylelintignore ├── .stylelintrc ├── README.md ├── commitlint.config.cjs ├── db.json ├── jsconfig.json ├── package.json ├── pnpm-lock.yaml ├── routes.json ├── setup.mjs ├── src ├── __tests__ │ ├── index.js │ └── setup.js ├── components │ ├── AddPet │ │ ├── assets │ │ │ ├── akita.svg │ │ │ └── arrow-left.svg │ │ ├── index.js │ │ └── index.scss │ ├── AvatarButton │ │ ├── images │ │ │ └── cross.svg │ │ ├── index.js │ │ └── index.scss │ ├── Button │ │ ├── index.js │ │ ├── index.scss │ │ └── index.spec.js │ ├── Card │ │ ├── index.js │ │ └── index.scss │ ├── ChangePassword │ │ ├── index.js │ │ └── index.scss │ ├── Checkbox │ │ ├── images │ │ │ └── check.svg │ │ ├── index.js │ │ └── index.scss │ ├── Drawer │ │ ├── images │ │ │ ├── close.svg │ │ │ └── line.svg │ │ ├── index.js │ │ ├── index.scss │ │ └── index.spec.js │ ├── Dropdown │ │ ├── components │ │ │ └── DropdownItem │ │ │ │ ├── index.js │ │ │ │ └── index.scss │ │ ├── index.js │ │ ├── index.scss │ │ └── index.spec.js │ ├── LoginForm │ │ ├── images │ │ │ ├── facebook-icon.svg │ │ │ ├── google-icon.svg │ │ │ └── paw-form-icon.svg │ │ ├── index.js │ │ └── index.scss │ ├── PetAvatar │ │ ├── index.js │ │ └── index.scss │ ├── PetCard │ │ ├── index.js │ │ └── index.scss │ ├── ProgressBar │ │ ├── index.js │ │ ├── index.scss │ │ └── index.spec.js │ ├── RadioButton │ │ ├── images │ │ │ └── ellipse.svg │ │ ├── index.js │ │ └── index.scss │ ├── RangeSlider │ │ ├── index.js │ │ └── index.scss │ ├── RegisterForm │ │ ├── components │ │ │ └── Field │ │ │ │ ├── index.js │ │ │ │ └── index.scss │ │ ├── images │ │ │ ├── facebook-icon.svg │ │ │ └── google-icon.svg │ │ ├── index.js │ │ └── index.scss │ ├── SizeSelector │ │ ├── images │ │ │ ├── large.js │ │ │ ├── medium.js │ │ │ └── small.js │ │ ├── index.js │ │ └── index.scss │ ├── Sliding │ │ ├── index.js │ │ ├── index.scss │ │ ├── index.spec.js │ │ └── utils │ │ │ ├── activeSlide.js │ │ │ └── activeSlideShuffle.js │ ├── Tabber │ │ ├── index.js │ │ └── index.scss │ ├── TextArea │ │ ├── index.js │ │ └── index.scss │ ├── TextInput │ │ ├── img │ │ │ ├── eye-icon-disable.svg │ │ │ └── eye-icon.svg │ │ ├── index.js │ │ ├── index.scss │ │ └── index.spec.js │ ├── Toggle │ │ ├── index.js │ │ └── index.scss │ ├── UploadImage │ │ ├── img │ │ │ ├── photo-icon.svg │ │ │ ├── placeholder.svg │ │ │ └── plus-icon.svg │ │ ├── index.js │ │ ├── index.scss │ │ └── index.spec.js │ ├── Vaccine │ │ ├── images │ │ │ ├── plus.svg │ │ │ └── vaccine.svg │ │ ├── index.js │ │ └── index.scss │ ├── VaccineGroup │ │ ├── index.js │ │ └── index.scss │ └── VaccineItem │ │ ├── images │ │ └── calendar.svg │ │ ├── index.js │ │ └── index.scss ├── images │ ├── pet-dex.svg │ └── ruler-slider.svg ├── layouts │ ├── components │ │ ├── Navigation │ │ │ ├── images │ │ │ │ ├── avatar.svg │ │ │ │ ├── bell.svg │ │ │ │ ├── exit.svg │ │ │ │ └── menu.svg │ │ │ ├── index.js │ │ │ └── index.scss │ │ └── SideMenu │ │ │ ├── images │ │ │ ├── configuracoes.svg │ │ │ ├── conta.svg │ │ │ ├── doacoes.svg │ │ │ ├── exit.svg │ │ │ ├── exitmenu.svg │ │ │ ├── meuspets.svg │ │ │ ├── notifications.svg │ │ │ ├── perfil.svg │ │ │ └── petdex.svg │ │ │ ├── index.js │ │ │ └── index.scss │ ├── index.html │ ├── index.js │ ├── index.scss │ ├── pages │ │ ├── NoPetRegirested │ │ │ ├── images │ │ │ │ └── no-pet-regirested-page.png │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── PetName │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── PetRegister │ │ │ ├── index.js │ │ │ ├── index.scss │ │ │ └── index.spec.js │ │ ├── PetSize │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── PetVet │ │ │ ├── images │ │ │ │ ├── check.svg │ │ │ │ ├── cuidadosEspeciais.svg │ │ │ │ └── estetoscopio.svg │ │ │ ├── index.js │ │ │ ├── index.scss │ │ │ └── index.spec.js │ │ └── PetWeight │ │ │ ├── index.js │ │ │ ├── index.scss │ │ │ └── petWeightPage.spec.js │ ├── sample-page │ │ └── index.html │ └── utils │ │ └── scrollable-sidemenu.js ├── public │ ├── favicon │ │ ├── android-icon-192x192.ico │ │ ├── apple-touch-icon-114x114.ico │ │ ├── apple-touch-icon-120x120.ico │ │ ├── apple-touch-icon-144x144.ico │ │ ├── apple-touch-icon-152x152.ico │ │ ├── apple-touch-icon-180x180.ico │ │ ├── apple-touch-icon-57x57.ico │ │ ├── apple-touch-icon-60x60.ico │ │ ├── apple-touch-icon-72x72.ico │ │ ├── apple-touch-icon-76x76.ico │ │ ├── apple-touch-icon-96x96.ico │ │ ├── favicon-16x16.ico │ │ └── favicon-32x32.ico │ └── helvetica │ │ ├── Helvetica.eot │ │ ├── Helvetica.otf │ │ ├── Helvetica.svg │ │ ├── Helvetica.ttf │ │ ├── Helvetica.woff │ │ └── Helvetica.woff2 ├── router │ ├── main-router.js │ └── routes │ │ ├── app │ │ ├── add-pet │ │ │ ├── add-pet.js │ │ │ └── steps │ │ │ │ ├── birthday.js │ │ │ │ ├── name.js │ │ │ │ ├── petvet.js │ │ │ │ ├── race.js │ │ │ │ ├── register.js │ │ │ │ ├── size.js │ │ │ │ └── weight.js │ │ ├── main-routes │ │ │ ├── main-routes.js │ │ │ └── routes │ │ │ │ ├── account.js │ │ │ │ ├── donates.js │ │ │ │ ├── error.js │ │ │ │ ├── home.js │ │ │ │ ├── pet-dex.js │ │ │ │ └── settings.js │ │ └── my-pets │ │ │ ├── my-pets.js │ │ │ └── steps │ │ │ ├── pet-profile.js │ │ │ └── pets.js │ │ └── create-account │ │ ├── account.js │ │ └── create-account.js ├── services │ ├── api.js │ ├── breeds.js │ └── userService.js ├── stories │ ├── AddPet.stories.js │ ├── AvatarButton.stories.js │ ├── Button.stories.js │ ├── ChangePassword.stories.js │ ├── Checkbox.stories.js │ ├── Drawer.stories.js │ ├── Dropdown.stories.js │ ├── LoginForm.stories.js │ ├── PetAvatar.stories.js │ ├── PetCard.stories.js │ ├── PetName.stories.js │ ├── PetRegister.stories.js │ ├── PetRegisterPage.stories.js │ ├── PetSizePage.stories.js │ ├── PetVetPage.stories.js │ ├── PetWeightPage.stories.js │ ├── RadioButton.stories.js │ ├── RangeSlider.stories.js │ ├── RegisterForm.stories.js │ ├── SizeSelector.stories.js │ ├── Sliding.stories.js │ ├── Tabber.stories.js │ ├── TextArea.stories.js │ ├── TextInput.stories.js │ ├── Toggle.stories.js │ ├── UploadImage.stories.js │ ├── Vaccine.stories.js │ ├── assets │ │ ├── petRegisterPage │ │ │ ├── afghanHound.svg │ │ │ ├── akita.svg │ │ │ ├── beagle.svg │ │ │ ├── bichonFrise.svg │ │ │ ├── borderCollie.svg │ │ │ ├── boxer.svg │ │ │ ├── chowChow.svg │ │ │ └── mixedBreed.svg │ │ └── tabber │ │ │ ├── birthday.svg │ │ │ └── home.svg │ └── readme │ │ ├── Example-1.png │ │ ├── Example-2.png │ │ ├── Example-2.stories.js │ │ ├── Example.js │ │ ├── Example.stories.js │ │ └── Readme.mdx ├── styles │ ├── base.scss │ ├── breakpoints.module.scss │ ├── breakpoints.scss │ ├── colors.scss │ ├── fonts.scss │ └── typography.scss └── utils │ ├── breakpoints │ └── breakpoints.js │ ├── swiper.js │ ├── swiper.spec.js │ └── validations.js └── vite.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | .next 2 | .vscode 3 | node_modules 4 | !*.scss 5 | dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb-base", "plugin:storybook/recommended", "prettier"], 3 | "env": { 4 | "browser": true 5 | }, 6 | "plugins": ["only-warn"], 7 | "settings": { 8 | "import/resolver": { 9 | "node": true, 10 | "jsconfig": { 11 | "config": "jsconfig.json" 12 | } 13 | } 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2024 17 | }, 18 | "rules": { 19 | "no-console": [ 20 | "warn", 21 | { 22 | "allow": ["warn"] 23 | } 24 | ], 25 | "object-curly-newline": "off", 26 | "import/prefer-default-export": "off" 27 | }, 28 | "overrides": [ 29 | { 30 | "env": { 31 | "node": true, 32 | "jest": true 33 | }, 34 | "plugins": ["vitest", "testing-library"], 35 | "files": ["**/*.spec.js", "**/*.test.js"], 36 | "extends": [ 37 | "plugin:testing-library/dom", 38 | "plugin:vitest/recommended", 39 | "plugin:vitest-globals/recommended" 40 | ], 41 | "rules": { 42 | "testing-library/prefer-user-event": ["warn"], 43 | "no-restricted-syntax": [ 44 | "warn", 45 | { 46 | "message": "Use screen 'methods' imported from '@testing-library/vanilla' instead.", 47 | "selector": "MemberExpression > Identifier[name=\"selected\"]" 48 | } 49 | ] 50 | }, 51 | "globals": { 52 | "vi": true 53 | } 54 | }, 55 | { 56 | "files": ["src/services/*.js"], 57 | "rules": { 58 | "no-console": "off" 59 | } 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @devhatt/hatts @devhatt/petdex-frontend-administrators 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a new bug report to help us improve Petdex 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A short description of what the bug is. 11 | 12 |
13 | 14 | Description 15 | 16 | [Describe the bug reported] 17 |
18 | 19 |
20 | 21 |
22 | 23 | Steps to Reproduce 24 | 25 | 26 | [If applicable, provide detailed steps to reproduce the bug.] 27 | 28 |
29 | 30 |
31 | 32 |
33 | 34 | Expected Behavior 35 | 36 | [Describe what is expected to happen.] 37 |
38 | 39 |
40 | 41 |
42 | 43 | Current Behavior 44 | 45 | [Describe what is currently happening.] 46 |
47 | 48 |
49 | 50 |
51 | 52 | Visual information 53 | 54 | [If possible, add screenshots to illustrate this bug.] 55 |
56 | 57 |
58 | 59 |
60 | 61 | Additional Information 62 | 63 | [Provide any additional information, such as relevant versions, browser, OS, context, etc.] 64 | 65 |
66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature to be added to Petdex 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the feature** 10 | A clear and short description of the feature. 11 | 12 |
13 | 14 | Description 15 | 16 | [Describe the new feature requested.] 17 |
18 | 19 |
20 | 21 |
22 | 23 | Use Case 24 | 25 | 26 | [Explain the use for this feature and how it might benefits the project.] 27 |
28 | 29 |
30 | 31 |
32 | 33 | Implementation Details 34 | 35 | 36 | [Provide any details or suggestions on how this feature could be implemented.] 37 | 38 |
39 | 40 |
41 | 42 |
43 | 44 | Visual Concepts 45 | 46 | 47 | [Include any visual representations or concepts if those are available and applicable.] 48 | 49 |
50 | 51 |
52 | 53 |
54 | 55 | Additional Information 56 | 57 | 58 | [Provide any additional information, such as context that might be relevant to the implementation of this specific feature.] 59 |
60 | -------------------------------------------------------------------------------- /.github/actions/install-deps/action.yml: -------------------------------------------------------------------------------- 1 | name: Reusable install deps 2 | description: This workflow installs dependencies using PNPM and caches the store directory. It is meant to be used as a reusable workflow. 3 | inputs: 4 | NODE_VERSION: 5 | description: 'The version of Node.js to use' 6 | required: true 7 | default: '20.x' 8 | PNPM_VERSION: 9 | description: 'The version of PNPM to use' 10 | required: true 11 | default: '8.x' 12 | runs: 13 | using: composite 14 | steps: 15 | - name: Code checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Use Node.js ${{ inputs.NODE_VERSION }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ inputs.NODE_VERSION }} 22 | 23 | - uses: pnpm/action-setup@v3 24 | name: Install pnpm 25 | with: 26 | version: ${{ inputs.PNPM_VERSION }} 27 | run_install: false 28 | 29 | - name: Get PNPM store directory 30 | shell: bash 31 | run: | 32 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 33 | 34 | - uses: actions/cache@v4 35 | name: Setup PNPM cache 36 | with: 37 | path: ${{ env.STORE_PATH }} 38 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 39 | restore-keys: | 40 | ${{ runner.os }}-pnpm-store- 41 | 42 | - name: Install dependencies 43 | shell: bash 44 | run: pnpm install --frozen-lockfile -r 45 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - directory: '/' 4 | package-ecosystem: 'npm' 5 | schedule: 6 | interval: 'weekly' 7 | time: '06:00' 8 | open-pull-requests-limit: 5 9 | versioning-strategy: increase 10 | groups: 11 | all-minor-updates: 12 | update-types: ['minor', 'patch'] 13 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Closes 2 | 3 |
4 | 5 | Feature 6 | 7 | 8 | N/A 9 | 10 |
11 | 12 |
13 | 14 | Bugfix 15 | 16 | 17 | - **Description** 18 | N/A 19 | 20 | - **Cause** 21 | N/A 22 | 23 | - **Solution** 24 | N/A 25 |
26 | 27 |
28 | 29 | Changelog 30 | 31 | N/A 32 |
33 | 34 |
35 | 36 | Visual evidences :framed_picture: 37 | 38 | 39 |
40 | 41 |
42 | 43 | Checklist 44 | 45 | 46 | - [ ] Issue linked 47 | - [ ] Build working correctly 48 | - [ ] Tests created 49 |
50 | 51 |
52 | 53 | Additional info 54 | 55 | N/A 56 |
57 | -------------------------------------------------------------------------------- /.github/workflows/autoMilestone.yml: -------------------------------------------------------------------------------- 1 | name: Auto-assign milestone on issue creation 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | jobs: 8 | auto-assign-milestone: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check for issue creation 12 | run: | 13 | # Extract the milestone name you want to assign 14 | milestone="primeira página" 15 | 16 | # Add the milestone to the issue 17 | echo "Assigning milestone $milestone to the issue..." 18 | curl -X PATCH -H "Authorization: token ${{ secrets.PERSONAL_ACCESS_TOKEN }}" \ 19 | -d "{\"milestone\": \"$milestone\"}" \ 20 | "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}" 21 | - name: Add a comment to the issue 22 | run: | 23 | # Comment to be added 24 | comment="### Obrigado por abrir a issue\n### Verifique os nossos guidelines:\n- [Código de conduta](https://github.com/devhatt/octopost/blob/master/docs/CODE_OF_CONDUCT.md)\n- [Contribuição](https://github.com/devhatt/octopost/blob/master/docs/CONTRIBUTING.md)\n- [Guia de Estilo](https://github.com/devhatt/octopost/blob/master/docs/STYLEGUIDE.md)" 25 | 26 | # Add the comment to the issue 27 | echo "Adding comment to the issue..." 28 | curl -X POST -H "Authorization: token ${{ secrets.PERSONAL_ACCESS_TOKEN }}" \ 29 | -d "{\"body\": \"$comment\"}" \ 30 | "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments" 31 | -------------------------------------------------------------------------------- /.github/workflows/base-workflows.yml: -------------------------------------------------------------------------------- 1 | name: Base workflows 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request: 7 | 8 | jobs: 9 | assignes: 10 | uses: devhatt/workflows/.github/workflows/auto-assign.yml@main 11 | secrets: inherit 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build project 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | contents: read 17 | pull-requests: write 18 | 19 | steps: 20 | - name: Code Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup deps 24 | uses: devhatt/workflows/.github/actions/pnpm-setup@main 25 | 26 | - name: Build 27 | run: pnpm build 28 | -------------------------------------------------------------------------------- /.github/workflows/code-style.yml: -------------------------------------------------------------------------------- 1 | name: Code Style 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | code-style: 13 | runs-on: ubuntu-latest 14 | name: code-style 15 | steps: 16 | - name: Code Checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: Setup deps 20 | uses: devhatt/workflows/.github/actions/pnpm-setup@main 21 | 22 | - name: Run prettier in all files 23 | run: pnpm exec prettier . --check --ignore-unknown 24 | 25 | - name: Run stylelint in scss files 26 | run: pnpm exec stylelint . --allow-empty-input 27 | 28 | - name: Run eslint in code files 29 | run: pnpm exec eslint . --report-unused-disable-directives --max-warnings 0 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: ['main'] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Code Checkout 12 | uses: actions/checkout@v3 13 | 14 | - name: Setup deps 15 | uses: ./.github/actions/install-deps 16 | 17 | - name: Run unit Tests 18 | run: pnpm vitest --silent --coverage 19 | 20 | - name: 'Report Coverage' 21 | uses: davelosert/vitest-coverage-report-action@v2 22 | 23 | - name: 'Upload Coverage' 24 | uses: actions/upload-artifact@v4 25 | with: 26 | name: coverage 27 | path: coverage 28 | -------------------------------------------------------------------------------- /.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 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | package-lock.json 26 | yarn.lock 27 | 28 | coverage -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.js": ["eslint --max-warnings=0 --fix ", "prettier --write"], 3 | "*.scss": ["stylelint --fix", "prettier --write"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ## PNPM related ############### 2 | ## https://pnpm.io/npmrc # 3 | ############################### 4 | 5 | # Not always possible to be strict, but if it works for you, keep it to true. 6 | # https://pnpm.io/next/npmrc#strict-peer-dependencies 7 | strict-peer-dependencies=true 8 | 9 | # https://pnpm.io/npmrc#auto-install-peers 10 | auto-install-peers=true 11 | 12 | # Helps locating duplicates, default in v8 13 | # https://pnpm.io/next/npmrc#use-lockfile-v6 14 | use-lockfile-v6=true 15 | 16 | # Will fix duplicates due to peer-dependencies (>=7.29.0), default in v8 17 | # https://github.com/pnpm/pnpm/releases/tag/v7.29.0 18 | dedupe-peer-dependents=true 19 | 20 | # Helps with peer-deps (>=7.23.0), default in v8 21 | # https://pnpm.io/npmrc#resolve-peers-from-workspace-root 22 | resolve-peers-from-workspace-root=true 23 | 24 | # default to 'lowest' in v8.5.0 25 | # set to highest for reasons specified here: https://github.com/pnpm/pnpm/issues/6463 26 | # https://pnpm.io/npmrc#resolution-mode 27 | resolution-mode=highest 28 | 29 | # Default in 8.1.0 to fix issues with root/workspaces hoisting 30 | # https://pnpm.io/npmrc#dedupe-direct-deps 31 | dedupe-direct-deps=false 32 | 33 | # Pinlock to exact version (default is '^') 34 | # https://pnpm.io/npmrc#save-prefix 35 | # see also how save-workspace-protocol affect this https://pnpm.io/npmrc#save-workspace-protocol 36 | save-prefix='' 37 | 38 | # Most of the time, you want to use the rolling protocol for monorepos 39 | # https://pnpm.io/npmrc#save-workspace-protocol 40 | save-workspace-protocol=rolling 41 | 42 | # Specifies which exact Node.js version should be used for the project's runtime. 43 | # pnpm will automatically install the specified version of Node.js and use it 44 | # for running pnpm run commands or the pnpm node command. 45 | # https://pnpm.io/npmrc#save-workspace-protocol 46 | use-node-version=20.10.0 47 | 48 | # Prevent contributors of your project from adding new incompatible dependencies 49 | # This way, even if someone is using Node.js v16, they will not be able to install 50 | # a new dependency that doesn't support Node.js v20.10.0 51 | # https://pnpm.io/npmrc#use-node-version 52 | node-version=20.10.0 53 | engine-strict=true 54 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": true, 7 | "jsxSingleQuote": false, 8 | "arrowParens": "always", 9 | "endOfLine": "auto", 10 | "printWidth": 80 11 | } 12 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | /** @type { import('@storybook/html-vite').StorybookConfig } */ 2 | const config = { 3 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 4 | addons: [ 5 | '@storybook/addon-links', 6 | '@storybook/addon-essentials', 7 | '@storybook/addon-interactions', 8 | ], 9 | framework: { 10 | name: '@storybook/html-vite', 11 | options: {}, 12 | }, 13 | docs: { 14 | autodocs: true, 15 | defaultName: 'Documentation', 16 | }, 17 | }; 18 | export default config; 19 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import '../src/styles/base.scss'; 2 | 3 | /** @type { import('@storybook/html').Preview } */ 4 | const preview = { 5 | parameters: { 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/i, 10 | }, 11 | }, 12 | }, 13 | }; 14 | 15 | export default preview; 16 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | .next 2 | .vscode 3 | node_modules 4 | *.* 5 | !*.scss -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | 'scope-empty': [2, 'always'], 5 | 'body-empty': [2, 'always'], 6 | 'footer-empty': [2, 'always'], 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["vitest/globals"], 4 | "baseUrl": ".", 5 | "paths": { 6 | "@testing-library/vanilla": ["src/__tests__/index.js"], 7 | "~src/*": ["src/*"], 8 | "~styles/*": ["src/styles/*"], 9 | "~stories/*": ["src/stories/*"], 10 | "~layouts/*": ["src/layouts/*"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/:resource/:id/my-pets": "/:resource/:id" 3 | } 4 | -------------------------------------------------------------------------------- /setup.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = path.dirname(__filename); 7 | 8 | const envFilePath = path.join(__dirname, '.env'); 9 | 10 | async function setupDotEnv() { 11 | if (fs.existsSync(envFilePath)) { 12 | console.log( 13 | 'O .env já existe, se você deseja recriá-lo, exclua o arquivo e execute este script novamente.', 14 | ); 15 | return; 16 | } 17 | 18 | try { 19 | console.log('Baixando o .env do repositório...'); 20 | const envData = await fetch( 21 | 'https://raw.githubusercontent.com/devhatt/envs/main/petdex-front.env', 22 | ).then((response) => response.text()); 23 | fs.writeFileSync(envFilePath, envData); 24 | console.log('O arquivo .env foi criado com sucesso!'); 25 | } catch (error) { 26 | console.error('Erro ao criar o arquivo .env:', error); 27 | } 28 | } 29 | 30 | setupDotEnv(); 31 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import { userEvent } from '@testing-library/user-event'; 2 | import { beforeEach } from 'vitest'; 3 | 4 | export * from '@testing-library/user-event'; 5 | export * from '@testing-library/dom'; 6 | 7 | // eslint-disable-next-line import/no-mutable-exports 8 | export let user = userEvent.setup(); 9 | 10 | /** 11 | * @template T 12 | * @param {T} element 13 | * @returns {T} 14 | */ 15 | export const render = (element) => { 16 | element.mount(document.body); 17 | 18 | return element; 19 | }; 20 | 21 | beforeEach(() => { 22 | user = userEvent.setup(); 23 | }); 24 | -------------------------------------------------------------------------------- /src/__tests__/setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { afterEach, vi } from 'vitest'; 3 | 4 | afterEach(() => { 5 | document.body.innerHTML = ''; 6 | vi.clearAllMocks(); 7 | }); 8 | 9 | Object.defineProperty(window, 'matchMedia', { 10 | writable: true, 11 | value: vi.fn().mockImplementation((query) => ({ 12 | matches: false, 13 | media: query, 14 | onchange: null, 15 | addListener: vi.fn(), 16 | removeListener: vi.fn(), 17 | addEventListener: vi.fn(), 18 | removeEventListener: vi.fn(), 19 | dispatchEvent: vi.fn(), 20 | })), 21 | }); 22 | -------------------------------------------------------------------------------- /src/components/AddPet/assets/arrow-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/AddPet/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/breakpoints.scss' as breakpoints; 2 | @use '~styles/colors.scss' as colors; 3 | @use '~styles/fonts' as fonts; 4 | 5 | .add-pet { 6 | &__header { 7 | padding: 2.4rem 2.4rem 0; 8 | } 9 | 10 | &__content { 11 | display: flex; 12 | 13 | justify-content: space-between; 14 | } 15 | 16 | &__text { 17 | text-align: center; 18 | } 19 | 20 | &__title { 21 | font-family: fonts.$sixthFont; 22 | font-size: fonts.$sm; 23 | font-weight: fonts.$semiBold; 24 | } 25 | 26 | &__subtitle { 27 | font-family: fonts.$fourthFont; 28 | color: colors.$gray600; 29 | font-size: 1.4rem; 30 | font-weight: fonts.$regular; 31 | line-height: 2; 32 | } 33 | 34 | &__steps { 35 | display: flex; 36 | gap: 0.2rem; 37 | 38 | justify-content: center; 39 | 40 | font-family: fonts.$fourthFont; 41 | color: colors.$gray800; 42 | font-size: fonts.$sm; 43 | font-weight: fonts.$regular; 44 | line-height: 1.6; 45 | 46 | &--first { 47 | font-family: fonts.$primaryFont; 48 | color: colors.$gray800; 49 | font-weight: fonts.$semiBold; 50 | } 51 | 52 | &--last { 53 | color: colors.$gray400; 54 | } 55 | } 56 | 57 | &__progress-bar { 58 | margin-top: 1.8rem; 59 | margin-bottom: 3rem; 60 | } 61 | 62 | &__previous-step { 63 | cursor: pointer; 64 | } 65 | } 66 | 67 | @include breakpoints.from1024 { 68 | .add-pet { 69 | &__header { 70 | font-family: fonts.$fourthFont; 71 | 72 | padding: 3rem 4rem 0; 73 | } 74 | 75 | &__title { 76 | font-size: 2rem; 77 | font-weight: fonts.$bold; 78 | } 79 | 80 | &__subtitle { 81 | font-size: fonts.$sm; 82 | font-weight: fonts.$regular; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/components/AvatarButton/images/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/components/AvatarButton/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | 4 | import cross from './images/cross.svg'; 5 | 6 | const html = ` 7 |
8 | 9 |
10 | cross 11 |
12 |
Adicionar amigo
13 |
14 |
15 | `; 16 | 17 | export default function AvatarButton() { 18 | Component.call(this, { html }); 19 | } 20 | 21 | AvatarButton.prototype = Object.assign( 22 | AvatarButton.prototype, 23 | Component.prototype, 24 | ); 25 | -------------------------------------------------------------------------------- /src/components/AvatarButton/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors' as colors; 2 | @use '~styles/fonts' as fonts; 3 | 4 | .avatar-button { 5 | display: flex; 6 | 7 | &__container { 8 | display: flex; 9 | flex-direction: column; 10 | gap: 0.8rem; 11 | 12 | align-items: center; 13 | 14 | text-decoration: none; 15 | 16 | &:hover { 17 | .avatar-button__bg { 18 | border: 0.15rem solid colors.$gray900; 19 | 20 | background: colors.$secondary100; 21 | } 22 | 23 | .avatar-button__cross { 24 | filter: invert(1); 25 | } 26 | 27 | .avatar-button__text { 28 | opacity: 0.8; 29 | } 30 | } 31 | } 32 | 33 | &__bg { 34 | width: 3rem; 35 | height: 3rem; 36 | 37 | display: flex; 38 | 39 | align-items: center; 40 | justify-content: center; 41 | 42 | padding: 1.8rem; 43 | border: 0.15rem solid colors.$secondary100; 44 | 45 | background: colors.$gray900; 46 | border-radius: 8rem; 47 | 48 | transition: 0.6s; 49 | } 50 | 51 | &__cross { 52 | filter: invert(0); 53 | 54 | transition: 0.6s; 55 | } 56 | 57 | &__text { 58 | font-family: fonts.$primaryFont; 59 | color: colors.$secondary100; 60 | font-size: 1.4rem; 61 | 62 | transition: 0.6s; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | 4 | const events = ['click', 'text:change', 'enable', 'disable', 'id:changed']; 5 | 6 | const html = ` 7 | 8 | `; 9 | 10 | export default function Button({ 11 | id = '', 12 | text = '', 13 | isFullWidth = false, 14 | isDisabled = false, 15 | } = {}) { 16 | Component.call(this, { html, events }); 17 | 18 | this.setText(text); 19 | this.setIsFullWidth(isFullWidth); 20 | this.setIsDisabled(isDisabled); 21 | this.setID(id); 22 | 23 | const $button = this.selected.get('button'); 24 | 25 | const handleClick = () => { 26 | this.click(); 27 | }; 28 | 29 | this.listen('mount', () => { 30 | $button.addEventListener('click', handleClick); 31 | }); 32 | 33 | this.listen('unmount', () => { 34 | $button.removeEventListener('click', handleClick); 35 | }); 36 | } 37 | 38 | Button.prototype = Object.assign(Button.prototype, Component.prototype, { 39 | getText() { 40 | return this.selected.get('button').textContent; 41 | }, 42 | 43 | setText(text = '') { 44 | this.selected.get('button').textContent = text; 45 | this.emit('text:change', text); 46 | }, 47 | 48 | isFullWidth() { 49 | return this.selected.get('button').classList.has('button--block'); 50 | }, 51 | 52 | setIsFullWidth(isFullWidth = false) { 53 | const { classList } = this.selected.get('button'); 54 | if (isFullWidth) classList.add('button--block'); 55 | else classList.remove('button--block'); 56 | }, 57 | 58 | disable() { 59 | this.selected.get('button').setAttribute('disabled', true); 60 | this.emit('disable'); 61 | }, 62 | 63 | enable() { 64 | this.selected.get('button').removeAttribute('disabled'); 65 | this.emit('enable'); 66 | }, 67 | 68 | setIsDisabled(isDisabled = false) { 69 | if (isDisabled) this.disable(); 70 | else this.enable(); 71 | }, 72 | 73 | isDisabled() { 74 | return this.selected.get('button').hasAttribute('disabled'); 75 | }, 76 | 77 | click() { 78 | if (this.isDisabled()) return; 79 | this.emit('click'); 80 | }, 81 | 82 | getID() { 83 | return this.selected.get('button').id; 84 | }, 85 | 86 | setID(id = '') { 87 | this.selected.get('button').id = id; 88 | this.emit('id:changed', id); 89 | }, 90 | }); 91 | -------------------------------------------------------------------------------- /src/components/Button/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | 3 | .button { 4 | font-family: 'Noto Sans', sans-serif; 5 | color: rgb(255, 255, 255); 6 | font-size: 1.6rem; 7 | font-weight: 500; 8 | 9 | padding: 1.6rem; 10 | border: unset; 11 | 12 | background-color: colors.$primary200; 13 | border-radius: 1.4rem; 14 | 15 | transition: 0.3s ease-in-out; 16 | 17 | cursor: pointer; 18 | appearance: none; 19 | 20 | &--block { 21 | width: 100%; 22 | 23 | display: block; 24 | } 25 | 26 | &:hover:not(:disabled) { 27 | background: colors.$primary600; 28 | transform: scale(1.02); 29 | } 30 | 31 | &:disabled { 32 | background: colors.$gray600; 33 | 34 | cursor: not-allowed; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Button/index.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { render, screen, user } from '@testing-library/vanilla'; 3 | import Button from '.'; 4 | 5 | const makeSut = (parameters) => render(new Button(parameters)); 6 | 7 | describe('Button', () => { 8 | describe('when mount', () => { 9 | it('render with correct text', () => { 10 | makeSut({ text: 'content' }); 11 | const text = screen.getByText('content'); 12 | 13 | expect(text).toBeInTheDocument(); 14 | }); 15 | 16 | it('render disabled', () => { 17 | makeSut({ isDisabled: true }); 18 | 19 | const element = screen.getByRole('button'); 20 | 21 | expect(element).toBeDisabled(); 22 | }); 23 | 24 | it('render enabled', async () => { 25 | makeSut({ isDisabled: false }); 26 | 27 | const button = screen.getByRole('button'); 28 | 29 | expect(button).toBeEnabled(); 30 | }); 31 | }); 32 | 33 | describe('when call [setIsFullWidth]', () => { 34 | it('append button--block class to button', async () => { 35 | const element = makeSut({ isFullWidth: false }); 36 | element.setIsFullWidth(true); 37 | 38 | const button = screen.getByRole('button'); 39 | 40 | expect(button).toHaveClass('button--block'); 41 | }); 42 | }); 43 | 44 | describe('when click in the button', () => { 45 | it('should emit click', async () => { 46 | const element = makeSut(); 47 | const onClick = vi.fn(); 48 | element.listen('click', onClick); 49 | 50 | const button = screen.getByRole('button'); 51 | await user.click(button); 52 | 53 | expect(onClick).toHaveBeenCalled(); 54 | }); 55 | }); 56 | 57 | describe('when call [click]', () => { 58 | it('should not emit click', async () => { 59 | const element = makeSut({ isDisabled: true }); 60 | const onClick = vi.fn(); 61 | element.listen('click', onClick); 62 | 63 | element.click(); 64 | 65 | expect(onClick).not.toHaveBeenCalled(); 66 | }); 67 | }); 68 | 69 | describe('when unmount', () => { 70 | it('remove click eventListener', () => { 71 | const element = makeSut(); 72 | const button = screen.getByRole('button'); 73 | 74 | const buttonSpy = vi.spyOn(button, 'removeEventListener'); 75 | 76 | element.unmount(); 77 | 78 | expect(buttonSpy).toHaveBeenCalled(); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/components/Card/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | 4 | const events = ['purchase']; 5 | 6 | const html = ` 7 |
8 | 9 | 10 | 11 |
12 | `; 13 | 14 | export default function Card() { 15 | Component.call(this, { html, events }); 16 | this.enable = true; 17 | 18 | this.selected.get('card-button').addEventListener('click', () => { 19 | if (!this.enable) return; 20 | this.purchase(); 21 | }); 22 | } 23 | 24 | Card.prototype = Object.assign(Card.prototype, Component.prototype, { 25 | setTitle(text) { 26 | this.selected.get('card-title').textContent = text; 27 | }, 28 | 29 | purchase() { 30 | this.emit('purchase'); 31 | }, 32 | 33 | disable() { 34 | this.enable = false; 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /src/components/Card/index.scss: -------------------------------------------------------------------------------- 1 | .card-container { 2 | width: 377px; 3 | height: 500px; 4 | overflow: hidden; 5 | 6 | display: flex; 7 | flex-direction: column; 8 | 9 | background-color: rgb(255, 255, 255); 10 | box-shadow: 0 4px 8px 0 rgb(0 0 0 / 31%); 11 | border-radius: 21px; 12 | 13 | &__image { 14 | height: 73%; 15 | 16 | background-color: rgb(0, 128, 0); 17 | } 18 | 19 | &__title { 20 | display: block; 21 | flex-grow: 1; 22 | 23 | font-family: Arial; 24 | font-size: 3.4rem; 25 | font-weight: bold; 26 | 27 | padding: 2.1rem; 28 | } 29 | 30 | &__button { 31 | font-size: 2.1rem; 32 | font-weight: bold; 33 | 34 | padding: 2.1rem; 35 | border: 0; 36 | 37 | transition: 0.21s all ease; 38 | 39 | cursor: pointer; 40 | 41 | &--active { 42 | color: rgb(255, 255, 255); 43 | 44 | background-color: rgb(255, 101, 0); 45 | } 46 | 47 | &:hover { 48 | transform: scale(1.1); 49 | } 50 | 51 | &--disabled { 52 | color: rgb(69, 69, 69); 53 | 54 | background-color: rgb(156, 156, 156); 55 | 56 | &:hover { 57 | transform: none; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/ChangePassword/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/fonts.scss' as fonts; 3 | @use '~styles/breakpoints.scss' as breakpoints; 4 | 5 | .change-password { 6 | height: 100%; 7 | 8 | display: flex; 9 | flex-direction: column; 10 | gap: 2rem; 11 | 12 | font-family: fonts.$primaryFont; 13 | line-height: 1.5; 14 | 15 | padding: 0 2rem; 16 | 17 | background-color: colors.$secondary100; 18 | 19 | &__title { 20 | color: colors.$gray600; 21 | font-size: fonts.$sm; 22 | font-weight: fonts.$bold; 23 | } 24 | 25 | &__error { 26 | display: none; 27 | 28 | color: colors.$error100; 29 | 30 | &.show-error { 31 | display: block; 32 | } 33 | } 34 | 35 | &__tips { 36 | color: colors.$gray500; 37 | font-weight: fonts.$regular; 38 | 39 | list-style-type: disc; 40 | padding-inline-start: 3rem; 41 | } 42 | 43 | &__button { 44 | margin-top: auto; 45 | margin-bottom: 1rem; 46 | } 47 | 48 | &__separator { 49 | width: 100%; 50 | 51 | display: flex; 52 | 53 | border: 0; 54 | border-top: 0.1rem solid colors.$gray200; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Checkbox/images/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | 4 | const events = [ 5 | 'change', 6 | 'value:change', 7 | 'text:change', 8 | 'name:change', 9 | 'disable', 10 | ]; 11 | 12 | const html = ` 13 | 17 | `; 18 | 19 | export default function Checkbox({ 20 | check = false, 21 | name = '', 22 | text = '', 23 | disabled = false, 24 | value = '', 25 | } = {}) { 26 | Component.call(this, { html, events }); 27 | 28 | this.setCheck(check); 29 | this.setText(text); 30 | this.setName(name); 31 | this.setDisabled(disabled); 32 | this.setValue(value); 33 | 34 | const $checkbox = this.selected.get('checkbox'); 35 | 36 | $checkbox.addEventListener('change', (e) => { 37 | this.setCheck(e.target.checked); 38 | }); 39 | } 40 | 41 | Checkbox.prototype = Object.assign(Checkbox.prototype, Component.prototype, { 42 | isChecked() { 43 | return this.selected.get('checkbox').checked; 44 | }, 45 | 46 | setCheck(check = false) { 47 | const $checkbox = this.selected.get('checkbox'); 48 | $checkbox.checked = check; 49 | this.emit('change', check); 50 | }, 51 | 52 | setText(text = '') { 53 | const $checkboxText = this.selected.get('checkbox-text'); 54 | $checkboxText.textContent = text; 55 | this.emit('text:change', text); 56 | }, 57 | 58 | getText() { 59 | return this.selected.get('checkbox-text').textContent; 60 | }, 61 | 62 | isDisabled() { 63 | return this.selected.get('checkbox').disabled; 64 | }, 65 | 66 | setDisabled(disabled = false) { 67 | const $checkbox = this.selected.get('checkbox'); 68 | $checkbox.disabled = disabled; 69 | this.emit('disable', disabled); 70 | }, 71 | 72 | setValue(value = '') { 73 | const $checkbox = this.selected.get('checkbox'); 74 | $checkbox.value = value; 75 | this.emit('value:change', value); 76 | }, 77 | 78 | getValue() { 79 | return this.selected.get('checkbox').value; 80 | }, 81 | 82 | getName() { 83 | const $checkbox = this.selected.get('checkbox'); 84 | return $checkbox.name; 85 | }, 86 | 87 | setName(name = '') { 88 | const $checkbox = this.selected.get('checkbox'); 89 | $checkbox.name = name; 90 | this.emit('name:change', name); 91 | }, 92 | }); 93 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/fonts.scss' as fonts; 3 | 4 | .checkbox-container { 5 | display: inline-flex; 6 | 7 | align-items: center; 8 | 9 | cursor: pointer; 10 | 11 | &__input { 12 | width: 2.2rem; 13 | height: 2.2rem; 14 | 15 | margin: 0.8rem; 16 | border: 2px solid colors.$gray250; 17 | 18 | border-radius: 0.5rem; 19 | 20 | cursor: pointer; 21 | appearance: none; 22 | 23 | &:checked { 24 | border-color: colors.$primary200; 25 | 26 | background: colors.$primary200 url('./images/check.svg') no-repeat center 27 | center/70%; 28 | } 29 | 30 | &:disabled { 31 | cursor: not-allowed; 32 | } 33 | } 34 | 35 | &__checkboxText { 36 | font-family: fonts.$primaryFont; 37 | color: colors.$gray800; 38 | font-size: fonts.$xs; 39 | font-weight: fonts.$semiBold; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Drawer/images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Drawer/images/line.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Drawer/index.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { render, screen } from '@testing-library/vanilla'; 3 | import { userEvent } from '@testing-library/user-event'; 4 | import Drawer from '.'; 5 | import Button from '../Button'; 6 | 7 | const makeSut = () => 8 | render( 9 | new Drawer({ 10 | title: 'Add dates', 11 | content: new Button({ 12 | text: 'Cadastrar pet', 13 | isFullWidth: true, 14 | isDisabled: false, 15 | }), 16 | }), 17 | ); 18 | 19 | describe('Drawer', () => { 20 | describe('on mount', () => { 21 | it('renders', () => { 22 | const drawer = makeSut(); 23 | 24 | drawer.open(); 25 | 26 | const firstText = screen.getByText('Add dates'); 27 | const secondText = screen.getByText('Cadastrar pet'); 28 | 29 | expect(firstText).toBeInTheDocument(); 30 | expect(secondText).toBeInTheDocument(); 31 | }); 32 | }); 33 | 34 | describe('on unmount', () => { 35 | it('remove event listeners', () => { 36 | const drawer = makeSut(); 37 | const removeEventListenerSpy = vi.spyOn(window, 'removeEventListener'); 38 | const onEscapeKeySpy = vi.spyOn(drawer, 'onEscapeKey'); 39 | 40 | drawer.open(); 41 | drawer.emit('unmount'); 42 | 43 | expect(removeEventListenerSpy).toHaveBeenCalledWith( 44 | 'keydown', 45 | onEscapeKeySpy, 46 | ); 47 | }); 48 | }); 49 | 50 | it('closes when Esc is pressed', async () => { 51 | const drawer = makeSut(); 52 | const closeSpy = vi.spyOn(drawer, 'close'); 53 | 54 | await userEvent.keyboard('{Escape}'); 55 | 56 | expect(closeSpy).toHaveBeenCalled(); 57 | }); 58 | 59 | it('closes when the close button is clicked', async () => { 60 | const drawer = makeSut(); 61 | const closeSpy = vi.spyOn(drawer, 'close'); 62 | 63 | drawer.open(); 64 | 65 | const button = screen.getByLabelText('close-drawer'); 66 | await userEvent.click(button); 67 | 68 | expect(closeSpy).toHaveBeenCalled(); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/components/Dropdown/components/DropdownItem/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | 4 | const events = ['text:change', 'value:change', 'select', 'unselect']; 5 | 6 | const html = ` 7 | 8 | `; 9 | 10 | export default function DropdownItem({ text = '', value = '' } = {}) { 11 | Component.call(this, { html, events }); 12 | 13 | this.setText(text); 14 | this.setValue(value); 15 | 16 | this.selected.get('option').addEventListener('click', () => this.toggle()); 17 | } 18 | 19 | DropdownItem.prototype = Object.assign( 20 | DropdownItem.prototype, 21 | Component.prototype, 22 | { 23 | getText() { 24 | return this.selected.get('option').textContent; 25 | }, 26 | 27 | setText(text = '') { 28 | this.emit('text:change', text); 29 | this.selected.get('option').textContent = text; 30 | }, 31 | 32 | getValue() { 33 | return this.selected.get('option').dataset.value; 34 | }, 35 | 36 | setValue(value = '') { 37 | this.emit('value:change', value); 38 | this.selected.get('option').dataset.value = value; 39 | }, 40 | 41 | isSelected() { 42 | return this.selected.get('option').dataset.selected === 'true'; 43 | }, 44 | 45 | toggle(condition = this.isSelected()) { 46 | if (condition) this.unselect(); 47 | else this.select(); 48 | }, 49 | 50 | select() { 51 | this.selected.get('option').dataset.selected = true; 52 | this.emit('select', this); 53 | }, 54 | 55 | unselect() { 56 | this.selected.get('option').dataset.selected = false; 57 | this.emit('unselect', this); 58 | }, 59 | 60 | toJSON() { 61 | return { 62 | text: this.getText(), 63 | value: this.getValue(), 64 | }; 65 | }, 66 | }, 67 | ); 68 | -------------------------------------------------------------------------------- /src/components/Dropdown/components/DropdownItem/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | 3 | .dropdown__options--option { 4 | padding: 0.3rem; 5 | 6 | &:hover { 7 | background-color: colors.$primary200; 8 | } 9 | 10 | &[data-selected='true'] { 11 | color: colors.$blue600; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Dropdown/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/fonts.scss' as fonts; 3 | 4 | .dropdown { 5 | width: 100%; 6 | 7 | font-family: fonts.$primaryFont; 8 | 9 | color: colors.$gray800; 10 | font-size: 1.4rem; 11 | font-weight: fonts.$semiBold; 12 | 13 | position: relative; 14 | 15 | &--open { 16 | .dropdown__toggle { 17 | .dropdown__icon { 18 | rotate: 180deg; 19 | } 20 | } 21 | 22 | .dropdown__options { 23 | display: block; 24 | } 25 | } 26 | 27 | &:hover { 28 | cursor: pointer; 29 | } 30 | 31 | &__options { 32 | width: 100%; 33 | 34 | display: none; 35 | 36 | margin: 0; 37 | padding: 1rem; 38 | box-sizing: border-box; 39 | 40 | position: absolute; 41 | z-index: 2; 42 | 43 | list-style: none; 44 | 45 | background-color: colors.$white; 46 | 47 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.2); 48 | border-radius: 1.4rem; 49 | 50 | animation: dropdownAnimation 0.3s ease-out; 51 | } 52 | 53 | &__toggle { 54 | display: flex; 55 | gap: 2rem; 56 | 57 | justify-content: space-between; 58 | 59 | padding: 1.8rem 1.6rem; 60 | 61 | border: 1px solid colors.$gray200; 62 | 63 | box-sizing: border-box; 64 | 65 | background-color: colors.$white; 66 | 67 | border-radius: 1.4rem; 68 | 69 | .dropdown__icon { 70 | color: colors.$gray600; 71 | 72 | transition: transform 0.4s ease-out; 73 | } 74 | 75 | .dropdown__selected { 76 | &--placeholder { 77 | color: colors.$gray600; 78 | font-weight: 400; 79 | } 80 | } 81 | } 82 | } 83 | 84 | @keyframes dropdownAnimation { 85 | from { 86 | transform: translateY(-2.5rem); 87 | opacity: 0; 88 | } 89 | 90 | to { 91 | transform: translateY(0); 92 | opacity: 1; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/components/LoginForm/images/facebook-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/LoginForm/images/google-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Google-color 6 | Created with Sketch. 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/PetAvatar/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | import { routeLocation } from 'vanilla-routing'; 4 | 5 | const events = ['active']; 6 | 7 | const html = ` 8 | 9 | 10 |

11 |
12 | `; 13 | 14 | export default function PetAvatar({ id, title, imgSrc, imgAlt } = {}) { 15 | Component.call(this, { html, events }); 16 | 17 | if (id) this.setHref(id); 18 | if (title) this.setTitle(title); 19 | if (imgSrc) this.setImgSrc(imgSrc); 20 | if (imgAlt) this.setImgAlt(imgAlt); 21 | 22 | if (routeLocation().pathname === `/petperfil/${id}`) { 23 | this.activate(); 24 | } 25 | } 26 | 27 | PetAvatar.prototype = Object.assign(PetAvatar.prototype, Component.prototype, { 28 | setTitle(text) { 29 | this.selected.get('pet-title').textContent = text; 30 | }, 31 | setImgSrc(src) { 32 | this.selected.get('pet-image').src = src; 33 | }, 34 | setImgAlt(alt) { 35 | this.selected.get('pet-image').alt = alt; 36 | }, 37 | setHref(id) { 38 | this.selected.get('pet-avatar').href += id; 39 | }, 40 | activate() { 41 | const image = this.selected.get('pet-image'); 42 | const title = this.selected.get('pet-title'); 43 | image.classList.add('pet-avatar__img--active'); 44 | title.classList.add('pet-avatar__title--active'); 45 | this.emit('active'); 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /src/components/PetAvatar/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors' as colors; 2 | @use '~styles/fonts' as fonts; 3 | 4 | .pet-avatar { 5 | display: flex; 6 | flex-direction: column; 7 | gap: 1rem; 8 | 9 | align-items: center; 10 | 11 | text-decoration: none; 12 | aspect-ratio: 1/1; 13 | 14 | opacity: 0.85; 15 | 16 | transition: 0.3s ease-in-out; 17 | 18 | &__img { 19 | height: 100%; 20 | 21 | outline: 0.15rem solid colors.$gray500; 22 | 23 | border-radius: 50%; 24 | object-fit: cover; 25 | aspect-ratio: 1/1; 26 | 27 | transition: 0.3s ease-in-out; 28 | 29 | &--active { 30 | outline: 0.3rem solid colors.$primary200; 31 | } 32 | } 33 | 34 | &__title { 35 | font-family: fonts.$fourthFont; 36 | 37 | color: colors.$gray200; 38 | text-align: center; 39 | font-size: 1.4rem; 40 | font-weight: fonts.$regular; 41 | 42 | transition: 0.3s ease-in-out; 43 | 44 | &--active { 45 | color: colors.$primary200; 46 | font-weight: fonts.$semiBold; 47 | } 48 | } 49 | 50 | &:hover { 51 | transform: scale(1.02); 52 | opacity: 1; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/components/PetCard/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | 4 | const events = ['active', 'deactive', 'title:change']; 5 | 6 | const html = ` 7 |
8 |

9 | 10 |
11 | `; 12 | 13 | export default function PetCard({ title, imgSrc, imgAlt }) { 14 | Component.call(this, { html, events }); 15 | 16 | const petContainer = this.selected.get('pet-container'); 17 | const petTitle = this.selected.get('pet-title'); 18 | const petImage = this.selected.get('pet-image'); 19 | 20 | petContainer.setAttribute('aria-label', title.toLowerCase()); 21 | 22 | petContainer.addEventListener('click', () => { 23 | this.toggle(); 24 | }); 25 | 26 | petContainer.addEventListener('mouseenter', () => { 27 | if (petTitle.scrollHeight > petTitle.clientHeight) { 28 | petContainer.classList.add('pet-container__title--extended'); 29 | petImage.style.marginBottom = `-${ 30 | petTitle.scrollHeight - petTitle.clientHeight 31 | }px`; 32 | } 33 | }); 34 | 35 | petContainer.addEventListener('mouseleave', () => { 36 | if (petContainer.classList.contains('pet-container__title--extended')) { 37 | const transitionEndHandler = () => { 38 | petContainer.classList.remove('pet-container__title--extended'); 39 | petImage.removeEventListener('transitionend', transitionEndHandler); 40 | }; 41 | 42 | petImage.addEventListener('transitionend', transitionEndHandler); 43 | petImage.style.marginBottom = '0px'; 44 | } 45 | }); 46 | 47 | if (title) this.setTitle(title); 48 | if (imgSrc) this.setImgSrc(imgSrc); 49 | if (imgAlt) this.setImgAlt(imgAlt); 50 | } 51 | 52 | PetCard.prototype = Object.assign(PetCard.prototype, Component.prototype, { 53 | getTitle() { 54 | return this.selected.get('pet-title').textContent; 55 | }, 56 | setTitle(text) { 57 | this.selected.get('pet-title').textContent = text; 58 | }, 59 | setImgSrc(src) { 60 | this.selected.get('pet-image').src = src; 61 | }, 62 | setImgAlt(alt) { 63 | this.selected.get('pet-image').alt = alt; 64 | }, 65 | isActive() { 66 | return this.selected 67 | .get('pet-container') 68 | .classList.contains('pet-container--active'); 69 | }, 70 | toggle() { 71 | if (this.isActive()) this.deactivate(); 72 | else this.activate(); 73 | }, 74 | activate() { 75 | const petContainer = this.selected.get('pet-container'); 76 | petContainer.classList.add('pet-container--active'); 77 | this.emit('active'); 78 | }, 79 | deactivate() { 80 | const petContainer = this.selected.get('pet-container'); 81 | petContainer.classList.remove('pet-container--active'); 82 | this.emit('deactive'); 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /src/components/PetCard/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/base.scss'; 2 | 3 | .pet-container { 4 | aspect-ratio: 1/1; 5 | overflow: hidden; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | 10 | border: 1px solid rgb(236, 239, 242); 11 | box-sizing: border-box; 12 | 13 | background-color: rgb(255, 255, 255); 14 | box-shadow: 0 0 0.5rem 0 rgba(12, 26, 75, 0.05); 15 | 16 | border-radius: 1.8rem; 17 | 18 | &__image { 19 | height: 100%; 20 | 21 | transition: margin-bottom 0.3s ease-in-out; 22 | } 23 | 24 | &__title { 25 | height: 100%; 26 | min-height: 2.4em; 27 | max-height: 2.4em; 28 | overflow: hidden; 29 | 30 | display: -webkit-box; 31 | flex-grow: 1; 32 | 33 | font-family: 'Montserrat', sans-serif; 34 | text-align: center; 35 | font-size: clamp(1.2rem, 1.5vw, 1.6rem); 36 | font-weight: 600; 37 | line-height: 1.2; 38 | text-overflow: ellipsis; 39 | word-wrap: break-word; 40 | 41 | margin: 15% 0.5rem 0; 42 | 43 | -webkit-line-clamp: 2; 44 | -webkit-box-orient: vertical; 45 | } 46 | 47 | &--active { 48 | color: rgb(27, 133, 243); 49 | 50 | outline: 2px solid rgb(27, 133, 243); 51 | } 52 | 53 | &__title--extended { 54 | .pet-container__title { 55 | max-height: fit-content; 56 | overflow: unset; 57 | -webkit-line-clamp: unset; 58 | } 59 | } 60 | 61 | &:hover { 62 | box-shadow: rgba(100, 100, 111, 0.2) 0 0.2rem 1.5rem 0; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/ProgressBar/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | 4 | const events = ['setProgress']; 5 | 6 | const html = ` 7 |
8 |
9 |
10 |
11 | `; 12 | 13 | function isValueValid(value, minimum, maximum) { 14 | return value >= minimum && value <= maximum; 15 | } 16 | 17 | function getRatio(value, minimum, maximum) { 18 | return (value - minimum) / (maximum - minimum); 19 | } 20 | 21 | function getWidthFormated(value, minimum, maximum) { 22 | const ratio = getRatio(value, minimum, maximum); 23 | return `${ratio * 100}%`; 24 | } 25 | 26 | export default function ProgressBar(minimum, maximum, startValue = minimum) { 27 | Component.call(this, { html, events }); 28 | this.minimum = minimum; 29 | this.maximum = maximum; 30 | this.currentProgress = isValueValid(startValue, this.minimum, this.maximum) 31 | ? startValue 32 | : this.minimum; 33 | 34 | this.setProgress(this.currentProgress); 35 | this.selected.get('progress-bar').ariaValueMin = this.minimum; 36 | this.selected.get('progress-bar').ariaValueMax = this.maximum; 37 | this.selected.get('progress-bar').ariaValueNow = this.currentProgress; 38 | } 39 | 40 | ProgressBar.prototype = Object.assign( 41 | ProgressBar.prototype, 42 | Component.prototype, 43 | { 44 | setProgress(value) { 45 | if (!isValueValid(value, this.minimum, this.maximum)) return; 46 | this.currentProgress = value; 47 | this.selected.get('progress-bar-foreground').style.width = 48 | getWidthFormated(this.currentProgress, this.minimum, this.maximum); 49 | this.selected.get('progress-bar').ariaValueNow = this.currentProgress; 50 | this.emit('setProgress', this.currentProgress); 51 | }, 52 | next() { 53 | this.setProgress(this.currentProgress + 1); 54 | }, 55 | prev() { 56 | this.setProgress(this.currentProgress - 1); 57 | }, 58 | }, 59 | ); 60 | -------------------------------------------------------------------------------- /src/components/ProgressBar/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | 3 | .progress-bar { 4 | width: 100%; 5 | 6 | display: grid; 7 | 8 | &__background { 9 | width: 100%; 10 | height: 50%; 11 | 12 | grid-row: 1; 13 | grid-column: 1; 14 | 15 | position: relative; 16 | top: 25%; 17 | z-index: 0; 18 | 19 | background-color: colors.$gray150; 20 | border-radius: 100000px; 21 | } 22 | 23 | &__foreground { 24 | height: 6px; 25 | 26 | grid-row: 1; 27 | grid-column: 1; 28 | 29 | position: relative; 30 | z-index: 1; 31 | 32 | background-color: colors.$primary300; 33 | border-radius: 100000px; 34 | 35 | transition: width 1s ease-in-out; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/ProgressBar/index.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, test } from 'vitest'; 2 | import ProgressBar from './index'; 3 | 4 | describe('ProgressBar', () => { 5 | it('is a Function', () => { 6 | expect(ProgressBar).toBeInstanceOf(Function); 7 | }); 8 | 9 | it('returns an object', () => { 10 | expect(new ProgressBar()).toBeInstanceOf(Object); 11 | }); 12 | 13 | test.each([ 14 | [-1, 0], 15 | [0, 0], 16 | [1, 1], 17 | [2, 2], 18 | [3, 3], 19 | [4, 4], 20 | [5, 5], 21 | [6, 0], 22 | ])('it initializes with a specific valid value set', (value, expected) => { 23 | const progressBar = new ProgressBar(0, 5, value); 24 | expect(progressBar.currentProgress).toBe(expected); 25 | }); 26 | 27 | it('increments value when next is called', () => { 28 | const progressBar = new ProgressBar(0, 5, 10); 29 | progressBar.next(); 30 | expect(progressBar.currentProgress).toBe(1); 31 | }); 32 | 33 | it('keeps the maximum value when next is called and it is already at the maximum', () => { 34 | const progressBar = new ProgressBar(0, 5, 5); 35 | progressBar.next(); 36 | expect(progressBar.currentProgress).toBe(progressBar.maximum); 37 | }); 38 | 39 | it('decrements value when previous is called', () => { 40 | const progressBar = new ProgressBar(0, 5, 5); 41 | progressBar.prev(); 42 | expect(progressBar.currentProgress).toBe(4); 43 | }); 44 | 45 | it('keeps the minimum value when previous is called and it is already at the minimum', () => { 46 | const progressBar = new ProgressBar(0, 5, 10); 47 | progressBar.prev(); 48 | expect(progressBar.currentProgress).toBe(progressBar.minimum); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/RadioButton/images/ellipse.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/RadioButton/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors' as colors; 2 | @use '~styles/fonts.scss' as fonts; 3 | 4 | .radio-container { 5 | display: inline-flex; 6 | 7 | align-items: center; 8 | 9 | color: colors.$gray600; 10 | 11 | padding: 1rem 8rem 1rem 1rem; 12 | border: 0.15rem solid colors.$gray200; 13 | 14 | border-radius: 1.5rem; 15 | 16 | transition: border-color 0.3s; 17 | 18 | cursor: pointer; 19 | 20 | &--borderless { 21 | padding: 0; 22 | border: 0; 23 | } 24 | 25 | &:has(&__input:checked) { 26 | color: colors.$primary200; 27 | 28 | border-color: colors.$primary200; 29 | } 30 | 31 | &__input { 32 | width: 1.6rem; 33 | height: 1.6rem; 34 | 35 | margin: 0.8rem; 36 | border: 1px solid colors.$gray200; 37 | 38 | border-radius: 50%; 39 | 40 | cursor: pointer; 41 | appearance: none; 42 | 43 | &:checked { 44 | border-color: colors.$primary200; 45 | 46 | background: url('./images/ellipse.svg') no-repeat center center/70% 70%; 47 | } 48 | 49 | &:disabled { 50 | border-color: colors.$gray200; 51 | 52 | cursor: not-allowed; 53 | } 54 | } 55 | 56 | &__dot { 57 | font-family: fonts.$primaryFont; 58 | font-size: fonts.$xs; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/RangeSlider/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/base.scss'; 2 | @use '~styles/colors.scss' as colors; 3 | @use '~styles/breakpoints.scss' as breakpoints; 4 | 5 | .range-slider { 6 | width: 100%; 7 | 8 | display: flex; 9 | flex-direction: column; 10 | 11 | align-items: center; 12 | 13 | &__button { 14 | color: rgb(209, 230, 255); 15 | 16 | font-size: 2rem; 17 | 18 | padding: 1rem; 19 | border: 2px solid colors.$blue600; 20 | 21 | position: absolute; 22 | top: 50%; 23 | left: 50%; 24 | 25 | background-color: colors.$primary200; 26 | transform: translate(-50%, -50%); 27 | 28 | border-radius: 0.8rem; 29 | 30 | cursor: grab; 31 | letter-spacing: 1; 32 | } 33 | 34 | &__info { 35 | display: inline-block; 36 | 37 | font-family: 'Noto Sans', sans-serif; 38 | color: colors.$primary200; 39 | 40 | text-align: center; 41 | font-size: 8rem; 42 | font-weight: 700; 43 | 44 | margin-bottom: 2rem; 45 | } 46 | 47 | &__info:nth-child(2) { 48 | display: none; 49 | } 50 | 51 | &__content { 52 | width: 100%; 53 | height: 100px; 54 | overflow: hidden; 55 | 56 | position: relative; 57 | 58 | background-image: url('../../home/images/ruler-slider.svg'); 59 | background-repeat: repeat-x; 60 | background-position: 0 center; 61 | } 62 | 63 | &__content::before { 64 | left: 0; 65 | 66 | background: linear-gradient( 67 | to right, 68 | colors.$secondary100, 69 | rgba(255, 255, 255, 0) 70 | ); 71 | } 72 | 73 | &__content::after { 74 | right: 0; 75 | 76 | background: linear-gradient( 77 | to left, 78 | colors.$secondary100, 79 | rgba(255, 255, 255, 0) 80 | ); 81 | } 82 | 83 | &__content::before, 84 | &__content::after { 85 | width: 30px; 86 | height: 100%; 87 | 88 | position: absolute; 89 | z-index: 2; 90 | 91 | pointer-events: none; 92 | content: ''; 93 | } 94 | } 95 | 96 | @include breakpoints.from667 { 97 | .range-slider__content::before, 98 | .range-slider__content::after { 99 | width: 16%; 100 | } 101 | 102 | .range-slider__button { 103 | padding: 1rem 2rem; 104 | } 105 | 106 | .range-slider__info:first-child { 107 | padding-right: 2rem; 108 | } 109 | 110 | .range-slider__info:nth-child(2) { 111 | display: inline-block; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/components/RegisterForm/components/Field/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | 4 | const events = [ 5 | 'label:changed', 6 | 'error:message', 7 | 'content:changed', 8 | 'error:visible', 9 | 'error:resolved', 10 | ]; 11 | 12 | const html = ` 13 |
14 | 15 |
16 | 17 |
18 | `; 19 | 20 | export default function Field({ label = '', error = '', content } = {}) { 21 | Component.call(this, { html, events }); 22 | this.setLabel(label); 23 | this.setError(error); 24 | this.setContent(content); 25 | } 26 | 27 | Field.prototype = Object.assign(Field.prototype, Component.prototype, { 28 | getLabel() { 29 | return this.selected.get('field-label').innerText; 30 | }, 31 | 32 | setLabel(label = '') { 33 | this.selected.get('field-label').innerText = label; 34 | this.emit('label:changed', label); 35 | }, 36 | 37 | getError() { 38 | return this.selected.get('field-error').innerText; 39 | }, 40 | 41 | setError(error = '') { 42 | this.selected.get('field-error').innerText = error; 43 | this.emit('error:message', error); 44 | }, 45 | 46 | showError(error) { 47 | this.selected.get('field-error').classList.add('field__error--show-error'); 48 | this.emit('error:visible', error); 49 | }, 50 | 51 | resolveError(error) { 52 | this.selected 53 | .get('field-error') 54 | .classList.remove('field__error--show-error'); 55 | this.emit('error:resolved', error); 56 | }, 57 | 58 | getContent() { 59 | return this.content; 60 | }, 61 | 62 | setContent(content) { 63 | if (content?.mount == null) return; 64 | 65 | this.content = content; 66 | this.content.mount(this.selected.get('field-input')); 67 | this.emit('content:changed', this.content); 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /src/components/RegisterForm/components/Field/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/fonts.scss' as fonts; 3 | 4 | .field { 5 | &__label { 6 | display: block; 7 | 8 | color: colors.$gray800; 9 | 10 | font-size: fonts.$xs; 11 | font-weight: fonts.$medium; 12 | 13 | padding: 0 0 0.5rem 0.5rem; 14 | } 15 | 16 | &__error { 17 | display: none; 18 | 19 | color: colors.$error100; 20 | 21 | text-align: justify; 22 | 23 | margin: 0.8rem 0 0 0.5rem; 24 | 25 | &--show-error { 26 | display: block; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/RegisterForm/images/facebook-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/RegisterForm/images/google-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/RegisterForm/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/fonts.scss' as fonts; 3 | @use '~styles/breakpoints.scss' as breakpoints; 4 | 5 | .register-form { 6 | display: flex; 7 | flex-direction: column; 8 | 9 | align-items: start; 10 | 11 | justify-content: center; 12 | 13 | font-family: fonts.$primaryFont; 14 | 15 | margin: 0 auto; 16 | 17 | &__title { 18 | color: colors.$gray800; 19 | font-size: fonts.$lg; 20 | font-weight: fonts.$bold; 21 | 22 | margin-bottom: 3.2rem; 23 | } 24 | 25 | &__socials, 26 | &__divisor { 27 | max-width: 45rem; 28 | } 29 | 30 | &__socials { 31 | width: 100%; 32 | 33 | display: flex; 34 | flex-direction: row; 35 | gap: 2rem; 36 | } 37 | 38 | &__social { 39 | max-width: 21.7rem; 40 | 41 | display: flex; 42 | flex-direction: row; 43 | 44 | flex-grow: 1; 45 | 46 | gap: 1rem; 47 | 48 | align-items: center; 49 | 50 | justify-content: center; 51 | 52 | font-family: fonts.$fifthFont; 53 | font-size: fonts.$sm; 54 | font-weight: fonts.$medium; 55 | 56 | margin-bottom: 3.2rem; 57 | 58 | padding: 1.8rem 0; 59 | 60 | border: 0; 61 | 62 | background-color: colors.$secondary100; 63 | 64 | box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 8%); 65 | 66 | border-radius: 10px; 67 | 68 | cursor: pointer; 69 | 70 | &:hover { 71 | transform: scale(1.02); 72 | 73 | transition: all 0.4s ease-in-out; 74 | } 75 | } 76 | 77 | &__social-img { 78 | width: 2.4rem; 79 | height: 2.4rem; 80 | } 81 | 82 | &__divisor { 83 | width: 100%; 84 | 85 | display: flex; 86 | flex-direction: row; 87 | 88 | align-items: center; 89 | 90 | justify-content: center; 91 | 92 | margin-bottom: 3rem; 93 | } 94 | 95 | &__divisor-line { 96 | width: 100%; 97 | height: 1px; 98 | 99 | border: 0; 100 | 101 | background-color: colors.$gray200; 102 | } 103 | 104 | &__divisor-text { 105 | font-size: fonts.$sm; 106 | 107 | margin: 0 2.9rem; 108 | } 109 | 110 | &__form { 111 | width: 100%; 112 | } 113 | 114 | &__form-fields { 115 | display: grid; 116 | grid-template-columns: 1fr; 117 | gap: 3rem; 118 | 119 | margin-bottom: 3rem; 120 | } 121 | 122 | &__form-button { 123 | max-width: 45rem; 124 | } 125 | } 126 | 127 | @include breakpoints.from667 { 128 | .register-form { 129 | max-width: 72.5rem; 130 | 131 | &__title { 132 | font-size: fonts.$xl2; 133 | } 134 | 135 | &__form-fields { 136 | display: grid; 137 | grid-template-columns: repeat(2, 1fr); 138 | gap: 3rem; 139 | 140 | margin-bottom: 6rem; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/components/SizeSelector/images/large.js: -------------------------------------------------------------------------------- 1 | const large = ` 2 | 3 | `; 4 | 5 | export default large; 6 | -------------------------------------------------------------------------------- /src/components/SizeSelector/images/small.js: -------------------------------------------------------------------------------- 1 | const small = ` 2 | 3 | `; 4 | 5 | export default small; 6 | -------------------------------------------------------------------------------- /src/components/Sliding/index.scss: -------------------------------------------------------------------------------- 1 | .sliding { 2 | width: 100%; 3 | 4 | overflow: hidden; 5 | 6 | margin: auto; 7 | 8 | position: relative; 9 | 10 | &__content { 11 | display: flex; 12 | 13 | align-items: center; 14 | 15 | transform: translateX(0); 16 | 17 | transition: transform 0.6s ease-in-out; 18 | } 19 | 20 | &__slide { 21 | flex: 0 0 100%; 22 | } 23 | 24 | &--shuffle { 25 | .sliding__slide { 26 | width: 100%; 27 | 28 | display: none; 29 | flex: unset; 30 | 31 | position: relative; 32 | 33 | z-index: 0; 34 | 35 | transition: all 450ms ease; 36 | 37 | &--active { 38 | display: block; 39 | 40 | z-index: 2; 41 | } 42 | 43 | &--prev, 44 | &--next, 45 | &--unfocused { 46 | width: 50%; 47 | 48 | display: block; 49 | 50 | position: absolute; 51 | 52 | transform: scale(0.9); 53 | 54 | opacity: 0.6; 55 | inset: 0; 56 | } 57 | 58 | &--next { 59 | left: auto; 60 | } 61 | 62 | &--unfocused { 63 | width: 100%; 64 | 65 | transform: scale(0.94); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/components/Sliding/utils/activeSlide.js: -------------------------------------------------------------------------------- 1 | export const activeSlide = (slides, slide) => { 2 | Array.from(slides).forEach((item) => { 3 | item.classList.toggle('sliding__slide--active', item === slide); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /src/components/Sliding/utils/activeSlideShuffle.js: -------------------------------------------------------------------------------- 1 | export const activeSlideShuffle = (slides, activeIndex) => { 2 | const totalSlides = slides.length; 3 | const hasMoreThanTwoSlides = totalSlides > 2; 4 | const prevIndex = activeIndex === 0 ? totalSlides - 1 : activeIndex - 1; 5 | const nextIndex = activeIndex === totalSlides - 1 ? 0 : activeIndex + 1; 6 | 7 | Array.from(slides).forEach((slide, index) => { 8 | slide.classList.remove( 9 | 'sliding__slide--active', 10 | 'sliding__slide--unfocused', 11 | 'sliding__slide--prev', 12 | 'sliding__slide--next', 13 | ); 14 | 15 | if (index === activeIndex) slide.classList.add('sliding__slide--active'); 16 | 17 | if (index !== activeIndex && !hasMoreThanTwoSlides) 18 | slide.classList.add('sliding__slide--unfocused'); 19 | 20 | if (index === prevIndex && hasMoreThanTwoSlides) 21 | slide.classList.add('sliding__slide--prev'); 22 | 23 | if (index === nextIndex && hasMoreThanTwoSlides) 24 | slide.classList.add('sliding__slide--next'); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/TextArea/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | 4 | const events = [ 5 | 'name:change', 6 | 'placeholder:change', 7 | 'maxLength:change', 8 | 'required:change', 9 | 'error', 10 | ]; 11 | 12 | const html = ` 13 |
14 | 15 | 16 |
17 | `; 18 | 19 | export default function TextArea({ 20 | name = '', 21 | placeholder = '', 22 | maxLength, 23 | required = true, 24 | }) { 25 | Component.call(this, { html, events }); 26 | const $textarea = this.selected.get('textarea'); 27 | const $resizeTrigger = this.selected.get('resize-trigger'); 28 | 29 | this.setName(name); 30 | this.setPlaceholder(placeholder); 31 | this.setRequired(required); 32 | if (maxLength) this.setMaxLength(maxLength); 33 | 34 | function autoResize() { 35 | $resizeTrigger.innerText = $textarea.value; 36 | 37 | $textarea.style.height = 'auto'; 38 | $textarea.style.height = `${$resizeTrigger.offsetHeight}px`; 39 | } 40 | 41 | this.listen('mount', () => { 42 | $textarea.addEventListener('focus', () => 43 | $textarea.classList.remove('textarea__input--error'), 44 | ); 45 | $textarea.addEventListener('input', autoResize); 46 | window.addEventListener('resize', autoResize); 47 | }); 48 | 49 | this.listen('unmount', () => { 50 | $textarea.removeEventListener('focus', () => 51 | $textarea.classList.remove('textarea__input--error'), 52 | ); 53 | $textarea.removeEventListener('input', autoResize); 54 | window.removeEventListener('resize', autoResize); 55 | }); 56 | } 57 | 58 | TextArea.prototype = Object.assign(TextArea.prototype, Component.prototype, { 59 | setName(name = '') { 60 | this.selected.get('textarea').name = name; 61 | this.emit('name:change', name); 62 | }, 63 | setPlaceholder(placeholder = '') { 64 | this.selected.get('textarea').placeholder = placeholder; 65 | this.emit('placeholder:change', placeholder); 66 | }, 67 | setMaxLength(maxLength) { 68 | const $textArea = this.selected.get('textarea'); 69 | 70 | if (maxLength) $textArea.maxLength = maxLength; 71 | else $textArea.removeAttribute('maxlength'); 72 | 73 | this.emit('maxLength:change', maxLength ?? Infinity); 74 | }, 75 | setRequired(required = true) { 76 | this.selected.get('textarea').required = required; 77 | this.emit('required:change', required); 78 | }, 79 | error() { 80 | this.selected.get('textarea').classList.add('textarea__input--error'); 81 | this.emit('error'); 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /src/components/TextArea/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/fonts.scss' as fonts; 2 | @use '~styles/colors.scss' as colors; 3 | 4 | .textarea { 5 | position: relative; 6 | 7 | &__input, 8 | &__trigger { 9 | width: 100%; 10 | 11 | font-family: fonts.$fifthFont; 12 | font-size: fonts.$sm; 13 | line-height: 1; 14 | } 15 | 16 | &__input { 17 | overflow: hidden; 18 | 19 | padding-bottom: 1.4rem; 20 | 21 | border: unset; 22 | border-bottom: 0.1rem solid colors.$gray400; 23 | 24 | position: relative; 25 | z-index: 1; 26 | 27 | outline-color: transparent; 28 | 29 | transition: 0.2s ease-in-out outline; 30 | 31 | resize: none; 32 | 33 | &::placeholder { 34 | color: colors.$gray400; 35 | } 36 | 37 | &:focus { 38 | outline-color: colors.$primary200; 39 | } 40 | 41 | &--error { 42 | color: colors.$secondary100; 43 | 44 | background: colors.$error100; 45 | } 46 | } 47 | 48 | &__trigger { 49 | display: block; 50 | 51 | position: absolute; 52 | top: 0; 53 | 54 | visibility: hidden; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/TextInput/img/eye-icon-disable.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/TextInput/img/eye-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/TextInput/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors' as colors; 2 | @use '~styles/fonts' as fonts; 3 | 4 | .input-text-container { 5 | display: flex; 6 | 7 | border: 1px solid colors.$gray200; 8 | 9 | background-color: colors.$white; 10 | 11 | border-radius: 14px; 12 | 13 | transition: border-color 0.3s ease-in-out; 14 | 15 | &:focus-within { 16 | border-color: colors.$primary200; 17 | } 18 | 19 | &:has(> &__input.standard.input-error) { 20 | border-color: colors.$error100; 21 | 22 | outline-color: colors.$error100; 23 | } 24 | 25 | &:has(> &__input.outlined) { 26 | border-top: 0; 27 | border-right: 0; 28 | border-bottom: 1px solid colors.$gray200; 29 | border-left: 0; 30 | } 31 | 32 | &:has(> &__input.outlined:focus) { 33 | border-bottom-color: colors.$primary200; 34 | } 35 | 36 | &:has(> &__input.outlined.input-error) { 37 | border-bottom-color: colors.$error100; 38 | } 39 | 40 | &__input { 41 | width: 100%; 42 | height: 100%; 43 | 44 | flex-grow: 1; 45 | 46 | font-size: fonts.$xs; 47 | 48 | padding: 1.5rem; 49 | 50 | border: 0; 51 | 52 | box-sizing: border-box; 53 | 54 | background-color: transparent; 55 | background-repeat: no-repeat; 56 | background-size: auto 60%; 57 | filter: opacity(0.85); 58 | border-radius: 14px; 59 | 60 | &::placeholder { 61 | font-weight: fonts.$regular; 62 | } 63 | 64 | &:disabled { 65 | filter: opacity(0.55); 66 | } 67 | 68 | &.standard { 69 | padding: 1.8rem 1.6rem; 70 | 71 | &.prefix { 72 | background-position: 0.8rem center; 73 | } 74 | 75 | &.suffix { 76 | background-position: calc(100% - 0.8rem) center; 77 | } 78 | 79 | &:focus { 80 | border: 0; 81 | 82 | filter: opacity(1); 83 | 84 | outline: none; 85 | } 86 | 87 | &.input-error { 88 | color: colors.$secondary100; 89 | } 90 | } 91 | 92 | &.outlined { 93 | padding: 0.5rem 0.35rem; 94 | 95 | outline: none; 96 | 97 | transition: border-color 0.3s ease-in-out; 98 | 99 | &.input-error { 100 | filter: opacity(0.65); 101 | 102 | &::placeholder { 103 | color: colors.$error100; 104 | } 105 | } 106 | 107 | &.suffix { 108 | padding: 0.8rem 3.5rem 0.8rem 1rem; 109 | 110 | background-position: calc(100% - 0.8rem) center; 111 | } 112 | 113 | &.prefix { 114 | padding: 0.8rem 1rem 0.8rem 3.5rem; 115 | 116 | background-position: 0.8rem center; 117 | } 118 | } 119 | } 120 | 121 | &__button { 122 | display: flex; 123 | 124 | align-items: center; 125 | 126 | margin-right: 1rem; 127 | 128 | border: 0; 129 | 130 | background-color: transparent; 131 | opacity: 0.3; 132 | 133 | cursor: pointer; 134 | 135 | &--hidden { 136 | display: none; 137 | } 138 | } 139 | 140 | &__image { 141 | height: 20px; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/components/TextInput/index.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { render, screen, userEvent } from '@testing-library/vanilla'; 3 | import TextInput from '.'; 4 | 5 | const propsMock = { 6 | id: 'test', 7 | placeholder: 'Write something', 8 | assetUrl: '', 9 | assetPosition: 'prefix', 10 | variation: 'standard', 11 | value: 'Something', 12 | type: 'text', 13 | }; 14 | 15 | const renderTextInput = (parameters) => render(new TextInput(parameters)); 16 | 17 | describe('TextInput', () => { 18 | it('renders with props', () => { 19 | renderTextInput(propsMock); 20 | const textInput = screen.getByPlaceholderText(propsMock.placeholder); 21 | 22 | expect(textInput).toBeInTheDocument(); 23 | }); 24 | 25 | it('renders without props', () => { 26 | renderTextInput(); 27 | const textInput = screen.getByRole('textbox'); 28 | 29 | expect(textInput).toBeInTheDocument(); 30 | }); 31 | 32 | it('sets placeholder correctly', () => { 33 | renderTextInput({ placeholder: 'Write something' }); 34 | const textInput = screen.getByPlaceholderText('Write something'); 35 | 36 | expect(textInput.placeholder).toBe('Write something'); 37 | }); 38 | 39 | it('sets value correctly', () => { 40 | renderTextInput({ value: 'Something' }); 41 | const textInput = screen.getByRole('textbox'); 42 | 43 | expect(textInput.value).toBe('Something'); 44 | }); 45 | 46 | it('changes input value', async () => { 47 | renderTextInput(); 48 | const textInput = screen.getByRole('textbox'); 49 | 50 | await userEvent.type(textInput, 'New value'); 51 | 52 | expect(textInput.value).toBe('New value'); 53 | }); 54 | 55 | it('toggles password visibility', async () => { 56 | renderTextInput({ type: 'password', placeholder: 'Write something' }); 57 | const textInput = screen.getByPlaceholderText('Write something'); 58 | const toggleButton = screen.getByRole('button'); 59 | 60 | expect(textInput.type).toBe('password'); 61 | 62 | await userEvent.click(toggleButton); 63 | expect(textInput.type).toBe('text'); 64 | 65 | await userEvent.click(toggleButton); 66 | expect(textInput.type).toBe('password'); 67 | }); 68 | 69 | it('disables and enables input correctly', () => { 70 | const textInput = renderTextInput(); 71 | const element = screen.getByRole('textbox'); 72 | 73 | expect(element).toBeEnabled(); 74 | 75 | textInput.disable(); 76 | expect(element).toBeDisabled(); 77 | 78 | textInput.enable(); 79 | expect(element).toBeEnabled(); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/components/Toggle/index.js: -------------------------------------------------------------------------------- 1 | import { Component, createIDFactory } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | 4 | const generateID = createIDFactory('toggle'); 5 | 6 | const events = ['toggle']; 7 | 8 | const html = ` 9 |
10 | 11 | 12 |
13 | `; 14 | 15 | export default function Toggle({ checked = false } = {}) { 16 | Component.call(this, { html, events }); 17 | 18 | const id = generateID(); 19 | this.selected.get('toggle-input').setAttribute('id', id); 20 | this.selected.get('toggle-label').setAttribute('for', id); 21 | 22 | this.setToggle(checked, false); 23 | 24 | this.selected 25 | .get('toggle-input') 26 | .addEventListener('change', () => this.emitToggle()); 27 | } 28 | 29 | Toggle.prototype = Object.assign(Toggle.prototype, Component.prototype, { 30 | emitToggle() { 31 | this.emit('toggle', this.selected.get('toggle-input').checked); 32 | }, 33 | setToggle(checked, emit = true) { 34 | this.selected.get('toggle-input').checked = checked; 35 | if (emit) this.emitToggle(); 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /src/components/Toggle/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | 3 | .toggle-container { 4 | display: inline-block; 5 | 6 | position: relative; 7 | 8 | &__input { 9 | width: 0; 10 | height: 0; 11 | 12 | position: absolute; 13 | 14 | opacity: 0; 15 | 16 | &:checked + .toggle-container__label { 17 | background-color: colors.$primary200; 18 | 19 | &::after { 20 | transform: translateX(100%); 21 | } 22 | } 23 | } 24 | 25 | &__label { 26 | min-width: 4.4rem; 27 | min-height: 2.4rem; 28 | 29 | display: flex; 30 | 31 | align-items: center; 32 | 33 | background-color: colors.$gray100; 34 | border-radius: 3.4rem; 35 | 36 | transition: background-color 0.3s; 37 | 38 | cursor: pointer; 39 | 40 | &::after { 41 | width: 41%; 42 | height: 75%; 43 | 44 | position: absolute; 45 | left: 10%; 46 | 47 | background-color: colors.$secondary100; 48 | border-radius: 50%; 49 | 50 | transition: transform 0.3s; 51 | 52 | content: ''; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/UploadImage/img/photo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/UploadImage/img/placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/UploadImage/img/plus-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/UploadImage/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | import placeholderImage from './img/placeholder.svg'; 4 | import plusIcon from './img/plus-icon.svg'; 5 | import photoIcon from './img/photo-icon.svg'; 6 | 7 | const events = ['value:change']; 8 | 9 | const html = ` 10 |
11 |
12 |
13 |
14 |
15 | 25 |
26 | `; 27 | 28 | export default function UploadImage() { 29 | Component.call(this, { html, events }); 30 | 31 | const imagePreview = this.selected.get('image-preview'); 32 | const buttonIcon = this.selected.get('button-icon'); 33 | const uploadInput = this.selected.get('upload-input'); 34 | 35 | this.reader = new FileReader(); 36 | 37 | const readAndDisplayImage = (file) => { 38 | this.reader.addEventListener('load', (e) => { 39 | const readerTarget = e.target; 40 | imagePreview.src = readerTarget.result; 41 | imagePreview.classList.remove('hidden'); 42 | buttonIcon.src = photoIcon; 43 | this.emit('value:change', readerTarget.result); 44 | }); 45 | 46 | this.reader.readAsDataURL(file); 47 | }; 48 | 49 | const handleInputChange = (e) => { 50 | const inputTarget = e.target; 51 | const file = inputTarget.files[0]; 52 | 53 | if (file) readAndDisplayImage(file); 54 | }; 55 | 56 | uploadInput.addEventListener('change', handleInputChange); 57 | } 58 | 59 | UploadImage.prototype = Object.assign( 60 | UploadImage.prototype, 61 | Component.prototype, 62 | { 63 | getImage() { 64 | return this.reader.result; 65 | }, 66 | }, 67 | ); 68 | -------------------------------------------------------------------------------- /src/components/UploadImage/index.scss: -------------------------------------------------------------------------------- 1 | .container-upload-image { 2 | position: relative; 3 | aspect-ratio: 1 / 1; 4 | 5 | &__label { 6 | cursor: pointer; 7 | } 8 | 9 | &__input { 10 | display: none; 11 | } 12 | 13 | &__image-container { 14 | min-width: 10rem; 15 | height: 100%; 16 | min-height: 10rem; 17 | 18 | overflow: hidden; 19 | 20 | display: flex; 21 | 22 | align-items: center; 23 | justify-content: center; 24 | 25 | position: relative; 26 | 27 | background-color: rgb(0, 52, 89); 28 | border-radius: 50%; 29 | } 30 | 31 | &__placeholder-image { 32 | max-width: 50%; 33 | 34 | position: absolute; 35 | 36 | border-radius: 50%; 37 | } 38 | 39 | &__preview-image { 40 | max-width: 100%; 41 | 42 | position: absolute; 43 | inset: 0; 44 | 45 | object-fit: cover; 46 | 47 | &.hidden { 48 | display: none; 49 | } 50 | } 51 | 52 | &__button { 53 | width: 20%; 54 | height: 20%; 55 | 56 | display: flex; 57 | 58 | align-items: center; 59 | justify-content: center; 60 | 61 | border: 1px solid rgb(244, 248, 251); 62 | 63 | position: absolute; 64 | top: 85%; 65 | left: 60%; 66 | 67 | background: rgb(255, 255, 255); 68 | box-shadow: 0 1px 4px 0 rgba(139, 158, 184, 0.2); 69 | border-radius: 10%; 70 | } 71 | 72 | &__animation { 73 | position: absolute; 74 | inset: 0; 75 | } 76 | 77 | &__circle, 78 | &__circle-duplicate { 79 | width: 100%; 80 | height: 100%; 81 | 82 | border: 2px solid rgb(236, 239, 242); 83 | 84 | position: absolute; 85 | top: 50%; 86 | left: 50%; 87 | 88 | transform: translate(-50%, -50%); 89 | border-radius: 50%; 90 | 91 | animation: radar-pulse 6s infinite; 92 | } 93 | 94 | &__circle-duplicate { 95 | animation-delay: 2s; 96 | } 97 | } 98 | 99 | @keyframes radar-pulse { 100 | 0%, 101 | 100% { 102 | transform: translate(-50%, -50%) scale(1); 103 | opacity: 1; 104 | } 105 | 106 | 25% { 107 | transform: translate(-50%, -50%) scale(1.2); 108 | opacity: 1; 109 | } 110 | 111 | 50% { 112 | transform: translate(-50%, -50%) scale(1.5); 113 | opacity: 0.3; 114 | } 115 | 116 | 75% { 117 | transform: translate(-50%, -50%); 118 | opacity: 0; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/components/UploadImage/index.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { render, screen, userEvent, waitFor } from '@testing-library/vanilla'; 3 | import UploadImage from '.'; 4 | import plusIcon from './img/plus-icon.svg'; 5 | import photoIcon from './img/photo-icon.svg'; 6 | 7 | const renderUploadImage = () => render(new UploadImage()); 8 | 9 | describe('UploadImage', () => { 10 | it('renders correctly', () => { 11 | renderUploadImage(); 12 | const uploadInput = screen.getByLabelText('Carregar imagem'); 13 | 14 | expect(uploadInput).toBeInTheDocument(); 15 | }); 16 | 17 | describe('Upload input', () => { 18 | it('file loaded', async () => { 19 | renderUploadImage(); 20 | const user = userEvent.setup(); 21 | 22 | const file = new File(['hello'], 'hello.png', { type: 'image/png' }); 23 | const uploadInput = screen.getByLabelText('Carregar imagem'); 24 | 25 | await user.upload(uploadInput, file); 26 | 27 | expect(uploadInput.files[0]).toBe(file); 28 | }); 29 | }); 30 | 31 | describe('Button icon', () => { 32 | it('renders with the correct initial icon', async () => { 33 | renderUploadImage(); 34 | const buttonIcon = screen.getByAltText('Botão com ícone'); 35 | 36 | expect(buttonIcon).toHaveAttribute('src', plusIcon); 37 | }); 38 | 39 | it('changes icon after file upload', async () => { 40 | renderUploadImage(); 41 | const user = userEvent.setup(); 42 | 43 | const uploadInput = screen.getByLabelText('Carregar imagem'); 44 | const buttonIcon = screen.getByAltText('Botão com ícone'); 45 | const file = new File(['hello'], 'hello.png', { type: 'image/png' }); 46 | 47 | await user.upload(uploadInput, file); 48 | 49 | await waitFor(() => { 50 | expect(buttonIcon).toHaveAttribute('src', photoIcon); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('Image preview', () => { 56 | it('does not display preview if no file is selected', () => { 57 | renderUploadImage(); 58 | const imagePreview = screen.getByAltText('Imagem carregada'); 59 | 60 | expect(imagePreview).toHaveClass('hidden'); 61 | }); 62 | 63 | it('display preview if file is selected', async () => { 64 | renderUploadImage(); 65 | const user = userEvent.setup(); 66 | 67 | const imagePreview = screen.getByAltText('Imagem carregada'); 68 | const uploadInput = screen.getByLabelText('Carregar imagem'); 69 | const file = new File(['hello'], 'hello.png', { type: 'image/png' }); 70 | 71 | await user.upload(uploadInput, file); 72 | await waitFor(() => { 73 | expect(imagePreview).toBeInTheDocument(); 74 | }); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/components/Vaccine/images/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Vaccine/images/vaccine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/Vaccine/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/fonts.scss' as fonts; 3 | @use '~styles/breakpoints.scss' as breakpoints; 4 | 5 | .vaccine { 6 | display: flex; 7 | flex-direction: column; 8 | 9 | gap: 2rem; 10 | 11 | font-family: fonts.$primaryFont; 12 | 13 | padding: 2rem; 14 | border: 1px solid colors.$gray150; 15 | 16 | box-shadow: 0 0 2px 2px rgba(50, 50, 71, 0.04); 17 | 18 | border-radius: 1.8rem; 19 | 20 | &__header { 21 | display: flex; 22 | 23 | justify-content: space-between; 24 | } 25 | 26 | &__header-title { 27 | display: flex; 28 | 29 | gap: 2rem; 30 | 31 | align-items: center; 32 | } 33 | 34 | &__add-vacine { 35 | display: flex; 36 | 37 | align-items: center; 38 | } 39 | 40 | &__add-vacine { 41 | display: flex; 42 | gap: 1rem; 43 | 44 | font-family: fonts.$tertiaryFont; 45 | 46 | color: colors.$primary200; 47 | font-size: fonts.$xs; 48 | font-weight: fonts.$bold; 49 | line-height: 2.1; 50 | 51 | cursor: pointer; 52 | } 53 | 54 | &__add-vaccine-img { 55 | max-width: 1.2rem; 56 | } 57 | 58 | &__img { 59 | min-width: 6rem; 60 | min-height: 6rem; 61 | } 62 | 63 | &__add-vacine-text { 64 | padding-right: 0.9rem; 65 | } 66 | 67 | &__text { 68 | font-family: fonts.$primaryFont; 69 | color: colors.$gray800; 70 | font-size: fonts.$xs; 71 | font-weight: fonts.$semiBold; 72 | 73 | line-height: 2; 74 | } 75 | 76 | &__add-vacine-text { 77 | display: none; 78 | } 79 | 80 | &__sections { 81 | margin-right: 2rem; 82 | } 83 | } 84 | 85 | @include breakpoints.from667 { 86 | .vaccine { 87 | &__text { 88 | color: colors.$gray800; 89 | font-size: fonts.$sm; 90 | font-size: 1.8rem; 91 | font-weight: fonts.$semiBold; 92 | line-height: 2.4; 93 | } 94 | 95 | &__add-vacine-text { 96 | display: block; 97 | } 98 | } 99 | } 100 | 101 | @container (min-height: 790px) { 102 | @include breakpoints.from667 { 103 | .vaccine { 104 | overflow: auto; 105 | 106 | &__scroll { 107 | overflow: auto; 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/components/VaccineGroup/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import VaccineItem from '../VaccineItem'; 3 | import './index.scss'; 4 | 5 | const events = ['item:change', 'title:change']; 6 | 7 | const html = ` 8 |
9 |

10 |
11 | `; 12 | 13 | export default function VaccineGroup({ year, vaccine } = {}) { 14 | Component.call(this, { html, events }); 15 | 16 | this.items = new Map(); 17 | if (year) this.setTitle(year); 18 | if (vaccine) this.setItem(vaccine); 19 | } 20 | 21 | VaccineGroup.prototype = Object.assign( 22 | VaccineGroup.prototype, 23 | Component.prototype, 24 | { 25 | getTitle() { 26 | return this.selected.get('title').textContent; 27 | }, 28 | getItem(itemId) { 29 | return this.items.get(itemId); 30 | }, 31 | setTitle(year) { 32 | this.selected.get('title').textContent = year; 33 | this.emit('title:change', year); 34 | }, 35 | setItem(item) { 36 | const $vaccineGroup = this.selected.get('group'); 37 | const vaccineItem = new VaccineItem(item); 38 | 39 | vaccineItem.mount($vaccineGroup); 40 | this.items.set(item.id, vaccineItem); 41 | this.emit('item:change', vaccineItem); 42 | }, 43 | listItems() { 44 | const items = []; 45 | 46 | Array.from(this.items.values()).forEach((item) => { 47 | items.push({ 48 | id: item.id, 49 | title: item.getTitle(), 50 | veterinary: item.getVeterinary(), 51 | date: item.getDate(), 52 | }); 53 | }); 54 | 55 | return items; 56 | }, 57 | }, 58 | ); 59 | -------------------------------------------------------------------------------- /src/components/VaccineGroup/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/fonts.scss' as fonts; 3 | @use '~styles/breakpoints.scss' as breakpoints; 4 | 5 | .vaccine-group { 6 | display: flex; 7 | flex-direction: column; 8 | gap: 2rem; 9 | 10 | &__vaccine-title { 11 | color: colors.$gray800; 12 | font-size: 1.3rem; 13 | font-weight: fonts.$semiBold; 14 | line-height: 2.4; 15 | } 16 | } 17 | 18 | @include breakpoints.from667 { 19 | .vaccine-group { 20 | &__vaccine-title { 21 | font-size: 1.8rem; 22 | font-weight: fonts.$semiBold; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/VaccineItem/images/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/VaccineItem/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import dayjs from 'dayjs'; 3 | import calendarUrl from './images/calendar.svg'; 4 | import './index.scss'; 5 | 6 | const events = ['date:change', 'title:change', 'vet:change', 'id:change']; 7 | 8 | const html = ` 9 |
10 |

11 |
12 |
13 | calendar 14 |

15 |
16 |
17 |

18 |
19 | `; 20 | 21 | function formatDate(date) { 22 | return date ? dayjs(date).format('MM/DD/YYYY') : ''; 23 | } 24 | 25 | export default function VaccineItem({ 26 | id = '', 27 | title = '', 28 | veterinary = '', 29 | date = '', 30 | } = {}) { 31 | Component.call(this, { html, events }); 32 | 33 | this.setId(id); 34 | this.setTitle(title); 35 | this.setVeterinary(veterinary); 36 | this.setDate(date); 37 | } 38 | 39 | VaccineItem.prototype = Object.assign( 40 | VaccineItem.prototype, 41 | Component.prototype, 42 | { 43 | getId() { 44 | return this.id; 45 | }, 46 | getTitle() { 47 | return this.selected.get('title').textContent; 48 | }, 49 | getVeterinary() { 50 | return this.selected.get('veterinary').textContent; 51 | }, 52 | getDate() { 53 | return this.selected.get('date').textContent; 54 | }, 55 | setId(id) { 56 | this.id = id; 57 | this.emit('id:change', id); 58 | }, 59 | setTitle(title) { 60 | this.selected.get('title').textContent = title; 61 | this.emit('title:change', title); 62 | }, 63 | setVeterinary(veterinary) { 64 | this.selected.get('veterinary').textContent = veterinary; 65 | this.emit('vet:change', veterinary); 66 | }, 67 | setDate(date) { 68 | const dateFormatted = formatDate(date); 69 | this.selected.get('date').textContent = dateFormatted; 70 | this.emit('date:change', date); 71 | }, 72 | }, 73 | ); 74 | -------------------------------------------------------------------------------- /src/components/VaccineItem/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/fonts.scss' as fonts; 3 | @use '~styles/breakpoints.scss' as breakpoints; 4 | 5 | .vaccine-item { 6 | display: flex; 7 | gap: 1.2rem; 8 | 9 | align-items: center; 10 | justify-content: space-between; 11 | 12 | color: colors.$gray600; 13 | font-size: 1.2rem; 14 | font-weight: fonts.$medium; 15 | line-height: 2.4; 16 | 17 | padding: 2rem; 18 | 19 | border: 1px solid colors.$gray150; 20 | 21 | border-radius: 14px; 22 | 23 | &__title { 24 | color: colors.$gray800; 25 | font-weight: fonts.$semiBold; 26 | } 27 | 28 | &__date { 29 | display: flex; 30 | gap: 0.8rem; 31 | 32 | align-items: center; 33 | } 34 | 35 | &__divider { 36 | width: 1px; 37 | 38 | align-self: stretch; 39 | 40 | border: 0; 41 | 42 | background: colors.$gray150; 43 | } 44 | } 45 | 46 | @include breakpoints.from667 { 47 | .vaccine-item { 48 | font-size: 1.6rem; 49 | 50 | padding: 1.5rem; 51 | 52 | &__title { 53 | font-weight: fonts.$semiBold; 54 | } 55 | 56 | &__date { 57 | margin-right: 0; 58 | margin-left: auto; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/layouts/components/Navigation/images/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/layouts/components/Navigation/images/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/layouts/components/Navigation/images/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/layouts/components/Navigation/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import './index.scss'; 3 | 4 | import petUrl from '../../../images/pet-dex.svg'; 5 | import avatarUrl from './images/avatar.svg'; 6 | import bellUrl from './images/bell.svg'; 7 | import exitUrl from './images/exit.svg'; 8 | import menuUrl from './images/menu.svg'; 9 | 10 | const html = ` 11 | 30 | `; 31 | 32 | export default function Navigation() { 33 | Component.call(this, { html }); 34 | } 35 | 36 | Navigation.prototype = Object.assign(Navigation.prototype, Component.prototype); 37 | -------------------------------------------------------------------------------- /src/layouts/components/Navigation/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/breakpoints.scss' as breakpoints; 2 | 3 | .navigation { 4 | width: 100%; 5 | 6 | display: flex; 7 | 8 | align-items: center; 9 | justify-content: space-between; 10 | 11 | padding-inline: 2rem; 12 | 13 | &__icons { 14 | display: flex; 15 | 16 | gap: 2.4rem; 17 | } 18 | 19 | &__icon-container { 20 | width: 4rem; 21 | height: 4rem; 22 | 23 | display: grid; 24 | 25 | place-items: center; 26 | 27 | &--menu { 28 | display: grid; 29 | } 30 | 31 | &--avatar, 32 | &--bell, 33 | &--exit { 34 | display: none; 35 | } 36 | } 37 | } 38 | 39 | @include breakpoints.from1024 { 40 | .navigation { 41 | &__icon-container { 42 | &--menu { 43 | display: none; 44 | } 45 | 46 | &--avatar, 47 | &--bell, 48 | &--exit { 49 | display: grid; 50 | } 51 | } 52 | 53 | &__logo { 54 | display: none; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/layouts/components/SideMenu/images/configuracoes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/layouts/components/SideMenu/images/conta.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/layouts/components/SideMenu/images/doacoes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/layouts/components/SideMenu/images/exit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/layouts/components/SideMenu/images/exitmenu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/layouts/components/SideMenu/images/meuspets.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/layouts/components/SideMenu/images/notifications.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/layouts/components/SideMenu/images/petdex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/layouts/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | Pet Hat 14 | 15 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 77 | 83 | 84 | 85 | 86 |
87 | 88 | 89 |
94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import { extractElements } from 'pet-dex-utilities'; 2 | import mainRouter from '../router/main-router'; 3 | import { initializeSwiper } from '../utils/swiper'; 4 | import Navigation from './components/Navigation'; 5 | import SideMenu from './components/SideMenu'; 6 | import './index.scss'; 7 | import initializeScrollable from './utils/scrollable-sidemenu'; 8 | 9 | document.addEventListener('DOMContentLoaded', () => { 10 | const selected = extractElements([document.body]); 11 | 12 | const $home = selected.get('home'); 13 | 14 | const $sidemenu = selected.get('sidemenu'); 15 | const sideMenu = new SideMenu(); 16 | sideMenu.mount($sidemenu); 17 | 18 | const $navigation = selected.get('navigation'); 19 | const navigation = new Navigation(); 20 | navigation.mount($navigation); 21 | 22 | const $hamburgerMenu = navigation.selected.get('hamburger-menu'); 23 | const $exitMenu = sideMenu.selected.get('exitMenu'); 24 | const $itemsMenu = sideMenu.selected.get('menuitens').querySelectorAll('li'); 25 | 26 | initializeScrollable($hamburgerMenu, $exitMenu, $itemsMenu, $home); 27 | initializeSwiper(); 28 | mainRouter(); 29 | }); 30 | -------------------------------------------------------------------------------- /src/layouts/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/base.scss'; 2 | @use '~styles/colors.scss' as colors; 3 | @use '~styles/breakpoints.scss' as breakpoints; 4 | @use '~styles/fonts.scss' as fonts; 5 | 6 | .home { 7 | width: 200vw; 8 | height: 100dvh; 9 | overflow-x: hidden; 10 | 11 | display: grid; 12 | grid-template-areas: 13 | 'sidemenu nav' 14 | 'sidemenu content'; 15 | grid-template-rows: 8.1rem 1fr; 16 | grid-template-columns: 100vw 100vw; 17 | 18 | background-color: colors.$primary600; 19 | transform: translateX(-100vw); 20 | 21 | &--open-menu { 22 | width: 100vw; 23 | 24 | transform: translateX(0); 25 | 26 | transition: transform 0.3s; 27 | } 28 | 29 | &--exit-menu { 30 | width: 200vw; 31 | 32 | transform: translateX(-100vw); 33 | 34 | transition: transform 0.3s; 35 | } 36 | 37 | &__navigation { 38 | display: flex; 39 | grid-area: nav; 40 | 41 | align-items: center; 42 | } 43 | 44 | &__sidemenu { 45 | display: grid; 46 | grid-area: sidemenu; 47 | grid-template-rows: 8.1rem 1fr; 48 | } 49 | 50 | &__content { 51 | overflow: auto; 52 | 53 | grid-area: content; 54 | 55 | background-color: colors.$secondary100; 56 | } 57 | 58 | &__content-page { 59 | width: 100%; 60 | height: 100%; 61 | } 62 | } 63 | 64 | @include breakpoints.from1024 { 65 | .home { 66 | width: 100vw; 67 | 68 | grid-template-areas: 69 | 'sidemenu nav' 70 | 'sidemenu content'; 71 | grid-template-rows: 8.1rem 1fr; 72 | grid-template-columns: 31.3rem 1fr; 73 | 74 | transform: translateX(0); 75 | 76 | &__content { 77 | margin-inline: 2.4rem; 78 | 79 | margin-bottom: 2.4rem; 80 | 81 | border-radius: 1.6rem; 82 | } 83 | 84 | &__sidemenu { 85 | display: block; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/layouts/pages/NoPetRegirested/images/no-pet-regirested-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/layouts/pages/NoPetRegirested/images/no-pet-regirested-page.png -------------------------------------------------------------------------------- /src/layouts/pages/NoPetRegirested/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import { Router } from 'vanilla-routing'; 3 | import Button from '~src/components/Button'; 4 | import petUrl from './images/no-pet-regirested-page.png'; 5 | import './index.scss'; 6 | 7 | const html = ` 8 |
9 |
10 |
11 |

Você ainda não tem nenhum pet cadastrado

12 |

Crie o perfil do seu pet e deixe o nosso site com o focinho do seu filhote!

13 |
14 | dog in an smart phone 15 |
16 |
; 17 | `; 18 | 19 | export default function NoPetRegirested() { 20 | Component.call(this, { html }); 21 | 22 | const $container = this.selected.get('container'); 23 | 24 | this.button = new Button({ 25 | text: 'Cadastrar pet', 26 | isFullWidth: true, 27 | isDisabled: false, 28 | }); 29 | 30 | this.button.selected 31 | .get('button') 32 | .classList.add('no-pet-regirested-page__button'); 33 | this.button.mount($container); 34 | 35 | this.button.listen('click', () => { 36 | Router.go('/add/addpets'); 37 | }); 38 | } 39 | 40 | NoPetRegirested.prototype = Object.assign( 41 | NoPetRegirested.prototype, 42 | Component.prototype, 43 | ); 44 | -------------------------------------------------------------------------------- /src/layouts/pages/NoPetRegirested/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/breakpoints.scss' as breakpoints; 3 | 4 | .no-pet-regirested-page { 5 | min-height: 100%; 6 | 7 | display: grid; 8 | 9 | place-items: center; 10 | 11 | &__description { 12 | max-width: 33.5rem; 13 | 14 | font-family: 'Montserrat', sans-serif; 15 | } 16 | 17 | &__title { 18 | color: colors.$gray800; 19 | font-size: 2.4rem; 20 | font-weight: 700; 21 | line-height: 1.25; 22 | } 23 | 24 | &__hint { 25 | color: colors.$gray600; 26 | font-size: 1.6rem; 27 | font-weight: 500; 28 | line-height: 1.875; 29 | 30 | margin-top: 1.8rem; 31 | } 32 | 33 | &__image { 34 | max-width: 100%; 35 | 36 | margin-top: 2.4rem; 37 | } 38 | 39 | &__button { 40 | max-width: 42rem; 41 | 42 | margin-top: 3.2rem; 43 | } 44 | 45 | &__content { 46 | text-align: center; 47 | } 48 | } 49 | 50 | @include breakpoints.from1024 { 51 | .no-pet-regirested-page { 52 | &__description { 53 | max-width: 46rem; 54 | } 55 | 56 | &__title { 57 | font-size: 3.4rem; 58 | } 59 | 60 | &__hint { 61 | font-size: 1.8rem; 62 | 63 | margin-top: 2rem; 64 | } 65 | 66 | &__image { 67 | margin-top: 0; 68 | } 69 | 70 | &__button { 71 | margin-top: 4rem; 72 | } 73 | 74 | &__content { 75 | display: flex; 76 | 77 | align-items: center; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/layouts/pages/PetName/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import TextInput from '../../../components/TextInput'; 3 | import UploadImage from '../../../components/UploadImage'; 4 | import Button from '../../../components/Button'; 5 | import './index.scss'; 6 | 7 | const events = ['submit']; 8 | 9 | const html = ` 10 |
11 |
12 |
13 |

Qual o nome do seu bichinho?

14 |
15 |
16 | 19 |
20 | `; 21 | 22 | export default function PetName() { 23 | Component.call(this, { html, events }); 24 | 25 | const $inputContainer = this.selected.get('input-container'); 26 | const $uploadImage = this.selected.get('upload-image-container'); 27 | const $buttonContainer = this.selected.get('button-container'); 28 | 29 | this.input = new TextInput({ 30 | placeholder: 'Nome do Pet', 31 | }); 32 | 33 | this.upload = new UploadImage(); 34 | this.button = new Button({ 35 | text: 'Continuar', 36 | isFullWidth: true, 37 | isDisabled: false, 38 | }); 39 | 40 | const updateButtonVisibility = () => { 41 | const input = this.input.getValue(); 42 | const image = this.upload.getImage(); 43 | 44 | this.button.setIsDisabled(!(input && image)); 45 | }; 46 | updateButtonVisibility(); 47 | 48 | this.upload.listen('value:change', updateButtonVisibility); 49 | this.input.listen('value:change', updateButtonVisibility); 50 | 51 | this.button.listen('click', () => { 52 | const image = this.upload.getImage(); 53 | const name = this.input.getValue(); 54 | this.emit('submit', { image, name }); 55 | }); 56 | 57 | this.upload.mount($uploadImage); 58 | this.input.mount($inputContainer); 59 | this.button.mount($buttonContainer); 60 | } 61 | 62 | PetName.prototype = Object.assign(PetName.prototype, Component.prototype); 63 | -------------------------------------------------------------------------------- /src/layouts/pages/PetName/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/breakpoints.scss' as breakpoints; 2 | @use '~styles/colors.scss' as colors; 3 | @use '~styles/fonts.scss' as fonts; 4 | 5 | .pet-name { 6 | display: flex; 7 | flex-direction: column; 8 | 9 | &__container { 10 | width: 100%; 11 | 12 | margin-bottom: 2.4rem; 13 | } 14 | 15 | &__image { 16 | max-width: 18.6rem; 17 | 18 | margin: 5rem auto; 19 | } 20 | 21 | &__title { 22 | font-family: fonts.$primaryFont; 23 | color: colors.$gray800; 24 | text-align: center; 25 | font-size: fonts.$xs; 26 | font-weight: fonts.$medium; 27 | 28 | margin: 7rem auto 3rem; 29 | } 30 | 31 | &__input { 32 | width: min(100%, 42rem); 33 | 34 | margin: 0 auto; 35 | } 36 | 37 | &__footer { 38 | width: 100%; 39 | 40 | display: flex; 41 | 42 | justify-content: center; 43 | 44 | margin-top: auto; 45 | } 46 | 47 | &__button { 48 | width: min(100%, 42rem); 49 | } 50 | } 51 | 52 | @include breakpoints.from1024 { 53 | .pet-name { 54 | align-items: center; 55 | 56 | &__container { 57 | margin-bottom: 0; 58 | } 59 | 60 | &__title { 61 | font-size: fonts.$sm; 62 | 63 | margin-top: 15rem; 64 | } 65 | 66 | &__button { 67 | margin-top: 4rem; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/layouts/pages/PetRegister/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import Button from '~src/components/Button'; 3 | import PetCard from '~src/components/PetCard'; 4 | import './index.scss'; 5 | 6 | const events = ['select:card', 'submit']; 7 | 8 | const html = ` 9 |
10 |
11 | 12 |
13 | `; 14 | 15 | export default function PetRegister({ cards = [] } = {}) { 16 | Component.call(this, { html, events }); 17 | 18 | const $container = this.selected.get('container'); 19 | const $footerContainer = this.selected.get('footer-container'); 20 | this.activeCard = null; 21 | 22 | const $button = new Button({ 23 | text: 'Continuar', 24 | isFullWidth: false, 25 | isDisabled: true, 26 | }); 27 | 28 | cards.forEach((data) => { 29 | const card = new PetCard(data); 30 | 31 | card.selected.get('pet-container').classList.add('pet-card'); 32 | card.selected.get('pet-container').classList.toggle('pet-card--active'); 33 | card.mount($container); 34 | 35 | card.listen('active', () => { 36 | if (this.activeCard) this.activeCard.deactivate(); 37 | 38 | this.activeCard = card; 39 | this.breedSelected = this.activeCard.getTitle(); 40 | 41 | this.emit('select:card', card); 42 | $button.enable(); 43 | }); 44 | 45 | card.listen('deactive', () => { 46 | $button.disable(); 47 | this.activeCard = null; 48 | }); 49 | }); 50 | 51 | $button.listen('click', () => { 52 | this.emit('submit', { breedSelected: this.breedSelected }); 53 | }); 54 | 55 | $button.selected.get('button').classList.add('breed-page__button'); 56 | $button.mount($footerContainer); 57 | } 58 | 59 | PetRegister.prototype = Object.assign( 60 | PetRegister.prototype, 61 | Component.prototype, 62 | ); 63 | -------------------------------------------------------------------------------- /src/layouts/pages/PetRegister/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/breakpoints.scss' as breakpoints; 3 | 4 | .breed-page { 5 | height: 100%; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | 10 | &__breed-grid { 11 | overflow: auto; 12 | 13 | display: flex; 14 | flex-wrap: wrap; 15 | gap: 1.6rem; 16 | 17 | align-items: start; 18 | justify-content: center; 19 | 20 | padding: 1rem; 21 | 22 | .pet-card { 23 | aspect-ratio: auto; 24 | 25 | cursor: pointer; 26 | } 27 | } 28 | 29 | &__footer { 30 | width: 100%; 31 | 32 | display: flex; 33 | 34 | justify-content: center; 35 | 36 | margin-top: auto; 37 | } 38 | 39 | &__button { 40 | width: min(100%, 42rem); 41 | } 42 | } 43 | 44 | @include breakpoints.from1024 { 45 | .breed-page { 46 | &__breed-grid { 47 | display: flex; 48 | 49 | align-items: center; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/layouts/pages/PetSize/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | import Button from '~src/components/Button'; 3 | import UploadImage from '~src/components/UploadImage'; 4 | import SizeSelector from '~src/components/SizeSelector'; 5 | import './index.scss'; 6 | 7 | const events = ['submit']; 8 | 9 | const html = ` 10 |
11 |
12 |
13 | 14 |
15 |

Qual é o tamanho do seu animal de estimação?

16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 |
25 |
; 26 | `; 27 | 28 | export default function PetSize() { 29 | Component.call(this, { html, events }); 30 | this.container = this.selected.get('container'); 31 | 32 | this.imageContainer = this.selected.get('image-container'); 33 | this.sizeselectorContainer = this.selected.get('size-selector'); 34 | this.buttonComponent = this.selected.get('button'); 35 | this.uploadImage = new UploadImage(); 36 | this.sizeselector = new SizeSelector(); 37 | this.button = new Button({ 38 | text: 'Continuar', 39 | isFullWidth: true, 40 | isDisabled: false, 41 | }); 42 | 43 | this.renderComponents(); 44 | this.getSize(); 45 | this.button.listen('click', () => { 46 | this.emit('submit', { 47 | sizeTitle: this.sizeTitle, 48 | weightRange: this.weightRange, 49 | sizeIndex: this.sizeIndex, 50 | }); 51 | }); 52 | } 53 | 54 | PetSize.prototype = Object.assign(PetSize.prototype, Component.prototype, { 55 | renderComponents() { 56 | this.uploadImage.mount(this.imageContainer); 57 | this.sizeselector.mount(this.sizeselectorContainer); 58 | this.button.mount(this.buttonComponent); 59 | }, 60 | getSize() { 61 | this.sizeTitle = this.sizeselector 62 | .activeCardInit() 63 | .card.querySelector('.container-size-selector__title').textContent; 64 | this.weightRange = this.sizeselector 65 | .activeCardInit() 66 | .card.querySelector('.container-size-selector__text').textContent; 67 | this.sizeIndex = this.sizeselector.activeCardInit().index; 68 | 69 | this.sizeselector.listen('size:change', (card, index) => { 70 | this.sizeTitle = card.querySelector( 71 | '.container-size-selector__title', 72 | ).textContent; 73 | this.weightRange = card.querySelector( 74 | '.container-size-selector__text', 75 | ).textContent; 76 | this.sizeIndex = index; 77 | }); 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /src/layouts/pages/PetSize/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/breakpoints.scss' as breakpoints; 3 | @use '~styles/fonts.scss' as fonts; 4 | 5 | .pet-size-page { 6 | width: 100%; 7 | min-height: 100%; 8 | 9 | display: grid; 10 | grid-template-rows: 1fr auto; 11 | 12 | &__content { 13 | display: flex; 14 | flex-direction: column; 15 | gap: 4rem; 16 | 17 | align-items: center; 18 | justify-content: center; 19 | 20 | text-align: center; 21 | } 22 | 23 | &__description { 24 | display: flex; 25 | flex-direction: column; 26 | gap: 0.1rem; 27 | 28 | font-family: fonts.$primaryFont; 29 | } 30 | 31 | &__title { 32 | color: colors.$gray800; 33 | font-size: fonts.$xs; 34 | font-weight: fonts.$medium; 35 | } 36 | 37 | &__hint { 38 | color: colors.$gray800; 39 | font-size: 1.1rem; 40 | font-weight: fonts.$regular; 41 | 42 | margin-top: 1.6rem; 43 | } 44 | 45 | &__size-selector { 46 | min-height: 28rem; 47 | } 48 | } 49 | 50 | .pet-size-page__button-container { 51 | display: flex; 52 | 53 | justify-content: center; 54 | 55 | padding: 4rem 0; 56 | 57 | box-shadow: 0 0 0.8rem 0 rgba(73, 77, 90, 0.12); 58 | border-radius: 1rem 1rem 0 0; 59 | } 60 | 61 | .pet-size-page__button { 62 | width: 100%; 63 | max-width: 42rem; 64 | 65 | padding: 0 2rem; 66 | } 67 | 68 | @include breakpoints.from1024 { 69 | .pet-size-page { 70 | &__button-container { 71 | box-shadow: none; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/layouts/pages/PetVet/images/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/layouts/pages/PetVet/images/estetoscopio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/layouts/pages/PetWeight/index.scss: -------------------------------------------------------------------------------- 1 | @use '~styles/colors.scss' as colors; 2 | @use '~styles/breakpoints.scss' as breakpoints; 3 | @use '~styles/fonts.scss' as fonts; 4 | 5 | .pet-weight-page { 6 | min-height: 100%; 7 | 8 | display: grid; 9 | 10 | place-items: center; 11 | 12 | &__content { 13 | width: 100%; 14 | 15 | text-align: center; 16 | } 17 | 18 | &__image-container { 19 | width: max-content; 20 | 21 | display: inline-block; 22 | 23 | margin-bottom: 4rem; 24 | } 25 | 26 | &__description { 27 | font-family: fonts.$primaryFont; 28 | } 29 | 30 | &__title { 31 | color: colors.$gray800; 32 | font-size: fonts.$xs; 33 | font-weight: fonts.$medium; 34 | } 35 | 36 | &__hint { 37 | color: colors.$gray800; 38 | font-size: 1.1rem; 39 | font-weight: fonts.$regular; 40 | 41 | margin-top: 1.6rem; 42 | } 43 | 44 | &__slider-container { 45 | max-width: 100%; 46 | 47 | margin-top: 3rem; 48 | } 49 | 50 | &__value { 51 | margin-bottom: 0; 52 | } 53 | 54 | &__inputs { 55 | display: flex; 56 | 57 | justify-content: center; 58 | } 59 | 60 | &__input { 61 | display: none; 62 | } 63 | 64 | &__radio { 65 | margin-right: 1rem; 66 | 67 | &:last-child { 68 | margin-right: 0; 69 | } 70 | } 71 | 72 | &__footer { 73 | width: 100%; 74 | 75 | display: flex; 76 | 77 | justify-content: center; 78 | 79 | margin-top: auto; 80 | } 81 | 82 | &__button { 83 | width: min(100%, 42rem); 84 | } 85 | } 86 | 87 | @include breakpoints.from1024 { 88 | .pet-weight-page { 89 | align-items: flex-start; 90 | 91 | &__image-container { 92 | margin-top: 3rem; 93 | margin-bottom: 3rem; 94 | } 95 | 96 | &__slider-container { 97 | margin-top: 2rem; 98 | } 99 | 100 | &__title { 101 | font-size: fonts.$sm; 102 | font-weight: fonts.$medium; 103 | } 104 | 105 | &__hint { 106 | font-size: fonts.$xs; 107 | } 108 | 109 | &__inputs { 110 | margin-top: 3rem; 111 | margin-bottom: 2rem; 112 | } 113 | 114 | &__input-container { 115 | margin-right: 1rem; 116 | } 117 | 118 | &__input { 119 | display: block; 120 | } 121 | 122 | &__radio { 123 | margin-right: 1rem; 124 | } 125 | } 126 | } 127 | 128 | @include breakpoints.from1280 { 129 | .pet-weight-page { 130 | &__image-container { 131 | margin-top: 4rem; 132 | margin-bottom: 4rem; 133 | } 134 | 135 | &__title { 136 | font-size: fonts.$md; 137 | font-weight: fonts.$semiBold; 138 | } 139 | 140 | &__hint { 141 | font-size: fonts.$sm; 142 | } 143 | 144 | &__input-container { 145 | width: 20%; 146 | 147 | margin-right: 2rem; 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/layouts/pages/PetWeight/petWeightPage.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { render, screen, userEvent, waitFor } from '@testing-library/vanilla'; 3 | import PetWeightPage from '.'; 4 | 5 | const propsMock = { 6 | petPhoto: 'https://via.placeholder.com/150', 7 | }; 8 | 9 | const makeComponent = (params) => render(new PetWeightPage(params)); 10 | 11 | describe('Pet Weight page', () => { 12 | it('renders image', async () => { 13 | const page = makeComponent(propsMock.petPhoto); 14 | 15 | render(page); 16 | const image = screen.getByAltText('Imagem carregada'); 17 | 18 | expect(image).toBeInTheDocument(); 19 | }); 20 | 21 | it('KG radio is checked by default', () => { 22 | const page = makeComponent(propsMock.petPhoto); 23 | render(page); 24 | 25 | const radioKG = screen.getByLabelText('KG'); 26 | 27 | expect(radioKG.checked).toBe(true); 28 | }); 29 | 30 | it('selects radio buttons when clicked and desselects the other', async () => { 31 | const page = makeComponent(propsMock.petPhoto); 32 | render(page); 33 | 34 | const radioButtonKG = screen.getByLabelText('KG'); 35 | const radioButtonLB = screen.getByLabelText('LB'); 36 | 37 | await userEvent.click(radioButtonKG); 38 | expect(radioButtonKG).toBeChecked(); 39 | expect(radioButtonLB).not.toBeChecked(); 40 | 41 | await userEvent.click(radioButtonLB); 42 | expect(radioButtonLB).toBeChecked(); 43 | expect(radioButtonKG).not.toBeChecked(); 44 | }); 45 | 46 | it('allows typing in the input field', async () => { 47 | const page = makeComponent(propsMock.petPhoto); 48 | render(page); 49 | 50 | const input = screen.getByPlaceholderText('Peso'); 51 | 52 | await userEvent.type(input, '5'); 53 | 54 | expect(input).toHaveValue('5'); 55 | }); 56 | 57 | it('shows right value in slider when value changes in the input field', async () => { 58 | const page = makeComponent(propsMock.petPhoto); 59 | render(page); 60 | 61 | const input = screen.getByPlaceholderText('Peso'); 62 | const slider = screen.getByText('10.0'); 63 | 64 | await userEvent.clear(input); 65 | await userEvent.type(input, '5'); 66 | 67 | await waitFor(() => { 68 | expect(slider).toHaveTextContent('5.0'); 69 | }); 70 | }); 71 | 72 | it('emits data when continue button is clicked', async () => { 73 | const page = makeComponent(propsMock.petPhoto); 74 | render(page); 75 | 76 | const input = screen.getByPlaceholderText('Peso'); 77 | const radioKG = screen.getByLabelText('KG'); 78 | const continueButton = screen.getByRole('button', { name: 'Continuar' }); 79 | 80 | await userEvent.clear(input); 81 | await userEvent.type(input, '5'); 82 | await userEvent.click(radioKG); 83 | 84 | const mockEmit = vi.spyOn(page, 'emit'); 85 | 86 | await userEvent.click(continueButton); 87 | 88 | expect(mockEmit).toHaveBeenCalledWith('submit', { 89 | weight: 5.0, 90 | weightUnit: 'kg', 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/layouts/sample-page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | Hello world 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/layouts/utils/scrollable-sidemenu.js: -------------------------------------------------------------------------------- 1 | export default function initializeScrollable( 2 | hamburger, 3 | exitmenu, 4 | itemsMenu, 5 | home, 6 | ) { 7 | const breakpointDesktop = 1024; 8 | 9 | function openMenu() { 10 | home.classList.remove('home--exit-menu'); 11 | home.classList.add('home--open-menu'); 12 | } 13 | 14 | function closeMenu() { 15 | home.classList.remove('home--open-menu'); 16 | home.classList.add('home--exit-menu'); 17 | } 18 | 19 | if (window.innerWidth < breakpointDesktop) { 20 | hamburger.addEventListener('click', openMenu); 21 | exitmenu.addEventListener('click', closeMenu); 22 | } 23 | 24 | function activeClassMenu(selectedItem) { 25 | itemsMenu.forEach((li) => { 26 | li.classList.remove('side-menu-content__menuitens--active'); 27 | }); 28 | 29 | selectedItem.classList.add('side-menu-content__menuitens--active'); 30 | 31 | if (window.innerWidth < breakpointDesktop) closeMenu(); 32 | } 33 | 34 | itemsMenu.forEach((li) => { 35 | li.addEventListener('click', activeClassMenu.bind(null, li)); 36 | }); 37 | 38 | return { 39 | openMenu, 40 | closeMenu, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/public/favicon/android-icon-192x192.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/android-icon-192x192.ico -------------------------------------------------------------------------------- /src/public/favicon/apple-touch-icon-114x114.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/apple-touch-icon-114x114.ico -------------------------------------------------------------------------------- /src/public/favicon/apple-touch-icon-120x120.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/apple-touch-icon-120x120.ico -------------------------------------------------------------------------------- /src/public/favicon/apple-touch-icon-144x144.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/apple-touch-icon-144x144.ico -------------------------------------------------------------------------------- /src/public/favicon/apple-touch-icon-152x152.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/apple-touch-icon-152x152.ico -------------------------------------------------------------------------------- /src/public/favicon/apple-touch-icon-180x180.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/apple-touch-icon-180x180.ico -------------------------------------------------------------------------------- /src/public/favicon/apple-touch-icon-57x57.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/apple-touch-icon-57x57.ico -------------------------------------------------------------------------------- /src/public/favicon/apple-touch-icon-60x60.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/apple-touch-icon-60x60.ico -------------------------------------------------------------------------------- /src/public/favicon/apple-touch-icon-72x72.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/apple-touch-icon-72x72.ico -------------------------------------------------------------------------------- /src/public/favicon/apple-touch-icon-76x76.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/apple-touch-icon-76x76.ico -------------------------------------------------------------------------------- /src/public/favicon/apple-touch-icon-96x96.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/apple-touch-icon-96x96.ico -------------------------------------------------------------------------------- /src/public/favicon/favicon-16x16.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/favicon-16x16.ico -------------------------------------------------------------------------------- /src/public/favicon/favicon-32x32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/favicon/favicon-32x32.ico -------------------------------------------------------------------------------- /src/public/helvetica/Helvetica.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/helvetica/Helvetica.eot -------------------------------------------------------------------------------- /src/public/helvetica/Helvetica.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/helvetica/Helvetica.otf -------------------------------------------------------------------------------- /src/public/helvetica/Helvetica.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/helvetica/Helvetica.ttf -------------------------------------------------------------------------------- /src/public/helvetica/Helvetica.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/helvetica/Helvetica.woff -------------------------------------------------------------------------------- /src/public/helvetica/Helvetica.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/public/helvetica/Helvetica.woff2 -------------------------------------------------------------------------------- /src/router/main-router.js: -------------------------------------------------------------------------------- 1 | import { BrowserRoute } from 'vanilla-routing'; 2 | import addPet from './routes/app/add-pet/add-pet'; 3 | import mainRoutes from './routes/app/main-routes/main-routes'; 4 | import myPets from './routes/app/my-pets/my-pets'; 5 | import account from './routes/create-account/account'; // 6 | 7 | export default function mainRouter() { 8 | const routes = [ 9 | ...Object.values(mainRoutes), 10 | ...Object.values(myPets), 11 | ...Object.values(addPet), 12 | ...Object.values(account), // 13 | ]; 14 | 15 | BrowserRoute(routes); 16 | } 17 | -------------------------------------------------------------------------------- /src/router/routes/app/add-pet/add-pet.js: -------------------------------------------------------------------------------- 1 | import birthday from './steps/birthday'; 2 | import register from './steps/register'; 3 | import name from './steps/name'; 4 | import weight from './steps/weight'; 5 | import petvet from './steps/petvet'; 6 | import race from './steps/race'; 7 | import size from './steps/size'; 8 | 9 | export default { 10 | register, 11 | race, 12 | name, 13 | weight, 14 | size, 15 | birthday, 16 | petvet, 17 | }; 18 | -------------------------------------------------------------------------------- /src/router/routes/app/add-pet/steps/birthday.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/pet-birthday', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'brown'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/add-pet/steps/name.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/pet-name', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'yellow'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/add-pet/steps/petvet.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/pet-vet', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'red'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/add-pet/steps/race.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/pet-race', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'black'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/add-pet/steps/register.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/pet-register', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'blue'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/add-pet/steps/size.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/pet-size', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'purple'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/add-pet/steps/weight.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/pet-weight', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'orange'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/main-routes/main-routes.js: -------------------------------------------------------------------------------- 1 | import settings from './routes/settings'; 2 | import account from './routes/account'; 3 | import donates from './routes/donates'; 4 | import error from './routes/error'; 5 | import home from './routes/home'; 6 | import petDex from './routes/pet-dex'; 7 | 8 | export default { home, petDex, donates, account, settings, error }; 9 | -------------------------------------------------------------------------------- /src/router/routes/app/main-routes/routes/account.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/account', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'red'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/main-routes/routes/donates.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/donates', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'brown'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/main-routes/routes/error.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '*', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | const noPetRegirestedPage = new NoPetRegirestedPage(); 9 | noPetRegirestedPage.elements[0].textContent = 'ERRO 404!'; 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/main-routes/routes/home.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | const noPetRegirestedPage = new NoPetRegirestedPage(); 9 | noPetRegirestedPage.mount($content); 10 | return $content; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/router/routes/app/main-routes/routes/pet-dex.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/pet-dex', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'red'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/main-routes/routes/settings.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/settings', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'orange'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/my-pets/my-pets.js: -------------------------------------------------------------------------------- 1 | import pets from './steps/pets'; 2 | import petProfile from './steps/pet-profile'; 3 | 4 | export default { 5 | pets, 6 | petProfile, 7 | }; 8 | -------------------------------------------------------------------------------- /src/router/routes/app/my-pets/steps/pet-profile.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/pet-profile', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'blue'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/app/my-pets/steps/pets.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/pets', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'red'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/router/routes/create-account/account.js: -------------------------------------------------------------------------------- 1 | import createAccount from './create-account'; 2 | 3 | export default { 4 | createAccount, 5 | }; 6 | -------------------------------------------------------------------------------- /src/router/routes/create-account/create-account.js: -------------------------------------------------------------------------------- 1 | import NoPetRegirestedPage from '~src/layouts/pages/NoPetRegirested'; 2 | 3 | export default { 4 | pathname: '/account/create-account', 5 | element: () => { 6 | const $content = document.createElement('div'); 7 | $content.classList.add('home__content-page'); 8 | $content.style.backgroundColor = 'orange'; 9 | const noPetRegirestedPage = new NoPetRegirestedPage(); 10 | noPetRegirestedPage.mount($content); 11 | return $content; 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | export const url = 'http://localhost:3000'; 2 | -------------------------------------------------------------------------------- /src/services/breeds.js: -------------------------------------------------------------------------------- 1 | import { url } from './api'; 2 | 3 | export const BreedsService = { 4 | list: async () => { 5 | try { 6 | const response = await fetch(`${url}/breed`); 7 | 8 | if (!response.ok) { 9 | throw new Error({ 10 | status: response.status, 11 | message: response.statusText, 12 | }); 13 | } 14 | return response.json(); 15 | } catch (error) { 16 | console.error(`An error occurred: ${error}`); 17 | return error; 18 | } 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /src/services/userService.js: -------------------------------------------------------------------------------- 1 | import { url } from './api'; 2 | 3 | export const UserService = { 4 | getPets: async (userId) => { 5 | try { 6 | const response = await fetch(`${url}/api/user/${userId}/my-pets`); 7 | 8 | if (!response.ok) { 9 | throw new Error('Failed request'); 10 | } 11 | 12 | const { pets } = await response.json(); 13 | return pets; 14 | } catch (error) { 15 | console.error(`An error occurred: ${error}`); 16 | return `An error occurred: ${error}`; 17 | } 18 | }, 19 | 20 | login: async (email, password) => { 21 | try { 22 | const response = await fetch(`${url}/api/user/login`, { 23 | method: 'POST', 24 | body: JSON.stringify({ email, password }), 25 | }); 26 | 27 | if (!response.ok) throw new Error('Failed to login'); 28 | 29 | const { token } = await response.json(); 30 | return { 31 | success: true, 32 | token, 33 | }; 34 | } catch (error) { 35 | console.error(`An error occurred: ${error}`); 36 | return { 37 | success: false, 38 | message: error.message, 39 | }; 40 | } 41 | }, 42 | 43 | registerUser: async (userData) => { 44 | try { 45 | const response = await fetch(`${url}/naoSeiARota`, { 46 | method: 'POST', 47 | headers: { 48 | 'Content-Type': 'application/json', 49 | }, 50 | body: JSON.stringify(userData), 51 | }); 52 | 53 | if (!response.ok) { 54 | throw new Error('Failed to register user'); 55 | } 56 | 57 | const result = await response.json(); 58 | return { 59 | success: true, 60 | data: result, 61 | }; 62 | } catch (error) { 63 | console.error(`An error occurred: ${error}`); 64 | return { 65 | success: false, 66 | message: error.message, 67 | }; 68 | } 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /src/stories/AddPet.stories.js: -------------------------------------------------------------------------------- 1 | import AddPet from '../components/AddPet'; 2 | 3 | export default { 4 | title: 'Components/AddPet', 5 | 6 | render: (args = {}) => { 7 | const $container = document.createElement('div'); 8 | const component = new AddPet(args); 9 | component.mount($container); 10 | return $container; 11 | }, 12 | }; 13 | 14 | export const Default = {}; 15 | -------------------------------------------------------------------------------- /src/stories/AvatarButton.stories.js: -------------------------------------------------------------------------------- 1 | import AvatarButton from '../components/AvatarButton'; 2 | 3 | // Configuração dos stories 4 | export default { 5 | title: 'Components/AvatarButton', 6 | 7 | // Note que podemos receber argumentos 8 | render: (args) => { 9 | const $container = document.createElement('div'); 10 | const component = new AvatarButton(args); 11 | component.mount($container); 12 | return $container; 13 | }, 14 | }; 15 | 16 | // Aqui criamos um story, passando os parametros que será injetado pela configuração 17 | export const Default = {}; 18 | -------------------------------------------------------------------------------- /src/stories/Button.stories.js: -------------------------------------------------------------------------------- 1 | import Button from '../components/Button'; 2 | 3 | export default { 4 | title: 'Components/Button', 5 | render: (args) => { 6 | const button = new Button(args); 7 | const $container = document.createElement('div'); 8 | button.mount($container); 9 | 10 | return $container; 11 | }, 12 | argTypes: { 13 | text: { control: 'text', default: '' }, 14 | isFullWidth: { control: 'boolean', default: false }, 15 | isDisabled: { control: 'boolean', default: false }, 16 | }, 17 | }; 18 | 19 | export const Default = { 20 | args: { 21 | text: 'Button', 22 | }, 23 | }; 24 | 25 | export const Full = { 26 | args: { 27 | ...Default.args, 28 | isFullWidth: true, 29 | }, 30 | }; 31 | 32 | export const Disabled = { 33 | args: { 34 | ...Default.args, 35 | isDisabled: true, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/stories/ChangePassword.stories.js: -------------------------------------------------------------------------------- 1 | import ChangePassword from '../components/ChangePassword'; 2 | import { initializeSwiper } from '../utils/swiper'; 3 | import Drawer from '../components/Drawer'; 4 | import Button from '../components/Button'; 5 | 6 | export default { 7 | title: 'Components/ChangePassword', 8 | render: (args) => { 9 | initializeSwiper(); 10 | const button = new Button({ 11 | text: 'Abrir change password', 12 | isFullWidth: true, 13 | isDisabled: false, 14 | }); 15 | 16 | const changePassword = new ChangePassword(args); 17 | const drawer = new Drawer({ 18 | title: 'Alterar Senha', 19 | content: changePassword, 20 | }); 21 | 22 | const $container = document.createElement('div'); 23 | button.mount($container); 24 | 25 | button.listen('click', () => { 26 | drawer.open(); 27 | }); 28 | 29 | return $container; 30 | }, 31 | }; 32 | 33 | export const Default = {}; 34 | -------------------------------------------------------------------------------- /src/stories/Checkbox.stories.js: -------------------------------------------------------------------------------- 1 | import Checkbox from '../components/Checkbox'; 2 | 3 | export default { 4 | title: 'Components/Checkbox', 5 | render: (args) => { 6 | const button = new Checkbox(args); 7 | const $container = document.createElement('div'); 8 | button.mount($container); 9 | 10 | return $container; 11 | }, 12 | argTypes: { 13 | value: { control: 'number', default: 1 }, 14 | text: { control: 'text', default: '' }, 15 | check: { control: 'boolean', default: false }, 16 | disabled: { control: 'boolean', default: false }, 17 | name: { control: 'text', default: '' }, 18 | }, 19 | }; 20 | 21 | export const Default = { 22 | args: { 23 | value: 1, 24 | text: 'Checkbox example', 25 | check: false, 26 | disabled: false, 27 | name: 'Example', 28 | }, 29 | }; 30 | 31 | export const Disabled = { 32 | args: { 33 | ...Default.args, 34 | disabled: true, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/stories/Drawer.stories.js: -------------------------------------------------------------------------------- 1 | import Button from '../components/Button'; 2 | import Drawer from '../components/Drawer'; 3 | import { initializeSwiper } from '../utils/swiper'; 4 | 5 | export default { 6 | title: 'Components/Drawer', 7 | render: (args) => { 8 | initializeSwiper(); 9 | const drawer = new Drawer(args); 10 | const button = new Button({ 11 | text: 'Abrir drawer', 12 | isFullWidth: true, 13 | isDisabled: false, 14 | }); 15 | const $container = document.createElement('div'); 16 | button.mount($container); 17 | 18 | button.listen('click', () => { 19 | drawer.open(); 20 | }); 21 | 22 | return $container; 23 | }, 24 | argTypes: { 25 | title: { control: 'text', description: 'Title of the drawer' }, 26 | content: { control: null, description: 'Component inside drawer' }, 27 | }, 28 | }; 29 | 30 | export const Default = { 31 | args: { 32 | title: 'Add dates', 33 | content: new Button({ 34 | text: 'Cadastrar pet', 35 | isFullWidth: true, 36 | isDisabled: false, 37 | }), 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/stories/Dropdown.stories.js: -------------------------------------------------------------------------------- 1 | import Dropdown from '../components/Dropdown'; 2 | 3 | const mockItems = [ 4 | { 5 | text: 'Raiva', 6 | value: 'raiva', 7 | }, 8 | { 9 | text: 'Soninho', 10 | value: 'soninho', 11 | }, 12 | { 13 | text: 'Castração', 14 | value: 'castracao', 15 | }, 16 | ]; 17 | 18 | export default { 19 | title: 'Components/Dropdown', 20 | render: (args) => { 21 | const dropdown = new Dropdown(args); 22 | const $container = document.createElement('div'); 23 | dropdown.mount($container); 24 | 25 | return $container; 26 | }, 27 | argsTyes: { 28 | items: { control: 'object', defaultValue: [] }, 29 | placeholder: { control: 'text', defaultValue: '' }, 30 | }, 31 | }; 32 | 33 | export const Default = { 34 | args: { 35 | items: [], 36 | placeholder: '', 37 | }, 38 | }; 39 | 40 | export const Fill = { 41 | args: { 42 | items: mockItems, 43 | placeholder: 'Selecione', 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/stories/LoginForm.stories.js: -------------------------------------------------------------------------------- 1 | import LoginForm from '../components/LoginForm'; 2 | 3 | export default { 4 | title: 'Components/LoginForm', 5 | parameters: { 6 | backgrounds: { 7 | default: 'petdex', 8 | values: [ 9 | { 10 | name: 'default', 11 | value: '#F8F8F8', 12 | }, 13 | { 14 | name: 'petdex', 15 | value: '#003459', 16 | }, 17 | ], 18 | }, 19 | }, 20 | render: () => { 21 | const loginForm = new LoginForm(); 22 | const $container = document.createElement('div'); 23 | loginForm.mount($container); 24 | 25 | return $container; 26 | }, 27 | }; 28 | 29 | export const Default = {}; 30 | -------------------------------------------------------------------------------- /src/stories/PetAvatar.stories.js: -------------------------------------------------------------------------------- 1 | import PetAvatar from '../components/PetAvatar'; 2 | 3 | import akita from './assets/petRegisterPage/akita.svg'; 4 | 5 | export default { 6 | title: 'Components/PetAvatar', 7 | render: (args) => { 8 | const $container = document.createElement('div'); 9 | $container.style.width = '60px'; 10 | $container.style.height = '90px'; 11 | $container.style.overflow = 'visible'; 12 | const petAvatar = new PetAvatar(args); 13 | petAvatar.mount($container); 14 | 15 | return $container; 16 | }, 17 | argTypes: { 18 | id: { control: 'text', description: 'Pet id' }, 19 | title: { control: 'text', description: 'Pet name' }, 20 | imgSrc: { control: 'text', description: 'url source for a image pet' }, 21 | imgAlt: { control: 'text', description: 'Pet name alt description' }, 22 | }, 23 | }; 24 | 25 | export const Default = { 26 | args: { 27 | id: '1', 28 | title: 'Carlos', 29 | imgSrc: akita, 30 | imgAlt: 'breed alt description', 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/stories/PetCard.stories.js: -------------------------------------------------------------------------------- 1 | import PetCard from '../components/PetCard'; 2 | 3 | import akita from './assets/petRegisterPage/akita.svg'; 4 | 5 | export default { 6 | title: 'Components/PetCard', 7 | render: (args) => { 8 | const $container = document.createElement('div'); 9 | const card = new PetCard(args); 10 | card.mount($container); 11 | 12 | return $container; 13 | }, 14 | argTypes: { 15 | title: { control: 'text', description: 'Breed name pet' }, 16 | imgAlt: { control: 'text', description: 'breed alt description' }, 17 | imgSrc: { control: 'text', description: 'url source for a image pet' }, 18 | }, 19 | }; 20 | 21 | export const Default = { 22 | args: { 23 | title: 'akita', 24 | imgAlt: 'breed alt description', 25 | imgSrc: akita, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/stories/PetName.stories.js: -------------------------------------------------------------------------------- 1 | import PetRegister from '~layouts/pages/PetName'; 2 | 3 | export default { 4 | title: 'Pages/PetName', 5 | 6 | render: (args = {}) => { 7 | const $container = document.createElement('div'); 8 | const component = new PetRegister(args); 9 | component.mount($container); 10 | return $container; 11 | }, 12 | }; 13 | 14 | export const Default = {}; 15 | -------------------------------------------------------------------------------- /src/stories/PetRegister.stories.js: -------------------------------------------------------------------------------- 1 | import PetRegister from '~layouts/pages/PetRegister'; 2 | 3 | export default { 4 | title: 'Pages/PetProfile', 5 | 6 | render: (args = {}) => { 7 | const $container = document.createElement('div'); 8 | const component = new PetRegister(args); 9 | component.mount($container); 10 | return $container; 11 | }, 12 | }; 13 | 14 | export const Default = {}; 15 | -------------------------------------------------------------------------------- /src/stories/PetRegisterPage.stories.js: -------------------------------------------------------------------------------- 1 | import PetRegisterPage from '~layouts/pages/PetRegister'; 2 | 3 | import afghanHound from './assets/petRegisterPage/afghanHound.svg'; 4 | import akita from './assets/petRegisterPage/akita.svg'; 5 | import beagle from './assets/petRegisterPage/beagle.svg'; 6 | import bichonFrise from './assets/petRegisterPage/bichonFrise.svg'; 7 | import borderCollie from './assets/petRegisterPage/borderCollie.svg'; 8 | import boxer from './assets/petRegisterPage/boxer.svg'; 9 | import chowChow from './assets/petRegisterPage/chowChow.svg'; 10 | import mixedBreed from './assets/petRegisterPage/mixedBreed.svg'; 11 | 12 | const cards = [ 13 | { 14 | title: 'Akita', 15 | imgSrc: akita, 16 | imgAlt: 'akita', 17 | }, 18 | { 19 | title: 'Boxer', 20 | imgSrc: boxer, 21 | imgAlt: 'boxer', 22 | }, 23 | { 24 | title: 'Akita', 25 | imgSrc: akita, 26 | imgAlt: 'akita', 27 | }, 28 | { 29 | title: 'Boxer', 30 | imgSrc: boxer, 31 | imgAlt: 'boxer', 32 | }, 33 | { 34 | title: 'Beagle', 35 | imgSrc: beagle, 36 | imgAlt: 'beagle', 37 | }, 38 | { 39 | title: 'Afghan Hound', 40 | imgSrc: afghanHound, 41 | imgAlt: 'afghan hound', 42 | }, 43 | { 44 | title: 'Bichon Frise', 45 | imgSrc: bichonFrise, 46 | imgAlt: 'bichon frise', 47 | }, 48 | { 49 | title: 'Chow Chow', 50 | imgSrc: chowChow, 51 | imgAlt: 'chow chow', 52 | }, 53 | { 54 | title: 'Border Collie', 55 | imgSrc: borderCollie, 56 | imgAlt: 'border collie', 57 | }, 58 | { 59 | title: 'Mixed Breed', 60 | imgSrc: mixedBreed, 61 | imgAlt: 'mixed breed', 62 | }, 63 | ]; 64 | 65 | export default { 66 | title: 'Pages/PetRegister', 67 | render: (args = {}) => { 68 | const card = new PetRegisterPage(args); 69 | const $container = document.createElement('div'); 70 | $container.style.height = '610px'; 71 | card.mount($container); 72 | 73 | return $container; 74 | }, 75 | }; 76 | 77 | export const Default = { 78 | args: { 79 | cards, 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /src/stories/PetSizePage.stories.js: -------------------------------------------------------------------------------- 1 | import PetSize from '~layouts/pages/PetSize'; 2 | 3 | export default { 4 | title: 'Pages/PetSizePage', 5 | render: () => { 6 | const petSizePage = new PetSize(); 7 | const $container = document.createElement('div'); 8 | petSizePage.mount($container); 9 | 10 | return $container; 11 | }, 12 | component: PetSize, 13 | }; 14 | 15 | export const PetSizePageStory = {}; 16 | -------------------------------------------------------------------------------- /src/stories/PetVetPage.stories.js: -------------------------------------------------------------------------------- 1 | import PetVetPage from '~layouts/pages/PetVet'; 2 | 3 | export default { 4 | title: 'Pages/PetVetPage', 5 | render: (args) => { 6 | const petVetPage = new PetVetPage(args); 7 | const $container = document.createElement('div'); 8 | 9 | $container.style.height = '826px'; 10 | $container.style.containerType = 'size'; 11 | $container.style.overflow = 'auto'; 12 | 13 | petVetPage.mount($container); 14 | 15 | return $container; 16 | }, 17 | args: { 18 | vaccines: [ 19 | { 20 | id: '1', 21 | veterinary: 'Dr octopus', 22 | title: 'Antirrábica', 23 | date: new Date().toISOString(), 24 | }, 25 | { 26 | id: '2', 27 | veterinary: 'Dr Felipa', 28 | title: 'Raiva', 29 | date: new Date(2023, 5, 2).toISOString(), 30 | }, 31 | { 32 | id: '3', 33 | veterinary: 'Dr octopus', 34 | title: 'Raiva', 35 | date: new Date(2023, 2, 2).toISOString(), 36 | }, 37 | ], 38 | }, 39 | component: PetVetPage, 40 | }; 41 | 42 | export const PetVetPageStory = {}; 43 | -------------------------------------------------------------------------------- /src/stories/PetWeightPage.stories.js: -------------------------------------------------------------------------------- 1 | import PetWeightPage from '~layouts/pages/PetWeight'; 2 | 3 | export default { 4 | title: 'Pages/PetWeightPage', 5 | render: (args) => { 6 | const petWeightPage = new PetWeightPage(args); 7 | const $container = document.createElement('div'); 8 | petWeightPage.mount($container); 9 | 10 | return $container; 11 | }, 12 | args: { 13 | petPhoto: { control: 'string', default: 'https://via.placeholder.com/150' }, 14 | }, 15 | component: PetWeightPage, 16 | }; 17 | 18 | export const PetWeightPageStory = {}; 19 | -------------------------------------------------------------------------------- /src/stories/RadioButton.stories.js: -------------------------------------------------------------------------------- 1 | import RadioButton from '../components/RadioButton'; 2 | 3 | export default { 4 | title: 'Components/RadioButton', 5 | render: (args) => { 6 | const button = new RadioButton(args); 7 | const $container = document.createElement('div'); 8 | button.mount($container); 9 | 10 | return $container; 11 | }, 12 | argTypes: { 13 | value: { control: 'number', default: 1 }, 14 | text: { control: 'text', default: '' }, 15 | check: { control: 'boolean', default: false }, 16 | name: { control: 'text', default: '' }, 17 | }, 18 | }; 19 | 20 | export const Default = { 21 | args: { 22 | value: 1, 23 | text: 'Radio Button Example', 24 | check: false, 25 | name: 'Example', 26 | }, 27 | }; 28 | 29 | export const Disabled = { 30 | args: { 31 | ...Default.args, 32 | disabled: true, 33 | }, 34 | }; 35 | 36 | export const Borderless = { 37 | args: { 38 | ...Default.args, 39 | borderless: true, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/stories/RangeSlider.stories.js: -------------------------------------------------------------------------------- 1 | import RangeSlider from '../components/RangeSlider'; 2 | 3 | export default { 4 | title: 'Components/RangeSlider', 5 | render: (args) => { 6 | const rangeSlider = new RangeSlider(args); 7 | const $container = document.createElement('div'); 8 | rangeSlider.mount($container); 9 | 10 | return $container; 11 | }, 12 | argTypes: { 13 | minimum: { control: 'number', default: 0 }, 14 | maximum: { control: 'number', default: 100 }, 15 | unit: { 16 | control: { 17 | type: 'text', 18 | options: ['kg', 'lb'], 19 | }, 20 | default: 'kg', 21 | }, 22 | value: { control: 'number', default: 10 }, 23 | stepSize: { control: 'number', default: 0.05 }, 24 | }, 25 | }; 26 | 27 | export const Default = { 28 | args: {}, 29 | }; 30 | 31 | export const WithLbUnit = { 32 | args: { 33 | ...Default.args, 34 | unit: 'lb', 35 | value: 20, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/stories/RegisterForm.stories.js: -------------------------------------------------------------------------------- 1 | import RegisterForm from '../components/RegisterForm'; 2 | 3 | export default { 4 | title: 'Components/RegisterForm', 5 | render: () => { 6 | const registerForm = new RegisterForm(); 7 | const $container = document.createElement('div'); 8 | registerForm.mount($container); 9 | 10 | return $container; 11 | }, 12 | argTypes: {}, 13 | }; 14 | 15 | export const Default = {}; 16 | -------------------------------------------------------------------------------- /src/stories/SizeSelector.stories.js: -------------------------------------------------------------------------------- 1 | import SizeSelector from '../components/SizeSelector'; 2 | 3 | export default { 4 | title: 'Components/SizeSelector', 5 | render: () => { 6 | const sizeselector = new SizeSelector(); 7 | 8 | const $container = document.createElement('div'); 9 | $container.style.width = '100%'; 10 | $container.style.display = 'flex'; 11 | $container.style.justifyContent = 'center'; 12 | sizeselector.mount($container); 13 | 14 | return $container; 15 | }, 16 | }; 17 | 18 | export const Default = {}; 19 | -------------------------------------------------------------------------------- /src/stories/Sliding.stories.js: -------------------------------------------------------------------------------- 1 | import { initializeSwiper } from '../utils/swiper'; 2 | import Sliding from '../components/Sliding'; 3 | import Button from '../components/Button'; 4 | 5 | const $slide1 = document.createElement('div'); 6 | $slide1.style.height = '200px'; 7 | $slide1.style.backgroundColor = 'red'; 8 | 9 | const $slide2 = document.createElement('div'); 10 | $slide2.style.height = '200px'; 11 | $slide2.style.backgroundColor = 'pink'; 12 | 13 | const $slide3 = document.createElement('div'); 14 | $slide3.style.height = '200px'; 15 | $slide3.style.backgroundColor = 'green'; 16 | 17 | const button4 = new Button({ 18 | text: '<', 19 | isFullWidth: false, 20 | }); 21 | 22 | const button5 = new Button({ 23 | text: '>', 24 | isFullWidth: false, 25 | }); 26 | 27 | export default { 28 | title: 'Components/Sliding', 29 | render: (args) => { 30 | const sliding = new Sliding(args); 31 | const $container = document.createElement('div'); 32 | initializeSwiper(); 33 | window.sliding = sliding; 34 | sliding.mount($container); 35 | button4.mount($container); 36 | button5.mount($container); 37 | 38 | button4.listen('click', () => sliding.previous()); 39 | button5.listen('click', () => sliding.next()); 40 | 41 | return $container; 42 | }, 43 | argsTypes: { 44 | slides: { control: 'object', defaultValue: [] }, 45 | slideSideSpacing: { control: 'number', defaultValue: 0 }, 46 | shuffleMode: { control: 'boolean', defaultValue: false }, 47 | }, 48 | }; 49 | 50 | export const Default = { 51 | args: { 52 | slides: [$slide1, $slide2, $slide3], 53 | slideSideSpacing: 60, 54 | shuffleMode: true, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /src/stories/Tabber.stories.js: -------------------------------------------------------------------------------- 1 | import Button from '../components/Button'; 2 | import Tabber from '../components/Tabber'; 3 | import iconBirthday from './assets/tabber/birthday.svg'; 4 | import iconHome from './assets/tabber/home.svg'; 5 | 6 | export default { 7 | title: 'Components/Tabber', 8 | render: (args) => { 9 | const tabber = new Tabber(args); 10 | const $container = document.createElement('div'); 11 | tabber.mount($container); 12 | 13 | return $container; 14 | }, 15 | argTypes: { 16 | tabs: { control: 'array', default: [] }, 17 | activeTabOnInit: { control: 'number', default: 0 }, 18 | }, 19 | }; 20 | 21 | export const Default = { 22 | args: { 23 | tabs: [ 24 | { 25 | title: 'Aba 1', 26 | icon: iconHome, 27 | content: new Button({ 28 | text: 'Cadastrar pet', 29 | isFullWidth: true, 30 | isDisabled: false, 31 | }), 32 | }, 33 | { 34 | title: 'Aba 2', 35 | icon: iconBirthday, 36 | content: new Button({ 37 | text: 'Excluir pet', 38 | isFullWidth: true, 39 | isDisabled: false, 40 | }), 41 | }, 42 | { 43 | title: 'Aba 3', 44 | content: new Button({ 45 | text: 'Editar pet', 46 | isFullWidth: true, 47 | isDisabled: false, 48 | }), 49 | }, 50 | { 51 | title: 'Aba 4', 52 | content: new Button({ 53 | text: 'Consultar pet', 54 | isFullWidth: true, 55 | isDisabled: false, 56 | }), 57 | }, 58 | ], 59 | activeTabOnInit: 0, 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /src/stories/TextArea.stories.js: -------------------------------------------------------------------------------- 1 | import Textarea from '../components/TextArea'; 2 | 3 | export default { 4 | title: 'Components/TextArea', 5 | render: (args) => { 6 | const textarea = new Textarea(args); 7 | const $container = document.createElement('div'); 8 | textarea.mount($container); 9 | 10 | return $container; 11 | }, 12 | argTypes: { 13 | name: { control: 'text', default: '' }, 14 | placeholder: { control: 'text', default: '' }, 15 | maxLength: { control: 'number', default: 524288 }, 16 | required: { control: 'boolean', default: true }, 17 | }, 18 | }; 19 | 20 | export const Default = { 21 | args: { 22 | name: 'textarea', 23 | placeholder: 'Escreva o cuidado especial', 24 | required: true, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/stories/TextInput.stories.js: -------------------------------------------------------------------------------- 1 | import TextInput from '../components/TextInput/index'; 2 | 3 | export default { 4 | title: 'components/TextInput', 5 | 6 | render: (args) => { 7 | const input = new TextInput(args); 8 | const $container = document.createElement('div'); 9 | input.mount($container); 10 | 11 | return $container; 12 | }, 13 | argTypes: { 14 | placeholder: { control: 'text', default: '' }, 15 | assetUrl: {}, 16 | assetPosition: {}, 17 | variation: {}, 18 | type: { control: 'text', default: '' }, 19 | }, 20 | }; 21 | 22 | export const Default = { 23 | args: { 24 | placeholder: 'email@petdex.com.br', 25 | }, 26 | }; 27 | 28 | export const Password = { 29 | args: { 30 | type: 'password', 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/stories/Toggle.stories.js: -------------------------------------------------------------------------------- 1 | import Toggle from '../components/Toggle'; 2 | 3 | export default { 4 | title: 'Components/Toggle', 5 | 6 | render: (args) => { 7 | const $container = document.createElement('div'); 8 | const toggle = new Toggle(args); 9 | toggle.mount($container); 10 | return $container; 11 | }, 12 | argTypes: { 13 | checked: { control: 'boolean', default: false }, 14 | }, 15 | }; 16 | 17 | export const Default = {}; 18 | -------------------------------------------------------------------------------- /src/stories/UploadImage.stories.js: -------------------------------------------------------------------------------- 1 | import UploadImage from '../components/UploadImage'; 2 | 3 | export default { 4 | title: 'Components/UploadImage', 5 | 6 | render: () => { 7 | const $container = document.createElement('div'); 8 | const uploadImage = new UploadImage(); 9 | uploadImage.mount($container); 10 | return $container; 11 | }, 12 | }; 13 | 14 | export const Default = {}; 15 | -------------------------------------------------------------------------------- /src/stories/Vaccine.stories.js: -------------------------------------------------------------------------------- 1 | import Vaccine from '../components/Vaccine'; 2 | 3 | export default { 4 | title: 'Components/Vaccine', 5 | 6 | render: (args) => { 7 | const $container = document.createElement('div'); 8 | const toggle = new Vaccine(args); 9 | toggle.mount($container); 10 | 11 | return $container; 12 | }, 13 | argTypes: { 14 | vaccines: { control: 'array', default: [] }, 15 | }, 16 | }; 17 | 18 | export const Default = { 19 | args: { 20 | vaccines: [ 21 | { 22 | id: '1', 23 | veterinary: 'Dr octopus', 24 | title: 'Antirrábica', 25 | date: new Date().toISOString(), 26 | }, 27 | { 28 | id: '2', 29 | veterinary: 'Dr Felipa', 30 | title: 'Raiva', 31 | date: new Date(2023, 5, 2).toISOString(), 32 | }, 33 | { 34 | id: '3', 35 | veterinary: 'Dr octopus', 36 | title: 'Raiva', 37 | date: new Date(2023, 2, 2).toISOString(), 38 | }, 39 | ], 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/stories/assets/tabber/birthday.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/stories/assets/tabber/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/stories/readme/Example-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/stories/readme/Example-1.png -------------------------------------------------------------------------------- /src/stories/readme/Example-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devhatt/pet-dex-frontend/a505ea41b5aad302b26a19ced32570c147ba509c/src/stories/readme/Example-2.png -------------------------------------------------------------------------------- /src/stories/readme/Example-2.stories.js: -------------------------------------------------------------------------------- 1 | import Example from './Example'; 2 | 3 | export default { 4 | title: 'Examples/Example v2', 5 | 6 | render: (args) => { 7 | const $container = document.createElement('div'); 8 | const component = new Example(args); 9 | component.mount($container); 10 | return $container; 11 | }, 12 | 13 | /* no argTypes podemos adicionar os nossos controles 14 | Visite https://storybook.js.org/docs/api/arg-types para a documentação completa dos argTypes */ 15 | argTypes: { 16 | text: { control: 'text', default: 'Example' }, 17 | size: { control: 'select', options: ['32', '64', '86'] }, 18 | color: { control: 'color', default: '#000' }, 19 | }, 20 | }; 21 | 22 | export const Default = { 23 | args: { 24 | size: '64', // Vamos adicionar um tamanho padrão 25 | }, 26 | }; 27 | 28 | /* Podemos criar multiplas stories 29 | Observe que o nome da variavel será o nome do story */ 30 | export const Greeting = { 31 | args: { 32 | ...Default.args, // podemos aproveitar propriedades previamente configuradas 33 | text: 'Hello World', 34 | color: '#f00', 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/stories/readme/Example.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'pet-dex-utilities'; 2 | 3 | const html = ` 4 |

5 | `; 6 | 7 | export default function Example({ 8 | text = 'Example', 9 | color = '#000', 10 | size = 16, 11 | } = {}) { 12 | Component.call(this, { html }); 13 | this.setText(text); 14 | this.setColor(color); 15 | this.setSize(size); 16 | } 17 | 18 | Example.prototype = Object.assign(Example.prototype, Component.prototype, { 19 | setText(text = '') { 20 | this.selected.get('text').textContent = text; 21 | }, 22 | 23 | setColor(color = '#000') { 24 | this.selected.get('text').style.color = color; 25 | }, 26 | 27 | setSize(size = 32) { 28 | this.selected.get('text').style.fontSize = `${size}px`; 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /src/stories/readme/Example.stories.js: -------------------------------------------------------------------------------- 1 | import Example from './Example'; 2 | 3 | // Configuração dos stories 4 | export default { 5 | title: 'Examples/Example', 6 | 7 | // Note que podemos receber argumentos 8 | render: (args) => { 9 | const $container = document.createElement('div'); 10 | const component = new Example(args); 11 | component.mount($container); 12 | return $container; 13 | }, 14 | }; 15 | 16 | // Aqui criamos um story, passando os parametros que será injetado pela configuração 17 | export const Default = {}; 18 | -------------------------------------------------------------------------------- /src/styles/base.scss: -------------------------------------------------------------------------------- 1 | @use 'reset-css/sass/reset.scss'; 2 | @use '~styles/typography'; 3 | 4 | :root { 5 | font-size: 62.5%; 6 | } 7 | 8 | body { 9 | font-size: 1.6rem; 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/breakpoints.module.scss: -------------------------------------------------------------------------------- 1 | @import 'breakpoints'; 2 | 3 | :export { 4 | extraSmallSize: $extraSmallDeviceSizeMin; 5 | smallSize: $smallDeviceSizeMin; 6 | mediumSize: $mediumDeviceSizeMin; 7 | largeSize: $largeDeviceSizeMin; 8 | largestSize: $largestDeviceSizeMin; 9 | } 10 | -------------------------------------------------------------------------------- /src/styles/breakpoints.scss: -------------------------------------------------------------------------------- 1 | $extraSmallDeviceSizeMin: 20em; 2 | $smallDeviceSizeMin: 22.5em; 3 | $mediumDeviceSizeMin: 41.6875em; 4 | $largeDeviceSizeMin: 64em; 5 | $largestDeviceSizeMin: 80em; 6 | 7 | @mixin from320 { 8 | @media (min-width: $extraSmallDeviceSizeMin) { 9 | @content; 10 | } 11 | } 12 | 13 | @mixin from360 { 14 | @media (min-width: $smallDeviceSizeMin) { 15 | @content; 16 | } 17 | } 18 | 19 | @mixin from667 { 20 | @media (min-width: $mediumDeviceSizeMin) { 21 | @content; 22 | } 23 | } 24 | 25 | @mixin from1024 { 26 | @media (min-width: $largeDeviceSizeMin) { 27 | @content; 28 | } 29 | } 30 | 31 | @mixin from1280 { 32 | @media (min-width: $largestDeviceSizeMin) { 33 | @content; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/styles/colors.scss: -------------------------------------------------------------------------------- 1 | // primary fonts 2 | $primary100: rgb(98, 220, 246); 3 | $primary200: rgb(27, 133, 243); 4 | $primary300: rgb(255, 197, 66); 5 | $primary400: rgb(58, 141, 168); 6 | $primary500: rgb(86, 73, 228); 7 | $primary600: rgb(0, 52, 89); 8 | $primary700: rgb(9, 24, 70); 9 | $primary800: rgb(223, 61, 130); 10 | 11 | // secondary fonts 12 | $secondary100: rgb(255, 255, 255); 13 | $secondary200: rgb(186, 216, 235); 14 | $secondary300: rgb(255, 241, 166); 15 | $secondary400: rgb(255, 198, 218); 16 | 17 | // success 18 | $success100: rgb(210, 239, 224); 19 | $success200: rgb(49, 138, 94); 20 | 21 | // error 22 | $error100: rgb(179, 38, 30); 23 | 24 | // White 25 | 26 | $white: rgb(255, 255, 255); 27 | 28 | // neutrals (Gray) 29 | $gray100: rgb(236, 239, 242); 30 | $gray150: rgb(236, 239, 242); 31 | $gray200: rgb(224, 224, 224); 32 | $gray250: rgb(172, 172, 181); 33 | $gray300: rgb(179, 190, 205); 34 | $gray400: rgb(141, 141, 141); 35 | $gray500: rgb(96, 104, 115); 36 | $gray600: rgb(102, 116, 121); 37 | $gray700: rgb(6, 8, 9); 38 | $gray800: rgb(57, 67, 79); 39 | $gray900: rgb(32, 35, 38); 40 | 41 | // dark mode 42 | $dark100: rgb(30, 40, 51); 43 | $dark200: rgb(18, 20, 20); 44 | $dark300: rgb(0, 0, 0); 45 | 46 | // shades 47 | $shade50: rgb(255, 255, 255); 48 | $shade100: rgb(0, 0, 0); 49 | 50 | // custom 51 | 52 | $blue600: rgb(18, 104, 204); 53 | -------------------------------------------------------------------------------- /src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | $primaryFont: 'Montserrat', sans-serif; 2 | $secondaryFont: 'Wix Madefor Display', sans-serif; 3 | $tertiaryFont: 'Helvetica', sans-serif; 4 | $fourthFont: 'Noto Sans', sans-serif; 5 | $fifthFont: 'Poppins', sans-serif; 6 | $sixthFont: 'Catamaran', sans-serif; 7 | 8 | $xxs: 1.2rem; 9 | $xs: 1.4rem; 10 | $sm: 1.6rem; 11 | $md: 2rem; 12 | $lg: 2.4rem; 13 | $xl: 3rem; 14 | $xl2: 3.6rem; 15 | $xl3: 4.8rem; 16 | $xl4: 6rem; 17 | 18 | $regular: 400; 19 | $medium: 500; 20 | $semiBold: 600; 21 | $bold: 700; 22 | 23 | $normal: normal; 24 | $italic: italic; 25 | $underline: underline; 26 | 27 | //função para criar estilos tipográficos em geral 28 | @function get-font-style($color, $family, $weight, $size, $style) { 29 | @return ( 30 | color: $color, 31 | font-family: $family, 32 | font-weight: $weight, 33 | font-size: $size, 34 | font-style: $style 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/styles/typography.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Helvetica'; 3 | font-weight: normal; 4 | font-style: normal; 5 | src: 6 | local('Helvetica'), 7 | url('/helvetica/Helvetica.woff') format('woff'), 8 | url('/helvetica/Helvetica.woff2') format('woff2'), 9 | url('/helvetica/Helvetica.eot') format('embedded-opentype'), 10 | url('/helvetica/Helvetica.svg') format('svg'), 11 | url('/helvetica/Helvetica.ttf') format('truetype'), 12 | url('/helvetica/Helvetica.otf') format('opentype'); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/breakpoints/breakpoints.js: -------------------------------------------------------------------------------- 1 | import variables from '../../styles/breakpoints.module.scss'; 2 | 3 | const { extraSmallSize, smallSize, mediumSize, largeSize, largestSize } = 4 | variables; 5 | 6 | const events = new Map(); 7 | events.set('from320', new Set()); 8 | events.set('from360', new Set()); 9 | events.set('from667', new Set()); 10 | events.set('from1024', new Set()); 11 | events.set('from1280', new Set()); 12 | 13 | const obj = { 14 | from320: window.matchMedia(`(min-width: ${extraSmallSize})`), 15 | from360: window.matchMedia(`(min-width: ${smallSize})`), 16 | from667: window.matchMedia(`(min-width: ${mediumSize})`), 17 | from1024: window.matchMedia(`(min-width: ${largeSize})`), 18 | from1280: window.matchMedia(`(min-width: ${largestSize})`), 19 | }; 20 | 21 | export function listenBreakpoint(breakpoint, callback) { 22 | const callbacks = events.get(breakpoint); 23 | if (!callbacks) { 24 | console.warn('callback not found: ', breakpoint); 25 | return; 26 | } 27 | callbacks.add(callback); 28 | callback(obj[breakpoint].matches); 29 | } 30 | 31 | export function unlistenBreakpoint(breakpoint, callback) { 32 | const callbacks = events.get(breakpoint); 33 | if (!callbacks) { 34 | console.warn('callback not found: ', breakpoint); 35 | return; 36 | } 37 | callbacks.delete(callback); 38 | } 39 | 40 | Object.keys(obj).forEach((key) => { 41 | obj[key].addEventListener('change', (e) => { 42 | const callbacks = events.get(key); 43 | callbacks.forEach((callback) => callback(e.matches)); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/utils/validations.js: -------------------------------------------------------------------------------- 1 | export function isNameValid(name) { 2 | const nameRegex = /[a-zA-Z]$/; 3 | 4 | return nameRegex.test(name); 5 | } 6 | 7 | export function isBirthValid(birth) { 8 | const birthRegex = /(\d{2})\/?(\d{2})\/?(\d{4})$/; 9 | 10 | return birthRegex.test(birth); 11 | } 12 | 13 | export function isEmailValid(email) { 14 | const emailRegex = /[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-z]{2,}/; 15 | 16 | return emailRegex.test(email); 17 | } 18 | 19 | export function isPhoneValid(phone) { 20 | const phoneRegex = /(\d{2})\d{5}\d{4}/; 21 | 22 | return phoneRegex.test(phone); 23 | } 24 | 25 | export function isPasswordValid(password) { 26 | const hasMinLength = password.length >= 10; 27 | const hasUppercase = /[A-Z]/g.test(password); 28 | const hasNumber = /[0-9]/g.test(password); 29 | const hasSpecialCharacter = /[!@#$%^&*{}<>;'(),.?":|]/g.test(password); 30 | 31 | return hasMinLength && hasUppercase && hasNumber && hasSpecialCharacter; 32 | } 33 | 34 | export function isLocalValid(local) { 35 | let isFilled = true; 36 | 37 | if (local === '' || local === undefined) { 38 | isFilled = false; 39 | } 40 | 41 | return isFilled; 42 | } 43 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | /// 2 | // vite.config.js 3 | import { resolve } from 'node:path'; 4 | import { defineConfig } from 'vite'; 5 | import jsconfigPaths from 'vite-jsconfig-paths'; 6 | import { VitePWA } from 'vite-plugin-pwa'; 7 | 8 | export default defineConfig({ 9 | root: resolve(__dirname, 'src/layouts/'), 10 | define: { 11 | __isBrowser__: true, 12 | }, 13 | test: { 14 | environment: 'jsdom', 15 | globals: true, 16 | root: resolve(__dirname, ''), 17 | coverage: { 18 | include: ['src/**/*.js'], 19 | exclude: ['src/**/*.spec.js'], 20 | reportOnFailure: true, 21 | thresholds: { 22 | lines: 0, 23 | statements: 0, 24 | branches: 0, 25 | functions: 0, 26 | }, 27 | reporter: ['text', 'lcov', 'html', 'json', 'json-summary'], 28 | }, 29 | include: ['src/**/*.spec.js'], 30 | setupFiles: ['src/__tests__/setup.js'], 31 | }, 32 | build: { 33 | outDir: resolve(__dirname, 'dist'), 34 | rollupOptions: { 35 | input: { 36 | index: resolve(__dirname, 'src/layouts/index.html'), 37 | teste: resolve(__dirname, 'src/layouts/sample-page/index.html'), 38 | }, 39 | }, 40 | }, 41 | resolve: { 42 | alias: { 43 | '~src': resolve(__dirname, 'src'), 44 | '~styles': resolve(__dirname, 'src/styles'), 45 | '~stories': resolve(__dirname, 'src/stories'), 46 | '~layouts': resolve(__dirname, 'src/layouts'), 47 | }, 48 | }, 49 | plugins: [ 50 | VitePWA({ 51 | manifest: { 52 | name: 'PetDex', 53 | short_name: 'PetDex', 54 | icons: [ 55 | { 56 | src: '/favicon/android-icon-36x36.ico', 57 | sizes: '36x36', 58 | type: 'image/png', 59 | }, 60 | { 61 | src: '/favicon/android-icon-48x48.ico', 62 | sizes: '48x48', 63 | type: 'image/png', 64 | }, 65 | { 66 | src: '/favicon/android-icon-72x72.ico', 67 | sizes: '72x72', 68 | type: 'image/png', 69 | }, 70 | { 71 | src: '/favicon/android-icon-96x96.ico', 72 | sizes: '96x96', 73 | type: 'image/png', 74 | }, 75 | { 76 | src: '/favicon/android-icon-144x144.ico', 77 | sizes: '144x144', 78 | type: 'image/png', 79 | }, 80 | { 81 | src: '/favicon/android-icon-192x192.ico', 82 | sizes: '192x192', 83 | type: 'image/png', 84 | }, 85 | ], 86 | theme_color: '#ffffff', 87 | background_color: '#ffffff', 88 | display: 'standalone', 89 | }, 90 | }), 91 | jsconfigPaths(), 92 | ], 93 | }); 94 | --------------------------------------------------------------------------------