From d0f6b182938fede3dbb736f02fad81c7363ebdb7 Mon Sep 17 00:00:00 2001 From: BlackPoretsky <84518407+BlackPoretsky@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:17:29 +0300 Subject: [PATCH 01/44] Refactor: FormInput wrapped to forwardRef --- src/shared/ui/Input/FormInput/ui/FormInput.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/shared/ui/Input/FormInput/ui/FormInput.tsx b/src/shared/ui/Input/FormInput/ui/FormInput.tsx index ea7a95de..0d8d9009 100644 --- a/src/shared/ui/Input/FormInput/ui/FormInput.tsx +++ b/src/shared/ui/Input/FormInput/ui/FormInput.tsx @@ -1,16 +1,17 @@ import { Input } from '@shared/ui/Input'; import { clsx } from 'clsx'; +import { type ForwardedRef, forwardRef } from 'react'; import type { TFormInputProps } from '../types/TFormInputProps'; import st from './FormInput.module.scss'; -function FormInput(props: TFormInputProps) { +function FormInput(props: TFormInputProps, ref: ForwardedRef) { const { className, errorMessage, ...otherProps } = props; return (
- +

{errorMessage}

@@ -18,4 +19,4 @@ function FormInput(props: TFormInputProps) { ); } -export default FormInput; +export default forwardRef(FormInput); From 617e650576461a4b500ca0b697f9c4646a4894cd Mon Sep 17 00:00:00 2001 From: BlackPoretsky <84518407+BlackPoretsky@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:20:17 +0300 Subject: [PATCH 02/44] Fix: update Dialog component to delete click outside handler and ref correctly --- src/shared/ui/Dialog/ui/Dialog.module.scss | 4 ++++ src/shared/ui/Dialog/ui/Dialog.tsx | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/shared/ui/Dialog/ui/Dialog.module.scss b/src/shared/ui/Dialog/ui/Dialog.module.scss index 3c2c2159..9540a8ef 100644 --- a/src/shared/ui/Dialog/ui/Dialog.module.scss +++ b/src/shared/ui/Dialog/ui/Dialog.module.scss @@ -3,6 +3,10 @@ padding: 0; background: transparent; + &:where(:focus, :focus-visible, :focus-within) { + outline-color: transparent; + } + &::backdrop { background: rgba($color: var(--brand), $alpha: 0.5); } diff --git a/src/shared/ui/Dialog/ui/Dialog.tsx b/src/shared/ui/Dialog/ui/Dialog.tsx index bf7e25e3..95897aa4 100644 --- a/src/shared/ui/Dialog/ui/Dialog.tsx +++ b/src/shared/ui/Dialog/ui/Dialog.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useClickOutside } from '@shared/hooks'; import clsx from 'clsx'; import { type ForwardedRef, forwardRef, useImperativeHandle, useRef } from 'react'; @@ -12,10 +11,9 @@ function Dialog(props: TDialogProps, ref: ForwardedRef) { const dialogRef = useRef(null); useImperativeHandle(ref, () => dialogRef.current as HTMLDialogElement); - useClickOutside(dialogRef, () => dialogRef.current?.close()); return ( - + {children} ); From 64dee815a6f5878a12f35c3dbfb9b2ac8a913425 Mon Sep 17 00:00:00 2001 From: BlackPoretsky <84518407+BlackPoretsky@users.noreply.github.com> Date: Sat, 11 Jan 2025 22:20:31 +0300 Subject: [PATCH 03/44] Refactor: remove unused export from surreal index file --- src/settings/surreal/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/settings/surreal/index.ts b/src/settings/surreal/index.ts index 963cd2fe..1aa7bb2f 100644 --- a/src/settings/surreal/index.ts +++ b/src/settings/surreal/index.ts @@ -1,3 +1,2 @@ -export * from './createRequest'; export * from './surreal'; export { SurrealError } from './utils/surreal.error'; From 5906cf4c7f6184371ccf3946608eac01d4924d5c Mon Sep 17 00:00:00 2001 From: BlackPoretsky <84518407+BlackPoretsky@users.noreply.github.com> Date: Sun, 12 Jan 2025 00:43:06 +0300 Subject: [PATCH 04/44] Refactor: implement NewFolderModal with improved state management and modal handling --- src/widgets/Modal/NewFolderModal/index.ts | 3 + .../NewFolderModal/model/newFolder.model.ts | 25 +++++--- .../NewFolderModal/story/Story.stories.tsx | 20 +++++- .../NewFolderModal/ui/NewFolderModal.tsx | 46 +++++++++---- .../NewFolderModal/ui/useNewFolderModal.ts | 64 ++++++++++++++++++- 5 files changed, 135 insertions(+), 23 deletions(-) diff --git a/src/widgets/Modal/NewFolderModal/index.ts b/src/widgets/Modal/NewFolderModal/index.ts index e69de29b..213cfaba 100644 --- a/src/widgets/Modal/NewFolderModal/index.ts +++ b/src/widgets/Modal/NewFolderModal/index.ts @@ -0,0 +1,3 @@ +import NewFolderModal from './ui/NewFolderModal'; + +export default NewFolderModal; diff --git a/src/widgets/Modal/NewFolderModal/model/newFolder.model.ts b/src/widgets/Modal/NewFolderModal/model/newFolder.model.ts index 4cf625c4..4c4811b5 100644 --- a/src/widgets/Modal/NewFolderModal/model/newFolder.model.ts +++ b/src/widgets/Modal/NewFolderModal/model/newFolder.model.ts @@ -1,7 +1,7 @@ import type Surreal from 'surrealdb'; import { createMutation, createQuery } from '@farfetched/core'; -import { $db } from '@settings/surreal'; +import { $db, SurrealError } from '@settings/surreal'; import { createFormInput } from '@shared/factories'; import { displayRequestError, displayRequestSuccess, type TTranslationOptions } from '@widgets/ToastNotification'; import { createEffect, createEvent, createStore, sample, type StoreWritable } from 'effector'; @@ -9,17 +9,21 @@ import { and, not, reset } from 'patronum'; import { z } from 'zod'; const $modalRef = createStore(null); -export const modalRefChanged = createEvent(); +export const modalRefChanged = createEvent(); -const NameSchema = z.string().min(1, 'empty'); +const NameSchema = z.string().trim().min(1, 'empty'); export const input = createFormInput('name', '', NameSchema); export const createFolder = createEvent(); export const closeModal = createEvent(); -const closeModalFx = createEffect((ref) => ref?.close()); +const closeModalFx = createEffect((ref) => { + if (ref) ref.close(); +}); const countDuplicateMut = createQuery<[{ db: Surreal; name: string }], number>({ handler: async ({ db, name }) => { + if (!(await db.info())?.id) throw SurrealError.DatabaseUnauthorized(); + const [result] = await db.query<[{ count: number }]>( /* surql */ ` SELECT count() as count FROM folder WHERE $auth.id = author @@ -37,11 +41,13 @@ const countDuplicateMut = createQuery<[{ db: Surreal; name: string }], number>({ const createFolderMut = createMutation<{ db: Surreal; name: string }, void>({ handler: async ({ db, name }) => { + if (!(await db.info())?.id) throw SurrealError.DatabaseUnauthorized(); + await db.query( /*surql */ ` CREATE folder CONTENT { name: string::trim($name) -s } + } `, { name @@ -56,11 +62,14 @@ export const $creationInProgress = and(countDuplicateMut.$pending, createFolderM $modalRef.on(modalRefChanged, (_, ref) => ref); // Reset model -reset({ clock: closeModal, target: [input.$nameInput, input.$nameInputError, input.$nameInputRef, $modalRef] }); -sample({ clock: closeModal, target: [countDuplicateMut.reset, createFolderMut.reset] }); +reset({ + clock: closeModalFx.finally, + target: [input.$nameInput, input.$nameInputError] +}); +sample({ clock: closeModalFx.finally, target: [countDuplicateMut.reset, createFolderMut.reset] }); // Close modal -sample({ clock: closeModal, source: $modalRef, filter: (ref) => !!ref, target: closeModalFx }); +sample({ clock: closeModal, source: $modalRef, target: closeModalFx }); // Checking the entered name sample({ diff --git a/src/widgets/Modal/NewFolderModal/story/Story.stories.tsx b/src/widgets/Modal/NewFolderModal/story/Story.stories.tsx index 424e53d2..f441b54c 100644 --- a/src/widgets/Modal/NewFolderModal/story/Story.stories.tsx +++ b/src/widgets/Modal/NewFolderModal/story/Story.stories.tsx @@ -1,4 +1,6 @@ +import { Button } from '@shared/ui/Button'; import { type Meta, type StoryObj } from '@storybook/react'; +import { useRef } from 'react'; import NewFolderModal from '../ui/NewFolderModal'; @@ -12,7 +14,19 @@ export default meta; type NewFolderModalStory = StoryObj; export const Default: NewFolderModalStory = { - parameters: { - layout: '' - } + parameters: {}, + render: () => }; + +function WithToggleButton() { + const ref = useRef(null); + + return ( + <> + + + + ); +} diff --git a/src/widgets/Modal/NewFolderModal/ui/NewFolderModal.tsx b/src/widgets/Modal/NewFolderModal/ui/NewFolderModal.tsx index f3f0db52..6959ad2f 100644 --- a/src/widgets/Modal/NewFolderModal/ui/NewFolderModal.tsx +++ b/src/widgets/Modal/NewFolderModal/ui/NewFolderModal.tsx @@ -2,33 +2,57 @@ import { SuccessIcon } from '@shared/icons'; import { Button } from '@shared/ui/Button'; import { Dialog } from '@shared/ui/Dialog'; import { FormInput } from '@shared/ui/Input'; -import { createPortal } from 'react-dom'; +import { type ChangeEvent, type ForwardedRef, forwardRef } from 'react'; import { useTranslation } from 'react-i18next'; import st from './NewFolderModal.module.scss'; +import { useNewFolderModal } from './useNewFolderModal'; -function NewFolderModal() { +function NewFolderModal(_: object, ref: ForwardedRef) { const { t } = useTranslation('modal', { keyPrefix: 'createFolder' }); + const { + creationInProgress, + modalRef, + nameInput, + nameInputError, + onCloseModal, + onCreateFolder, + onNameInputChanged, + onNameInputRefChanged + } = useNewFolderModal(ref); - return createPortal( - -
+ return ( + +
{ + event.preventDefault(); + onCreateFolder(); + }} + >

{t('title')}

- + ) => onNameInputChanged(event.target.value)} + placeholder={t('input.placeholder')} + ref={onNameInputRefChanged} + size={'large'} + value={nameInput} + />
-
-
-
, - document.body + +
); } -export default NewFolderModal; +export default forwardRef(NewFolderModal); diff --git a/src/widgets/Modal/NewFolderModal/ui/useNewFolderModal.ts b/src/widgets/Modal/NewFolderModal/ui/useNewFolderModal.ts index 4b1aa3ef..256546e5 100644 --- a/src/widgets/Modal/NewFolderModal/ui/useNewFolderModal.ts +++ b/src/widgets/Modal/NewFolderModal/ui/useNewFolderModal.ts @@ -1 +1,63 @@ -const useNewFolderModal = () => {}; +import { useUnit } from 'effector-react'; +import { type ForwardedRef, useEffect, useImperativeHandle, useRef } from 'react'; + +import { $creationInProgress, closeModal, createFolder, input, modalRefChanged } from '../model/newFolder.model'; + +export const useNewFolderModal = (ref: ForwardedRef) => { + const [ + creationInProgress, + onCreateFolder, + onCloseModal, + onModalRefChanged, + nameInput, + nameInputError, + onNameInputChanged, + onNameInputRefChanged + ] = useUnit([ + $creationInProgress, + createFolder, + closeModal, + modalRefChanged, + input.$nameInput, + input.$nameInputError, + input.nameInputChanged, + input.nameInputRefChanged + ]); + const modalRef = useRef(null); + + useImperativeHandle(ref, () => modalRef.current as HTMLDialogElement); + + useEffect(() => { + if (modalRef.current) onModalRefChanged(modalRef.current); + + return () => { + onModalRefChanged(null); + }; + }, [onModalRefChanged]); + + useEffect(() => { + const modal = modalRef.current; + + const clickOutside = (event: MouseEvent) => { + if (event.target instanceof Node && modal && (!modal.contains(event.target) || modal == event.target)) + onCloseModal(); + }; + + if (modal) document.addEventListener('mousedown', clickOutside); + + return () => { + if (modal) document.removeEventListener('mousedown', clickOutside); + }; + }, [onCloseModal]); + + return { + creationInProgress, + modalRef, + nameInput, + nameInputError, + onCloseModal, + onCreateFolder, + onNameInputChanged, + onNameInputRefChanged + }; +}; From 65f1582bc81af39f989e84a549ee574b7c9322a2 Mon Sep 17 00:00:00 2001 From: BlackPoretsky <84518407+BlackPoretsky@users.noreply.github.com> Date: Sun, 12 Jan 2025 00:43:17 +0300 Subject: [PATCH 05/44] Refactor: integrate NewFolderModal into Header component with modal handling --- src/modules/header/view/Header.tsx | 18 +++++++++++++++--- src/modules/header/vm/useHeader.ts | 8 ++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/modules/header/view/Header.tsx b/src/modules/header/view/Header.tsx index 1f328982..9e8fdf5d 100644 --- a/src/modules/header/view/Header.tsx +++ b/src/modules/header/view/Header.tsx @@ -3,6 +3,7 @@ import { AddIcon, ExploreIcon, FolderIcon, HomeIcon, InterestsIcon, Notification import { Button, IconButton } from '@shared/ui/Button'; import { NavigationLink } from '@shared/ui/Link'; import { Separator } from '@shared/ui/Separator'; +import NewFolderModal from '@widgets/Modal/NewFolderModal'; import { Link } from 'atomic-router-react'; import { useTranslation } from 'react-i18next'; @@ -11,8 +12,18 @@ import { useHeader } from '../vm/useHeader'; import st from './Header.module.scss'; function Header() { - const { currentLanguage, folders, onLanguageSwitch, onLogout, onToAuth, onToSettings, recent, sessionForHeader } = - useHeader(); + const { + currentLanguage, + folders, + newFolderModalRef, + onLanguageSwitch, + onLogout, + onOpenNewFolderModal, + onToAuth, + onToSettings, + recent, + sessionForHeader + } = useHeader(); const { t } = useTranslation('header'); return ( @@ -57,9 +68,10 @@ function Header() {

{t('yourFolders')}

- + +