feat: add project's user count (#6992)

* feat: add project user count

* feat: add user count

* fix: user count
feat/roadmap-page
Arik Chakma 3 months ago committed by GitHub
parent c48c9e75f9
commit d5b9c97fed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 15
      src/api/project.ts
  2. 8
      src/components/Projects/ProjectCard.tsx
  3. 10
      src/components/Projects/ProjectsList.tsx
  4. 34
      src/pages/[roadmapId]/projects.astro

@ -0,0 +1,15 @@
import { type APIContext } from 'astro';
import { api } from './api.ts';
export function projectApi(context: APIContext) {
return {
listProjectsUserCount: async function (projectIds: string[]) {
return api(context).post<Record<string, number>>(
`${import.meta.env.PUBLIC_API_URL}/v1-list-projects-user-count`,
{
projectIds,
},
);
},
};
}

@ -3,9 +3,11 @@ import type {
ProjectDifficultyType, ProjectDifficultyType,
ProjectFileType, ProjectFileType,
} from '../../lib/project.ts'; } from '../../lib/project.ts';
import { Users } from 'lucide-react';
type ProjectCardProps = { type ProjectCardProps = {
project: ProjectFileType; project: ProjectFileType;
userCount?: number;
}; };
const badgeVariants: Record<ProjectDifficultyType, string> = { const badgeVariants: Record<ProjectDifficultyType, string> = {
@ -15,7 +17,7 @@ const badgeVariants: Record<ProjectDifficultyType, string> = {
}; };
export function ProjectCard(props: ProjectCardProps) { export function ProjectCard(props: ProjectCardProps) {
const { project } = props; const { project, userCount = 0 } = props;
const { frontmatter, id } = project; const { frontmatter, id } = project;
@ -33,6 +35,10 @@ export function ProjectCard(props: ProjectCardProps) {
</span> </span>
<span className="mb-1 mt-2.5 font-medium">{frontmatter.title}</span> <span className="mb-1 mt-2.5 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 className="mt-2.5 flex items-center gap-2 text-xs text-gray-500">
<Users className="inline-block size-3.5" />
{userCount > 0 ? <>{userCount} Started</> : <>Be the first to solve!</>}
</span>
</a> </a>
); );
} }

@ -40,10 +40,11 @@ function DifficultyButton(props: DifficultyButtonProps) {
type ProjectsListProps = { type ProjectsListProps = {
projects: ProjectFileType[]; projects: ProjectFileType[];
userCounts: Record<string, number>;
}; };
export function ProjectsList(props: ProjectsListProps) { export function ProjectsList(props: ProjectsListProps) {
const { projects } = props; const { projects, userCounts } = props;
const { difficulty: urlDifficulty } = getUrlParams(); const { difficulty: urlDifficulty } = getUrlParams();
const [difficulty, setDifficulty] = useState< const [difficulty, setDifficulty] = useState<
@ -127,9 +128,10 @@ export function ProjectsList(props: ProjectsListProps) {
.sort((a, b) => { .sort((a, b) => {
return a.frontmatter.sort - b.frontmatter.sort; return a.frontmatter.sort - b.frontmatter.sort;
}) })
.map((matchingProject) => ( .map((matchingProject) => {
<ProjectCard project={matchingProject} /> const count = userCounts[matchingProject?.id] || 0;
))} return <ProjectCard project={matchingProject} userCount={count} />;
})}
</div> </div>
</div> </div>
); );

