refactor: remove react-hotkeys-hook and use inertia propos instead of recreating a local store

This commit is contained in:
Sonny
2025-08-19 23:47:52 +02:00
parent 1d1e182523
commit 42f391d99a
16 changed files with 122 additions and 182 deletions

View File

@@ -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<HTMLAnchorElement>(null);
const { activeCollection } = useActiveCollection();
const activeCollection = useActiveCollection();
const isActiveCollection = collection.id === activeCollection?.id;
const FolderIcon = isActiveCollection ? AiFillFolderOpen : AiOutlineFolder;

View File

@@ -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 (
<Box className={styles.sideMenu}>
<Box className={styles.listContainer}>

View File

@@ -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 (
<AppShell.Header style={{ display: 'flex', alignItems: 'center' }}>
<Group justify="space-between" px="md" flex={1} wrap="nowrap">

View File

@@ -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();

View File

@@ -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 (
<Group justify="center">
<Text c="dimmed" size="sm" mt="sm">
@@ -20,10 +20,10 @@ export function FavoriteList() {
return (
<Flex direction="column">
<Text c="dimmed" mt="xs" ml="md" mb={4}>
{t('favorite')} {favorites.length}
{t('favorite')} {favoriteLinks.length}
</Text>
<Stack gap={4}>
{favorites.map((link) => (
{favoriteLinks.map((link) => (
<FavoriteItem link={link} key={link.id} />
))}
</Stack>

View File

@@ -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<HTMLButtonElement>) =>
event.preventDefault();

View File

@@ -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 (
<Box className={styles.noCollection} p="xl">
<Text

View File

@@ -0,0 +1,25 @@
import { PageProps } from '@adonisjs/inertia/types';
import { usePage } from '@inertiajs/react';
import { CollectionWithLinks } from '~/types/app';
interface UseActiveCollectionProps {
activeCollection?: CollectionWithLinks;
}
export const useActiveCollection = () => {
const { props } = usePage<PageProps & UseActiveCollectionProps>();
return props.activeCollection;
};
export type WithActiveCollectionProps = {
activeCollection?: CollectionWithLinks;
};
export const withActiveCollection = (
Component: React.ComponentType<WithActiveCollectionProps>
) => {
return (props: WithActiveCollectionProps) => {
const activeCollection = useActiveCollection();
return <Component {...props} activeCollection={activeCollection} />;
};
};

View File

@@ -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: <TbHash size={20} />,
},
{
label: 'List',
value: 'list',
icon: <IoListOutline size={20} />,
},
];
type ListDisplay = (typeof listDisplayOptions)[number]['value'];
export const useCollectionListSelector = () => {
const [listDisplay, setListDisplay] = usePersisted<ListDisplay>(
'inline',
'list'
);
return {
listDisplay,
listDisplayOptions,
setListDisplay,
};
};

View File

@@ -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<PageProps & UseCollectionsProps>();
return props.collections;
};
export type WithCollectionsProps = {
collections: CollectionWithLinks[];
};
export const withCollections = <T extends object>(
Component: React.ComponentType<T & WithCollectionsProps>
): React.ComponentType<Omit<T, 'collections'>> => {
return (props: Omit<T, 'collections'>) => {
const collections = useCollections();
return <Component {...(props as T)} collections={collections} />;
};
};

View File

@@ -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<PageProps & UseFavoriteLinksProps>();
return props.favoriteLinks;
};

View File

@@ -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
);
}

View File

@@ -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 (
<>
<Flex direction="column">

View File

@@ -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<CollectionStore>((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[]);
}

View File

@@ -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",

14
pnpm-lock.yaml generated
View File

@@ -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