From 91f21737642499f787f863e3acfec821f1f93b52 Mon Sep 17 00:00:00 2001 From: Sonny Date: Mon, 24 Apr 2023 15:19:54 +0200 Subject: [PATCH] feat: add search modal --- components/Modal/Modal.tsx | 12 +- components/Modal/modal.module.scss | 36 +++- components/SearchModal/SearchModal.tsx | 108 ++++++----- components/SearchModal/search.module.scss | 47 ++++- components/SideMenu/SideMenu.tsx | 6 +- components/SideMenu/sidemenu.module.scss | 8 +- example.env | 17 +- pages/index.tsx | 5 +- pages/search.tsx | 218 ---------------------- styles/keyframes.scss | 22 +++ 10 files changed, 183 insertions(+), 296 deletions(-) delete mode 100644 pages/search.tsx diff --git a/components/Modal/Modal.tsx b/components/Modal/Modal.tsx index 1e93548..47b66f4 100644 --- a/components/Modal/Modal.tsx +++ b/components/Modal/Modal.tsx @@ -1,5 +1,6 @@ import { ReactNode } from "react"; import { createPortal } from "react-dom"; +import { GrClose } from "react-icons/gr"; import styles from "./modal.module.scss"; @@ -17,12 +18,19 @@ export default function Modal({ children, showCloseBtn = true, }: ModalProps) { + const handleWrapperClick = (event) => + event.target.classList?.[0] === styles["modal-wrapper"] && close(); + return createPortal( -
+

{title}

- {showCloseBtn && } + {showCloseBtn && ( + + )}
{children}
diff --git a/components/Modal/modal.module.scss b/components/Modal/modal.module.scss index cde0bbe..8b5f307 100644 --- a/components/Modal/modal.module.scss +++ b/components/Modal/modal.module.scss @@ -1,4 +1,5 @@ @import "../../styles/colors.scss"; +@import "../../styles/keyframes.scss"; .modal-wrapper { z-index: 9999; @@ -13,10 +14,41 @@ align-items: center; justify-content: center; flex-direction: column; + animation: opacityin 0.3s both; } .modal-container { background: $light-grey; - min-height: 250px; - min-width: 250px; + min-width: 500px; + padding: 1em 1.5em; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + animation: fadeintop 0.3s both; +} + +.modal-header { + width: 100%; + margin-bottom: 1.5em; + display: flex; + align-items: center; + justify-content: space-between; + + & button.btn-close { + color: $blue; + background-color: transparent; + border: 0; + padding: 0; + margin: 0; + } +} + +.modal-body { + width: 100%; + display: flex; + flex: 1; + align-items: center; + flex-direction: column; } diff --git a/components/SearchModal/SearchModal.tsx b/components/SearchModal/SearchModal.tsx index 5f5e858..3d601d2 100644 --- a/components/SearchModal/SearchModal.tsx +++ b/components/SearchModal/SearchModal.tsx @@ -4,7 +4,6 @@ import { FcGoogle } from "react-icons/fc"; import useAutoFocus from "../../hooks/useAutoFocus"; -import FormLayout from "../FormLayout"; import LinkFavicon from "../Links/LinkFavicon"; import Modal from "../Modal/Modal"; import TextBox from "../TextBox"; @@ -13,6 +12,8 @@ import { Category, ItemComplete, Link } from "../../types"; import styles from "./search.module.scss"; +const GOOGLE_SEARCH_URL = "https://google.com/search?q="; + export default function SearchModal({ close, handleSelectCategory, @@ -31,33 +32,38 @@ export default function SearchModal({ const [search, setSearch] = useState(""); const canSubmit = useMemo(() => search.length > 0, [search]); - const itemsCompletion = useMemo(() => { - if (search.length === 0) { - return []; - } - return items.filter((item) => - item.name.toLocaleLowerCase().includes(search.toLocaleLowerCase().trim()) - ); - }, [items, search]); + const itemsCompletion = useMemo( + () => + search.length === 0 + ? [] + : items.filter((item) => + item.name + .toLocaleLowerCase() + .includes(search.toLocaleLowerCase().trim()) + ), + [items, search] + ); const handleSubmit = useCallback( (event) => { event.preventDefault(); setSearch(""); - if (itemsCompletion.length > 0) { - const firstItem = itemsCompletion[0]; - if (firstItem.type === "link") { - window.open(firstItem.url); - } else { - const category = categories.find((c) => c.id === firstItem.id); - console.log(category); - if (category) { - handleSelectCategory(category); - } - } - } else { - window.open(`https://google.com/search?q=${encodeURI(search.trim())}`); + + if (itemsCompletion.length === 0) { + window.open(GOOGLE_SEARCH_URL + encodeURI(search.trim())); + return close(); } + + // TODO: replace "firstItem" by a "cursor" + const firstItem = itemsCompletion[0]; + + const category = categories.find((c) => c.id === firstItem.id); + if (firstItem.type === "category" && category) { + handleSelectCategory(category); + return close(); + } + + window.open(firstItem.url); close(); }, [categories, close, handleSelectCategory, itemsCompletion, search] @@ -65,11 +71,7 @@ export default function SearchModal({ return ( - +
setSearch(value)} @@ -97,28 +99,33 @@ export default function SearchModal({ url: item.url, type: item.type, }))} - noItem={ - - Recherche avec{" "} - - - oogle - - - } + noItem={} /> )} - + +
); } +function LabelSearchWithGoogle() { + return ( + + Recherche avec{" "} + + + oogle + + + ); +} + +function LabelNoItem() { + return Aucun élément trouvé; +} + function ListItemComponent({ items, noItem, @@ -127,16 +134,7 @@ function ListItemComponent({ noItem?: ReactNode; }) { return ( -
    +
      {items.length > 0 ? ( items.map((item) => ( no item found + )}
    ); @@ -161,7 +159,7 @@ function ListItemComponent({ function ItemComponent({ item }: { item: ItemComplete }) { const { name, type, url } = item; return ( -
  • +
  • span { + display: flex; + align-items: "center"; + } +} + +.list-item { + margin: 1em 0; + display: flex; + gap: 1em; + align-items: center; + justify-content: center; + flex-wrap: wrap; +} + +.item { + & > a { + display: flex; + flex-direction: column; + } } diff --git a/components/SideMenu/SideMenu.tsx b/components/SideMenu/SideMenu.tsx index 7ffd8a8..5067b9e 100644 --- a/components/SideMenu/SideMenu.tsx +++ b/components/SideMenu/SideMenu.tsx @@ -50,15 +50,15 @@ function MenuControls({ }) { return (
    -
    +
    Rechercher S
    -
    +
    Créer categorie C
    -
    +
    Créer lien diff --git a/components/SideMenu/sidemenu.module.scss b/components/SideMenu/sidemenu.module.scss index 861e50a..dae04a6 100644 --- a/components/SideMenu/sidemenu.module.scss +++ b/components/SideMenu/sidemenu.module.scss @@ -2,7 +2,7 @@ .side-menu { height: 100%; - width: 300px; + width: 325px; padding: 0 25px 0 10px; border-right: 1px solid $lightest-grey; margin-right: 15px; @@ -15,7 +15,13 @@ .menu-controls { margin: 10px 0; display: flex; + gap: 0.25em; align-items: center; justify-content: center; flex-direction: column; + + & .action { + display: flex; + gap: 0.5em; + } } diff --git a/example.env b/example.env index bcd6322..14f2f1b 100644 --- a/example.env +++ b/example.env @@ -1,16 +1,17 @@ -DB_USER="my_user" -DB_PASSWORD="" -DB_DATABASE="my-links" +MYSQL_USER="my_user" +MYSQL_PASSWORD="" +MYSQL_DATABASE="my-links" +MYSQL_ROOT_PASSWORD="root" # Or if you need external Database # DATABASE_IP="localhost" # DATABASE_PORT="3306" # DATABASE_URL="mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@${DATABASE_IP}:${DATABASE_PORT}/${MYSQL_DATABASE}" -NEXTAUTH_URL=http://localhost:3000 -NEXTAUTH_URL_INTERNAL=http://localhost:3000 +NEXTAUTH_URL="http://localhost:3000" +NEXTAUTH_URL_INTERNAL="http://localhost:3000" -NEXTAUTH_SECRET= +NEXTAUTH_SECRET="" -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" diff --git a/pages/index.tsx b/pages/index.tsx index f169790..0630b38 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -61,7 +61,10 @@ function Home({ categories, favorites, currentCategory, items }: HomeProps) { }, [router]); useHotkeys(OPEN_SEARCH_KEY, openSearchModal, { enabled: !modal.isShowing }); - useHotkeys(CLOSE_SEARCH_KEY, closeSearchModal, { enabled: modal.isShowing }); + useHotkeys(CLOSE_SEARCH_KEY, closeSearchModal, { + enabled: modal.isShowing, + enableOnFormTags: ["INPUT"], + }); useHotkeys(OPEN_CREATE_LINK_KEY, gotoCreateLink, { enabled: !modal.isShowing, diff --git a/pages/search.tsx b/pages/search.tsx deleted file mode 100644 index fde3361..0000000 --- a/pages/search.tsx +++ /dev/null @@ -1,218 +0,0 @@ -import LinkTag from "next/link"; -import { useRouter } from "next/router"; -import { ReactNode, useCallback, useMemo, useState } from "react"; -import { FcGoogle } from "react-icons/fc"; - -import FormLayout from "../components/FormLayout"; -import TextBox from "../components/TextBox"; - -import useAutoFocus from "../hooks/useAutoFocus"; - -import LinkFavicon from "../components/Links/LinkFavicon"; - -import { Link } from "../types"; -import { prisma } from "../utils/back"; -import { BuildCategory } from "../utils/front"; - -import styles from "../styles/search.module.scss"; - -interface ItemComplete { - id: number; - name: string; - url: string; - type: "category" | "link"; -} - -interface SearchPageProps { - favorites: Link[]; - items: ItemComplete[]; -} - -export default function SearchPage({ favorites, items }: SearchPageProps) { - const router = useRouter(); - const autoFocusRef = useAutoFocus(); - - const [search, setSearch] = useState(""); - const canSubmit = useMemo(() => search.length > 0, [search]); - - const itemsCompletion = useMemo( - () => - search.length > 0 - ? items.filter((item) => - item.name - .toLocaleLowerCase() - .includes(search.toLocaleLowerCase().trim()) - ) - : [], - [items, search] - ); - console.log("itemsCompletion", itemsCompletion); - - const handleSubmit = useCallback( - (event) => { - event.preventDefault(); - setSearch(""); - if (itemsCompletion.length > 0) { - const firstItem = itemsCompletion[0]; - if (firstItem.type === "link") { - window.open(firstItem.url); - } else { - router.push(firstItem.url); - } - } else { - window.open(`https://google.com/search?q=${encodeURI(search.trim())}`); - } - }, - [itemsCompletion, router, search] - ); - - return ( - <> - - setSearch(value)} - value={search} - placeholder="Rechercher" - innerRef={autoFocusRef} - fieldClass={styles["search-input-field"]} - /> - {search.length === 0 && favorites.length > 0 && ( - ({ - id: favorite.id, - name: favorite.name, - url: favorite.url, - type: "link", - }))} - noItem={

    ajouter un favoris

    } - /> - )} - {search.length > 0 && ( - ({ - id: item.id, - name: item.name, - url: item.url, - type: item.type, - }))} - noItem={ - - Recherche avec{" "} - - - oogle - - - } - /> - )} -
    - - ); -} - -function ListItemComponent({ - items, - noItem, -}: { - items: ItemComplete[]; - noItem?: ReactNode; -}) { - return ( -
      - {items.length > 0 ? ( - items.map((item) => ( - - )) - ) : noItem ? ( - noItem - ) : ( - no item found - )} -
    - ); -} - -function ItemComponent({ item }: { item: ItemComplete }) { - const { name, type, url } = item; - return ( -
  • - - {type === "link" ? ( - - ) : ( - category - )} - {name} - -
  • - ); -} - -export async function getServerSideProps() { - const categoriesDB = await prisma.category.findMany({ - include: { links: true }, - }); - - const items = [] as ItemComplete[]; - - const favorites = [] as Link[]; - categoriesDB.forEach((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 { - props: { - favorites: JSON.parse(JSON.stringify(favorites)), - items: JSON.parse(JSON.stringify(items)), - }, - }; -} diff --git a/styles/keyframes.scss b/styles/keyframes.scss index 9ebeace..c95c563 100644 --- a/styles/keyframes.scss +++ b/styles/keyframes.scss @@ -10,6 +10,28 @@ } } +@keyframes fadeintop { + 0% { + transform: translateY(-15px); + opacity: 0; + } + + 100% { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes opacityin { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + @keyframes rotate { to { transform: rotate(0deg);