From 376e9e32c38f652a9a168b28e48b57bd457dd70f Mon Sep 17 00:00:00 2001 From: Sonny Date: Thu, 21 Aug 2025 15:59:03 +0200 Subject: [PATCH] feat: create dedicated settings page instead of creating many modals --- .../show_user_settings_controller.ts | 10 +++ app/user_settings/routes/routes.ts | 1 + .../routes/user_settings_routes.ts | 8 ++ .../common/floating_navbar/user_dropdown.tsx | 16 +--- .../floating_tabs/floating_tabs.module.css | 36 +++++++++ .../common/floating_tabs/floating_tabs.tsx | 79 +++++++++++++++++++ .../user_preferences/user_preferences.tsx | 17 +--- .../collection/collection_list_selector.tsx | 20 +++-- .../dashboard/link/link_list_selector.tsx | 28 +++++++ inertia/i18n/locales/en/common.json | 5 +- inertia/i18n/locales/fr/common.json | 5 +- inertia/layouts/small_content.tsx | 5 +- inertia/lib/display_preferences.tsx | 34 -------- inertia/pages/user_settings/show.tsx | 24 ++++++ package.json | 1 + start/routes.ts | 1 + 16 files changed, 219 insertions(+), 71 deletions(-) create mode 100644 app/user_settings/controllers/show_user_settings_controller.ts create mode 100644 app/user_settings/routes/routes.ts create mode 100644 app/user_settings/routes/user_settings_routes.ts create mode 100644 inertia/components/common/floating_tabs/floating_tabs.module.css create mode 100644 inertia/components/common/floating_tabs/floating_tabs.tsx create mode 100644 inertia/components/dashboard/link/link_list_selector.tsx delete mode 100644 inertia/lib/display_preferences.tsx create mode 100644 inertia/pages/user_settings/show.tsx diff --git a/app/user_settings/controllers/show_user_settings_controller.ts b/app/user_settings/controllers/show_user_settings_controller.ts new file mode 100644 index 0000000..7fd5841 --- /dev/null +++ b/app/user_settings/controllers/show_user_settings_controller.ts @@ -0,0 +1,10 @@ +import { HttpContext } from '@adonisjs/core/http'; + +export default class ShowUserSettingsController { + public async render({ auth, inertia }: HttpContext) { + const user = await auth.authenticate(); + return inertia.render('user_settings/show', { + user, + }); + } +} diff --git a/app/user_settings/routes/routes.ts b/app/user_settings/routes/routes.ts new file mode 100644 index 0000000..4f62e47 --- /dev/null +++ b/app/user_settings/routes/routes.ts @@ -0,0 +1 @@ +import './user_settings_routes.js'; diff --git a/app/user_settings/routes/user_settings_routes.ts b/app/user_settings/routes/user_settings_routes.ts new file mode 100644 index 0000000..265a370 --- /dev/null +++ b/app/user_settings/routes/user_settings_routes.ts @@ -0,0 +1,8 @@ +import router from '@adonisjs/core/services/router'; + +const ShowUserSettingsController = () => + import('#user_settings/controllers/show_user_settings_controller'); + +router + .get('/user/settings', [ShowUserSettingsController, 'render']) + .as('user.settings'); diff --git a/inertia/components/common/floating_navbar/user_dropdown.tsx b/inertia/components/common/floating_navbar/user_dropdown.tsx index acf37b4..c70037f 100644 --- a/inertia/components/common/floating_navbar/user_dropdown.tsx +++ b/inertia/components/common/floating_navbar/user_dropdown.tsx @@ -1,11 +1,9 @@ import { Avatar, Group, Menu, Text, UnstyledButton } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; -import { modals } from '@mantine/modals'; import cx from 'clsx'; import { useTranslation } from 'react-i18next'; import { TbChevronDown, TbLogout, TbSettings, TbShield } from 'react-icons/tb'; import { InternalLinkUnstyled } from '~/components/common/links/internal_link_unstyled'; -import { UserPreferences } from '~/components/common/user_preferences/user_preferences'; import { useAuth } from '~/hooks/use_auth'; import classes from './user_dropdown.module.css'; @@ -15,13 +13,6 @@ export function UserDropdown() { useDisclosure(false); const { t } = useTranslation(); - const handlePreferencesModal = () => { - modals.open({ - title: t('user-preferences'), - children: , - }); - }; - return ( )} - {t('common:settings')} + {t('common:user')} } - onClick={handlePreferencesModal} + component={InternalLinkUnstyled} + href="/user/settings" > - {t('common:preferences')} + {t('common:settings')} } diff --git a/inertia/components/common/floating_tabs/floating_tabs.module.css b/inertia/components/common/floating_tabs/floating_tabs.module.css new file mode 100644 index 0000000..1b4eb02 --- /dev/null +++ b/inertia/components/common/floating_tabs/floating_tabs.module.css @@ -0,0 +1,36 @@ +.list { + position: relative; + margin-bottom: var(--mantine-spacing-md); +} + +.indicator { + z-index: -1 !important; + background-color: var(--mantine-color-white); + border-radius: var(--mantine-radius-md); + border: 1px solid var(--mantine-color-gray-2); + box-shadow: var(--mantine-shadow-sm); + + @mixin dark { + background-color: var(--mantine-color-dark-6); + border-color: var(--mantine-color-dark-4); + } +} + +.tab { + z-index: 1; + font-weight: 500; + transition: color 100ms ease; + color: var(--mantine-color-gray-7); + + &[data-active] { + color: var(--mantine-color-black); + } + + @mixin dark { + color: var(--mantine-color-dark-1); + + &[data-active] { + color: var(--mantine-color-white); + } + } +} diff --git a/inertia/components/common/floating_tabs/floating_tabs.tsx b/inertia/components/common/floating_tabs/floating_tabs.tsx new file mode 100644 index 0000000..b852b0e --- /dev/null +++ b/inertia/components/common/floating_tabs/floating_tabs.tsx @@ -0,0 +1,79 @@ +import { + FloatingIndicator, + Indicator, + Tabs as MantineTabs, + Stack, +} from '@mantine/core'; +import { useState } from 'react'; +import classes from './floating_tabs.module.css'; + +export type FloatingTab = { + value: string; + label: string; + content: React.ReactNode; + disabled?: boolean; + indicator?: { + content: string; + color?: string; + pulse?: boolean; + disabled?: boolean; + }; +}; + +interface FloatingTabsProps { + tabs: FloatingTab[]; + keepMounted?: boolean; +} + +export function FloatingTabs({ tabs, keepMounted = false }: FloatingTabsProps) { + const [rootRef, setRootRef] = useState(null); + const [value, setValue] = useState(tabs[0].value); + const [controlsRefs, setControlsRefs] = useState< + Record + >({}); + const setControlRef = (val: string) => (node: HTMLButtonElement) => { + controlsRefs[val] = node; + setControlsRefs(controlsRefs); + }; + + return ( + + + {tabs.map((tab) => ( + + + {tab.label} + + + ))} + + + {tabs.map((tab) => ( + + {tab.content} + + ))} + + ); +} diff --git a/inertia/components/common/user_preferences/user_preferences.tsx b/inertia/components/common/user_preferences/user_preferences.tsx index a5c35bf..5f1e639 100644 --- a/inertia/components/common/user_preferences/user_preferences.tsx +++ b/inertia/components/common/user_preferences/user_preferences.tsx @@ -1,15 +1,10 @@ -import { LinkListDisplay } from '#shared/types/index'; import { Fieldset, Stack, Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; -import { ComboList } from '~/components/common/combo_list/combo_list'; import { CollectionListSelector } from '~/components/dashboard/collection/collection_list_selector'; -import { useDisplayPreferences } from '~/hooks/use_display_preferences'; +import { LinkListSelector } from '~/components/dashboard/link/link_list_selector'; import { useIsMobile } from '~/hooks/use_is_mobile'; -import { getLinkListDisplayOptions } from '~/lib/display_preferences'; export function UserPreferences() { - const { displayPreferences, handleUpdateDisplayPreferences } = - useDisplayPreferences(); const { t } = useTranslation(); const isMobile = useIsMobile(); @@ -28,15 +23,7 @@ export function UserPreferences() { {t('display-preferences.link-list-display')} - - handleUpdateDisplayPreferences({ - linkListDisplay: value as LinkListDisplay, - }) - } - /> + ); diff --git a/inertia/components/dashboard/collection/collection_list_selector.tsx b/inertia/components/dashboard/collection/collection_list_selector.tsx index f7bbacd..7347123 100644 --- a/inertia/components/dashboard/collection/collection_list_selector.tsx +++ b/inertia/components/dashboard/collection/collection_list_selector.tsx @@ -1,20 +1,28 @@ +import { COLLECTION_LIST_DISPLAYS } from '#shared/lib/display_preferences'; import { CollectionListDisplay } from '#shared/types/index'; -import { ComboList } from '~/components/common/combo_list/combo_list'; +import { SegmentedControl } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; import { useDisplayPreferences } from '~/hooks/use_display_preferences'; -import { getCollectionListDisplayOptions } from '~/lib/display_preferences'; export function CollectionListSelector() { + const { t } = useTranslation(); const { displayPreferences, handleUpdateDisplayPreferences } = useDisplayPreferences(); + + const data = COLLECTION_LIST_DISPLAYS.map((display) => ({ + label: t(`display-preferences.${display}`), + value: display, + })); return ( - + handleUpdateDisplayPreferences({ collectionListDisplay: value as CollectionListDisplay, }) } + w="50%" /> ); } diff --git a/inertia/components/dashboard/link/link_list_selector.tsx b/inertia/components/dashboard/link/link_list_selector.tsx new file mode 100644 index 0000000..dbda552 --- /dev/null +++ b/inertia/components/dashboard/link/link_list_selector.tsx @@ -0,0 +1,28 @@ +import { LINK_LIST_DISPLAYS } from '#shared/lib/display_preferences'; +import { LinkListDisplay } from '#shared/types/index'; +import { SegmentedControl } from '@mantine/core'; +import { useTranslation } from 'react-i18next'; +import { useDisplayPreferences } from '~/hooks/use_display_preferences'; + +export function LinkListSelector() { + const { t } = useTranslation(); + const { displayPreferences, handleUpdateDisplayPreferences } = + useDisplayPreferences(); + + const data = LINK_LIST_DISPLAYS.map((display) => ({ + label: t(`display-preferences.${display}`), + value: display, + })); + return ( + + handleUpdateDisplayPreferences({ + linkListDisplay: value as LinkListDisplay, + }) + } + w="50%" + /> + ); +} diff --git a/inertia/i18n/locales/en/common.json b/inertia/i18n/locales/en/common.json index 6d7edee..e1bb1f3 100644 --- a/inertia/i18n/locales/en/common.json +++ b/inertia/i18n/locales/en/common.json @@ -89,7 +89,10 @@ "preferences-description": "Display preferences do not apply on mobile", "display-preferences": { "collection-list-display": "Collection list display", - "link-list-display": "Link list display" + "link-list-display": "Link list display", + "inline": "Inline", + "list": "List", + "grid": "Grid" }, "coming-soon": "Under development" } diff --git a/inertia/i18n/locales/fr/common.json b/inertia/i18n/locales/fr/common.json index d998637..66e1b3e 100644 --- a/inertia/i18n/locales/fr/common.json +++ b/inertia/i18n/locales/fr/common.json @@ -89,7 +89,10 @@ "preferences-description": "Les préférences d'affichage ne s'appliquent pas sur mobile", "display-preferences": { "collection-list-display": "Affichage de la liste des collections", - "link-list-display": "Affichage de la liste des liens" + "link-list-display": "Affichage de la liste des liens", + "inline": "En ligne", + "list": "Liste", + "grid": "Grille" }, "coming-soon": "En cours de développement" } diff --git a/inertia/layouts/small_content.tsx b/inertia/layouts/small_content.tsx index 5c371e2..842e5c8 100644 --- a/inertia/layouts/small_content.tsx +++ b/inertia/layouts/small_content.tsx @@ -12,7 +12,8 @@ const SmallContentLayout = ({ children }: PropsWithChildren) => ( export default SmallContentLayout; -const LAYOUT_WIDTH = '800px'; +const LAYOUT_WIDTH = '1500px'; +const CONTENT_WIDTH = '800px'; const Layout = ({ children }: PropsWithChildren) => ( <> {/* Top navbar */} @@ -29,7 +30,7 @@ const Layout = ({ children }: PropsWithChildren) => ( style={{ height: '100%', maxWidth: '100%', - width: LAYOUT_WIDTH, + width: CONTENT_WIDTH, marginInline: 'auto', marginBlock: rem(30), }} diff --git a/inertia/lib/display_preferences.tsx b/inertia/lib/display_preferences.tsx deleted file mode 100644 index 109148b..0000000 --- a/inertia/lib/display_preferences.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { - COLLECTION_LIST_DISPLAYS, - LINK_LIST_DISPLAYS, -} from '#shared/lib/display_preferences'; -import { AiOutlineFolder } from 'react-icons/ai'; -import { IoGridOutline } from 'react-icons/io5'; -import { TbList } from 'react-icons/tb'; -import { ValueWithIcon } from '~/components/common/combo_list/combo_list'; - -const collectionListDisplayIcons = { - list: , - inline: , -} as const; - -export function getCollectionListDisplayOptions(): ValueWithIcon[] { - return COLLECTION_LIST_DISPLAYS.map((display) => ({ - label: display, - value: display, - icon: collectionListDisplayIcons[display], - })); -} - -const linkListDisplayIcons = { - list: , - grid: , -} as const; - -export function getLinkListDisplayOptions(): ValueWithIcon[] { - return LINK_LIST_DISPLAYS.map((display) => ({ - label: display, - value: display, - icon: linkListDisplayIcons[display], - })); -} diff --git a/inertia/pages/user_settings/show.tsx b/inertia/pages/user_settings/show.tsx new file mode 100644 index 0000000..394f278 --- /dev/null +++ b/inertia/pages/user_settings/show.tsx @@ -0,0 +1,24 @@ +import { useTranslation } from 'react-i18next'; +import { + FloatingTab, + FloatingTabs, +} from '~/components/common/floating_tabs/floating_tabs'; +import { UserPreferences } from '~/components/common/user_preferences/user_preferences'; +import SmallContentLayout from '~/layouts/small_content'; + +function UserSettingsShow() { + const { t } = useTranslation(); + const tabs: FloatingTab[] = [ + { + label: t('preferences'), + value: 'preferences', + content: , + }, + ]; + return ; +} + +UserSettingsShow.layout = (page: React.ReactNode) => ( + {page} +); +export default UserSettingsShow; diff --git a/package.json b/package.json index 4b2e07d..7c81290 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "#search/*": "./app/search/*.js", "#shared_collections/*": "./app/shared_collections/*.js", "#user/*": "./app/user/*.js", + "#user_settings/*": "./app/user_settings/*.js", "#providers/*": "./providers/*.js", "#database/*": "./database/*.js", "#tests/*": "./tests/*.js", diff --git a/start/routes.ts b/start/routes.ts index 93eb0e2..e2b8ca2 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -8,3 +8,4 @@ import '#links/routes/routes'; import '#search/routes/routes'; import '#shared_collections/routes/routes'; import '#user/routes/routes'; +import '#user_settings/routes/routes';