diff --git a/.cspell.json b/.cspell.json index 58253ec3..c1c794ba 100644 --- a/.cspell.json +++ b/.cspell.json @@ -17,6 +17,7 @@ "dictionaries": ["charity-en", "charity-fa"], "ignorePaths": [ "node_modules/", + "pnpm-lock.yaml", "dist/", "*.svg", "*.tsbuildinfo", diff --git a/.eslintrc.js b/.eslintrc.js index fa3c6fe0..f3b25fb8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,7 @@ const { init } = require('@fullstacksjs/eslint-config/init'); module.exports = init({ + root: true, modules: { auto: true, esm: true, @@ -9,10 +10,12 @@ module.exports = init({ resolverProject: ['./tsconfig.json'], }, }, - root: true, + extends: ['plugin:@cspell/recommended'], rules: { // NOTE: https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unused-prop-types.md#known-issueslimitations 'react/no-unused-prop-types': 'off', + // NOTE: Lot of false positive in props + 'react/jsx-no-leaked-render': 'off', 'no-console': 'error', }, overrides: [ diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 85130579..1d1292a1 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,25 +18,26 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8 + - uses: actions/setup-node@v3 with: node-version-file: 'package.json' - cache: 'npm' + cache: 'pnpm' - name: Install dependencies - run: npm ci + run: pnpm install - name: Lint and Format - run: npm run lint:ci - - - name: Spell Check - run: npm run spell + run: pnpm run lint:ci - name: Build - run: npm run build + run: pnpm run build - name: Test - run: npm run test:unit + run: pnpm run test:unit chromatic: runs-on: ubuntu-latest @@ -59,13 +60,17 @@ jobs: # NOTE: required because https://www.chromatic.com/docs/github-actions#support-for-codeactionscheckoutv2code-and-above fetch-depth: 0 + - uses: pnpm/action-setup@v2 + with: + version: 8 + - uses: actions/setup-node@v3 with: node-version-file: 'package.json' - cache: 'npm' + cache: 'pnpm' - name: Install dependencies - run: npm ci + run: pnpm install - name: Deploy Chromatic uses: chromaui/action@v1 @@ -91,11 +96,15 @@ jobs: - name: Checkout uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 + with: + version: 8 + - name: Run E2E tests uses: cypress-io/github-action@v4 with: - start: npx vite --port ${{ env.PORT }} --host 127.0.0.1 - build: npm run build + start: pnpm exec vite --port ${{ env.PORT }} --host 127.0.0.1 + build: pnpm run build wait-on: https://127.0.0.1:${{ env.PORT }} wait-on-timeout: 20 browser: chrome diff --git a/.lintstagedrc.json b/.lintstagedrc.json index ff46ff93..20d7da4b 100644 --- a/.lintstagedrc.json +++ b/.lintstagedrc.json @@ -1,5 +1,5 @@ { "*.{json,html}": ["prettier --write", "cspell --no-must-find-files"], "*.{md,yaml,yml}": ["cspell --no-must-find-files"], - "*.ts(x)?": ["eslint --fix", "cspell --no-must-find-files"] + "*.ts(x)?": ["eslint --fix"] } diff --git a/@types/mantine.d.ts b/@types/mantine.d.ts index 3a9fc1ad..d0810bd9 100644 --- a/@types/mantine.d.ts +++ b/@types/mantine.d.ts @@ -1,6 +1,6 @@ import type { Tuple } from '@mantine/core'; -import type { colors } from '../libs/design/theme/theme'; +import type { Palette } from '../libs/design/theme/palette'; type DefaultMantineColors = | 'blue' @@ -19,11 +19,9 @@ type DefaultMantineColors = | 'violet' | 'yellow'; -type IndexifyColor = T extends DefaultMantineColors - ? T | `${T}.${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}` - : T; +type IndexifyColor = T | `${T}.${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`; -type Colors = IndexifyColor; +type Colors = IndexifyColor; declare module '@mantine/core' { export interface MantineThemeColorsOverride { diff --git a/README.md b/README.md index 34c3448e..facd2ae4 100644 --- a/README.md +++ b/README.md @@ -40,9 +40,9 @@ The FullstacksJS team is monitoring for pull requests. We will review your pull Before submitting a pull request, please make sure the following is done: - Fork/Clone the repository and create your feature branch from dev. -- Run `npm install` to have all dependencies and husky hooks needed for development. -- Run `npm run codegen` to have latest API types. -- To start development run `npm start`. +- Run `pnpm install` to have all dependencies and husky hooks needed for development. +- Run `pnpm run codegen` to have latest API types. +- To start development run `pnpm start`. - If you've fixed a bug or added code that should be tested, add tests, please. - Create a PR (You can use `./scripts/pr` to create one). @@ -81,9 +81,9 @@ The project contains multiple modules which have their specific responsibilities To start the app first set the envs as described in the [envs section](#envs) then run: ```bash -npm install -npm run codegen -npm run dev +pnpm install +pnpm run codegen +pnpm run dev ``` > If you're using [VSCode][vscode] you can install the [Apollo Graphql][apollo-graphql-extension] extension to get features like autocomplete in your graphql queries @@ -95,7 +95,7 @@ This project uses [GraphQL Code Generator][codegen] to generate types and interf to generate the latest type from the API you need to have (a proper environment)[#Envs] in place and run: ```bash -npm run codegen +pnpm run codegen ``` ### Verify @@ -107,7 +107,7 @@ The verify command runs all tests to make sure that your changes are working. To build the app just run: ```bash -npm run build +pnpm run build ``` ### Lint @@ -115,7 +115,7 @@ npm run build To run the linter to auto-fix all the problems run: ```bash -npm run lint +pnpm run lint ``` ### Test @@ -123,31 +123,31 @@ npm run lint To run the unit tests run: ```bash -npm run test:unit +pnpm run test:unit ``` To run e2e tests you need to have the dev server running at configured port and run: ```bash -npm run test:e2e +pnpm run test:e2e ``` If you want only to run a specific e2e test based on a pattern you can run the `test:e2ep` command: ```bash -npm run test:e2ep [PATTERN] +pnpm run test:e2ep [PATTERN] ``` if the pattern matches more than one file it will still run all of them, for example, the following command will run the `createHousehold.cy.ts` and `householdList.cy.ts` test ```bash -npm run test:e2ep household +pnpm run test:e2ep household ``` To run component tests run: ```bash -npm run test:component +pnpm run test:component ``` ### Storybook @@ -157,7 +157,7 @@ npm run test:component To run the storybook dev server run: ```bash -npm run storybook +pnpm run storybook ``` ### Spell Check @@ -165,10 +165,10 @@ npm run storybook To find spelling errors just run ```bash -npm run spell +pnpm run spell ``` -And if you want to add a new word so that it won't count as a spelling error, just add it to the `.cspell/charity.txt` and separate it with a new line +And if you want to add a new word so that it won't count as a spelling error, just add it to the `configs/cspell/charity.en.txt` or `configs/cspell/charity.en.txt` and separate it with a new line > Note: These two (linting and finding spelling errors) are run automatically on each commit and the commit won't be done if there's anything wrong, even for the commit messages, so be careful what you write as a commit message :) diff --git a/app/Auth/Login/Login.tsx b/app/Auth/Login/Login.tsx index 555039c8..a341180c 100644 --- a/app/Auth/Login/Login.tsx +++ b/app/Auth/Login/Login.tsx @@ -1,25 +1,24 @@ import { useAuth0 } from '@camp/auth'; import { messages } from '@camp/messages'; -import { Button, Center, Group, Image } from '@mantine/core'; +import { Button, Center, Flex, Image, Text, Title } from '@mantine/core'; export const Login = () => { const { loginWithRedirect } = useAuth0(); return ( - -
- -
-
({ - backgroundColor: theme.colors.bgCanvas[6], - height: '100%', - })} - > - login -
-
+
+ + logo + + + {messages.login.title} + {messages.login.desc} + + + + +
); }; diff --git a/app/Dashboard/DashboardLayout.tsx b/app/Dashboard/DashboardLayout.tsx index 7ef16ba6..3436f96a 100644 --- a/app/Dashboard/DashboardLayout.tsx +++ b/app/Dashboard/DashboardLayout.tsx @@ -18,7 +18,7 @@ const useStyles = createStyles(theme => ({ paddingTop: 30, paddingBottom: 0, paddingInline: 40, - backgroundColor: theme.colors.bgCanvas[6], + backgroundColor: theme.colors.bg[6], overflow: 'auto', }, })); diff --git a/app/Dashboard/Households/HouseholdDetail/HouseholdDetail.ids.ts b/app/Dashboard/Households/HouseholdDetail/HouseholdDetail.ids.ts index 9d37e613..58c00689 100644 --- a/app/Dashboard/Households/HouseholdDetail/HouseholdDetail.ids.ts +++ b/app/Dashboard/Households/HouseholdDetail/HouseholdDetail.ids.ts @@ -9,7 +9,9 @@ export const householdDetailIds = { submitBtn: 'household-submit-btn', editBtn: 'household-edit-btn', notification: { - success: 'household-successful-update', - failure: 'household-failed-update', + edit: { + success: 'edit-household-successful', + failure: 'edit-household-failed', + }, }, }; diff --git a/app/Dashboard/Households/HouseholdDetail/HouseholdDetail.tsx b/app/Dashboard/Households/HouseholdDetail/HouseholdDetail.tsx index 62b83e45..19660995 100644 --- a/app/Dashboard/Households/HouseholdDetail/HouseholdDetail.tsx +++ b/app/Dashboard/Households/HouseholdDetail/HouseholdDetail.tsx @@ -25,9 +25,9 @@ import { import { ArrowUpIcon, CheckIcon, EditIcon, TrashIcon } from '@camp/icons'; import { errorMessages, messages } from '@camp/messages'; import { AppRoute, useNavigate, useParams } from '@camp/router'; -import { createTestAttr } from '@camp/test'; +import { tid } from '@camp/test'; import { isNull } from '@fullstacksjs/toolbox'; -import { Button, createStyles, Flex, Title } from '@mantine/core'; +import { Button, Flex, Title } from '@mantine/core'; import { useBoolean } from 'ahooks'; import { useForm } from 'react-hook-form'; @@ -37,6 +37,7 @@ import { openDeleteHouseholdModal } from '../_components/DeleteHouseholdModal'; import { HouseholderDetail } from './_components/HouseholderDetail'; import { MemberList } from './_components/MemberList'; import { householdDetailIds as ids } from './HouseholdDetail.ids'; +import { householdNotifications } from './householdNotifications'; interface FormSchema { name: string; @@ -50,14 +51,6 @@ const resolver = createResolver({ membersCount: householderSchema.membersCount(), }); -const useStyles = createStyles(theme => ({ - input: { - label: { - color: theme.colors.fgSubtle[6], - }, - }, -})); - // eslint-disable-next-line max-lines-per-function export const HouseholdDetail = () => { const t = messages.householdDetail; @@ -90,7 +83,8 @@ export const HouseholdDetail = () => { const isReadOnly = !isEditing; - const [deleteHousehold] = useDeleteHouseholdMutation(); + const [deleteHousehold, { loading: isDeleting }] = + useDeleteHouseholdMutation(); const [completeHousehold] = useCompleteHouseholdMutation(); const [updateHousehold] = useEditHouseholdMutation(); @@ -99,40 +93,23 @@ export const HouseholdDetail = () => { variables: { id }, }); - const onUpdateHousehold = handleSubmit(async formData => { + const onUpdateHousehold = handleSubmit(async values => { try { await updateHousehold({ variables: { id, - update: { name: formData.name, severity: formData.severity }, + update: { name: values.name, severity: values.severity }, }, }); setIsEditing(false); - - showNotification({ - title: t.title, - message: messages.householder.form.notification.successfulUpdate( - household!.name, - ), - type: 'success', - ...createTestAttr(ids.notification.success), - }); + householdNotifications.edit.success(values.name); } catch (err) { debug.error(err); - - showNotification({ - title: t.title, - message: messages.householder.form.notification.failedUpdate( - household!.name, - ), - type: 'failure', - ...createTestAttr(ids.notification.failure), - }); + householdNotifications.edit.failure(values.name); } }); const householder = householderData?.householder; - const { classes } = useStyles(); if (loading) return ; @@ -198,7 +175,7 @@ export const HouseholdDetail = () => { return ( <> -
+ { {messages.actions.undoBtn} - - ) : ( - - )} - - + setIsEditing(true)} + /> ({ value: v, label: messages.nationalities[v], @@ -212,8 +153,7 @@ export const HouseholderForm = ({ /> ({ value: v, label: messages.genders[v], @@ -237,7 +177,7 @@ export const HouseholderForm = ({ readOnly={isReadOnly} name="religion" control={control} - wrapperProps={createTestAttr(ids.religionInput)} + wrapperProps={tid(ids.religionInput)} data={religions.map(v => ({ value: v, label: messages.religions[v], @@ -250,7 +190,7 @@ export const HouseholderForm = ({ readOnly={isReadOnly} name="cityOfBirth" control={control} - wrapperProps={createTestAttr(ids.cityOfBirthInput)} + wrapperProps={tid(ids.cityOfBirthInput)} data={cities.map(v => ({ value: v, label: messages.cities[v], @@ -263,8 +203,7 @@ export const HouseholderForm = ({ name="dob" control={control} readOnly={isReadOnly} - wrapperProps={createTestAttr(ids.dobInput)} - className={classes.input} + wrapperProps={tid(ids.dobInput)} label={`${t.dobInput.label}:`} placeholder={t.selectInputs.placeholder} /> diff --git a/app/Dashboard/Households/HouseholdDetail/_components/HouseholderForm/HouseholderFormActions.tsx b/app/Dashboard/Households/HouseholdDetail/_components/HouseholderForm/HouseholderFormActions.tsx new file mode 100644 index 00000000..34f5fc64 --- /dev/null +++ b/app/Dashboard/Households/HouseholdDetail/_components/HouseholderForm/HouseholderFormActions.tsx @@ -0,0 +1,62 @@ +import { DashboardTitle, DestructiveButton } from '@camp/design'; +import { CheckIcon, EditIcon } from '@camp/icons'; +import { messages } from '@camp/messages'; +import { tid } from '@camp/test'; +import { Button, Group } from '@mantine/core'; + +import { householderFormIds as ids } from './HouseholderForm.ids'; + +const t = messages.householder.form; + +interface Props { + isEditMode?: boolean; + canUndo?: boolean; + canSubmit?: boolean; + onUndo: VoidFunction; + onEdit: VoidFunction; +} + +export const HouseholderFormActions = ({ + canSubmit, + canUndo, + isEditMode, + onUndo, + onEdit, +}: Props) => { + return ( + + {t.title} + + {isEditMode ? ( + <> + + {messages.actions.undoBtn} + + + + ) : ( + + )} + + + ); +}; diff --git a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/CreateMemberButton/CreateMemberButton.tsx b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/CreateMemberButton/CreateMemberButton.tsx index b0e53390..a4a007a2 100644 --- a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/CreateMemberButton/CreateMemberButton.tsx +++ b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/CreateMemberButton/CreateMemberButton.tsx @@ -1,6 +1,6 @@ import { PlusIcon } from '@camp/icons'; import { messages } from '@camp/messages'; -import { createTestAttr } from '@camp/test'; +import { tid } from '@camp/test'; import { Button } from '@mantine/core'; import { createMemberButtonId as ids } from './CreateMemberButton.ids'; @@ -17,7 +17,7 @@ export const CreateMemberButton = ({ variant = 'outline', onClick }: Props) => { variant={variant} size="sm" onClick={onClick} - {...createTestAttr(ids)} + {...tid(ids)} leftIcon={} > {t.addNewMember} diff --git a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberEmptyState/MemberEmptyState.tsx b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberEmptyState/MemberEmptyState.tsx index 31802c49..7d1e9290 100644 --- a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberEmptyState/MemberEmptyState.tsx +++ b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberEmptyState/MemberEmptyState.tsx @@ -1,7 +1,6 @@ import { EmptyState } from '@camp/design'; import { UserIcon } from '@camp/icons'; import { messages } from '@camp/messages'; -import { useMantineTheme } from '@mantine/core'; import { CreateMemberButton } from '../CreateMemberButton'; @@ -10,21 +9,10 @@ interface Props { } export const MemberEmptyState = ({ addNewMember }: Props) => { - const theme = useMantineTheme(); const t = messages.member.empty; return ( - - } - title={t.title} - message={t.description} - > + ); diff --git a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.cy.tsx b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.cy.tsx index 61dbcbcd..114b312f 100644 --- a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.cy.tsx +++ b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.cy.tsx @@ -13,10 +13,7 @@ describe('Create member form Form', () => { it('contains a first name input with correct label', () => { cy.findByTestId(memberFormIds.form).within(() => { - cy.findByRole('textbox', { - name: /نام:/i, - exact: false, - }); + cy.findByRole('textbox', { name: /نام:/i }); }); }); diff --git a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.ids.ts b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.ids.ts index cd50d279..0251f385 100644 --- a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.ids.ts +++ b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.ids.ts @@ -15,7 +15,17 @@ export const memberFormIds = { editBtn: 'edit-button', deleteBtn: 'delete-button', notification: { - success: 'member-successful', - failure: 'member-failed', + delete: { + success: 'delete-member-successful', + failure: 'delete-member-failed', + }, + edit: { + success: 'edit-member-successful', + failure: 'edit-member-failed', + }, + create: { + success: 'create-member-successful', + failure: 'create-member-failed', + }, }, } as const; diff --git a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.tsx b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.tsx index 6c7a8b1f..c9515668 100644 --- a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.tsx +++ b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberForm.tsx @@ -1,4 +1,3 @@ -/* eslint-disable max-lines-per-function */ import { useDeleteMemberMutation, useUpsertMemberMutation, @@ -8,8 +7,6 @@ import { CollapsibleDashboardCard, ControlledDateInput, ControlledSelect, - DestructiveButton, - showNotification, TextInput, } from '@camp/design'; import type { @@ -25,37 +22,18 @@ import { nationalities, religions, } from '@camp/domain'; -import { CheckIcon, EditIcon, TrashIcon } from '@camp/icons'; import { messages } from '@camp/messages'; -import { createTestAttr } from '@camp/test'; +import { tid } from '@camp/test'; import { isNull } from '@fullstacksjs/toolbox'; -import { - Button, - createStyles, - Group, - SimpleGrid, - Stack, - Title, -} from '@mantine/core'; +import { SimpleGrid, Stack } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { useBoolean } from 'ahooks'; import { useForm } from 'react-hook-form'; -import { InformationBadge } from '../../../../_components/InformationBadge'; import { memberFormIds as ids } from './MemberForm.ids'; - -const useStyles = createStyles(theme => ({ - textInput: { - label: { - color: theme.colors.fgSubtle[6], - }, - }, - dateInput: { - label: { - color: theme.colors.fgSubtle[6], - }, - }, -})); +import { MemberFormActions } from './MemberFormActions'; +import { MemberFormHeader } from './MemberFormHeader'; +import { memberFormNotifications as notifications } from './memberFormNotifications'; interface FormSchema { name: string; @@ -79,9 +57,7 @@ const resolver = createResolver({ dob: memberSchema.dob(), }); -const t = messages.member; -const tt = t.createForm; -const tNotification = messages.notification.member; +const t = messages.member.createForm; interface Props { initialMember?: MemberListItem; @@ -91,6 +67,8 @@ interface Props { onUndo?: VoidFunction; } +// NOTE: Cannot extract anymore concerns without loosing cohesion. +// eslint-disable-next-line max-lines-per-function export const MemberForm = ({ initialMember, onSuccess, @@ -98,40 +76,10 @@ export const MemberForm = ({ memberId, onUndo, }: Props) => { - const [createMemberMutation] = useUpsertMemberMutation(); + const [createMember, { loading: isCreating }] = useUpsertMemberMutation(); const [opened, { toggle }] = useDisclosure(!initialMember?.isCompleted); - const [isEditableMode, { toggle: toggleEditableMode }] = useBoolean( - !initialMember, - ); - const { classes } = useStyles(); - const [deleteMember] = useDeleteMemberMutation(); - - const onDeleteMember = async () => { - const member = initialMember; - const id = memberId; - - if (!member || !id) return; - - try { - const { data } = await deleteMember({ - variables: { id }, - }); - - if (isNull(data.member)) throw Error('Assert: data is null'); - showNotification({ - title: tNotification.delete.title, - message: tNotification.delete.success(member.name), - type: 'success', - }); - } catch (err) { - debug.error(err); - showNotification({ - title: tNotification.delete.title, - message: tNotification.delete.failed(member.name), - type: 'failure', - }); - } - }; + const [isEditMode, { toggle: toggleEditMode }] = useBoolean(!initialMember); + const [deleteMember, { loading: isDeleting }] = useDeleteMemberMutation(); const { handleSubmit, @@ -147,182 +95,143 @@ export const MemberForm = ({ }); const [name, surname] = watch(['name', 'surname']); + const onDeleteMember = async () => { + const member = initialMember; + const id = memberId; + + if (!member || !id) return; + + try { + const { data } = await deleteMember({ variables: { id } }); + if (isNull(data.member)) throw Error('Assert: data is null'); + notifications.delete.success(member.name); + } catch (err) { + debug.error(err); + notifications.delete.failure(member.name); + } + }; + const onSubmit = handleSubmit(async values => { try { - const { data } = await createMemberMutation({ - variables: { - id: memberId, - ...values, - householdId, - }, - }); - toggleEditableMode(); - showNotification({ - title: t.title, - message: t.notification.successful(data.member?.name ?? ''), - type: 'success', - ...createTestAttr(ids.notification.success), + await createMember({ + variables: { id: memberId, ...values, householdId }, }); + toggleEditMode(); + notifications.create.success(values.name); onSuccess?.(); } catch { - return showNotification({ - title: t.title, - message: t.notification.failed(values.name), - type: 'failure', - ...createTestAttr(ids.notification.failure), - }); + notifications.create.failure(values.name); } }); + const handleUndo = () => { + if (initialMember) toggleEditMode(); + reset(); + onUndo?.(); + }; + return ( - - {name ? `${name} ${surname ?? ''}` : t.createForm.title} - - - + } > - + ({ value: v, label: messages.nationalities[v], }))} - placeholder={tt.selectInputs.placeholder} - label={`${tt.nationalityInput.label}:`} + placeholder={t.selectInputs.placeholder} + label={`${t.nationalityInput.label}:`} /> ({ value: v, label: messages.genders[v], }))} - label={`${tt.genderInput.label}:`} - placeholder={tt.selectInputs.placeholder} + label={`${t.genderInput.label}:`} + placeholder={t.selectInputs.placeholder} /> ({ value: v, label: messages.religions[v], }))} - placeholder={tt.selectInputs.placeholder} - label={`${tt.religionInput.label}:`} + placeholder={t.selectInputs.placeholder} + label={`${t.religionInput.label}:`} /> - {!isEditableMode ? ( - - - - - ) : ( - - { - reset(); - onUndo?.(); - }} - > - {messages.actions.undoBtn} - - - - )} + onDeleteMember()} + isEditMode={isEditMode} + toggleEditMode={toggleEditMode} + isCreating={isCreating} + onUndo={handleUndo} + canSubmit={isValid && isDirty} + /> diff --git a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberFormActions.tsx b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberFormActions.tsx new file mode 100644 index 00000000..bc15fb18 --- /dev/null +++ b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberFormActions.tsx @@ -0,0 +1,68 @@ +import { DestructiveButton } from '@camp/design'; +import { CheckIcon, EditIcon, TrashIcon } from '@camp/icons'; +import { messages } from '@camp/messages'; +import { tid } from '@camp/test'; +import { Button, Group } from '@mantine/core'; + +import { memberFormIds as ids } from './MemberForm.ids'; + +interface Props { + isEditMode: boolean; + isDeleting: boolean; + onDelete: VoidFunction; + toggleEditMode: VoidFunction; + onUndo: VoidFunction; + isCreating: boolean; + canSubmit: boolean; +} + +export const MemberFormActions = ({ + isEditMode, + isDeleting, + onDelete, + toggleEditMode, + onUndo, + canSubmit, + isCreating, +}: Props) => { + return !isEditMode ? ( + + } + onClick={onDelete} + > + {messages.actions.delete} + + + + ) : ( + + + {messages.actions.undoBtn} + + + + ); +}; diff --git a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberFormHeader.tsx b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberFormHeader.tsx new file mode 100644 index 00000000..68bae972 --- /dev/null +++ b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/MemberFormHeader.tsx @@ -0,0 +1,23 @@ +import { messages } from '@camp/messages'; +import { Group, Title } from '@mantine/core'; + +import { InformationBadge } from '../../../../_components/InformationBadge'; + +const t = messages.member; + +interface Props { + name: string; + surname: string | undefined; + isCompleted?: boolean; +} + +export const MemberFormHeader = ({ isCompleted, name, surname }: Props) => { + return ( + + + {name ? `${name} ${surname ?? ''}` : t.createForm.title} + + + + ); +}; diff --git a/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/memberFormNotifications.tsx b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/memberFormNotifications.tsx new file mode 100644 index 00000000..053f2a98 --- /dev/null +++ b/app/Dashboard/Households/HouseholdDetail/_components/MemberForm/memberFormNotifications.tsx @@ -0,0 +1,60 @@ +import { showNotification } from '@camp/design'; +import { messages } from '@camp/messages'; +import { tid } from '@camp/test'; + +import { memberFormIds as ids } from './MemberForm.ids'; + +const t = messages.notification.member; + +export const memberFormNotifications = { + delete: { + success: (name: string) => + showNotification({ + type: 'success', + title: t.delete.title, + message: t.delete.success(name), + ...tid(ids.notification.delete.success), + }), + failure: (name: string) => + showNotification({ + type: 'failure', + title: t.delete.title, + message: t.delete.failed(name), + ...tid(ids.notification.delete.failure), + }), + }, + edit: { + success: (name: string) => + showNotification({ + type: 'success', + title: t.edit.title, + message: t.edit.success(name), + ...tid(ids.notification.edit.success), + }), + + failure: (name: string) => + showNotification({ + type: 'failure', + title: t.edit.title, + message: t.edit.failed(name), + ...tid(ids.notification.edit.failure), + }), + }, + create: { + success: (name: string) => + showNotification({ + type: 'success', + title: t.create.title, + message: t.create.success(name), + ...tid(ids.notification.create.success), + }), + + failure: (name: string) => + showNotification({ + type: 'failure', + title: t.create.title, + message: t.create.failed(name), + ...tid(ids.notification.create.failure), + }), + }, +}; diff --git a/app/Dashboard/Households/HouseholdDetail/_components/MemberList/MemberList.tsx b/app/Dashboard/Households/HouseholdDetail/_components/MemberList/MemberList.tsx index ed2d5134..b5b4d5a5 100644 --- a/app/Dashboard/Households/HouseholdDetail/_components/MemberList/MemberList.tsx +++ b/app/Dashboard/Households/HouseholdDetail/_components/MemberList/MemberList.tsx @@ -2,7 +2,7 @@ import { useMemberListQuery } from '@camp/data-layer'; import { debug } from '@camp/debug'; import { DashboardTitle, FullPageLoader, showNotification } from '@camp/design'; import { errorMessages, messages } from '@camp/messages'; -import { Center, Group, Stack, Title } from '@mantine/core'; +import { Center, Group, Stack } from '@mantine/core'; import { useState } from 'react'; import { CreateMemberButton, MemberForm } from '../MemberForm'; diff --git a/app/Dashboard/Households/HouseholdDetail/householdNotifications.tsx b/app/Dashboard/Households/HouseholdDetail/householdNotifications.tsx new file mode 100644 index 00000000..1bc9b825 --- /dev/null +++ b/app/Dashboard/Households/HouseholdDetail/householdNotifications.tsx @@ -0,0 +1,27 @@ +import { showNotification } from '@camp/design'; +import { messages } from '@camp/messages'; +import { tid } from '@camp/test'; + +import { householdDetailIds as ids } from './HouseholdDetail.ids'; + +const t = messages.notification.household; + +export const householdNotifications = { + edit: { + success: (name: string) => + showNotification({ + type: 'success', + title: t.edit.title, + message: t.edit.success(name), + ...tid(ids.notification.edit.success), + }), + + failure: (name: string) => + showNotification({ + type: 'failure', + title: t.edit.title, + message: t.edit.failed(name), + ...tid(ids.notification.edit.failure), + }), + }, +}; diff --git a/app/Dashboard/Households/HouseholdEmptyState/HouseholdEmptyState.tsx b/app/Dashboard/Households/HouseholdEmptyState/HouseholdEmptyState.tsx index 39a5db9a..505a637c 100644 --- a/app/Dashboard/Households/HouseholdEmptyState/HouseholdEmptyState.tsx +++ b/app/Dashboard/Households/HouseholdEmptyState/HouseholdEmptyState.tsx @@ -1,21 +1,13 @@ import { EmptyState } from '@camp/design'; import { PeopleIcon } from '@camp/icons'; import { messages } from '@camp/messages'; -import { useMantineTheme } from '@mantine/core'; import { CreateHouseholdButton } from '../_components/CreateHousehold'; export const HouseholdEmptyState = () => { - const theme = useMantineTheme(); return ( - } + Icon={PeopleIcon} title={messages.households.empty.title} message={messages.households.empty.description} > diff --git a/app/Dashboard/Households/HouseholdList/HouseholdList.tsx b/app/Dashboard/Households/HouseholdList/HouseholdList.tsx index bbfadb52..2c7650b2 100644 --- a/app/Dashboard/Households/HouseholdList/HouseholdList.tsx +++ b/app/Dashboard/Households/HouseholdList/HouseholdList.tsx @@ -8,7 +8,7 @@ import { import { householdColumnHelper } from '@camp/domain'; import { errorMessages, messages } from '@camp/messages'; import { AppRoute } from '@camp/router'; -import { createTestAttr } from '@camp/test'; +import { tid } from '@camp/test'; import { isEmpty, isNull } from '@fullstacksjs/toolbox'; import { Group } from '@mantine/core'; import type { SortingState } from '@tanstack/react-table'; @@ -84,7 +84,7 @@ export const HouseholdList = () => { type: 'failure', title: t.title, message: errorMessages.UNKNOWN_ERROR, - ...createTestAttr(ids.householdListFailureNotification), + ...tid(ids.householdListFailureNotification), }); return null; } diff --git a/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdButton/CreateHouseholdButton.tsx b/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdButton/CreateHouseholdButton.tsx index 59df3daf..0742e94a 100644 --- a/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdButton/CreateHouseholdButton.tsx +++ b/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdButton/CreateHouseholdButton.tsx @@ -1,6 +1,6 @@ import { PlusIcon } from '@camp/icons'; import { messages } from '@camp/messages'; -import { createTestAttr } from '@camp/test'; +import { tid } from '@camp/test'; import { Button } from '@mantine/core'; import { openCreateHouseholdModal } from '../CreateHouseholdModal'; @@ -17,7 +17,7 @@ export const CreateHouseholdButton = ({ variant = 'outline' }: Props) => { size="sm" leftIcon={} onClick={() => openCreateHouseholdModal()} - {...createTestAttr(id)} + {...tid(id)} > {messages.households.create} diff --git a/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdForm/CreateHouseholdForm.tsx b/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdForm/CreateHouseholdForm.tsx index dbae64f2..f10e9afe 100644 --- a/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdForm/CreateHouseholdForm.tsx +++ b/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdForm/CreateHouseholdForm.tsx @@ -4,7 +4,7 @@ import { createResolver, householdSchema } from '@camp/domain'; import { messages } from '@camp/messages'; import type { AppRoute } from '@camp/router'; import { useNavigate } from '@camp/router'; -import { createTestAttr } from '@camp/test'; +import { tid } from '@camp/test'; import { isNull } from '@fullstacksjs/toolbox'; import { Button, Group, Stack, TextInput } from '@mantine/core'; import { useForm } from 'react-hook-form'; @@ -45,7 +45,7 @@ export const CreateHouseholdForm = ({ dismiss }: Props) => { title: messages.households.create, message: notification.success(household.name), type: 'success', - ...createTestAttr(ids.notification.success), + ...tid(ids.notification.success), }); dismiss(); @@ -56,13 +56,13 @@ export const CreateHouseholdForm = ({ dismiss }: Props) => { title: messages.households.create, message: notification.failure(), type: 'failure', - ...createTestAttr(ids.notification.failure), + ...tid(ids.notification.failure), }), ); }); return ( -
+ { description={nameInput.description} size="sm" error={formState.errors.name?.message} - wrapperProps={createTestAttr(ids.nameInput)} + wrapperProps={tid(ids.nameInput)} {...register('name')} /> - - diff --git a/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdModal/CreateHouseholdModal.tsx b/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdModal/CreateHouseholdModal.tsx index d45f86b2..de9adaa1 100644 --- a/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdModal/CreateHouseholdModal.tsx +++ b/app/Dashboard/Households/_components/CreateHousehold/CreateHouseholdModal/CreateHouseholdModal.tsx @@ -1,5 +1,5 @@ import { messages } from '@camp/messages'; -import { createTestAttr } from '@camp/test'; +import { tid } from '@camp/test'; import { closeModal, openModal } from '@mantine/modals'; import { CreateHouseholdForm } from '../CreateHouseholdForm'; @@ -16,5 +16,5 @@ export const openCreateHouseholdModal = () => title: messages.households.create, size: 'md', centered: true, - ...createTestAttr(id), + ...tid(id), }); diff --git a/app/Dashboard/Households/_components/DeleteHouseholdModal/DeleteHouseholdModal.tsx b/app/Dashboard/Households/_components/DeleteHouseholdModal/DeleteHouseholdModal.tsx index a277235b..5b402b17 100644 --- a/app/Dashboard/Households/_components/DeleteHouseholdModal/DeleteHouseholdModal.tsx +++ b/app/Dashboard/Households/_components/DeleteHouseholdModal/DeleteHouseholdModal.tsx @@ -19,4 +19,5 @@ export const openDeleteHouseholdModal = ({ name, onDeleteHousehold }: Props) => confirmLabel: t.confirm, size: 'sm', onConfirm: () => void onDeleteHousehold(), + destructive: true, }); diff --git a/app/Dashboard/Households/_components/HouseholdActionButton/HouseholdActionButton.tsx b/app/Dashboard/Households/_components/HouseholdActionButton/HouseholdActionButton.tsx index 9420218c..374f7604 100644 --- a/app/Dashboard/Households/_components/HouseholdActionButton/HouseholdActionButton.tsx +++ b/app/Dashboard/Households/_components/HouseholdActionButton/HouseholdActionButton.tsx @@ -5,7 +5,7 @@ import { MenuIcon } from '@camp/icons'; import { messages } from '@camp/messages'; import type { AppRoute, PathParams } from '@camp/router'; import { Link } from '@camp/router'; -import { createTestAttr } from '@camp/test'; +import { tid } from '@camp/test'; import { isNull } from '@fullstacksjs/toolbox'; import { ActionIcon, Menu } from '@mantine/core'; @@ -43,7 +43,7 @@ export const HouseholdActionButton = ({ title: t.title, message: t.success(data.household!.name), type: 'success', - ...createTestAttr(ids.notifications.delete.success), + ...tid(ids.notifications.delete.success), }); } catch (err) { debug.error(err); @@ -51,7 +51,7 @@ export const HouseholdActionButton = ({ title: t.title, message: t.failed(name), type: 'failure', - ...createTestAttr(ids.notifications.delete.failure), + ...tid(ids.notifications.delete.failure), }); } }; @@ -62,17 +62,17 @@ export const HouseholdActionButton = ({ return ( - + e.stopPropagation()}> - + {messages.actions.open} { e.stopPropagation(); handleDeleteHousehold(); diff --git a/app/Dashboard/Projects/CreateProject/CreateProjectButton/CreateProjectButton.tsx b/app/Dashboard/Projects/CreateProject/CreateProjectButton/CreateProjectButton.tsx index 98f3ddd9..4b9dd29c 100644 --- a/app/Dashboard/Projects/CreateProject/CreateProjectButton/CreateProjectButton.tsx +++ b/app/Dashboard/Projects/CreateProject/CreateProjectButton/CreateProjectButton.tsx @@ -1,6 +1,6 @@ import { PlusIcon } from '@camp/icons'; import { messages } from '@camp/messages'; -import { createTestAttr } from '@camp/test'; +import { tid } from '@camp/test'; import { Button } from '@mantine/core'; import { openCreateProjectModal } from '../CreateProjectModal'; @@ -16,7 +16,7 @@ export const CreateProjectButton = ({ variant = 'outline' }: Props) => { variant={variant} size="sm" leftIcon={} - {...createTestAttr(ids)} + {...tid(ids)} onClick={() => openCreateProjectModal()} > {messages.projects.create} diff --git a/app/Dashboard/Projects/CreateProject/CreateProjectForm/CreateProjectForm.tsx b/app/Dashboard/Projects/CreateProject/CreateProjectForm/CreateProjectForm.tsx index 265800f6..26481f1b 100644 --- a/app/Dashboard/Projects/CreateProject/CreateProjectForm/CreateProjectForm.tsx +++ b/app/Dashboard/Projects/CreateProject/CreateProjectForm/CreateProjectForm.tsx @@ -5,7 +5,7 @@ import { createResolver, projectSchema } from '@camp/domain'; import { messages } from '@camp/messages'; import type { AppRoute } from '@camp/router'; import { useNavigate } from '@camp/router'; -import { createTestAttr } from '@camp/test'; +import { tid } from '@camp/test'; import { isNull } from '@fullstacksjs/toolbox'; import { Button, Group, Stack, Textarea, TextInput } from '@mantine/core'; import { useForm } from 'react-hook-form'; @@ -51,7 +51,7 @@ export const CreateProjectForm = ({ dismiss }: Props) => { title: messages.projects.create, message: messages.projects.notification.successfulCreate(name), type: 'success', - ...createTestAttr(ids.notification.success), + ...tid(ids.notification.success), }); dismiss(); navigate({ to: `/dashboard/projects/${project.id}` as AppRoute }); @@ -62,7 +62,7 @@ export const CreateProjectForm = ({ dismiss }: Props) => { title: messages.projects.create, message: messages.projects.notification.failedCreate(name), type: 'failure', - ...createTestAttr(ids.notification.failure), + ...tid(ids.notification.failure), }); } }); @@ -79,7 +79,7 @@ export const CreateProjectForm = ({ dismiss }: Props) => { size="sm" error={errors.name?.message} {...register('name')} - wrapperProps={createTestAttr(ids.nameInput)} + wrapperProps={tid(ids.nameInput)} />