mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-13 20:54:16 +02:00
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>
This commit is contained in:
@@ -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>
|
||||||
|
Reference in New Issue
Block a user