feat: add favorite link btn

This commit is contained in:
Sonny
2023-04-28 00:32:22 +02:00
parent 74191b9134
commit 1e3264e2a0
8 changed files with 144 additions and 91 deletions

View File

@@ -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 (
<li className={styles["link"]} key={id}>
@@ -23,7 +42,7 @@ export default function LinkItem({ link }: { link: Link }) {
<LinkItemURL url={url} />
</LinkTag>
<div className={styles["controls"]}>
<FavoriteItem isFavorite={favorite} />
<FavoriteItem isFavorite={favorite} onClick={onFavorite} />
<EditItem type="link" id={id} />
<RemoveItem type="link" id={id} />
</div>
@@ -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 = "";

View File

@@ -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 (
<div className={styles["no-category"]}>
@@ -44,7 +50,7 @@ export default function Links({ category }: { category: Category }) {
</h2>
<ul className={styles["links"]} key={Math.random()}>
{links.map((link, key) => (
<LinkItem key={key} link={link} />
<LinkItem key={key} link={link} toggleFavorite={toggleFavorite} />
))}
</ul>
</div>

View File

@@ -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<boolean>(isFavorite);
const handleClick = (event) => {
onClick && onClick(event);
setFavorite((v) => !v);
};
return (
<div onClick={handleClick}>
{isItemFavorite ? (
<div onClick={onClick}>
{isFavorite ? (
<AiFillStar color={starColor} />
) : (
<AiOutlineStar color={starColor} />

View File

@@ -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 (
<li className={styles["item"]}>

View File

@@ -9,8 +9,8 @@ export default function Favorites({ favorites }: { favorites: Link[] }) {
<div className={styles["favorites"]}>
<h4>Favoris</h4>
<ul className={styles["items"]}>
{favorites.map((link, key) => (
<FavoriteItem link={link} key={key} />
{favorites.map((link) => (
<FavoriteItem link={link} key={link.id} />
))}
</ul>
</div>

View File

@@ -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<Category[]>(props.categories);
const [categoryActive, setCategoryActive] = useState<Category | null>(
currentCategory || categories?.[0]
props.currentCategory || categories?.[0]
);
const favorites = useMemo<Link[]>(
() =>
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<SearchItem[]>(() => {
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 (
<div className="App">
@@ -82,13 +139,13 @@ function Home({ categories, favorites, currentCategory, items }: HomeProps) {
categoryActive={categoryActive}
openSearchModal={modal.open}
/>
<Links category={categoryActive} />
<Links category={categoryActive} toggleFavorite={toggleFavorite} />
{modal.isShowing && (
<SearchModal
close={modal.close}
categories={categories}
favorites={favorites}
items={items}
items={itemsSearch}
handleSelectCategory={handleSelectCategory}
/>
)}
@@ -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,

2
src/types.d.ts vendored
View File

@@ -27,7 +27,7 @@ export interface Link {
updatedAt: Date;
}
export interface ItemComplete {
export interface SearchItem {
id: number;
name: string;
url: string;

View File

@@ -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
);
}