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