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
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals"]
}
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions next.config.ts → next.config.mjs
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Binary file added package-lock.json
Binary file not shown.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
177 changes: 175 additions & 2 deletions src/components/dashboard/recent-activity.tsx
Original file line number Diff line number Diff line change
@@ -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<Notification[]> {
// 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<void> {
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<string, { icon: typeof DollarSign; color: string; bg: string }> = {
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 <div className="bg-white rounded-xl border border-gray-200 p-5 h-64 flex items-center justify-center text-gray-400 text-sm">recent-activity</div>;
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 (
<div className="bg-white rounded-xl border border-gray-200 p-5 h-64 flex flex-col items-center justify-center text-center gap-3">
<Wallet className="w-8 h-8 text-gray-300" />
<p className="text-sm text-gray-500">Connect your wallet to see activity</p>
<button
type="button"
onClick={connect}
className="text-xs font-medium text-brand-600 hover:text-brand-700 underline"
>
Connect Wallet
</button>
</div>
);
}

if (isLoading) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-5 space-y-3">
<div className="h-4 w-24 bg-gray-100 animate-pulse rounded" />
{[...Array(4)].map((_, i) => (
<div key={i} className="flex items-center gap-3">
<div className="w-8 h-8 bg-gray-100 animate-pulse rounded-full" />
<div className="flex-1 space-y-2">
<div className="h-3 bg-gray-100 animate-pulse rounded" />
<div className="h-2 w-16 bg-gray-100 animate-pulse rounded" />
</div>
</div>
))}
</div>
);
}

return (
<div className="bg-white rounded-xl border border-gray-200 p-5 h-64 flex flex-col">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-gray-900">Recent Activity</h3>
{hasUnread && (
<button
type="button"
disabled={mutation.isPending}
onClick={() => {
data.filter((n) => !n.read).forEach((n) => mutation.mutate(n.id));
}}
className="text-xs text-brand-600 hover:text-brand-700 font-medium flex items-center gap-1 disabled:opacity-50"
>
<CheckCheck className="w-3.5 h-3.5" />
Mark all read
</button>
)}
</div>
<div className="flex-1 overflow-y-auto -mx-5 px-5">
{data.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-center">
<p className="text-sm text-gray-400">No recent activity</p>
</div>
) : (
<div className="divide-y divide-gray-100">
{data.map((notification) => {
const config = ACTIVITY_CONFIG[notification.type] || ACTIVITY_CONFIG.contribution;
const Icon = config.icon;
return (
<div key={notification.id} className="flex items-start gap-3 py-3">
<div className={clsx("w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0", config.bg)}>
<Icon className={clsx("w-4 h-4", config.color)} />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-gray-700 leading-snug">{notification.description}</p>
<p className="text-xs text-gray-400 mt-0.5">
{formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true })}
</p>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
);
}
5 changes: 2 additions & 3 deletions src/hooks/use-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/stellar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
9 changes: 9 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading