Skip to content

feat(dapp): add savings goals list UI and cards; wire deposit & archive#749

Open
EDOHWARES wants to merge 3 commits into
Suncrest-Labs:mainfrom
EDOHWARES:feat/dapp/savings-goals-list
Open

feat(dapp): add savings goals list UI and cards; wire deposit & archive#749
EDOHWARES wants to merge 3 commits into
Suncrest-Labs:mainfrom
EDOHWARES:feat/dapp/savings-goals-list

Conversation

@EDOHWARES

Copy link
Copy Markdown

feat(dapp): build savings goals list page — fetch and render user goals from API

Summary

  • Add a full My Goals list UI inside the Savings page that fetches user goals from the backend and provides goal-level actions (Deposit, Archive, Edit placeholder).
  • Use the existing React Query hook useSavingsGoals() to fetch /api/v1/users/savings-goals.
  • Implement client-side filter chips (All | Active | Completed | Archived).
  • Provide loading, empty, error, and wallet-not-connected states.
  • Wire the Deposit CTA on each goal to open the existing DepositModal pre-filled for the goal's linked vault.
  • Implement archive behavior that calls DELETE /api/v1/users/savings-goals/{id} and refreshes the list.
  • Add and adjust unit tests to cover list rendering, empty state, and filter behavior.

What I changed

New component

SavingsGoalCard.tsx

Presentational card for a savings goal displaying:

  • Goal icon
  • Deadline badge
  • Progress bar
  • Saved amount and target amount
  • Category
  • Actions:
    • Edit
    • Deposit
    • Archive

Updated component

SavingsGoalsSection.tsx

Enhancements include:

  • Integration with SavingsGoalCard
  • Client-side filter chips
  • Filtering logic
  • Data fetching using useSavingsGoals()
  • useMutation integration for archive requests
  • Query invalidation after successful archive

Implemented UI states:

  • Skeleton loaders while fetching
  • Empty state with CTA
  • Inline error message with retry button
  • Wallet-not-connected prompt

Exposed callbacks:

onCreateGoal?: () => void
onDepositGoal?: (goal) => void

The parent component remains responsible for opening DepositModal.

Tests

Updated and added Vitest tests to verify:

  • Wallet connection prompt appears when disconnected
  • Goals render correctly when data is available
  • Empty state appears when no goals exist
  • Filter chips correctly update visible goals

Files touched

  • SavingsGoalCard.tsx (new)
  • SavingsGoalsSection.tsx (modified)
  • savings-goals-section.test.tsx (updated)

Behavior / Acceptance Criteria Mapping

"My Goals" section renders a list of user goals

  • ✅ Implemented in SavingsGoalsSection

Goals fetched from GET /api/v1/users/savings-goals

  • ✅ Retrieved through useSavingsGoals()

Loading, empty, error, and not-connected states

Implemented with:

  • ✅ Skeleton loaders
  • ✅ Empty-state CTA
  • ✅ Retry button
  • ✅ Wallet connection prompt

Filter chips filter the list

  • ✅ Implemented using client-side filtering

Deposit opens DepositModal pre-filled

  • SavingsGoalCard exposes onDeposit
  • ✅ Parent component wires the callback into the existing DepositModal

Archive action calls DELETE and removes the goal

  • ✅ Implemented with a React Query mutation
  • ✅ Invalidates the savings-goals query after success

Tests

  • ✅ Unit tests added and updated to validate:
    • Rendering
    • Empty state
    • Filter behavior

Notes & follow-ups

  • The Edit action is currently a placeholder and serves as scaffolding for future goal-editing functionality.
  • Deposit functionality relies on the application's existing DepositModal implementation.
  • If the backend exposes an archived field, the Archived filter uses it directly.
  • Otherwise, archive behavior may remain client-side until backend support is added.

Testing instructions

Run unit tests:

yarn
yarn test

Or use the repository's standard test command.

Manual verification

Visit the /savings page and verify:

  • Disconnecting the wallet shows the connection prompt.
  • Connecting the wallet displays savings goals.
  • Filter chips correctly change the visible list.
  • Clicking Deposit opens the existing modal.
  • Clicking Archive removes the card after a successful API response.

Possible follow-ups

  • Implement full goal editing support.
  • Add integration and end-to-end tests for archive and deposit flows.
  • Improve optimistic updates for archiving goals.

Closes #699
Closes #698
Closes #695
Closes #700

Copilot AI review requested due to automatic review settings June 26, 2026 10:24
@EDOHWARES EDOHWARES requested a review from 0xDeon as a code owner June 26, 2026 10:24
@drips-wave

drips-wave Bot commented Jun 26, 2026

Copy link
Copy Markdown

@EDOHWARES Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a “My Goals” view to the Savings page that fetches and renders savings goals from the API, introduces a dedicated SavingsGoalCard UI component, and wires goal-level actions (deposit + archive) into existing page state.

Changes:

  • Refactors the goals list to render via a new SavingsGoalCard component and adds client-side filter chips.
  • Adds archive support via a React Query mutation + query invalidation, and exposes a deposit callback so the page can open the existing DepositModal.
  • Updates the Savings page to toggle between Products and My Goals views.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.

