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"
}
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
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
170 changes: 97 additions & 73 deletions src/components/dashboard/treasury-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,24 @@ const MOCK_DATA: ChartDataPoint[] = [
{ period: "C8", totalContributions: 850_000_000_000, loansOutstanding: 80_000_000_000 },
];

async function fetchContributionData(): Promise<ChartDataPoint[]> {
async function fetchContributionData(groupId?: string): Promise<ChartDataPoint[]> {
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/contributions`
);
let url = `${process.env.NEXT_PUBLIC_API_URL}/api/contributions`;

if (groupId) {
url = `${process.env.NEXT_PUBLIC_API_URL}/api/groups/${groupId}/contributions`;
} else {
const groupsRes = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/groups`);
if (groupsRes.ok) {
const groups = await groupsRes.json();
if (Array.isArray(groups) && groups.length > 0) {
const firstGroupId = groups[0].id;
url = `${process.env.NEXT_PUBLIC_API_URL}/api/groups/${firstGroupId}/contributions`;
}
}
}

const res = await fetch(url);
if (!res.ok) throw new Error("Failed to fetch contributions");
const data: unknown = await res.json();
if (!Array.isArray(data) || data.length === 0) throw new Error("Empty contributions");
Expand All @@ -44,23 +57,29 @@ async function fetchContributionData(): Promise<ChartDataPoint[]> {
}
}

