feat: add dropdown for favorite items

This commit is contained in:
Sonny
2024-07-22 12:10:47 +02:00
parent 2c499a7789
commit 442a1003bb
12 changed files with 174 additions and 94 deletions

View File

@@ -1,5 +1,5 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { ReactNode, useRef } from 'react'; import { HtmlHTMLAttributes, ReactNode, useRef } from 'react';
import DropdownContainer from '~/components/common/dropdown/dropdown_container'; import DropdownContainer from '~/components/common/dropdown/dropdown_container';
import DropdownLabel from '~/components/common/dropdown/dropdown_label'; import DropdownLabel from '~/components/common/dropdown/dropdown_label';
import useClickOutside from '~/hooks/use_click_outside'; import useClickOutside from '~/hooks/use_click_outside';
@@ -34,8 +34,8 @@ export default function Dropdown({
label, label,
className, className,
svgSize, svgSize,
}: { onClick,
children: ReactNode; }: HtmlHTMLAttributes<HTMLDivElement> & {
label: ReactNode | string; label: ReactNode | string;
className?: string; className?: string;
svgSize?: number; svgSize?: number;
@@ -49,7 +49,10 @@ export default function Dropdown({
return ( return (
<DropdownStyle <DropdownStyle
opened={isShowing} opened={isShowing}
onClick={toggle} onClick={(event) => {
onClick?.(event);
toggle();
}}
ref={dropdownRef} ref={dropdownRef}
className={className} className={className}
svgSize={svgSize} svgSize={svgSize}

View File

@@ -0,0 +1,8 @@
import styled from '@emotion/styled';
const Legend = styled.span(({ theme }) => ({
fontSize: '13px',
color: theme.colors.grey,
}));
export default Legend;

View File

@@ -7,7 +7,7 @@ import FavoritesContext from '~/contexts/favorites_context';
import GlobalHotkeysContext from '~/contexts/global_hotkeys_context'; import GlobalHotkeysContext from '~/contexts/global_hotkeys_context';
import useShortcut from '~/hooks/use_shortcut'; import useShortcut from '~/hooks/use_shortcut';
import { appendCollectionId } from '~/lib/navigation'; import { appendCollectionId } from '~/lib/navigation';
import { CollectionWithLinks, Link } from '~/types/app'; import { CollectionWithLinks, LinkWithCollection } from '~/types/app';
export default function DashboardProviders( export default function DashboardProviders(
props: Readonly<{ props: Readonly<{
@@ -31,14 +31,18 @@ export default function DashboardProviders(
router.visit(appendCollectionId(route('dashboard').url, collection.id)); router.visit(appendCollectionId(route('dashboard').url, collection.id));
}; };
const favorites = useMemo<Link[]>( // TODO: compute this in controller
const favorites = useMemo<LinkWithCollection[]>(
() => () =>
collections.reduce((acc, collection) => { collections.reduce((acc, collection) => {
collection.links.forEach((link) => collection.links.forEach((link) => {
link.favorite ? acc.push(link) : null if (link.favorite) {
); const newLink: LinkWithCollection = { ...link, collection };
acc.push(newLink);
}
});
return acc; return acc;
}, [] as Link[]), }, [] as LinkWithCollection[]),
[collections] [collections]
); );

View File

@@ -1,71 +1,18 @@
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { route } from '@izzyjs/route/client'; import { route } from '@izzyjs/route/client';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
import { BsThreeDotsVertical } from 'react-icons/bs'; import { BsThreeDotsVertical } from 'react-icons/bs';
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 Dropdown from '~/components/common/dropdown/dropdown'; import Dropdown from '~/components/common/dropdown/dropdown';
import { import { DropdownItemLink } from '~/components/common/dropdown/dropdown_item';
DropdownItemButton, import FavoriteDropdownItem from '~/components/dashboard/side_nav/favorite/favorite_dropdown_item';
DropdownItemLink,
} from '~/components/common/dropdown/dropdown_item';
import useActiveCollection from '~/hooks/use_active_collection';
import useCollections from '~/hooks/use_collections';
import { appendLinkId } from '~/lib/navigation'; import { appendLinkId } from '~/lib/navigation';
import { makeRequest } from '~/lib/request';
import { Link } from '~/types/app'; import { Link } from '~/types/app';
const StartItem = styled(DropdownItemButton)(({ theme }) => ({
color: theme.colors.yellow,
}));
export default function LinkControls({ link }: { link: Link }) { export default function LinkControls({ link }: { link: Link }) {
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation('common'); const { t } = useTranslation('common');
const { collections, setCollections } = useCollections();
const { setActiveCollection } = useActiveCollection();
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 onFavorite = () => {
const { url, method } = route('link.toggle-favorite', {
params: { id: link.id.toString() },
});
makeRequest({
url,
method,
body: {
favorite: !link.favorite,
},
})
.then(() => toggleFavorite(link.id))
.catch(console.error);
};
return ( return (
<Dropdown <Dropdown
@@ -73,17 +20,7 @@ export default function LinkControls({ link }: { link: Link }) {
css={{ backgroundColor: theme.colors.secondary }} css={{ backgroundColor: theme.colors.secondary }}
svgSize={18} svgSize={18}
> >
<StartItem onClick={onFavorite}> <FavoriteDropdownItem link={link} />
{!link.favorite ? (
<>
<AiFillStar /> {t('add-favorite')}
</>
) : (
<>
<AiOutlineStar /> {t('remove-favorite')}
</>
)}
</StartItem>
<DropdownItemLink <DropdownItemLink
href={appendLinkId(route('link.edit-form').url, link.id)} href={appendLinkId(route('link.edit-form').url, link.id)}
> >

View File

@@ -1,6 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { RefObject, useEffect, useRef, useState } from 'react'; import { RefObject, useEffect, useRef, useState } from 'react';
import { AiOutlineFolder } from 'react-icons/ai'; import { AiOutlineFolder } from 'react-icons/ai';
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 '~/hooks/use_collections';
@@ -22,11 +23,6 @@ const SearchItemStyle = styled('li', {
padding: '0.25em 0.35em !important', padding: '0.25em 0.35em !important',
})); }));
const ItemLegeng = styled.span(({ theme }) => ({
fontSize: '13px',
color: theme.colors.grey,
}));
interface CommonResultProps { interface CommonResultProps {
innerRef: RefObject<HTMLLIElement>; innerRef: RefObject<HTMLLIElement>;
isActive: boolean; isActive: boolean;
@@ -100,7 +96,7 @@ function ResultLink({
__html: result.matched_part ?? result.name, __html: result.matched_part ?? result.name,
}} }}
/> />
<ItemLegeng>({collection.name})</ItemLegeng> <Legend>({collection.name})</Legend>
</SearchItemStyle> </SearchItemStyle>
); );
} }

View File

@@ -0,0 +1,60 @@
import styled from '@emotion/styled';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
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 { Link } from '~/types/app';
const StarItem = styled(DropdownItemButton)(({ theme }) => ({
color: theme.colors.yellow,
}));
export default function FavoriteDropdownItem({ link }: { link: Link }) {
const { collections, setCollections } = useCollections();
const { setActiveCollection } = useActiveCollection();
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);
return (
<StarItem
onClick={() => onFavorite(link.id, !link.favorite, onFavoriteCallback)}
>
{!link.favorite ? (
<>
<AiFillStar /> {t('add-favorite')}
</>
) : (
<>
<AiOutlineStar /> {t('remove-favorite')}
</>
)}
</StarItem>
);
}

View File

@@ -1,8 +1,61 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { route } from '@izzyjs/route/client';
import { useTranslation } from 'react-i18next';
import { BsThreeDotsVertical } from 'react-icons/bs';
import { FaRegEye } from 'react-icons/fa';
import { GoPencil } from 'react-icons/go';
import { IoTrashOutline } from 'react-icons/io5';
import Dropdown from '~/components/common/dropdown/dropdown';
import { DropdownItemLink } from '~/components/common/dropdown/dropdown_item';
import Legend from '~/components/common/legend';
import TextEllipsis from '~/components/common/text_ellipsis';
import LinkFavicon from '~/components/dashboard/link/link_favicon';
import FavoriteDropdownItem from '~/components/dashboard/side_nav/favorite/favorite_dropdown_item';
import { ItemExternalLink } from '~/components/dashboard/side_nav/nav_item'; import { ItemExternalLink } from '~/components/dashboard/side_nav/nav_item';
import { appendCollectionId, appendLinkId } from '~/lib/navigation';
import { LinkWithCollection } from '~/types/app';
const FavoriteItem = styled(ItemExternalLink)(({ theme }) => ({ const FavoriteItemStyle = styled(ItemExternalLink)(({ theme }) => ({
backgroundColor: theme.colors.secondary, backgroundColor: theme.colors.secondary,
})); }));
export default FavoriteItem; const FavoriteDropdown = styled(Dropdown)(({ theme }) => ({
backgroundColor: theme.colors.secondary,
}));
export default function FavoriteItem({ link }: { link: LinkWithCollection }) {
const { t } = useTranslation();
return (
<FavoriteItemStyle href={link.url}>
<LinkFavicon url={link.url} size={24} />
<TextEllipsis>{link.name}</TextEllipsis>
<Legend>({link.collection.name})</Legend>
<FavoriteDropdown
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
}}
label={<BsThreeDotsVertical />}
svgSize={18}
>
<DropdownItemLink
href={appendCollectionId(route('dashboard').url, link.collection.id)}
>
<FaRegEye /> {t('go-to-collection')}
</DropdownItemLink>
<FavoriteDropdownItem link={link} />
<DropdownItemLink
href={appendLinkId(route('link.edit-form').url, link.id)}
>
<GoPencil /> {t('link.edit')}
</DropdownItemLink>
<DropdownItemLink
href={appendLinkId(route('link.delete-form').url, link.id)}
danger
>
<IoTrashOutline /> {t('link.delete')}
</DropdownItemLink>
</FavoriteDropdown>
</FavoriteItemStyle>
);
}

View File

@@ -1,7 +1,5 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import TextEllipsis from '~/components/common/text_ellipsis';
import LinkFavicon from '~/components/dashboard/link/link_favicon';
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 '~/hooks/use_favorites';
@@ -44,11 +42,8 @@ export default function FavoriteList() {
{t('favorite')} {favorites.length} {t('favorite')} {favorites.length}
</FavoriteLabel> </FavoriteLabel>
<FavoriteListStyle> <FavoriteListStyle>
{favorites.map(({ id, name, url }) => ( {favorites.map((link) => (
<FavoriteItem href={url} key={id}> <FavoriteItem link={link} key={link.id} />
<LinkFavicon url={url} size={24} />
<TextEllipsis>{name}</TextEllipsis>
</FavoriteItem>
))} ))}
</FavoriteListStyle> </FavoriteListStyle>
</FavoriteListContainer> </FavoriteListContainer>

View File

@@ -1,12 +1,12 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { Link } from '~/types/app'; import { LinkWithCollection } from '~/types/app';
type FavoritesContextType = { type FavoritesContextType = {
favorites: Link[]; favorites: LinkWithCollection[];
}; };
const iFavoritesContextState = { const iFavoritesContextState = {
favorites: [] as Link[], favorites: [] as LinkWithCollection[],
}; };
const FavoritesContext = createContext<FavoritesContextType>( const FavoritesContext = createContext<FavoritesContextType>(

View File

@@ -32,6 +32,7 @@
"add-favorite": "Add to favorites", "add-favorite": "Add to favorites",
"remove-favorite": "Remove from favorites", "remove-favorite": "Remove from favorites",
"favorites-appears-here": "Your favorites will appear here", "favorites-appears-here": "Your favorites will appear here",
"go-to-collection": "Go to collection",
"no-item-found": "No item found", "no-item-found": "No item found",
"admin": "Administrator", "admin": "Administrator",
"search": "Search", "search": "Search",

View File

@@ -32,6 +32,7 @@
"add-favorite": "Ajouter aux favoris", "add-favorite": "Ajouter aux favoris",
"remove-favorite": "Retirer des favoris", "remove-favorite": "Retirer des favoris",
"favorites-appears-here": "Vos favoris apparaîtront ici", "favorites-appears-here": "Vos favoris apparaîtront ici",
"go-to-collection": "Voir la collection",
"no-item-found": "Aucun élément trouvé", "no-item-found": "Aucun élément trouvé",
"admin": "Administrateur", "admin": "Administrateur",
"search": "Rechercher", "search": "Rechercher",

22
inertia/lib/favorite.ts Normal file
View File

@@ -0,0 +1,22 @@
import { route } from '@izzyjs/route/client';
import { makeRequest } from '~/lib/request';
import { Link } from '~/types/app';
export const onFavorite = (
linkId: Link['id'],
isFavorite: boolean,
cb: () => void
) => {
const { url, method } = route('link.toggle-favorite', {
params: { id: linkId.toString() },
});
makeRequest({
url,
method,
body: {
favorite: isFavorite,
},
})
.then(() => cb())
.catch(console.error);
};