Skip to content
25 changes: 25 additions & 0 deletions apps/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
"dependencies": {
"@emotion/react": "^11.14.0",
"@chakra-ui/react": "^3.33.0",
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"next": "15.5.4",
"react": "19.1.0",
"react-dom": "19.1.0",
Expand Down
76 changes: 76 additions & 0 deletions apps/frontend/src/app/components/ProjectCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react';
import { LuDollarSign } from "react-icons/lu";
import { RxPeople } from "react-icons/rx";
import { FaArrowRight } from "react-icons/fa6";

type ActiveProps = {
variant: 'active';
name: string;
total_budget: number;
budget_used: number;
members: number;
};

type ArchiveProps = {
variant: 'archive';
name: string;
total_budget: number;
members: number;
start_date: string;
end_date: string;
};

type ProjectCardProps = ActiveProps | ArchiveProps;

export default function ProjectCard(props: ProjectCardProps) {
return (
<div className="!border-[1px] border-solid border-black-300 w-full sm:w-[50%] md:w-[35%] lg:w-[25%] rounded-[4px] overflow-hidden">
<div className="flex flex-col !gap-4 !p-4">
<h4 className="!px-2">{props.name}</h4>
<div className="flex flex-row !px-2 !gap-4 min-w-0">
<div className="flex flex-col !gap-1 !pr-4 !border-r-[2px] border-black-300 min-w-0">
<div className="flex flex-row items-center !gap-1 !pr-4">
<LuDollarSign />
<h5 className="!font-bold">Budget</h5>
</div>
<p className="truncate">
{props.variant === 'active'
? `$${props.budget_used.toLocaleString()}/$${props.total_budget.toLocaleString()}`
: `$${props.total_budget.toLocaleString()}`}
</p>
</div>
<div className="flex flex-col !gap-1 !pl-1 min-w-0">
<div className="flex flex-row items-center !gap-1">
<RxPeople />
<h5 className="!font-bold">Staff</h5>
</div>
<p className="truncate">{props.members.toLocaleString()} members</p>
</div>
</div>
{props.variant === 'active' ? (
<div className="flex flex-row items-center !gap-2">
<div className="w-full !h-[24px] rounded-full bg-black-100">
<div
style={{ width: `${Math.round((props.budget_used / props.total_budget) * 100)}%` }}
className="!h-full rounded-full bg-core-green"
/>
</div>
<p>{Math.round((props.budget_used / props.total_budget) * 100)}%</p>
</div>
) : (
<div className="flex flex-row w-full items-center !gap-4 !px-2">
<div className="flex flex-col !gap-1">
<h5 className="!font-bold">Start Date</h5>
<p>{props.start_date}</p>
</div>
<FaArrowRight className="shrink-0" />
<div className="flex flex-col !gap-1">
<h5 className="!font-bold">End Date</h5>
<p>{props.end_date}</p>
</div>
</div>
)}
</div>
</div>
);
}
1 change: 1 addition & 0 deletions apps/frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
import ProjectCard from "./components/ProjectCard";

Check warning on line 2 in apps/frontend/src/app/page.tsx

View workflow job for this annotation

GitHub Actions / frontend-ci

'ProjectCard' is defined but never used
import NavBar from "./components/Navbar";

export default function Home() {
Expand Down
75 changes: 75 additions & 0 deletions apps/frontend/test/components/ProjectCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { render, screen } from '../utils';
import ProjectCard from '@/app/components/ProjectCard';

const activeMockProps = {
variant: 'active' as const,
name: 'Clinician Communication Study',
total_budget: 500000,
budget_used: 150000,
members: 3,
};

const archiveMockProps = {
variant: 'archive' as const,
name: 'Health Education Initiative',
total_budget: 300000,
members: 2,
start_date: 'Jan 1, 2025',
end_date: 'Mar 1, 2026',
};

describe('ProjectCard (active)', () => {
it('renders the project name', () => {
render(<ProjectCard {...activeMockProps} />);
expect(screen.getByText('Clinician Communication Study')).toBeInTheDocument();
});

it('renders the budget label and values', () => {
render(<ProjectCard {...activeMockProps} />);
expect(screen.getByText('Budget')).toBeInTheDocument();
expect(screen.getByText((content) => content.includes('150,000') && content.includes('500,000'))).toBeInTheDocument();
});

it('renders the staff label and member count', () => {
render(<ProjectCard {...activeMockProps} />);
expect(screen.getByText('Staff')).toBeInTheDocument();
expect(screen.getByText('3 members')).toBeInTheDocument();
});

it('renders the correct percentage', () => {
render(<ProjectCard {...activeMockProps} />);
const percentage = Math.round((activeMockProps.budget_used / activeMockProps.total_budget) * 100);
expect(screen.getByText(`${percentage}%`)).toBeInTheDocument();
});
});

describe('ProjectCard (archive)', () => {
it('renders the project name', () => {
render(<ProjectCard {...archiveMockProps} />);
expect(screen.getByText('Health Education Initiative')).toBeInTheDocument();
});

it('renders the budget label and total', () => {
render(<ProjectCard {...archiveMockProps} />);
expect(screen.getByText('Budget')).toBeInTheDocument();
expect(screen.getByText('$300,000')).toBeInTheDocument();
});

it('renders the staff label and member count', () => {
render(<ProjectCard {...archiveMockProps} />);
expect(screen.getByText('Staff')).toBeInTheDocument();
expect(screen.getByText('2 members')).toBeInTheDocument();
});

it('renders the start and end date labels', () => {
render(<ProjectCard {...archiveMockProps} />);
expect(screen.getByText('Start Date')).toBeInTheDocument();
expect(screen.getByText('End Date')).toBeInTheDocument();
});

it('renders the correct start and end date values', () => {
render(<ProjectCard {...archiveMockProps} />);
expect(screen.getByText('Jan 1, 2025')).toBeInTheDocument();
expect(screen.getByText('Mar 1, 2026')).toBeInTheDocument();
});
});
Loading