diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 15d81db..d8b2b8d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@v2
with:
version: 10
@@ -39,9 +39,39 @@ jobs:
- name: Run ESLint
run: pnpm run lint
+ test:
+ name: Run Tests
+ needs: lint
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: frontend
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v2
+ with:
+ version: 10
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ cache-dependency-path: frontend/pnpm-lock.yaml
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Run tests
+ run: pnpm run test
+
build:
name: Build Application
- needs: lint
+ needs: test
runs-on: ubuntu-latest
defaults:
run:
@@ -52,7 +82,7 @@ jobs:
uses: actions/checkout@v4
- name: Install pnpm
- uses: pnpm/action-setup@v4
+ uses: pnpm/action-setup@v2
with:
version: 10
diff --git a/frontend/src/components/common/Breadcrumb/Breadcrumb.tsx b/frontend/src/components/common/Breadcrumb/Breadcrumb.tsx
new file mode 100644
index 0000000..567f6f9
--- /dev/null
+++ b/frontend/src/components/common/Breadcrumb/Breadcrumb.tsx
@@ -0,0 +1,82 @@
+import type { ReactElement } from 'react';
+import { Link } from 'react-router-dom';
+import type { BreadcrumbItem, BreadcrumbProps } from './types';
+
+const DEFAULT_MOBILE_COLLAPSE_AT = 4;
+
+function Breadcrumb({
+ items,
+ current,
+ separator = '/',
+ className = '',
+ mobileCollapseAt = DEFAULT_MOBILE_COLLAPSE_AT,
+}: BreadcrumbProps): ReactElement {
+ const hasCurrentItem = items.some((item) => item.label === current);
+ const breadcrumbItems: BreadcrumbItem[] = hasCurrentItem
+ ? items
+ : [...items, { label: current }];
+
+ const mobileItems =
+ breadcrumbItems.length > mobileCollapseAt
+ ? [breadcrumbItems[0], { label: '...' }, breadcrumbItems[breadcrumbItems.length - 1]]
+ : breadcrumbItems;
+
+ const renderItem = (item: BreadcrumbItem, isCurrentPage: boolean) => {
+ if (isCurrentPage) {
+ return (
+
+ {item.label}
+
+ );
+ }
+
+ if (!item.href) {
+ return {item.label};
+ }
+
+ return (
+
+ {item.label}
+
+ );
+ };
+
+ return (
+
+ );
+}
+
+export default Breadcrumb;
diff --git a/frontend/src/components/common/Breadcrumb/index.ts b/frontend/src/components/common/Breadcrumb/index.ts
new file mode 100644
index 0000000..a00a917
--- /dev/null
+++ b/frontend/src/components/common/Breadcrumb/index.ts
@@ -0,0 +1,2 @@
+export { default } from './Breadcrumb';
+export type { BreadcrumbProps, BreadcrumbItem } from './types';
diff --git a/frontend/src/components/common/Breadcrumb/types.ts b/frontend/src/components/common/Breadcrumb/types.ts
new file mode 100644
index 0000000..17975c2
--- /dev/null
+++ b/frontend/src/components/common/Breadcrumb/types.ts
@@ -0,0 +1,12 @@
+export interface BreadcrumbItem {
+ label: string;
+ href?: string;
+}
+
+export interface BreadcrumbProps {
+ items: BreadcrumbItem[];
+ current: string;
+ separator?: '/' | '>';
+ className?: string;
+ mobileCollapseAt?: number;
+}
diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts
index a666b46..b99c40a 100644
--- a/frontend/src/components/index.ts
+++ b/frontend/src/components/index.ts
@@ -6,6 +6,8 @@ export { default as DataTable } from './common/DataTable';
export type { DataTableProps, ColumnDef, PaginationConfig, DataTableEmptyState, SortDirection } from './common/DataTable';
export { default as Pagination } from './common/Pagination';
export type { PaginationProps } from './common/Pagination';
+export { default as Breadcrumb } from './common/Breadcrumb';
+export type { BreadcrumbProps, BreadcrumbItem } from './common/Breadcrumb';
// Reusable UI primitives
export { default as Button } from './Button';