From 9c27ebe066d928120c61621322af2dbbe09b850c Mon Sep 17 00:00:00 2001
From: JayWebtech
Date: Fri, 27 Mar 2026 11:49:30 +0100
Subject: [PATCH] feat: governance page and others
---
.../dto/proposal-votes-response.dto.ts | 46 ++++
.../governance-proposals.controller.ts | 41 +++
.../modules/governance/governance.module.ts | 3 +-
.../governance/governance.service.spec.ts | 14 +-
.../modules/governance/governance.service.ts | 59 ++++-
.../app/components/dashboard/ProposalCard.tsx | 21 +-
frontend/app/dashboard/analytics/page.tsx | 83 +++++-
.../dashboard/governance/GovernanceClient.tsx | 220 ++++++++++++++++
frontend/app/dashboard/governance/page.tsx | 164 +-----------
frontend/app/savings/page.tsx | 245 +++++++++++++-----
10 files changed, 665 insertions(+), 231 deletions(-)
create mode 100644 backend/src/modules/governance/dto/proposal-votes-response.dto.ts
create mode 100644 backend/src/modules/governance/governance-proposals.controller.ts
create mode 100644 frontend/app/dashboard/governance/GovernanceClient.tsx
diff --git a/backend/src/modules/governance/dto/proposal-votes-response.dto.ts b/backend/src/modules/governance/dto/proposal-votes-response.dto.ts
new file mode 100644
index 000000000..196509cd1
--- /dev/null
+++ b/backend/src/modules/governance/dto/proposal-votes-response.dto.ts
@@ -0,0 +1,46 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { VoteDirection } from '../entities/vote.entity';
+
+class ProposalVoteTallyDto {
+ @ApiProperty({ example: 128 })
+ forVotes: number;
+
+ @ApiProperty({ example: 64 })
+ againstVotes: number;
+
+ @ApiProperty({ example: '84250.5' })
+ forWeight: string;
+
+ @ApiProperty({ example: '29300' })
+ againstWeight: string;
+
+ @ApiProperty({ example: '113550.5' })
+ totalWeight: string;
+}
+
+class RecentVoterDto {
+ @ApiProperty({
+ example: 'GB7TAYQB6A6E7MCCKRUYJ4JYK2YTHJOTD4A5Q65XAH2EJQ2F6J67P5ST',
+ })
+ walletAddress: string;
+
+ @ApiProperty({ enum: VoteDirection, enumName: 'VoteDirection' })
+ direction: VoteDirection;
+
+ @ApiProperty({ example: '5000' })
+ weight: string;
+
+ @ApiProperty({ example: '2026-03-26T13:01:15.518Z' })
+ votedAt: string;
+}
+
+export class ProposalVotesResponseDto {
+ @ApiProperty({ example: 12 })
+ proposalOnChainId: number;
+
+ @ApiProperty({ type: ProposalVoteTallyDto })
+ tally: ProposalVoteTallyDto;
+
+ @ApiProperty({ type: [RecentVoterDto] })
+ recentVoters: RecentVoterDto[];
+}
diff --git a/backend/src/modules/governance/governance-proposals.controller.ts b/backend/src/modules/governance/governance-proposals.controller.ts
new file mode 100644
index 000000000..4bf657dc5
--- /dev/null
+++ b/backend/src/modules/governance/governance-proposals.controller.ts
@@ -0,0 +1,41 @@
+import {
+ Controller,
+ DefaultValuePipe,
+ Get,
+ Param,
+ ParseIntPipe,
+ Query,
+} from '@nestjs/common';
+import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
+import { ProposalVotesResponseDto } from './dto/proposal-votes-response.dto';
+import { GovernanceService } from './governance.service';
+
+@ApiTags('governance')
+@Controller('governance/proposals')
+export class GovernanceProposalsController {
+ constructor(private readonly governanceService: GovernanceService) {}
+
+ @Get(':id/votes')
+ @ApiOperation({
+ summary: 'Get proposal vote tally and recent voters',
+ description:
+ 'Returns a proposal vote tally plus the most recent voters for a given proposal onChainId.',
+ })
+ @ApiQuery({
+ name: 'limit',
+ required: false,
+ description: 'Maximum number of recent voter entries to return',
+ example: 20,
+ })
+ @ApiResponse({
+ status: 200,
+ description: 'Vote tally and recent voter list for proposal',
+ type: ProposalVotesResponseDto,
+ })
+ getProposalVotes(
+ @Param('id', ParseIntPipe) id: number,
+ @Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
+ ): Promise {
+ return this.governanceService.getProposalVotesByOnChainId(id, limit);
+ }
+}
diff --git a/backend/src/modules/governance/governance.module.ts b/backend/src/modules/governance/governance.module.ts
index 1439ab428..983e7bcdc 100644
--- a/backend/src/modules/governance/governance.module.ts
+++ b/backend/src/modules/governance/governance.module.ts
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { GovernanceController } from './governance.controller';
+import { GovernanceProposalsController } from './governance-proposals.controller';
import { GovernanceService } from './governance.service';
import { GovernanceIndexerService } from './governance-indexer.service';
import { UserModule } from '../user/user.module';
@@ -14,7 +15,7 @@ import { Vote } from './entities/vote.entity';
BlockchainModule,
TypeOrmModule.forFeature([GovernanceProposal, Vote]),
],
- controllers: [GovernanceController],
+ controllers: [GovernanceController, GovernanceProposalsController],
providers: [GovernanceService, GovernanceIndexerService],
})
export class GovernanceModule {}
diff --git a/backend/src/modules/governance/governance.service.spec.ts b/backend/src/modules/governance/governance.service.spec.ts
index 6d3443575..f5738f0d8 100644
--- a/backend/src/modules/governance/governance.service.spec.ts
+++ b/backend/src/modules/governance/governance.service.spec.ts
@@ -1,26 +1,38 @@
import { Test, TestingModule } from '@nestjs/testing';
+import { getRepositoryToken } from '@nestjs/typeorm';
import { GovernanceService } from './governance.service';
import { UserService } from '../user/user.service';
import { StellarService } from '../blockchain/stellar.service';
import { SavingsService } from '../blockchain/savings.service';
+import { GovernanceProposal } from './entities/governance-proposal.entity';
+import { Vote } from './entities/vote.entity';
describe('GovernanceService', () => {
let service: GovernanceService;
let userService: { findById: jest.Mock };
let stellarService: { getDelegationForUser: jest.Mock };
let savingsService: { getUserVaultBalance: jest.Mock };
+ let proposalRepo: { findOneBy: jest.Mock };
+ let voteRepo: { find: jest.Mock };
beforeEach(async () => {
userService = { findById: jest.fn() };
stellarService = { getDelegationForUser: jest.fn() };
savingsService = { getUserVaultBalance: jest.fn() };
+ proposalRepo = { findOneBy: jest.fn() };
+ voteRepo = { find: jest.fn() };
const module: TestingModule = await Test.createTestingModule({
providers: [
GovernanceService,
{ provide: UserService, useValue: userService },
{ provide: StellarService, useValue: stellarService },
- { provide: SavingsService, useValue: savingsService }, // 👈 this was missing
+ { provide: SavingsService, useValue: savingsService },
+ {
+ provide: getRepositoryToken(GovernanceProposal),
+ useValue: proposalRepo,
+ },
+ { provide: getRepositoryToken(Vote), useValue: voteRepo },
],
}).compile();
diff --git a/backend/src/modules/governance/governance.service.ts b/backend/src/modules/governance/governance.service.ts
index 3269c1aa8..7ef6622f8 100644
--- a/backend/src/modules/governance/governance.service.ts
+++ b/backend/src/modules/governance/governance.service.ts
@@ -1,8 +1,13 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
import { StellarService } from '../blockchain/stellar.service';
import { SavingsService } from '../blockchain/savings.service';
import { UserService } from '../user/user.service';
import { DelegationResponseDto } from './dto/delegation-response.dto';
+import { ProposalVotesResponseDto } from './dto/proposal-votes-response.dto';
+import { GovernanceProposal } from './entities/governance-proposal.entity';
+import { Vote, VoteDirection } from './entities/vote.entity';
import { VotingPowerResponseDto } from './dto/voting-power-response.dto';
@Injectable()
@@ -11,6 +16,10 @@ export class GovernanceService {
private readonly userService: UserService,
private readonly stellarService: StellarService,
private readonly savingsService: SavingsService,
+ @InjectRepository(GovernanceProposal)
+ private readonly proposalRepo: Repository,
+ @InjectRepository(Vote)
+ private readonly voteRepo: Repository,
) {}
async getUserDelegation(userId: string): Promise {
@@ -46,4 +55,52 @@ export class GovernanceService {
});
return { votingPower: `${votingPower} NST` };
}
+
+ async getProposalVotesByOnChainId(
+ onChainId: number,
+ limit = 20,
+ ): Promise {
+ const proposal = await this.proposalRepo.findOneBy({ onChainId });
+ if (!proposal) {
+ throw new NotFoundException(`Proposal ${onChainId} not found`);
+ }
+
+ const safeLimit = Math.max(1, Math.min(100, Math.floor(limit)));
+ const votes = await this.voteRepo.find({
+ where: { proposalId: proposal.id },
+ order: { createdAt: 'DESC' },
+ take: safeLimit,
+ });
+
+ let forWeight = 0;
+ let againstWeight = 0;
+ for (const vote of votes) {
+ const voteWeight = Number(vote.weight) || 0;
+ if (vote.direction === VoteDirection.FOR) {
+ forWeight += voteWeight;
+ } else {
+ againstWeight += voteWeight;
+ }
+ }
+
+ return {
+ proposalOnChainId: onChainId,
+ tally: {
+ forVotes: votes.filter((vote) => vote.direction === VoteDirection.FOR)
+ .length,
+ againstVotes: votes.filter(
+ (vote) => vote.direction === VoteDirection.AGAINST,
+ ).length,
+ forWeight: String(forWeight),
+ againstWeight: String(againstWeight),
+ totalWeight: String(forWeight + againstWeight),
+ },
+ recentVoters: votes.map((vote) => ({
+ walletAddress: vote.walletAddress,
+ direction: vote.direction,
+ weight: String(vote.weight),
+ votedAt: vote.createdAt.toISOString(),
+ })),
+ };
+ }
}
diff --git a/frontend/app/components/dashboard/ProposalCard.tsx b/frontend/app/components/dashboard/ProposalCard.tsx
index 865c2710d..068f4ae0c 100644
--- a/frontend/app/components/dashboard/ProposalCard.tsx
+++ b/frontend/app/components/dashboard/ProposalCard.tsx
@@ -23,6 +23,17 @@ export default function ProposalCard({
}: ProposalCardProps) {
const safeFor = Math.max(0, Math.min(100, Math.round(forPercent)));
const safeAgainst = Math.max(0, Math.min(100, Math.round(againstPercent)));
+ const normalizedStatus = status.toUpperCase();
+ const isActive = normalizedStatus === "ACTIVE";
+ const isRejected =
+ normalizedStatus === "REJECTED" || normalizedStatus === "FAILED";
+ const statusPillClass = isRejected
+ ? "bg-red-500/15 text-red-300 border-red-400/40"
+ : "bg-[#10314F] border-2 border-[#215091] text-[#60A5FA]";
+ const mobilePillClass = isRejected
+ ? "bg-red-500/15 text-red-300"
+ : "bg-[#08333a] text-sky-200";
+ const ctaLabel = isActive ? "Vote Now" : "View details";
return (
@@ -30,7 +41,9 @@ export default function ProposalCard({
{/* Top row on mobile: id at left, status at right (status shown only on mobile here) */}
{id}
-
+
{status}
@@ -95,7 +108,7 @@ export default function ProposalCard({
{/* Right column for desktop: status + button. Hidden on mobile. */}
-
+
{status}
@@ -105,7 +118,7 @@ export default function ProposalCard({
onClick={onVote}
className="px-5 py-3 rounded-xl bg-linear-to-r from-sky-400 to-cyan-300 text-[#042024] font-semibold shadow-md hover:brightness-110"
>
- Vote Now
+ {ctaLabel}
@@ -116,7 +129,7 @@ export default function ProposalCard({
onClick={onVote}
className="w-full px-5 py-3 rounded-xl bg-linear-to-r from-sky-400 to-cyan-300 text-[#042024] font-semibold shadow-md hover:brightness-110"
>
- Vote Now
+ {ctaLabel}
diff --git a/frontend/app/dashboard/analytics/page.tsx b/frontend/app/dashboard/analytics/page.tsx
index a0992c9ba..5dd07861f 100644
--- a/frontend/app/dashboard/analytics/page.tsx
+++ b/frontend/app/dashboard/analytics/page.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { PieChart } from "lucide-react";
+import { MoreHorizontal, PieChart } from "lucide-react";
export const metadata = { title: "Analytics – Nestera" };
@@ -23,6 +23,87 @@ export default function AnalyticsPage() {
Analytics charts and data will appear here.
+
+
+
+
+
Asset Allocation
+
+
+
+
+
+
+ {[
+ { asset: "USDC", percent: 40, color: "bg-cyan-400" },
+ { asset: "XLM", percent: 35, color: "bg-blue-400" },
+ { asset: "ETH", percent: 25, color: "bg-violet-400" },
+ ].map((item) => (
+
+
+
+ {item.asset}
+
+
{item.percent}%
+
+ ))}
+
+
+
+
+
+
Yield Breakdown
+
+ APY High
+
+
+
+ Earnings from your active pools
+
+
+
+
+ Total Interest Earned
+
+
$743.20
+
+
+
+ {[
+ { label: "XLM Staking", amount: "+$420.50", progress: 57 },
+ { label: "USDC Flexible", amount: "+$322.70", progress: 43 },
+ ].map((pool) => (
+
+
+ {pool.label}
+ {pool.amount}
+
+
+
+ ))}
+
+
+
);
}
diff --git a/frontend/app/dashboard/governance/GovernanceClient.tsx b/frontend/app/dashboard/governance/GovernanceClient.tsx
new file mode 100644
index 000000000..f1ef24a9a
--- /dev/null
+++ b/frontend/app/dashboard/governance/GovernanceClient.tsx
@@ -0,0 +1,220 @@
+"use client";
+
+import { useState } from "react";
+import { ShieldCheck, Users, Vote, FileText, XCircle } from "lucide-react";
+import PassedProposalCard, {
+ type PassedProposal,
+} from "@/app/components/dashboard/PassedProposalCard";
+import ProposalCard from "@/app/components/dashboard/ProposalCard";
+
+export default function GovernanceClient() {
+ const [activeTab, setActiveTab] = useState("Overview");
+ const tabs = [
+ { label: "Overview", icon: FileText },
+ { label: "Active Votes", icon: Vote },
+ { label: "Rejected", icon: XCircle },
+ { label: "Delegations", icon: Users },
+ ];
+
+ const passedProposals: PassedProposal[] = [
+ {
+ id: "p-001",
+ title: "Reduce protocol fees for small deposits",
+ category: "Parameters",
+ passedOn: "Mar 18, 2026",
+ forVotes: 1824,
+ againstVotes: 312,
+ },
+ {
+ id: "p-002",
+ title: "Add USDT (testnet) as a supported stablecoin",
+ category: "Assets",
+ passedOn: "Feb 27, 2026",
+ forVotes: 1490,
+ againstVotes: 410,
+ },
+ {
+ id: "p-003",
+ title: "Increase timelock delay to 24 hours",
+ category: "Security",
+ passedOn: "Jan 30, 2026",
+ forVotes: 2055,
+ againstVotes: 155,
+ },
+ ];
+ const activeProposals = [
+ {
+ id: "NIP-4",
+ title: "Increase USDC Base Yield to 14%",
+ categories: ["Finance"],
+ countdownText: "Ends in 2 days",
+ forPercent: 75,
+ againstPercent: 25,
+ status: "ACTIVE",
+ },
+ {
+ id: "NIP-12",
+ title: "Add new ecosystem grants program",
+ categories: ["Ecosystem", "Finance"],
+ countdownText: "Ends in 6 days",
+ forPercent: 52,
+ againstPercent: 48,
+ status: "ACTIVE",
+ },
+ ];
+ const rejectedProposals = [
+ {
+ id: "NIP-1",
+ title: "Increase Treasury Risk Exposure",
+ categories: ["Treasury"],
+ countdownText: "Ended Mar 10, 2026",
+ forPercent: 34,
+ againstPercent: 66,
+ status: "REJECTED",
+ },
+ {
+ id: "NIP-6",
+ title: "Remove XLM Staking Incentives",
+ categories: ["Staking"],
+ countdownText: "Ended Feb 19, 2026",
+ forPercent: 41,
+ againstPercent: 59,
+ status: "REJECTED",
+ },
+ ];
+
+ return (
+
+
+
+
+
+
+
+
Governance
+
+ Vote on proposals and protocol decisions
+
+
+
+
+
+
+
+ Voting Power
+
+
+ Connected
+
+
+
+ 12,480
+
+
+ NSTR delegated to your wallet
+
+
+
+
+
+
+
+ {tabs.map((tab) => {
+ const TabIcon = tab.icon;
+ return (
+
+ );
+ })}
+
+
+
+
+ {activeTab === "Overview" && (
+ <>
+
+
+
+
+ >
+ )}
+
+ {(activeTab === "Overview" || activeTab === "Active Votes") && (
+
+
+
+
+ {activeProposals.map((proposal) => (
+
+ ))}
+
+
+ )}
+
+ {activeTab === "Rejected" && (
+
+
+
+
+ {rejectedProposals.map((proposal) => (
+
+ ))}
+
+
+ )}
+
+ {activeTab === "Overview" && (
+
+ {passedProposals.map((proposal) => (
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/frontend/app/dashboard/governance/page.tsx b/frontend/app/dashboard/governance/page.tsx
index 1e3e8e94a..8ca5cbebc 100644
--- a/frontend/app/dashboard/governance/page.tsx
+++ b/frontend/app/dashboard/governance/page.tsx
@@ -1,166 +1,8 @@
-import { ShieldCheck, Users, Vote, FileText } from "lucide-react";
-import PassedProposalCard, {
- type PassedProposal,
-} from "@/app/components/dashboard/PassedProposalCard";
-import ProposalCard from "@/app/components/dashboard/ProposalCard";
+import React from "react";
+import GovernanceClient from "./GovernanceClient";
export const metadata = { title: "Governance – Nestera" };
export default function GovernancePage() {
- const tabs = [
- { label: "Overview", icon: FileText, active: true },
- { label: "Active Votes", icon: Vote, active: false },
- { label: "Delegations", icon: Users, active: false },
- ];
-
- const passedProposals: PassedProposal[] = [
- {
- id: "p-001",
- title: "Reduce protocol fees for small deposits",
- category: "Parameters",
- passedOn: "Mar 18, 2026",
- forVotes: 1824,
- againstVotes: 312,
- },
- {
- id: "p-002",
- title: "Add USDT (testnet) as a supported stablecoin",
- category: "Assets",
- passedOn: "Feb 27, 2026",
- forVotes: 1490,
- againstVotes: 410,
- },
- {
- id: "p-003",
- title: "Increase timelock delay to 24 hours",
- category: "Security",
- passedOn: "Jan 30, 2026",
- forVotes: 2055,
- againstVotes: 155,
- },
- ];
-
- return (
-
-
-
-
-
-
-
-
Governance
-
- Vote on proposals and protocol decisions
-
-
-
-
-
-
-
- Voting Power
-
-
- Connected
-
-
-
- 12,480
-
-
- NSTR delegated to your wallet
-
-
-
-
-
-
-
- {tabs.map((tab) => {
- const TabIcon = tab.icon;
- return (
-
- );
- })}
-
-
-
-
-
-
-
-
-
- {/* Active proposals list (uses ProposalCard) */}
-
-
-
-
- {/* Example active proposals - replace with API data mapping in real app */}
-
-
-
-
-
-
-
- {passedProposals.map((proposal) => (
-
- ))}
-
-
-
- );
+ return ;
}
diff --git a/frontend/app/savings/page.tsx b/frontend/app/savings/page.tsx
index 86f3c8a23..90b145e40 100644
--- a/frontend/app/savings/page.tsx
+++ b/frontend/app/savings/page.tsx
@@ -4,6 +4,9 @@ import React from "react";
import Link from "next/link";
import {
LayoutGrid,
+ List,
+ Search,
+ ChevronDown,
CheckCircle2,
Trophy,
Banknote,
@@ -18,6 +21,89 @@ import GoalCard, { GoalStatus } from "./components/GoalCard";
// export const metadata = { title: "Goal-Based Savings - Nestera" };
export default function GoalBasedSavingsPage() {
+ const [searchQuery, setSearchQuery] = React.useState("");
+ const [statusFilter, setStatusFilter] = React.useState("All");
+ const [sortBy, setSortBy] = React.useState("Progress");
+ const [viewMode, setViewMode] = React.useState<"grid" | "list">("grid");
+ const goals = [
+ {
+ id: "1",
+ icon: ,
+ title: "Emergency Fund",
+ status: "active" as GoalStatus,
+ targetAmount: "$12,000",
+ currentSaved: "$6,400",
+ remainingAmount: "$5,600",
+ progressPercent: 53,
+ scheduleLabel: "By Dec 20, 2026",
+ contributionFrequency: "Weekly",
+ nextContributionLabel: "Next contribution",
+ nextContributionValue: "$150 on Jun 28",
+ },
+ {
+ id: "2",
+ icon: ,
+ title: "Down Payment",
+ status: "near-deadline" as GoalStatus,
+ targetAmount: "$40,000",
+ currentSaved: "$28,000",
+ remainingAmount: "$12,000",
+ progressPercent: 70,
+ scheduleLabel: "Due Oct 03, 2026",
+ contributionFrequency: "Monthly",
+ nextContributionLabel: "Next contribution",
+ nextContributionValue: "$1,000 on Jul 01",
+ },
+ {
+ id: "3",
+ icon: ,
+ title: "Summer Trip",
+ status: "behind-schedule" as GoalStatus,
+ targetAmount: "$8,000",
+ currentSaved: "$3,100",
+ remainingAmount: "$4,900",
+ progressPercent: 39,
+ scheduleLabel: "By Aug 15, 2026",
+ contributionFrequency: "Every other week",
+ nextContributionLabel: "Next contribution",
+ nextContributionValue: "$250 on Jul 05",
+ },
+ {
+ id: "4",
+ icon: ,
+ title: "New Laptop",
+ status: "paused" as GoalStatus,
+ targetAmount: "$2,500",
+ currentSaved: "$1,500",
+ remainingAmount: "$1,000",
+ progressPercent: 60,
+ scheduleLabel: "Paused until decision",
+ contributionFrequency: "Paused",
+ nextContributionLabel: "Next contribution",
+ nextContributionValue: "N/A",
+ },
+ ];
+ const featuredGoal = goals[1];
+ const filteredGoals = React.useMemo(() => {
+ const query = searchQuery.trim().toLowerCase();
+ let filtered = goals.filter((goal) => {
+ const matchesSearch =
+ !query ||
+ goal.title.toLowerCase().includes(query) ||
+ goal.contributionFrequency.toLowerCase().includes(query);
+ const matchesStatus = statusFilter === "All" || goal.status === statusFilter;
+ return matchesSearch && matchesStatus;
+ });
+
+ filtered = filtered.sort((a, b) =>
+ sortBy === "Target"
+ ? parseInt(b.targetAmount.replace(/[$,]/g, ""), 10) -
+ parseInt(a.targetAmount.replace(/[$,]/g, ""), 10)
+ : b.progressPercent - a.progressPercent,
+ );
+ return filtered;
+ }, [goals, searchQuery, sortBy, statusFilter]);
+
return (
{/* Header Band */}
@@ -101,67 +187,102 @@ export default function GoalBasedSavingsPage() {
{/* Goal cards grid */}
+
+
+
Featured Goal
+
+ Focus
+
+
+
console.log("Add funds", featuredGoal.id)}
+ onViewDetails={() => console.log("View details", featuredGoal.id)}
+ onOverflowAction={() => console.log("More actions", featuredGoal.id)}
+ />
+
+
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Search goals..."
+ className="w-full bg-[#0e2330] border border-white/5 rounded-xl py-3 pl-12 pr-4 text-white placeholder:text-[#4e7a86] focus:outline-hidden focus:border-cyan-500/50 transition-colors"
+ />
+
+
+
+
+
+
+
+
+
+
+
Your Savings Goals
-
- {[
- {
- id: '1',
- icon:
,
- title: 'Emergency Fund',
- status: 'active' as GoalStatus,
- targetAmount: '$12,000',
- currentSaved: '$6,400',
- remainingAmount: '$5,600',
- progressPercent: 53,
- scheduleLabel: 'By Dec 20, 2026',
- contributionFrequency: 'Weekly',
- nextContributionLabel: 'Next contribution',
- nextContributionValue: '$150 on Jun 28',
- },
- {
- id: '2',
- icon:
,
- title: 'Down Payment',
- status: 'near-deadline' as GoalStatus,
- targetAmount: '$40,000',
- currentSaved: '$28,000',
- remainingAmount: '$12,000',
- progressPercent: 70,
- scheduleLabel: 'Due Oct 03, 2026',
- contributionFrequency: 'Monthly',
- nextContributionLabel: 'Next contribution',
- nextContributionValue: '$1,000 on Jul 01',
- },
- {
- id: '3',
- icon:
,
- title: 'Summer Trip',
- status: 'behind-schedule' as GoalStatus,
- targetAmount: '$8,000',
- currentSaved: '$3,100',
- remainingAmount: '$4,900',
- progressPercent: 39,
- scheduleLabel: 'By Aug 15, 2026',
- contributionFrequency: 'Every other week',
- nextContributionLabel: 'Next contribution',
- nextContributionValue: '$250 on Jul 05',
- },
- {
- id: '4',
- icon:
,
- title: 'New Laptop',
- status: 'paused' as GoalStatus,
- targetAmount: '$2,500',
- currentSaved: '$1,500',
- remainingAmount: '$1,000',
- progressPercent: 60,
- scheduleLabel: 'Paused until decision',
- contributionFrequency: 'Paused',
- nextContributionLabel: 'Next contribution',
- nextContributionValue: 'N/A',
- },
- ].map((goal) => (
+
+ {filteredGoals.map((goal) => (
console.log('Add funds', goal.id)}
- onViewDetails={() => console.log('View details', goal.id)}
- onOverflowAction={() => console.log('More actions', goal.id)}
+ onAddFunds={() => console.log("Add funds", goal.id)}
+ onViewDetails={() => console.log("View details", goal.id)}
+ onOverflowAction={() => console.log("More actions", goal.id)}
/>
))}