diff --git a/apps/frontend/package-lock.json b/apps/frontend/package-lock.json
index 4e772a6..94f7fae 100644
--- a/apps/frontend/package-lock.json
+++ b/apps/frontend/package-lock.json
@@ -9,7 +9,9 @@
"version": "0.1.0",
"dependencies": {
"@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",
@@ -976,6 +978,29 @@
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
+ "node_modules/@emotion/styled": {
+ "version": "11.14.1",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
+ "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.13.5",
+ "@emotion/is-prop-valid": "^1.3.0",
+ "@emotion/serialize": "^1.3.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
+ "@emotion/utils": "^1.4.2"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0-rc.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index 0c8d0fb..af35674 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -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",
diff --git a/apps/frontend/src/app/components/ArchiveProjectCard.tsx b/apps/frontend/src/app/components/ArchiveProjectCard.tsx
new file mode 100644
index 0000000..3b1de70
--- /dev/null
+++ b/apps/frontend/src/app/components/ArchiveProjectCard.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { LuDollarSign } from "react-icons/lu";
+import { RxPeople } from "react-icons/rx";
+import { FaArrowRight } from "react-icons/fa6";
+
+interface ArchiveCardProps {
+ name: string;
+ total_budget: number;
+ members: number;
+ start_date: string;
+ end_date: string;
+}
+
+export default function ArchiveProjectCard({ name, total_budget, members, start_date, end_date }: ArchiveCardProps) {
+ return (
+
+
+
{name}
+
+
+
+
+
Budget
+
+
${total_budget.toLocaleString()}
+
+
+
+
+
Staff
+
+
{members.toLocaleString()} members
+
+
+
+
+
Start Date
+
{start_date}
+
+
+
+
End Date
+
{end_date}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/frontend/src/app/components/ProjectCard.tsx b/apps/frontend/src/app/components/ProjectCard.tsx
new file mode 100644
index 0000000..c27adbb
--- /dev/null
+++ b/apps/frontend/src/app/components/ProjectCard.tsx
@@ -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 (
+
+
+
{props.name}
+
+
+
+
+
Budget
+
+
+ {props.variant === 'active'
+ ? `$${props.budget_used.toLocaleString()}/$${props.total_budget.toLocaleString()}`
+ : `$${props.total_budget.toLocaleString()}`}
+
+
+
+
+
+
Staff
+
+
{props.members.toLocaleString()} members
+
+
+ {props.variant === 'active' ? (
+
+
+
{Math.round((props.budget_used / props.total_budget) * 100)}%
+
+ ) : (
+
+
+
Start Date
+
{props.start_date}
+
+
+
+
End Date
+
{props.end_date}
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/frontend/src/app/page.tsx b/apps/frontend/src/app/page.tsx
index edfb214..101bfb7 100644
--- a/apps/frontend/src/app/page.tsx
+++ b/apps/frontend/src/app/page.tsx
@@ -1,4 +1,5 @@
"use client";
+import ProjectCard from "./components/ProjectCard";
import NavBar from "./components/Navbar";
export default function Home() {
diff --git a/apps/frontend/test/components/ProjectCard.test.tsx b/apps/frontend/test/components/ProjectCard.test.tsx
new file mode 100644
index 0000000..2a95cbe
--- /dev/null
+++ b/apps/frontend/test/components/ProjectCard.test.tsx
@@ -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();
+ expect(screen.getByText('Clinician Communication Study')).toBeInTheDocument();
+ });
+
+ it('renders the budget label and values', () => {
+ render();
+ 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();
+ expect(screen.getByText('Staff')).toBeInTheDocument();
+ expect(screen.getByText('3 members')).toBeInTheDocument();
+ });
+
+ it('renders the correct percentage', () => {
+ render();
+ 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();
+ expect(screen.getByText('Health Education Initiative')).toBeInTheDocument();
+ });
+
+ it('renders the budget label and total', () => {
+ render();
+ expect(screen.getByText('Budget')).toBeInTheDocument();
+ expect(screen.getByText('$300,000')).toBeInTheDocument();
+ });
+
+ it('renders the staff label and member count', () => {
+ render();
+ expect(screen.getByText('Staff')).toBeInTheDocument();
+ expect(screen.getByText('2 members')).toBeInTheDocument();
+ });
+
+ it('renders the start and end date labels', () => {
+ render();
+ expect(screen.getByText('Start Date')).toBeInTheDocument();
+ expect(screen.getByText('End Date')).toBeInTheDocument();
+ });
+
+ it('renders the correct start and end date values', () => {
+ render();
+ expect(screen.getByText('Jan 1, 2025')).toBeInTheDocument();
+ expect(screen.getByText('Mar 1, 2026')).toBeInTheDocument();
+ });
+});
\ No newline at end of file