diff --git a/components/Modal.tsx b/components/Modal.tsx index 2878928..76f02e6 100644 --- a/components/Modal.tsx +++ b/components/Modal.tsx @@ -6,13 +6,14 @@ import {useRouter} from "next/router"; import {Update} from "../utils/types"; import {useTheme} from "next-themes"; -export default function Modal({isOpen, setIsOpen, children, wide = false}: { +export default function Modal({isOpen, setIsOpen, children, wide = false, className}: { isOpen: boolean, setIsOpen: Dispatch>, children: ReactNode, wide?: boolean, + className?: string, }) { - const modalClasses = "top-24 left-1/2 fixed bg-white dark:bg-gray-900 p-4 rounded-md shadow-xl mx-4 overflow-y-auto"; + const modalClasses = "top-24 left-1/2 fixed bg-white dark:bg-gray-900 p-4 rounded-md shadow-xl mx-4 overflow-y-auto " + className; const {theme} = useTheme(); return ( (any) }) => { + const [query, setQuery] = useState(""); + const [page, setPage] = useState(0); + const [selectedIndex, setSelectedIndex] = useState(0); + const { data } = useSWR<{ data: DatedObj[], count: number }>(`/api/search?query=${query}&page=${page}`, query.length ? fetcher : async () => []); + + const onRequestClose = (x) => { + props.onRequestClose(); + setQuery(""); + setPage(0); + setSelectedIndex(0); + } + + // Height of area of modal that is not scroll + const heightOfInput = 32 + 24 + // = height + margin-top (mt-6 => 6 * 4) + + return ( + + {/* Because I want scrollbar to be snug against border of modal, i can't add padding x or y to the modal directly. */} + {/* Every direct child of modal has px-4 */} + {/* Also modal has py-6 so top should have mt-6 and bottom mb-6 */} +
+ + { + setQuery(e.target.value); + setPage(0); + setSelectedIndex(0); + }} + id="quick-switcher-input" + placeholder="Go to update" + className="w-full focus:online-none outline-none py-1 text-gray-500" + autoFocus + onKeyDown={e => { + if (data && data.data && data.data.length) { + if (e.key === "ArrowDown") { + e.preventDefault() + const newSelectedIndex = selectedIndex === (data.data.length - 1) ? 0 : selectedIndex + 1 + setSelectedIndex(newSelectedIndex) + + // Scroll to selected element + const modal = document.getElementById("quick-switcher-scroll-area") + if (newSelectedIndex !== (data.data.length - 1)) { + // Scroll such that the lower edge of the element we want is at the bottom of the modal viewing area + var elmntAfter = document.getElementById(`searched-doc-${newSelectedIndex + 1}`); + modal.scroll(0, elmntAfter.offsetTop - modal.offsetHeight - heightOfInput) + } else { + // Is last element + var elmnt = document.getElementById(`searched-doc-${newSelectedIndex}`); + modal.scroll(0, elmnt.offsetTop - heightOfInput) + } + } else if (e.key === "ArrowUp") { + e.preventDefault() + const newSelectedIndex = selectedIndex === 0 ? (data.data.length - 1) : (selectedIndex - 1) + setSelectedIndex(newSelectedIndex) + + // Scroll to selected element + var elmnt = document.getElementById(`searched-doc-${newSelectedIndex}`); + const modal = document.getElementById("quick-switcher-scroll-area") + modal.scroll(0, (elmnt.offsetTop - heightOfInput)) + } else if (e.key === "Enter") { + waitForEl(`searched-doc-${selectedIndex}`) + } + } + }} + /> +
+
+
+ { /* Every outermost element inside this div has px-8 */} + {(data) ? (data.data && data.data.length) ? ( +
+ {data.data.map((u, idx) => { + + let buttonChildren = ( + <> + {/* @ts-ignore */} +
+

{format(new Date(u.date), "M/d/yy")} • {u.user.name}

+ {`${u.title || "Unknown update"}`} +
+ + + ) + let onClick = () => { + onRequestClose(false) + } + + return ( + setSelectedIndex(idx)} + href={"/@" + u.user.urlName + "/" + u.url} + > +
+ {buttonChildren} +
+ + ) + })} + {/* Pagination bar */} +
+ {data.count > 10 && Array.from(Array(Math.ceil(data.count / 10)).keys()).map(n => + + )} +
+

+ Showing results {page * 10 + 1}-{(page * 10 + 10) < data.count ? (page * 10 + 10) : data.count} out of {data.count} +

+
+ ) : (query.length ? ( +

No documents containing the given query were found.

+ ) : <>) : ( +
+

Loading...

+ +
+ )} +
+
+ ) +} + +const includesAQueryWord = (string: string, queryWords: string[]) => { + for (let word of queryWords) { + if (string.toLowerCase().includes(word.toLowerCase())) return true + } + return false +} + +const SearchNameH3 = ({ children, query }: { children: string, query: string }) => { + const queryWords = query.split(" ") + const nameWords = children.split(" ") + const newNameWords = nameWords.map(word => ( + includesAQueryWord(word, queryWords) + ? {word} + : {word} + )) + return ( +

{newNameWords.map((element, idx) => ( + idx === 0 + ? {element} + : {element} + ))}

+ ) +} + +const SearchBody = ({ update, query }: {update: Update, query: string}) => { + if (!update.body) return; + // s.body.substr(s.body.indexOf(query) - 50, 100) + const queryWords = query.split(" ") + const paragraphsArr = update.body.split(` +`) + const newParagraphs = paragraphsArr.filter(p => ( + includesAQueryWord(p, queryWords) + )).map(p => { + // Some really jank shit for bolding certain words + const paragraphWords = p.split(" ") + const newParagraphWords = paragraphWords.map(w => includesAQueryWord(w, queryWords) ? {w} : {w}) + // return newParagraphWords.join(" ") + return newParagraphWords + }) + return ( + // newParagraphs.map( (p, idx) =>
+        //     {p}
+        // 
) +
+ {newParagraphs.map((p, idx) =>

{ + p.map((f, id) => {f} ) + }

)} +
+ ) +} + + +export default QuickSwitcher \ No newline at end of file diff --git a/components/navbar.tsx b/components/navbar.tsx index ec7970d..137bd52 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -14,6 +14,11 @@ import {IoMdExit} from "react-icons/io"; import SignInButton from "./SignInButton"; import FloatingCta from "./FloatingCTA"; import NavbarNotificationMenu from "./NavbarNotificationMenu"; +import {useKey} from "../utils/hooks"; +import Mousetrap from "mousetrap"; +import 'mousetrap/plugins/global-bind/mousetrap-global-bind'; +import QuickSwitcher from "./QuickSwitcher"; + export default function Navbar() { const router = useRouter(); @@ -23,6 +28,7 @@ export default function Navbar() { const { data: notificationData, error: notificationsError }: responseInterface<{ notifications: RichNotif[] }, any> = useSWR(session ? `/api/get-notifications?iter=${notificationsIter}` : null, fetcher); const [ notifications, setNotifications ] = useState([]); const numNotifications = notifications.filter(d => !d.read).length + const [isQuickSwitcher, setIsQuickSwitcher] = useState(false); useEffect(() => { if (notificationData && notificationData.notifications) { @@ -36,6 +42,26 @@ export default function Navbar() { const {theme, setTheme} = useTheme(); + useKey("KeyF", () => {if (router.route !== "/") router.push("/")}) + useKey("KeyE", () => {if (router.route !== "/explore") router.push("/explore")}) + useKey("KeyP", () => {if (router.route !== "/profile" && session) router.push("/@" + data.data.urlName)}) + useKey("KeyN", () => {if (router.route !== "/new-update" && session) router.push("/new-update")}) + + useEffect(() => { + + function onQuickSwitcherShortcut(e) { + e.preventDefault(); + setIsQuickSwitcher(prev => !prev); + } + + Mousetrap.bindGlobal(['command+k', 'ctrl+k'], onQuickSwitcherShortcut); + + return () => { + Mousetrap.unbind(['command+k', 'ctrl+k'], onQuickSwitcherShortcut); + } + }); + + const NavbarDarkModeButton = () => (