From 42f391d99a7e407f3c346f35ab65eb22856c7bd9 Mon Sep 17 00:00:00 2001 From: Sonny Date: Tue, 19 Aug 2025 23:47:52 +0200 Subject: [PATCH] refactor: remove react-hotkeys-hook and use inertia propos instead of recreating a local store --- .../collection/item/collection_item.tsx | 4 +- .../collection/list/collection_list.tsx | 42 +-------- .../components/dashboard/dashboard_header.tsx | 4 +- .../components/dashboard/dashboard_navbar.tsx | 4 +- .../dashboard/favorite/favorite_list.tsx | 10 +- .../dashboard/link/item/link_controls.tsx | 11 ++- .../dashboard/link/no_link/no_link.tsx | 4 +- .../collections/use_active_collection.tsx | 25 +++++ .../use_collection_list_selector.tsx | 31 +++++++ inertia/hooks/collections/use_collections.tsx | 25 +++++ .../hooks/collections/use_favorite_links.tsx | 12 +++ inertia/hooks/use_shortcut.tsx | 17 ++-- inertia/pages/shared.tsx | 8 -- inertia/stores/collection_store.ts | 92 ------------------- package.json | 1 - pnpm-lock.yaml | 14 --- 16 files changed, 122 insertions(+), 182 deletions(-) create mode 100644 inertia/hooks/collections/use_active_collection.tsx create mode 100644 inertia/hooks/collections/use_collection_list_selector.tsx create mode 100644 inertia/hooks/collections/use_collections.tsx create mode 100644 inertia/hooks/collections/use_favorite_links.tsx delete mode 100644 inertia/stores/collection_store.ts diff --git a/inertia/components/dashboard/collection/item/collection_item.tsx b/inertia/components/dashboard/collection/item/collection_item.tsx index 55b5ea5..843e623 100644 --- a/inertia/components/dashboard/collection/item/collection_item.tsx +++ b/inertia/components/dashboard/collection/item/collection_item.tsx @@ -3,8 +3,8 @@ import { route } from '@izzyjs/route/client'; import { Text } from '@mantine/core'; import { useEffect, useRef } from 'react'; import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai'; +import { useActiveCollection } from '~/hooks/collections/use_active_collection'; import { appendCollectionId } from '~/lib/navigation'; -import { useActiveCollection } from '~/stores/collection_store'; import { CollectionWithLinks } from '~/types/app'; import classes from './collection_item.module.css'; @@ -14,7 +14,7 @@ export default function CollectionItem({ collection: CollectionWithLinks; }) { const itemRef = useRef(null); - const { activeCollection } = useActiveCollection(); + const activeCollection = useActiveCollection(); const isActiveCollection = collection.id === activeCollection?.id; const FolderIcon = isActiveCollection ? AiFillFolderOpen : AiOutlineFolder; diff --git a/inertia/components/dashboard/collection/list/collection_list.tsx b/inertia/components/dashboard/collection/list/collection_list.tsx index 353d844..d70a78d 100644 --- a/inertia/components/dashboard/collection/list/collection_list.tsx +++ b/inertia/components/dashboard/collection/list/collection_list.tsx @@ -1,50 +1,12 @@ -import { router } from '@inertiajs/react'; -import { route } from '@izzyjs/route/client'; import { Box, ScrollArea, Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import CollectionItem from '~/components/dashboard/collection/item/collection_item'; -import useShortcut from '~/hooks/use_shortcut'; -import { appendCollectionId } from '~/lib/navigation'; -import { useActiveCollection, useCollections } from '~/stores/collection_store'; +import { useCollections } from '~/hooks/collections/use_collections'; import styles from './collection_list.module.css'; export default function CollectionList() { const { t } = useTranslation('common'); - const { collections } = useCollections(); - const { activeCollection, setActiveCollection } = useActiveCollection(); - - const replaceUrl = (collectionId: number) => - router.get(appendCollectionId(route('dashboard').path, collectionId)); - - const goToPreviousCollection = () => { - const currentCategoryIndex = collections.findIndex( - ({ id }) => id === activeCollection?.id - ); - if (currentCategoryIndex === -1 || currentCategoryIndex === 0) return; - - const collection = collections[currentCategoryIndex - 1]; - replaceUrl(collection.id); - setActiveCollection(collection); - }; - - const goToNextCollection = () => { - const currentCategoryIndex = collections.findIndex( - ({ id }) => id === activeCollection?.id - ); - if ( - currentCategoryIndex === -1 || - currentCategoryIndex === collections.length - 1 - ) - return; - - const collection = collections[currentCategoryIndex + 1]; - replaceUrl(collection.id); - setActiveCollection(collection); - }; - - useShortcut('ARROW_UP', goToPreviousCollection); - useShortcut('ARROW_DOWN', goToNextCollection); - + const collections = useCollections(); return ( diff --git a/inertia/components/dashboard/dashboard_header.tsx b/inertia/components/dashboard/dashboard_header.tsx index 426deb6..217d7fe 100644 --- a/inertia/components/dashboard/dashboard_header.tsx +++ b/inertia/components/dashboard/dashboard_header.tsx @@ -16,8 +16,8 @@ import { GoPencil } from 'react-icons/go'; import { IoIosAddCircleOutline } from 'react-icons/io'; import { IoTrashOutline } from 'react-icons/io5'; import { ShareCollection } from '~/components/share/share_collection'; +import { useActiveCollection } from '~/hooks/collections/use_active_collection'; import { appendCollectionId } from '~/lib/navigation'; -import { useActiveCollection } from '~/stores/collection_store'; import { Visibility } from '~/types/app'; interface DashboardHeaderProps { @@ -32,7 +32,7 @@ interface DashboardHeaderProps { } export function DashboardHeader({ navbar, aside }: DashboardHeaderProps) { const { t } = useTranslation('common'); - const { activeCollection } = useActiveCollection(); + const activeCollection = useActiveCollection(); return ( diff --git a/inertia/components/dashboard/dashboard_navbar.tsx b/inertia/components/dashboard/dashboard_navbar.tsx index 80c580e..eca3d21 100644 --- a/inertia/components/dashboard/dashboard_navbar.tsx +++ b/inertia/components/dashboard/dashboard_navbar.tsx @@ -20,10 +20,10 @@ import { PiGearLight } from 'react-icons/pi'; import { UserCard } from '~/components/common/user_card'; import { FavoriteList } from '~/components/dashboard/favorite/favorite_list'; import { SearchSpotlight } from '~/components/search/search'; +import { useActiveCollection } from '~/hooks/collections/use_active_collection'; import { useAuth } from '~/hooks/use_auth'; import useShortcut from '~/hooks/use_shortcut'; import { appendCollectionId } from '~/lib/navigation'; -import { useActiveCollection } from '~/stores/collection_store'; import { useGlobalHotkeysStore } from '~/stores/global_hotkeys_store'; interface DashboardNavbarProps { @@ -34,7 +34,7 @@ export function DashboardNavbar({ isOpen, toggle }: DashboardNavbarProps) { const { t } = useTranslation('common'); const { isAuthenticated, user } = useAuth(); - const { activeCollection } = useActiveCollection(); + const activeCollection = useActiveCollection(); const { globalHotkeysEnabled, setGlobalHotkeysEnabled } = useGlobalHotkeysStore(); diff --git a/inertia/components/dashboard/favorite/favorite_list.tsx b/inertia/components/dashboard/favorite/favorite_list.tsx index d48bd87..6105c30 100644 --- a/inertia/components/dashboard/favorite/favorite_list.tsx +++ b/inertia/components/dashboard/favorite/favorite_list.tsx @@ -1,13 +1,13 @@ import { Flex, Group, Stack, Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { FavoriteItem } from '~/components/dashboard/favorite/item/favorite_item'; -import { useFavorites } from '~/stores/collection_store'; +import { useFavoriteLinks } from '~/hooks/collections/use_favorite_links'; export function FavoriteList() { const { t } = useTranslation('common'); - const { favorites } = useFavorites(); + const favoriteLinks = useFavoriteLinks(); - if (favorites.length === 0) { + if (favoriteLinks.length === 0) { return ( @@ -20,10 +20,10 @@ export function FavoriteList() { return ( - {t('favorite')} • {favorites.length} + {t('favorite')} • {favoriteLinks.length} - {favorites.map((link) => ( + {favoriteLinks.map((link) => ( ))} diff --git a/inertia/components/dashboard/link/item/link_controls.tsx b/inertia/components/dashboard/link/item/link_controls.tsx index 4678768..924f40c 100644 --- a/inertia/components/dashboard/link/item/link_controls.tsx +++ b/inertia/components/dashboard/link/item/link_controls.tsx @@ -1,4 +1,4 @@ -import { Link as InertiaLink } from '@inertiajs/react'; +import { Link as InertiaLink, router } from '@inertiajs/react'; import { route } from '@izzyjs/route/client'; import { ActionIcon, Menu } from '@mantine/core'; import { MouseEvent } from 'react'; @@ -10,7 +10,6 @@ import { IoTrashOutline } from 'react-icons/io5'; import { MdFavorite, MdFavoriteBorder } from 'react-icons/md'; import { onFavorite } from '~/lib/favorite'; import { appendCollectionId, appendLinkId } from '~/lib/navigation'; -import { useFavorites } from '~/stores/collection_store'; import { Link, PublicLink } from '~/types/app'; interface LinksControlsProps { @@ -21,10 +20,14 @@ export default function LinkControls({ link, showGoToCollection = false, }: LinksControlsProps) { - const { toggleFavorite } = useFavorites(); const { t } = useTranslation('common'); - const onFavoriteCallback = () => toggleFavorite(link.id); + const onFavoriteCallback = () => { + const path = route('link.toggle-favorite', { + params: { id: link.id.toString() }, + }).path; + router.put(path); + }; const handleStopPropagation = (event: MouseEvent) => event.preventDefault(); diff --git a/inertia/components/dashboard/link/no_link/no_link.tsx b/inertia/components/dashboard/link/no_link/no_link.tsx index a216aab..e2c899b 100644 --- a/inertia/components/dashboard/link/no_link/no_link.tsx +++ b/inertia/components/dashboard/link/no_link/no_link.tsx @@ -3,15 +3,15 @@ import { route } from '@izzyjs/route/client'; import { Anchor, Box, Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import type { LinkListProps } from '~/components/dashboard/link/list/link_list'; +import { useActiveCollection } from '~/hooks/collections/use_active_collection'; import { appendCollectionId } from '~/lib/navigation'; -import { useActiveCollection } from '~/stores/collection_store'; import styles from './no_link.module.css'; interface NoLinkProps extends LinkListProps {} export function NoLink({ hideMenu }: NoLinkProps) { const { t } = useTranslation('common'); - const { activeCollection } = useActiveCollection(); + const activeCollection = useActiveCollection(); return ( { + const { props } = usePage(); + return props.activeCollection; +}; + +export type WithActiveCollectionProps = { + activeCollection?: CollectionWithLinks; +}; + +export const withActiveCollection = ( + Component: React.ComponentType +) => { + return (props: WithActiveCollectionProps) => { + const activeCollection = useActiveCollection(); + return ; + }; +}; diff --git a/inertia/hooks/collections/use_collection_list_selector.tsx b/inertia/hooks/collections/use_collection_list_selector.tsx new file mode 100644 index 0000000..18e3767 --- /dev/null +++ b/inertia/hooks/collections/use_collection_list_selector.tsx @@ -0,0 +1,31 @@ +import { IoListOutline } from 'react-icons/io5'; +import { TbHash } from 'react-icons/tb'; +import { ValueWithIcon } from '~/components/common/combo_list/combo_list'; +import { usePersisted } from '~/hooks/use_persisted'; + +const listDisplayOptions: ValueWithIcon[] = [ + { + label: 'Inline', + value: 'inline', + icon: , + }, + { + label: 'List', + value: 'list', + icon: , + }, +]; +type ListDisplay = (typeof listDisplayOptions)[number]['value']; + +export const useCollectionListSelector = () => { + const [listDisplay, setListDisplay] = usePersisted( + 'inline', + 'list' + ); + + return { + listDisplay, + listDisplayOptions, + setListDisplay, + }; +}; diff --git a/inertia/hooks/collections/use_collections.tsx b/inertia/hooks/collections/use_collections.tsx new file mode 100644 index 0000000..6bc987c --- /dev/null +++ b/inertia/hooks/collections/use_collections.tsx @@ -0,0 +1,25 @@ +import { PageProps } from '@adonisjs/inertia/types'; +import { usePage } from '@inertiajs/react'; +import { CollectionWithLinks } from '~/types/app'; + +interface UseCollectionsProps { + collections: CollectionWithLinks[]; +} + +export const useCollections = () => { + const { props } = usePage(); + return props.collections; +}; + +export type WithCollectionsProps = { + collections: CollectionWithLinks[]; +}; + +export const withCollections = ( + Component: React.ComponentType +): React.ComponentType> => { + return (props: Omit) => { + const collections = useCollections(); + return ; + }; +}; diff --git a/inertia/hooks/collections/use_favorite_links.tsx b/inertia/hooks/collections/use_favorite_links.tsx new file mode 100644 index 0000000..fec9a96 --- /dev/null +++ b/inertia/hooks/collections/use_favorite_links.tsx @@ -0,0 +1,12 @@ +import { PageProps } from '@adonisjs/inertia/types'; +import { usePage } from '@inertiajs/react'; +import { LinkWithCollection } from '~/types/app'; + +interface UseFavoriteLinksProps { + favoriteLinks: LinkWithCollection[]; +} + +export const useFavoriteLinks = () => { + const { props } = usePage(); + return props.favoriteLinks; +}; diff --git a/inertia/hooks/use_shortcut.tsx b/inertia/hooks/use_shortcut.tsx index 776f326..0163e12 100644 --- a/inertia/hooks/use_shortcut.tsx +++ b/inertia/hooks/use_shortcut.tsx @@ -1,5 +1,5 @@ import KEYS from '#core/constants/keys'; -import { useHotkeys } from 'react-hotkeys-hook'; +import { useHotkeys } from '@mantine/hooks'; import { useGlobalHotkeysStore } from '~/stores/global_hotkeys_store'; type ShortcutOptions = { @@ -16,15 +16,12 @@ export default function useShortcut( } ) { const { globalHotkeysEnabled } = useGlobalHotkeysStore(); + const isEnabled = disableGlobalCheck + ? enabled + : enabled && globalHotkeysEnabled; return useHotkeys( - KEYS[key], - (event) => { - event.preventDefault(); - cb(); - }, - { - enabled: disableGlobalCheck ? enabled : enabled && globalHotkeysEnabled, - enableOnFormTags: ['INPUT'], - } + [[KEYS[key], () => isEnabled && cb(), { preventDefault: true }]], + undefined, + true ); } diff --git a/inertia/pages/shared.tsx b/inertia/pages/shared.tsx index f1582be..d16a0c0 100644 --- a/inertia/pages/shared.tsx +++ b/inertia/pages/shared.tsx @@ -1,8 +1,6 @@ import { Flex, Text } from '@mantine/core'; -import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { LinkList } from '~/components/dashboard/link/list/link_list'; -import { useCollectionsSetter } from '~/stores/collection_store'; import type { CollectionWithLinks, PublicUser } from '~/types/app'; interface SharedPageProps { @@ -11,12 +9,6 @@ interface SharedPageProps { export default function SharedPage({ collection }: SharedPageProps) { const { t } = useTranslation('common'); - const { setActiveCollection } = useCollectionsSetter(); - - useEffect(() => { - setActiveCollection(collection); - }, []); - return ( <> diff --git a/inertia/stores/collection_store.ts b/inertia/stores/collection_store.ts deleted file mode 100644 index 72504b4..0000000 --- a/inertia/stores/collection_store.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { create } from 'zustand'; -import { useShallow } from 'zustand/react/shallow'; -import { CollectionWithLinks, Link, LinkWithCollection } from '~/types/app'; - -type Collections = CollectionWithLinks[]; - -interface CollectionStore { - collections: Collections; - _setCollections: (collections: Collections) => void; - - activeCollection: CollectionWithLinks | null; - setActiveCollection: (collection: CollectionWithLinks) => void; - - favorites: LinkWithCollection[]; - toggleFavorite: (link: Link['id']) => void; -} - -export const useCollectionStore = create((set, get) => ({ - collections: [], - _setCollections: (collections) => { - const favorites = getFavoriteLinks(collections); - set({ collections, favorites }); - }, - - activeCollection: null, - setActiveCollection: (collection) => set({ activeCollection: collection }), - - favorites: [], - toggleFavorite: (linkId) => { - const { collections } = get(); - let linkIndex = 0; - const collectionIndex = collections.findIndex(({ links }) => { - const lIndex = links.findIndex((l) => l.id === linkId); - if (lIndex !== -1) { - linkIndex = lIndex; - } - return lIndex !== -1; - }); - - const collectionLink = collections[collectionIndex].links[linkIndex]; - const collectionsCopy = [...collections]; - collectionsCopy[collectionIndex].links[linkIndex] = { - ...collectionLink, - favorite: !collectionLink.favorite, - }; - - set({ - collections: collectionsCopy, - activeCollection: collectionsCopy[collectionIndex], - favorites: getFavoriteLinks(collectionsCopy), - }); - }, -})); - -export const useActiveCollection = () => - useCollectionStore( - useShallow((state) => ({ - activeCollection: state.activeCollection, - setActiveCollection: state.setActiveCollection, - })) - ); - -export const useCollections = () => - useCollectionStore( - useShallow((state) => ({ collections: state.collections })) - ); - -export const useFavorites = () => - useCollectionStore( - useShallow((state) => ({ - favorites: state.favorites, - toggleFavorite: state.toggleFavorite, - })) - ); - -export function useCollectionsSetter() { - // eslint-disable-next-line @typescript-eslint/naming-convention - const { _setCollections, setActiveCollection } = useCollectionStore(); - return { _setCollections, setActiveCollection }; -} - -function getFavoriteLinks(collections: Collections) { - return collections.reduce((acc, collection) => { - collection.links.forEach((link) => { - if (link.favorite) { - const newLink: LinkWithCollection = { ...link, collection }; - acc.push(newLink); - } - }); - return acc; - }, [] as LinkWithCollection[]); -} diff --git a/package.json b/package.json index b852d88..86c736d 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,6 @@ "pg": "^8.16.3", "react": "^19.1.1", "react-dom": "^19.1.1", - "react-hotkeys-hook": "^5.1.0", "react-i18next": "^15.6.1", "react-icons": "^5.5.0", "reflect-metadata": "^0.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16845b4..55f5bc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,9 +104,6 @@ importers: react-dom: specifier: ^19.1.1 version: 19.1.1(react@19.1.1) - react-hotkeys-hook: - specifier: ^5.1.0 - version: 5.1.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) react-i18next: specifier: ^15.6.1 version: 15.6.1(i18next@25.3.2(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2) @@ -4349,12 +4346,6 @@ packages: peerDependencies: react: ^19.1.1 - react-hotkeys-hook@5.1.0: - resolution: {integrity: sha512-GCNGXjBzV9buOS3REoQFmSmE4WTvBhYQ0YrAeeMZI83bhXg3dRWsLHXDutcVDdEjwJqJCxk5iewWYX5LtFUd7g==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' - react-i18next@15.6.1: resolution: {integrity: sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==} peerDependencies: @@ -9689,11 +9680,6 @@ snapshots: react: 19.1.1 scheduler: 0.26.0 - react-hotkeys-hook@5.1.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): - dependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-i18next@15.6.1(i18next@25.3.2(typescript@5.9.2))(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2): dependencies: '@babel/runtime': 7.28.2