@ -1,25 +1,12 @@
--- ---
import { EditorRoadmap } from '../../components/EditorRoadmap/EditorRoadmap';
import FAQs, { type FAQType } from '../../components/FAQs/FAQs.astro';
import FrameRenderer from '../../components/FrameRenderer/FrameRenderer.astro';
import RelatedRoadmaps from '../../components/RelatedRoadmaps.astro';
import RoadmapHeader from '../../components/RoadmapHeader.astro'; import RoadmapHeader from '../../components/RoadmapHeader.astro';
import { FolderKanbanIcon } from 'lucide-react';
import { EmptyProjects } from '../../components/Projects/EmptyProjects'; import { EmptyProjects } from '../../components/Projects/EmptyProjects';
import { ProjectsList } from '../../components/Projects/ProjectsList'; import { ProjectsList } from '../../components/Projects/ProjectsList';
import ShareIcons from '../../components/ShareIcons/ShareIcons.astro';
import { UserProgressModal } from '../../components/UserProgress/UserProgressModal';
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import { getProjectsByRoadmapId } from '../../lib/project'; import { getProjectsByRoadmapId } from '../../lib/project';
import {
generateArticleSchema,
generateFAQSchema,
} from '../../lib/jsonld-schema';
import { getOpenGraphImageUrl } from '../../lib/open-graph'; import { getOpenGraphImageUrl } from '../../lib/open-graph';
import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap'; import { type RoadmapFrontmatter, getRoadmapIds } from '../../lib/roadmap';
import RoadmapNote from '../../components/RoadmapNote.astro'; import { projectApi } from '../../api/project';
import { RoadmapTitleQuestion } from '../../components/RoadmapTitleQuestion';
import ResourceProgressStats from '../../components/ResourceProgressStats.astro';
export async function getStaticPaths() { export async function getStaticPaths() {
const roadmapIds = await getRoadmapIds(); const roadmapIds = await getRoadmapIds();
@ -48,7 +35,7 @@ const ogImageUrl =
resourceId: roadmapId, resourceId: roadmapId,
}); });
const descriptionNoun = { const descriptionNoun: Record<string, string> = {
'AI and Data Scientist': 'AI and Data Science', 'AI and Data Scientist': 'AI and Data Science',
'Game Developer': 'Game Development', 'Game Developer': 'Game Development',
'Technical Writer': 'Technical Writing', 'Technical Writer': 'Technical Writing',
@ -61,10 +48,15 @@ const description = `Project ideas to take you from beginner to advanced in ${de
// `Seeking backend projects to enhance your skills? Explore our top 20 project ideas, from simple apps to complex systems. Start building today!` // `Seeking backend projects to enhance your skills? Explore our top 20 project ideas, from simple apps to complex systems. Start building today!`
const seoTitle = `${roadmapData.briefTitle} Projects`; const seoTitle = `${roadmapData.briefTitle} Projects`;
const nounTitle = const nounTitle =
descriptionNoun[roadmapData.briefTitle] || roadmapData.briefTitle; descriptionNoun[roadmapData?.briefTitle] || roadmapData.briefTitle;
const seoDescription = `Seeking ${nounTitle.toLowerCase()} projects to enhance your skills? Explore our top 20 project ideas, from simple apps to complex systems. Start building today!`; const seoDescription = `Seeking ${nounTitle.toLowerCase()} projects to enhance your skills? Explore our top 20 project ideas, from simple apps to complex systems. Start building today!`;
const projects = await getProjectsByRoadmapId(roadmapId); const projects = await getProjectsByRoadmapId(roadmapId);
const projectIds = projects.map((project) => project.id);
const projectApiClient = projectApi(Astro);
const { response: userCounts } =
await projectApiClient.listProjectsUserCount(projectIds);
--- ---
<BaseLayout <BaseLayout
@ -95,7 +87,15 @@ const projects = await getProjectsByRoadmapId(roadmapId);
<div class='container'> <div class='container'>
{projects.length === 0 && <EmptyProjects client:load />} {projects.length === 0 && <EmptyProjects client:load />}
{projects.length > 0 && <ProjectsList projects={projects} client:load />} {
projects.length > 0 && (
<ProjectsList
projects={projects}
userCounts={userCounts || {}}
client:load
/>
)
}
</div> </div>
</div> </div>
</BaseLayout> </BaseLayout>

Loading…
Cancel
Save