fix: select language

feat/languages
Arik Chakma 4 months ago
parent 76970ab2ac
commit 4adcf16f2d
  1. 79
      src/components/Projects/ListProjectSolutions.tsx
  2. 103
      src/components/Projects/SelectLanguages.tsx
  3. 17
      src/pages/projects/[projectId]/solutions.astro

@ -14,6 +14,7 @@ import { showLoginPopup } from '../../lib/popup';
import { VoteButton } from './VoteButton.tsx'; import { VoteButton } from './VoteButton.tsx';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx'; import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
import { SelectLanguages } from './SelectLanguages.tsx'; import { SelectLanguages } from './SelectLanguages.tsx';
import type { ProjectFileType, ProjectFrontmatter } from '../../lib/project.ts';
export interface ProjectStatusDocument { export interface ProjectStatusDocument {
_id?: string; _id?: string;
@ -59,12 +60,11 @@ type QueryParams = {
type PageState = { type PageState = {
currentPage: number; currentPage: number;
languages: string[]; language: string;
}; };
const VISITED_SOLUTIONS_KEY = 'visited-project-solutions';
type ListProjectSolutionsProps = { type ListProjectSolutionsProps = {
project: ProjectFrontmatter;
projectId: string; projectId: string;
}; };
@ -93,12 +93,12 @@ const submittedAlternatives = [
]; ];
export function ListProjectSolutions(props: ListProjectSolutionsProps) { export function ListProjectSolutions(props: ListProjectSolutionsProps) {
const { projectId } = props; const { projectId, project: projectData } = props;
const toast = useToast(); const toast = useToast();
const [pageState, setPageState] = useState<PageState>({ const [pageState, setPageState] = useState<PageState>({
currentPage: 0, currentPage: 0,
languages: [], language: '',
}); });
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -106,14 +106,13 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
const [showLeavingRoadmapModal, setShowLeavingRoadmapModal] = useState< const [showLeavingRoadmapModal, setShowLeavingRoadmapModal] = useState<
ListProjectSolutionsResponse['data'][number] | null ListProjectSolutionsResponse['data'][number] | null
>(null); >(null);
const [distinctLanguages, setDistinctLanguages] = useState<string[]>([]);
const loadSolutions = async (page = 1, languages: string[] = []) => { const loadSolutions = async (page = 1, language: string = '') => {
const { response, error } = await httpGet<ListProjectSolutionsResponse>( const { response, error } = await httpGet<ListProjectSolutionsResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-list-project-solutions/${projectId}`, `${import.meta.env.PUBLIC_API_URL}/v1-list-project-solutions/${projectId}`,
{ {
currPage: page, currPage: page,
...(languages.length > 0 ? { languages: languages.join(',') } : {}), ...(language ? { languages: language } : {}),
}, },
); );
@ -126,19 +125,6 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
setSolutions(response); setSolutions(response);
}; };
const loadDistinctLanguages = async () => {
const { response, error } = await httpGet<string[]>(
`${import.meta.env.PUBLIC_API_URL}/v1-list-project-languages/${projectId}`,
);
if (error || !response) {
toast.error(error?.message || 'Failed to load project languages');
return;
}
setDistinctLanguages(response);
};
const handleSubmitVote = async ( const handleSubmitVote = async (
solutionId: string, solutionId: string,
voteType: AllowedVoteType, voteType: AllowedVoteType,
@ -190,7 +176,7 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
const queryParams = getUrlParams() as QueryParams; const queryParams = getUrlParams() as QueryParams;
setPageState({ setPageState({
currentPage: +(queryParams.p || '1'), currentPage: +(queryParams.p || '1'),
languages: (queryParams.l || '').split(',').filter(Boolean), language: queryParams.l || '',
}); });
}, []); }, []);
@ -200,25 +186,21 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
return; return;
} }
if (pageState.currentPage !== 1 || pageState.languages.length > 0) { if (pageState.currentPage !== 1 || pageState.language !== '') {
setUrlParams({ setUrlParams({
p: String(pageState.currentPage), p: String(pageState.currentPage),
l: pageState.languages.join(','), l: pageState.language,
}); });
} else { } else {
deleteUrlParam('p'); deleteUrlParam('p');
deleteUrlParam('l'); deleteUrlParam('l');
} }
loadSolutions(pageState.currentPage, pageState.languages).finally(() => { loadSolutions(pageState.currentPage, pageState.language).finally(() => {
setIsLoading(false); setIsLoading(false);
}); });
}, [pageState]); }, [pageState]);
useEffect(() => {
loadDistinctLanguages().finally(() => {});
}, []);
const isEmpty = solutions?.data.length === 0; const isEmpty = solutions?.data.length === 0;
if (isEmpty) { if (isEmpty) {
return <EmptySolutions projectId={projectId} />; return <EmptySolutions projectId={projectId} />;
@ -231,26 +213,29 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
/> />
) : null; ) : null;
const selectedLanguages = pageState.languages; const selectedLanguage = pageState.language;
return ( return (
<section> <div className="mb-4 overflow-hidden rounded-lg border bg-white p-3 sm:p-5">
{leavingRoadmapModal} {leavingRoadmapModal}
<SelectLanguages <div className="relative mb-5 hidden sm:block">
languages={distinctLanguages} <div className="flex items-center justify-between gap-2">
selectedLanguages={pageState.languages} <h1 className="mb-1 text-xl font-semibold">
onSelectLanguage={(language) => { {projectData.title} Solutions
const isAlreadySelected = selectedLanguages.includes(language); </h1>
const newLanguages = isAlreadySelected <SelectLanguages
? selectedLanguages.filter((l) => l !== language) projectId={projectId}
: [...selectedLanguages, language]; selectedLanguage={selectedLanguage}
onSelectLanguage={(language) => {
setPageState({ setPageState((prev) => ({
...pageState, ...prev,
languages: newLanguages, language: prev.language === language ? '' : language,
}); }));
}} }}
/> />
</div>
<p className="text-sm text-gray-500">{projectData.description}</p>
</div>
{isLoading ? ( {isLoading ? (
<LoadingSolutions /> <LoadingSolutions />
@ -344,6 +329,6 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
)} )}
</> </>
)} )}
</section> </div>
); );
} }

@ -1,6 +1,8 @@
import { useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { cn } from '../../lib/classname'; import { cn } from '../../lib/classname';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
import { httpGet } from '../../lib/http';
import { useToast } from '../../hooks/use-toast';
const languageColors = new Map([ const languageColors = new Map([
['JavaScript', 'bg-[#f1e05a]'], ['JavaScript', 'bg-[#f1e05a]'],
@ -37,69 +39,74 @@ const languageColors = new Map([
]); ]);
type SelectLanguagesProps = { type SelectLanguagesProps = {
languages: string[]; projectId: string;
selectedLanguages: string[]; selectedLanguage: string;
onSelectLanguage: (language: string) => void; onSelectLanguage: (language: string) => void;
}; };
export function SelectLanguages(props: SelectLanguagesProps) { export function SelectLanguages(props: SelectLanguagesProps) {
const { languages, onSelectLanguage, selectedLanguages } = props; const { projectId, onSelectLanguage, selectedLanguage } = props;
const dropdownRef = useRef<HTMLDivElement>(null); const dropdownRef = useRef<HTMLDivElement>(null);
const toast = useToast();
const [distinctLanguages, setDistinctLanguages] = useState<string[]>([]);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const loadDistinctLanguages = async () => {
const { response, error } = await httpGet<string[]>(
`${import.meta.env.PUBLIC_API_URL}/v1-list-project-languages/${projectId}`,
);
if (error || !response) {
toast.error(error?.message || 'Failed to load project languages');
return;
}
setDistinctLanguages(response);
};
useOutsideClick(dropdownRef, () => { useOutsideClick(dropdownRef, () => {
setIsOpen(false); setIsOpen(false);
}); });
useEffect(() => {
loadDistinctLanguages().finally(() => {});
}, []);
return ( return (
<div className="mb-4 flex justify-end"> <div className="relative flex">
{selectedLanguages.length > 0 && ( <button
<> className="rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-900"
{selectedLanguages.map((language) => ( onClick={() => setIsOpen(!isOpen)}
<span >
key={language} {selectedLanguage || 'Select Language'}
className="mr-3 flex items-center gap-2 text-sm text-gray-600" </button>
>
<span
className={cn(
'size-2 rounded-full',
languageColors.get(language) || 'bg-gray-300',
)}
/>
{language}
</span>
))}
</>
)}
<div className="relative flex"> {isOpen && (
<button <div
className="rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-900" className="absolute right-0 top-full z-10 w-full min-w-[200px] max-w-[200px] translate-y-1.5 overflow-hidden rounded-md border border-gray-300 bg-white p-1 shadow-lg"
onClick={() => setIsOpen(!isOpen)} ref={dropdownRef}
> >
Select Languages {distinctLanguages.map((language) => {
</button> const isSelected = selectedLanguage === language;
{isOpen && ( return (
<div <button
className="absolute right-0 top-full z-10 w-full max-w-[200px] translate-y-1.5 overflow-hidden rounded-md border border-gray-300 bg-white p-1 shadow-lg" key={language}
ref={dropdownRef} className="flex w-full items-center rounded-md px-4 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100 aria-selected:bg-gray-100"
> onClick={() => {
{languages.map((language) => { onSelectLanguage(language);
return ( setIsOpen(false);
<button }}
key={language} aria-selected={isSelected}
className="flex w-full items-center rounded-md px-4 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-100" >
onClick={() => onSelectLanguage(language)} {language}
> </button>
{language} );
</button> })}
); </div>
})} )}
</div>
)}
</div>
</div> </div>
); );
} }

@ -49,18 +49,11 @@ const githubUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/maste
<div class='container'> <div class='container'>
<ProjectTabs projectId={projectId} activeTab='solutions' /> <ProjectTabs projectId={projectId} activeTab='solutions' />
<div class='mb-4 overflow-hidden rounded-lg border bg-white p-3 sm:p-5'> <ListProjectSolutions
<div class='relative mb-5 hidden sm:block'> project={projectData}
<h1 class='mb-1 text-xl font-semibold'> projectId={projectId}
{projectData.title} Solutions client:load
</h1> />
<p class='text-sm text-gray-500'>
{projectData.description}
</p>
</div>
<ListProjectSolutions projectId={projectId} client:load />
</div>
</div> </div>
</div> </div>
</BaseLayout> </BaseLayout>

Loading…
Cancel
Save