diff --git a/package-lock.json b/package-lock.json
index 717d7ed..087a80b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"@internxt/css-config": "^1.1.0",
+ "@internxt/lib": "^1.4.1",
"@internxt/sdk": "^1.15.1",
"@internxt/ui": "^0.1.11",
"@phosphor-icons/react": "^2.1.10",
@@ -1173,6 +1174,28 @@
"typescript": ">=4.2.0"
}
},
+ "node_modules/@internxt/lib": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/@internxt/lib/-/lib-1.4.1.tgz",
+ "integrity": "sha512-sWNp57IKCk0HjzTdPSuxOgZWvrSDWGYrzNOq90LIZTzr1HwkxObicUaZqSzmw4uDKrJhsdFdzwdywk3g8gwDDA==",
+ "license": "MIT",
+ "dependencies": {
+ "uuid": "^11.1.0"
+ }
+ },
+ "node_modules/@internxt/lib/node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
"node_modules/@internxt/prettier-config": {
"version": "1.0.2",
"resolved": "git+ssh://git@github.com/internxt/prettier-config.git#9fa74e9a2805e1538b50c3809324f1c9d0f3e4f9",
diff --git a/package.json b/package.json
index 69af1b8..88ccd86 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
},
"dependencies": {
"@internxt/css-config": "^1.1.0",
+ "@internxt/lib": "^1.4.1",
"@internxt/sdk": "^1.15.1",
"@internxt/ui": "^0.1.11",
"@phosphor-icons/react": "^2.1.10",
diff --git a/src/App.tsx b/src/App.tsx
index c109209..a444e2f 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,10 +1,12 @@
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
import { routes } from './routes';
import { NavigationService } from './services/navigation';
-import { useEffect } from 'react';
+import { Activity, useEffect } from 'react';
import { useAppDispatch } from './store/hooks';
import { initializeUserThunk } from './store/slices/user/thunks';
import { Toaster } from 'react-hot-toast';
+import { ActionDialog, useActionDialog } from './context/dialog-manager';
+import { ComposeMessageDialog } from './components/compose-message';
const router = createBrowserRouter(routes);
const navigation = router.navigate;
@@ -13,6 +15,8 @@ NavigationService.instance.init(navigation);
function App() {
const dispatch = useAppDispatch();
+ const { isDialogOpen } = useActionDialog();
+ const isComposeMessageDialogOpen = isDialogOpen(ActionDialog.ComposeMessage);
useEffect(() => {
dispatch(initializeUserThunk());
@@ -27,6 +31,9 @@ function App() {
}}
/>
+
+
+
>
);
}
diff --git a/src/components/Sidenav/index.tsx b/src/components/Sidenav/index.tsx
index b2e752e..36c535d 100644
--- a/src/components/Sidenav/index.tsx
+++ b/src/components/Sidenav/index.tsx
@@ -1,6 +1,6 @@
import { useState } from 'react';
-import { Sidenav as SidenavComponent } from '@internxt/ui';
+import { Button, Sidenav as SidenavComponent } from '@internxt/ui';
import logo from '../../assets/logos/Internxt/small-logo.svg';
import { useTranslationContext } from '@/i18n';
import { NavigationService } from '@/services/navigation';
@@ -11,7 +11,8 @@ import { useSuiteLauncher } from '@/hooks/navigation/useSuiteLauncher';
import { useSidenavNavigation } from '@/hooks/navigation/useSidenavNavigation';
import { useGetStorageLimitQuery, useGetStorageUsageQuery } from '@/store/queries/storage/storage.query';
import { useAppSelector } from '@/store/hooks';
-import { bytesToString } from '@/utils/bytesToString';
+import { bytesToString } from '@/utils/bytes-to-string';
+import { ActionDialog, useActionDialog } from '@/context/dialog-manager';
const Sidenav = () => {
const { translate } = useTranslationContext();
@@ -19,6 +20,7 @@ const Sidenav = () => {
const { isLoading: isLoadingPlanLimit, data: planLimit = 1 } = useGetStorageLimitQuery();
const { isLoading: isLoadingPlanUsage, data: planUsage = 0 } = useGetStorageUsageQuery();
const storagePercentage = planLimit > 0 ? Math.min((planUsage / planLimit) * 100, 100) : 0;
+ const { openDialog } = useActionDialog();
const { itemsNavigation } = useSidenavNavigation();
const { suiteArray } = useSuiteLauncher();
@@ -48,6 +50,10 @@ const Sidenav = () => {
});
};
+ const onPrimaryActionClicked = () => {
+ openDialog(ActionDialog.ComposeMessage);
+ };
+
return (
{
onClick: onLogoClicked,
className: '!pt-0 pb-3',
}}
+ primaryAction={
+
+ }
suiteLauncher={{
suiteArray: suiteArray,
soonText: translate('modals.upgradePlanDialog.soonBadge'),
@@ -69,7 +80,7 @@ const Sidenav = () => {
limit: bytesToString({ size: planLimit }),
percentage: storagePercentage,
onUpgradeClick: () => {},
- upgradeLabel: isUpgradeAvailable() ? translate('preferences.account.plans.upgrade') : undefined,
+ upgradeLabel: isUpgradeAvailable() ? translate('actions.upgrade') : undefined,
isLoading: isLoadingPlanUsage || isLoadingPlanLimit,
}}
/>
diff --git a/src/components/compose-message/components/RecipientInput.tsx b/src/components/compose-message/components/RecipientInput.tsx
index 7ac10fa..9f76ae2 100644
--- a/src/components/compose-message/components/RecipientInput.tsx
+++ b/src/components/compose-message/components/RecipientInput.tsx
@@ -1,6 +1,8 @@
import { useState, type KeyboardEvent } from 'react';
import type { Recipient } from '../types';
import UserChip from '@/components/user-chip';
+import { DEFAULT_USER_NAME } from '@/constants';
+import isValidEmail from '@internxt/lib/dist/src/auth/isValidEmail';
interface RecipientInputProps {
label: string;
@@ -37,7 +39,7 @@ export const RecipientInput = ({
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
const email = inputValue.trim().replace(/,$/, '');
- if (email) {
+ if (email && isValidEmail(email)) {
onAddRecipient(email);
setInputValue('');
}
@@ -48,7 +50,7 @@ export const RecipientInput = ({
const handleBlur = () => {
const email = inputValue.trim();
- if (email) {
+ if (email && isValidEmail(email)) {
onAddRecipient(email);
setInputValue('');
}
@@ -60,15 +62,15 @@ export const RecipientInput = ({
{recipients.map((recipient) => (
onRemoveRecipient(recipient.id)}
/>
))}
setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
diff --git a/src/components/compose-message/components/RichTextEditor.tsx b/src/components/compose-message/components/RichTextEditor.tsx
new file mode 100644
index 0000000..3ba29e0
--- /dev/null
+++ b/src/components/compose-message/components/RichTextEditor.tsx
@@ -0,0 +1,13 @@
+import { EditorContent, Editor } from '@tiptap/react';
+
+export interface RichTextEditorProps {
+ editor: Editor | null;
+}
+
+const RichTextEditor = ({ editor }: RichTextEditorProps) => (
+
+
+
+);
+
+export default RichTextEditor;
diff --git a/src/components/compose-message/components/editorBar/EditorBarButton.tsx b/src/components/compose-message/components/editorBar/EditorBarButton.tsx
new file mode 100644
index 0000000..8b91a72
--- /dev/null
+++ b/src/components/compose-message/components/editorBar/EditorBarButton.tsx
@@ -0,0 +1,17 @@
+export interface EditorBarButtonProps {
+ onClick: () => void;
+ isActive?: boolean;
+ disabled?: boolean;
+ children: React.ReactNode;
+}
+
+export const EditorBarButton = ({ onClick, isActive, disabled, children }: EditorBarButtonProps) => (
+
+);
diff --git a/src/components/compose-message/components/editorBar/EditorBarGroup.tsx b/src/components/compose-message/components/editorBar/EditorBarGroup.tsx
new file mode 100644
index 0000000..8157f30
--- /dev/null
+++ b/src/components/compose-message/components/editorBar/EditorBarGroup.tsx
@@ -0,0 +1,17 @@
+import type { EditorBarItem } from '../../types';
+import { EditorBarButton } from './EditorBarButton';
+
+export interface EditorBarGroupProps {
+ items: EditorBarItem[];
+ disabled?: boolean;
+}
+
+export const EditorBarGroup = ({ items, disabled }: EditorBarGroupProps) => (
+
+ {items.map((item) => (
+
+
+
+ ))}
+
+);
diff --git a/src/components/compose-message/components/editorBar/index.tsx b/src/components/compose-message/components/editorBar/index.tsx
new file mode 100644
index 0000000..53a0508
--- /dev/null
+++ b/src/components/compose-message/components/editorBar/index.tsx
@@ -0,0 +1,245 @@
+import {
+ TextBIcon,
+ TextItalicIcon,
+ TextUnderlineIcon,
+ TextStrikethroughIcon,
+ ListBulletsIcon,
+ ListNumbersIcon,
+ TextAlignLeftIcon,
+ TextAlignCenterIcon,
+ TextAlignRightIcon,
+ LinkIcon,
+ EraserIcon,
+ ImageIcon,
+ CaretDownIcon,
+ PaintBucketIcon,
+} from '@phosphor-icons/react';
+import { Editor } from '@tiptap/react';
+import { EditorBarButton } from './EditorBarButton';
+import { EditorBarGroup } from './EditorBarGroup';
+import type { EditorBarItem } from '../../types';
+import { useEditorBar } from '../../hooks/useEditorBar';
+import { Activity } from 'react';
+import { COLORS, FONT_SIZES, FONTS } from '../../config';
+
+export interface EditorBarProps {
+ editor: Editor;
+ disabled?: boolean;
+}
+
+export const EditorBar = ({ editor, disabled }: EditorBarProps) => {
+ const {
+ showColorPicker,
+ showFontPicker,
+ showSizePicker,
+ currentFont,
+ currentSize,
+ colorPickerRef,
+ fontPickerRef,
+ sizePickerRef,
+ setShowColorPicker,
+ setShowFontPicker,
+ setShowSizePicker,
+ setLink,
+ addImage,
+ setColor,
+ setFont,
+ setFontSize,
+ } = useEditorBar(editor);
+
+ const textStyles = [
+ {
+ id: 'bold',
+ icon: TextBIcon,
+ onClick: () => editor.chain().focus().toggleBold().run(),
+ isActive: editor.isActive('bold'),
+ },
+ {
+ id: 'italic',
+ icon: TextItalicIcon,
+ onClick: () => editor.chain().focus().toggleItalic().run(),
+ isActive: editor.isActive('italic'),
+ },
+ {
+ id: 'underline',
+ icon: TextUnderlineIcon,
+ onClick: () => editor.chain().focus().toggleUnderline().run(),
+ isActive: editor.isActive('underline'),
+ },
+ {
+ id: 'strike',
+ icon: TextStrikethroughIcon,
+ onClick: () => editor.chain().focus().toggleStrike().run(),
+ isActive: editor.isActive('strike'),
+ },
+ ] satisfies EditorBarItem[];
+
+ const textList = [
+ {
+ id: 'bulletList',
+ icon: ListBulletsIcon,
+ onClick: () => editor.chain().focus().toggleBulletList().run(),
+ isActive: editor.isActive('bulletList'),
+ },
+ {
+ id: 'orderedList',
+ icon: ListNumbersIcon,
+ onClick: () => editor.chain().focus().toggleOrderedList().run(),
+ isActive: editor.isActive('orderedList'),
+ },
+ ] satisfies EditorBarItem[];
+
+ const textAlignment = [
+ {
+ id: 'alignLeft',
+ icon: TextAlignLeftIcon,
+ onClick: () => editor.chain().focus().setTextAlign('left').run(),
+ isActive: editor.isActive({ textAlign: 'left' }),
+ },
+ {
+ id: 'alignCenter',
+ icon: TextAlignCenterIcon,
+ onClick: () => editor.chain().focus().setTextAlign('center').run(),
+ isActive: editor.isActive({ textAlign: 'center' }),
+ },
+ {
+ id: 'alignRight',
+ icon: TextAlignRightIcon,
+ onClick: () => editor.chain().focus().setTextAlign('right').run(),
+ isActive: editor.isActive({ textAlign: 'right' }),
+ },
+ ] satisfies EditorBarItem[];
+
+ const messageAttachment = [
+ {
+ id: 'link',
+ icon: LinkIcon,
+ onClick: setLink,
+ isActive: editor.isActive('link'),
+ },
+ {
+ id: 'clear',
+ icon: EraserIcon,
+ onClick: () => editor.chain().focus().unsetAllMarks().clearNodes().run(),
+ },
+ ] satisfies EditorBarItem[];
+
+ return (
+
+ {/* Font selector */}
+
+
+
+
+ {FONTS.map((font) => (
+
+ ))}
+
+
+
+
+ {/* Size selector */}
+
+
+
+
+ {FONT_SIZES.map((size) => (
+
+ ))}
+
+
+
+
+ {/* Color picker */}
+
+
setShowColorPicker(!showColorPicker)} disabled={disabled}>
+
+
+
+
+
+ {COLORS.map((color) => (
+
+
+
+
+
+ {/* Text styles */}
+
+
+ {/* Lists */}
+
+
+ {/* Text alignment */}
+
+
+ {/* Link, clear, image */}
+
+
+ {/* Image */}
+
+
+
+
+ );
+};
diff --git a/src/components/compose-message/hooks/useEditorBar.ts b/src/components/compose-message/hooks/useEditorBar.ts
index 6f64338..a35ac90 100644
--- a/src/components/compose-message/hooks/useEditorBar.ts
+++ b/src/components/compose-message/hooks/useEditorBar.ts
@@ -1,5 +1,6 @@
import { useCallback, useState, useRef, useEffect, useReducer } from 'react';
import { Editor } from '@tiptap/react';
+import { useClickOutside } from '@/hooks/useClickOutside';
export const useEditorBar = (editor: Editor | null) => {
const [showColorPicker, setShowColorPicker] = useState(false);
@@ -25,22 +26,9 @@ export const useEditorBar = (editor: Editor | null) => {
};
}, [editor]);
- useEffect(() => {
- const handleClickOutside = (event: MouseEvent) => {
- if (colorPickerRef.current && !colorPickerRef.current.contains(event.target as Node)) {
- setShowColorPicker(false);
- }
- if (fontPickerRef.current && !fontPickerRef.current.contains(event.target as Node)) {
- setShowFontPicker(false);
- }
- if (sizePickerRef.current && !sizePickerRef.current.contains(event.target as Node)) {
- setShowSizePicker(false);
- }
- };
-
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }, []);
+ useClickOutside(colorPickerRef, () => setShowColorPicker(false));
+ useClickOutside(fontPickerRef, () => setShowFontPicker(false));
+ useClickOutside(sizePickerRef, () => setShowSizePicker(false));
// !TODO: use custom modal to attach a URL
const setLink = useCallback(() => {
diff --git a/src/components/compose-message/index.tsx b/src/components/compose-message/index.tsx
new file mode 100644
index 0000000..00d022a
--- /dev/null
+++ b/src/components/compose-message/index.tsx
@@ -0,0 +1,148 @@
+import { PaperclipIcon, XIcon } from '@phosphor-icons/react';
+import { useCallback } from 'react';
+import type { Recipient } from './types';
+import { RecipientInput } from './components/RecipientInput';
+import { Button, Input } from '@internxt/ui';
+import RichTextEditor from './components/RichTextEditor';
+import { EditorBar } from './components/editorBar';
+import { ActionDialog, useActionDialog } from '@/context/dialog-manager';
+import { useTranslationContext } from '@/i18n';
+import useComposeMessage from './hooks/useComposeMessage';
+import { useEditor } from '@tiptap/react';
+import { EDITOR_CONFIG } from './config';
+
+export interface DraftMessage {
+ subject?: string;
+ to?: Recipient[];
+ cc?: Recipient[];
+ bcc?: Recipient[];
+ body?: string;
+}
+
+export const ComposeMessageDialog = () => {
+ const { translate } = useTranslationContext();
+ const { closeDialog: onComposeMessageDialogClose, getDialogData: getComposeMessageDialogData } = useActionDialog();
+
+ const draft = (getComposeMessageDialogData(ActionDialog.ComposeMessage) ?? {}) as DraftMessage;
+ const {
+ showBcc,
+ showCc,
+ subjectValue,
+ toRecipients,
+ bccRecipients,
+ ccRecipients,
+ onAddBccRecipient,
+ onAddCcRecipient,
+ onRemoveBccRecipient,
+ onAddToRecipient,
+ onRemoveCcRecipient,
+ onRemoveToRecipient,
+ onShowBccRecipient,
+ onShowCcRecipient,
+ onSubjectChange,
+ } = useComposeMessage();
+
+ const title = draft.subject ?? translate('modals.composeMessageDialog.title');
+ const editor = useEditor(EDITOR_CONFIG);
+
+ const onClose = useCallback(() => {
+ onComposeMessageDialogClose(ActionDialog.ComposeMessage);
+ }, [onComposeMessageDialogClose]);
+
+ const handlePrimaryAction = useCallback(() => {
+ const html = editor?.getHTML();
+ console.log('html', html);
+ onClose();
+ }, [editor, onClose]);
+
+ if (!editor) return null;
+
+ return (
+
+
+
+
+
+
+
onAddToRecipient?.(email)}
+ onRemoveRecipient={(id) => onRemoveToRecipient?.(id)}
+ showCcBcc
+ onCcClick={onShowCcRecipient}
+ onBccClick={onShowBccRecipient}
+ showCcButton={!showCc}
+ showBccButton={!showBcc}
+ ccButtonText={translate('modals.composeMessageDialog.cc')}
+ bccButtonText={translate('modals.composeMessageDialog.bcc')}
+ disabled={false}
+ />
+ {showCc && (
+ onAddCcRecipient?.(email)}
+ onRemoveRecipient={(id) => onRemoveCcRecipient?.(id)}
+ disabled={false}
+ />
+ )}
+ {showBcc && (
+ onAddBccRecipient?.(email)}
+ onRemoveRecipient={(id) => onRemoveBccRecipient?.(id)}
+ disabled={false}
+ />
+ )}
+
+
+ {translate('modals.composeMessageDialog.subject')}
+
+
+
+
+
+
+
+
+
+ {/* !TODO: Handle attachments */}
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/constants.ts b/src/constants.ts
index 7bfa0b7..11fa395 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -6,4 +6,5 @@ export const INTERNXT_BASE_URL = 'https://internxt.com';
export const HUNDRED_TB = 100 * 1024 * 1024 * 1024 * 1024;
+export const DEFAULT_USER_NAME = 'My Internxt';
export const INTERNXT_EMAIL_DOMAINS = ['@inxt.me', '@inxt.eu', '@encrypt.eu'] as const;
diff --git a/src/hooks/useClickOutside.test.ts b/src/hooks/useClickOutside.test.ts
new file mode 100644
index 0000000..805a04d
--- /dev/null
+++ b/src/hooks/useClickOutside.test.ts
@@ -0,0 +1,73 @@
+import { renderHook } from '@testing-library/react';
+import { describe, test, expect, vi, beforeEach } from 'vitest';
+import { useRef } from 'react';
+import { useClickOutside } from './useClickOutside';
+
+describe('Click outside - Custom hook', () => {
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ test('When clicking outside the ref element, then the function should be called', () => {
+ const onClickOutside = vi.fn();
+ const { result } = renderHook(() => {
+ const ref = useRef(document.createElement('div'));
+ useClickOutside(ref, onClickOutside);
+ return ref;
+ });
+
+ document.body.appendChild(result.current.current!);
+
+ const outsideElement = document.createElement('button');
+ document.body.appendChild(outsideElement);
+ outsideElement.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
+
+ expect(onClickOutside).toHaveBeenCalledOnce();
+
+ document.body.removeChild(result.current.current!);
+ document.body.removeChild(outsideElement);
+ });
+
+ test('When clicking inside the ref element, then it should do nothing', () => {
+ const onClickOutside = vi.fn();
+ const { result } = renderHook(() => {
+ const ref = useRef(document.createElement('div'));
+ useClickOutside(ref, onClickOutside);
+ return ref;
+ });
+
+ document.body.appendChild(result.current.current!);
+
+ result.current.current!.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
+
+ expect(onClickOutside).not.toHaveBeenCalled();
+
+ document.body.removeChild(result.current.current!);
+ });
+
+ test('When ref is null, then it should do nothing', () => {
+ const onClickOutside = vi.fn();
+ renderHook(() => {
+ const ref = useRef(null);
+ useClickOutside(ref, onClickOutside);
+ });
+
+ document.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
+
+ expect(onClickOutside).not.toHaveBeenCalled();
+ });
+
+ test('When the hook unmounts, then it should remove the event listener', () => {
+ const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
+ const onClickOutside = vi.fn();
+
+ const { unmount } = renderHook(() => {
+ const ref = useRef(document.createElement('div'));
+ useClickOutside(ref, onClickOutside);
+ });
+
+ unmount();
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('mousedown', expect.any(Function));
+ });
+});
diff --git a/src/hooks/useClickOutside.ts b/src/hooks/useClickOutside.ts
new file mode 100644
index 0000000..cb3c614
--- /dev/null
+++ b/src/hooks/useClickOutside.ts
@@ -0,0 +1,13 @@
+import { useEffect, type RefObject } from 'react';
+
+export const useClickOutside = (ref: RefObject, onClickOutside: () => void) => {
+ useEffect(() => {
+ const handler = (event: MouseEvent) => {
+ if (ref.current && !ref.current.contains(event.target as Node)) {
+ onClickOutside();
+ }
+ };
+ document.addEventListener('mousedown', handler);
+ return () => document.removeEventListener('mousedown', handler);
+ }, [ref, onClickOutside]);
+};
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 0dfa486..6aaa4e2 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -7,7 +7,9 @@
"newMessage": "New Message",
"search": "Search",
"trashAll": "Move all to trash",
- "archiveAll": "Move all to archive"
+ "archiveAll": "Move all to archive",
+ "upgrade": "Upgrade",
+ "send": "Send"
},
"filter": {
"all": "All",
@@ -114,6 +116,14 @@
"description": "Upgrade now to keep your files optimized and free up space."
}
},
+ "composeMessageDialog": {
+ "title": "New Message",
+ "to": "To",
+ "cc": "Cc",
+ "bcc": "Bcc",
+ "subject": "Subject",
+ "message": "Message"
+ },
"preferences": {
"title": "Preferences",
"sections": {
diff --git a/src/i18n/provider/TranslationProvider.tsx b/src/i18n/provider/TranslationProvider.tsx
index 59e21b1..063c9a0 100644
--- a/src/i18n/provider/TranslationProvider.tsx
+++ b/src/i18n/provider/TranslationProvider.tsx
@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import type { Translate, TranslateArray } from '../types';
import { TranslationContext } from './useTranslationContext';
+import type { Translate, TranslateArray } from '@/i18n';
export interface TranslationContextProps {
translate: Translate;
diff --git a/src/main.tsx b/src/main.tsx
index 98ca6eb..8864e7e 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -6,18 +6,21 @@ import { TranslationProvider } from './i18n/index.ts';
import { store } from './store/index.ts';
import { userActions } from './store/slices/user/index.ts';
import { Provider } from 'react-redux';
+import { DialogManagerProvider } from './context/dialog-manager/DialogManager.context.tsx';
import { ThemeProvider } from './context/theme/ThemeProvider.tsx';
store.dispatch(userActions.initialize());
createRoot(document.getElementById('root')!).render(
-
-
+
+
+
-
-
+
+
+
,
);
diff --git a/src/utils/bytesToString/bytesToString.test.ts b/src/utils/bytes-to-string/bytesToString.test.ts
similarity index 100%
rename from src/utils/bytesToString/bytesToString.test.ts
rename to src/utils/bytes-to-string/bytesToString.test.ts
diff --git a/src/utils/bytesToString/index.ts b/src/utils/bytes-to-string/index.ts
similarity index 100%
rename from src/utils/bytesToString/index.ts
rename to src/utils/bytes-to-string/index.ts