Skip to content
Open
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
26 changes: 23 additions & 3 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import { AnalyticsDashboardPage } from './pages/AnalyticsDashboardPage';
import { AdminDashboardPage } from './pages/AdminDashboardPage';
import { ActivityFeedPage } from './pages/ActivityFeedPage';
import { CampaignDetailPage } from './pages/CampaignDetailPage';
import { FarmerProfilePage } from './pages/FarmerProfilePage';
import { CreateCampaignPage } from './pages/CreateCampaignPage';
import { InvestorDashboardPage } from './pages/InvestorDashboardPage';
import {
FarmerProfilePage,
CampaignsPage,
FarmerDashboardPage,
NotFoundPage,
} from './pages';
import './App.css';

export default function App() {
Expand Down Expand Up @@ -47,15 +54,28 @@ export default function App() {
<Routes>
<Route path="/" element={<DesignFoundationsPage />} />
<Route path="/analytics" element={<AnalyticsDashboardPage />} />
<Route path="/admin" element={<AdminDashboardPage />} />
<Route path="/dev/components" element={<Playground />} />

{/* AppLayout routes */}
{/* AppLayout routes — all pages wired into the router (Issue #143) */}
<Route element={<AppLayout />}>
<Route path="/activity" element={<ActivityFeedPage />} />

{/* Campaign routes */}
<Route path="/campaigns" element={<CampaignsPage />} />
<Route path="/campaigns/new" element={<CreateCampaignPage />} />
<Route path="/campaigns/:id" element={<CampaignDetailPage />} />

{/* Dashboard routes */}
<Route path="/dashboard/investor" element={<InvestorDashboardPage />} />
<Route path="/dashboard/farmer" element={<FarmerDashboardPage />} />
<Route path="/dashboard/admin" element={<AdminDashboardPage />} />

{/* Profile routes */}
<Route path="/profile" element={<FarmerProfilePage />} />
<Route path="/farmers/:address" element={<FarmerProfilePage />} />

{/* Catch-all 404 route (Issue #143) */}
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
);
Expand Down
65 changes: 34 additions & 31 deletions client/src/components/campaign/FundCampaignModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ import {
} from '../../lib/soroban/campaignService';
import { toUserFacingError } from '../../lib/soroban/userFacingError';

const cardClass =
'rounded-campaign border border-soil-200 bg-white p-3 shadow-campaign';
const primaryButtonClass =
'rounded-lg bg-leaf-700 px-5 py-2.5 font-semibold text-white shadow-sm transition hover:bg-leaf-800 disabled:opacity-50';
const secondaryButtonClass =
'rounded-lg border border-soil-300 px-4 py-2.5 font-medium text-soil-700 transition hover:bg-soil-50 disabled:opacity-50';
const inputClass =
'w-full rounded-lg border border-soil-300 bg-white px-4 py-2.5 pl-8 text-body-sm text-soil-900 outline-none transition focus:border-leaf-500 focus:ring-1 focus:ring-leaf-500';
const labelClass = 'mb-1 block text-body-sm font-medium text-soil-700';

export interface FundCampaignResult {
success: boolean;
txHash?: string;
Expand Down Expand Up @@ -111,7 +121,7 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
title={
<>
Fund Campaign
<span className="mt-1 block text-sm font-normal text-slate-500 dark:text-slate-400">
<span className="mt-1 block text-body-sm font-normal text-soil-500">
{campaignTitle}
</span>
</>
Expand All @@ -122,28 +132,26 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
{successResult ? (
<div className="space-y-4 py-2 text-center">
<div
className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-emerald-100 text-2xl font-bold text-emerald-700 dark:bg-emerald-950 dark:text-emerald-400"
className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-status-resolved-light text-2xl font-bold text-status-resolved"
aria-hidden="true"
>
</div>
<h3 className="text-lg font-semibold text-slate-900 dark:text-white">
Contribution Successful!
</h3>
<p className="text-sm text-slate-600 dark:text-slate-300">
<h3 className="text-h4 text-soil-900">Contribution Successful!</h3>
<p className="text-body-sm text-soil-600">
You contributed{' '}
<span className="font-semibold text-emerald-700 dark:text-emerald-400">
<span className="font-semibold text-status-resolved">
${numAmount.toLocaleString()}
</span>{' '}
to {campaignTitle}.
</p>

{successResult.txHash && (
<div className="rounded-xl bg-slate-50 p-3 text-left dark:bg-slate-800/60">
<span className="block font-mono text-xs text-slate-600 dark:text-slate-400">
<div className={cardClass}>
<span className="block font-mono text-caption text-soil-500">
Transaction Status
</span>
<span className="break-all font-mono text-xs text-slate-700 dark:text-slate-300">
<span className="break-all font-mono text-caption text-soil-700">
{successResult.txHash}
</span>
</div>
Expand All @@ -153,7 +161,7 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
<button
type="button"
onClick={resetAndClose}
className="w-full rounded-xl bg-emerald-700 px-4 py-3 font-medium text-white transition hover:bg-emerald-800"
className={`${primaryButtonClass} w-full`}
>
Done
</button>
Expand All @@ -166,27 +174,27 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
{!isConnected && (
<div
role="status"
className="rounded-xl border border-amber-200 bg-amber-50 p-3 text-sm font-medium text-amber-800 dark:border-amber-800 dark:bg-amber-950/50 dark:text-amber-300"
className="rounded-campaign border border-status-harvested/20 bg-status-harvested-light p-3 text-body-sm font-medium text-status-harvested-dark"
>
Connect your wallet to fund this campaign.
</div>
)}

{/* Stats bar */}
<div className="grid grid-cols-2 gap-3 rounded-xl border border-slate-100 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-800/40">
<div className="grid grid-cols-2 gap-3 rounded-campaign border border-soil-100 bg-soil-50 p-3">
<div>
<span className="block text-xs text-slate-600 dark:text-slate-400">
<span className="block text-caption text-soil-500">
Remaining Target
</span>
<span className="text-sm font-semibold text-slate-900 dark:text-white">
<span className="text-body-sm font-semibold text-soil-900">
${remainingTarget.toLocaleString()}
</span>
</div>
<div>
<span className="block text-xs text-slate-600 dark:text-slate-400">
<span className="block text-caption text-soil-500">
Est. Share
</span>
<span className="text-sm font-semibold text-emerald-700 dark:text-emerald-400">
<span className="text-body-sm font-semibold text-status-resolved">
{estimatedShare}%
</span>
</div>
Expand All @@ -197,23 +205,20 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
<div
id="contribution-amount-error"
role="alert"
className="rounded-xl border border-red-200 bg-red-50 p-3 text-sm text-red-700 dark:border-red-800 dark:bg-red-950/50 dark:text-red-300"
className="rounded-campaign border border-status-failed/20 bg-status-failed-light p-3 text-body-sm text-status-failed-dark"
>
{error}
</div>
)}

{/* Input field */}
<div>
<label
htmlFor="contribution-amount"
className="mb-1 block text-sm font-medium text-slate-700 dark:text-slate-300"
>
<label htmlFor="contribution-amount" className={labelClass}>
Contribution Amount (USDC)
</label>
<div className="relative">
<span
className="absolute left-3 top-1/2 -translate-y-1/2 font-semibold text-slate-600 dark:text-slate-400"
className="absolute left-3 top-1/2 -translate-y-1/2 font-semibold text-soil-500"
aria-hidden="true"
>
$
Expand All @@ -234,41 +239,39 @@ export const FundCampaignModal: React.FC<FundCampaignModalProps> = ({
aria-describedby={
error ? 'contribution-amount-error' : undefined
}
className="w-full rounded-xl border border-slate-300 bg-white py-2.5 pl-8 pr-4 text-slate-900 outline-none transition focus:border-transparent focus:ring-2 focus:ring-emerald-500 dark:border-slate-700 dark:bg-slate-800 dark:text-white"
className={inputClass}
/>
</div>
</div>

{/* Quick selectors */}
<div className="flex items-center gap-2">
<span className="text-xs text-slate-600 dark:text-slate-400">
Quick fill:
</span>
<span className="text-caption text-soil-500">Quick fill:</span>
{[25, 50, 100].map((pct) => (
<button
type="button"
key={pct}
onClick={() => handlePercentageSelect(pct)}
className="rounded-lg border border-slate-200 px-2.5 py-1 text-xs text-slate-600 transition hover:bg-slate-100 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
className="rounded-lg border border-soil-200 px-2.5 py-1 text-caption text-soil-600 transition hover:bg-soil-50"
>
{pct}%
</button>
))}
</div>

{/* Footer Buttons */}
<div className="flex items-center justify-end gap-3 border-t border-slate-100 pt-3 dark:border-slate-800">
<div className="flex items-center justify-end gap-3 border-t border-soil-100 pt-3">
<button
type="button"
onClick={resetAndClose}
className="rounded-xl border border-slate-300 px-4 py-2.5 font-medium text-slate-700 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
className={secondaryButtonClass}
>
Cancel
</button>
<button
type="submit"
disabled={loading || remainingTarget <= 0 || !isConnected}
className="rounded-xl bg-emerald-700 px-5 py-2.5 font-semibold text-white shadow-sm transition hover:bg-emerald-800 disabled:opacity-50"
className={primaryButtonClass}
>
{loading ? 'Confirming...' : 'Confirm Contribution'}
</button>
Expand Down
48 changes: 24 additions & 24 deletions client/src/components/investor/InvestmentCard.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
import React, { useState } from 'react';
import type { FundedInvestment } from '../../lib/soroban/investorService';
import { STATUS_META } from '../../lib/campaignStatus';
import type { CampaignStatusTag } from '../../lib/soroban/types';

export interface InvestmentCardProps {
investment: FundedInvestment;
onClaimRefund: (campaignId: string) => Promise<void>;
onClaimReturn: (campaignId: string) => Promise<void>;
}

const statusBadgeStyles: Record<string, string> = {
Active: 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-300',
Funding:
'bg-emerald-100 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300',
Settled:
'bg-purple-100 text-purple-800 dark:bg-purple-950 dark:text-purple-300',
Resolved: 'bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300',
Failed: 'bg-rose-100 text-rose-800 dark:bg-rose-950 dark:text-rose-300',
};
const cardClass =
'rounded-campaign border border-soil-200 bg-white p-6 shadow-campaign flex flex-col gap-4 transition hover:border-soil-300 md:flex-row md:items-center md:justify-between';
const primaryValueClass = 'font-semibold text-soil-900';

export const InvestmentCard: React.FC<InvestmentCardProps> = ({
investment,
Expand Down Expand Up @@ -50,37 +46,41 @@ export const InvestmentCard: React.FC<InvestmentCardProps> = ({
investment.claimableAmount > 0 &&
!investment.claimed;

const statusMeta = STATUS_META[investment.status as CampaignStatusTag] ?? {
label: investment.status,
bgLight: 'bg-soil-100',
text: 'text-soil-800',
};

return (
<div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl p-6 shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4 transition hover:border-slate-300 dark:hover:border-slate-700">
<div className={cardClass}>
<div className="space-y-2">
<div className="flex items-center gap-3">
<span
className={`px-3 py-1 text-xs font-semibold rounded-full ${
statusBadgeStyles[investment.status] ||
'bg-slate-100 text-slate-800'
}`}
className={`status-badge ${statusMeta.bgLight} ${statusMeta.text}`}
>
{investment.status}
<span className={`h-2 w-2 rounded-full ${statusMeta.bg}`} aria-hidden="true" />
{statusMeta.label}
</span>
<span className="text-xs font-mono text-slate-600 dark:text-slate-400">
<span className="font-mono text-caption text-soil-500">
ID: {investment.campaignId}
</span>
</div>

<h3 className="text-lg font-bold text-slate-900 dark:text-white">
<h3 className="text-lg font-bold text-soil-900">
{investment.title}
</h3>

<div className="flex items-center gap-6 text-sm text-slate-600 dark:text-slate-400">
<div className="flex flex-wrap items-center gap-x-6 gap-y-1 text-body-sm text-soil-600">
<div>
Contributed:{' '}
<span className="font-semibold text-slate-900 dark:text-white">
<span className={primaryValueClass}>
${investment.amountContributed.toLocaleString()}
</span>
</div>
<div>
Claimable:{' '}
<span className="font-semibold text-emerald-700 dark:text-emerald-400">
<span className="font-semibold text-status-resolved">
${investment.claimableAmount.toLocaleString()}
</span>
</div>
Expand All @@ -89,15 +89,15 @@ export const InvestmentCard: React.FC<InvestmentCardProps> = ({

<div className="flex items-center justify-end">
{investment.claimed ? (
<span className="rounded-xl border border-slate-200 bg-slate-100 px-4 py-2 text-xs font-semibold text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400">
<span className="rounded-lg border border-soil-200 bg-soil-50 px-4 py-2 text-caption font-semibold text-soil-600">
<span aria-hidden="true">✓</span> Claimed
</span>
) : isRefundable ? (
<button
type="button"
onClick={() => void handleClaim()}
disabled={claiming}
className="rounded-xl bg-amber-700 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-amber-800 disabled:opacity-50"
className="rounded-lg bg-status-harvested px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:opacity-90 disabled:opacity-50"
>
{claiming ? 'Claiming...' : 'Claim Refund'}
</button>
Expand All @@ -106,12 +106,12 @@ export const InvestmentCard: React.FC<InvestmentCardProps> = ({
type="button"
onClick={() => void handleClaim()}
disabled={claiming}
className="rounded-xl bg-emerald-700 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-emerald-800 disabled:opacity-50"
className="rounded-lg bg-status-resolved px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:opacity-90 disabled:opacity-50"
>
{claiming ? 'Claiming...' : 'Claim Return'}
</button>
) : (
<span className="text-xs italic text-slate-600 dark:text-slate-400">
<span className="text-caption italic text-soil-500">
No payout pending
</span>
)}
Expand Down
Loading