mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-08 22:53:25 +00:00
refactor: recreate collection and global hotkey contexts as store (using zustand)
This commit is contained in:
@@ -10,8 +10,8 @@ import {
|
|||||||
import ModalWrapper from '~/components/common/modal/_modal_wrapper';
|
import ModalWrapper from '~/components/common/modal/_modal_wrapper';
|
||||||
import TextEllipsis from '~/components/common/text_ellipsis';
|
import TextEllipsis from '~/components/common/text_ellipsis';
|
||||||
import useClickOutside from '~/hooks/use_click_outside';
|
import useClickOutside from '~/hooks/use_click_outside';
|
||||||
import useGlobalHotkeys from '~/hooks/use_global_hotkeys';
|
|
||||||
import useShortcut from '~/hooks/use_shortcut';
|
import useShortcut from '~/hooks/use_shortcut';
|
||||||
|
import { useGlobalHotkeysStore } from '~/store/global_hotkeys_store';
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -32,7 +32,7 @@ export default function Modal({
|
|||||||
close,
|
close,
|
||||||
}: ModalProps) {
|
}: ModalProps) {
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
const { setGlobalHotkeysEnabled } = useGlobalHotkeys();
|
const { setGlobalHotkeysEnabled } = useGlobalHotkeysStore();
|
||||||
|
|
||||||
useClickOutside(modalRef, close);
|
useClickOutside(modalRef, close);
|
||||||
useShortcut('ESCAPE_KEY', close, { disableGlobalCheck: true });
|
useShortcut('ESCAPE_KEY', close, { disableGlobalCheck: true });
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import CollectionHeader from '~/components/dashboard/collection/header/collectio
|
|||||||
import LinkList from '~/components/dashboard/link/link_list';
|
import LinkList from '~/components/dashboard/link/link_list';
|
||||||
import { NoCollection } from '~/components/dashboard/link/no_item';
|
import { NoCollection } from '~/components/dashboard/link/no_item';
|
||||||
import Footer from '~/components/footer/footer';
|
import Footer from '~/components/footer/footer';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
import { useActiveCollection } from '~/store/collection_store';
|
||||||
|
|
||||||
export interface CollectionHeaderProps {
|
export interface CollectionHeaderProps {
|
||||||
showButtons: boolean;
|
showButtons: boolean;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import TextEllipsis from '~/components/common/text_ellipsis';
|
import TextEllipsis from '~/components/common/text_ellipsis';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
import { useActiveCollection } from '~/store/collection_store';
|
||||||
|
|
||||||
const CollectionDescriptionStyle = styled.div({
|
const CollectionDescriptionStyle = styled.div({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { CollectionHeaderProps } from '~/components/dashboard/collection/collect
|
|||||||
import CollectionControls from '~/components/dashboard/collection/header/collection_controls';
|
import CollectionControls from '~/components/dashboard/collection/header/collection_controls';
|
||||||
import CollectionDescription from '~/components/dashboard/collection/header/collection_description';
|
import CollectionDescription from '~/components/dashboard/collection/header/collection_description';
|
||||||
import VisibilityBadge from '~/components/visibilty/visibilty';
|
import VisibilityBadge from '~/components/visibilty/visibilty';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
import { useActiveCollection } from '~/store/collection_store';
|
||||||
|
|
||||||
const paddingLeft = '1.25em';
|
const paddingLeft = '1.25em';
|
||||||
const paddingRight = '1.65em';
|
const paddingRight = '1.65em';
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { useEffect, useRef } from 'react';
|
|||||||
import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai';
|
import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai';
|
||||||
import TextEllipsis from '~/components/common/text_ellipsis';
|
import TextEllipsis from '~/components/common/text_ellipsis';
|
||||||
import { Item } from '~/components/dashboard/side_nav/nav_item';
|
import { Item } from '~/components/dashboard/side_nav/nav_item';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
|
||||||
import { appendCollectionId } from '~/lib/navigation';
|
import { appendCollectionId } from '~/lib/navigation';
|
||||||
|
import { useActiveCollection } from '~/store/collection_store';
|
||||||
import { CollectionWithLinks } from '~/types/app';
|
import { CollectionWithLinks } from '~/types/app';
|
||||||
|
|
||||||
const CollectionItemStyle = styled(Item, {
|
const CollectionItemStyle = styled(Item, {
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ import styled from '@emotion/styled';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import CollectionItem from '~/components/dashboard/collection/list/collection_item';
|
import CollectionItem from '~/components/dashboard/collection/list/collection_item';
|
||||||
import CollectionListContainer from '~/components/dashboard/collection/list/collection_list_container';
|
import CollectionListContainer from '~/components/dashboard/collection/list/collection_list_container';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
|
||||||
import useCollections from '~/hooks/use_collections';
|
|
||||||
import useShortcut from '~/hooks/use_shortcut';
|
import useShortcut from '~/hooks/use_shortcut';
|
||||||
|
import { useActiveCollection, useCollections } from '~/store/collection_store';
|
||||||
|
|
||||||
const SideMenu = styled.nav(({ theme }) => ({
|
const SideMenu = styled.nav(({ theme }) => ({
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { Link as InertiaLink } from '@inertiajs/react';
|
import { Link as InertiaLink } from '@inertiajs/react';
|
||||||
import { route } from '@izzyjs/route/client';
|
import { route } from '@izzyjs/route/client';
|
||||||
import { ActionIcon, Menu } from '@mantine/core';
|
import { ActionIcon, Menu } from '@mantine/core';
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { BsThreeDotsVertical } from 'react-icons/bs';
|
import { BsThreeDotsVertical } from 'react-icons/bs';
|
||||||
import { FaRegEye } from 'react-icons/fa';
|
import { FaRegEye } from 'react-icons/fa';
|
||||||
import { GoPencil } from 'react-icons/go';
|
import { GoPencil } from 'react-icons/go';
|
||||||
import { IoTrashOutline } from 'react-icons/io5';
|
import { IoTrashOutline } from 'react-icons/io5';
|
||||||
import { MdFavorite, MdFavoriteBorder } from 'react-icons/md';
|
import { MdFavorite, MdFavoriteBorder } from 'react-icons/md';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
|
||||||
import useCollections from '~/hooks/use_collections';
|
|
||||||
import { onFavorite } from '~/lib/favorite';
|
import { onFavorite } from '~/lib/favorite';
|
||||||
import { appendCollectionId, appendLinkId } from '~/lib/navigation';
|
import { appendCollectionId, appendLinkId } from '~/lib/navigation';
|
||||||
|
import { useFavorites } from '~/store/collection_store';
|
||||||
import { Link } from '~/types/app';
|
import { Link } from '~/types/app';
|
||||||
|
|
||||||
interface LinksControlsProps {
|
interface LinksControlsProps {
|
||||||
@@ -22,34 +20,9 @@ export default function LinkControls({
|
|||||||
link,
|
link,
|
||||||
showGoToCollection = false,
|
showGoToCollection = false,
|
||||||
}: LinksControlsProps) {
|
}: LinksControlsProps) {
|
||||||
const { collections, setCollections } = useCollections();
|
const { toggleFavorite } = useFavorites();
|
||||||
const { setActiveCollection } = useActiveCollection();
|
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
const toggleFavorite = useCallback(
|
|
||||||
(linkId: Link['id']) => {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
setCollections(collectionsCopy);
|
|
||||||
setActiveCollection(collectionsCopy[collectionIndex]);
|
|
||||||
},
|
|
||||||
[collections, setCollections]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onFavoriteCallback = () => toggleFavorite(link.id);
|
const onFavoriteCallback = () => toggleFavorite(link.id);
|
||||||
return (
|
return (
|
||||||
<Menu withinPortal shadow="md" width={200}>
|
<Menu withinPortal shadow="md" width={200}>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import styled from '@emotion/styled';
|
|||||||
import { Link } from '@inertiajs/react';
|
import { Link } from '@inertiajs/react';
|
||||||
import { route } from '@izzyjs/route/client';
|
import { route } from '@izzyjs/route/client';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
|
||||||
import { appendCollectionId } from '~/lib/navigation';
|
import { appendCollectionId } from '~/lib/navigation';
|
||||||
|
import { useActiveCollection } from '~/store/collection_store';
|
||||||
import { fadeIn } from '~/styles/keyframes';
|
import { fadeIn } from '~/styles/keyframes';
|
||||||
|
|
||||||
const NoCollectionStyle = styled.div({
|
const NoCollectionStyle = styled.div({
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import Modal from '~/components/common/modal/modal';
|
|||||||
import NoSearchResult from '~/components/dashboard/search/no_search_result';
|
import NoSearchResult from '~/components/dashboard/search/no_search_result';
|
||||||
import SearchResultList from '~/components/dashboard/search/search_result_list';
|
import SearchResultList from '~/components/dashboard/search/search_result_list';
|
||||||
import { GOOGLE_SEARCH_URL } from '~/constants';
|
import { GOOGLE_SEARCH_URL } from '~/constants';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
|
||||||
import useCollections from '~/hooks/use_collections';
|
|
||||||
import useToggle from '~/hooks/use_modal';
|
import useToggle from '~/hooks/use_modal';
|
||||||
import useShortcut from '~/hooks/use_shortcut';
|
import useShortcut from '~/hooks/use_shortcut';
|
||||||
import { makeRequest } from '~/lib/request';
|
import { makeRequest } from '~/lib/request';
|
||||||
|
import { useActiveCollection, useCollections } from '~/store/collection_store';
|
||||||
import { SearchResult } from '~/types/search';
|
import { SearchResult } from '~/types/search';
|
||||||
|
|
||||||
const SearchInput = styled.input(({ theme }) => ({
|
const SearchInput = styled.input(({ theme }) => ({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AiOutlineFolder } from 'react-icons/ai';
|
|||||||
import Legend from '~/components/common/legend';
|
import Legend from '~/components/common/legend';
|
||||||
import TextEllipsis from '~/components/common/text_ellipsis';
|
import TextEllipsis from '~/components/common/text_ellipsis';
|
||||||
import LinkFavicon from '~/components/dashboard/link/link_favicon';
|
import LinkFavicon from '~/components/dashboard/link/link_favicon';
|
||||||
import useCollections from '~/hooks/use_collections';
|
import { useCollections } from '~/store/collection_store';
|
||||||
import {
|
import {
|
||||||
SearchResult,
|
SearchResult,
|
||||||
SearchResultCollection,
|
SearchResultCollection,
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
|
import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
|
||||||
import { DropdownItemButton } from '~/components/common/dropdown/dropdown_item';
|
import { DropdownItemButton } from '~/components/common/dropdown/dropdown_item';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
|
||||||
import useCollections from '~/hooks/use_collections';
|
|
||||||
import { onFavorite } from '~/lib/favorite';
|
import { onFavorite } from '~/lib/favorite';
|
||||||
|
import { useFavorites } from '~/store/collection_store';
|
||||||
import { Link } from '~/types/app';
|
import { Link } from '~/types/app';
|
||||||
|
|
||||||
const StarItem = styled(DropdownItemButton)(({ theme }) => ({
|
const StarItem = styled(DropdownItemButton)(({ theme }) => ({
|
||||||
@@ -13,34 +11,9 @@ const StarItem = styled(DropdownItemButton)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export default function FavoriteDropdownItem({ link }: { link: Link }) {
|
export default function FavoriteDropdownItem({ link }: { link: Link }) {
|
||||||
const { collections, setCollections } = useCollections();
|
const { toggleFavorite } = useFavorites();
|
||||||
const { setActiveCollection } = useActiveCollection();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const toggleFavorite = useCallback(
|
|
||||||
(linkId: Link['id']) => {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
setCollections(collectionsCopy);
|
|
||||||
setActiveCollection(collectionsCopy[collectionIndex]);
|
|
||||||
},
|
|
||||||
[collections, setCollections]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onFavoriteCallback = () => toggleFavorite(link.id);
|
const onFavoriteCallback = () => toggleFavorite(link.id);
|
||||||
return (
|
return (
|
||||||
<StarItem
|
<StarItem
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import FavoriteListContainer from '~/components/dashboard/side_nav/favorite/favorite_container';
|
import FavoriteListContainer from '~/components/dashboard/side_nav/favorite/favorite_container';
|
||||||
import FavoriteItem from '~/components/dashboard/side_nav/favorite/favorite_item';
|
import FavoriteItem from '~/components/dashboard/side_nav/favorite/favorite_item';
|
||||||
import useFavorites from '~/hooks/use_favorites';
|
import { useFavorites } from '~/store/collection_store';
|
||||||
|
|
||||||
const FavoriteLabel = styled.p(({ theme }) => ({
|
const FavoriteLabel = styled.p(({ theme }) => ({
|
||||||
color: theme.colors.grey,
|
color: theme.colors.grey,
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import FavoriteList from '~/components/dashboard/side_nav/favorite/favorite_list
|
|||||||
import { Item, ItemLink } from '~/components/dashboard/side_nav/nav_item';
|
import { Item, ItemLink } from '~/components/dashboard/side_nav/nav_item';
|
||||||
import UserCard from '~/components/dashboard/side_nav/user_card';
|
import UserCard from '~/components/dashboard/side_nav/user_card';
|
||||||
import ModalSettings from '~/components/settings/settings_modal';
|
import ModalSettings from '~/components/settings/settings_modal';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
|
||||||
import useUser from '~/hooks/use_user';
|
import useUser from '~/hooks/use_user';
|
||||||
import { rgba } from '~/lib/color';
|
import { rgba } from '~/lib/color';
|
||||||
import { appendCollectionId } from '~/lib/navigation';
|
import { appendCollectionId } from '~/lib/navigation';
|
||||||
|
import { useActiveCollection } from '~/store/collection_store';
|
||||||
|
|
||||||
const SideMenu = styled.nav(({ theme }) => ({
|
const SideMenu = styled.nav(({ theme }) => ({
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|||||||
9
inertia/hooks/mantine/use_disable_overflow.ts
Normal file
9
inertia/hooks/mantine/use_disable_overflow.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export const useDisableOverflow = () =>
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = 'auto';
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import { ActiveCollectionContext } from '~/contexts/active_collection_context';
|
|
||||||
|
|
||||||
const useActiveCollection = () => useContext(ActiveCollectionContext);
|
|
||||||
export default useActiveCollection;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import CollectionsContext from '~/contexts/collections_context';
|
|
||||||
|
|
||||||
const useCollections = () => useContext(CollectionsContext);
|
|
||||||
export default useCollections;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import KEYS from '#constants/keys';
|
import KEYS from '#constants/keys';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import useGlobalHotkeys from '~/hooks/use_global_hotkeys';
|
import { useGlobalHotkeysStore } from '~/store/global_hotkeys_store';
|
||||||
|
|
||||||
type ShortcutOptions = {
|
type ShortcutOptions = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@@ -15,7 +15,7 @@ export default function useShortcut(
|
|||||||
disableGlobalCheck: false,
|
disableGlobalCheck: false,
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { globalHotkeysEnabled } = useGlobalHotkeys();
|
const { globalHotkeysEnabled } = useGlobalHotkeysStore();
|
||||||
return useHotkeys(
|
return useHotkeys(
|
||||||
KEYS[key],
|
KEYS[key],
|
||||||
(event) => {
|
(event) => {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { route } from '@izzyjs/route/client';
|
|||||||
import { Text } from '@mantine/core';
|
import { Text } from '@mantine/core';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai';
|
import { AiFillFolderOpen, AiOutlineFolder } from 'react-icons/ai';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
|
||||||
import { appendCollectionId } from '~/lib/navigation';
|
import { appendCollectionId } from '~/lib/navigation';
|
||||||
|
import { useActiveCollection } from '~/store/collection_store';
|
||||||
import { CollectionWithLinks } from '~/types/app';
|
import { CollectionWithLinks } from '~/types/app';
|
||||||
import classes from './collection_item.module.css';
|
import classes from './collection_item.module.css';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Box, ScrollArea, Text } from '@mantine/core';
|
import { Box, ScrollArea, Text } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
|
||||||
import useCollections from '~/hooks/use_collections';
|
|
||||||
import useShortcut from '~/hooks/use_shortcut';
|
import useShortcut from '~/hooks/use_shortcut';
|
||||||
import CollectionItem from '~/mantine/components/dashboard/collection/item/collection_item';
|
import CollectionItem from '~/mantine/components/dashboard/collection/item/collection_item';
|
||||||
|
import { useActiveCollection, useCollections } from '~/store/collection_store';
|
||||||
import styles from './collection_list.module.css';
|
import styles from './collection_list.module.css';
|
||||||
|
|
||||||
export default function CollectionList() {
|
export default function CollectionList() {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { BsThreeDotsVertical } from 'react-icons/bs';
|
|||||||
import { GoPencil } from 'react-icons/go';
|
import { GoPencil } from 'react-icons/go';
|
||||||
import { IoIosAddCircleOutline } from 'react-icons/io';
|
import { IoIosAddCircleOutline } from 'react-icons/io';
|
||||||
import { IoTrashOutline } from 'react-icons/io5';
|
import { IoTrashOutline } from 'react-icons/io5';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
|
||||||
import { appendCollectionId } from '~/lib/navigation';
|
import { appendCollectionId } from '~/lib/navigation';
|
||||||
|
import { useActiveCollection } from '~/store/collection_store';
|
||||||
|
|
||||||
interface DashboardHeaderProps {
|
interface DashboardHeaderProps {
|
||||||
navbar: {
|
navbar: {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Box, Group, ScrollArea, Stack, Text } from '@mantine/core';
|
import { Box, Group, ScrollArea, Stack, Text } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import useFavorites from '~/hooks/use_favorites';
|
|
||||||
import { FavoriteItem } from '~/mantine/components/dashboard/favorite/item/favorite_item';
|
import { FavoriteItem } from '~/mantine/components/dashboard/favorite/item/favorite_item';
|
||||||
|
import { useFavorites } from '~/store/collection_store';
|
||||||
import styles from './favorite_list.module.css';
|
import styles from './favorite_list.module.css';
|
||||||
|
|
||||||
export function FavoriteList() {
|
export function FavoriteList() {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Link } from '@inertiajs/react';
|
|||||||
import { route } from '@izzyjs/route/client';
|
import { route } from '@izzyjs/route/client';
|
||||||
import { Anchor, Box, Text } from '@mantine/core';
|
import { Anchor, Box, Text } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import useActiveCollection from '~/hooks/use_active_collection';
|
|
||||||
import { appendCollectionId } from '~/lib/navigation';
|
import { appendCollectionId } from '~/lib/navigation';
|
||||||
|
import { useActiveCollection } from '~/store/collection_store';
|
||||||
import styles from './no_link.module.css';
|
import styles from './no_link.module.css';
|
||||||
|
|
||||||
export function NoLink() {
|
export function NoLink() {
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
|
import { router } from '@inertiajs/react';
|
||||||
|
import { route } from '@izzyjs/route/client';
|
||||||
import { AppShell, ScrollArea, Stack } from '@mantine/core';
|
import { AppShell, ScrollArea, Stack } from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import DashboardProviders from '~/components/dashboard/dashboard_provider';
|
|
||||||
import LinkItem from '~/components/dashboard/link/link_item';
|
import LinkItem from '~/components/dashboard/link/link_item';
|
||||||
import { MantineFooter } from '~/components/footer/mantine_footer';
|
import { MantineFooter } from '~/components/footer/mantine_footer';
|
||||||
|
import { useDisableOverflow } from '~/hooks/mantine/use_disable_overflow';
|
||||||
import useShortcut from '~/hooks/use_shortcut';
|
import useShortcut from '~/hooks/use_shortcut';
|
||||||
|
import { appendCollectionId } from '~/lib/navigation';
|
||||||
import { DashboardAside } from '~/mantine/components/dashboard/dashboard_aside';
|
import { DashboardAside } from '~/mantine/components/dashboard/dashboard_aside';
|
||||||
import { DashboardHeader } from '~/mantine/components/dashboard/dashboard_header';
|
import { DashboardHeader } from '~/mantine/components/dashboard/dashboard_header';
|
||||||
import { DashboardNavbar } from '~/mantine/components/dashboard/dashboard_navbar';
|
import { DashboardNavbar } from '~/mantine/components/dashboard/dashboard_navbar';
|
||||||
import { NoLink } from '~/mantine/components/dashboard/link/no_link';
|
import { NoLink } from '~/mantine/components/dashboard/link/no_link';
|
||||||
import { MantineDashboardLayout } from '~/mantine/layouts/mantine_dashboard_layout';
|
import { MantineDashboardLayout } from '~/mantine/layouts/mantine_dashboard_layout';
|
||||||
|
import {
|
||||||
|
useActiveCollection,
|
||||||
|
useCollectionsSetter,
|
||||||
|
} from '~/store/collection_store';
|
||||||
|
import { useGlobalHotkeysStore } from '~/store/global_hotkeys_store';
|
||||||
import { CollectionWithLinks } from '~/types/app';
|
import { CollectionWithLinks } from '~/types/app';
|
||||||
import classes from './dashboard.module.css';
|
import classes from './dashboard.module.css';
|
||||||
|
|
||||||
@@ -24,71 +32,91 @@ export default function MantineDashboard(props: Readonly<DashboardPageProps>) {
|
|||||||
const [openedAside, { toggle: toggleAside, close: closeAside }] =
|
const [openedAside, { toggle: toggleAside, close: closeAside }] =
|
||||||
useDisclosure();
|
useDisclosure();
|
||||||
|
|
||||||
|
const { activeCollection } = useActiveCollection();
|
||||||
|
const { _setCollections, setActiveCollection } = useCollectionsSetter();
|
||||||
|
const { globalHotkeysEnabled } = useGlobalHotkeysStore();
|
||||||
|
|
||||||
useShortcut('ESCAPE_KEY', () => {
|
useShortcut('ESCAPE_KEY', () => {
|
||||||
closeNavbar();
|
closeNavbar();
|
||||||
closeAside();
|
closeAside();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useDisableOverflow();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.style.overflow = 'hidden';
|
_setCollections(props.collections);
|
||||||
return () => {
|
setActiveCollection(props.activeCollection);
|
||||||
document.body.style.overflow = 'auto';
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useShortcut(
|
||||||
|
'OPEN_CREATE_LINK_KEY',
|
||||||
|
() =>
|
||||||
|
router.visit(
|
||||||
|
appendCollectionId(route('link.create-form').url, activeCollection?.id)
|
||||||
|
),
|
||||||
|
{
|
||||||
|
enabled: globalHotkeysEnabled,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
useShortcut(
|
||||||
|
'OPEN_CREATE_COLLECTION_KEY',
|
||||||
|
() => router.visit(route('collection.create-form').url),
|
||||||
|
{
|
||||||
|
enabled: globalHotkeysEnabled,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineDashboardLayout>
|
<MantineDashboardLayout>
|
||||||
<DashboardProviders {...props}>
|
<div className={classes.app_wrapper}>
|
||||||
<div className={classes.app_wrapper}>
|
<AppShell
|
||||||
<AppShell
|
layout="alt"
|
||||||
layout="alt"
|
header={{ height: 50 }}
|
||||||
header={{ height: 50 }}
|
navbar={{
|
||||||
navbar={{
|
width: 300,
|
||||||
width: 300,
|
breakpoint: 'sm',
|
||||||
breakpoint: 'sm',
|
collapsed: { mobile: !openedNavbar },
|
||||||
collapsed: { mobile: !openedNavbar },
|
}}
|
||||||
}}
|
aside={{
|
||||||
aside={{
|
width: 300,
|
||||||
width: 300,
|
breakpoint: 'md',
|
||||||
breakpoint: 'md',
|
collapsed: { mobile: !openedAside },
|
||||||
collapsed: { mobile: !openedAside },
|
}}
|
||||||
}}
|
classNames={{
|
||||||
classNames={{
|
aside: classes.ml_custom_class,
|
||||||
aside: classes.ml_custom_class,
|
footer: classes.ml_custom_class,
|
||||||
footer: classes.ml_custom_class,
|
navbar: classes.ml_custom_class,
|
||||||
navbar: classes.ml_custom_class,
|
header: classes.ml_custom_class,
|
||||||
header: classes.ml_custom_class,
|
}}
|
||||||
}}
|
className={classes.app_shell}
|
||||||
className={classes.app_shell}
|
>
|
||||||
>
|
<DashboardHeader
|
||||||
<DashboardHeader
|
navbar={{ opened: openedNavbar, toggle: toggleNavbar }}
|
||||||
navbar={{ opened: openedNavbar, toggle: toggleNavbar }}
|
aside={{ opened: openedAside, toggle: toggleAside }}
|
||||||
aside={{ opened: openedAside, toggle: toggleAside }}
|
/>
|
||||||
/>
|
<DashboardNavbar isOpen={openedNavbar} toggle={toggleNavbar} />
|
||||||
<DashboardNavbar isOpen={openedNavbar} toggle={toggleNavbar} />
|
<AppShell.Main>
|
||||||
<AppShell.Main>
|
{activeCollection?.links && activeCollection.links.length > 0 ? (
|
||||||
{props.activeCollection.links.length > 0 ? (
|
<ScrollArea
|
||||||
<ScrollArea
|
h="calc(100vh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))"
|
||||||
h="calc(100vh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))"
|
p="md"
|
||||||
p="md"
|
>
|
||||||
>
|
<Stack gap="xs">
|
||||||
<Stack gap="xs">
|
{activeCollection?.links.map((link) => (
|
||||||
{props.activeCollection.links.map((link) => (
|
<LinkItem key={link.id} link={link} showUserControls />
|
||||||
<LinkItem key={link.id} link={link} showUserControls />
|
))}
|
||||||
))}
|
</Stack>
|
||||||
</Stack>
|
</ScrollArea>
|
||||||
</ScrollArea>
|
) : (
|
||||||
) : (
|
<NoLink key={activeCollection?.id} />
|
||||||
<NoLink key={props.activeCollection.id} />
|
)}
|
||||||
)}
|
</AppShell.Main>
|
||||||
</AppShell.Main>
|
<DashboardAside isOpen={openedAside} toggle={toggleAside} />
|
||||||
<DashboardAside isOpen={openedAside} toggle={toggleAside} />
|
<AppShell.Footer pl="xs" pr="xs">
|
||||||
<AppShell.Footer pl="xs" pr="xs">
|
<MantineFooter />
|
||||||
<MantineFooter />
|
</AppShell.Footer>
|
||||||
</AppShell.Footer>
|
</AppShell>
|
||||||
</AppShell>
|
</div>
|
||||||
</div>
|
|
||||||
</DashboardProviders>
|
|
||||||
</MantineDashboardLayout>
|
</MantineDashboardLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
91
inertia/store/collection_store.ts
Normal file
91
inertia/store/collection_store.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
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() {
|
||||||
|
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[]);
|
||||||
|
}
|
||||||
11
inertia/store/global_hotkeys_store.ts
Normal file
11
inertia/store/global_hotkeys_store.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface GlobalHotkeysStore {
|
||||||
|
globalHotkeysEnabled: boolean;
|
||||||
|
setGlobalHotkeysEnabled: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGlobalHotkeysStore = create<GlobalHotkeysStore>((set) => ({
|
||||||
|
globalHotkeysEnabled: true,
|
||||||
|
setGlobalHotkeysEnabled: (value) => set({ globalHotkeysEnabled: value }),
|
||||||
|
}));
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node bin/server.js",
|
"start": "node bin/server.js",
|
||||||
"build": "node ace build",
|
"build": "node ace build",
|
||||||
"dev": "node ace serve --hmr",
|
"dev": "node ace serve --watch",
|
||||||
"test": "node ace test",
|
"test": "node ace test",
|
||||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||||
"format": "prettier --write --parser typescript '**/*.{ts,tsx}'",
|
"format": "prettier --write --parser typescript '**/*.{ts,tsx}'",
|
||||||
@@ -106,7 +106,8 @@
|
|||||||
"react-select": "^5.8.2",
|
"react-select": "^5.8.2",
|
||||||
"react-swipeable": "^7.0.1",
|
"react-swipeable": "^7.0.1",
|
||||||
"react-toggle": "^4.1.3",
|
"react-toggle": "^4.1.3",
|
||||||
"reflect-metadata": "^0.2.2"
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"zustand": "^5.0.1"
|
||||||
},
|
},
|
||||||
"hotHook": {
|
"hotHook": {
|
||||||
"boundaries": [
|
"boundaries": [
|
||||||
|
|||||||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@@ -125,6 +125,9 @@ importers:
|
|||||||
reflect-metadata:
|
reflect-metadata:
|
||||||
specifier: ^0.2.2
|
specifier: ^0.2.2
|
||||||
version: 0.2.2
|
version: 0.2.2
|
||||||
|
zustand:
|
||||||
|
specifier: ^5.0.1
|
||||||
|
version: 5.0.1(@types/react@18.3.12)(react@18.3.1)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@adonisjs/assembler':
|
'@adonisjs/assembler':
|
||||||
specifier: ^7.8.2
|
specifier: ^7.8.2
|
||||||
@@ -5115,6 +5118,24 @@ packages:
|
|||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
zustand@5.0.1:
|
||||||
|
resolution: {integrity: sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=18.0.0'
|
||||||
|
immer: '>=9.0.6'
|
||||||
|
react: '>=18.0.0'
|
||||||
|
use-sync-external-store: '>=1.2.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
use-sync-external-store:
|
||||||
|
optional: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@adonisjs/ace@13.3.0':
|
'@adonisjs/ace@13.3.0':
|
||||||
@@ -10181,3 +10202,8 @@ snapshots:
|
|||||||
validator: 13.12.0
|
validator: 13.12.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
commander: 9.5.0
|
commander: 9.5.0
|
||||||
|
|
||||||
|
zustand@5.0.1(@types/react@18.3.12)(react@18.3.1):
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 18.3.12
|
||||||
|
react: 18.3.1
|
||||||
|
|||||||
Reference in New Issue
Block a user