diff --git a/client/src/App.tsx b/client/src/App.tsx index 2c64f58..852da64 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -7,6 +7,7 @@ import { AnalyticsDashboardPage } from './pages/AnalyticsDashboardPage'; import { AdminDashboardPage } from './pages/AdminDashboardPage'; import { ActivityFeedPage } from './pages/ActivityFeedPage'; import { CampaignDetailPage } from './pages/CampaignDetailPage'; +import { CreateCampaignPage, CampaignsPage } from './pages'; import './App.css'; export default function App() { @@ -52,6 +53,8 @@ export default function App() { {/* AppLayout routes */} }> } /> + } /> + } /> } /> diff --git a/client/src/components/campaign/FundCampaignModal.tsx b/client/src/components/campaign/FundCampaignModal.tsx index 8d9579e..439a7ef 100644 --- a/client/src/components/campaign/FundCampaignModal.tsx +++ b/client/src/components/campaign/FundCampaignModal.tsx @@ -106,39 +106,38 @@ export const FundCampaignModal: React.FC = ({ title={ <> Fund Campaign - + {campaignTitle} } size="md" > - {/* Success View */} {successResult ? (
-

+

Contribution Successful!

-

+

You contributed{' '} - + ${numAmount.toLocaleString()} {' '} to {campaignTitle}.

{successResult.txHash && ( -
- +
+ Transaction Hash - + {successResult.txHash}
@@ -148,36 +147,33 @@ export const FundCampaignModal: React.FC = ({
) : ( - /* Input Form View */
- {/* Stats bar */} -
+
- + Remaining Target - + ${remainingTarget.toLocaleString()}
- + Est. Share - + {estimatedShare}%
- {/* Error banner */} {error && (
= ({
)} - {/* Input field */}
- {/* Quick selectors */}
- + Quick fill: {[25, 50, 100].map((pct) => ( @@ -234,26 +228,25 @@ export const FundCampaignModal: React.FC = ({ 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-xs text-soil-600 transition hover:bg-soil-100 dark:border-soil-700 dark:text-soil-300 dark:hover:bg-soil-800" > {pct}% ))}
- {/* Footer Buttons */} -
+
diff --git a/client/src/components/investor/InvestmentCard.tsx b/client/src/components/investor/InvestmentCard.tsx index 0410188..7b2bc3f 100644 --- a/client/src/components/investor/InvestmentCard.tsx +++ b/client/src/components/investor/InvestmentCard.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import type { FundedInvestment } from '../../lib/soroban/investorService'; +import { StatusBadge } from '../campaign/StatusBadge'; export interface InvestmentCardProps { investment: FundedInvestment; @@ -7,16 +8,6 @@ export interface InvestmentCardProps { onClaimReturn: (campaignId: string) => Promise; } -const statusBadgeStyles: Record = { - 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', -}; - export const InvestmentCard: React.FC = ({ investment, onClaimRefund, @@ -51,36 +42,29 @@ export const InvestmentCard: React.FC = ({ !investment.claimed; return ( -
+
- - {investment.status} - - + + ID: {investment.campaignId}
-

+

{investment.title}

-
+
Contributed:{' '} - + ${investment.amountContributed.toLocaleString()}
Claimable:{' '} - + ${investment.claimableAmount.toLocaleString()}
@@ -89,7 +73,7 @@ export const InvestmentCard: React.FC = ({
{investment.claimed ? ( - + Claimed ) : isRefundable ? ( @@ -97,7 +81,7 @@ export const InvestmentCard: React.FC = ({ 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-xl bg-status-harvested-dark px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-bark-800 disabled:opacity-50" > {claiming ? 'Claiming...' : 'Claim Refund'} @@ -106,12 +90,12 @@ export const InvestmentCard: React.FC = ({ 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-xl bg-leaf-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-leaf-700 disabled:opacity-50" > {claiming ? 'Claiming...' : 'Claim Return'} ) : ( - + No payout pending )} diff --git a/client/src/components/investor/InvestorSummaryStats.tsx b/client/src/components/investor/InvestorSummaryStats.tsx index 3806e61..71e6cc4 100644 --- a/client/src/components/investor/InvestorSummaryStats.tsx +++ b/client/src/components/investor/InvestorSummaryStats.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type { PortfolioStats } from '../../lib/soroban/investorService'; +import { StatTile } from '../analytics/StatTile'; export interface InvestorSummaryStatsProps { stats: PortfolioStats; @@ -11,42 +12,20 @@ export const InvestorSummaryStats: React.FC = ({ totalCampaigns, }) => { return ( -
-
- - Funded Projects - - - {totalCampaigns} - -
- -
- - Total Contributed - - - ${stats.totalInvested.toLocaleString()} - -
- -
- - Claimable / Pending - - - ${stats.totalPending.toLocaleString()} - -
- -
- - Total Claimed - - - ${stats.totalClaimed.toLocaleString()} - -
+
+ + + +
); }; diff --git a/client/src/pages/CampaignDetailPage.tsx b/client/src/pages/CampaignDetailPage.tsx index bcc8407..dc0ef39 100644 --- a/client/src/pages/CampaignDetailPage.tsx +++ b/client/src/pages/CampaignDetailPage.tsx @@ -1,100 +1,104 @@ import React, { useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; import { FundCampaignModal } from '../components/campaign/FundCampaignModal'; import { StatusBadge } from '../components/campaign/StatusBadge'; import { ActivityFeed } from '../components/campaign/ActivityFeed'; import { useCampaignLiveUpdates } from '../hooks/useCampaignLiveUpdates'; +import { useCampaign } from '../hooks/contract/useEscrowQueries'; +import { contractQueryKeys } from '../hooks/contract/queryKeys'; import { DetailPageSkeleton } from '../components/ui/Skeleton/Skeleton'; +import type { CampaignStatusTag } from '../lib/soroban/types'; -export interface CampaignData { - id: string; - title: string; - description: string; - totalTarget: number; - currentRaised: number; - status: 'Active' | 'Funding' | 'Resolved' | 'Failed' | 'Settled'; +function toDisplayAmount(value: bigint): number { + const asNumber = Number(value); + return Number.isFinite(asNumber) ? asNumber : 0; } export const CampaignDetailPage: React.FC = () => { - const [campaign, setCampaign] = useState(null); + const { id } = useParams<{ id: string }>(); + const queryClient = useQueryClient(); const [isModalOpen, setIsModalOpen] = useState(false); - React.useEffect(() => { - const timer = window.setTimeout(() => { - setCampaign({ - id: 'camp-101', - title: 'Organic Maize Irrigation & Harvesting PIP', - description: - 'Scaling sustainable maize production across 250 hectares with automated precision drip irrigation and AI-powered yield monitoring.', - totalTarget: 50000, - currentRaised: 32500, - status: 'Funding', - }); - }, 0); - return () => window.clearTimeout(timer); - }, []); - - // Refreshes this page when another wallet's contribution changes funding - // progress; no-op (and no page breakage) if VITE_WS_URL isn't configured. - // Called unconditionally (before the loading early-return) per rules of - // hooks; the hook itself no-ops until a campaign id is available. - useCampaignLiveUpdates(campaign?.id); - - if (!campaign) { + const { data: campaign, isLoading, isError, error } = useCampaign(id); + + useCampaignLiveUpdates(id); + + if (!id) { return ( -
- +
+

No campaign id in the URL.

); } - const percentage = Math.min( - 100, - Math.round((campaign.currentRaised / campaign.totalTarget) * 100), - ); + if (isLoading) { + return ( +
+ +
+ ); + } - const handleFundingSuccess = (_res: unknown, addedAmount: number) => { - setCampaign((prev) => - prev - ? { - ...prev, - currentRaised: prev.currentRaised + addedAmount, - } - : prev, + if (isError || !campaign) { + return ( +
+

+ {error instanceof Error + ? error.message + : `Could not load campaign ${id}.`} +

+
); + } + + const status = campaign.status.tag as CampaignStatusTag; + const totalTarget = toDisplayAmount(campaign.target_amount); + const currentRaised = toDisplayAmount(campaign.total_funded); + const percentage = + totalTarget > 0 + ? Math.min(100, Math.round((currentRaised / totalTarget) * 100)) + : 0; + const title = campaign.harvest_metadata || `Campaign ${id}`; + + const handleFundingSuccess = () => { + void queryClient.invalidateQueries({ + queryKey: contractQueryKeys.campaign(id), + }); }; return ( -
-
+
+
- - - ID: {campaign.id} + + + ID: {id}
-

- {campaign.title} +

+ {title}

-

- {campaign.description} +

+ Farmer {campaign.farmer}

- - ${campaign.currentRaised.toLocaleString()}{' '} - + + ${currentRaised.toLocaleString()}{' '} + raised - - Target: ${campaign.totalTarget.toLocaleString()} ({percentage}%) + + Target: ${totalTarget.toLocaleString()} ({percentage}%)
{ aria-label={`Campaign funding progress: ${percentage}% of target raised`} >
-
+
@@ -123,16 +127,16 @@ export const CampaignDetailPage: React.FC = () => { setIsModalOpen(false)} - campaignId={campaign.id} - campaignTitle={campaign.title} - totalTarget={campaign.totalTarget} - currentRaised={campaign.currentRaised} + campaignId={id} + campaignTitle={title} + totalTarget={totalTarget} + currentRaised={currentRaised} onSuccess={handleFundingSuccess} /> -
+
diff --git a/client/src/pages/InvestorDashboardPage.tsx b/client/src/pages/InvestorDashboardPage.tsx index 6afa2eb..d803099 100644 --- a/client/src/pages/InvestorDashboardPage.tsx +++ b/client/src/pages/InvestorDashboardPage.tsx @@ -75,19 +75,19 @@ export const InvestorDashboardPage: React.FC = () => { return (
{/* Header */} -
+
-

+

Investor Dashboard

-

+

Track funded campaigns, claimable returns, and pro-rata refunds.

-
+