mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 14:43:24 +00:00
refactor: remove react-hotkeys-hook and use inertia propos instead of recreating a local store
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
25
inertia/hooks/collections/use_active_collection.tsx
Normal file
25
inertia/hooks/collections/use_active_collection.tsx
Normal 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} />;
|
||||
};
|
||||
};
|
||||
31
inertia/hooks/collections/use_collection_list_selector.tsx
Normal file
31
inertia/hooks/collections/use_collection_list_selector.tsx
Normal 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,
|
||||
};
|
||||
};
|
||||
25
inertia/hooks/collections/use_collections.tsx
Normal file
25
inertia/hooks/collections/use_collections.tsx
Normal 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} />;
|
||||
};
|
||||
};
|
||||
12
inertia/hooks/collections/use_favorite_links.tsx
Normal file
12
inertia/hooks/collections/use_favorite_links.tsx
Normal 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;
|
||||
};
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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[]);
|
||||
}
|
||||
@@ -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
14
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user