fix: hydration errors

feat/ai-autocomplete
Arik Chakma 8 months ago
parent 270113f888
commit 4855d28144
  1. 100
      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 { 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<string, GetTopAIRoadmapTermResponse>(),
[],
);
const [officialRoadmaps, setOfficialRoadmaps] =
useState<GetTopAIRoadmapTermResponse>([]);
const toast = useToast();
const searchInputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLDivElement>(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<GetTopAIRoadmapTermResponse>(
`${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<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(() => {
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,20 +196,30 @@ export function AITermSuggestionInput(props: AITermSuggestionInputProps) {
setSearchedText('');
setIsActive(false);
} else if (e.key === 'Enter') {
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);
}
}
}
}}
/>
{isLoading && (
<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>
)}
@ -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);
}}
>
<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}
</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"
>
<AITermSuggestionInput
autoFocus={true}
value={roadmapTerm}
onValueChange={(value) => setRoadmapTerm(value)}
placeholder="e.g. Try searching for Ansible or DevOps"

@ -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'];

Loading…
Cancel
Save