diff --git a/apps/frontend/src/app.spec.tsx b/apps/frontend/src/app.spec.tsx
new file mode 100644
index 000000000..95caf44df
--- /dev/null
+++ b/apps/frontend/src/app.spec.tsx
@@ -0,0 +1,15 @@
+import { render } from '@testing-library/react';
+
+import App from './app';
+
+describe('App', () => {
+ it('should render successfully', () => {
+ const { baseElement } = render();
+ expect(baseElement).toBeTruthy();
+ });
+
+ it('should have a greeting as the title', () => {
+ const { getByText } = render();
+ expect(getByText(/Welcome frontend/gi)).toBeTruthy();
+ });
+});
diff --git a/apps/frontend/src/components/ApplicationTable.tsx b/apps/frontend/src/components/ApplicationTable.tsx
index 6a1d75cd7..535ade7a4 100644
--- a/apps/frontend/src/components/ApplicationTable.tsx
+++ b/apps/frontend/src/components/ApplicationTable.tsx
@@ -1,3 +1,4 @@
+import React from 'react';
import { Table } from '@chakra-ui/react';
const COLUMNS = [
@@ -63,7 +64,23 @@ const APPLICATIONS = [
},
];
-export const ApplicationTable: React.FC = () => {
+interface ApplicationTableProps {
+ searchQuery?: string;
+}
+
+export function ApplicationTable({ searchQuery = '' }: ApplicationTableProps) {
+ const filteredApplications = APPLICATIONS.filter((application) => {
+ if (!searchQuery) return true;
+ const query = searchQuery.toLowerCase();
+ return (
+ application.name.toLowerCase().includes(query) ||
+ application.discipline.toLowerCase().includes(query) ||
+ application.disciplineAdminName.toLowerCase().includes(query) ||
+ application.status.toLowerCase().includes(query) ||
+ application.experienceType.toLowerCase().includes(query)
+ );
+ });
+
return (
@@ -80,7 +97,7 @@ export const ApplicationTable: React.FC = () => {
- {APPLICATIONS.map((application) => (
+ {filteredApplications.map((application) => (
{application.name}
{application.proposedDate}
@@ -94,6 +111,6 @@ export const ApplicationTable: React.FC = () => {
);
-};
+}
export default ApplicationTable;
diff --git a/apps/frontend/src/components/ApprovedCard.tsx b/apps/frontend/src/components/ApprovedCard.tsx
new file mode 100644
index 000000000..87d59bac7
--- /dev/null
+++ b/apps/frontend/src/components/ApprovedCard.tsx
@@ -0,0 +1,64 @@
+import React from 'react';
+import { Box, Heading, Text, Flex } from '@chakra-ui/react';
+
+interface ApprovedCardProps {
+ title: string;
+ count: number;
+ description: string;
+ icon: React.ReactNode;
+}
+
+export function ApprovedCard({
+ title,
+ count,
+ description,
+ icon,
+}: ApprovedCardProps) {
+ return (
+
+
+
+ {title}
+
+
+ {icon}
+
+
+
+ {count}
+
+
+ {description}
+
+
+ );
+}
diff --git a/apps/frontend/src/components/PageCounter.tsx b/apps/frontend/src/components/PageCounter.tsx
new file mode 100644
index 000000000..d5d84d27c
--- /dev/null
+++ b/apps/frontend/src/components/PageCounter.tsx
@@ -0,0 +1,87 @@
+interface PageCounterProps {
+ page: number;
+ setPage: (page: number) => void;
+ maxPages: number;
+}
+
+function PageCounter({ page, setPage, maxPages }: PageCounterProps) {
+ const getPageNumbers = () => {
+ const pages: (number | string)[] = [];
+
+ if (maxPages <= 4) {
+ // Show all pages if 4 or fewer
+ for (let i = 1; i <= maxPages; i++) {
+ pages.push(i);
+ }
+ } else {
+ // Always show first 3 pages when near the start
+ if (page <= 3) {
+ pages.push(1, 2, 3);
+ pages.push('...');
+ pages.push(maxPages);
+ }
+ // Show last 3 pages when near the end
+ else if (page >= maxPages - 2) {
+ pages.push(1);
+ pages.push('...');
+ pages.push(maxPages - 2, maxPages - 1, maxPages);
+ }
+ // Show current page with neighbors in the middle
+ else {
+ pages.push(1);
+ pages.push('...');
+ pages.push(page - 1, page, page + 1);
+ pages.push('...');
+ pages.push(maxPages);
+ }
+ }
+
+ return pages;
+ };
+
+ return (
+
+ {getPageNumbers().map((p, index) =>
+ typeof p === 'string' ? (
+
+ {p}
+
+ ) : (
+
+ ),
+ )}
+
+ );
+}
+
+export default PageCounter;
diff --git a/apps/frontend/src/components/PageTransitionButton.tsx b/apps/frontend/src/components/PageTransitionButton.tsx
new file mode 100644
index 000000000..bc847112f
--- /dev/null
+++ b/apps/frontend/src/components/PageTransitionButton.tsx
@@ -0,0 +1,41 @@
+import { IoChevronBack, IoChevronForward } from 'react-icons/io5';
+
+function PageTransitionButton({
+ buttonType,
+ onClick,
+}: {
+ buttonType: 'previous' | 'next';
+ onClick: () => void;
+}) {
+ return (
+
+ );
+}
+
+export default PageTransitionButton;
diff --git a/apps/frontend/src/components/TableSearchBar.tsx b/apps/frontend/src/components/TableSearchBar.tsx
new file mode 100644
index 000000000..0bbe1e366
--- /dev/null
+++ b/apps/frontend/src/components/TableSearchBar.tsx
@@ -0,0 +1,52 @@
+import { IoSearch } from 'react-icons/io5';
+
+interface SearchbarProps {
+ value: string;
+ onChange: (e: React.ChangeEvent) => void;
+}
+
+function Searchbar({ value, onChange }: SearchbarProps) {
+ return (
+
+
+
+
+ );
+}
+
+export default Searchbar;
diff --git a/apps/frontend/src/containers/root.tsx b/apps/frontend/src/containers/root.tsx
index 4f55def13..7bd4f80cb 100644
--- a/apps/frontend/src/containers/root.tsx
+++ b/apps/frontend/src/containers/root.tsx
@@ -6,14 +6,31 @@ import clockIcon from '../assets/icons/clock.svg';
import crossIcon from '../assets/icons/cross.svg';
import checkmarkIcon from '../assets/icons/checkmark.svg';
import { Box } from '@chakra-ui/react';
+import { useState } from 'react';
+import PageTransitionButton from '@components/PageTransitionButton';
+import Searchbar from '@components/TableSearchBar';
+import PageCounter from '@components/PageCounter';
import ApplicationTable from '@components/ApplicationTable';
const Root: React.FC = () => {
+ const [searchQuery, setSearchQuery] = useState('');
+ const [page, setPage] = useState(1);
+
+ function onChange(e: React.ChangeEvent) {
+ setSearchQuery(e.target.value);
+ }
return (
-
+
-
-
+
+
{
icon={checkmarkIcon}
/>
-
+
+
+
+
+
+
+
+
+
setPage(page - 1)}
+ />
+
+ setPage(page + 1)}
+ />
+
);