export function TreasuryChart() {
const { data = MOCK_DATA, isLoading } = useQuery({
queryKey: ["treasury-chart"],
queryFn: fetchContributionData,
interface TreasuryChartProps {
groupId?: string;
}

export function TreasuryChart({ groupId }: TreasuryChartProps) {
const { data = MOCK_DATA, isLoading } = useQuery<ChartDataPoint[]>({
queryKey: ["treasury-chart", groupId],
queryFn: () => fetchContributionData(groupId),
refetchInterval: 60_000,
staleTime: 30_000,
});

const hasData = data.length > 0;
const hasData = data && data.length > 0;

return (
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="text-base font-semibold text-gray-900 mb-4">
Treasury Overview
</h3>
<div className="flex items-center justify-between mb-4">
<h3 className="text-base font-semibold text-gray-900">
Treasury Overview
</h3>
</div>
{isLoading ? (
<div className="h-64 space-y-3">
<div className="h-64 space-y-3 flex flex-col justify-between py-2">
<div className="h-4 w-32 bg-gray-100 animate-pulse rounded" />
<div className="h-48 w-full bg-gray-100 animate-pulse rounded" />
</div>
Expand All @@ -70,70 +89,75 @@ export function TreasuryChart() {
<p className="text-sm text-gray-500 font-medium mb-1">
No contribution data yet
</p>
<p className="text-xs text-gray-400 max-w-[220px]">
<p className="text-xs text-gray-400 max-w-[220px] mb-4">
Start contributing to your cooperative to see treasury growth here.
</p>
<button className="bg-brand-600 text-white px-4 py-2 rounded-lg text-sm hover:bg-brand-700 transition-colors">
Make Contribution
</button>
</div>
) : (
<ResponsiveContainer width="100%" height={220}>
<AreaChart
data={data}
margin={{ top: 5, right: 10, left: -10, bottom: 0 }}
>
<defs>
<linearGradient id="colorContributions" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#16a34a" stopOpacity={0.15} />
<stop offset="95%" stopColor="#16a34a" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorLoans" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#d97706" stopOpacity={0.15} />
<stop offset="95%" stopColor="#d97706" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" vertical={false} />
<XAxis
dataKey="period"
tick={{ fontSize: 12, fill: "#9ca3af" }}
axisLine={false}
tickLine={false}
/>
<YAxis
tick={{ fontSize: 12, fill: "#9ca3af" }}
axisLine={false}
tickLine={false}
tickFormatter={(v: number) => `$${formatAmount(v)}`}
/>
<Tooltip
contentStyle={{
backgroundColor: "#fff",
border: "1px solid #e5e7eb",
borderRadius: "0.5rem",
boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
fontSize: "0.875rem",
}}
formatter={(value: number, name: string) => [
`$${formatAmount(value)}`,
name,
]}
/>
<Area
type="monotone"
dataKey="totalContributions"
name="Total Contributions"
stroke="#16a34a"
strokeWidth={2}
fill="url(#colorContributions)"
/>
<Area
type="monotone"
dataKey="loansOutstanding"
name="Loans Outstanding"
stroke="#d97706"
strokeWidth={2}
fill="url(#colorLoans)"
/>
</AreaChart>
</ResponsiveContainer>
<div className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart
data={data}
margin={{ top: 5, right: 10, left: -10, bottom: 0 }}
>
<defs>
<linearGradient id="colorContributions" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#16a34a" stopOpacity={0.15} />
<stop offset="95%" stopColor="#16a34a" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorLoans" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#d97706" stopOpacity={0.15} />
<stop offset="95%" stopColor="#d97706" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" vertical={false} />
<XAxis
dataKey="period"
tick={{ fontSize: 12, fill: "#9ca3af" }}
axisLine={false}
tickLine={false}
/>
<YAxis
tick={{ fontSize: 12, fill: "#9ca3af" }}
axisLine={false}
tickLine={false}
tickFormatter={(v: number) => `$${formatAmount(v)}`}
/>
<Tooltip
contentStyle={{
backgroundColor: "#fff",
border: "1px solid #e5e7eb",
borderRadius: "0.5rem",
boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1)",
fontSize: "0.875rem",
}}
formatter={(value: number, name: string) => [
`$${formatAmount(value)}`,
name,
]}
/>
<Area
type="monotone"
dataKey="totalContributions"
name="Total Contributions"
stroke="#16a34a"
strokeWidth={2}
fill="url(#colorContributions)"
/>
<Area
type="monotone"
dataKey="loansOutstanding"
name="Loans Outstanding"
stroke="#d97706"
strokeWidth={2}
fill="url(#colorLoans)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
)}
</div>
);
Expand Down
25 changes: 16 additions & 9 deletions src/hooks/use-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,36 @@ import { useState, useCallback } from "react";
import {
StellarWalletsKit,
WalletNetwork,
allowAllModules,
FREIGHTER_ID,
LOBSTR_ID,
xBullWalletId,
} from "@creit.tech/stellar-wallets-kit";

const kit = new StellarWalletsKit({
network: WalletNetwork.TESTNET,
selectedWalletId: FREIGHTER_ID,
wallets: [FREIGHTER_ID, LOBSTR_ID, xBullWalletId],
});
let kit: StellarWalletsKit | null = null;
if (typeof window !== "undefined") {
kit = new StellarWalletsKit({
network: WalletNetwork.TESTNET,
selectedWalletId: FREIGHTER_ID,
modules: allowAllModules(),
});
}

export function useWallet() {
const [address, setAddress] = useState<string | null>(null);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<string | null>(null);

const connect = useCallback(async () => {
if (!kit) {
setError("Wallet kit is not initialized");
return;
}
setIsConnecting(true);
setError(null);
try {
await kit.openModal({
onWalletSelected: async (option) => {
kit.setWallet(option.id);
const { address: addr } = await kit.getAddress();
kit!.setWallet(option.id);
const { address: addr } = await kit!.getAddress();
setAddress(addr);
},
});
Expand All @@ -44,6 +50,7 @@ export function useWallet() {

const signTransaction = useCallback(async (xdr: string) => {
if (!address) throw new Error("Wallet not connected");
if (!kit) throw new Error("Wallet kit is not initialized");
const { signedTxXdr } = await kit.signTransaction(xdr, {
address,
networkPassphrase: WalletNetwork.TESTNET,
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