├── .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 | {props.alt 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 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/Tablet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/TriangleLeftArrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/Icon/icons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/back-track.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/backspace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/blind-eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/Icon/icons/bus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/Icon/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/car.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/Icon/icons/check-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/components/Icon/icons/checkbox-checked-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/checkbox-checked.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/Icon/icons/drop-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/Icon/icons/emoji.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/gif.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/hamburguer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/components/Icon/icons/hexagon-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/hexagon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/horizontal-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-13.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-15.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-16.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-17.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-18.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-19.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-20.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-29.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-30.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-33.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-34.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-35.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-36.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-37.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-41.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-42.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-45.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-46.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-47.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-48.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-49.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-50.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-51.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-55.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/icon-59.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/keyboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/left-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/letter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/mag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/mail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/markdown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/mic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/mobile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/options-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/pencil.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/pentagon-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/pentagon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/scissors.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/share.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/small-circle-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/small-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/square-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/star-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/sun.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/taxi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/tiktok.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/Icon/icons/timer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/train.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/Icon/icons/translate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/triangle-filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/Icon/icons/vertical-dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Icon/icons/world.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/InputSearch/assets/leftIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/InputSearch/assets/rightIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 |
7 | teste 8 |
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 |
36 | 40 |
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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/SocialMediaList/assets/xIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Switch/assets/disableIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 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 |
18 | 19 |

Compose

20 |
21 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/pages/home/components/Header/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 2 | 3 | 4 | 5 | 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 | 2 | 3 | 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 | 2 | 4 | -------------------------------------------------------------------------------- /src/pages/home/components/Sidebar/components/SocialMediaForm/images/tiktok.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /src/pages/home/components/Sidebar/components/SocialMediaForm/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | -------------------------------------------------------------------------------- /src/pages/home/components/Sidebar/components/SocialMediaForm/images/unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 |
35 | 36 |
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 |
7 | 8 |
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 |
10 | 11 | 12 | 13 |
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 | --------------------------------------------------------------------------------