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 { ReactNode, useRef } from 'react';
import { HtmlHTMLAttributes, ReactNode, useRef } from 'react';
import DropdownContainer from '~/components/common/dropdown/dropdown_container';
import DropdownLabel from '~/components/common/dropdown/dropdown_label';
import useClickOutside from '~/hooks/use_click_outside';
@@ -34,8 +34,8 @@ export default function Dropdown({
label,
className,
svgSize,
}: {
children: ReactNode;
onClick,
}: HtmlHTMLAttributes<HTMLDivElement> & {
label: ReactNode | string;
className?: string;
svgSize?: number;
@@ -49,7 +49,10 @@ export default function Dropdown({
return (
<DropdownStyle
opened={isShowing}
onClick={toggle}
onClick={(event) => {
onClick?.(event);
toggle();
}}
ref={dropdownRef}
className={className}
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 useShortcut from '~/hooks/use_shortcut';
import { appendCollectionId } from '~/lib/navigation';
import { CollectionWithLinks, Link } from '~/types/app';
import { CollectionWithLinks, LinkWithCollection } from '~/types/app';
export default function DashboardProviders(
props: Readonly<{
@@ -31,14 +31,18 @@ export default function DashboardProviders(
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) => {
collection.links.forEach((link) =>
link.favorite ? acc.push(link) : null
);
collection.links.forEach((link) => {
if (link.favorite) {
const newLink: LinkWithCollection = { ...link, collection };
acc.push(newLink);
}
});
return acc;
}, [] as Link[]),
}, [] as LinkWithCollection[]),
[collections]
);

View File

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

View File

@@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import { RefObject, useEffect, useRef, useState } from 'react';
import { AiOutlineFolder } from 'react-icons/ai';
import Legend from '~/components/common/legend';
import TextEllipsis from '~/components/common/text_ellipsis';
import LinkFavicon from '~/components/dashboard/link/link_favicon';
import useCollections from '~/hooks/use_collections';
@@ -22,11 +23,6 @@ const SearchItemStyle = styled('li', {
padding: '0.25em 0.35em !important',
}));
const ItemLegeng = styled.span(({ theme }) => ({
fontSize: '13px',
color: theme.colors.grey,
}));
interface CommonResultProps {
innerRef: RefObject<HTMLLIElement>;
isActive: boolean;
@@ -100,7 +96,7 @@ function ResultLink({
__html: result.matched_part ?? result.name,
}}
/>
<ItemLegeng>({collection.name})</ItemLegeng>
<Legend>({collection.name})</Legend>
</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 { 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 { appendCollectionId, appendLinkId } from '~/lib/navigation';
import { LinkWithCollection } from '~/types/app';
const FavoriteItem = styled(ItemExternalLink)(({ theme }) => ({
const FavoriteItemStyle = styled(ItemExternalLink)(({ theme }) => ({
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 { 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 FavoriteItem from '~/components/dashboard/side_nav/favorite/favorite_item';
import useFavorites from '~/hooks/use_favorites';
@@ -44,11 +42,8 @@ export default function FavoriteList() {
{t('favorite')} {favorites.length}
</FavoriteLabel>
<FavoriteListStyle>
{favorites.map(({ id, name, url }) => (
<FavoriteItem href={url} key={id}>
<LinkFavicon url={url} size={24} />
<TextEllipsis>{name}</TextEllipsis>
</FavoriteItem>
{favorites.map((link) => (
<FavoriteItem link={link} key={link.id} />
))}
</FavoriteListStyle>
</FavoriteListContainer>

View File

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

View File

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

View File

@@ -32,6 +32,7 @@
"add-favorite": "Ajouter aux favoris",
"remove-favorite": "Retirer des favoris",
"favorites-appears-here": "Vos favoris apparaîtront ici",
"go-to-collection": "Voir la collection",
"no-item-found": "Aucun élément trouvé",
"admin": "Administrateur",
"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);
};