feat: add project status (#7252)

* feat: add project status

* Update project card and fix warnings

* Add loading indicator to project card

---------

Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
pull/7236/head^2
Arik Chakma 2 months ago committed by GitHub
parent e3ca03e531
commit 52c1b20f56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 44
      src/components/Projects/ProjectCard.tsx
  2. 50
      src/components/Projects/ProjectsList.tsx

@ -5,10 +5,12 @@ import type {
} from '../../lib/project.ts'; } from '../../lib/project.ts';
import { Users } from 'lucide-react'; import { Users } from 'lucide-react';
import { formatCommaNumber } from '../../lib/number.ts'; import { formatCommaNumber } from '../../lib/number.ts';
import { cn } from '../../lib/classname.ts';
type ProjectCardProps = { type ProjectCardProps = {
project: ProjectFileType; project: ProjectFileType;
userCount?: number; userCount?: number;
status?: 'completed' | 'started' | 'none';
}; };
const badgeVariants: Record<ProjectDifficultyType, string> = { const badgeVariants: Record<ProjectDifficultyType, string> = {
@ -18,10 +20,13 @@ const badgeVariants: Record<ProjectDifficultyType, string> = {
}; };
export function ProjectCard(props: ProjectCardProps) { export function ProjectCard(props: ProjectCardProps) {
const { project, userCount = 0 } = props; const { project, userCount = 0, status } = props;
const { frontmatter, id } = project; const { frontmatter, id } = project;
const isLoadingStatus = status === undefined;
const userStartedCount =
status && status !== 'none' ? userCount + 1 : userCount;
return ( return (
<a <a
href={`/projects/${id}`} href={`/projects/${id}`}
@ -34,18 +39,47 @@ export function ProjectCard(props: ProjectCardProps) {
/> />
<Badge variant={'grey'} text={frontmatter.nature} /> <Badge variant={'grey'} text={frontmatter.nature} />
</span> </span>
<span className="my-3 flex flex-col"> <span className="my-3 flex min-h-[100px] flex-col">
<span className="mb-1 font-medium">{frontmatter.title}</span> <span className="mb-1 font-medium">{frontmatter.title}</span>
<span className="text-sm text-gray-500">{frontmatter.description}</span> <span className="text-sm text-gray-500">{frontmatter.description}</span>
</span> </span>
<span className="flex items-center gap-2 text-xs text-gray-400"> <span className="flex min-h-[22px] items-center justify-between gap-2 text-xs text-gray-400">
<Users className="inline-block size-3.5" /> {isLoadingStatus ? (
<>
<span className="h-5 w-24 animate-pulse rounded bg-gray-200" />{' '}
<span className="h-5 w-20 animate-pulse rounded bg-gray-200" />{' '}
</>
) : (
<>
<span className="flex items-center gap-1.5">
<Users className="size-3.5" />
{userCount > 0 ? ( {userCount > 0 ? (
<>{formatCommaNumber(userCount)} Started</> <>{formatCommaNumber(userCount)} Started</>
) : ( ) : (
<>Be the first to solve!</> <>Be the first to solve!</>
)} )}
</span> </span>
{status !== 'none' && (
<span
className={cn(
'flex items-center gap-1.5 rounded-full border border-current px-2 py-0.5 capitalize',
status === 'completed' && 'text-green-500',
status === 'started' && 'text-yellow-500',
)}
>
<span
className={cn('inline-block h-2 w-2 rounded-full', {
'bg-green-500': status === 'completed',
'bg-yellow-500': status === 'started',
})}
/>
{status}
</span>
)}
</>
)}
</span>
</a> </a>
); );
} }

@ -1,7 +1,7 @@
import { ProjectCard } from './ProjectCard.tsx'; import { ProjectCard } from './ProjectCard.tsx';
import { HeartHandshake, Trash2 } from 'lucide-react'; import { HeartHandshake, Trash2 } from 'lucide-react';
import { cn } from '../../lib/classname.ts'; import { cn } from '../../lib/classname.ts';
import { useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { import {
projectDifficulties, projectDifficulties,
type ProjectDifficultyType, type ProjectDifficultyType,
@ -12,6 +12,8 @@ import {
getUrlParams, getUrlParams,
setUrlParams, setUrlParams,
} from '../../lib/browser.ts'; } from '../../lib/browser.ts';
import { httpPost } from '../../lib/http.ts';
import { isLoggedIn } from '../../lib/jwt.ts';
type DifficultyButtonProps = { type DifficultyButtonProps = {
difficulty: ProjectDifficultyType; difficulty: ProjectDifficultyType;
@ -38,6 +40,11 @@ function DifficultyButton(props: DifficultyButtonProps) {
); );
} }
export type ListProjectStatusesResponse = Record<
string,
'completed' | 'started'
>;
type ProjectsListProps = { type ProjectsListProps = {
projects: ProjectFileType[]; projects: ProjectFileType[];
userCounts: Record<string, number>; userCounts: Record<string, number>;
@ -50,6 +57,25 @@ export function ProjectsList(props: ProjectsListProps) {
const [difficulty, setDifficulty] = useState< const [difficulty, setDifficulty] = useState<
ProjectDifficultyType | undefined ProjectDifficultyType | undefined
>(urlDifficulty); >(urlDifficulty);
const [projectStatuses, setProjectStatuses] =
useState<ListProjectStatusesResponse>();
const loadProjectStatuses = async () => {
const projectIds = projects.map((project) => project.id);
const { response, error } = await httpPost(
`${import.meta.env.PUBLIC_API_URL}/v1-list-project-statuses`,
{
projectIds,
},
);
if (error || !response) {
console.error(error);
return;
}
setProjectStatuses(response);
};
const projectsByDifficulty: Map<ProjectDifficultyType, ProjectFileType[]> = const projectsByDifficulty: Map<ProjectDifficultyType, ProjectFileType[]> =
useMemo(() => { useMemo(() => {
@ -72,12 +98,21 @@ export function ProjectsList(props: ProjectsListProps) {
? projectsByDifficulty.get(difficulty) || [] ? projectsByDifficulty.get(difficulty) || []
: projects; : projects;
useEffect(() => {
if (!isLoggedIn()) {
return;
}
loadProjectStatuses().finally();
}, []);
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<div className="my-2.5 flex items-center justify-between"> <div className="my-2.5 flex items-center justify-between">
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{projectDifficulties.map((projectDifficulty) => ( {projectDifficulties.map((projectDifficulty) => (
<DifficultyButton <DifficultyButton
key={projectDifficulty}
onClick={() => { onClick={() => {
setDifficulty(projectDifficulty); setDifficulty(projectDifficulty);
setUrlParams({ difficulty: projectDifficulty }); setUrlParams({ difficulty: projectDifficulty });
@ -130,7 +165,18 @@ export function ProjectsList(props: ProjectsListProps) {
}) })
.map((matchingProject) => { .map((matchingProject) => {
const count = userCounts[matchingProject?.id] || 0; const count = userCounts[matchingProject?.id] || 0;
return <ProjectCard project={matchingProject} userCount={count} />; return (
<ProjectCard
key={matchingProject.id}
project={matchingProject}
userCount={count}
status={
projectStatuses
? (projectStatuses?.[matchingProject.id] ?? 'none')
: undefined
}
/>
);
})} })}
</div> </div>
</div> </div>

Loading…
Cancel
Save