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