fix: hydration errors

feat/ai-autocomplete
Arik Chakma 8 months ago
parent 270113f888
commit 4855d28144
  1. 112
      src/components/GenerateRoadmap/AITermSuggestionInput.tsx
  2. 1
      src/components/GenerateRoadmap/GenerateRoadmap.tsx
  3. 20
      src/components/GenerateRoadmap/RoadmapSearch.tsx

@ -11,12 +11,14 @@ import { useDebounceValue } from '../../hooks/use-debounce';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { Loader2 } from 'lucide-react'; 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 = { type GetTopAIRoadmapTermResponse = {
_id: string; _id: string;
term: string; term: string;
title: string; title: string;
isOfficial: boolean;
}[]; }[];
type AITermSuggestionInputProps = { type AITermSuggestionInputProps = {
@ -46,10 +48,13 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
() => new Map<string, GetTopAIRoadmapTermResponse>(), () => new Map<string, GetTopAIRoadmapTermResponse>(),
[], [],
); );
const [officialRoadmaps, setOfficialRoadmaps] =
useState<GetTopAIRoadmapTermResponse>([]);
const toast = useToast(); const toast = useToast();
const searchInputRef = useRef<HTMLInputElement>(null); const searchInputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const isFirstRender = useRef(true);
const [isActive, setIsActive] = useState(false); const [isActive, setIsActive] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@ -60,19 +65,20 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
const debouncedSearchValue = useDebounceValue(searchedText, 500); const debouncedSearchValue = useDebounceValue(searchedText, 500);
const loadTopAIRoadmapTerm = async () => { const loadTopAIRoadmapTerm = async () => {
if (debouncedSearchValue.length === 0) { const trimmedValue = debouncedSearchValue.trim();
if (trimmedValue.length === 0) {
return []; return [];
} }
if (termCache.has(debouncedSearchValue)) { if (termCache.has(trimmedValue)) {
const cachedData = termCache.get(debouncedSearchValue); const cachedData = termCache.get(trimmedValue);
return cachedData || []; return cachedData || [];
} }
const { response, error } = await httpGet<GetTopAIRoadmapTermResponse>( const { response, error } = await httpGet<GetTopAIRoadmapTermResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-top-ai-roadmap-term`, `${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 []; return [];
} }
termCache.set(debouncedSearchValue, response); termCache.set(trimmedValue, response);
return response; return response;
}; };
const loadOfficialRoadmaps = async () => {
if (officialRoadmaps.length > 0) {
return officialRoadmaps;
}
const { error, response } = await httpGet<PageType[]>(`/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(() => { useEffect(() => {
if (debouncedSearchValue.length === 0) { if (debouncedSearchValue.length === 0 || isFirstRender.current) {
setSearchResults([]); setSearchResults([]);
return; return;
} }
@ -95,12 +134,26 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
setIsActive(true); setIsActive(true);
setIsLoading(true); setIsLoading(true);
loadTopAIRoadmapTerm().then((results) => { 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); setActiveCounter(0);
setIsLoading(false); setIsLoading(false);
}); });
}, [debouncedSearchValue]); }, [debouncedSearchValue]);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
loadOfficialRoadmaps().finally(() => {});
}
}, []);
useOutsideClick(dropdownRef, () => { useOutsideClick(dropdownRef, () => {
setIsActive(false); setIsActive(false);
}); });
@ -118,8 +171,8 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
)} )}
placeholder={placeholder} placeholder={placeholder}
autoComplete="off" autoComplete="off"
onInput={(e) => { onChange={(e) => {
const value = (e.target as HTMLInputElement).value.trim(); const value = (e.target as HTMLInputElement).value;
setSearchedText(value); setSearchedText(value);
onValueChange(value); onValueChange(value);
}} }}
@ -143,12 +196,19 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
setSearchedText(''); setSearchedText('');
setIsActive(false); setIsActive(false);
} else if (e.key === 'Enter') { } else if (e.key === 'Enter') {
e.preventDefault(); if (searchResults.length > 0) {
const activeData = searchResults[activeCounter]; e.preventDefault();
if (activeData) { const activeData = searchResults[activeCounter];
onValueChange(activeData.term); if (activeData) {
onSelect?.(activeData._id); if (activeData.isOfficial) {
setIsActive(false); window.location.href = `/${activeData._id}`;
return;
}
onValueChange(activeData.term);
onSelect?.(activeData._id);
setIsActive(false);
}
} }
} }
}} }}
@ -156,7 +216,10 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
{isLoading && ( {isLoading && (
<div className="absolute right-3 top-0 flex h-full items-center"> <div className="absolute right-3 top-0 flex h-full items-center">
<Spinner isDualRing={false} className="h-5 w-5 animate-spin stroke-[2.5]" /> <Spinner
isDualRing={false}
className="h-5 w-5 animate-spin stroke-[2.5]"
/>
</div> </div>
)} )}
@ -177,12 +240,27 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
)} )}
onMouseOver={() => setActiveCounter(counter)} onMouseOver={() => setActiveCounter(counter)}
onClick={() => { onClick={() => {
if (result.isOfficial) {
window.location.href = `/${result._id}`;
return;
}
onValueChange(result?.term); onValueChange(result?.term);
onSelect?.(result._id); onSelect?.(result._id);
setSearchedText(''); setSearchedText('');
setIsActive(false); setIsActive(false);
}} }}
> >
<span
className={cn(
'mr-2 rounded-full p-1 px-1.5 text-xs leading-none',
result.isOfficial
? 'bg-green-500 text-green-50'
: 'bg-blue-400 text-blue-50',
)}
>
{result.isOfficial ? 'Official' : 'Generated'}
</span>
{result?.title || result?.term} {result?.title || result?.term}
</button> </button>
); );

@ -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" className="my-3 flex w-full flex-col gap-2 sm:flex-row sm:items-center sm:justify-center"
> >
<AITermSuggestionInput <AITermSuggestionInput
autoFocus={true}
value={roadmapTerm} value={roadmapTerm}
onValueChange={(value) => setRoadmapTerm(value)} onValueChange={(value) => setRoadmapTerm(value)}
placeholder="e.g. Try searching for Ansible or DevOps" placeholder="e.g. Try searching for Ansible or DevOps"

@ -1,16 +1,9 @@
import { import { ArrowUpRight, Ban, Cog, Telescope, Wand } from 'lucide-react';
ArrowUpRight,
Ban,
CircleFadingPlus,
Cog,
Telescope,
Wand,
} from 'lucide-react';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import { getOpenAIKey, isLoggedIn } from '../../lib/jwt'; import { getOpenAIKey, isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup'; import { showLoginPopup } from '../../lib/popup';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { OpenAISettings } from './OpenAISettings.tsx'; import { OpenAISettings } from './OpenAISettings.tsx';
import { AITermSuggestionInput } from './AITermSuggestionInput.tsx'; import { AITermSuggestionInput } from './AITermSuggestionInput.tsx';
import { setUrlParams } from '../../lib/browser.ts'; import { setUrlParams } from '../../lib/browser.ts';
@ -40,8 +33,13 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
const canGenerateMore = limitUsed < limit; const canGenerateMore = limitUsed < limit;
const [isConfiguring, setIsConfiguring] = useState(false); const [isConfiguring, setIsConfiguring] = useState(false);
const openAPIKey = getOpenAIKey(); const [openAPIKey, setOpenAPIKey] = useState('');
const isAuthenticatedUser = isLoggedIn(); const [isAuthenticatedUser, setIsAuthenticatedUser] = useState(false);
useEffect(() => {
setOpenAPIKey(getOpenAIKey() || '');
setIsAuthenticatedUser(isLoggedIn());
}, [getOpenAIKey(), isLoggedIn()]);
const randomTerms = ['OAuth', 'APIs', 'UX Design', 'gRPC']; const randomTerms = ['OAuth', 'APIs', 'UX Design', 'gRPC'];

Loading…
Cancel
Save