diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..957cd15 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals"] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 550eba4..eb81ce4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: with: node-version: "20" cache: "npm" - - run: npm ci + - run: npm install --ignore-scripts - run: npm run type-check - run: npm run lint - run: npm run build diff --git a/next.config.ts b/next.config.mjs similarity index 86% rename from next.config.ts rename to next.config.mjs index f0a914d..80d5d21 100644 --- a/next.config.ts +++ b/next.config.mjs @@ -1,6 +1,5 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { +/** @type {import("next").NextConfig} */ +const nextConfig = { reactStrictMode: true, env: { NEXT_PUBLIC_STELLAR_NETWORK: process.env.NEXT_PUBLIC_STELLAR_NETWORK || "testnet", diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c3549f3 Binary files /dev/null and b/package-lock.json differ diff --git a/package.json b/package.json index 87c6767..f207c68 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "@radix-ui/react-dropdown-menu": "^2.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.2.0", - "@radix-ui/react-badge": "^1.0.0", "class-variance-authority": "^0.7.0" }, "devDependencies": { diff --git a/src/components/dashboard/recent-activity.tsx b/src/components/dashboard/recent-activity.tsx index f3ca9a0..0359b55 100644 --- a/src/components/dashboard/recent-activity.tsx +++ b/src/components/dashboard/recent-activity.tsx @@ -1,5 +1,178 @@ "use client"; -// TODO: Implement recent-activity component + +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { formatDistanceToNow } from "date-fns"; +import { CheckCheck, Wallet, DollarSign, CreditCard, ThumbsUp, Send } from "lucide-react"; +import { clsx } from "clsx"; +import { useWallet } from "@/hooks/use-wallet"; +import type { Notification } from "@/types"; + +// TODO: Replace with real API responses once backend is ready +const MOCK_NOTIFICATIONS: Notification[] = [ + { + id: "mock-1", + recipient: "", + type: "contribution", + description: "Adaeze contributed 50 USDC to Eko Savings", + read: false, + createdAt: new Date(Date.now() - 1000 * 60 * 5).toISOString(), + }, + { + id: "mock-2", + recipient: "", + type: "loan", + description: "Chidi applied for a 200 USDC loan", + read: true, + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(), + }, + { + id: "mock-3", + recipient: "", + type: "vote", + description: "Voting ended for Proposal #12", + read: false, + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), + }, + { + id: "mock-4", + recipient: "", + type: "distribution", + description: "50 USDC distributed to your account", + read: true, + createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48).toISOString(), + }, +]; + +async function fetchNotifications(recipient: string): Promise { + // TODO: connect to API + if (!recipient) return MOCK_NOTIFICATIONS; + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/notifications?recipient=${encodeURIComponent(recipient)}` + ); + if (!res.ok) throw new Error("Failed to fetch notifications"); + const data: unknown = await res.json(); + if (!Array.isArray(data) || data.length === 0) return []; + return data as Notification[]; + } catch { + // TODO: connect to API — returning mock data on error + return MOCK_NOTIFICATIONS; + } +} + +async function markNotificationAsRead(id: string): Promise { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/api/notifications/${encodeURIComponent(id)}/read`, + { method: "PATCH" } + ); + if (!res.ok) throw new Error("Failed to mark notification as read"); +} + +const ACTIVITY_CONFIG: Record = { + contribution: { icon: DollarSign, color: "text-green-600", bg: "bg-green-50" }, + loan: { icon: CreditCard, color: "text-amber-600", bg: "bg-amber-50" }, + vote: { icon: ThumbsUp, color: "text-blue-600", bg: "bg-blue-50" }, + distribution: { icon: Send, color: "text-purple-600", bg: "bg-purple-50" }, +}; + export function RecentActivity() { - return
recent-activity
; + const { address, connect } = useWallet(); + const queryClient = useQueryClient(); + + const { data = [], isLoading } = useQuery({ + queryKey: ["notifications", address], + queryFn: () => fetchNotifications(address ?? ""), + enabled: !!address, + staleTime: 30_000, + }); + + const mutation = useMutation({ + mutationFn: markNotificationAsRead, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["notifications"] }); + }, + }); + + const hasUnread = data.some((n) => !n.read); + + if (!address) { + return ( +
+ +

Connect your wallet to see activity

+ +
+ ); + } + + if (isLoading) { + return ( +
+
+ {[...Array(4)].map((_, i) => ( +
+
+
+
+
+
+
+ ))} +
+ ); + } + + return ( +
+
+

Recent Activity

+ {hasUnread && ( + + )} +
+
+ {data.length === 0 ? ( +
+

No recent activity

+
+ ) : ( +
+ {data.map((notification) => { + const config = ACTIVITY_CONFIG[notification.type] || ACTIVITY_CONFIG.contribution; + const Icon = config.icon; + return ( +
+
+ +
+
+

{notification.description}

+

+ {formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true })} +

+
+
+ ); + })} +
+ )} +
+
+ ); } diff --git a/src/hooks/use-wallet.ts b/src/hooks/use-wallet.ts index f8530fe..4ab248c 100644 --- a/src/hooks/use-wallet.ts +++ b/src/hooks/use-wallet.ts @@ -5,14 +5,13 @@ import { StellarWalletsKit, WalletNetwork, FREIGHTER_ID, - LOBSTR_ID, - xBullWalletId, + allowAllModules, } from "@creit.tech/stellar-wallets-kit"; const kit = new StellarWalletsKit({ network: WalletNetwork.TESTNET, selectedWalletId: FREIGHTER_ID, - wallets: [FREIGHTER_ID, LOBSTR_ID, xBullWalletId], + modules: allowAllModules(), }); export function useWallet() { diff --git a/src/lib/stellar.ts b/src/lib/stellar.ts index b05b840..a30e038 100644 --- a/src/lib/stellar.ts +++ b/src/lib/stellar.ts @@ -25,7 +25,7 @@ export const SOROBAN_RPC_URL = export const server = new SorobanRpc.Server(SOROBAN_RPC_URL); export const networkPassphrase = - STELLAR_NETWORK === "MAINNET" + STELLAR_NETWORK === "PUBLIC" ? Networks.PUBLIC : Networks.TESTNET; diff --git a/src/types/index.ts b/src/types/index.ts index 2791456..1392612 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -86,6 +86,15 @@ export interface ContractAddresses { dividend: string; } +export interface Notification { + id: string; + recipient: string; + type: "contribution" | "loan" | "vote" | "distribution"; + description: string; + read: boolean; + createdAt: string; +} + export interface DashboardStats { totalGroups: number; totalMembers: number;