├── .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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
4 |
--------------------------------------------------------------------------------
/src/components/Drawer/images/line.svg:
--------------------------------------------------------------------------------
1 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 = ``;
4 |
5 | export default large;
6 |
--------------------------------------------------------------------------------
/src/components/SizeSelector/images/small.js:
--------------------------------------------------------------------------------
1 | const small = ``;
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 |
4 |
--------------------------------------------------------------------------------
/src/components/UploadImage/img/placeholder.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/UploadImage/img/plus-icon.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
4 |
--------------------------------------------------------------------------------
/src/components/Vaccine/images/vaccine.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |

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 |
9 |
--------------------------------------------------------------------------------
/src/layouts/components/Navigation/images/exit.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/layouts/components/Navigation/images/menu.svg:
--------------------------------------------------------------------------------
1 |
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 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
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 |
--------------------------------------------------------------------------------
/src/layouts/components/SideMenu/images/conta.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layouts/components/SideMenu/images/doacoes.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layouts/components/SideMenu/images/exit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layouts/components/SideMenu/images/exitmenu.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layouts/components/SideMenu/images/meuspets.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layouts/components/SideMenu/images/notifications.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/layouts/components/SideMenu/images/petdex.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |

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 |
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 |
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 |
4 |
--------------------------------------------------------------------------------
/src/layouts/pages/PetVet/images/estetoscopio.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------