File Description
apps/dapp/frontend/components/savings/SavingsGoalsSection.tsx Adds filter chips, goal card rendering, and archive mutation/invalidation wiring.
apps/dapp/frontend/components/savings/SavingsGoalCard.tsx New presentational goal card with progress + deadline display and action buttons.
apps/dapp/frontend/app/savings/page.tsx Introduces Products/My Goals view toggle and wires goal deposit selection into existing modal state.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/dapp/frontend/components/savings/SavingsGoalsSection.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalsSection.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalsSection.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalsSection.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalsSection.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalCard.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalCard.tsx
Comment thread apps/dapp/frontend/app/savings/page.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalsSection.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalCard.tsx
Copilot AI review requested due to automatic review settings June 26, 2026 10:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

Comment thread apps/dapp/frontend/components/savings/SavingsGoalsSection.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalsSection.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalsSection.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalCard.tsx
Comment thread apps/dapp/frontend/components/savings/SavingsGoalCard.tsx
Comment thread apps/dapp/frontend/app/savings/page.tsx
Comment thread apps/dapp/frontend/app/savings/page.tsx
Comment thread apps/dapp/frontend/app/savings/page.tsx
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 26, 2026 11:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 12 comments.

Comment on lines 3 to 7
import { format } from "date-fns";
import {
Briefcase,
GraduationCap,
Heart,
Home,
Plane,
Shield,
Target,
type LucideIcon,
} from "lucide-react";
Comment on lines 17 to +21
const CATEGORY_ICONS: Record<string, LucideIcon> = {
emergency_fund: Shield,
education: GraduationCap,
housing: Home,
travel: Plane,
business: Briefcase,
health: Heart,
emergency_fund: Target,
education: Target,
housing: Target,
travel: Target,
Comment on lines +66 to +86
const [filter, setFilter] = useState<"all" | "active" | "completed" | "archived">("all");

const queryClient = useQueryClient();
const archiveMutation = useMutation({
mutationFn: async (id: string) => {
const res = await savingsGoals.delete(id);
if (!res.ok) throw new Error("Failed to archive savings goal");
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["savings-goals"] });
},
});

const filteredGoals = useMemo(() => {
if (!goals) return [];
if (filter === "all") return goals;
if (filter === "active") return goals.filter((g) => (g.progress_pct ?? 0) < 100);
if (filter === "completed") return goals.filter((g) => (g.progress_pct ?? 0) >= 100);
if (filter === "archived") return goals.filter((g: any) => !!g.archived);
return goals;
}, [goals, filter]);
Comment on lines +110 to +115
{[
{ key: "all", label: "All" },
{ key: "active", label: "Active" },
{ key: "completed", label: "Completed" },
{ key: "archived", label: "Archived" },
].map((c) => (
Comment on lines +79 to +86
const filteredGoals = useMemo(() => {
if (!goals) return [];
if (filter === "all") return goals;
if (filter === "active") return goals.filter((g) => (g.progress_pct ?? 0) < 100);
if (filter === "completed") return goals.filter((g) => (g.progress_pct ?? 0) >= 100);
if (filter === "archived") return goals.filter((g: any) => !!g.archived);
return goals;
}, [goals, filter]);
Comment on lines +67 to +82
<div className="flex gap-2">
<button
type="button"
onClick={() => onDeposit(goal)}
className="flex-1 rounded-xl border border-black/10 px-3 py-2 text-sm font-semibold text-black hover:bg-black/5"
>
Deposit
</button>
<button
type="button"
onClick={() => onArchive(goal.id)}
className="rounded-xl bg-black px-3 py-2 text-sm font-semibold text-white hover:opacity-90"
>
Archive
</button>
</div>
Comment on lines +80 to +84
onSuccess: (goal: any) => {
queryClient.invalidateQueries(["savings-goals"]);
addNotification({ title: "Goal created", message: `Goal '${goal.description ?? goal.id}' created!`, type: "info" }, { showToast: true });
onClose();
},
Comment on lines +65 to +72
const [step, setStep] = useState<1 | 2>(1);
const [name, setName] = useState("");
const [category, setCategory] = useState("other");
const [target, setTarget] = useState("");
const [currency, setCurrency] = useState<"USDC" | "XLM">("USDC");
const [deadline, setDeadline] = useState("");
const [description, setDescription] = useState("");
const [vaultId, setVaultId] = useState<string | undefined>(undefined);
Comment on lines +166 to +174
createMutation.mutate({
description: name,
category,
target_amount: parseFloat(target),
currency,
deadline: new Date(deadline).toISOString(),
vault_id: vaultId,
// description field used as name until API name added
});
Comment on lines +990 to +1014
<div id="vault-grid" className="mb-6 flex gap-1.5 border-b border-black/8 pb-px overflow-x-auto scrollbar-hide" role="tablist" aria-label="Savings plan filters">
{FILTERS.map((f) => (
<button
key={f.value}
role="tab"
aria-selected={filter === f.value}
onClick={() => setFilter(f.value)}
className={cn(
"relative pb-3 px-1 mr-4 text-sm whitespace-nowrap transition-colors shrink-0 focus-visible:ring-2 focus-visible:ring-black focus-visible:ring-offset-2",
filter === f.value
? "text-black font-semibold"
: "text-black/60 hover:text-black/80 font-medium"
)}
>
{f.label}
{filter === f.value && (
<motion.div
layoutId="savings-tab"
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black rounded-full"
aria-hidden="true"
/>
)}
</button>
))}
</div>

@0xDeon 0xDeon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI blocking merge: Dapp Frontend (Next.js) build failed. Fix the frontend build errors before merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants