fix: update select languages

feat/languages
Arik Chakma 4 months ago
parent bfb3a3eb30
commit 76970ab2ac
  1. 11
      src/components/Projects/LeavingRoadmapWarningModal.tsx
  2. 303
      src/components/Projects/ListProjectSolutions.tsx
  3. 105
      src/components/Projects/SelectLanguages.tsx

@ -4,13 +4,13 @@ import { SubmissionRequirement } from './SubmissionRequirement.tsx';
type LeavingRoadmapWarningModalProps = {
onClose: () => void;
onContinue: () => void;
repositoryUrl: string;
};
export function LeavingRoadmapWarningModal(
props: LeavingRoadmapWarningModalProps,
) {
const { onClose, onContinue } = props;
const { onClose, repositoryUrl } = props;
return (
<Modal onClose={onClose} bodyClassName="h-auto p-4">
@ -45,13 +45,14 @@ export function LeavingRoadmapWarningModal(
</p>
</div>
<button
<a
className="inline-flex w-full items-center gap-2 rounded-lg bg-black px-3 py-2.5 text-sm text-white"
onClick={onContinue}
href={repositoryUrl}
target="_blank"
>
<ArrowUpRight className="h-5 w-5" />
Continue to Solution
</button>
</a>
<button
className="absolute right-2.5 top-2.5 text-gray-600 hover:text-black"

@ -13,41 +13,7 @@ import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
import { VoteButton } from './VoteButton.tsx';
import { GitHubIcon } from '../ReactIcons/GitHubIcon.tsx';
import { cn } from '../../lib/classname.ts';
const languageColors = new Map([
['JavaScript', 'bg-[#f1e05a]'],
['Python', 'bg-[#3572A5]'],
['Java', 'bg-[#b07219]'],
['HTML', 'bg-[#e34c26]'],
['CSS', 'bg-[#563d7c]'],
['C++', 'bg-[#f34b7d]'],
['C', 'bg-[#555555]'],
['Go', 'bg-[#00ADD8]'],
['TypeScript', 'bg-[#2b7489]'],
['Shell', 'bg-[#89e051]'],
['Ruby', 'bg-[#701516]'],
['PHP', 'bg-[#4F5D95]'],
['Rust', 'bg-[#dea584]'],
['Swift', 'bg-[#ffac45]'],
['Kotlin', 'bg-[#A97BFF]'],
['Dart', 'bg-[#00B4AB]'],
['Scala', 'bg-[#c22d40]'],
['Objective-C', 'bg-[#438eff]'],
['Vue', 'bg-[#41b883]'],
['R', 'bg-[#198CE7]'],
['Perl', 'bg-[#0298c3]'],
['Haskell', 'bg-[#5e5086]'],
['Lua', 'bg-[#000080]'],
['Matlab', 'bg-[#e16737]'],
['Vim script', 'bg-[#199f4b]'],
['Elixir', 'bg-[#6e4a7e]'],
['Erlang', 'bg-[#B83998]'],
['Clojure', 'bg-[#db5855]'],
['Markdown', 'bg-[#083fa1]'],
['TeX', 'bg-[#3D6117]'],
['SQL', 'bg-[#e38c00]'],
]);
import { SelectLanguages } from './SelectLanguages.tsx';
export interface ProjectStatusDocument {
_id?: string;
@ -88,10 +54,12 @@ type ListProjectSolutionsResponse = {
type QueryParams = {
p?: string;
l?: string;
};
type PageState = {
currentPage: number;
languages: string[];
};
const VISITED_SOLUTIONS_KEY = 'visited-project-solutions';
@ -130,22 +98,22 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
const toast = useToast();
const [pageState, setPageState] = useState<PageState>({
currentPage: 0,
languages: [],
});
const [isLoading, setIsLoading] = useState(true);
const [solutions, setSolutions] = useState<ListProjectSolutionsResponse>();
const [alreadyVisitedSolutions, setAlreadyVisitedSolutions] = useState<
Record<string, boolean>
>({});
const [showLeavingRoadmapModal, setShowLeavingRoadmapModal] = useState<
ListProjectSolutionsResponse['data'][number] | null
>(null);
const [distinctLanguages, setDistinctLanguages] = useState<string[]>([]);
const loadSolutions = async (page = 1) => {
const loadSolutions = async (page = 1, languages: string[] = []) => {
const { response, error } = await httpGet<ListProjectSolutionsResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-list-project-solutions/${projectId}`,
{
currPage: page,
...(languages.length > 0 ? { languages: languages.join(',') } : {}),
},
);
@ -158,6 +126,19 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
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 (
solutionId: string,
voteType: AllowedVoteType,
@ -207,13 +188,9 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
useEffect(() => {
const queryParams = getUrlParams() as QueryParams;
const alreadyVisitedSolutions = JSON.parse(
localStorage.getItem(VISITED_SOLUTIONS_KEY) || '{}',
);
setAlreadyVisitedSolutions(alreadyVisitedSolutions);
setPageState({
currentPage: +(queryParams.p || '1'),
languages: (queryParams.l || '').split(',').filter(Boolean),
});
}, []);
@ -223,22 +200,24 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
return;
}
if (pageState.currentPage !== 1) {
if (pageState.currentPage !== 1 || pageState.languages.length > 0) {
setUrlParams({
p: String(pageState.currentPage),
l: pageState.languages.join(','),
});
} else {
deleteUrlParam('p');
deleteUrlParam('l');
}
loadSolutions(pageState.currentPage).finally(() => {
loadSolutions(pageState.currentPage, pageState.languages).finally(() => {
setIsLoading(false);
});
}, [pageState]);
if (isLoading) {
return <LoadingSolutions />;
}
useEffect(() => {
loadDistinctLanguages().finally(() => {});
}, []);
const isEmpty = solutions?.data.length === 0;
if (isEmpty) {
@ -248,132 +227,122 @@ export function ListProjectSolutions(props: ListProjectSolutionsProps) {
const leavingRoadmapModal = showLeavingRoadmapModal ? (
<LeavingRoadmapWarningModal
onClose={() => setShowLeavingRoadmapModal(null)}
onContinue={() => {
const visitedSolutions = {
...alreadyVisitedSolutions,
[showLeavingRoadmapModal._id!]: true,
};
localStorage.setItem(
VISITED_SOLUTIONS_KEY,
JSON.stringify(visitedSolutions),
);
window.open(showLeavingRoadmapModal.repositoryUrl, '_blank');
}}
repositoryUrl={showLeavingRoadmapModal?.repositoryUrl!}
/>
) : null;
const selectedLanguages = pageState.languages;
return (
<section>
{leavingRoadmapModal}
<div className="flex min-h-[500px] flex-col divide-y divide-gray-100">
{solutions?.data.map((solution, counter) => {
const avatar = solution.user.avatar || '';
const languages = solution?.languages || [];
return (
<div
key={solution._id}
className="flex flex-col gap-2 py-2 text-sm text-gray-500"
>
<div className="flex flex-col justify-between gap-2 text-sm text-gray-500 sm:flex-row sm:items-center sm:gap-0">
<div className="flex items-center gap-1.5">
<img
src={
avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
: '/images/default-avatar.png'
}
alt={solution.user.name}
className="mr-0.5 h-7 w-7 rounded-full"
/>
<span className="font-medium text-black">
{solution.user.name}
</span>
<span className="hidden sm:inline">
{submittedAlternatives[
counter % submittedAlternatives.length
] || 'submitted their solution'}
</span>{' '}
<span className="flex-grow text-right text-gray-400 sm:flex-grow-0 sm:text-left sm:font-medium sm:text-black">
{getRelativeTimeString(solution?.submittedAt!)}
</span>
</div>
<div className="flex items-center justify-end gap-1">
<span className="flex overflow-hidden rounded-full border">
<VoteButton
icon={ThumbsUp}
isActive={solution?.voteType === 'upvote'}
count={solution.upvotes || 0}
onClick={() => {
handleSubmitVote(solution._id!, 'upvote');
}}
/>
<VoteButton
icon={ThumbsDown}
isActive={solution?.voteType === 'downvote'}
count={solution.downvotes || 0}
hideCount={true}
onClick={() => {
handleSubmitVote(solution._id!, 'downvote');
}}
/>
</span>
<a
className="ml-1 flex items-center gap-1 rounded-full border px-2 py-1 text-xs text-black transition-colors hover:border-black hover:bg-black hover:text-white"
onClick={(e) => {
e.preventDefault();
setShowLeavingRoadmapModal(solution);
}}
target="_blank"
href={solution.repositoryUrl}
>
<GitHubIcon className="h-4 w-4 text-current" />
Visit Solution
</a>
<SelectLanguages
languages={distinctLanguages}
selectedLanguages={pageState.languages}
onSelectLanguage={(language) => {
const isAlreadySelected = selectedLanguages.includes(language);
const newLanguages = isAlreadySelected
? selectedLanguages.filter((l) => l !== language)
: [...selectedLanguages, language];
setPageState({
...pageState,
languages: newLanguages,
});
}}
/>
{isLoading ? (
<LoadingSolutions />
) : (
<>
<div className="flex min-h-[500px] flex-col divide-y divide-gray-100">
{solutions?.data.map((solution, counter) => {
const avatar = solution.user.avatar || '';
return (
<div
key={solution._id}
className="flex flex-col gap-2 py-2 text-sm text-gray-500"
>
<div className="flex flex-col justify-between gap-2 text-sm text-gray-500 sm:flex-row sm:items-center sm:gap-0">
<div className="flex items-center gap-1.5">
<img
src={
avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${avatar}`
: '/images/default-avatar.png'
}
alt={solution.user.name}
className="mr-0.5 h-7 w-7 rounded-full"
/>
<span className="font-medium text-black">
{solution.user.name}
</span>
<span className="hidden sm:inline">
{submittedAlternatives[
counter % submittedAlternatives.length
] || 'submitted their solution'}
</span>{' '}
<span className="flex-grow text-right text-gray-400 sm:flex-grow-0 sm:text-left sm:font-medium sm:text-black">
{getRelativeTimeString(solution?.submittedAt!)}
</span>
</div>
<div className="flex items-center justify-end gap-1">
<span className="flex shrink-0 overflow-hidden rounded-full border">
<VoteButton
icon={ThumbsUp}
isActive={solution?.voteType === 'upvote'}
count={solution.upvotes || 0}
onClick={() => {
handleSubmitVote(solution._id!, 'upvote');
}}
/>
<VoteButton
icon={ThumbsDown}
isActive={solution?.voteType === 'downvote'}
count={solution.downvotes || 0}
hideCount={true}
onClick={() => {
handleSubmitVote(solution._id!, 'downvote');
}}
/>
</span>
<button
className="ml-1 flex items-center gap-1 rounded-full border px-2 py-1 text-xs text-black transition-colors hover:border-black hover:bg-black hover:text-white"
onClick={() => {
setShowLeavingRoadmapModal(solution);
}}
>
<GitHubIcon className="h-4 w-4 text-current" />
Visit Solution
</button>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-2.5">
{languages.map((language) => (
<span
key={language}
className="flex items-center gap-2 text-sm"
>
<span
className={cn(
'h-2 w-2 rounded-full',
languageColors.get(language) || 'bg-gray-400',
)}
/>
{language}
</span>
))}
</div>
);
})}
</div>
{(solutions?.totalPages || 0) > 1 && (
<div className="mt-4">
<Pagination
totalPages={solutions?.totalPages || 1}
currPage={solutions?.currPage || 1}
perPage={solutions?.perPage || 21}
totalCount={solutions?.totalCount || 0}
onPageChange={(page) => {
setPageState({
...pageState,
currentPage: page,
});
}}
/>
</div>
);
})}
</div>
{(solutions?.totalPages || 0) > 1 && (
<div className="mt-4">
<Pagination
totalPages={solutions?.totalPages || 1}
currPage={solutions?.currPage || 1}
perPage={solutions?.perPage || 21}
totalCount={solutions?.totalCount || 0}
onPageChange={(page) => {
setPageState({
...pageState,
currentPage: page,
});
}}
/>
</div>
)}
</>
)}
</section>
);

