From 1e3264e2a088cb0ad713f52588716f3e834d6001 Mon Sep 17 00:00:00 2001 From: Sonny Date: Fri, 28 Apr 2023 00:32:22 +0200 Subject: [PATCH] feat: add favorite link btn --- src/components/Links/LinkItem.tsx | 25 ++- src/components/Links/Links.tsx | 12 +- src/components/QuickActions/FavoriteItem.tsx | 15 +- src/components/SearchModal/SearchModal.tsx | 8 +- .../SideMenu/Favorites/Favorites.tsx | 4 +- src/pages/index.tsx | 161 ++++++++++-------- src/types.d.ts | 2 +- src/utils/link.ts | 8 + 8 files changed, 144 insertions(+), 91 deletions(-) diff --git a/src/components/Links/LinkItem.tsx b/src/components/Links/LinkItem.tsx index e9dc0cb..6c022f3 100644 --- a/src/components/Links/LinkItem.tsx +++ b/src/components/Links/LinkItem.tsx @@ -1,3 +1,4 @@ +import axios from "axios"; import LinkTag from "next/link"; import { AiFillStar } from "react-icons/ai"; @@ -10,8 +11,26 @@ import LinkFavicon from "./LinkFavicon"; import styles from "./links.module.scss"; -export default function LinkItem({ link }: { link: Link }) { +export default function LinkItem({ + link, + toggleFavorite, +}: { + link: Link; + toggleFavorite: (linkId: Link["id"]) => void; +}) { const { id, name, url, favorite } = link; + const onFavorite = () => { + const payload = { + name, + url, + favorite: !favorite, + categoryId: link.category.id, + }; + axios + .put(`/api/link/edit/${link.id}`, payload) + .then(() => toggleFavorite(link.id)) + .catch(console.error); + }; return (
  • @@ -23,7 +42,7 @@ export default function LinkItem({ link }: { link: Link }) {
    - +
    @@ -31,7 +50,7 @@ export default function LinkItem({ link }: { link: Link }) { ); } -function LinkItemURL({ url }: { url: string }) { +function LinkItemURL({ url }: { url: Link["url"] }) { try { const { origin, pathname, search } = new URL(url); let text = ""; diff --git a/src/components/Links/Links.tsx b/src/components/Links/Links.tsx index d6d34d6..e97ac90 100644 --- a/src/components/Links/Links.tsx +++ b/src/components/Links/Links.tsx @@ -1,6 +1,6 @@ import LinkTag from "next/link"; -import { Category } from "types"; +import { Category, Link } from "types"; import EditItem from "components/QuickActions/EditItem"; import RemoveItem from "components/QuickActions/RemoveItem"; @@ -8,7 +8,13 @@ import LinkItem from "./LinkItem"; import styles from "./links.module.scss"; -export default function Links({ category }: { category: Category }) { +export default function Links({ + category, + toggleFavorite, +}: { + category: Category; + toggleFavorite: (linkId: Link["id"]) => void; +}) { if (category === null) { return (
    @@ -44,7 +50,7 @@ export default function Links({ category }: { category: Category }) {
      {links.map((link, key) => ( - + ))}
    diff --git a/src/components/QuickActions/FavoriteItem.tsx b/src/components/QuickActions/FavoriteItem.tsx index 0350697..7b5778b 100644 --- a/src/components/QuickActions/FavoriteItem.tsx +++ b/src/components/QuickActions/FavoriteItem.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState } from "react"; +import { ReactNode } from "react"; import { AiFillStar, AiOutlineStar } from "react-icons/ai"; export default function FavoriteItem({ @@ -8,19 +8,12 @@ export default function FavoriteItem({ }: { isFavorite: boolean; children?: ReactNode; - onClick?: (event: any) => void; // FIXME: find good event type + onClick: () => void; }) { const starColor = "#ffc107"; - const [isItemFavorite, setFavorite] = useState(isFavorite); - - const handleClick = (event) => { - onClick && onClick(event); - setFavorite((v) => !v); - }; - return ( -
    - {isItemFavorite ? ( +
    + {isFavorite ? ( ) : ( diff --git a/src/components/SearchModal/SearchModal.tsx b/src/components/SearchModal/SearchModal.tsx index 558dd8f..eaf1726 100644 --- a/src/components/SearchModal/SearchModal.tsx +++ b/src/components/SearchModal/SearchModal.tsx @@ -8,7 +8,7 @@ import LinkFavicon from "components/Links/LinkFavicon"; import Modal from "components/Modal/Modal"; import TextBox from "components/TextBox"; -import { Category, ItemComplete, Link } from "types"; +import { Category, Link, SearchItem } from "types"; import styles from "./search.module.scss"; @@ -25,7 +25,7 @@ export default function SearchModal({ handleSelectCategory: (category: Category) => void; categories: Category[]; favorites: Link[]; - items: ItemComplete[]; + items: SearchItem[]; }) { const autoFocusRef = useAutoFocus(); @@ -130,7 +130,7 @@ function ListItemComponent({ items, noItem, }: { - items: ItemComplete[]; + items: SearchItem[]; noItem?: ReactNode; }) { return ( @@ -156,7 +156,7 @@ function ListItemComponent({ ); } -function ItemComponent({ item }: { item: ItemComplete }) { +function ItemComponent({ item }: { item: SearchItem }) { const { name, type, url } = item; return (
  • diff --git a/src/components/SideMenu/Favorites/Favorites.tsx b/src/components/SideMenu/Favorites/Favorites.tsx index eb92581..9ccf356 100644 --- a/src/components/SideMenu/Favorites/Favorites.tsx +++ b/src/components/SideMenu/Favorites/Favorites.tsx @@ -9,8 +9,8 @@ export default function Favorites({ favorites }: { favorites: Link[] }) {

    Favoris

      - {favorites.map((link, key) => ( - + {favorites.map((link) => ( + ))}
    diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 358284a..9ca4470 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,5 +1,5 @@ import { useRouter } from "next/router"; -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import useModal from "hooks/useModal"; @@ -8,9 +8,10 @@ import Links from "components/Links/Links"; import SearchModal from "components/SearchModal/SearchModal"; import SideMenu from "components/SideMenu/SideMenu"; -import { Category, ItemComplete, Link } from "types"; +import { Category, Link, SearchItem } from "types"; import { prisma } from "utils/back"; import { BuildCategory } from "utils/front"; +import { pushStateVanilla } from "utils/link"; const OPEN_SEARCH_KEY = "s"; const CLOSE_SEARCH_KEY = "escape"; @@ -20,58 +21,114 @@ const OPEN_CREATE_CATEGORY_KEY = "c"; interface HomeProps { categories: Category[]; - favorites: Link[]; - items: ItemComplete[]; currentCategory: Category | undefined; } -function Home({ categories, favorites, currentCategory, items }: HomeProps) { +function Home(props: HomeProps) { const router = useRouter(); const modal = useModal(); + const [categories, setCategories] = useState(props.categories); const [categoryActive, setCategoryActive] = useState( - currentCategory || categories?.[0] + props.currentCategory || categories?.[0] + ); + + const favorites = useMemo( + () => + categories.reduce((acc, category) => { + category.links.forEach((link) => + link.favorite ? acc.push(link) : null + ); + return acc; + }, [] as Link[]), + [categories] + ); + + const searchItemBuilder = ( + item: Category | Link, + type: SearchItem["type"] + ): SearchItem => ({ + id: item.id, + name: item.name, + url: type === "link" ? (item as Link).url : `/?categoryId=${item.id}`, + type, + }); + + const itemsSearch = useMemo(() => { + const items = categories.reduce((acc, category) => { + const categoryItem = searchItemBuilder(category, "category"); + const items: SearchItem[] = category.links.map((link) => + searchItemBuilder(link, "link") + ); + return [...acc, ...items, categoryItem]; + }, [] as SearchItem[]); + + return items; + }, [categories]); + + // TODO: refacto + const toggleFavorite = useCallback( + (linkId: Link["id"]) => { + let linkIndex = 0; + const categoryIndex = categories.findIndex(({ links }) => { + const lIndex = links.findIndex((l) => l.id === linkId); + if (lIndex !== -1) { + linkIndex = lIndex; + } + return lIndex !== -1; + }); + + const link = categories[categoryIndex].links[linkIndex]; + const categoriesCopy = [...categories]; + categoriesCopy[categoryIndex].links[linkIndex] = { + ...link, + favorite: !link.favorite, + }; + + setCategories(categoriesCopy); + if (categories[categoryIndex].id === categoryActive.id) { + setCategoryActive(categories[categoryIndex]); + } + }, + [categories, categoryActive.id] ); const handleSelectCategory = (category: Category) => { setCategoryActive(category); - const newUrl = `/?categoryId=${category.id}`; - window.history.replaceState( - { ...window.history.state, as: newUrl, url: newUrl }, - "", - newUrl - ); + pushStateVanilla(`/?categoryId=${category.id}`); }; - const openSearchModal = useCallback( + useHotkeys( + OPEN_SEARCH_KEY, (event) => { event.preventDefault(); modal.open(); }, - [modal] + { enabled: !modal.isShowing } ); - const closeSearchModal = useCallback(() => modal.close(), [modal]); - - const gotoCreateLink = useCallback(() => { - router.push(`/link/create?categoryId=${categoryActive.id}`); - }, [categoryActive.id, router]); - - const gotoCreateCategory = useCallback(() => { - router.push("/category/create"); - }, [router]); - - useHotkeys(OPEN_SEARCH_KEY, openSearchModal, { enabled: !modal.isShowing }); - useHotkeys(CLOSE_SEARCH_KEY, closeSearchModal, { + useHotkeys(CLOSE_SEARCH_KEY, modal.close, { enabled: modal.isShowing, enableOnFormTags: ["INPUT"], }); - useHotkeys(OPEN_CREATE_LINK_KEY, gotoCreateLink, { - enabled: !modal.isShowing, - }); - useHotkeys(OPEN_CREATE_CATEGORY_KEY, gotoCreateCategory, { - enabled: !modal.isShowing, - }); + useHotkeys( + OPEN_CREATE_LINK_KEY, + () => { + router.push(`/link/create?categoryId=${categoryActive.id}`); + }, + { + enabled: !modal.isShowing, + } + ); + useHotkeys( + OPEN_CREATE_CATEGORY_KEY, + () => { + router.push("/category/create"); + }, + { + enabled: !modal.isShowing, + } + ); return (
    @@ -82,13 +139,13 @@ function Home({ categories, favorites, currentCategory, items }: HomeProps) { categoryActive={categoryActive} openSearchModal={modal.open} /> - + {modal.isShowing && ( )} @@ -98,40 +155,11 @@ function Home({ categories, favorites, currentCategory, items }: HomeProps) { export async function getServerSideProps({ query }) { const queryCategoryId = (query?.categoryId as string) || ""; - const categoriesDB = await prisma.category.findMany({ include: { links: true }, }); - const items = [] as ItemComplete[]; - - const favorites = [] as Link[]; - const categories = categoriesDB.map((categoryDB) => { - const category = BuildCategory(categoryDB); - - category.links.map((link) => { - if (link.favorite) { - favorites.push(link); - } - items.push({ - id: link.id, - name: link.name, - url: link.url, - type: "link", - }); - }); - - items.push({ - id: category.id, - name: category.name, - url: `/?categoryId=${category.id}`, - type: "category", - }); - - return category; - }); - - if (categories.length === 0) { + if (categoriesDB.length === 0) { return { redirect: { destination: "/category/create", @@ -139,15 +167,14 @@ export async function getServerSideProps({ query }) { }; } + const categories = categoriesDB.map(BuildCategory); const currentCategory = categories.find( - (c) => c.id === Number(queryCategoryId) + ({ id }) => id === Number(queryCategoryId) ); return { props: { categories: JSON.parse(JSON.stringify(categories)), - favorites: JSON.parse(JSON.stringify(favorites)), - items: JSON.parse(JSON.stringify(items)), currentCategory: currentCategory ? JSON.parse(JSON.stringify(currentCategory)) : null, diff --git a/src/types.d.ts b/src/types.d.ts index 91675e3..df30dea 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -27,7 +27,7 @@ export interface Link { updatedAt: Date; } -export interface ItemComplete { +export interface SearchItem { id: number; name: string; url: string; diff --git a/src/utils/link.ts b/src/utils/link.ts index e502eff..675225a 100644 --- a/src/utils/link.ts +++ b/src/utils/link.ts @@ -1,3 +1,11 @@ export function faviconLinkBuilder(origin: string) { return `http://localhost:3000/api/favicon?url=${origin}`; } + +export function pushStateVanilla(newUrl: string) { + window.history.replaceState( + { ...window.history.state, as: newUrl, url: newUrl }, + "", + newUrl + ); +}