Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Frontend CI

on:
push:
branches: [ "main" ]
paths:
- 'app/frontend/**'
- '.github/workflows/frontend-ci.yml'
pull_request:
branches: [ "main" ]
paths:
- 'app/frontend/**'
- '.github/workflows/frontend-ci.yml'

jobs:
lint-and-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'

- name: Install dependencies
run: pnpm install --no-frozen-lockfile --engine-strict=false

# Non-blocking until the pre-existing lint errors on main are fixed
# (tracked in docs/accessibility/audit-2026.md ticket backlog).
- name: Lint
continue-on-error: true
run: pnpm --filter chainforge-frontend run lint

- name: Type check
run: pnpm --filter chainforge-frontend run type-check

- name: Test (includes axe accessibility suite)
run: pnpm --filter chainforge-frontend run test
6 changes: 4 additions & 2 deletions app/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
},
"dependencies": {
"@heroicons/react": "^2.2.0",
"openapi-fetch": "^0.13.5",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
Expand All @@ -29,6 +28,7 @@
"next": "^16.2.1",
"next-intl": "^4.9.1",
"next-themes": "^0.4.6",
"openapi-fetch": "^0.13.5",
"papaparse": "^5.5.3",
"react": "19.2.3",
"react-dom": "19.2.3",
Expand All @@ -38,10 +38,10 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"openapi-typescript": "^7.6.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/jest": "^30.0.0",
"@types/jest-axe": "^3.5",
"@types/leaflet": "^1.9.21",
"@types/node": "^20",
"@types/papaparse": "^5.3.16",
Expand All @@ -50,7 +50,9 @@
"eslint": "^9.39.4",
"eslint-config-next": "^16.2.1",
"jest": "^30.4.2",
"jest-axe": "^10",
"jest-environment-jsdom": "^30.0.0",
"openapi-typescript": "^7.6.1",
"tailwindcss": "^4",
"ts-jest": "^29.4.6",
"ts-node": "^10.9.2",
Expand Down
36 changes: 24 additions & 12 deletions app/frontend/src/app/[locale]/campaigns/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use client';

import Link from 'next/link';
import { useMemo, useState, useCallback } from 'react';
import { useMemo, useState, useCallback, useRef, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { AppEmptyState } from '@/components/empty-state/AppEmptyState';
import { ExportControls } from '@/components/dashboard/ExportControls';
import { useCampaigns, useCreateCampaign } from '@/hooks/useCampaigns';
import { useCampaignAction, useCampaignActions } from '@/hooks/useOptimisticCampaignMutations';

Check warning on line 9 in app/frontend/src/app/[locale]/campaigns/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-test

'useCampaignActions' is defined but never used
import { InlineFeedback, OptimisticStatusBadge } from '@/components/InlineFeedback';
import {
canManageCampaigns,
Expand All @@ -17,7 +17,7 @@
import { getStatusTransitionMessage } from '@/lib/status-messages';
import type { CampaignStatus } from '@/types/campaign';

const statusStyles: Record<CampaignStatus, string> = {

Check warning on line 20 in app/frontend/src/app/[locale]/campaigns/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-test

'statusStyles' is assigned a value but never used
draft: 'bg-gray-100 text-gray-800',
active: 'bg-green-100 text-green-800',
paused: 'bg-yellow-100 text-yellow-800',
Expand Down Expand Up @@ -53,12 +53,15 @@
const [budget, setBudget] = useState('');
const [token, setToken] = useState('USDC');
const [expiry, setExpiry] = useState('');
const [formMessage, setFormMessage] = useState<string | null>(null);
const [formMessage, setFormMessage] = useState<{
text: string;
kind: 'success' | 'error';
} | null>(null);

const previousCampaignsRef = React.useRef(campaigns);
const previousCampaignsRef = useRef(campaigns);
const [announcement, setAnnouncement] = useState('');

React.useEffect(() => {
useEffect(() => {
if (campaigns.length > 0 && previousCampaignsRef.current.length > 0) {
for (const campaign of campaigns) {
const prev = previousCampaignsRef.current.find(c => c.id === campaign.id);
Expand All @@ -70,14 +73,14 @@
previousCampaignsRef.current = campaigns;
}, [campaigns]);

function updateParam(key: string, value: string) {

Check warning on line 76 in app/frontend/src/app/[locale]/campaigns/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-test

'updateParam' is defined but never used
const params = new URLSearchParams(searchParams.toString());
if (value) params.set(key, value);
else params.delete(key);
router.replace(`?${params.toString()}`, { scroll: false });
}

const handleApplyPreset = useCallback(

Check warning on line 83 in app/frontend/src/app/[locale]/campaigns/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-test

'handleApplyPreset' is assigned a value but never used
(preset: { status?: string | '' }) => {
const params = new URLSearchParams();
if (preset.status) params.set('status', preset.status);
Expand All @@ -103,27 +106,30 @@
setBudget('15000');
setToken('USDC');
setExpiry('2026-12-31');
setFormMessage('Sample campaign values loaded. Review and create when ready.');
setFormMessage({
text: 'Sample campaign values loaded. Review and create when ready.',
kind: 'success',
});
};

if (!canManageCampaigns(userRole)) {
return (
<div className="min-h-screen bg-gray-50 p-8 dark:bg-gray-900">
<main className="min-h-screen bg-gray-50 p-8 dark:bg-gray-900">
<div className="mx-auto max-w-lg rounded-xl border border-red-200 bg-white p-6 dark:border-red-800 dark:bg-gray-800">
<h1 className="text-2xl font-semibold text-red-600">Access Denied</h1>
<p className="mt-2 text-sm text-gray-700 dark:text-gray-200">
This page is reserved for NGO and Admin roles. Your role is{' '}
<strong>{userRoleLabel}</strong>.
</p>
</div>
</div>
</main>
);
}

const handleCreate = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!name.trim() || !budget.trim()) {
setFormMessage('Name and budget are required.');
setFormMessage({ text: 'Name and budget are required.', kind: 'error' });
return;
}

Expand All @@ -143,9 +149,12 @@
setBudget('');
setToken('USDC');
setExpiry('');
setFormMessage('Campaign created successfully.');
setFormMessage({ text: 'Campaign created successfully.', kind: 'success' });
} catch (err) {
setFormMessage((err as Error).message ?? 'Failed to create campaign.');
setFormMessage({
text: (err as Error).message ?? 'Failed to create campaign.',
kind: 'error',
});
}
};

Expand Down Expand Up @@ -178,8 +187,11 @@
<section className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-gray-900">
<h2 className="mb-4 text-xl font-semibold">Create New Campaign</h2>
{formMessage && (
<div className="mb-4 rounded-md border bg-gray-50 p-3 text-sm text-gray-700 dark:bg-gray-800 dark:text-gray-200">
{formMessage}
<div
role={formMessage.kind === 'error' ? 'alert' : 'status'}
className="mb-4 rounded-md border bg-gray-50 p-3 text-sm text-gray-700 dark:bg-gray-800 dark:text-gray-200"
>
{formMessage.text}
</div>
)}
<form onSubmit={handleCreate} className="space-y-3">
Expand Down Expand Up @@ -319,7 +331,7 @@
{campaignAction.isPending && campaignAction.variables?.id === campaign.id ? (
<InlineFeedback
isPending={true}
action={campaignAction.variables?.action.type === 'pause' ? 'pausing' : campaignAction.variables?.action.type === 'resume' ? 'resuming' : 'archiving' as any}

Check failure on line 334 in app/frontend/src/app/[locale]/campaigns/page.tsx

View workflow job for this annotation

GitHub Actions / lint-and-test

Unexpected any. Specify a different type
/>
) : (
<>
Expand Down
24 changes: 19 additions & 5 deletions app/frontend/src/app/[locale]/claim-receipt/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export default function ClaimReceiptPage() {
onClick={() => router.back()}
className="text-blue-600 dark:text-blue-400 hover:underline mb-4 flex items-center gap-2"
>
Back
<span aria-hidden="true">←</span> Back
</button>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">
Claim Receipt
Expand All @@ -110,8 +110,15 @@ export default function ClaimReceiptPage() {

{/* Loading State */}
{loading && (
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-8 text-center">
<Loader2 className="inline-block animate-spin text-blue-600 dark:text-blue-400 mb-4" size={32} />
<div
role="status"
className="bg-white dark:bg-slate-800 rounded-lg shadow p-8 text-center"
>
<Loader2
aria-hidden="true"
className="inline-block animate-spin text-blue-600 dark:text-blue-400 mb-4"
size={32}
/>
<p className="text-slate-600 dark:text-slate-400">
Loading your receipt…
</p>
Expand All @@ -120,8 +127,15 @@ export default function ClaimReceiptPage() {

{/* Error State */}
{error && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6 flex gap-4">
<AlertCircle className="text-red-600 dark:text-red-400 flex-shrink-0" size={24} />
<div
role="alert"
className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-6 flex gap-4"
>
<AlertCircle
aria-hidden="true"
className="text-red-600 dark:text-red-400 flex-shrink-0"
size={24}
/>
<div>
<h2 className="font-semibold text-red-900 dark:text-red-100 mb-1">
Error
Expand Down
10 changes: 5 additions & 5 deletions app/frontend/src/app/[locale]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,23 @@ export default async function Home({ params }: PageProps) {

<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-16">
<div className="p-6 rounded-lg border border-gray-200 dark:border-gray-800 text-left">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-white">Direct Aid Claims</h3>
<h2 className="text-lg font-semibold mb-2 text-blue-900 dark:text-white">Direct Aid Claims</h2>
<p className="text-gray-600 dark:text-gray-400 text-sm">
Wallet-based, passwordless claiming—no accounts required.
</p>
</div>
<div className="p-6 rounded-lg border border-gray-200 dark:border-gray-800 text-left">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-white">
<h2 className="text-lg font-semibold mb-2 text-blue-900 dark:text-white">
AI Need Verification
</h3>
</h2>
<p className="text-gray-600 dark:text-gray-400 text-sm">
Client-side analysis for privacy-preserving eligibility.
</p>
</div>
<div className="p-6 rounded-lg border border-gray-200 dark:border-gray-800 text-left">
<h3 className="text-lg font-semibold mb-2 text-blue-900 dark:text-white">
<h2 className="text-lg font-semibold mb-2 text-blue-900 dark:text-white">
Immutable Transparency
</h3>
</h2>
<p className="text-gray-600 dark:text-gray-400 text-sm">
On-chain anchoring of distributions and impact reports.
</p>
Expand Down
4 changes: 2 additions & 2 deletions app/frontend/src/components/AidPackageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function PackageCard({ pkg }: { pkg: AidPackage }) {
<div className="p-4 border rounded-lg shadow-sm bg-white dark:bg-gray-800 dark:border-gray-700">
<div className="flex justify-between items-start">
<div>
<h4 className="font-medium">{pkg.title}</h4>
<h3 className="font-medium">{pkg.title}</h3>
<p className="text-sm text-gray-500">ID: {pkg.id}</p>
<p className="text-sm text-gray-500">{pkg.region}</p>
</div>
Expand Down Expand Up @@ -53,7 +53,7 @@ export const AidPackageList: React.FC = () => {

return (
<div className="space-y-4">
<h3 className="text-lg font-semibold">Available Aid Packages</h3>
<h2 className="text-lg font-semibold">Available Aid Packages</h2>
<div className="grid gap-4 md:grid-cols-2">
{packages.map(pkg => (
<PackageCard key={pkg.id} pkg={pkg} />
Expand Down
Loading
Loading