mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-11 00:33:04 +00:00
feat: add search item cursor
This commit is contained in:
@@ -33,14 +33,14 @@ export default function Modal({
|
||||
onClick={handleWrapperClick}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, transition: { duration: 0.1 } }}
|
||||
exit={{ opacity: 0, transition: { duration: 0.1, delay: 0.1 } }}
|
||||
>
|
||||
<motion.div
|
||||
className={styles["modal-container"]}
|
||||
style={{ padding }}
|
||||
initial={{ opacity: 0, y: -15 }}
|
||||
initial={{ opacity: 0, y: "-6em" }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -15, transition: { duration: 0.1 } }}
|
||||
exit={{ opacity: 0, y: "-6em", transition: { duration: 0.1 } }}
|
||||
>
|
||||
{!noHeader && (
|
||||
<div className={styles["modal-header"]}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ReactNode } from "react";
|
||||
import { ReactNode, useMemo } from "react";
|
||||
|
||||
import { SearchItem } from "types";
|
||||
import SearchListItem from "./SearchListItem";
|
||||
@@ -8,14 +8,18 @@ import styles from "./search.module.scss";
|
||||
export default function SearchList({
|
||||
items,
|
||||
noItem,
|
||||
cursor,
|
||||
setCursor,
|
||||
}: {
|
||||
items: SearchItem[];
|
||||
noItem?: ReactNode;
|
||||
cursor: number;
|
||||
setCursor: (cursor: number) => void;
|
||||
}) {
|
||||
return (
|
||||
<ul className={styles["search-list"]}>
|
||||
{items.length > 0 ? (
|
||||
items.map((item) => (
|
||||
items.map((item, index) => (
|
||||
<SearchListItem
|
||||
item={{
|
||||
id: item.id,
|
||||
@@ -23,6 +27,9 @@ export default function SearchList({
|
||||
url: item.url,
|
||||
type: item.type,
|
||||
}}
|
||||
setCursor={setCursor}
|
||||
selected={index === cursor}
|
||||
index={index}
|
||||
key={item.type + "-" + item.id}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -1,23 +1,66 @@
|
||||
import { motion } from "framer-motion";
|
||||
import LinkTag from "next/link";
|
||||
import { AiOutlineFolder } from "react-icons/ai";
|
||||
|
||||
import LinkFavicon from "components/Links/LinkFavicon";
|
||||
import { SearchItem } from "types";
|
||||
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import styles from "./search.module.scss";
|
||||
|
||||
export default function SearchListItem({ item }: { item: SearchItem }) {
|
||||
export default function SearchListItem({
|
||||
item,
|
||||
index,
|
||||
selected,
|
||||
setCursor,
|
||||
}: {
|
||||
item: SearchItem;
|
||||
index?: number;
|
||||
selected: boolean;
|
||||
setCursor: (cursor: number) => void;
|
||||
}) {
|
||||
const id = useId();
|
||||
const ref = useRef<HTMLLIElement>(null);
|
||||
|
||||
const [isHover, setHover] = useState<boolean>(false);
|
||||
|
||||
const { name, type, url } = item;
|
||||
|
||||
useEffect(() => {
|
||||
if (selected && !isHover) {
|
||||
console.log(selected, ref.current);
|
||||
ref.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
}, [isHover, selected]);
|
||||
|
||||
return (
|
||||
<li className={styles["search-item"]}>
|
||||
<motion.li
|
||||
className={styles["search-item"]}
|
||||
initial={{ y: -15, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 260,
|
||||
damping: 20,
|
||||
delay: index * 0.025,
|
||||
}}
|
||||
ref={ref}
|
||||
onMouseEnter={() => {
|
||||
setCursor(index);
|
||||
setHover(true);
|
||||
}}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
key={id}
|
||||
>
|
||||
<LinkTag href={url} target="_blank" rel="no-referrer">
|
||||
{type === "link" ? (
|
||||
<LinkFavicon url={item.url} noMargin />
|
||||
<LinkFavicon url={item.url} noMargin size={24} />
|
||||
) : (
|
||||
<AiOutlineFolder size={32} />
|
||||
<AiOutlineFolder size={24} />
|
||||
)}
|
||||
<span>{name}</span>
|
||||
{selected && "selected"}
|
||||
</LinkTag>
|
||||
</li>
|
||||
</motion.li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import SearchList from "./SearchList";
|
||||
|
||||
import * as Keys from "constants/keys";
|
||||
import { GOOGLE_SEARCH_URL } from "constants/search-urls";
|
||||
import { Category, Link, SearchItem } from "types";
|
||||
import { Category, SearchItem } from "types";
|
||||
|
||||
import styles from "./search.module.scss";
|
||||
|
||||
@@ -19,13 +19,11 @@ export default function SearchModal({
|
||||
close,
|
||||
handleSelectCategory,
|
||||
categories,
|
||||
favorites,
|
||||
items,
|
||||
}: {
|
||||
close: any;
|
||||
handleSelectCategory: (category: Category) => void;
|
||||
categories: Category[];
|
||||
favorites: Link[];
|
||||
items: SearchItem[];
|
||||
}) {
|
||||
const autoFocusRef = useAutoFocus();
|
||||
@@ -51,12 +49,16 @@ export default function SearchModal({
|
||||
[items, search]
|
||||
);
|
||||
|
||||
useHotkeys(Keys.ARROW_LEFT, () => {
|
||||
console.log("left");
|
||||
useHotkeys(Keys.ARROW_UP, () => setCursor((cursor) => (cursor -= 1)), {
|
||||
enableOnFormTags: ["INPUT"],
|
||||
enabled: itemsCompletion.length > 1 && cursor !== 0,
|
||||
preventDefault: true,
|
||||
});
|
||||
|
||||
useHotkeys(Keys.ARROW_RIGHT, () => {
|
||||
console.log("right");
|
||||
useHotkeys(Keys.ARROW_DOWN, () => setCursor((cursor) => (cursor += 1)), {
|
||||
enableOnFormTags: ["INPUT"],
|
||||
enabled:
|
||||
itemsCompletion.length > 1 && cursor !== itemsCompletion.length - 1,
|
||||
preventDefault: true,
|
||||
});
|
||||
|
||||
const handleSearchInputChange = useCallback((value) => {
|
||||
@@ -74,19 +76,17 @@ export default function SearchModal({
|
||||
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) {
|
||||
const selectedItem = itemsCompletion[cursor];
|
||||
const category = categories.find((c) => c.id === selectedItem.id);
|
||||
if (selectedItem.type === "category" && category) {
|
||||
handleSelectCategory(category);
|
||||
return close();
|
||||
}
|
||||
|
||||
window.open(firstItem.url);
|
||||
window.open(selectedItem.url);
|
||||
close();
|
||||
},
|
||||
[categories, close, handleSelectCategory, itemsCompletion, search]
|
||||
[categories, close, cursor, handleSelectCategory, itemsCompletion, search]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -121,6 +121,8 @@ export default function SearchModal({
|
||||
type: item.type,
|
||||
}))}
|
||||
noItem={<LabelSearchWithGoogle />}
|
||||
cursor={cursor}
|
||||
setCursor={setCursor}
|
||||
/>
|
||||
)}
|
||||
<button type="submit" disabled={!canSubmit} style={{ display: "none" }}>
|
||||
|
||||
@@ -48,6 +48,7 @@ function Home(props: HomeProps) {
|
||||
name: item.name,
|
||||
url: type === "link" ? (item as Link).url : `/?categoryId=${item.id}`,
|
||||
type,
|
||||
category: type === "link" ? (item as Link).category : undefined,
|
||||
});
|
||||
|
||||
const itemsSearch = useMemo<SearchItem[]>(() => {
|
||||
@@ -179,7 +180,6 @@ function Home(props: HomeProps) {
|
||||
<SearchModal
|
||||
close={modal.close}
|
||||
categories={categories}
|
||||
favorites={favorites}
|
||||
items={itemsSearch}
|
||||
handleSelectCategory={handleSelectCategory}
|
||||
/>
|
||||
|
||||
1
src/types.d.ts
vendored
1
src/types.d.ts
vendored
@@ -32,4 +32,5 @@ export interface SearchItem {
|
||||
name: string;
|
||||
url: string;
|
||||
type: "category" | "link";
|
||||
category?: undefined | Link["category"];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user