From c8c0c86c967660b609a9d72825c539ec6279d67a Mon Sep 17 00:00:00 2001 From: Aslam Date: Thu, 5 Sep 2024 04:17:12 +0700 Subject: [PATCH] feat(landing): topic search (#134) * feat(landing): topic search * fix: node click --- web/app/(pages)/layout.tsx | 2 +- web/components/routes/PublicHomeRoute.tsx | 70 ---------- web/components/routes/public/Autocomplete.tsx | 123 ++++++++++++++++++ .../routes/public/PublicHomeRoute.tsx | 65 +++++++++ web/components/routes/{ => public}/anim.ts | 0 .../{ => public}/force-graph-client-lazy.tsx | 0 .../routes/{ => public}/graph-data.json | 0 web/package.json | 10 +- 8 files changed, 194 insertions(+), 76 deletions(-) delete mode 100644 web/components/routes/PublicHomeRoute.tsx create mode 100644 web/components/routes/public/Autocomplete.tsx create mode 100644 web/components/routes/public/PublicHomeRoute.tsx rename web/components/routes/{ => public}/anim.ts (100%) rename web/components/routes/{ => public}/force-graph-client-lazy.tsx (100%) rename web/components/routes/{ => public}/graph-data.json (100%) diff --git a/web/app/(pages)/layout.tsx b/web/app/(pages)/layout.tsx index fb32036d..8cadcceb 100644 --- a/web/app/(pages)/layout.tsx +++ b/web/app/(pages)/layout.tsx @@ -1,6 +1,6 @@ import { SignedInClient } from "@/components/custom/clerk/signed-in-client" import { Sidebar } from "@/components/custom/sidebar/sidebar" -import { PublicHomeRoute } from "@/components/routes/PublicHomeRoute" +import { PublicHomeRoute } from "@/components/routes/public/PublicHomeRoute" import { CommandPalette } from "@/components/ui/CommandPalette" import { JazzClerkAuth, JazzProvider } from "@/lib/providers/jazz-provider" import { currentUser } from "@clerk/nextjs/server" diff --git a/web/components/routes/PublicHomeRoute.tsx b/web/components/routes/PublicHomeRoute.tsx deleted file mode 100644 index 42739692..00000000 --- a/web/components/routes/PublicHomeRoute.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client" -import * as react from "react" -import { useCoState } from "@/lib/providers/jazz-provider" -import { PublicGlobalGroup } from "@/lib/schema/master/public-group" -import dynamic from "next/dynamic" -import Link from "next/link" -import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants" - -let graph_data_promise = import("./graph-data.json").then(a => a.default) -const ForceGraphClient = dynamic(() => import("./force-graph-client-lazy"), { ssr: false }) - -export function PublicHomeRoute() { - let raw_graph_data = react.use(graph_data_promise) - - const [placeholder, setPlaceholder] = react.useState("Search something...") - const [currentTopicIndex, setCurrentTopicIndex] = react.useState(0) - const [currentCharIndex, setCurrentCharIndex] = react.useState(0) - const globalGroup = useCoState(PublicGlobalGroup, JAZZ_GLOBAL_GROUP_ID, { - root: { - topics: [] - } - }) - const topics = globalGroup?.root.topics?.map(topic => topic?.prettyName) || [] - - react.useEffect(() => { - if (topics.length === 0) return - - const typingInterval = setInterval(() => { - const currentTopic = topics[currentTopicIndex] - if (currentTopic && currentCharIndex < currentTopic.length) { - setPlaceholder(`${currentTopic.slice(0, currentCharIndex + 1)}`) - setCurrentCharIndex(currentCharIndex + 1) - } else { - clearInterval(typingInterval) - setTimeout(() => { - setCurrentTopicIndex(prevIndex => (prevIndex + 1) % topics.length) - setCurrentCharIndex(0) - }, 1000) - } - }, 200) - - return () => clearInterval(typingInterval) - }, [currentTopicIndex, currentCharIndex, topics]) - - return ( -
- { - console.log("clicked", val) - }} - filter_query="" - /> -
-

Learn Anything

- Random Topic -
-
-
-

i want to learn

