diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6834544..fbcdf2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,37 +1,32 @@ name: CI on: - push: - branches: [main] pull_request: branches: [main] + push: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - lint-and-build: + lint-typecheck-build: + name: Lint, Typecheck & Build (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest - strategy: matrix: node-version: [18, 20] steps: - - name: Checkout code + - name: Checkout uses: actions/checkout@v4 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Cache node_modules - id: cache-node-modules - uses: actions/cache@v4 - with: - path: node_modules - key: node-modules-${{ runner.os }}-node${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} - restore-keys: | - node-modules-${{ runner.os }}-node${{ matrix.node-version }}- + cache: npm - name: Install dependencies run: npm ci @@ -39,11 +34,11 @@ jobs: - name: Verify contract bindings are up-to-date run: npm run codegen:validate - - name: Run linter - run: npm run lint - - - name: Run type check + - name: TypeScript type check run: npm run typecheck - - name: Build project + - name: Lint + run: npm run lint + + - name: Build run: npm run build diff --git a/.gitignore b/.gitignore index fcdd599..30ffc71 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,6 @@ yarn-error.log* *.sublime-workspace # Local scripts output - # Auto-generated contract bindings (regenerated via npm run codegen) /shared/contracts-gen +fix.md diff --git a/components/atoms/error-boundary/index.tsx b/components/atoms/error-boundary/index.tsx new file mode 100644 index 0000000..98c4c74 --- /dev/null +++ b/components/atoms/error-boundary/index.tsx @@ -0,0 +1,36 @@ +import React from 'react' + +interface ErrorBoundaryProps { + children: React.ReactNode + fallback?: React.ReactNode +} + +interface ErrorBoundaryState { + hasError: boolean +} + +export class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { + super(props) + this.state = { hasError: false } + } + + static getDerivedStateFromError(): ErrorBoundaryState { + return { hasError: true } + } + + render() { + if (this.state.hasError) { + return this.props.fallback ?? ( +
+
⚠️
+

+ Something went wrong rendering this section. +

+
+ ) + } + + return this.props.children + } +} diff --git a/components/atoms/index.tsx b/components/atoms/index.tsx index 19b60bc..554cbbb 100644 --- a/components/atoms/index.tsx +++ b/components/atoms/index.tsx @@ -10,3 +10,4 @@ export * from './toast' export * from './theme-toggle' export * from './spacer' export * from './markdown-renderer' +export * from './error-boundary' diff --git a/components/molecules/dispute-card/index.tsx b/components/molecules/dispute-card/index.tsx new file mode 100644 index 0000000..1c701eb --- /dev/null +++ b/components/molecules/dispute-card/index.tsx @@ -0,0 +1,94 @@ +import Link from 'next/link' +import type { Dispute, DisputeStatus } from '../../../shared/types/dispute' + +interface DisputeCardProps { + dispute: Dispute +} + +const STATUS_LABELS: Record = { + pending: 'Pending', + active: 'Active', + voting: 'Voting', + resolved: 'Resolved', + appealed: 'Appealed', +} + +const STATUS_COLORS: Record = { + pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300', + active: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300', + voting: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300', + resolved: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', + appealed: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300', +} + +function getRemainingTime(deadline: string): string { + const diff = new Date(deadline).getTime() - Date.now() + if (diff <= 0) return 'Expired' + const days = Math.floor(diff / 86400000) + const hours = Math.floor((diff % 86400000) / 3600000) + if (days > 0) return `${days}d ${hours}h left` + return `${hours}h left` +} + +export function DisputeCard({ dispute }: DisputeCardProps) { + return ( + +
+
+
+

+ {dispute.title} +

+

+ {dispute.description} +

+
+ + {STATUS_LABELS[dispute.status]} + +
+ +
+
+ + {dispute.plaintiffName} + vs + {dispute.defendantName} +
+ + | + +
+ + + + {dispute.amount.toLocaleString()} USDC +
+ + | + +
+ + + + {getRemainingTime(dispute.deadline)} +
+
+ + {dispute.jurors.length > 0 && ( +
+
+ + + + {dispute.jurors.length} juror{dispute.jurors.length !== 1 ? 's' : ''} assigned + {dispute.votes.length > 0 && ( + · {dispute.votes.length} vote{dispute.votes.length !== 1 ? 's' : ''} cast + )} +
+
+ )} +
+ + ) +} diff --git a/components/molecules/dispute-filters/index.tsx b/components/molecules/dispute-filters/index.tsx new file mode 100644 index 0000000..9d49f40 --- /dev/null +++ b/components/molecules/dispute-filters/index.tsx @@ -0,0 +1,65 @@ +import type { DisputeStatus } from '../../../shared/types/dispute' + +type RoleFilter = 'all' | 'plaintiff' | 'defendant' | 'juror' + +interface DisputeFiltersProps { + statusFilter: DisputeStatus | 'all' + roleFilter: RoleFilter + onStatusChange: (status: DisputeStatus | 'all') => void + onRoleChange: (role: RoleFilter) => void +} + +const STATUS_OPTIONS: { value: DisputeStatus | 'all'; label: string }[] = [ + { value: 'all', label: 'All' }, + { value: 'active', label: 'Active' }, + { value: 'voting', label: 'Voting' }, + { value: 'resolved', label: 'Resolved' }, + { value: 'pending', label: 'Pending' }, + { value: 'appealed', label: 'Appealed' }, +] + +const ROLE_OPTIONS: { value: RoleFilter; label: string }[] = [ + { value: 'all', label: 'All Roles' }, + { value: 'plaintiff', label: 'As Plaintiff' }, + { value: 'defendant', label: 'As Defendant' }, + { value: 'juror', label: 'As Juror' }, +] + +export function DisputeFilters({ + statusFilter, + roleFilter, + onStatusChange, + onRoleChange, +}: DisputeFiltersProps) { + return ( +
+
+ {STATUS_OPTIONS.map(opt => ( + + ))} +
+ + +
+ ) +} diff --git a/components/molecules/evidence-submission/index.tsx b/components/molecules/evidence-submission/index.tsx new file mode 100644 index 0000000..5abbe3e --- /dev/null +++ b/components/molecules/evidence-submission/index.tsx @@ -0,0 +1,92 @@ +import { useState } from 'react' +import { FileUpload } from '../file-upload' +import type { UploadedFile } from '../file-upload' + +export interface EvidenceSubmissionProps { + disputeId: string + onEvidenceSubmitted?: (evidence: { cid: string; fileName: string; description: string }) => void +} + +export function EvidenceSubmission({ disputeId, onEvidenceSubmitted }: EvidenceSubmissionProps) { + const [description, setDescription] = useState('') + const [uploadedFiles, setUploadedFiles] = useState([]) + const [uploadError, setUploadError] = useState(null) + + const handleUploadComplete = (entry: UploadedFile) => { + setUploadError(null) + setUploadedFiles(prev => [...prev, entry]) + if (entry.cid) { + onEvidenceSubmitted?.({ cid: entry.cid, fileName: entry.file.name, description }) + setDescription('') + } + } + + const handleUploadError = (entry: UploadedFile) => { + setUploadError(entry.error ?? 'Upload failed') + } + + return ( +
+
+ +