From 4855d28144b9f2a3d629938f4c903d1cf3f12049 Mon Sep 17 00:00:00 2001 From: Arik Chakma Date: Tue, 19 Mar 2024 22:51:13 +0600 Subject: [PATCH] fix: hydration errors --- .../GenerateRoadmap/AITermSuggestionInput.tsx | 112 +++++++++++++++--- .../GenerateRoadmap/GenerateRoadmap.tsx | 1 - .../GenerateRoadmap/RoadmapSearch.tsx | 20 ++-- 3 files changed, 104 insertions(+), 29 deletions(-) diff --git a/src/components/GenerateRoadmap/AITermSuggestionInput.tsx b/src/components/GenerateRoadmap/AITermSuggestionInput.tsx index 1f93b3da6..7b42f02d0 100644 --- a/src/components/GenerateRoadmap/AITermSuggestionInput.tsx +++ b/src/components/GenerateRoadmap/AITermSuggestionInput.tsx @@ -11,12 +11,14 @@ import { useDebounceValue } from '../../hooks/use-debounce'; import { httpGet } from '../../lib/http'; import { useToast } from '../../hooks/use-toast'; import { Loader2 } from 'lucide-react'; -import {Spinner} from "../ReactIcons/Spinner.tsx"; +import { Spinner } from '../ReactIcons/Spinner.tsx'; +import type { PageType } from '../CommandMenu/CommandMenu.tsx'; type GetTopAIRoadmapTermResponse = { _id: string; term: string; title: string; + isOfficial: boolean; }[]; type AITermSuggestionInputProps = { @@ -46,10 +48,13 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { () => new Map(), [], ); + const [officialRoadmaps, setOfficialRoadmaps] = + useState([]); const toast = useToast(); const searchInputRef = useRef(null); const dropdownRef = useRef(null); + const isFirstRender = useRef(true); const [isActive, setIsActive] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -60,19 +65,20 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { const debouncedSearchValue = useDebounceValue(searchedText, 500); const loadTopAIRoadmapTerm = async () => { - if (debouncedSearchValue.length === 0) { + const trimmedValue = debouncedSearchValue.trim(); + if (trimmedValue.length === 0) { return []; } - if (termCache.has(debouncedSearchValue)) { - const cachedData = termCache.get(debouncedSearchValue); + if (termCache.has(trimmedValue)) { + const cachedData = termCache.get(trimmedValue); return cachedData || []; } const { response, error } = await httpGet( `${import.meta.env.PUBLIC_API_URL}/v1-get-top-ai-roadmap-term`, { - term: debouncedSearchValue, + term: trimmedValue, }, ); @@ -82,12 +88,45 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { return []; } - termCache.set(debouncedSearchValue, response); + termCache.set(trimmedValue, response); return response; }; + const loadOfficialRoadmaps = async () => { + if (officialRoadmaps.length > 0) { + return officialRoadmaps; + } + + const { error, response } = await httpGet(`/pages.json`); + + if (error) { + toast.error(error.message || 'Something went wrong'); + return; + } + + if (!response) { + return []; + } + + const allRoadmaps = response + .filter((page) => page.group === 'Roadmaps') + .sort((a, b) => { + if (a.title === 'Android') return 1; + return a.title.localeCompare(b.title); + }) + .map((page) => ({ + _id: page.id, + term: page.title, + title: page.title, + isOfficial: true, + })); + + setOfficialRoadmaps(allRoadmaps); + return allRoadmaps; + }; + useEffect(() => { - if (debouncedSearchValue.length === 0) { + if (debouncedSearchValue.length === 0 || isFirstRender.current) { setSearchResults([]); return; } @@ -95,12 +134,26 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { setIsActive(true); setIsLoading(true); loadTopAIRoadmapTerm().then((results) => { - setSearchResults(results?.slice(0, 5) || []); + const normalizedSearchText = debouncedSearchValue.trim().toLowerCase(); + const matchingOfficialRoadmaps = officialRoadmaps.filter((roadmap) => { + return roadmap.title.toLowerCase().indexOf(normalizedSearchText) !== -1; + }); + + setSearchResults( + [...matchingOfficialRoadmaps, ...results]?.slice(0, 5) || [], + ); setActiveCounter(0); setIsLoading(false); }); }, [debouncedSearchValue]); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + loadOfficialRoadmaps().finally(() => {}); + } + }, []); + useOutsideClick(dropdownRef, () => { setIsActive(false); }); @@ -118,8 +171,8 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { )} placeholder={placeholder} autoComplete="off" - onInput={(e) => { - const value = (e.target as HTMLInputElement).value.trim(); + onChange={(e) => { + const value = (e.target as HTMLInputElement).value; setSearchedText(value); onValueChange(value); }} @@ -143,12 +196,19 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { setSearchedText(''); setIsActive(false); } else if (e.key === 'Enter') { - e.preventDefault(); - const activeData = searchResults[activeCounter]; - if (activeData) { - onValueChange(activeData.term); - onSelect?.(activeData._id); - setIsActive(false); + if (searchResults.length > 0) { + e.preventDefault(); + const activeData = searchResults[activeCounter]; + if (activeData) { + if (activeData.isOfficial) { + window.location.href = `/${activeData._id}`; + return; + } + + onValueChange(activeData.term); + onSelect?.(activeData._id); + setIsActive(false); + } } } }} @@ -156,7 +216,10 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { {isLoading && (
- +
)} @@ -177,12 +240,27 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) { )} onMouseOver={() => setActiveCounter(counter)} onClick={() => { + if (result.isOfficial) { + window.location.href = `/${result._id}`; + return; + } + onValueChange(result?.term); onSelect?.(result._id); setSearchedText(''); setIsActive(false); }} > + + {result.isOfficial ? 'Official' : 'Generated'} + {result?.title || result?.term} ); diff --git a/src/components/GenerateRoadmap/GenerateRoadmap.tsx b/src/components/GenerateRoadmap/GenerateRoadmap.tsx index 26695106a..c69db6291 100644 --- a/src/components/GenerateRoadmap/GenerateRoadmap.tsx +++ b/src/components/GenerateRoadmap/GenerateRoadmap.tsx @@ -519,7 +519,6 @@ export function GenerateRoadmap() { className="my-3 flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:justify-center" > setRoadmapTerm(value)} placeholder="e.g. Try searching for Ansible or DevOps" diff --git a/src/components/GenerateRoadmap/RoadmapSearch.tsx b/src/components/GenerateRoadmap/RoadmapSearch.tsx index 0b16f339e..eaaceee79 100644 --- a/src/components/GenerateRoadmap/RoadmapSearch.tsx +++ b/src/components/GenerateRoadmap/RoadmapSearch.tsx @@ -1,16 +1,9 @@ -import { - ArrowUpRight, - Ban, - CircleFadingPlus, - Cog, - Telescope, - Wand, -} from 'lucide-react'; +import { ArrowUpRight, Ban, Cog, Telescope, Wand } from 'lucide-react'; import type { FormEvent } from 'react'; import { getOpenAIKey, isLoggedIn } from '../../lib/jwt'; import { showLoginPopup } from '../../lib/popup'; import { cn } from '../../lib/classname.ts'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { OpenAISettings } from './OpenAISettings.tsx'; import { AITermSuggestionInput } from './AITermSuggestionInput.tsx'; import { setUrlParams } from '../../lib/browser.ts'; @@ -40,8 +33,13 @@ export function RoadmapSearch(props: RoadmapSearchProps) { const canGenerateMore = limitUsed < limit; const [isConfiguring, setIsConfiguring] = useState(false); - const openAPIKey = getOpenAIKey(); - const isAuthenticatedUser = isLoggedIn(); + const [openAPIKey, setOpenAPIKey] = useState(''); + const [isAuthenticatedUser, setIsAuthenticatedUser] = useState(false); + + useEffect(() => { + setOpenAPIKey(getOpenAIKey() || ''); + setIsAuthenticatedUser(isLoggedIn()); + }, [getOpenAIKey(), isLoggedIn()]); const randomTerms = ['OAuth', 'APIs', 'UX Design', 'gRPC'];