- -
-
-
- ) -} diff --git a/web/components/routes/public/Autocomplete.tsx b/web/components/routes/public/Autocomplete.tsx new file mode 100644 index 00000000..8b00b037 --- /dev/null +++ b/web/components/routes/public/Autocomplete.tsx @@ -0,0 +1,123 @@ +"use client" + +import React, { useState, useRef, useCallback, useMemo } from "react" +import { Command, CommandGroup, CommandItem, CommandList } from "@/components/ui/command" +import { Command as CommandPrimitive } from "cmdk" +import { motion, AnimatePresence } from "framer-motion" +import { cn } from "@/lib/utils" + +interface GraphNode { + name: string + prettyName: string + connectedTopics: string[] +} + +interface AutocompleteProps { + topics: GraphNode[] + onSelect: (topic: GraphNode) => void + onInputChange: (value: string) => void +} + +export function Autocomplete({ topics = [], onSelect, onInputChange }: AutocompleteProps): JSX.Element { + const inputRef = useRef(null) + const [open, setOpen] = useState(false) + const [inputValue, setInputValue] = useState("") + + const filteredTopics = useMemo(() => { + if (!inputValue) { + return topics.slice(0, 5) + } + const regex = new RegExp(inputValue.split("").join(".*"), "i") + return topics.filter( + topic => + regex.test(topic.name) || + regex.test(topic.prettyName) || + topic.connectedTopics.some(connectedTopic => regex.test(connectedTopic)) + ) + }, [inputValue, topics]) + + const handleSelect = useCallback( + (topic: GraphNode) => { + setInputValue(topic.prettyName) + setOpen(false) + onSelect(topic) + }, + [onSelect] + ) + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Enter" && filteredTopics.length > 0) { + handleSelect(filteredTopics[0]) + } else if ((e.key === "Backspace" || e.key === "Delete") && inputRef.current?.value === "") { + setOpen(true) + } + }, + [filteredTopics, handleSelect] + ) + + const handleInputChange = useCallback( + (value: string) => { + setInputValue(value) + setOpen(true) + onInputChange(value) + }, + [onInputChange] + ) + + return ( + +
+ setTimeout(() => setOpen(false), 100)} + onFocus={() => setOpen(true)} + placeholder="Search for a topic..." + className={cn("placeholder:text-muted-foreground flex-1 bg-transparent px-2 py-1 outline-none", { + "mb-1 border-b pb-2.5": open + })} + /> +
+
+ + {open && ( + + + + {filteredTopics.map(topic => ( + handleSelect(topic)} + className="min-h-10 rounded-none px-3 py-1.5" + > + {topic.prettyName} + + {topic.connectedTopics.length > 0 ? topic.connectedTopics.join(", ") : "-"} + + + ))} + + + + )} + +
+
+ ) +} + +export default Autocomplete diff --git a/web/components/routes/public/PublicHomeRoute.tsx b/web/components/routes/public/PublicHomeRoute.tsx new file mode 100644 index 00000000..f6668b7a --- /dev/null +++ b/web/components/routes/public/PublicHomeRoute.tsx @@ -0,0 +1,65 @@ +"use client" + +import * as React from "react" +import dynamic from "next/dynamic" +import { motion } from "framer-motion" +import { Autocomplete } from "./Autocomplete" +import { useRouter } from "next/navigation" + +let graph_data_promise = import("./graph-data.json").then(a => a.default) + +const ForceGraphClient = dynamic(() => import("./force-graph-client-lazy"), { ssr: false }) + +interface GraphNode { + name: string + prettyName: string + connectedTopics: string[] +} + +export function PublicHomeRoute() { + const router = useRouter() + const raw_graph_data = React.use(graph_data_promise) as GraphNode[] + const [filterQuery, setFilterQuery] = React.useState("") + + const handleTopicSelect = (topicName: string) => { + router.push(`/${topicName}`) + } + + const handleInputChange = (value: string) => { + setFilterQuery(value) + } + + return ( +
+ handleTopicSelect(val)} + filter_query={filterQuery} + /> + + + + I want to learn + + handleTopicSelect(topic.name)} + onInputChange={handleInputChange} + /> + +
+ ) +} + +export default PublicHomeRoute diff --git a/web/components/routes/anim.ts b/web/components/routes/public/anim.ts similarity index 100% rename from web/components/routes/anim.ts rename to web/components/routes/public/anim.ts diff --git a/web/components/routes/force-graph-client-lazy.tsx b/web/components/routes/public/force-graph-client-lazy.tsx similarity index 100% rename from web/components/routes/force-graph-client-lazy.tsx rename to web/components/routes/public/force-graph-client-lazy.tsx diff --git a/web/components/routes/graph-data.json b/web/components/routes/public/graph-data.json similarity index 100% rename from web/components/routes/graph-data.json rename to web/components/routes/public/graph-data.json diff --git a/web/package.json b/web/package.json index db76c0bb..9ddd9b66 100644 --- a/web/package.json +++ b/web/package.json @@ -9,7 +9,7 @@ "test": "jest" }, "dependencies": { - "@clerk/nextjs": "^5.4.0", + "@clerk/nextjs": "^5.4.1", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^8.0.0", "@hookform/resolvers": "^3.9.0", @@ -32,7 +32,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", - "@tanstack/react-virtual": "^3.10.6", + "@tanstack/react-virtual": "^3.10.7", "@tiptap/core": "^2.6.6", "@tiptap/extension-blockquote": "^2.6.6", "@tiptap/extension-bold": "^2.6.6", @@ -67,7 +67,7 @@ "clsx": "^2.1.1", "cmdk": "^1.0.0", "date-fns": "^3.6.0", - "framer-motion": "^11.4.0", + "framer-motion": "^11.5.2", "jazz-react": "0.7.35-unique.2", "jazz-react-auth-clerk": "0.7.33-new-auth.1", "jazz-tools": "0.7.35-unique.2", @@ -96,14 +96,14 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@types/jest": "^29.5.12", - "@types/node": "^22.5.2", + "@types/node": "^22.5.3", "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "eslint": "^8.57.0", "eslint-config-next": "14.2.5", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "postcss": "^8.4.44", + "postcss": "^8.4.45", "prettier-plugin-tailwindcss": "^0.6.6", "tailwindcss": "^3.4.10", "ts-jest": "^29.2.5",