├── .changeset
├── README.md
├── config.json
└── tender-cows-allow.md
├── .env.example
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── bug-report.md
│ └── feature-request.md
├── dependabot.yml
├── pull_request_template.md
└── workflows
│ ├── autoMilestone.yml
│ ├── base-workflows.yml
│ ├── code-style.yml
│ ├── createBranchAssign.yml
│ ├── main.yml
│ ├── release.yml
│ └── unassignIssue.yml
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .ladle
├── components.tsx
└── laddle.scss
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .stylelintrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── commitlint.config.js
├── docs
├── CODEREVIEW.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── FLOWISSUE.md
├── HOW_TO_ADD_ICONS.md
├── STYLEGUIDE.md
└── img
│ └── banner.png
├── e2e
└── example.spec.ts
├── electron-builder.json
├── electron
├── electron-env.d.ts
├── main.ts
├── preload.ts
└── tsconfig.json
├── eslint.config.mjs
├── index.html
├── mock-api.json
├── package.json
├── playwright-ct.config.mts
├── playwright.config.ts
├── playwright
├── index.html
└── index.tsx
├── pnpm-lock.yaml
├── public
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── social-github.svg
├── setup.mjs
├── src
├── App.tsx
├── assets
│ ├── imagetest.jpg
│ └── logo.png
├── components
│ ├── Accordion
│ │ ├── Accordion.module.scss
│ │ ├── Accordion.spec.tsx
│ │ ├── Accordion.stories.tsx
│ │ ├── Accordion.tsx
│ │ └── Accordion.types.ts
│ ├── AccordionTab
│ │ ├── AccordionTab.module.scss
│ │ ├── AccordionTab.spec.tsx
│ │ ├── AccordionTab.stories.tsx
│ │ ├── AccordionTab.tsx
│ │ └── AccordionTab.types.ts
│ ├── AccountCard
│ │ ├── AccountCard.module.scss
│ │ ├── AccountCard.spec.tsx
│ │ ├── AccountCard.stories.tsx
│ │ ├── AccountCard.tsx
│ │ └── AccountCard.types.ts
│ ├── Avatar
│ │ ├── Avatar.module.scss
│ │ ├── Avatar.spec.tsx
│ │ ├── Avatar.stories.tsx
│ │ ├── Avatar.tsx
│ │ └── Avatar.types.ts
│ ├── Button
│ │ ├── Button.module.scss
│ │ ├── Button.stories.tsx
│ │ ├── Button.tsx
│ │ └── Button.types.ts
│ ├── CharacterLimitMainText
│ │ ├── CharacterLimit.module.scss
│ │ ├── CharacterLimit.spec.tsx
│ │ ├── CharacterLimit.tsx
│ │ └── CharacterLimit.types.ts
│ ├── Checkbox
│ │ ├── Checkbox.module.scss
│ │ ├── Checkbox.spec.tsx
│ │ ├── Checkbox.stories.tsx
│ │ ├── Checkbox.tsx
│ │ ├── Checkbox.types.ts
│ │ └── assets
│ │ │ └── check.svg
│ ├── FeedbackError
│ │ ├── FeedbackError.data.ts
│ │ ├── FeedbackError.module.scss
│ │ ├── FeedbackError.spec.tsx
│ │ ├── FeedbackError.stories.tsx
│ │ ├── FeedbackError.tsx
│ │ ├── FeedbackError.type.ts
│ │ └── components
│ │ │ ├── FeedbackErrorMobile.module.scss
│ │ │ ├── FeedbackErrorMobile.spec.tsx
│ │ │ ├── FeedbackErrorMobile.stories.tsx
│ │ │ └── FeedbackErrorMobile.tsx
│ ├── Icon
│ │ ├── Icon.spec.tsx
│ │ ├── Icon.tsx
│ │ ├── Icon.types.ts
│ │ ├── data.ts
│ │ ├── icon.stories.tsx
│ │ └── icons
│ │ │ ├── Mobile.svg
│ │ │ ├── Pc.svg
│ │ │ ├── Tablet.svg
│ │ │ ├── TriangleLeftArrow.svg
│ │ │ ├── alert.svg
│ │ │ ├── arrow-right.svg
│ │ │ ├── back-track.svg
│ │ │ ├── backspace.svg
│ │ │ ├── blind-eye.svg
│ │ │ ├── bus.svg
│ │ │ ├── calendar.svg
│ │ │ ├── car.svg
│ │ │ ├── check-filled.svg
│ │ │ ├── check.svg
│ │ │ ├── checkbox-checked-filled.svg
│ │ │ ├── checkbox-checked.svg
│ │ │ ├── circle.svg
│ │ │ ├── close.svg
│ │ │ ├── discord.svg
│ │ │ ├── drop-down.svg
│ │ │ ├── emoji.svg
│ │ │ ├── facebook.svg
│ │ │ ├── gif.svg
│ │ │ ├── hamburguer.svg
│ │ │ ├── hexagon-filled.svg
│ │ │ ├── hexagon.svg
│ │ │ ├── horizontal-dots.svg
│ │ │ ├── icon-13.svg
│ │ │ ├── icon-15.svg
│ │ │ ├── icon-16.svg
│ │ │ ├── icon-17.svg
│ │ │ ├── icon-18.svg
│ │ │ ├── icon-19.svg
│ │ │ ├── icon-20.svg
│ │ │ ├── icon-29.svg
│ │ │ ├── icon-30.svg
│ │ │ ├── icon-33.svg
│ │ │ ├── icon-34.svg
│ │ │ ├── icon-35.svg
│ │ │ ├── icon-36.svg
│ │ │ ├── icon-37.svg
│ │ │ ├── icon-41.svg
│ │ │ ├── icon-42.svg
│ │ │ ├── icon-45.svg
│ │ │ ├── icon-46.svg
│ │ │ ├── icon-47.svg
│ │ │ ├── icon-48.svg
│ │ │ ├── icon-49.svg
│ │ │ ├── icon-50.svg
│ │ │ ├── icon-51.svg
│ │ │ ├── icon-55.svg
│ │ │ ├── icon-59.svg
│ │ │ ├── instagram.svg
│ │ │ ├── keyboard.svg
│ │ │ ├── left-arrow.svg
│ │ │ ├── letter.svg
│ │ │ ├── mag.svg
│ │ │ ├── mail.svg
│ │ │ ├── markdown.svg
│ │ │ ├── mic.svg
│ │ │ ├── minus.svg
│ │ │ ├── mobile.svg
│ │ │ ├── moon.svg
│ │ │ ├── options-filled.svg
│ │ │ ├── options.svg
│ │ │ ├── pencil.svg
│ │ │ ├── pentagon-filled.svg
│ │ │ ├── pentagon.svg
│ │ │ ├── play.svg
│ │ │ ├── plus.svg
│ │ │ ├── right-arrow.svg
│ │ │ ├── scissors.svg
│ │ │ ├── share.svg
│ │ │ ├── small-circle-filled.svg
│ │ │ ├── small-circle.svg
│ │ │ ├── square-filled.svg
│ │ │ ├── square.svg
│ │ │ ├── star-filled.svg
│ │ │ ├── star.svg
│ │ │ ├── sun.svg
│ │ │ ├── taxi.svg
│ │ │ ├── tiktok.svg
│ │ │ ├── timer.svg
│ │ │ ├── train.svg
│ │ │ ├── translate.svg
│ │ │ ├── trash.svg
│ │ │ ├── triangle-filled.svg
│ │ │ ├── triangle.svg
│ │ │ ├── twitter.svg
│ │ │ ├── vertical-dots.svg
│ │ │ └── world.svg
│ ├── InputSearch
│ │ ├── InputSearch.module.scss
│ │ ├── InputSearch.spec.tsx
│ │ ├── InputSearch.stories.tsx
│ │ ├── InputSearch.tsx
│ │ ├── InputSearch.types.ts
│ │ └── assets
│ │ │ ├── alertIcon.svg
│ │ │ ├── leftIcon.svg
│ │ │ └── rightIcon.svg
│ ├── MainComposer
│ │ ├── MainComposer.module.scss
│ │ ├── MainComposer.spec.tsx
│ │ ├── MainComposer.stories.tsx
│ │ ├── MainComposer.tsx
│ │ ├── MainComposer.type.ts
│ │ └── components
│ │ │ ├── ComposerEditor
│ │ │ ├── ComposeEditor.spec.tsx
│ │ │ ├── ComposerEditor.module.scss
│ │ │ ├── ComposerEditor.stories.tsx
│ │ │ ├── ComposerEditor.tsx
│ │ │ ├── ComposerEditor.types.ts
│ │ │ └── utils
│ │ │ │ └── textValidator
│ │ │ │ ├── textValidator.spec.ts
│ │ │ │ ├── textValidator.ts
│ │ │ │ ├── textValidator.types.ts
│ │ │ │ └── textValidators.ts
│ │ │ ├── InputMediaGroup
│ │ │ ├── InputMediaGroup.ct.spec.tsx
│ │ │ ├── InputMediaGroup.module.scss
│ │ │ ├── InputMediaGroup.spec.tsx
│ │ │ ├── InputMediaGroup.stories.tsx
│ │ │ ├── InputMediaGroup.tsx
│ │ │ ├── InputMediaGroup.type.ts
│ │ │ ├── components
│ │ │ │ ├── InputMedia
│ │ │ │ │ ├── InputMedia.ct.spec.tsx
│ │ │ │ │ ├── InputMedia.mock.tsx
│ │ │ │ │ ├── InputMedia.module.scss
│ │ │ │ │ ├── InputMedia.spec.tsx
│ │ │ │ │ ├── InputMedia.stories.tsx
│ │ │ │ │ ├── InputMedia.tsx
│ │ │ │ │ ├── InputMedia.types.ts
│ │ │ │ │ └── assets
│ │ │ │ │ │ └── imageEmptyGray.svg
│ │ │ │ └── MediaPreview
│ │ │ │ │ ├── MediaPreview.tsx
│ │ │ │ │ └── MediaPreview.types.ts
│ │ │ └── utils
│ │ │ │ ├── fileValidator
│ │ │ │ ├── fileValidator.ts
│ │ │ │ └── fileValidator.types.ts
│ │ │ │ └── mediaValidator
│ │ │ │ ├── imageValidator
│ │ │ │ ├── imageData.type.ts
│ │ │ │ ├── imageValidator.spec.ts
│ │ │ │ └── imageValidator.ts
│ │ │ │ ├── interfaces
│ │ │ │ └── fileValidator.types.ts
│ │ │ │ ├── mediaValidator.spec.ts
│ │ │ │ ├── mediaValidators.ts
│ │ │ │ ├── utils
│ │ │ │ └── getAspectRatio.ts
│ │ │ │ └── videoValidator
│ │ │ │ ├── videoData.type.ts
│ │ │ │ ├── videoValidator.spec.ts
│ │ │ │ └── videoValidator.ts
│ │ │ └── MainComposerBase
│ │ │ ├── MainComposerBase.components.tsx
│ │ │ ├── MainComposerBase.module.scss
│ │ │ ├── MainComposerBase.tsx
│ │ │ └── MainComposerBase.type.ts
│ ├── Modal
│ │ ├── Modal.module.scss
│ │ ├── Modal.spec.tsx
│ │ ├── Modal.stories.tsx
│ │ ├── Modal.tsx
│ │ └── Modal.types.ts
│ ├── Preview
│ │ ├── Preview.module.scss
│ │ ├── Preview.spec.tsx
│ │ ├── Preview.stories.tsx
│ │ ├── Preview.tsx
│ │ └── Preview.types.ts
│ ├── SentrySetup
│ │ └── SentrySetup.tsx
│ ├── SocialMediaList
│ │ ├── SocialMediaList.module.scss
│ │ ├── SocialMediaList.spec.tsx
│ │ ├── SocialMediaList.tsx
│ │ ├── SocialMediaList.type.ts
│ │ └── assets
│ │ │ ├── plusIcon.svg
│ │ │ └── xIcon.svg
│ ├── Switch
│ │ ├── Switch.module.scss
│ │ ├── Switch.spec.tsx
│ │ ├── Switch.stories.tsx
│ │ ├── Switch.tsx
│ │ ├── Switch.types.ts
│ │ └── assets
│ │ │ ├── checkIcon.svg
│ │ │ └── disableIcon.svg
│ └── TextInput
│ │ ├── TextInput.components.tsx
│ │ ├── TextInput.module.scss
│ │ ├── TextInput.spec.tsx
│ │ ├── TextInput.stories.tsx
│ │ ├── TextInput.tsx
│ │ ├── TextInput.types.ts
│ │ └── assets
│ │ └── alertIcon.svg
├── constants
│ └── social-medias.ts
├── enums
│ ├── HttpMethods.ts
│ └── httpStatusCodes.ts
├── env.d.ts
├── hooks
│ ├── useHorizontalScroll
│ │ └── useHorizontalScroll.ts
│ └── useKeyPress
│ │ ├── useKeyPress.spec.ts
│ │ └── useKeyPress.ts
├── i18n
│ ├── index.ts
│ └── locales
│ │ ├── en
│ │ └── en-us.json
│ │ └── pt
│ │ └── pt-br.json
├── index.tsx
├── mocks
│ └── mockMedias
│ │ ├── mockFiles.ts
│ │ ├── mockImage.ts
│ │ ├── mockMedia.type.ts
│ │ └── mockVideo.ts
├── pages
│ ├── home
│ │ ├── components
│ │ │ ├── ActionBar
│ │ │ │ ├── ActionBar.module.scss
│ │ │ │ └── ActionBar.tsx
│ │ │ ├── Header
│ │ │ │ ├── Header.module.scss
│ │ │ │ ├── Header.spec.tsx
│ │ │ │ ├── Header.stories.tsx
│ │ │ │ ├── Header.tsx
│ │ │ │ └── images
│ │ │ │ │ ├── instagram.svg
│ │ │ │ │ ├── octopost.svg
│ │ │ │ │ ├── tiktok.svg
│ │ │ │ │ └── twitter.svg
│ │ │ ├── Sidebar
│ │ │ │ ├── Sidebar.components.tsx
│ │ │ │ ├── Sidebar.module.scss
│ │ │ │ ├── Sidebar.spec.tsx
│ │ │ │ ├── Sidebar.stories.tsx
│ │ │ │ ├── Sidebar.tsx
│ │ │ │ ├── Sidebar.types.ts
│ │ │ │ ├── assets
│ │ │ │ │ └── plusIcon.svg
│ │ │ │ └── components
│ │ │ │ │ ├── AddAccount
│ │ │ │ │ ├── AddAccount.tsx
│ │ │ │ │ └── AddAccount.types.ts
│ │ │ │ │ ├── SocialAccordion
│ │ │ │ │ ├── SocialAccordion.components.tsx
│ │ │ │ │ ├── SocialAccordion.module.scss
│ │ │ │ │ ├── SocialAccordion.spec.tsx
│ │ │ │ │ ├── SocialAccordion.stories.tsx
│ │ │ │ │ ├── SocialAccordion.tsx
│ │ │ │ │ ├── SocialAccordion.type.ts
│ │ │ │ │ └── assets
│ │ │ │ │ │ ├── facebook.svg
│ │ │ │ │ │ ├── instagram.svg
│ │ │ │ │ │ └── x.svg
│ │ │ │ │ └── SocialMediaForm
│ │ │ │ │ ├── SocialMediaForm.components.tsx
│ │ │ │ │ ├── SocialMediaForm.module.scss
│ │ │ │ │ ├── SocialMediaForm.spec.tsx
│ │ │ │ │ ├── SocialMediaForm.stories.tsx
│ │ │ │ │ ├── SocialMediaForm.tsx
│ │ │ │ │ ├── SocialMediaForm.types.ts
│ │ │ │ │ ├── data.ts
│ │ │ │ │ └── images
│ │ │ │ │ ├── facebook.svg
│ │ │ │ │ ├── instagram.svg
│ │ │ │ │ ├── tiktok.svg
│ │ │ │ │ ├── twitter.svg
│ │ │ │ │ └── unknown.svg
│ │ │ └── Tabber
│ │ │ │ ├── PostModes
│ │ │ │ ├── PostModes.components.tsx
│ │ │ │ ├── PostModes.module.scss
│ │ │ │ ├── PostModes.spec.tsx
│ │ │ │ ├── PostModes.stories.tsx
│ │ │ │ ├── PostModes.tsx
│ │ │ │ └── PostModes.types.ts
│ │ │ │ ├── PreviewModeSelector
│ │ │ │ ├── PreviewModeSelector.components.tsx
│ │ │ │ ├── PreviewModeSelector.module.scss
│ │ │ │ ├── PreviewModeSelector.spec.tsx
│ │ │ │ ├── PreviewModeSelector.stories.tsx
│ │ │ │ ├── PreviewModeSelector.tsx
│ │ │ │ └── PreviewModeSelector.types.ts
│ │ │ │ ├── Tabber.module.scss
│ │ │ │ ├── Tabber.stories.tsx
│ │ │ │ ├── Tabber.tsx
│ │ │ │ ├── Tabber.types.ts
│ │ │ │ ├── Tabs
│ │ │ │ ├── Tabs.module.scss
│ │ │ │ ├── Tabs.tsx
│ │ │ │ ├── Tabs.types.ts
│ │ │ │ └── components
│ │ │ │ │ ├── Tab.tsx
│ │ │ │ │ └── Tab.types.ts
│ │ │ │ └── hooks
│ │ │ │ └── useSyncTabsWithPosts.ts
│ │ ├── home.module.scss
│ │ └── home.tsx
│ └── register
│ │ ├── components
│ │ ├── AlreadyHaveAnAccount
│ │ │ ├── AlreadyHaveAnAccount.module.scss
│ │ │ ├── AlreadyHaveAnAccount.spec.tsx
│ │ │ ├── AlreadyHaveAnAccount.stories.tsx
│ │ │ └── AlreadyHaveAnAccount.tsx
│ │ ├── DesktopHeader
│ │ │ ├── DesktopHeader.module.scss
│ │ │ ├── DesktopHeader.spec.tsx
│ │ │ ├── DesktopHeader.stories.tsx
│ │ │ ├── DesktopHeader.tsx
│ │ │ └── images
│ │ │ │ └── octopost.svg
│ │ ├── DesktopHero
│ │ │ ├── Hero.module.scss
│ │ │ ├── Hero.spec.tsx
│ │ │ ├── Hero.stories.tsx
│ │ │ ├── Hero.tsx
│ │ │ └── images
│ │ │ │ ├── octo.svg
│ │ │ │ └── octopost.svg
│ │ ├── Form
│ │ │ ├── Form.module.scss
│ │ │ ├── Form.spec.tsx
│ │ │ ├── Form.stories.tsx
│ │ │ ├── Form.tsx
│ │ │ ├── FormSchema.ts
│ │ │ └── images
│ │ │ │ └── octopost.svg
│ │ ├── MobileHero
│ │ │ ├── Hero.module.scss
│ │ │ ├── Hero.spec.tsx
│ │ │ ├── Hero.stories.tsx
│ │ │ ├── Hero.tsx
│ │ │ └── images
│ │ │ │ ├── octo.svg
│ │ │ │ └── octopost.svg
│ │ ├── SignUpPromotion
│ │ │ ├── SignUpPromotion.module.scss
│ │ │ ├── SignUpPromotion.stories.tsx
│ │ │ ├── SignUpPromotion.tsx
│ │ │ └── SignUpPromotionl.spec.tsx
│ │ └── SocialLogin
│ │ │ ├── SocialLogin.module.scss
│ │ │ ├── SocialLogin.spec.tsx
│ │ │ ├── SocialLogin.stories.tsx
│ │ │ └── SocialLogin.tsx
│ │ ├── register.module.scss
│ │ └── register.tsx
├── services
│ └── api
│ │ ├── accounts
│ │ ├── accounts.ts
│ │ └── accounts.types.ts
│ │ ├── index.ts
│ │ └── social-media
│ │ ├── social-media.ts
│ │ └── social-media.types.ts
├── setupTests.ts
├── stores
│ ├── __mocks__
│ │ ├── usePostStore.mock.ts
│ │ ├── useSocialMediaStore.mock.ts
│ │ └── zunstandMock.ts
│ ├── useErrorStore
│ │ ├── useErrorStore.spec.ts
│ │ ├── useErrorStore.ts
│ │ └── useErrorStore.types.ts
│ ├── usePostStore
│ │ ├── usePostStore.spec.ts
│ │ ├── usePostStore.ts
│ │ └── usePostStore.types.ts
│ ├── useSocialMediaStore
│ │ ├── useSocialMediaStore.spec.ts
│ │ ├── useSocialMediaStore.ts
│ │ └── useSocialMediaStore.types.ts
│ └── zustand.ts
├── styles
│ ├── base.scss
│ ├── breakpoints.scss
│ └── global.scss
├── types
│ └── object.ts
└── utils
│ └── test.ts
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
├── vite.electron.config.ts
└── vitest.config.ts
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
3 | "changelog": ["@changesets/changelog-github", { "repo": "devhatt/octopost" }],
4 | "commit": false,
5 | "fixed": [["@octopost/module-manager", "@octopost/core"]],
6 | "linked": [],
7 | "access": "public",
8 | "baseBranch": "master",
9 | "updateInternalDependencies": "patch",
10 | "ignore": [],
11 | "privatePackages": {
12 | "tag": true,
13 | "version": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.changeset/tender-cows-allow.md:
--------------------------------------------------------------------------------
1 | ---
2 | '@octopost/core': minor
3 | ---
4 |
5 | added navbar to the header component
6 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | SENTRY_KEY = key do sentry aki
2 | VITE_ENVIRONMENT=development
3 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @devhatt/hatts @devhatt/octopost-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 Octopost
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 | ## Describe the bug
10 |
11 | **[A short description of what the bug is.]**
12 |
13 | ## Description
14 |
15 | [Describe the bug reported.]
16 |
17 | ## Steps to Reproduce
18 |
19 | [If applicable, provide detailed steps to reproduce the bug.]
20 |
21 | ## Expected Behavior
22 |
23 | [Describe what is expected to happen.]
24 |
25 | ## Current Behavior
26 |
27 | [Describe what is currently happening.]
28 |
29 | ## Visual information
30 |
31 | [If possible, add screenshots to illustrate this bug.]
32 |
33 | ## Additional Information
34 |
35 | [Provide any additional information, such as relevant versions, browser, OS, context, etc.]
36 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest a feature to be added to Octopost
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 | ---
8 |
9 | ## Describe the feature
10 |
11 | [A clear description of the feature.]
12 |
13 | ## Use Case
14 |
15 | [Explain the use for this feature and how it might benefits the project.]
16 |
17 | ## Implementation Details
18 |
19 | [Provide any details or suggestions on how this feature could be implemented.]
20 |
21 | ## Visual Concepts
22 |
23 | **[Does it imply any visual changes to the application? Visual elements are required in the issue]**
24 |
25 | ## Additional Information
26 |
27 | [Provide any additional information, such as context that might be relevant to the implementation of this specific feature.]
28 |
--------------------------------------------------------------------------------
/.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/base-workflows.yml:
--------------------------------------------------------------------------------
1 | name: Base Actions
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/createBranchAssign.yml:
--------------------------------------------------------------------------------
1 | name: Create Branch on Assignment
2 |
3 | on:
4 | issues:
5 | types:
6 | - assigned
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | create-branch:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout Repository
17 | uses: actions/checkout@v2
18 |
19 | - name: Set up Git
20 | run: |
21 | git config user.name "${{ github.actor }}"
22 | git config user.email "${{ github.actor }}@users.noreply.github.com"
23 |
24 | - name: Create Branch
25 | run: |
26 | ISSUE_NUMBER=$(echo ${{ github.event.issue.number }})
27 | BRANCH_NAME="issue-${ISSUE_NUMBER}"
28 | git checkout -b $BRANCH_NAME
29 | git push origin $BRANCH_NAME
30 |
31 | - name: Notify Success
32 | run: echo "Branch created successfully!"
33 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on:
4 | push:
5 | branches: ['master']
6 | pull_request:
7 | branches: ['master']
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Code Checkout
14 | uses: actions/checkout@v3
15 |
16 | - name: Setup deps
17 | uses: devhatt/workflows/.github/actions/pnpm-setup@main
18 |
19 | - name: Run unit Tests
20 | run: pnpm test:coverage --silent
21 |
22 | - name: 'Report Coverage'
23 | if: ${{ always() && !github.event.pull_request.head.repo.fork }}
24 | uses: davelosert/vitest-coverage-report-action@v2
25 |
26 | - name: 'Upload Coverage'
27 | uses: actions/upload-artifact@v4
28 | with:
29 | name: coverage
30 | path: coverage
31 |
32 | - name: Build
33 | run: pnpm build
34 | e2e:
35 | needs: [build]
36 | uses: devhatt/workflows/.github/workflows/playwright-tests.yml@main
37 | with:
38 | pnpm-version: '8'
39 | node-version: '20'
40 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | jobs:
11 | release:
12 | name: Release
13 | runs-on: ubuntu-latest
14 |
15 | permissions:
16 | contents: write # to create release
17 | issues: write # to post issue comments
18 | pull-requests: write # to create pull request
19 |
20 | env:
21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
23 | NODE_VERSION: 18.18.2
24 | STORE_PATH: .pnpm-store
25 |
26 | steps:
27 | - name: Code Checkout
28 | uses: actions/checkout@v3
29 |
30 | - name: Setup deps
31 | uses: ./.github/actions/install-deps
32 |
33 | - name: Set NPM Token
34 | run: npm config set '//registry.npmjs.org/:_authToken' "${NPM_TOKEN}"
35 |
36 | - name: Create Release Pull Request or Publish to npm
37 | id: changesets
38 | uses: changesets/action@v1
39 | with:
40 | # This expects you to have a script called release which does a build for your packages and calls changeset publish
41 | publish: pnpm ci:publish
42 | version: pnpm ci:version
43 | title: '[BUMP] Lançar uma release para o octopost!'
44 | commit: 'chore: bump packages version'
45 |
--------------------------------------------------------------------------------
/.github/workflows/unassignIssue.yml:
--------------------------------------------------------------------------------
1 | name: Unassign Issue After 7 days
2 | on:
3 | schedule:
4 | - cron: '0 0 * * *' ### Runs everyday at 00h
5 |
6 | jobs:
7 | unassign-issues-labeled-waiting-for-contributor-after-7-days-of-inactivity:
8 | name: Unassign issues after 7 days of inactivity.
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: boundfoxstudios/action-unassign-contributor-after-days-of-inactivity@main
12 | with:
13 | last-activity: 7 # After how many days the issue should get unassigned ()
14 | message: 'Automatically unassigned after 7 days of inactivity.'
15 |
--------------------------------------------------------------------------------
/.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 | pnpm typecheck
6 |
--------------------------------------------------------------------------------
/.ladle/components.tsx:
--------------------------------------------------------------------------------
1 | import type { GlobalProvider } from '@ladle/react';
2 |
3 | import './laddle.scss';
4 |
5 | export const Provider: GlobalProvider = ({ children }) =>
{children}
;
6 |
--------------------------------------------------------------------------------
/.ladle/laddle.scss:
--------------------------------------------------------------------------------
1 | @use '../src/styles/global.scss';
2 | @use 'normalize.css';
3 |
4 | * {
5 | box-sizing: border-box;
6 | }
7 |
8 | html {
9 | font-family: global.$mainFont, 'Icomoon';
10 | font-size: 62.5%;
11 | }
12 |
13 | body {
14 | font-size: 1.6rem;
15 | }
16 |
17 | button {
18 | width: 100%;
19 |
20 | padding: 0;
21 |
22 | border: 0;
23 |
24 | background: none;
25 | outline-color: global.$secondaryPurple;
26 | }
27 |
28 | ul {
29 | margin: 0;
30 | padding: 0;
31 |
32 | li {
33 | list-style-type: none;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | pnpm-lock.yaml
2 | dist
3 | dist-electron
4 | node_modules
5 | playwright-report
6 | .prettierignore
7 | .changeset/pre.json
8 | .changeset/config.json
9 | .husky
10 | *.svg
11 | **/*.svg
12 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "useTabs": false,
4 | "tabWidth": 2,
5 | "semi": true,
6 | "singleQuote": true,
7 | "jsxSingleQuote": false,
8 | "arrowParens": "always",
9 | "endOfLine": "lf",
10 | "printWidth": 80
11 | }
12 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @octopost/core
2 |
3 | ## 0.1.0
4 |
5 | ### Minor Changes
6 |
7 | - [#339](https://github.com/devhatt/octopost/pull/339) [`6e598f5`](https://github.com/devhatt/octopost/commit/6e598f5e1f51dadc6d74347c729696bafe5a3929) Thanks [@alvarogfn](https://github.com/alvarogfn)! - automatizing release
8 |
9 | ### Patch Changes
10 |
11 | - Updated dependencies [[`6e598f5`](https://github.com/devhatt/octopost/commit/6e598f5e1f51dadc6d74347c729696bafe5a3929)]:
12 | - @octopost/module-manager@0.1.0
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Roman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | rules: {
4 | 'body-empty': [2, 'always'],
5 | 'footer-empty': [2, 'always'],
6 | 'scope-empty': [2, 'always'],
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/docs/img/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhatt/octopost/d8de6481a53c8cc6270e376df9523c29b5e8e0a3/docs/img/banner.png
--------------------------------------------------------------------------------
/e2e/example.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from '@playwright/test';
2 |
3 | test('has title', async ({ page }) => {
4 | await page.goto('/');
5 |
6 | // Expect a title "to contain" a substring.
7 | await expect(page).toHaveTitle(/Octopost/);
8 | });
9 |
--------------------------------------------------------------------------------
/electron-builder.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json",
3 | "productName": "octopost",
4 | "mac": {
5 | "target": {
6 | "target": "default",
7 | "arch": ["universal"]
8 | },
9 | "category": "public.app-category.productivity"
10 | },
11 | "extends": null,
12 | "files": ["dist/**/*", "dist-electron", "package.json"],
13 | "directories": {
14 | "buildResources": "assets"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/electron/electron-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare namespace NodeJS {
4 | interface ProcessEnv {
5 | /**
6 | * The built directory structure
7 | *
8 | * ```tree
9 | * ├─┬ dist
10 | * │ ├─┬ electron
11 | * │ │ ├── main.js
12 | * │ │ └── preload.js
13 | * │ ├── index.html
14 | * │ ├── ...other-static-files-from-public
15 | * │
16 | * ```
17 | */
18 | }
19 | }
20 |
21 | // Used in Renderer process, expose in `preload.ts`
22 | interface Window {
23 | ipcRenderer: import('electron').IpcRenderer;
24 | }
25 |
--------------------------------------------------------------------------------
/electron/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "esModuleInterop": true,
6 | "sourceMap": true,
7 | "strict": true,
8 | "outDir": "../build",
9 | "rootDir": "../",
10 | "noEmitOnError": true,
11 | "typeRoots": ["node_modules/@types", "./types"]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 | Octopost
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/playwright-ct.config.mts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/experimental-ct-react';
2 | import tsconfigPaths from 'vite-tsconfig-paths';
3 |
4 | import viteConfig from './vite.config';
5 |
6 | /**
7 | * See https://playwright.dev/docs/test-configuration.
8 | */
9 | export default defineConfig({
10 | forbidOnly: !!process.env.CI,
11 | fullyParallel: true,
12 | projects: [
13 | {
14 | name: 'chromium',
15 | use: { ...devices['Desktop Chrome'] },
16 | },
17 | ],
18 | reporter: 'html',
19 | retries: process.env.CI ? 2 : 0,
20 | snapshotDir: './__snapshots__',
21 | testDir: './',
22 | testMatch: '**/*.ct.spec.tsx',
23 | timeout: 10 * 1000,
24 |
25 | use: {
26 | ctPort: 3100,
27 |
28 | ctViteConfig: {
29 | // @ts-expect-error The playwright-ct references vite of version 4.5.1, and the plugin is for vite 5.
30 | plugins: [tsconfigPaths()],
31 | // @ts-expect-error The playwright-ct references vite of version 4.5.1 and config resolves is for version 5.
32 | resolve: viteConfig.resolve,
33 | },
34 | trace: 'on-first-retry',
35 | },
36 |
37 | workers: process.env.CI ? 1 : undefined,
38 | });
39 |
--------------------------------------------------------------------------------
/playwright.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, devices } from '@playwright/test';
2 |
3 | /**
4 | * Read environment variables from file.
5 | * https://github.com/motdotla/dotenv
6 | */
7 | // require('dotenv').config();
8 |
9 | /**
10 | * See https://playwright.dev/docs/test-configuration.
11 | */
12 | export default defineConfig({
13 | forbidOnly: !!process.env.CI,
14 | fullyParallel: true,
15 | projects: [
16 | {
17 | name: 'chromium',
18 | use: { ...devices['Desktop Chrome'] },
19 | },
20 | ],
21 | reporter: 'html',
22 | retries: process.env.CI ? 2 : 0,
23 | testDir: './e2e',
24 | use: {
25 | baseURL: 'http://localhost:5173/',
26 | trace: 'on-first-retry',
27 | },
28 | webServer: {
29 | command: 'pnpm start',
30 | reuseExistingServer: !process.env.CI,
31 | url: 'http://localhost:5173/',
32 | },
33 | workers: process.env.CI ? 1 : undefined,
34 | });
35 |
--------------------------------------------------------------------------------
/playwright/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Testing Page
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/playwright/index.tsx:
--------------------------------------------------------------------------------
1 | // Import styles, initialize component theme here.
2 | // import '../src/common.css';
3 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhatt/octopost/d8de6481a53c8cc6270e376df9523c29b5e8e0a3/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhatt/octopost/d8de6481a53c8cc6270e376df9523c29b5e8e0a3/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhatt/octopost/d8de6481a53c8cc6270e376df9523c29b5e8e0a3/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
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/octopost-frontend.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/App.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import { BrowserRouter, Route } from 'react-router-dom';
3 |
4 | import { ErrorBoundary } from '@sentry/react';
5 |
6 | import Home from './pages/home/home';
7 | import Register from './pages/register/register';
8 |
9 | import SentryRoutes from '~components/SentrySetup/SentrySetup';
10 |
11 | import './styles/base.scss';
12 |
13 | // TODO: Fazer o projeto DEIXAR de ser monorepo
14 | function App(): ReactNode {
15 | return (
16 | Ocorreu um erro inesperado!}>
17 |
18 |
19 | } path="/" />
20 | } path="/register" />
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | export default App;
28 |
--------------------------------------------------------------------------------
/src/assets/imagetest.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhatt/octopost/d8de6481a53c8cc6270e376df9523c29b5e8e0a3/src/assets/imagetest.jpg
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devhatt/octopost/d8de6481a53c8cc6270e376df9523c29b5e8e0a3/src/assets/logo.png
--------------------------------------------------------------------------------
/src/components/Accordion/Accordion.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | flex-flow: column nowrap;
4 |
5 | align-items: stretch;
6 | justify-content: stretch;
7 | }
8 |
9 | .content {
10 | overflow: hidden;
11 |
12 | display: flex;
13 |
14 | flex-grow: 1;
15 | }
16 |
17 | .header {
18 | display: flex;
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Accordion/Accordion.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, RenderResult, screen } from '@testing-library/react';
2 |
3 | import Accordion from './Accordion';
4 |
5 | import type { TAccordionProps } from './Accordion.types';
6 |
7 | afterEach(() => {
8 | vi.clearAllMocks();
9 | });
10 |
11 | const makeSut = ({
12 | content = content
,
13 | header = header
,
14 | isOpen = false,
15 | ...props
16 | }: Partial): RenderResult =>
17 | render(
18 |
19 | );
20 |
21 | describe('Accordion', () => {
22 | describe('when isOpen is false', () => {
23 | it('body cannot render', () => {
24 | makeSut({ isOpen: false });
25 |
26 | const content = screen.queryByText('content');
27 |
28 | expect(content).not.toBeInTheDocument();
29 | });
30 | });
31 |
32 | describe('when isOpen is true', () => {
33 | it('body can render', () => {
34 | makeSut({ isOpen: true });
35 |
36 | const content = screen.getByText('content');
37 |
38 | expect(content).toBeVisible();
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/components/Accordion/Accordion.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Story } from '@ladle/react';
4 |
5 | import Accordion from './Accordion';
6 |
7 | import { TAccordionProps } from './Accordion.types';
8 |
9 | export const AccordionStories: Story = (props) => {
10 | const [isOpen, setIsOpen] = React.useState(false);
11 |
12 | return (
13 |
17 | Lorem ipsum dolor sit, amet consectetur adipisicing elit. od quis
18 | molestias ab, voluptatem harum excepturi facere, necessitatibus
19 | officiis. Officiis excepturi aperiam error.
20 |
21 | }
22 | duration={0.3}
23 | header={
24 |
27 | }
28 | isOpen={isOpen}
29 | />
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/components/Accordion/Accordion.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import classNames from 'classnames';
4 | import { AnimatePresence, motion, Variant } from 'framer-motion';
5 |
6 | import scss from './Accordion.module.scss';
7 |
8 | import { TAccordionProps } from './Accordion.types';
9 |
10 | function computeVariants(duration: number): Record {
11 | return {
12 | collapsed: { height: 0, transition: { duration } },
13 | expanded: { height: 'auto', transition: { duration } },
14 | };
15 | }
16 |
17 | function Accordion({ duration = 0.3, ...props }: TAccordionProps): ReactNode {
18 | return (
19 |
20 | {props.header}
21 |
22 | {props.isOpen ? (
23 |
30 | {props.content}
31 |
32 | ) : null}
33 |
34 |
35 | );
36 | }
37 |
38 | export default Accordion;
39 |
--------------------------------------------------------------------------------
/src/components/Accordion/Accordion.types.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | export type TAccordionProps = {
4 | className?: string;
5 | content: ReactNode;
6 | duration?: number;
7 | header: ReactNode;
8 | isOpen: boolean;
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/AccordionTab/AccordionTab.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 |
3 | import AccordionTab from './AccordionTab';
4 |
5 | afterEach(() => {
6 | vi.clearAllMocks();
7 | });
8 |
9 | describe('AccordionTab', () => {
10 | describe('when [hideCloseButton] is false', () => {
11 | it('not render close button', () => {
12 | render();
13 |
14 | const button = screen.getByRole('button');
15 |
16 | expect(button).toBeInTheDocument();
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/components/AccordionTab/AccordionTab.stories.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import { Story } from '@ladle/react';
4 |
5 | import AccordionTab from './AccordionTab';
6 |
7 | import { TAccordionTabProps } from './AccordionTab.types';
8 |
9 | export const AccordionTabStories: Story = (props) => {
10 | const [isOpen, setIsOpen] = useState(true);
11 |
12 | return (
13 | setIsOpen((state) => !state)}
16 | title="Accordion Tab"
17 | {...props}
18 | >
19 | Lorem ipsum, dolor sit amet consectetur adipisicing elit. Magnam autem
20 | soluta labore nulla, nam quisquam sed, nostrum ab vero suscipit quae
21 | debitis inventore velit iste iusto earum, iure aspernatur provident.
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/AccordionTab/AccordionTab.types.ts:
--------------------------------------------------------------------------------
1 | import { PropsWithChildren } from 'react';
2 |
3 | type TOnChangeOpen = (newIsOpen: boolean) => void;
4 |
5 | export type TAccordionTabProps = PropsWithChildren<{
6 | className?: string;
7 | hideCloseButton?: boolean;
8 | isOpen?: boolean;
9 | onChangeOpen?: TOnChangeOpen;
10 | title?: string;
11 | }>;
12 |
--------------------------------------------------------------------------------
/src/components/AccountCard/AccountCard.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/global.scss' as *;
2 |
3 | .container {
4 | display: flex;
5 | flex-flow: row nowrap;
6 |
7 | gap: 0.8rem;
8 |
9 | align-items: center;
10 | justify-content: space-between;
11 |
12 | padding: 0.8rem 0.8rem 0.8rem 1.6rem;
13 |
14 | border-bottom: 0.1rem solid $primaryGray;
15 |
16 | background-color: $secondaryWhite;
17 |
18 | &.invalid {
19 | background-color: $primaryRed;
20 | }
21 | }
22 |
23 | .username {
24 | width: 100%;
25 | overflow: hidden;
26 |
27 | display: flex;
28 |
29 | font-size: 1.4rem;
30 | text-overflow: ellipsis;
31 |
32 | white-space: nowrap;
33 | }
34 |
35 | .favorite {
36 | padding: 0.6rem;
37 | }
38 |
39 | .text {
40 | font-size: 1.4rem;
41 | }
42 |
43 | .avatar {
44 | width: 40px;
45 | height: 40px;
46 |
47 | flex-shrink: 0;
48 |
49 | align-items: center;
50 |
51 | justify-content: center;
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/AccountCard/AccountCard.stories.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import { faker } from '@faker-js/faker';
4 | import type { Story, StoryDefault } from '@ladle/react';
5 |
6 | import { AccountCard } from './AccountCard';
7 |
8 | import { AccountCardProps } from './AccountCard.types';
9 |
10 | export default {
11 | title: 'Account Card',
12 | } satisfies StoryDefault;
13 |
14 | export const StoryAccountCard: Story = (props) => {
15 | const [favorite, setFavorite] = useState(false);
16 | const [enabled, setEnabled] = useState(false);
17 |
18 | const handleChange = (isEnabled: boolean): void => {
19 | if (props.hasError === true)
20 | return window.alert('Não foi possível autenticar');
21 | return setEnabled(isEnabled);
22 | };
23 |
24 | return (
25 |
33 | );
34 | };
35 |
36 | StoryAccountCard.args = {
37 | avatarURL: faker.image.avatar(),
38 | hasError: false,
39 | username: faker.internet.userName(),
40 | };
41 |
--------------------------------------------------------------------------------
/src/components/AccountCard/AccountCard.types.ts:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithoutRef } from 'react';
2 |
3 | type BaseAccountCardProps = ComponentPropsWithoutRef<'div'>;
4 |
5 | export type AccountCardProps = BaseAccountCardProps & {
6 | avatarURL?: string;
7 | hasError?: boolean;
8 | icon?: string;
9 | invalid?: boolean;
10 | isEnabled?: boolean;
11 | isFavorited?: boolean;
12 | onEnableChange?: (isEnabled: boolean) => void;
13 | onFavoriteChange?: (isFavorite: boolean) => void;
14 | username: string;
15 | };
16 |
--------------------------------------------------------------------------------
/src/components/Avatar/Avatar.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/global.scss';
2 |
3 | .image {
4 | width: 40px;
5 | height: 40px;
6 |
7 | border-radius: 50%;
8 | }
9 |
10 | .username {
11 | width: 40px;
12 | height: 40px;
13 |
14 | display: grid;
15 |
16 | place-items: center;
17 |
18 | font-size: 1.6rem;
19 | font-weight: 500;
20 |
21 | background-color: global.$primaryPurple;
22 | border-radius: 50%;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Avatar/Avatar.spec.tsx:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import { render, RenderResult, screen } from '@testing-library/react';
3 |
4 | import { Avatar } from './Avatar';
5 |
6 | import { AvatarProps } from './Avatar.types';
7 |
8 | const makeSut = ({
9 | username = faker.internet.userName(),
10 | ...props
11 | }: Partial): RenderResult =>
12 | render();
13 |
14 | describe('Avatar', () => {
15 | describe('when initialize', () => {
16 | it('renders an image', () => {
17 | makeSut({ image: faker.image.url() });
18 |
19 | const avatar = screen.getByRole('img');
20 |
21 | expect(avatar).toBeInTheDocument();
22 | });
23 |
24 | it('render initial letter of username', () => {
25 | const username = faker.internet.userName();
26 |
27 | makeSut({ username });
28 |
29 | const avatar = screen.getByText(username.split('')[0]);
30 |
31 | expect(avatar).toBeInTheDocument();
32 | });
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/components/Avatar/Avatar.stories.tsx:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import type { Story, StoryDefault } from '@ladle/react';
3 |
4 | import { Avatar } from './Avatar';
5 |
6 | import { AvatarProps } from './Avatar.types';
7 |
8 | export default {
9 | title: 'Avatar',
10 | } satisfies StoryDefault;
11 |
12 | export const StoryAvatar: Story = (props) => ;
13 |
14 | StoryAvatar.storyName = 'Avatar';
15 |
16 | StoryAvatar.args = {
17 | image: faker.image.avatarGitHub(),
18 | username: faker.internet.userName(),
19 | };
20 |
21 | export const StoryAvatarWithoutImage: Story = (props) => (
22 |
23 | );
24 |
25 | StoryAvatarWithoutImage.storyName = 'Avatar without image';
26 |
27 | StoryAvatarWithoutImage.args = {
28 | username: faker.internet.userName(),
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/Avatar/Avatar.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import classNames from 'classnames';
4 |
5 | import scss from './Avatar.module.scss';
6 |
7 | import { AvatarProps } from './Avatar.types';
8 |
9 | export function Avatar(props: AvatarProps): ReactNode {
10 | const [firstLetter] = props.username.split('');
11 |
12 | if (props.image !== undefined) {
13 | return (
14 |
20 | );
21 | }
22 |
23 | return (
24 |
25 | {firstLetter}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Avatar/Avatar.types.ts:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithRef } from 'react';
2 |
3 | type AvatarBaseProps = ComponentPropsWithRef<'img'>;
4 |
5 | export type AvatarProps = AvatarBaseProps & {
6 | image?: string;
7 | username: string;
8 | };
9 |
--------------------------------------------------------------------------------
/src/components/Button/Button.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import Icon from '~components/Icon/Icon';
4 |
5 | import Button from './Button';
6 |
7 | import { ICircleButtonProps, ITextButtonProps } from './Button.types';
8 |
9 | export default {
10 | argTypes: {
11 | color: {
12 | control: { type: 'select' },
13 | options: ['primary', 'secondary'],
14 | },
15 | disabled: {
16 | control: { type: 'check' },
17 | options: [''],
18 | },
19 | variant: {
20 | control: { type: 'select' },
21 | options: ['container', 'outlined', 'text'],
22 | },
23 | },
24 | };
25 |
26 | export const ButtonText: Story = (props) => (
27 |
35 | );
36 | ButtonText.args = {
37 | color: 'primary',
38 | variant: 'outlined',
39 | };
40 |
41 | export const CircleButton: Story = (props) => (
42 | }
47 | type={props.type}
48 | variant={props.variant}
49 | />
50 | );
51 | CircleButton.args = {
52 | color: 'primary',
53 | variant: 'outlined',
54 | };
55 |
--------------------------------------------------------------------------------
/src/components/Button/Button.types.ts:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes, ReactElement } from 'react';
2 |
3 | export type IButtonProps = HTMLAttributes & {
4 | color?: 'gray' | 'primary' | 'secondary';
5 | disabled?: boolean;
6 | type?: 'button' | 'reset' | 'submit';
7 | variant?: 'container' | 'outlined' | 'text';
8 | };
9 |
10 | export type ITextButtonProps = IButtonProps & {
11 | disableElevation?: boolean;
12 | };
13 |
14 | export type ICircleButtonProps = IButtonProps & {
15 | circle: boolean;
16 | icon: ReactElement;
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/CharacterLimitMainText/CharacterLimit.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/global';
2 |
3 | .specificFill {
4 | fill: global.$primaryWhite !important;
5 | }
6 |
7 | .characterLimit {
8 | width: auto;
9 |
10 | display: flex;
11 |
12 | align-items: center;
13 |
14 | color: global.$secondaryGray;
15 | font-size: 1.2rem;
16 |
17 | &.exceeded {
18 | color: global.$secondaryRed;
19 | font-weight: bold;
20 | }
21 | }
22 |
23 | .svgColor {
24 | display: flex;
25 | gap: 0.5rem;
26 |
27 | &.exceeded svg path {
28 | fill: global.$secondaryRed;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/CharacterLimitMainText/CharacterLimit.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import classNames from 'classnames';
4 |
5 | import scss from './CharacterLimit.module.scss';
6 |
7 | import { ModuleProps } from './CharacterLimit.types.ts';
8 |
9 | function CharacterLimit(props: ModuleProps): ReactNode {
10 | const remainingCharacters = props.maxLength - props.value.length;
11 |
12 | const svgColor = classNames(scss.svgColor, {
13 | [scss.exceeded]: 0 >= remainingCharacters,
14 | });
15 |
16 | const characterLimitClass = classNames(scss.characterLimit, {
17 | [scss.exceeded]: 0 >= remainingCharacters,
18 | });
19 |
20 | function renderWithIcon(): ReactNode {
21 | return (
22 |
23 | {props.svg}
24 |
25 | {remainingCharacters}
26 |
27 |
28 | );
29 | }
30 |
31 | function renderWithoutIcon(): ReactNode {
32 | return (
33 |
34 |
35 | {props.value.length}/{props.maxLength}
36 |
37 |
38 | );
39 | }
40 |
41 | return props.svg ? renderWithIcon() : renderWithoutIcon();
42 | }
43 |
44 | export default CharacterLimit;
45 |
--------------------------------------------------------------------------------
/src/components/CharacterLimitMainText/CharacterLimit.types.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | export type ModuleProps = {
4 | maxLength: number;
5 | svg: ReactNode;
6 | value: string;
7 | };
8 |
--------------------------------------------------------------------------------
/src/components/Checkbox/Checkbox.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, RenderResult, screen } from '@testing-library/react';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import { Checkbox } from './Checkbox';
5 |
6 | import type { CheckboxProps } from './Checkbox.types';
7 |
8 | const makeSut = (props: Partial): RenderResult =>
9 | render(checkbox);
10 |
11 | describe('Checkbox', () => {
12 | describe('when is mounted', () => {
13 | it('render a checkbox', async () => {
14 | makeSut({});
15 |
16 | const input = screen.getByRole('checkbox');
17 |
18 | await userEvent.click(input);
19 |
20 | expect(input).toBeInTheDocument();
21 | });
22 | });
23 |
24 | describe('when be clicked', () => {
25 | it('call onChange', async () => {
26 | const onChange = vi.fn();
27 |
28 | makeSut({ onChange });
29 |
30 | const input = screen.getByRole('checkbox');
31 |
32 | await userEvent.click(input);
33 |
34 | expect(onChange).toHaveBeenCalledWith(true);
35 | });
36 |
37 | it('be checked', async () => {
38 | makeSut({});
39 |
40 | const input = screen.getByRole('checkbox');
41 |
42 | await userEvent.click(input);
43 |
44 | expect(input).toBeChecked();
45 | });
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/src/components/Checkbox/Checkbox.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Story, StoryDefault } from '@ladle/react';
2 |
3 | import { Checkbox } from './Checkbox';
4 |
5 | import { CheckboxProps } from './Checkbox.types';
6 |
7 | export default {
8 | title: 'Checkbox',
9 | } satisfies StoryDefault;
10 |
11 | export const UncontrolledCheckbox: Story = (props) => (
12 |
13 | );
14 | export const ControlledCheckbox: Story = (props) => {
15 | const test = false;
16 |
17 | return ;
18 | };
19 |
20 | UncontrolledCheckbox.args = {
21 | children: 'Hello',
22 | };
23 |
24 | ControlledCheckbox.args = {
25 | children: 'Hello',
26 | };
27 |
--------------------------------------------------------------------------------
/src/components/Checkbox/Checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, forwardRef, useState } from 'react';
2 |
3 | import classNames from 'classnames';
4 |
5 | import scss from './Checkbox.module.scss';
6 |
7 | import { CheckboxProps } from './Checkbox.types';
8 |
9 | export const Checkbox = forwardRef(
10 | ({ children, ...props }, ref) => {
11 | const [isChecked, setIsChecked] = useState(false);
12 | const handleChange = (event: ChangeEvent): void => {
13 | if (props.onChange) props.onChange(event.target.checked);
14 | setIsChecked(event.target.checked);
15 | };
16 | const value = props.checked ?? isChecked;
17 |
18 | return (
19 |
30 | );
31 | }
32 | );
33 |
--------------------------------------------------------------------------------
/src/components/Checkbox/Checkbox.types.ts:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithRef } from 'react';
2 |
3 | type CheckboxBaseProps = Omit, 'onChange'>;
4 |
5 | export type CheckboxProps = CheckboxBaseProps & {
6 | checked?: boolean;
7 | children?: string;
8 | onChange?: (checked: boolean) => void;
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/Checkbox/assets/check.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/FeedbackError/FeedbackError.data.ts:
--------------------------------------------------------------------------------
1 | export const animationVariants = {
2 | hidden: {
3 | opacity: 0,
4 | transition: {
5 | type: 'tween',
6 | },
7 | y: -100,
8 | },
9 | visible: {
10 | opacity: 1,
11 | transition: {
12 | type: 'tween',
13 | },
14 | y: 0,
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/FeedbackError/FeedbackError.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import * as useErrorHook from '~stores/useErrorStore/useErrorStore';
5 |
6 | import FeedbackError from './FeedbackError';
7 |
8 | describe('FeedbackError', () => {
9 | const useErrorSpy = vi.spyOn(useErrorHook, 'useError');
10 |
11 | it('dont render the component if theres no errors on the errors array', () => {
12 | useErrorSpy.mockReturnValue({ errors: {} });
13 |
14 | render();
15 |
16 | const feedbackError = screen.queryByText(
17 | 'Failed to progress, please click on the button on the side to see the errors'
18 | );
19 | expect(feedbackError).not.toBeInTheDocument();
20 | });
21 |
22 | it('renders the component if there is at least one error on errors object', async () => {
23 | useErrorSpy.mockReturnValue({
24 | errors: {
25 | 'some-id': { id: 'some-id', message: 'Test error message' },
26 | },
27 | });
28 |
29 | render();
30 | const dropDownButton = screen.getByRole('button');
31 |
32 | await userEvent.click(dropDownButton);
33 |
34 | const errorEvidence = screen.getByText(/Test error message/);
35 | expect(errorEvidence).toBeInTheDocument();
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/src/components/FeedbackError/FeedbackError.stories.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | import type { Story } from '@ladle/react';
4 |
5 | import { useError } from '~stores/useErrorStore/useErrorStore';
6 |
7 | import FeedbackError from './FeedbackError';
8 |
9 | import { TFeedbackErrorProps } from './FeedbackError.type';
10 |
11 | export const FeedbackErrorComponent: Story = (props) => {
12 | const { addError, removeError } = useError();
13 |
14 | useEffect(() => {
15 | const newErrorIds = props.errors.map((error) => addError(error));
16 |
17 | return (): void => {
18 | for (const errorId of newErrorIds) removeError(errorId);
19 | };
20 | }, []);
21 |
22 | return ;
23 | };
24 |
25 | FeedbackErrorComponent.args = {
26 | errors: [
27 | { id: 'another-id', message: 'First generic error message' },
28 | { id: 'some-id', message: 'Second generic error message' },
29 | ],
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/FeedbackError/FeedbackError.type.ts:
--------------------------------------------------------------------------------
1 | export type TFeedbackErrorProps = {
2 | errors: { id: string; message: string }[];
3 | };
4 |
--------------------------------------------------------------------------------
/src/components/FeedbackError/components/FeedbackErrorMobile.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/global.scss' as *;
2 |
3 | .errorContainer {
4 | width: 100%;
5 |
6 | display: flex;
7 | gap: 1.125rem;
8 |
9 | align-items: center;
10 |
11 | padding-left: 0.875rem;
12 |
13 | background: $secondaryRed;
14 |
15 | cursor: pointer;
16 |
17 | .alertIcon {
18 | color: $primaryWhite;
19 | }
20 |
21 | .errorMessage {
22 | color: $primaryWhite;
23 | line-height: 2;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/FeedbackError/components/FeedbackErrorMobile.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 |
3 | import * as useErrorHook from '~stores/useErrorStore/useErrorStore';
4 |
5 | import FeedbackErrorMobile from './FeedbackErrorMobile';
6 |
7 | describe('FeedbackError', () => {
8 | const useErrorSpy = vi.spyOn(useErrorHook, 'useError');
9 |
10 | it('dont render the component if theres no errors on the errors object', () => {
11 | useErrorSpy.mockReturnValue({ errors: {} });
12 |
13 | render();
14 |
15 | const errorToast = screen.queryByText(
16 | 'Algumas publicações estão com erros, corrija-os para prosseguir. Clique aqui para mais detalhes.'
17 | );
18 |
19 | expect(errorToast).not.toBeInTheDocument();
20 | });
21 |
22 | it('renders the component if there is at least one error on errors object', () => {
23 | useErrorSpy.mockReturnValue({
24 | errors: {
25 | 'some-id': { id: 'some-id', message: 'Test error message' },
26 | },
27 | });
28 |
29 | render();
30 |
31 | const errorToast = screen.getByRole('button');
32 |
33 | expect(errorToast).toBeInTheDocument();
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/components/FeedbackError/components/FeedbackErrorMobile.stories.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | import type { Story } from '@ladle/react';
4 |
5 | import { useError } from '~stores/useErrorStore/useErrorStore';
6 |
7 | import FeedbackErrorMobile from './FeedbackErrorMobile';
8 |
9 | export const FeedbackErrorComponent: Story = () => {
10 | const { addError, removeError } = useError();
11 |
12 | useEffect(() => {
13 | const newErrorId = addError({ message: 'error message' });
14 |
15 | return (): void => {
16 | removeError(newErrorId);
17 | };
18 | }, []);
19 |
20 | return ;
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/FeedbackError/components/FeedbackErrorMobile.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import isEmpty from 'lodash.isempty';
4 |
5 | import { useError } from '~stores/useErrorStore/useErrorStore';
6 |
7 | import Icon from '~components/Icon/Icon';
8 |
9 | import scss from './FeedbackErrorMobile.module.scss';
10 |
11 | function FeedbackErrorMobile(): ReactNode {
12 | const { errors } = useError();
13 |
14 | const renderErrorMobile = (): ReactNode => (
15 |
24 | );
25 |
26 | return !isEmpty(errors) && renderErrorMobile();
27 | }
28 |
29 | export default FeedbackErrorMobile;
30 |
--------------------------------------------------------------------------------
/src/components/Icon/Icon.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, RenderResult, screen } from '@testing-library/react';
2 |
3 | import Icon from './Icon';
4 |
5 | import { IconProps } from './Icon.types';
6 |
7 | const makeSut = ({
8 | icon = 'alert',
9 | ...props
10 | }: Partial): RenderResult => render();
11 |
12 | describe('Icon', () => {
13 | describe('when is mounted', () => {
14 | it('render an image', () => {
15 | makeSut({});
16 | const icon = screen.getByRole('img');
17 | expect(icon).toBeInTheDocument();
18 | });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/components/Icon/Icon.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import omit from 'lodash.omit';
4 |
5 | import { icons } from './data';
6 |
7 | import { IconProps } from './Icon.types';
8 |
9 | function Icon(props: IconProps): ReactNode {
10 | const Component = icons[props.icon];
11 |
12 | return (
13 |
19 | );
20 | }
21 |
22 | export default Icon;
23 |
--------------------------------------------------------------------------------
/src/components/Icon/Icon.types.ts:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithoutRef } from 'react';
2 |
3 | import { icons } from './data';
4 |
5 | export type IconsType = keyof typeof icons;
6 |
7 | export type IconProps = ComponentPropsWithoutRef<'svg'> & {
8 | icon: IconsType;
9 | size?: number;
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/Icon/icon.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import Icon from './Icon';
4 |
5 | import { IconProps, IconsType } from './Icon.types';
6 |
7 | export default {
8 | argTypes: {
9 | color: {
10 | control: { type: 'select' },
11 | options: ['black', 'purple', 'red', 'blue', 'violet', 'cyan'],
12 | },
13 | icon: {
14 | control: { type: 'select' },
15 | options: [
16 | 'left-arrow',
17 | 'right-arrow',
18 | 'check',
19 | 'hamburguer',
20 | 'close',
21 | 'plus',
22 | 'small-circle-filled',
23 | 'mag',
24 | 'alert',
25 | 'star',
26 | 'star-filled',
27 | 'minus',
28 | 'facebook',
29 | 'instagram',
30 | 'tiktok',
31 | 'discord',
32 | 'twitter',
33 | ] as IconsType[],
34 | },
35 | size: {
36 | control: {
37 | max: 100,
38 | min: 0,
39 | step: 1,
40 | type: 'range',
41 | },
42 | },
43 | },
44 | component: Icon,
45 | title: 'Icon',
46 | };
47 |
48 | const Template: Story = (args) => ;
49 |
50 | export const DefaultIconStory: Story = Template.bind({});
51 | DefaultIconStory.args = {
52 | color: 'black',
53 | icon: 'left-arrow',
54 | size: 24,
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/Mobile.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/Tablet.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/TriangleLeftArrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/alert.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/back-track.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/backspace.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/blind-eye.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/bus.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/calendar.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/car.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/check-filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/check.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/checkbox-checked-filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/checkbox-checked.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/circle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/close.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/discord.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/drop-down.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/emoji.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/facebook.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/gif.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/hamburguer.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/hexagon-filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/hexagon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/horizontal-dots.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-13.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-15.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-16.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-17.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-18.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-19.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-20.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-29.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-30.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-33.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-34.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-35.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-36.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-37.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-41.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-42.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-45.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-46.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-47.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-48.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-49.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-50.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-51.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-55.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/icon-59.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/keyboard.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/left-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/letter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/mag.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/mail.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/markdown.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/mic.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/minus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/mobile.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/moon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/options-filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/pencil.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/pentagon-filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/pentagon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/play.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/plus.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/right-arrow.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/scissors.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/share.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/small-circle-filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/small-circle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/square-filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/square.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/star-filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/star.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/sun.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/taxi.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/tiktok.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/timer.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/train.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/translate.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/trash.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/triangle-filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/triangle.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/vertical-dots.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Icon/icons/world.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/InputSearch/InputSearch.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import InputSearch from './InputSearch';
4 |
5 | import { TInputProps } from './InputSearch.types';
6 |
7 | export default {
8 | component: InputSearch,
9 | title: 'InputSearch',
10 | };
11 |
12 | const Template: Story = (args) => (
13 |
14 | );
15 |
16 | export const InputSearchComponent = Template.bind({});
17 | InputSearchComponent.args = {
18 | error: false,
19 | errorMessage: 'Erro no campo de entrada',
20 | name: 'Input Search',
21 | placeholder: 'Search Social Media',
22 | required: true,
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/InputSearch/InputSearch.types.ts:
--------------------------------------------------------------------------------
1 | import { ForwardedRef, HTMLProps } from 'react';
2 |
3 | type HtmlInputProps = Omit, 'onChange'>;
4 |
5 | export type TInputProps = HtmlInputProps & {
6 | error?: boolean;
7 | errorMessage?: string;
8 | onChange?: (value: string) => void;
9 | };
10 |
11 | export type TInputComponent = {
12 | clearInput: () => void;
13 | };
14 |
15 | export type TInputComponentRef = ForwardedRef;
16 |
--------------------------------------------------------------------------------
/src/components/InputSearch/assets/alertIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/InputSearch/assets/leftIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/InputSearch/assets/rightIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/MainComposer/MainComposer.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/global.scss';
2 | @use '~styles/breakpoints.scss';
3 |
4 | .container {
5 | min-height: 40rem;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/MainComposer/MainComposer.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 |
3 | import MainComposer from './MainComposer';
4 |
5 | describe('MainComposer', () => {
6 | describe('when component loads', () => {
7 | it('MediaInputs must be visible when component starts', () => {
8 | render();
9 |
10 | const mediaInputs = screen.getByTestId('manyMediaInputs');
11 | expect(mediaInputs).toBeInTheDocument();
12 | });
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/src/components/MainComposer/MainComposer.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import MainComposer from './MainComposer';
4 |
5 | export const MainComposerStories: Story = (props) => (
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/src/components/MainComposer/MainComposer.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode, useState } from 'react';
2 |
3 | import AccordionTab from '~components/AccordionTab/AccordionTab';
4 | import '~i18n';
5 |
6 | import MainComposerBase from './components/MainComposerBase/MainComposerBase';
7 |
8 | import scss from './MainComposer.module.scss';
9 |
10 | function MainComposer(): ReactNode {
11 | const [isOpen, setIsOpen] = useState(true);
12 | return (
13 |
19 |
20 |
21 | );
22 | }
23 |
24 | export default MainComposer;
25 |
--------------------------------------------------------------------------------
/src/components/MainComposer/MainComposer.type.ts:
--------------------------------------------------------------------------------
1 | import { ChangeEvent } from 'react';
2 |
3 | import { PostMode } from '~services/api/social-media/social-media.types';
4 |
5 | export type TMainComposer = {
6 | onChange?: (e: ChangeEvent) => void;
7 | postMode?: PostMode;
8 | value?: string;
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/ComposerEditor/ComposeEditor.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import ComposerEditor from './ComposerEditor';
5 |
6 | describe('ComposeEditor', () => {
7 | describe('when render component', () => {
8 | it('should mount a textbox', () => {
9 | render();
10 |
11 | const textArea = screen.getByRole('textbox');
12 |
13 | expect(textArea).toBeInTheDocument();
14 | });
15 | });
16 |
17 | describe('When user type', () => {
18 | it('update input value', async () => {
19 | const mockOnChange = vi.fn();
20 | render();
21 |
22 | const inputElement = screen.getByRole('textbox');
23 | const testInputValue = 'Testing TextArea input';
24 |
25 | await userEvent.type(inputElement, testInputValue);
26 |
27 | expect(inputElement).toHaveValue(testInputValue);
28 | });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/ComposerEditor/ComposerEditor.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/global.scss';
2 |
3 | .inputContainer {
4 | width: 100%;
5 |
6 | display: flex;
7 | flex-direction: column;
8 | flex-grow: 1;
9 | }
10 |
11 | textarea {
12 | flex-grow: 1;
13 |
14 | color: global.$tertiaryGray;
15 |
16 | padding: 2.4rem 1.6rem 2.6rem;
17 |
18 | border: 0;
19 |
20 | background-color: global.$primaryWhite;
21 |
22 | resize: none;
23 | }
24 |
25 | .charactersLimitContainer {
26 | display: flex;
27 | flex-wrap: wrap;
28 |
29 | justify-content: space-between;
30 |
31 | padding: 1.7rem;
32 | }
33 |
34 | .characterLimitWrapper {
35 | display: flex;
36 | flex-wrap: wrap;
37 | gap: 0.8rem;
38 |
39 | align-items: center;
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/ComposerEditor/ComposerEditor.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { StoryDefault } from '@ladle/react';
2 | import { Story } from '@ladle/react';
3 |
4 | import ComposerEditor from './ComposerEditor';
5 |
6 | import { ComposerEditorProps } from './ComposerEditor.types';
7 |
8 | export default {
9 | title: 'Composer Editor',
10 | } satisfies StoryDefault;
11 |
12 | export const ComposerEditorStories: Story = (props) => (
13 |
14 | );
15 |
16 | ComposerEditorStories.args = {
17 | value: '',
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/ComposerEditor/ComposerEditor.types.ts:
--------------------------------------------------------------------------------
1 | import { ChangeEvent } from 'react';
2 |
3 | import { MainComposerChildrens } from '../MainComposerBase/MainComposerBase.type';
4 |
5 | export type InputChange = (event: ChangeEvent) => void;
6 |
7 | export type ComposerEditorProps = MainComposerChildrens & {
8 | maxCharacters?: number;
9 | onChange?: (e: ChangeEvent) => void;
10 | value?: string;
11 | };
12 |
13 | export type HigherLimitSocial = {
14 | limit: number;
15 | socialMediaId: string;
16 | };
17 |
18 | export enum TEXT_ERRORS {
19 | MAX_LENGTH_EXCEED = 1,
20 | }
21 |
22 | export type TextErrorMap = Partial>;
23 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/ComposerEditor/utils/textValidator/textValidator.spec.ts:
--------------------------------------------------------------------------------
1 | import { TextValidators } from './textValidators';
2 |
3 | describe('check texts', () => {
4 | it('limit text length', () => {
5 | const textValidator = new TextValidators('Good Morning');
6 | const limiteLength = 2000;
7 |
8 | expect(textValidator.textLength(limiteLength)).toBe(true);
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/ComposerEditor/utils/textValidator/textValidator.ts:
--------------------------------------------------------------------------------
1 | import { TextValidators } from './textValidators';
2 |
3 | import { ComposerEditorProps, TEXT_ERRORS } from '../../ComposerEditor.types';
4 | import {
5 | Payload,
6 | Validator,
7 | ValidatorError,
8 | Validators,
9 | } from './textValidator.types';
10 |
11 | export const textValidator = ({
12 | text,
13 | validatorRules,
14 | }: Validator): Validators => {
15 | const textValidators = new TextValidators(text);
16 |
17 | return {
18 | textLength: (props: ComposerEditorProps): ValidatorError => {
19 | const isTextTooLong = !textValidators.textLength(
20 | validatorRules.maxLength
21 | );
22 |
23 | const payload: Payload = {
24 | type: TEXT_ERRORS.MAX_LENGTH_EXCEED,
25 | };
26 | if (isTextTooLong && props.accountId && props.postModeId) {
27 | payload.error = {
28 | accountId: props.accountId,
29 | message: `Account ${props.accountId} on ${props.postModeId} type of post overflowed the character limit`,
30 | postModeId: props.postModeId,
31 | };
32 | }
33 | return payload;
34 | },
35 | };
36 | };
37 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/ComposerEditor/utils/textValidator/textValidator.types.ts:
--------------------------------------------------------------------------------
1 | import { TextValidator } from '~services/api/social-media/social-media.types';
2 |
3 | import { TextValidators } from './textValidators';
4 |
5 | import { Error } from '~components/MainComposer/components/MainComposerBase/MainComposerBase.type';
6 |
7 | import { ComposerEditorProps, TEXT_ERRORS } from '../../ComposerEditor.types';
8 |
9 | export type Validators = Record<
10 | keyof typeof TextValidators.prototype,
11 | (props: ComposerEditorProps) => ValidatorError
12 | >;
13 |
14 | export type Validator = {
15 | text: string;
16 | validatorRules: TextValidator;
17 | };
18 |
19 | export type ValidatorError = {
20 | error?: Error;
21 | type: TEXT_ERRORS;
22 | };
23 |
24 | export type Payload = {
25 | error?: Error;
26 | type: TEXT_ERRORS;
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/ComposerEditor/utils/textValidator/textValidators.ts:
--------------------------------------------------------------------------------
1 | export class TextValidators {
2 | private readonly text: string;
3 |
4 | constructor(text: string) {
5 | this.text = text;
6 | }
7 |
8 | public textLength = (limitLength: number): boolean =>
9 | this.text.length <= limitLength;
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/InputMediaGroup.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import InputMediaGroup from './InputMediaGroup';
5 |
6 | global.URL.createObjectURL = (): string => '';
7 |
8 | describe('InputMediaGroup', () => {
9 | it('renders the media inputs', async () => {
10 | const user = userEvent.setup();
11 | render();
12 |
13 | const file = new File(['hello'], 'hello.png', { type: 'image/png' });
14 | const changeFile = new File(['world'], 'world.png', { type: 'image/png' });
15 |
16 | const input = screen.getByLabelText('Upload media files');
17 | await user.upload(input, file);
18 |
19 | const changeInput = await screen.findAllByLabelText('Upload media files');
20 | await user.upload(changeInput[0], changeFile);
21 |
22 | const removeImage = await screen.findByRole('button', { name: /close/i });
23 | await user.click(removeImage);
24 |
25 | expect(removeImage).not.toBeInTheDocument();
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/InputMediaGroup.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Story } from '@ladle/react';
2 |
3 | import InputMediaGroup from './InputMediaGroup';
4 |
5 | export const ManyMediaInputsComponent: Story = () => ;
6 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/InputMediaGroup.type.ts:
--------------------------------------------------------------------------------
1 | import { MainComposerChildrens } from '../MainComposerBase/MainComposerBase.type';
2 |
3 | export type MediaInputProps = MainComposerChildrens;
4 |
5 | export enum MEDIA_ERRORS {
6 | MAX_AR_EXCEED = 1,
7 | MAX_DURATION_EXCEED = 2,
8 | MAX_RESOLUTION_EXCEED = 3,
9 | MAX_SIZE_EXCEED = 4,
10 | }
11 |
12 | export type ErrorMap = Record;
13 |
14 | export type MediaErrorMap = Record;
15 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/components/InputMedia/InputMedia.mock.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import InputMedia from './InputMedia';
4 |
5 | import { IInputMediaTestWrapper } from './InputMedia.types';
6 |
7 | /*
8 | *playwright's tests runs on the server and in a real browser,
9 | *because of that he can only pass as props primitives types.
10 | *This wrapper takes the file's name and passes to the component so it can be used on test*
11 | *github.com/microsoft/playwright/issues/27439
12 | */
13 |
14 | export function InputMediaForTest(props: IInputMediaTestWrapper): ReactNode {
15 | return (
16 | {
18 | props.onChange(media[0].file.name);
19 | }}
20 | />
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/components/InputMedia/InputMedia.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/global.scss';
2 |
3 | .button,
4 | .buttonSelected {
5 | height: 100%;
6 | overflow: hidden;
7 |
8 | display: flex;
9 |
10 | align-items: center;
11 | justify-content: center;
12 |
13 | padding: 5.2rem;
14 |
15 | position: relative;
16 |
17 | transition: color 0.3s ease;
18 |
19 | cursor: pointer;
20 |
21 | &:hover {
22 | background-color: global.$pressing;
23 | }
24 | }
25 |
26 | .button {
27 | border: 2px dotted global.$baseColor;
28 | }
29 |
30 | .buttonSelected {
31 | width: 100%;
32 |
33 | border: 2px solid global.$tertiaryPurple;
34 | }
35 |
36 | .hidden {
37 | display: none;
38 | }
39 |
40 | .imagePlaceholder,
41 | .imageSelected {
42 | max-width: 100%;
43 | max-height: 100%;
44 |
45 | position: absolute;
46 | object-fit: cover;
47 | }
48 |
49 | .imageSelected {
50 | overflow: hidden;
51 | aspect-ratio: 4 / 4;
52 |
53 | transition:
54 | filter 0.3s ease,
55 | transform 0.3s ease;
56 |
57 | &:hover {
58 | transform: scale(1.1);
59 | filter: brightness(70%);
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/components/InputMedia/InputMedia.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Story } from '@ladle/react';
2 |
3 | import InputMedia from './InputMedia';
4 |
5 | import { InputMediaProps } from './InputMedia.types';
6 |
7 | export const InputMediaComponent: Story = (props) => (
8 |
9 | );
10 |
11 | InputMediaComponent.args = {
12 | onChange: (media): void => alert(`image selected ${media[0].file.name}`),
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/components/InputMedia/InputMedia.types.ts:
--------------------------------------------------------------------------------
1 | export type Media = {
2 | file: File;
3 | id: string;
4 | };
5 |
6 | export type InputMediaProps = {
7 | files?: File;
8 | onChange: (media: Media[]) => void;
9 | };
10 |
11 | export type IInputMediaTestWrapper = {
12 | onChange: (media: string) => void;
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/components/MediaPreview/MediaPreview.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import Icon from '~components/Icon/Icon';
4 |
5 | import InputMedia from '../InputMedia/InputMedia';
6 |
7 | import scss from '../../InputMediaGroup.module.scss';
8 |
9 | import { MediaPreviewProps } from './MediaPreview.types';
10 |
11 | function MediaPreview({
12 | media,
13 | onImageChange,
14 | onRemove,
15 | }: MediaPreviewProps): ReactNode {
16 | return (
17 |
18 |
19 | onImageChange(newMedias, media.id)}
22 | />
23 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default MediaPreview;
37 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/components/MediaPreview/MediaPreview.types.ts:
--------------------------------------------------------------------------------
1 | import { Media } from '~components/MainComposer/components/InputMediaGroup/components/InputMedia/InputMedia.types';
2 |
3 | export type MediaPreviewProps = {
4 | media: Media;
5 | onImageChange: (newMedias: Media[], id: Media['id']) => void;
6 | onRemove: (id: Media['id']) => void;
7 | };
8 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/utils/fileValidator/fileValidator.types.ts:
--------------------------------------------------------------------------------
1 | import { MediaValidator } from '~services/api/social-media/social-media.types';
2 |
3 | import { MediaValidators } from '../mediaValidator/mediaValidators';
4 |
5 | import { Error } from '~components/MainComposer/components/MainComposerBase/MainComposerBase.type';
6 |
7 | import { MEDIA_ERRORS, MediaInputProps } from '../../InputMediaGroup.type';
8 |
9 | export type ValidatorError = {
10 | error?: Error;
11 | type: MEDIA_ERRORS;
12 | };
13 |
14 | export type Validators = Record<
15 | keyof typeof MediaValidators.prototype,
16 | (
17 | props: MediaInputProps,
18 | fileId: string
19 | ) => Promise | ValidatorError
20 | >;
21 |
22 | export type Validator = {
23 | media: File;
24 | validatorRules: MediaValidator;
25 | };
26 |
27 | export type Payload = {
28 | error?: Error;
29 | type: MEDIA_ERRORS;
30 | };
31 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/utils/mediaValidator/imageValidator/imageData.type.ts:
--------------------------------------------------------------------------------
1 | export type ImageData = {
2 | height: number;
3 | width: number;
4 | };
5 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/utils/mediaValidator/interfaces/fileValidator.types.ts:
--------------------------------------------------------------------------------
1 | export type FileValidator = {
2 | aspectRatio: (limitAspectRatio: string) => Promise;
3 | resolution: (limitWidth: number, limitHeight: number) => Promise;
4 | size: (limitSize: number) => boolean;
5 | };
6 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/utils/mediaValidator/mediaValidators.ts:
--------------------------------------------------------------------------------
1 | import { ImageValidator } from './imageValidator/imageValidator';
2 | import { VideoValidator } from './videoValidator/videoValidator';
3 |
4 | import { FileValidator } from './interfaces/fileValidator.types';
5 |
6 | export class MediaValidators implements FileValidator {
7 | private readonly media: ImageValidator | VideoValidator;
8 |
9 | constructor(file: File) {
10 | this.media = file.type.includes('video')
11 | ? new VideoValidator(file)
12 | : new ImageValidator(file);
13 | }
14 |
15 | public size = (limitSize: number): boolean => this.media.size(limitSize);
16 |
17 | public resolution = async (
18 | limitWidth: number,
19 | limitHeight: number
20 | ): Promise => this.media.resolution(limitWidth, limitHeight);
21 |
22 | public aspectRatio = async (limitAspectRatio: string): Promise =>
23 | this.media.aspectRatio(limitAspectRatio);
24 |
25 | public duration = async (
26 | limitDuration: number
27 | ): Promise => {
28 | if (this.media instanceof VideoValidator) {
29 | return this.media.duration(limitDuration);
30 | }
31 | return true;
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/utils/mediaValidator/utils/getAspectRatio.ts:
--------------------------------------------------------------------------------
1 | export function getAspectRatio(aspectRatio: string): number {
2 | const dimensions = aspectRatio.split(/:|x/);
3 | const widthAspectRatio = Number(dimensions[0]);
4 | const heightAspectRatio = Number(dimensions[1]);
5 |
6 | return widthAspectRatio / heightAspectRatio;
7 | }
8 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/InputMediaGroup/utils/mediaValidator/videoValidator/videoData.type.ts:
--------------------------------------------------------------------------------
1 | export type VideoData = {
2 | duration: number;
3 | height: number;
4 | width: number;
5 | };
6 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/MainComposerBase/MainComposerBase.components.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import InputMediaGroup from '../InputMediaGroup/InputMediaGroup';
4 |
5 | import { PostModeInputMediaGroupProps } from './MainComposerBase.type';
6 |
7 | export function PostModeInputMediaGroup(
8 | props: PostModeInputMediaGroupProps
9 | ): ReactNode {
10 | return (
11 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/MainComposerBase/MainComposerBase.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/global.scss';
2 | @use '~styles/breakpoints.scss';
3 |
4 | .container {
5 | display: flex;
6 | flex-direction: column;
7 | flex-grow: 1;
8 |
9 | .bottomWrapper {
10 | padding: 1.6rem;
11 | padding-top: 0;
12 |
13 | .divider {
14 | width: 100%;
15 | height: 1px;
16 |
17 | margin: 1.6rem 0;
18 | border: 0;
19 |
20 | background-color: global.$primaryGray;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/MainComposer/components/MainComposerBase/MainComposerBase.type.ts:
--------------------------------------------------------------------------------
1 | import { ChangeEvent } from 'react';
2 |
3 | import {
4 | PostMode,
5 | SocialMedia,
6 | } from '~services/api/social-media/social-media.types';
7 |
8 | export type MainComposerChildrens = {
9 | accountId?: string;
10 | addError?: MainComposerErrorEmiter;
11 | postModeId?: PostMode['id'];
12 | removeError?: (id: string) => void;
13 | socialMediaId?: SocialMedia['id'];
14 | };
15 |
16 | export type Error = {
17 | accountId: string;
18 | fileId?: string;
19 | message: string;
20 | postModeId: string;
21 | };
22 |
23 | export type MainComposerBaseProps = {
24 | accountId?: string;
25 | maxCharacters?: number;
26 | onChange?: (e: ChangeEvent) => void;
27 | onError?: (hasError: boolean) => void;
28 | postModeId?: PostMode['id'];
29 | socialMediaId?: SocialMedia['id'];
30 | value?: string;
31 | };
32 |
33 | export type MainComposerErrorEmiter = (key: string, error: Error) => void;
34 |
35 | export type PostModeInputMediaGroupProps = {
36 | accountId?: string;
37 | addError: (id: string, error: Error) => void;
38 | errorRemover: (id: string) => void;
39 | postModeId?: PostMode['id'];
40 | socialMediaId?: SocialMedia['id'];
41 | };
42 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 |
3 | import Modal from './Modal';
4 |
5 | describe('Modal', () => {
6 | describe('when [isOpen] is true', () => {
7 | it('renders children', () => {
8 | render(content);
9 |
10 | const content = screen.getByText('content');
11 |
12 | expect(content).toBeInTheDocument();
13 | });
14 | });
15 |
16 | describe('when [isOpen] is false', () => {
17 | it('not renders children', () => {
18 | render(content);
19 |
20 | const titleComponent = screen.queryByText('content');
21 | expect(titleComponent).not.toBeInTheDocument();
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/components/Modal/Modal.types.ts:
--------------------------------------------------------------------------------
1 | import { MouseEventHandler, PropsWithChildren, ReactNode } from 'react';
2 |
3 | export type TModalProps = PropsWithChildren<{
4 | className?: string;
5 | footer?: ReactNode;
6 | isOpen: boolean;
7 | onClickOutside?: MouseEventHandler;
8 | title?: string;
9 | }>;
10 |
--------------------------------------------------------------------------------
/src/components/Preview/Preview.module.scss:
--------------------------------------------------------------------------------
1 | @use '../../styles/global';
2 | @use '~styles/breakpoints.scss' as *;
3 |
4 | .container {
5 | height: 69.7rem;
6 |
7 | display: none;
8 | flex-direction: column;
9 |
10 | align-items: center;
11 |
12 | word-break: break-all;
13 |
14 | padding: 4.2rem 1.6rem;
15 |
16 | background-color: global.$tertiaryWhite;
17 |
18 | border-radius: 12px;
19 | }
20 |
21 | .previewContent {
22 | width: 100%;
23 |
24 | display: flex;
25 |
26 | justify-content: end;
27 | }
28 |
29 | @include from905 {
30 | .container {
31 | display: flex;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/Preview/Preview.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 |
3 | import Preview from './Preview';
4 |
5 | describe('Preview', () => {
6 | describe('when initialized', () => {
7 | it('renders the element preview inside the container', () => {
8 | render(
9 |
10 | test
11 |
12 | );
13 |
14 | const titleComponent = screen.getByText('test');
15 | expect(titleComponent).toBeInTheDocument();
16 | });
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/src/components/Preview/Preview.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import Preview from './Preview';
4 |
5 | export const PreviewStories: Story = () => (
6 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/components/Preview/Preview.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, ReactNode } from 'react';
2 |
3 | import { PreviewModeSelector } from '~pages/home/components/Tabber/PreviewModeSelector/PreviewModeSelector';
4 |
5 | import { IconsType } from '~components/Icon/Icon.types';
6 |
7 | import scss from './Preview.module.scss';
8 |
9 | import { PreviewProps } from './Preview.types';
10 |
11 | const previewModeMockList = [
12 | {
13 | icon: 'mobile' as IconsType,
14 | id: 'mobile',
15 | name: 'mobile',
16 | },
17 | {
18 | icon: 'tablet' as IconsType,
19 | id: 'tablet',
20 | name: 'tablet',
21 | },
22 | {
23 | icon: 'pc' as IconsType,
24 | id: 'pc',
25 | name: 'pc',
26 | },
27 | ];
28 |
29 | const getTargetValue = (e: ChangeEvent): string =>
30 | e.target.value;
31 |
32 | function Preview(props: PreviewProps): ReactNode {
33 | return (
34 |
35 |
41 |
{props.children}
42 |
43 | );
44 | }
45 |
46 | export default Preview;
47 |
--------------------------------------------------------------------------------
/src/components/Preview/Preview.types.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | export type PreviewProps = {
4 | children: ReactNode;
5 | };
6 |
--------------------------------------------------------------------------------
/src/components/SentrySetup/SentrySetup.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import {
3 | createRoutesFromChildren,
4 | matchRoutes,
5 | Routes,
6 | useLocation,
7 | useNavigationType,
8 | } from 'react-router-dom';
9 |
10 | import * as Sentry from '@sentry/react';
11 |
12 | type SentryProps = {
13 | children: ReactNode;
14 | };
15 |
16 | Sentry.init({
17 | dsn: import.meta.env.REACT_APP_SENTRY_KEY,
18 | integrations: [
19 | new Sentry.BrowserTracing({
20 | routingInstrumentation: Sentry.reactRouterV6Instrumentation(
21 | React.useEffect,
22 | useLocation,
23 | useNavigationType,
24 | createRoutesFromChildren,
25 | matchRoutes
26 | ),
27 | }),
28 | ],
29 | replaysOnErrorSampleRate: 1,
30 | replaysSessionSampleRate: 0.1,
31 | tracesSampleRate: 1,
32 | });
33 |
34 | const SentrySetup = Sentry.withSentryReactRouterV6Routing(Routes);
35 |
36 | function SentryRoutes({ children }: SentryProps): ReactNode {
37 | return {children};
38 | }
39 |
40 | export default SentryRoutes;
41 |
--------------------------------------------------------------------------------
/src/components/SocialMediaList/SocialMediaList.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, within } from '@testing-library/react';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import SocialMediaList from './SocialMediaList';
5 |
6 | const mock = [
7 | { icon: , id: 1, name: 'Facebook' },
8 | { icon: , id: 2, name: 'Twitter' },
9 | { icon: , id: 3, name: 'Instagram' },
10 | ];
11 |
12 | describe('SocialMediaList', () => {
13 | it('renders empty tags placeholder when no tags are added', () => {
14 | render();
15 | const placeholderText = screen.getByText('Select Social Account');
16 | expect(placeholderText).toBeInTheDocument();
17 | });
18 |
19 | it('renders tags when the tags array is filled', () => {
20 | render();
21 |
22 | const tags = screen.getAllByTestId('tag');
23 | expect(tags).toHaveLength(mock.length);
24 | });
25 |
26 | it('should remove a tag when the remove button is clicked', async () => {
27 | render();
28 | const zero = 0;
29 |
30 | const tags = screen.getAllByTestId('tag');
31 | const removeButton = within(tags[zero]).getByRole('button');
32 | await userEvent.click(removeButton);
33 |
34 | expect(tags[zero]).not.toBeInTheDocument();
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/src/components/SocialMediaList/SocialMediaList.type.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export type ISocialMediaListProps = {
4 | tags: ISocialMedia[];
5 | };
6 |
7 | export type ISocialMedia = {
8 | icon: React.ReactElement;
9 | id: number;
10 | name: string;
11 | };
12 |
--------------------------------------------------------------------------------
/src/components/SocialMediaList/assets/plusIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/SocialMediaList/assets/xIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Switch/Switch.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Story, StoryDefault } from '@ladle/react';
2 |
3 | import { Switch } from './Switch';
4 |
5 | import { SwitchProps } from './Switch.types';
6 |
7 | export default {
8 | title: 'Switch',
9 | } satisfies StoryDefault;
10 |
11 | export const StorySwitch: Story = (props) => ;
12 |
13 | StorySwitch.args = {
14 | variant: 'default',
15 | };
16 |
17 | StorySwitch.argTypes = {
18 | variant: {
19 | control: { type: 'select' },
20 | options: ['default', 'error'],
21 | },
22 | };
23 |
--------------------------------------------------------------------------------
/src/components/Switch/Switch.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, forwardRef } from 'react';
2 |
3 | import classNames from 'classnames';
4 |
5 | import scss from './Switch.module.scss';
6 |
7 | import { SwitchProps } from './Switch.types';
8 |
9 | export const Switch = forwardRef(
10 | ({ invalid, variant = 'default', ...props }, ref) => {
11 | const handleChange = (event: ChangeEvent): void => {
12 | if (props.onChange) props.onChange(event.target.checked);
13 | };
14 |
15 | const classes = classNames(scss.input, {
16 | [scss.error]: variant === 'error',
17 | [scss.invalid]: invalid,
18 | });
19 |
20 | return (
21 |
28 | );
29 | }
30 | );
31 |
--------------------------------------------------------------------------------
/src/components/Switch/Switch.types.ts:
--------------------------------------------------------------------------------
1 | import { ComponentPropsWithRef } from 'react';
2 |
3 | export type BaseSwitchProps = Omit, 'onChange'>;
4 |
5 | export type SwitchProps = BaseSwitchProps & {
6 | checked?: boolean;
7 | invalid?: boolean;
8 | onChange?: (checked: boolean) => void;
9 | variant?: 'default' | 'error';
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/Switch/assets/checkIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/Switch/assets/disableIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/components/TextInput/TextInput.components.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import classNames from 'classnames';
4 |
5 | import Icon from '~components/Icon/Icon';
6 |
7 | import scss from './TextInput.module.scss';
8 |
9 | import { RightIconProps } from './TextInput.types';
10 |
11 | export function RightIcon(props: RightIconProps): React.JSX.Element | null {
12 | const { error, handleRightIconClick, rightIcon } = props;
13 |
14 | const rightIconClass = [scss.rightIcon];
15 |
16 | if (error) {
17 | rightIconClass.push(scss.rightIconError);
18 | }
19 |
20 | let iconElement;
21 |
22 | if (rightIcon)
23 | iconElement = (
24 |
29 | );
30 |
31 | if (error)
32 | iconElement = (
33 |
38 | );
39 |
40 | if (!iconElement) return null;
41 |
42 | return handleRightIconClick ? (
43 |
50 | ) : (
51 | iconElement
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/components/TextInput/TextInput.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import TextInput from './TextInput';
4 |
5 | import { TInputProps } from './TextInput.types';
6 |
7 | export default {
8 | component: TextInput,
9 | title: 'TextInput',
10 | };
11 |
12 | const Template: Story = (args) => (
13 |
14 | );
15 |
16 | export const InputSearchComponent = Template.bind({});
17 | InputSearchComponent.args = {
18 | error: false,
19 | name: 'Input Search',
20 | placeholder: 'Search Social Media',
21 | required: true,
22 | supportText: 'Erro no campo de entrada',
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/TextInput/TextInput.types.ts:
--------------------------------------------------------------------------------
1 | import { HTMLProps } from 'react';
2 |
3 | import { IconsType } from '~components/Icon/Icon.types';
4 |
5 | type HtmlInputProps = Omit, 'onChange'>;
6 |
7 | export type TInputProps = HtmlInputProps & {
8 | error?: boolean;
9 | handleRightIconClick?: () => void;
10 | rightIcon?: IconsType;
11 | supportText?: string;
12 | };
13 |
14 | export type RightIconProps = {
15 | error?: boolean;
16 | handleRightIconClick?: () => void;
17 | rightIcon?: IconsType;
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/TextInput/assets/alertIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/constants/social-medias.ts:
--------------------------------------------------------------------------------
1 | export const SOCIAL_MEDIAS = {
2 | INSTAGRAM: 'https://www.instagram.com/devhatt_',
3 | TIKTOK: 'https://www.tiktok.com/@devhatt_',
4 | TWITTER: 'https://twitter.com/DevHatt',
5 | };
6 |
--------------------------------------------------------------------------------
/src/enums/HttpMethods.ts:
--------------------------------------------------------------------------------
1 | export enum EHttpMethods {
2 | CONNECT = 'CONNECT',
3 | DELETE = 'DELETE',
4 | GET = 'GET',
5 | HEAD = 'HEAD',
6 | OPTIONS = 'OPTIONS',
7 | PATCH = 'PATCH',
8 | POST = 'POST',
9 | PUT = 'PUT',
10 | TRACE = 'TRACE',
11 | }
12 |
--------------------------------------------------------------------------------
/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | ///
4 |
5 | declare module '*.scss' {
6 | const content: Record;
7 | export default content;
8 | }
9 |
10 | declare module '*.png';
11 | declare module '*.jpg';
12 | declare module '*.jpeg';
13 | declare module '*.svg';
14 | declare module '*.gif';
15 |
16 | type ImportMetaEnv = {
17 | readonly REACT_APP_SENTRY_KEY: string;
18 | readonly VITE_ENVIRONMENT: string;
19 | // more env variables...
20 | };
21 |
22 | type ImportMeta = {
23 | readonly env: ImportMetaEnv;
24 | };
25 |
--------------------------------------------------------------------------------
/src/hooks/useKeyPress/useKeyPress.spec.ts:
--------------------------------------------------------------------------------
1 | import { act, renderHook } from '@testing-library/react';
2 |
3 | import useKeyPress from './useKeyPress';
4 |
5 | describe('useKeyPress', () => {
6 | let action = vi.fn();
7 | let targetKey = 'Enter';
8 |
9 | it('calls action function when target key is pressed', () => {
10 | renderHook(() => useKeyPress(targetKey, action));
11 |
12 | act(() => {
13 | const event = new KeyboardEvent('keydown', { key: targetKey });
14 | window.dispatchEvent(event);
15 | });
16 |
17 | expect(action).toHaveBeenCalled();
18 | });
19 |
20 | it('does not call action function when a different key is pressed', () => {
21 | renderHook(() => useKeyPress(targetKey, action));
22 |
23 | act(() => {
24 | const event = new KeyboardEvent('keydown', { key: 'Escape' });
25 | window.dispatchEvent(event);
26 | });
27 |
28 | expect(action).not.toHaveBeenCalled();
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/hooks/useKeyPress/useKeyPress.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect } from 'react';
2 |
3 | function useKeyPress(
4 | targetKey: string,
5 | action: (event: KeyboardEvent) => void
6 | ): void {
7 | const downHandler = useCallback(
8 | (event: KeyboardEvent) => {
9 | if (event.key === targetKey) action(event);
10 | },
11 | [action, targetKey]
12 | );
13 |
14 | useEffect(() => {
15 | window.addEventListener('keydown', downHandler);
16 | return (): void => {
17 | window.removeEventListener('keydown', downHandler);
18 | };
19 | }, [downHandler]);
20 | }
21 |
22 | export default useKeyPress;
23 |
--------------------------------------------------------------------------------
/src/i18n/index.ts:
--------------------------------------------------------------------------------
1 | import { initReactI18next } from 'react-i18next';
2 |
3 | import { use } from 'i18next';
4 |
5 | import ENUS from './locales/en/en-us.json';
6 | import PTBR from './locales/pt/pt-br.json';
7 |
8 | const resources = {
9 | 'en-us': ENUS,
10 | 'pt-BR': PTBR,
11 | };
12 | void use(initReactI18next).init({
13 | interpolation: {
14 | escapeValue: false,
15 | },
16 | lng: navigator.language,
17 | resources,
18 | });
19 |
20 | export { default } from 'i18next';
21 |
--------------------------------------------------------------------------------
/src/i18n/locales/en/en-us.json:
--------------------------------------------------------------------------------
1 | {
2 | "translation": {
3 | "We have a lot of work": "We have a lot of work"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/i18n/locales/pt/pt-br.json:
--------------------------------------------------------------------------------
1 | {
2 | "translation": {
3 | "We have a lot of work": "Nos temos muito trabalho"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 |
4 | import App from './App';
5 |
6 | import 'normalize.css';
7 |
8 | const root = ReactDOM.createRoot(
9 | document.querySelector('#root') as HTMLElement
10 | );
11 |
12 | root.render(
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/src/mocks/mockMedias/mockFiles.ts:
--------------------------------------------------------------------------------
1 | const sizeFile = 10_000;
2 |
3 | export const mockFileImage = new File(
4 | [new ArrayBuffer(sizeFile)],
5 | 'mockfile.jpg',
6 | {
7 | type: 'image/jpeg',
8 | }
9 | );
10 |
11 | export const mockFileVideo = new File(
12 | [new ArrayBuffer(sizeFile)],
13 | 'mockfile.mp4',
14 | {
15 | type: 'video/mp4',
16 | }
17 | );
18 |
--------------------------------------------------------------------------------
/src/mocks/mockMedias/mockImage.ts:
--------------------------------------------------------------------------------
1 | import { Image } from './mockMedia.type';
2 |
3 | const intervalTime = 100;
4 | const timeoutTime = 1000;
5 |
6 | export function mockImage(): Image {
7 | let img = '';
8 |
9 | return {
10 | addEventListener: (event: string, cb: () => void): void => {
11 | const intervalId = setInterval(() => {
12 | if (event === 'load' && img) {
13 | cb();
14 | clearInterval(intervalId);
15 | }
16 | }, intervalTime);
17 | },
18 | height: 900,
19 | set src(value: string) {
20 | setTimeout(() => {
21 | img = value;
22 | }, timeoutTime);
23 | },
24 | width: 1300,
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/src/mocks/mockMedias/mockMedia.type.ts:
--------------------------------------------------------------------------------
1 | export type Image = {
2 | addEventListener: (event: string, cb: () => void) => void;
3 | height: number;
4 | src: string;
5 | width: number;
6 | };
7 |
8 | export type Video = {
9 | addEventListener: (event: string, cb: () => void) => void;
10 | duration: number;
11 | src: string;
12 | videoHeight: number;
13 | videoWidth: number;
14 | };
15 |
--------------------------------------------------------------------------------
/src/mocks/mockMedias/mockVideo.ts:
--------------------------------------------------------------------------------
1 | import { Video } from './mockMedia.type';
2 |
3 | const intervalTime = 100;
4 | const timeoutTime = 1000;
5 |
6 | export function mockVideo(
7 | videoWidth: number,
8 | videoHeight: number,
9 | duration: number
10 | ) {
11 | let video = '';
12 |
13 | return (elementType: string): Video | undefined => {
14 | if (elementType === 'video') {
15 | return {
16 | addEventListener: (event: string, cb: () => void): void => {
17 | const intervalId = setInterval(() => {
18 | if (event === 'loadeddata' && video) {
19 | cb();
20 | clearInterval(intervalId);
21 | }
22 | }, intervalTime);
23 | },
24 | duration,
25 | set src(value: string) {
26 | setTimeout(() => {
27 | video = value;
28 | }, timeoutTime);
29 | },
30 | videoHeight,
31 | videoWidth,
32 | };
33 | }
34 | };
35 | }
36 |
--------------------------------------------------------------------------------
/src/pages/home/components/ActionBar/ActionBar.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import isEmpty from 'lodash.isempty';
4 |
5 | import { usePostStore } from '~stores/usePostStore/usePostStore';
6 |
7 | import Button from '~components/Button/Button';
8 | import Icon from '~components/Icon/Icon';
9 |
10 | import scss from './ActionBar.module.scss';
11 |
12 | function ActionBar(): ReactNode {
13 | const { posts } = usePostStore();
14 |
15 | return (
16 |
17 |
21 |
}
27 | />
28 |
31 |
32 | );
33 | }
34 |
35 | export default ActionBar;
36 |
--------------------------------------------------------------------------------
/src/pages/home/components/Header/Header.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import Header from './Header';
4 |
5 | export const HeaderStories: Story = () => (
6 |
7 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/pages/home/components/Header/images/tiktok.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/home/components/Header/images/twitter.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/Sidebar.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import Sidebar from './Sidebar';
4 |
5 | export const SidebarStories: Story = () => (
6 |
7 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/Sidebar.types.ts:
--------------------------------------------------------------------------------
1 | import { StoreAccount } from '~stores/useSocialMediaStore/useSocialMediaStore.types';
2 |
3 | export type FilteredAccountsProps = {
4 | socialMedia: FilteredSocialMedia[];
5 | };
6 |
7 | export type FilteredSocialMedia = {
8 | socialMediaAccounts: StoreAccount[];
9 | socialMediaId: string;
10 | };
11 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/assets/plusIcon.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/AddAccount/AddAccount.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent, ReactNode } from 'react';
2 |
3 | import { useSocialMediaStore } from '~stores/useSocialMediaStore/useSocialMediaStore';
4 |
5 | import { AddAccountProps } from './AddAccount.types';
6 |
7 | function AddAccount(props: AddAccountProps): ReactNode {
8 | const { socialMedias } = useSocialMediaStore();
9 |
10 | const handleChange = (e: ChangeEvent): void => {
11 | props.onChange(e.target.value, e);
12 | };
13 |
14 | return (
15 |
16 |
26 |
27 | );
28 | }
29 |
30 | export default AddAccount;
31 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/AddAccount/AddAccount.types.ts:
--------------------------------------------------------------------------------
1 | import { ChangeEvent } from 'react';
2 |
3 | export type AddAccountProps = {
4 | onChange: (addonId: string, event: ChangeEvent) => void;
5 | };
6 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialAccordion/SocialAccordion.components.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import scss from './SocialAccordion.module.scss';
4 |
5 | import { AccountQuantityProps } from './SocialAccordion.type';
6 |
7 | export function AccountQuantity({
8 | accountQuantity,
9 | }: AccountQuantityProps): ReactNode {
10 | return {accountQuantity}+;
11 | }
12 |
13 | export function RenderError(): ReactNode {
14 | return error!!!!;
15 | }
16 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialAccordion/SocialAccordion.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/global' as *;
2 |
3 | .wrapper {
4 | overflow: hidden;
5 |
6 | .icon {
7 | width: 24px;
8 | height: 24px;
9 | }
10 | }
11 |
12 | .header {
13 | display: flex;
14 |
15 | align-items: center;
16 | justify-content: space-between;
17 |
18 | padding: 1.5rem 0.6rem;
19 |
20 | border: 1px solid $primaryGray;
21 |
22 | cursor: pointer;
23 |
24 | .alertIcon {
25 | color: $secondaryRed;
26 | }
27 | }
28 |
29 | .socialInfo {
30 | width: 100%;
31 |
32 | overflow: hidden;
33 |
34 | display: flex;
35 |
36 | gap: 1rem;
37 |
38 | align-items: center;
39 | }
40 |
41 | .accordionInfo {
42 | display: flex;
43 |
44 | align-items: center;
45 | justify-content: flex-end;
46 | }
47 |
48 | .socialInfo {
49 | p {
50 | color: $secondaryPurple;
51 | font-size: 1.6rem;
52 | }
53 | }
54 |
55 | .accordionInfo {
56 | gap: 1rem;
57 |
58 | .accountQuantity {
59 | color: $tertiaryGray;
60 | font-size: 1.2rem;
61 | font-weight: 500;
62 | }
63 |
64 | .error {
65 | color: $secondaryRed;
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialAccordion/SocialAccordion.stories.tsx:
--------------------------------------------------------------------------------
1 | import type { Story } from '@ladle/react';
2 |
3 | import SocialAccordion from './SocialAccordion';
4 |
5 | import { SocialAccordionProps } from './SocialAccordion.type';
6 |
7 | const accounts: SocialAccordionProps['accounts'] = [
8 | {
9 | avatar: 'http://someurl.com',
10 | expiresAt: '2022-12-31T23:59:59Z',
11 | favorite: false,
12 | generatedAt: '2022-01-01T00:00:00Z',
13 | id: '21_231',
14 | socialMediaId: '123',
15 | token: 'token1',
16 | userName: 'jhon doe',
17 | valid: true,
18 | },
19 | {
20 | avatar: 'http://someurl.com',
21 | expiresAt: '2022-12-31T23:59:59Z',
22 | favorite: false,
23 | generatedAt: '2022-01-01T00:00:00Z',
24 | id: '1234',
25 | socialMediaId: '456',
26 | token: 'token2',
27 | userName: 'joão da silva',
28 | valid: false,
29 | },
30 | ];
31 |
32 | export const SocicialAccordionComponent: Story = (
33 | props
34 | ) => (
35 |
40 | );
41 |
42 | SocicialAccordionComponent.args = {
43 | accounts,
44 | error: false,
45 | };
46 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialAccordion/SocialAccordion.type.ts:
--------------------------------------------------------------------------------
1 | import { ReactElement } from 'react';
2 |
3 | import { StoreAccount } from '~stores/useSocialMediaStore/useSocialMediaStore.types';
4 |
5 | export type SocialAccordionProps = {
6 | accounts: StoreAccount[];
7 | error: boolean;
8 | icon?: ReactElement;
9 | title: string;
10 | };
11 |
12 | export type IAccountList = {
13 | id: number | string;
14 | image: string;
15 | socialMediaName?: string;
16 | username: string;
17 | };
18 |
19 | export type AccountQuantityProps = {
20 | accountQuantity: number;
21 | };
22 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialAccordion/assets/facebook.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialAccordion/assets/instagram.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialAccordion/assets/x.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialMediaForm/SocialMediaForm.components.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import Button from '~components/Button/Button.tsx';
4 |
5 | import scss from './SocialMediaForm.module.scss';
6 |
7 | import { SocialMediaFormProps } from './SocialMediaForm.types';
8 |
9 | export function ConnectAccountButton({
10 | disabled = false,
11 | onOpenModal,
12 | }: SocialMediaFormProps): ReactNode {
13 | return (
14 |
22 | );
23 | }
24 |
25 | export function ConnectGroupButton({
26 | onOpenModal,
27 | }: SocialMediaFormProps): ReactNode {
28 | return (
29 |
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialMediaForm/SocialMediaForm.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import userEvent from '@testing-library/user-event';
3 |
4 | import SocialMediaForm from './SocialMediaForm';
5 |
6 | const setIsOpenMock = (): boolean => false;
7 |
8 | describe('Social Media Form', () => {
9 | it('renders correctly', () => {
10 | const { container } = render(
11 | setIsOpenMock} />
12 | );
13 |
14 | expect(container).toBeDefined();
15 | });
16 | it('calls onClick prop when clicked', async () => {
17 | const handleClick = vi.fn();
18 |
19 | render();
20 |
21 | const [button] = screen.getAllByRole('button');
22 | await userEvent.click(button);
23 |
24 | expect(handleClick).toHaveBeenCalledTimes(1);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialMediaForm/SocialMediaForm.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import SocialMediaForm from './SocialMediaForm';
4 |
5 | export const SocialMediaFormStories: Story = () => (
6 |
7 | {
10 | throw new Error('Function not implemented.');
11 | }}
12 | />
13 |
14 | );
15 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialMediaForm/SocialMediaForm.types.ts:
--------------------------------------------------------------------------------
1 | export type SocialMediaFormProps = {
2 | disabled?: boolean;
3 | onOpenModal: () => void;
4 | };
5 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialMediaForm/data.ts:
--------------------------------------------------------------------------------
1 | import FacebookIcon from './images/facebook.svg?react';
2 | import InstagramIcon from './images/instagram.svg?react';
3 | import TiktokIcon from './images/tiktok.svg?react';
4 | import TwitterIcon from './images/twitter.svg?react';
5 |
6 | const socialMedias = [
7 | {
8 | hasAccount: true,
9 | hasGroup: true,
10 | icon: FacebookIcon, // FALTA TIPAR O ICON
11 | name: 'Facebook',
12 | },
13 | {
14 | hasAccount: false,
15 | hasGroup: true,
16 | icon: InstagramIcon,
17 | name: 'Instagram',
18 | },
19 | {
20 | hasAccount: true,
21 | hasGroup: false,
22 | icon: TwitterIcon,
23 | name: 'Twitter',
24 | },
25 | {
26 | hasAccount: true,
27 | hasGroup: false,
28 | icon: TiktokIcon,
29 | name: 'Tiktok',
30 | },
31 | {
32 | hasAccount: false,
33 | hasGroup: false,
34 | icon: FacebookIcon,
35 | name: 'LinkedIn',
36 | },
37 | ];
38 |
39 | export { socialMedias };
40 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialMediaForm/images/facebook.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialMediaForm/images/tiktok.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialMediaForm/images/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/home/components/Sidebar/components/SocialMediaForm/images/unknown.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/PostModes/PostModes.components.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import { Checkbox } from '~components/Checkbox/Checkbox';
4 |
5 | import { PostModeProps } from './PostModes.types';
6 |
7 | export function PostMode(props: PostModeProps): ReactNode {
8 | const handlePostModeClick = (postModeElement: HTMLElement): void => {
9 | props.onClickPostMode(postModeElement);
10 | };
11 |
12 | return (
13 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/PostModes/PostModes.stories.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | import { Story } from '@ladle/react';
4 |
5 | import { PostMode } from '~services/api/social-media/social-media.types';
6 |
7 | import PostModes from './PostModes';
8 |
9 | const mock = {
10 | account: {
11 | id: '1',
12 | socialMediaId: 'DISCORD_EXAMPLE_ID',
13 | },
14 | };
15 |
16 | export const PostmodesStories: Story = () => {
17 | const [postModeOnView, setPostModeOnView] = useState(
18 | 'DISCORD_STORY_POSTMODE_ID'
19 | );
20 | const changePostMode = (postModeId: PostMode['id']): void => {
21 | setPostModeOnView(postModeId);
22 | };
23 |
24 | return (
25 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/PostModes/PostModes.types.ts:
--------------------------------------------------------------------------------
1 | import { Account } from '~services/api/accounts/accounts.types';
2 | import {
3 | PostMode,
4 | SocialMedia,
5 | } from '~services/api/social-media/social-media.types';
6 | import { DataPost } from '~stores/usePostStore/usePostStore.types';
7 |
8 | export type PostModesProps = {
9 | changePostModeId: (postModeId: PostMode['id']) => void;
10 | postId: DataPost['id'];
11 | postModeId: PostMode['id'];
12 | socialMediaId: SocialMedia['id'];
13 | };
14 |
15 | export type SelectedPostMode = Partial>;
16 |
17 | export type PostModeProps = {
18 | changeCheckBox: (postModeId: PostMode['id'], isChecked: boolean) => void;
19 | changePostMode: (postModeId: PostMode['id']) => void;
20 | isChecked: (postModeId: PostMode['id']) => boolean;
21 | onClickPostMode: (tabElement: HTMLElement) => void;
22 | postMode: PostMode;
23 | postModeClasses: (postModeId: PostMode['id']) => string;
24 | };
25 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/PreviewModeSelector/PreviewModeSelector.components.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Icon from '~components/Icon/Icon';
4 |
5 | import scss from './PreviewModeSelector.module.scss';
6 |
7 | import { PreviewModeProps } from './PreviewModeSelector.types';
8 |
9 | export function PreviewMode(props: PreviewModeProps): React.JSX.Element {
10 | return (
11 |
12 |
20 |
21 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/PreviewModeSelector/PreviewModeSelector.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/global.scss' as *;
2 |
3 | .containerPreview {
4 | overflow: hidden;
5 |
6 | display: flex;
7 |
8 | align-items: center;
9 |
10 | border: 1px solid $primaryGray;
11 |
12 | background-color: $secondaryWhite;
13 |
14 | border-radius: 16px;
15 |
16 | &:focus-within,
17 | &:active {
18 | border: 1px solid $secondaryPurple;
19 | }
20 |
21 | .icon {
22 | display: flex;
23 |
24 | padding: 1.25rem 1.3rem;
25 |
26 | path {
27 | fill: $tertiaryGray;
28 | }
29 | }
30 |
31 | .previewModeInput {
32 | position: absolute;
33 |
34 | clip-path: inset(50%);
35 | }
36 |
37 | .previewModeInput:checked + label .icon {
38 | background-color: $primaryWhite;
39 |
40 | path {
41 | fill: $secondaryPurple;
42 | }
43 | }
44 |
45 | .previewModeLabel {
46 | cursor: pointer;
47 | }
48 |
49 | .previewModeLabel span {
50 | position: absolute;
51 |
52 | visibility: hidden;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/PreviewModeSelector/PreviewModeSelector.stories.tsx:
--------------------------------------------------------------------------------
1 | import { ChangeEvent } from 'react';
2 |
3 | import { Story } from '@ladle/react';
4 |
5 | import { IconsType } from '~components/Icon/Icon.types';
6 |
7 | import { PreviewModeSelector } from './PreviewModeSelector';
8 |
9 | const previewModes = [
10 | {
11 | icon: 'mobile' as IconsType,
12 | id: 'mobile',
13 | name: 'mobile',
14 | },
15 | {
16 | icon: 'pc' as IconsType,
17 | id: 'pc',
18 | name: 'pc',
19 | },
20 | {
21 | icon: 'tablet' as IconsType,
22 | id: 'tablet',
23 | name: 'tablet',
24 | },
25 | ];
26 |
27 | export const PreviewModeSelectorStories: Story = () => (
28 | ) => e.target.value}
30 | list={previewModes}
31 | />
32 | );
33 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/PreviewModeSelector/PreviewModeSelector.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import scss from './PreviewModeSelector.module.scss';
4 |
5 | import { PreviewMode } from './PreviewModeSelector.components';
6 | import { PreviewModeSelectorProps } from './PreviewModeSelector.types';
7 |
8 | export function PreviewModeSelector(
9 | props: PreviewModeSelectorProps
10 | ): React.JSX.Element {
11 | return (
12 |
13 | {props.list.map((item) => (
14 |
15 | ))}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/PreviewModeSelector/PreviewModeSelector.types.ts:
--------------------------------------------------------------------------------
1 | import { ChangeEvent } from 'react';
2 |
3 | import { IconsType } from '~components/Icon/Icon.types';
4 |
5 | export type PreviewMode = {
6 | icon: IconsType;
7 | id: string;
8 | name: string;
9 | };
10 |
11 | export type PreviewModeProps = {
12 | item: PreviewMode;
13 | onSelect: (e: ChangeEvent) => string;
14 | };
15 |
16 | export type PreviewModeSelectorProps = {
17 | changeDevice: (e: ChangeEvent) => string;
18 | list: PreviewMode[];
19 | };
20 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/Tabber.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/global';
2 | @use '~styles/breakpoints.scss' as *;
3 |
4 | .gridContainer {
5 | min-height: 25rem;
6 |
7 | align-items: center;
8 |
9 | justify-content: center;
10 |
11 | padding: 2px;
12 |
13 | background-color: global.$tertiaryWhite;
14 | }
15 |
16 | .postModesContainer {
17 | height: 100%;
18 |
19 | padding: 2.4rem 1.2rem;
20 |
21 | background-color: global.$primaryWhite;
22 | }
23 |
24 | .mainComposerContainer {
25 | min-height: 40rem;
26 |
27 | display: flex;
28 | }
29 |
30 | .previewContainer {
31 | text-align: center;
32 | }
33 |
34 | @include from905 {
35 | .gridContainer {
36 | display: grid;
37 | grid-template-columns: 1fr 1fr;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/Tabber.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import Tabber from './Tabber';
4 |
5 | export const TabberStories: Story = () => ;
6 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/Tabber.types.ts:
--------------------------------------------------------------------------------
1 | import { PostMode } from '~services/api/social-media/social-media.types';
2 | import { DataPost } from '~stores/usePostStore/usePostStore.types';
3 |
4 | export type Tab = {
5 | id: string;
6 | postId: DataPost['id'];
7 | postModeId: PostMode['id'];
8 | };
9 |
10 | export type Tabs = Record;
11 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/Tabs/Tabs.types.ts:
--------------------------------------------------------------------------------
1 | import { StoreAccount } from '~stores/useSocialMediaStore/useSocialMediaStore.types';
2 |
3 | import { Tab, Tabs } from '../Tabber.types';
4 |
5 | export type TabsProps = {
6 | currentTab: Tab;
7 | onChangeTab: (tab: Tab) => void;
8 | tabs: Tabs;
9 | };
10 |
11 | export type FoundAccount = StoreAccount | null | undefined;
12 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/Tabs/components/Tab.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import classNames from 'classnames';
4 |
5 | import scss from '../Tabs.module.scss';
6 |
7 | import { TabProps } from './Tab.types';
8 |
9 | export default function Tab(props: TabProps): ReactNode {
10 | const isActiveClasses = classNames(scss.tab, props.isActive && scss.active);
11 |
12 | const handleTabClick = (tabElement: HTMLElement): void => {
13 | props.onClickTab(tabElement);
14 | };
15 |
16 | return (
17 | handleTabClick(e.currentTarget)}
21 | >
22 | {props.socialMediaIcon}
23 | {props.title}
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/pages/home/components/Tabber/Tabs/components/Tab.types.ts:
--------------------------------------------------------------------------------
1 | import { SocialMedia } from '~services/api/social-media/social-media.types';
2 | import { StoreAccount } from '~stores/useSocialMediaStore/useSocialMediaStore.types';
3 |
4 | import { Tab } from '../../Tabber.types';
5 |
6 | export type TabProps = {
7 | id: Tab['id'];
8 | isActive: boolean;
9 | onClickTab: (tabElement: HTMLElement) => void;
10 | socialMediaIcon: SocialMedia['icon'] | undefined;
11 | title: StoreAccount['userName'];
12 | };
13 |
--------------------------------------------------------------------------------
/src/pages/home/home.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/breakpoints.scss' as *;
2 |
3 | .content {
4 | overflow: hidden;
5 |
6 | display: grid;
7 | grid-template-areas:
8 | 'header'
9 | 'actions'
10 | 'aside'
11 | 'editor';
12 |
13 | grid-template-columns: 100vw;
14 |
15 | .aside {
16 | grid-area: aside;
17 | }
18 |
19 | .editor {
20 | overflow-x: hidden;
21 |
22 | grid-area: editor;
23 | }
24 |
25 | .editor {
26 | > :first-child {
27 | display: none;
28 | }
29 | }
30 |
31 | .actions {
32 | grid-area: actions;
33 | }
34 | }
35 |
36 | @include from905 {
37 | .content {
38 | width: 90%;
39 | max-width: 128rem;
40 |
41 | grid-template-areas:
42 | 'aside editor'
43 | 'aside actions';
44 | grid-template-columns: 30rem 1fr;
45 | gap: 2.4rem;
46 |
47 | margin: 0 auto;
48 | }
49 |
50 | .content {
51 | .editor {
52 | > :first-child {
53 | display: flex;
54 | }
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/pages/home/home.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import isEmpty from 'lodash.isempty';
4 |
5 | import { useSocialMediaStore } from '~stores/useSocialMediaStore/useSocialMediaStore';
6 |
7 | import FeedbackError from '~components/FeedbackError/FeedbackError';
8 | import MainComposer from '~components/MainComposer/MainComposer';
9 |
10 | import ActionBar from './components/ActionBar/ActionBar';
11 | import Header from './components/Header/Header';
12 | import Sidebar from './components/Sidebar/Sidebar';
13 | import Tabber from './components/Tabber/Tabber';
14 |
15 | import scss from './home.module.scss';
16 |
17 | function Home(): ReactNode {
18 | const { accounts } = useSocialMediaStore();
19 |
20 | return (
21 | <>
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 | {!isEmpty(accounts.data) && }
32 |
33 |
34 |
37 |
38 | >
39 | );
40 | }
41 |
42 | export default Home;
43 |
--------------------------------------------------------------------------------
/src/pages/register/components/AlreadyHaveAnAccount/AlreadyHaveAnAccount.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/breakpoints.scss' as *;
2 | @use '~styles/global.scss';
3 |
4 | .alreadyHaveAnAccount {
5 | display: flex;
6 |
7 | justify-content: flex-end;
8 |
9 | font-size: 1.6rem;
10 |
11 | font-weight: 500;
12 |
13 | padding: 3.2rem 3.2rem 1.6rem;
14 |
15 | .text {
16 | color: global.$tertiaryGray;
17 | }
18 |
19 | .link {
20 | color: global.$secondaryPurple;
21 | text-decoration: none;
22 | }
23 | }
24 |
25 | @include from905 {
26 | .alreadyHaveAnAccount {
27 | display: none;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/pages/register/components/AlreadyHaveAnAccount/AlreadyHaveAnAccount.spec.tsx:
--------------------------------------------------------------------------------
1 | import { NavLink } from 'react-router-dom';
2 |
3 | import { render, screen } from '@testing-library/react';
4 |
5 | import AlreadyHaveAnAccount from './AlreadyHaveAnAccount';
6 |
7 | vi.mock('react-router-dom', () => ({
8 | NavLink: vi.fn(({ children }) => children),
9 | }));
10 |
11 | const NavLinkMocked = vi.mocked(NavLink);
12 |
13 | describe('alreadyHaveAnAccount component', () => {
14 | describe('show the content on screen', () => {
15 | it('renders its content', () => {
16 | render();
17 |
18 | const text = screen.getByText(/Already have an account/);
19 | const link = screen.getByText(/Sign In/);
20 |
21 | expect(text).toBeInTheDocument();
22 | expect(link).toBeInTheDocument();
23 | });
24 | it('push generate link to sign in page', () => {
25 | render();
26 |
27 | expect(NavLinkMocked).toHaveBeenCalledWith(
28 | expect.objectContaining({
29 | to: 'login',
30 | }),
31 | {}
32 | );
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/pages/register/components/AlreadyHaveAnAccount/AlreadyHaveAnAccount.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import AlreadyHaveAnAccount from './AlreadyHaveAnAccount';
4 |
5 | export const AlreadyHaveAnAccountStories: Story = () => (
6 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/pages/register/components/AlreadyHaveAnAccount/AlreadyHaveAnAccount.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import { NavLink } from 'react-router-dom';
3 |
4 | import scss from './AlreadyHaveAnAccount.module.scss';
5 |
6 | function AlreadyHaveAnAccount(): ReactNode {
7 | return (
8 |
9 |
10 | Already have an account{' '}
11 |
12 | Sign In
13 |
14 |
15 |
16 | );
17 | }
18 |
19 | export default AlreadyHaveAnAccount;
20 |
--------------------------------------------------------------------------------
/src/pages/register/components/DesktopHeader/DesktopHeader.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/breakpoints.scss' as *;
2 | @use '~styles/global.scss';
3 |
4 | .header {
5 | display: none;
6 |
7 | padding: 3.2rem 0 0 3.2rem;
8 | }
9 |
10 | @include from905 {
11 | .header {
12 | display: block;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/pages/register/components/DesktopHeader/DesktopHeader.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 |
3 | import DesktopHeader from './DesktopHeader';
4 |
5 | describe('DesktopHeader component', () => {
6 | describe('show the content on screen', () => {
7 | it('renders its content', () => {
8 | render();
9 |
10 | const logo = screen.getByLabelText('octopost logo');
11 |
12 | expect(logo).toBeInTheDocument();
13 | });
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/pages/register/components/DesktopHeader/DesktopHeader.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import DesktopHeader from './DesktopHeader';
4 |
5 | export const DesktopHeaderStories: Story = () => (
6 |
7 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/pages/register/components/DesktopHeader/DesktopHeader.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import scss from './DesktopHeader.module.scss';
4 |
5 | import OctopostLogo from './images/octopost.svg?react';
6 |
7 | function DesktopHeader(): ReactNode {
8 | return (
9 |
14 | );
15 | }
16 |
17 | export default DesktopHeader;
18 |
--------------------------------------------------------------------------------
/src/pages/register/components/DesktopHero/Hero.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import Hero from './Hero';
4 |
5 | export const DesktopHeroStories: Story = () => (
6 |
7 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/pages/register/components/DesktopHero/Hero.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 |
4 | import Button from '~components/Button/Button';
5 |
6 | import scss from './Hero.module.scss';
7 |
8 | import Octo from './images/octo.svg?react';
9 |
10 | function Hero(): ReactNode {
11 | const navigate = useNavigate();
12 |
13 | return (
14 |
15 |
16 | Welcome back
17 |
18 | To keep connected to yourself please login with your personal info
19 |
20 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export default Hero;
36 |
--------------------------------------------------------------------------------
/src/pages/register/components/Form/Form.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/breakpoints.scss' as *;
2 | @use '~styles/global.scss' as *;
3 |
4 | .form {
5 | display: flex;
6 | flex-direction: column;
7 | }
8 |
9 | .submitButton {
10 | text-align: center;
11 | font-size: 1.6rem;
12 | font-weight: 700;
13 |
14 | margin-top: 1.2rem;
15 | padding: 1.6rem;
16 | }
17 |
--------------------------------------------------------------------------------
/src/pages/register/components/Form/Form.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import Form from './Form';
4 |
5 | export const FormStories: Story = () => (
6 |
7 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/pages/register/components/Form/FormSchema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | const MIN_PASSWORD_CHARACTERS = 8;
4 |
5 | export const schema = z.object({
6 | email: z
7 | .string()
8 | .min(1, { message: 'Email is required.' })
9 | .email({ message: 'Must be a valid email.' }),
10 | password: z
11 | .string()
12 | .min(MIN_PASSWORD_CHARACTERS, {
13 | message: 'Must have 8 or more characters.',
14 | })
15 | .refine(
16 | (v) => {
17 | const hasNumber = /\d/.test(v);
18 | const hasSpecialChar = /[!"#$%&()*,.:<>?@^{|}]/.test(v);
19 |
20 | return hasNumber && hasSpecialChar;
21 | },
22 | { message: 'Must contain at least 1 number and 1 special character.' }
23 | ),
24 | });
25 |
--------------------------------------------------------------------------------
/src/pages/register/components/MobileHero/Hero.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import Hero from './Hero';
4 |
5 | export const MobileHeroStories: Story = () => (
6 |
7 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/pages/register/components/MobileHero/Hero.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 |
4 | import Button from '~components/Button/Button';
5 |
6 | import scss from './Hero.module.scss';
7 |
8 | import Octo from './images/octo.svg?react';
9 | import OctopostLogo from './images/octopost.svg?react';
10 |
11 | function Hero(): ReactNode {
12 | const navigate = useNavigate();
13 |
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
27 |
28 |
29 | New here?
30 |
31 | Welcome to Octopost. Enter your personal details and start your
32 | journey with us
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
42 | export default Hero;
43 |
--------------------------------------------------------------------------------
/src/pages/register/components/SignUpPromotion/SignUpPromotion.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/breakpoints.scss' as *;
2 | @use '~styles/global.scss' as *;
3 |
4 | .signUpPromotion {
5 | display: none;
6 |
7 | .title {
8 | color: $tertiaryGray;
9 | font-size: 4.8rem;
10 | font-weight: 700;
11 |
12 | margin: 0;
13 | }
14 |
15 | .description {
16 | color: $secondaryGray;
17 | font-size: 1.6rem;
18 | font-weight: 500;
19 | line-height: 2.4rem;
20 |
21 | margin: 0;
22 | }
23 | }
24 |
25 | @include from905 {
26 | .signUpPromotion {
27 | display: block;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/pages/register/components/SignUpPromotion/SignUpPromotion.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | import SignUpPromotion from './SignUpPromotion';
4 |
5 | export const SignUpPromotionStories: Story = () => (
6 |
7 |
8 |
9 | );
10 |
--------------------------------------------------------------------------------
/src/pages/register/components/SignUpPromotion/SignUpPromotion.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import scss from './SignUpPromotion.module.scss';
4 |
5 | function DesktopCall(): ReactNode {
6 | return (
7 |
8 | Join us right now!
9 | Register your account now!
10 |
11 | );
12 | }
13 |
14 | export default DesktopCall;
15 |
--------------------------------------------------------------------------------
/src/pages/register/components/SignUpPromotion/SignUpPromotionl.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 |
3 | import SignUpPromotion from './SignUpPromotion';
4 |
5 | describe('SignUpPromotion component', () => {
6 | describe('show the content on screen', () => {
7 | it('renders its content', () => {
8 | render();
9 |
10 | const title = screen.getByText('Join us right now!');
11 | const description = screen.getByText('Register your account now!');
12 |
13 | expect(title).toBeInTheDocument();
14 | expect(description).toBeInTheDocument();
15 | });
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/pages/register/components/SocialLogin/SocialLogin.spec.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 |
3 | import SocialLogin from './SocialLogin';
4 |
5 | describe('SocialLogin component', () => {
6 | describe('show the content on screen', () => {
7 | it('renders its content', () => {
8 | render();
9 |
10 | const title = screen.getByText('or continue with');
11 | const twitterLogo = screen.getByLabelText('twitter logo');
12 | const tiktokLogo = screen.getByLabelText('twitter logo');
13 | const instagramLogo = screen.getByLabelText('twitter logo');
14 |
15 | expect(title).toBeInTheDocument();
16 | expect(twitterLogo).toBeInTheDocument();
17 | expect(tiktokLogo).toBeInTheDocument();
18 | expect(instagramLogo).toBeInTheDocument();
19 | });
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/pages/register/components/SocialLogin/SocialLogin.stories.tsx:
--------------------------------------------------------------------------------
1 | import { Story } from '@ladle/react';
2 |
3 | export const SocialLoginStories: Story = () => (
4 |
5 |
6 |
7 | );
8 |
--------------------------------------------------------------------------------
/src/pages/register/register.module.scss:
--------------------------------------------------------------------------------
1 | @use '~styles/breakpoints.scss' as *;
2 |
3 | .wrapper {
4 | height: 100vh;
5 |
6 | display: grid;
7 | grid-template-rows: auto 1fr;
8 | grid-template-columns: 1fr;
9 | grid-auto-flow: column;
10 | }
11 |
12 | .formSection {
13 | width: 100%;
14 | height: 100%;
15 |
16 | display: flex;
17 | flex-direction: column;
18 |
19 | justify-content: space-between;
20 |
21 | .main {
22 | width: 100%;
23 | max-width: min(460px, calc(100% - 2rem));
24 |
25 | display: flex;
26 | flex-direction: column;
27 | gap: 3.2rem;
28 |
29 | margin-right: auto;
30 | margin-left: auto;
31 | padding-top: 6rem;
32 | }
33 | }
34 |
35 | @include from905 {
36 | .wrapper {
37 | height: 100vh;
38 |
39 | display: grid;
40 | grid-template-rows: 1fr;
41 | grid-template-columns: 1fr 1fr;
42 | grid-auto-flow: row;
43 | }
44 |
45 | .main {
46 | display: flex;
47 | flex-direction: column;
48 |
49 | gap: 1.6rem;
50 |
51 | justify-content: center;
52 |
53 | margin: auto;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/pages/register/register.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | import AlreadyHaveAnAccount from './components/AlreadyHaveAnAccount/AlreadyHaveAnAccount';
4 | import DesktopHeader from './components/DesktopHeader/DesktopHeader';
5 | import DesktopHero from './components/DesktopHero/Hero';
6 | import Form from './components/Form/Form';
7 | import MobileHero from './components/MobileHero/Hero';
8 | import SignUpPromotion from './components/SignUpPromotion/SignUpPromotion';
9 | import SocialLogin from './components/SocialLogin/SocialLogin';
10 |
11 | import scss from './register.module.scss';
12 |
13 | function Home(): ReactNode {
14 | return (
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
31 | export default Home;
32 |
--------------------------------------------------------------------------------
/src/services/api/accounts/accounts.ts:
--------------------------------------------------------------------------------
1 | import { octopostApi } from '..';
2 |
3 | import { Account } from './accounts.types';
4 |
5 | const AccountsService = {
6 | async favorite(
7 | accountId: Account['id'],
8 | favorite: boolean
9 | ): Promise {
10 | try {
11 | const res = await octopostApi.patch(`accounts/${accountId}`, favorite);
12 | return res.data;
13 | } catch (error) {
14 | console.error(error);
15 | }
16 | },
17 | async fetchAll(): Promise {
18 | try {
19 | const res = await octopostApi.get('/accounts');
20 | return res.data;
21 | } catch (error) {
22 | console.error(error);
23 | return [];
24 | }
25 | },
26 | };
27 |
28 | export { AccountsService };
29 |
--------------------------------------------------------------------------------
/src/services/api/accounts/accounts.types.ts:
--------------------------------------------------------------------------------
1 | export type Account = {
2 | avatar: string;
3 | expiresAt: string;
4 | favorite: boolean;
5 | generatedAt: string;
6 | id: string;
7 | socialMediaId: string;
8 | token: string;
9 | userName: string;
10 | };
11 |
--------------------------------------------------------------------------------
/src/services/api/index.ts:
--------------------------------------------------------------------------------
1 | import Axios from 'axios';
2 |
3 | const octopostApi = Axios.create({
4 | baseURL: 'http://localhost:3000',
5 | });
6 |
7 | export { octopostApi };
8 |
--------------------------------------------------------------------------------
/src/services/api/social-media/social-media.ts:
--------------------------------------------------------------------------------
1 | import { GenericObject } from '~types/object';
2 |
3 | import { octopostApi } from '..';
4 |
5 | import { Post, SocialMedia } from './social-media.types';
6 |
7 | const SocialMediaService = {
8 | async fetch(socialMedias: string[]): Promise {
9 | try {
10 | const res = await octopostApi.get('/social-medias', {
11 | params: { socialMedias },
12 | });
13 | return res.data;
14 | } catch (error) {
15 | console.error(error);
16 | return [];
17 | }
18 | },
19 |
20 | async sendPosts(posts: Post[]): Promise {
21 | try {
22 | const res = await octopostApi.post('/send-posts', { posts });
23 | return res.data;
24 | } catch (error) {
25 | console.error(error);
26 | return {};
27 | }
28 | },
29 | };
30 |
31 | export { SocialMediaService };
32 |
--------------------------------------------------------------------------------
/src/services/api/social-media/social-media.types.ts:
--------------------------------------------------------------------------------
1 | import { IconsType } from '~components/Icon/Icon.types';
2 |
3 | export type Post = {
4 | data: {
5 | text: string;
6 | };
7 | postModeId: PostMode['id'];
8 | socialMediaId: SocialMedia['id'];
9 | };
10 |
11 | export type SocialMedia = {
12 | icon: IconsType;
13 | id: string;
14 | name: string;
15 | postModes: PostMode[];
16 | };
17 |
18 | export type PostMode = {
19 | id: string;
20 | name: string;
21 | previewComponent: string;
22 | validators: Validators;
23 | widgets: Widget[];
24 | };
25 |
26 | export type Widget = {
27 | component: string;
28 | icon: string;
29 | name: string;
30 | };
31 |
32 | export type AspectRatio = `${number}:${number}`;
33 | export type ImageFormats = 'gif' | 'jpeg' | 'jpg' | 'png' | 'webp';
34 | export type VideoFormats = 'avi' | 'mov' | 'mp4' | 'webm';
35 | export type MediaFormats = ImageFormats | VideoFormats;
36 |
37 | export type TextValidator = {
38 | maxLength: number;
39 | };
40 |
41 | export type MediaValidator = {
42 | allowedFormats: MediaFormats[];
43 | ar: AspectRatio[];
44 | maxDuration: number;
45 | maxFileSize: number;
46 | maxHeight: number;
47 | maxWidth: number;
48 | mediaQtyLimit: number;
49 | };
50 |
51 | export type Validators = {
52 | media?: MediaValidator;
53 | text?: TextValidator;
54 | };
55 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // add Vitest functions here globally
2 | import matchers from '@testing-library/jest-dom/matchers';
3 | import { cleanup } from '@testing-library/react';
4 | import { afterEach, expect } from 'vitest';
5 | // Extend Vitest's expect method with methods from react-testing-library
6 | expect.extend(matchers);
7 |
8 | // Run cleanup after each test case (e.g., clearing jsdom)
9 | afterEach(() => {
10 | vi.clearAllMocks();
11 | cleanup();
12 | });
13 |
14 | // global mocks
15 | window.scrollTo = vi.fn();
16 |
--------------------------------------------------------------------------------
/src/stores/__mocks__/zunstandMock.ts:
--------------------------------------------------------------------------------
1 | import * as zustand from 'zustand';
2 |
3 | const actualCreate = zustand.create;
4 | export const storeResetFns = new Set<() => void>();
5 | export const myCustomCreate =
6 | () =>
7 | (stateCreator: zustand.StateCreator): zustand.StoreApi => {
8 | const store = actualCreate(stateCreator);
9 | const initialState = store.getState();
10 | storeResetFns.add(() => {
11 | store.setState(initialState, true);
12 | });
13 | return store;
14 | };
15 |
--------------------------------------------------------------------------------
/src/stores/useErrorStore/useErrorStore.spec.ts:
--------------------------------------------------------------------------------
1 | import { act, renderHook } from '@testing-library/react';
2 |
3 | import { useError } from './useErrorStore';
4 |
5 | describe('useError', () => {
6 | it('starts with an empty error object', () => {
7 | const { result } = renderHook(() => useError());
8 |
9 | expect(result.current.errors).toEqual({});
10 | });
11 |
12 | it('adds an error to error object', () => {
13 | const { result } = renderHook(() => useError());
14 |
15 | let errorId = '';
16 | act(() => {
17 | errorId = result.current.addError({ message: 'Something went wrong' });
18 | });
19 |
20 | expect(result.current.errors[errorId]).toEqual({
21 | message: 'Something went wrong',
22 | });
23 | });
24 |
25 | it('removes an error from error object', () => {
26 | const { result } = renderHook(() => useError());
27 |
28 | let errorId = '';
29 | act(() => {
30 | errorId = result.current.addError({ message: 'Something went wrong' });
31 | });
32 |
33 | act(() => {
34 | result.current.removeError(errorId);
35 | });
36 |
37 | expect(result.current.errors).not.toHaveProperty(errorId);
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/src/stores/useErrorStore/useErrorStore.ts:
--------------------------------------------------------------------------------
1 | import omit from 'lodash.omit';
2 | import { nanoid } from 'nanoid';
3 |
4 | import { create } from '~stores/zustand';
5 |
6 | import { ErrorStore } from './useErrorStore.types';
7 |
8 | export const useError = create((set) => ({
9 | addError: (error): string => {
10 | const id = nanoid();
11 |
12 | set((state) => ({
13 | errors: { ...state.errors, [id]: { message: error.message } },
14 | }));
15 |
16 | return id;
17 | },
18 |
19 | errors: {},
20 |
21 | removeError: (idToRemove: string): void => {
22 | set((state) => {
23 | const errors = omit(state.errors, idToRemove);
24 | return { errors };
25 | });
26 | },
27 | }));
28 |
--------------------------------------------------------------------------------
/src/stores/useErrorStore/useErrorStore.types.ts:
--------------------------------------------------------------------------------
1 | export type Error = {
2 | message: string;
3 | };
4 |
5 | export type Errors = Record;
6 |
7 | export type ErrorStore = {
8 | addError: (error: Error) => string;
9 | errors: Errors;
10 | removeError: (idToRemove: string) => void;
11 | };
12 |
--------------------------------------------------------------------------------
/src/stores/usePostStore/usePostStore.types.ts:
--------------------------------------------------------------------------------
1 | import { Account } from '~services/api/accounts/accounts.types';
2 | import {
3 | PostMode,
4 | SocialMedia,
5 | } from '~services/api/social-media/social-media.types';
6 | import { StoreAccount } from '~stores/useSocialMediaStore/useSocialMediaStore.types';
7 |
8 | import { Media } from '~components/MainComposer/components/InputMediaGroup/components/InputMedia/InputMedia.types';
9 |
10 | export type MainContent = {
11 | medias?: Media[];
12 | text?: string;
13 | };
14 |
15 | export type PostModes = Record;
16 |
17 | export type DataPost = {
18 | accountId: Account['id'];
19 | id: string;
20 | postModes: PostModes;
21 | socialMediaId: SocialMedia['id'];
22 | };
23 |
24 | type UpdateText = {
25 | postId: DataPost['id'];
26 | postModeId: PostMode['id'];
27 | text: string;
28 | };
29 |
30 | export type PostStore = {
31 | add: (account: StoreAccount, postsModes: PostMode[]) => void;
32 | mainContent: MainContent;
33 | posts: Record;
34 | remove: (postId: string) => void;
35 | updateMainContent: (newContent: MainContent) => void;
36 | updateText: ({ postId, postModeId, text }: UpdateText) => void;
37 | };
38 |
--------------------------------------------------------------------------------
/src/stores/useSocialMediaStore/useSocialMediaStore.types.ts:
--------------------------------------------------------------------------------
1 | import { Account } from '~services/api/accounts/accounts.types';
2 | import { SocialMedia } from '~services/api/social-media/social-media.types';
3 |
4 | export type StoreAccount = Account & { favorite: boolean; valid: boolean };
5 |
6 | export type NewAccount = Omit;
7 |
8 | export type SocialMediaState = {
9 | accounts: {
10 | data: Record;
11 | error: string;
12 | loading: boolean;
13 | };
14 |
15 | addAccount: (newAccount: NewAccount) => Promise;
16 |
17 | favoriteAccount: (
18 | accountId: Account['id'],
19 | favorite: boolean
20 | ) => Promise;
21 |
22 | favoriteAccounts: StoreAccount[];
23 |
24 | getAllAccounts: () => Promise;
25 |
26 | socialMedias: Map;
27 | };
28 |
29 | export type SocialMediaData = Pick;
30 |
--------------------------------------------------------------------------------
/src/stores/zustand.ts:
--------------------------------------------------------------------------------
1 | import {
2 | create as ZustandCreate,
3 | StateCreator,
4 | StoreApi,
5 | UseBoundStore,
6 | } from 'zustand';
7 | import { devtools } from 'zustand/middleware';
8 | import '@redux-devtools/extension';
9 |
10 | export const create = (
11 | store: StateCreator
12 | ): UseBoundStore> =>
13 | ZustandCreate()(
14 | devtools(store, {
15 | enabled: import.meta.env.VITE_ENVIRONMENT === 'development',
16 | })
17 | );
18 |
--------------------------------------------------------------------------------
/src/styles/base.scss:
--------------------------------------------------------------------------------
1 | @use './global';
2 |
3 | * {
4 | box-sizing: border-box;
5 | }
6 |
7 | html {
8 | font-family: global.$mainFont;
9 | font-size: 62.5%;
10 | scrollbar-gutter: stable;
11 | }
12 |
13 | body {
14 | font-size: 1.6rem;
15 | }
16 |
17 | button {
18 | width: 100%;
19 |
20 | padding: 0;
21 |
22 | border: 0;
23 |
24 | background: none;
25 | outline-color: global.$secondaryPurple;
26 | }
27 |
28 | ul {
29 | margin: 0;
30 | padding: 0;
31 |
32 | li {
33 | list-style-type: none;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/styles/breakpoints.scss:
--------------------------------------------------------------------------------
1 | $phoneScreen: 37.5rem; // 600px
2 | $tabletScreen: 56.5rem; // 905px
3 | $desktopScreen: 77.5rem; // 1240px
4 | $largeDesktopScreen: 90rem; // 1440px
5 |
6 | $breakpointPhone: 20rem; // 320px
7 | $breakpointTablet: 30rem; // 480px
8 | $breakpointDesktop: 120rem; // 1920px
9 |
10 | @mixin from320 {
11 | @media screen and (min-width: $breakpointPhone) {
12 | @content;
13 | }
14 | }
15 |
16 | @mixin from480 {
17 | @media screen and (min-width: $breakpointTablet) {
18 | @content;
19 | }
20 | }
21 |
22 | @mixin from600 {
23 | @media screen and (min-width: $phoneScreen) {
24 | @content;
25 | }
26 | }
27 |
28 | @mixin from905 {
29 | @media screen and (min-width: $tabletScreen) {
30 | @content;
31 | }
32 | }
33 |
34 | @mixin from1240 {
35 | @media screen and (min-width: $desktopScreen) {
36 | @content;
37 | }
38 | }
39 |
40 | @mixin from1440 {
41 | @media screen and (min-width: $largeDesktopScreen) {
42 | @content;
43 | }
44 | }
45 |
46 | @mixin from1920 {
47 | @media screen and (min-width: $breakpointDesktop) {
48 | @content;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/styles/global.scss:
--------------------------------------------------------------------------------
1 | $mainFont: 'Roboto', Arial, sans-serif;
2 |
3 | $primaryWhite: #fff;
4 | $secondaryWhite: #fdf8fd;
5 | $tertiaryWhite: #ece6eb;
6 | $quaternaryWhite: #f8f2f7;
7 | $primaryGray: #cbc4cf;
8 | $secondaryGray: #7a757f;
9 | $tertiaryGray: #49454e;
10 | $primaryPurple: #eaddff;
11 | $secondaryPurple: #7033e0;
12 | $tertiaryPurple: #5800c8;
13 | $primaryRed: #ffdad6;
14 | $secondaryRed: #c00016;
15 | $baseColor: #6750a4;
16 | $hovering: #6750a414; // Hovering with 8% opacity
17 | $pressing: #6750a41f; // Pressing with 12% opacity
18 | $darkPurple: #1e192b;
19 |
--------------------------------------------------------------------------------
/src/types/object.ts:
--------------------------------------------------------------------------------
1 | export type GenericObject = Record;
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "types": ["vitest/globals", "@testing-library/jest-dom"],
4 | "allowImportingTsExtensions": true,
5 | "baseUrl": ".",
6 | "paths": {
7 | "~*": ["src/*"],
8 | "~electron/*": ["electron/*"]
9 | },
10 | "isolatedModules": true,
11 | "jsx": "react-jsx",
12 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
13 | "module": "ESNext",
14 | "plugins": [{ "name": "typescript-plugin-css-modules" }],
15 |
16 | /* Bundler mode */
17 | "moduleResolution": "bundler",
18 | "noEmit": true,
19 | "noFallthroughCasesInSwitch": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "resolveJsonModule": true,
23 | "skipLibCheck": true,
24 |
25 | /* Linting */
26 | "strict": true,
27 | "target": "ES2020",
28 | "useDefineForClassFields": true
29 | },
30 | "exclude": ["node_modules", "dist"],
31 | "include": [
32 | "src/**/*",
33 | "electron/**/*",
34 | "playwright/**/*",
35 | "e2e/**/*",
36 | ".ladle/**/*",
37 | "vite.config.ts",
38 | "vitest.config.ts",
39 | "vite.electron.config.ts"
40 | ],
41 | "references": [{ "path": "./tsconfig.node.json" }]
42 | }
43 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "allowJs": true,
9 | "strictNullChecks": true
10 | },
11 | "include": [
12 | "**/vitest.config.ts",
13 | "**/vite.electron.config.ts",
14 | "**/vite.config.ts",
15 | "**/playwright.config.ts",
16 | "**/playwright-ct.config.ts",
17 | "playwright-ct.config.mts"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react-swc';
2 | import path, { dirname } from 'node:path';
3 | import { fileURLToPath } from 'node:url';
4 | import { defineConfig } from 'vite';
5 | import svgr from 'vite-plugin-svgr';
6 | import tsconfigPaths from 'vite-tsconfig-paths';
7 |
8 | const __filename = fileURLToPath(import.meta.url);
9 | const __dirname = dirname(__filename);
10 |
11 | export default defineConfig({
12 | build: { target: ['edge88', 'firefox85', 'chrome88', 'safari14', 'ios14'] },
13 | plugins: [react(), tsconfigPaths(), svgr()],
14 | resolve: { alias: { '~styles': path.join(__dirname, 'src/styles') } },
15 | });
16 |
--------------------------------------------------------------------------------
/vite.electron.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react-swc';
2 | import { defineConfig } from 'vite';
3 | import electron from 'vite-plugin-electron/simple';
4 | import svgr from 'vite-plugin-svgr';
5 | import tsconfigPaths from 'vite-tsconfig-paths';
6 |
7 | import viteConfig from './vite.config';
8 |
9 | export default defineConfig({
10 | build: viteConfig.build,
11 | plugins: [
12 | react(),
13 | tsconfigPaths(),
14 | svgr(),
15 | electron({
16 | main: { entry: 'electron/main.ts' },
17 | preload: { input: 'electron/preload.ts' },
18 | }),
19 | ],
20 | resolve: viteConfig.resolve,
21 | });
22 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import react from '@vitejs/plugin-react-swc';
2 | import svgr from 'vite-plugin-svgr';
3 | import tsconfigPaths from 'vite-tsconfig-paths';
4 | import { defineConfig } from 'vitest/config';
5 |
6 | import viteConfig from './vite.config';
7 |
8 | export default defineConfig({
9 | plugins: [tsconfigPaths(), react(), svgr()],
10 | resolve: viteConfig.resolve,
11 | server: { open: false },
12 | test: {
13 | coverage: {
14 | exclude: ['src/**/*.stories.tsx'],
15 | include: ['src/**/*.ts', 'src/**/*.tsx'],
16 | provider: 'istanbul',
17 | reporter: ['json', 'json-summary', 'html'],
18 | reportOnFailure: true,
19 | thresholds: {
20 | branches: 0,
21 | functions: 0,
22 | lines: 0,
23 | statements: 0,
24 | },
25 | },
26 | css: {
27 | modules: { classNameStrategy: 'non-scoped' },
28 | },
29 | environment: 'jsdom',
30 | exclude: ['src/**/*.ct.spec.ts', 'src/**/*.ct.spec.tsx'],
31 | globals: true,
32 | include: ['src/**/*.spec.ts', 'src/**/*.spec.tsx'],
33 | setupFiles: ['src/setupTests.ts'],
34 | },
35 | });
36 |
--------------------------------------------------------------------------------