@ -0,0 +1,105 @@
import { useRef, useState } from 'react';
import { cn } from '../../lib/classname';
import { useOutsideClick } from '../../hooks/use-outside-click';
const languageColors = new Map([
['JavaScript', 'bg-[#f1e05a]'],
['Python', 'bg-[#3572A5]'],
['Java', 'bg-[#b07219]'],
['HTML', 'bg-[#e34c26]'],
['CSS', 'bg-[#563d7c]'],
['C++', 'bg-[#f34b7d]'],
['C', 'bg-[#555555]'],
['Go', 'bg-[#00ADD8]'],
['TypeScript', 'bg-[#2b7489]'],
['Shell', 'bg-[#89e051]'],
['Ruby', 'bg-[#701516]'],
['PHP', 'bg-[#4F5D95]'],
['Rust', 'bg-[#dea584]'],
['Swift', 'bg-[#ffac45]'],
['Kotlin', 'bg-[#A97BFF]'],
['Dart', 'bg-[#00B4AB]'],
['Scala', 'bg-[#c22d40]'],
['Objective-C', 'bg-[#438eff]'],
['Vue', 'bg-[#41b883]'],
['R', 'bg-[#198CE7]'],
['Perl', 'bg-[#0298c3]'],
['Haskell', 'bg-[#5e5086]'],
['Lua', 'bg-[#000080]'],
['Matlab', 'bg-[#e16737]'],
['Vim script', 'bg-[#199f4b]'],
['Elixir', 'bg-[#6e4a7e]'],
['Erlang', 'bg-[#B83998]'],
['Clojure', 'bg-[#db5855]'],
['Markdown', 'bg-[#083fa1]'],
['TeX', 'bg-[#3D6117]'],
['SQL', 'bg-[#e38c00]'],
]);
type SelectLanguagesProps = {
languages: string[];
selectedLanguages: string[];
onSelectLanguage: (language: string) => void;
};
export function SelectLanguages(props: SelectLanguagesProps) {
const { languages, onSelectLanguage, selectedLanguages } = props;
const dropdownRef = useRef<HTMLDivElement>(null);
const [isOpen, setIsOpen] = useState(false);
useOutsideClick(dropdownRef, () => {
setIsOpen(false);
});
return (
<div className="mb-4 flex justify-end">
{selectedLanguages.length > 0 && (
<>
{selectedLanguages.map((language) => (
<span
key={language}
className="mr-3 flex items-center gap-2 text-sm text-gray-600"
>
<span
className={cn(
'size-2 rounded-full',
languageColors.get(language) || 'bg-gray-300',
)}
/>
{language}
</span>
))}
</>
)}
<div className="relative flex">
<button
className="rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-900"
onClick={() => setIsOpen(!isOpen)}
>
Select Languages
</button>
{isOpen && (
<div
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"
ref={dropdownRef}
>
{languages.map((language) => {
return (
<button
key={language}
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>
);
})}
</div>
)}
</div>
</div>
);
}
Loading…
Cancel
Save