diff --git a/inertia/components/common/dropdown/dropdown.tsx b/inertia/components/common/dropdown/dropdown.tsx index 685cd26..9944be3 100644 --- a/inertia/components/common/dropdown/dropdown.tsx +++ b/inertia/components/common/dropdown/dropdown.tsx @@ -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 & { label: ReactNode | string; className?: string; svgSize?: number; @@ -49,7 +49,10 @@ export default function Dropdown({ return ( { + onClick?.(event); + toggle(); + }} ref={dropdownRef} className={className} svgSize={svgSize} diff --git a/inertia/components/common/legend.tsx b/inertia/components/common/legend.tsx new file mode 100644 index 0000000..187e85f --- /dev/null +++ b/inertia/components/common/legend.tsx @@ -0,0 +1,8 @@ +import styled from '@emotion/styled'; + +const Legend = styled.span(({ theme }) => ({ + fontSize: '13px', + color: theme.colors.grey, +})); + +export default Legend; diff --git a/inertia/components/dashboard/dashboard_provider.tsx b/inertia/components/dashboard/dashboard_provider.tsx index 90651e3..a8e3c82 100644 --- a/inertia/components/dashboard/dashboard_provider.tsx +++ b/inertia/components/dashboard/dashboard_provider.tsx @@ -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( + // TODO: compute this in controller + const favorites = useMemo( () => 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] ); diff --git a/inertia/components/dashboard/link/link_controls.tsx b/inertia/components/dashboard/link/link_controls.tsx index c76c5f3..12dd582 100644 --- a/inertia/components/dashboard/link/link_controls.tsx +++ b/inertia/components/dashboard/link/link_controls.tsx @@ -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 ( - - {!link.favorite ? ( - <> - {t('add-favorite')} - - ) : ( - <> - {t('remove-favorite')} - - )} - + diff --git a/inertia/components/dashboard/search/search_result_item.tsx b/inertia/components/dashboard/search/search_result_item.tsx index ef8ac08..03877d0 100644 --- a/inertia/components/dashboard/search/search_result_item.tsx +++ b/inertia/components/dashboard/search/search_result_item.tsx @@ -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; isActive: boolean; @@ -100,7 +96,7 @@ function ResultLink({ __html: result.matched_part ?? result.name, }} /> - ({collection.name}) + ({collection.name}) ); } diff --git a/inertia/components/dashboard/side_nav/favorite/favorite_dropdown_item.tsx b/inertia/components/dashboard/side_nav/favorite/favorite_dropdown_item.tsx new file mode 100644 index 0000000..f4ed1d1 --- /dev/null +++ b/inertia/components/dashboard/side_nav/favorite/favorite_dropdown_item.tsx @@ -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 ( + onFavorite(link.id, !link.favorite, onFavoriteCallback)} + > + {!link.favorite ? ( + <> + {t('add-favorite')} + + ) : ( + <> + {t('remove-favorite')} + + )} + + ); +} diff --git a/inertia/components/dashboard/side_nav/favorite/favorite_item.tsx b/inertia/components/dashboard/side_nav/favorite/favorite_item.tsx index b6eb366..a0ffceb 100644 --- a/inertia/components/dashboard/side_nav/favorite/favorite_item.tsx +++ b/inertia/components/dashboard/side_nav/favorite/favorite_item.tsx @@ -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 ( + + + {link.name} + ({link.collection.name}) + { + event.preventDefault(); + event.stopPropagation(); + }} + label={} + svgSize={18} + > + + {t('go-to-collection')} + + + + {t('link.edit')} + + + {t('link.delete')} + + + + ); +} diff --git a/inertia/components/dashboard/side_nav/favorite/favorite_list.tsx b/inertia/components/dashboard/side_nav/favorite/favorite_list.tsx index 306b7ed..da6acc0 100644 --- a/inertia/components/dashboard/side_nav/favorite/favorite_list.tsx +++ b/inertia/components/dashboard/side_nav/favorite/favorite_list.tsx @@ -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} - {favorites.map(({ id, name, url }) => ( - - - {name} - + {favorites.map((link) => ( + ))} diff --git a/inertia/contexts/favorites_context.ts b/inertia/contexts/favorites_context.ts index 2ec796b..0bed1e9 100644 --- a/inertia/contexts/favorites_context.ts +++ b/inertia/contexts/favorites_context.ts @@ -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( diff --git a/inertia/i18n/locales/en/common.json b/inertia/i18n/locales/en/common.json index cc403c8..5893dfd 100644 --- a/inertia/i18n/locales/en/common.json +++ b/inertia/i18n/locales/en/common.json @@ -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", diff --git a/inertia/i18n/locales/fr/common.json b/inertia/i18n/locales/fr/common.json index cfa990a..3c16af8 100644 --- a/inertia/i18n/locales/fr/common.json +++ b/inertia/i18n/locales/fr/common.json @@ -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", diff --git a/inertia/lib/favorite.ts b/inertia/lib/favorite.ts new file mode 100644 index 0000000..817fb53 --- /dev/null +++ b/inertia/lib/favorite.ts @@ -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); +};