Skip to content

Commit d4d8bcb

Browse files
committed
added requested features
1 parent a47649b commit d4d8bcb

File tree

11 files changed

+901
-77
lines changed

11 files changed

+901
-77
lines changed

README.md

Lines changed: 196 additions & 48 deletions
Large diffs are not rendered by default.

src/app/balance/page.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { useState, Suspense } from "react";
44
import {
55
Typography, Box, Paper, Grid, Chip, Table, TableBody, TableCell,
66
TableContainer, TableHead, TableRow, TextField, Button, Alert,
7-
CircularProgress,
7+
CircularProgress, Select, MenuItem, FormControl, InputLabel,
88
} from "@mui/material";
99
import { useAccount, useReadContract, useReadContracts } from "wagmi";
1010
import { useSearchParams } from "next/navigation";
1111
import { zeroAddress } from "viem";
12-
import { CONTRACTS, REWARDS_PROGRAM_ABI, MemberRoleLabels } from "@/config/contracts";
13-
import { toBytes12, fromBytes12, fromBytes8, shortenAddress, formatFula, formatContractError } from "@/lib/utils";
14-
import { useProgramCount, useProgram, useTransferToParent, useWithdraw, useApproveToken, useAddTokens } from "@/hooks/useRewardsProgram";
12+
import { CONTRACTS, REWARDS_PROGRAM_ABI, MemberRoleLabels, MemberTypeLabels } from "@/config/contracts";
13+
import { toBytes12, fromBytes12, fromBytes8, shortenAddress, formatFula, formatContractError, fromBytes16 } from "@/lib/utils";
14+
import { useProgramCount, useProgram, useTransferToParent, useWithdraw, useApproveToken, useAddTokens, useRewardTypes } from "@/hooks/useRewardsProgram";
1515
import { OnChainDisclaimer } from "@/components/common/OnChainDisclaimer";
1616
import { QRCodeDisplay } from "@/components/common/QRCodeDisplay";
1717
import { QRScannerButton } from "@/components/common/QRScannerButton";
@@ -49,6 +49,9 @@ function MemberProgramRow({ memberID, programId }: { memberID: string; programId
4949
<Chip label={MemberRoleLabels[Number(member.role)] || "Unknown"} size="small"
5050
color={Number(member.role) === 3 ? "primary" : Number(member.role) === 2 ? "secondary" : "default"} />
5151
</TableCell>
52+
<TableCell>
53+
<Chip label={MemberTypeLabels[Number(member.memberType)] || "Free"} size="small" variant="outlined" />
54+
</TableCell>
5255
<TableCell>{shortenAddress(member.parent)}</TableCell>
5356
<TableCell sx={{ color: "success.main" }}>{balance ? formatFula(balance[0]) : "-"}</TableCell>
5457
<TableCell sx={{ color: "error.main" }}>{balance ? formatFula(balance[1]) : "-"}</TableCell>
@@ -81,8 +84,11 @@ function OwnerActions({ memberWallet }: { memberWallet: string }) {
8184

8285
// Deposit
8386
const [depositAmount, setDepositAmount] = useState("");
87+
const [depositRewardType, setDepositRewardType] = useState(0);
88+
const [depositNote, setDepositNote] = useState("");
8489
const { approve, isPending: isApproving, isConfirming: isAppConf, isSuccess: approveSuccess } = useApproveToken();
8590
const { addTokens, isPending: isDepositing, isConfirming: isDepConf, isSuccess: depositSuccess, error: depositError } = useAddTokens();
91+
const { data: rewardTypesData } = useRewardTypes();
8692

8793
const [disclaimer, setDisclaimer] = useState(false);
8894

@@ -100,12 +106,26 @@ function OwnerActions({ memberWallet }: { memberWallet: string }) {
100106
<Typography variant="subtitle2" gutterBottom>Deposit FULA</Typography>
101107
<TextField label="Amount (FULA)" value={depositAmount} onChange={(e) => setDepositAmount(e.target.value)}
102108
fullWidth size="small" type="number" />
109+
<FormControl fullWidth size="small" sx={{ mt: 1 }}>
110+
<InputLabel>Reward Type</InputLabel>
111+
<Select value={depositRewardType} onChange={(e) => setDepositRewardType(Number(e.target.value))} label="Reward Type">
112+
<MenuItem value={0}>None</MenuItem>
113+
{rewardTypesData && (rewardTypesData as [number[], string[]])[0]?.map((id: number, idx: number) => (
114+
<MenuItem key={id} value={id}>
115+
{fromBytes16((rewardTypesData as [number[], `0x${string}`[]])[1][idx]) || `Type ${id}`}
116+
</MenuItem>
117+
))}
118+
</Select>
119+
</FormControl>
120+
<TextField label="Note (max 128)" value={depositNote}
121+
onChange={(e) => setDepositNote(e.target.value.slice(0, 128))}
122+
fullWidth size="small" sx={{ mt: 1 }} inputProps={{ maxLength: 128 }} />
103123
<Box sx={{ display: "flex", gap: 1, mt: 1 }}>
104124
<Button size="small" variant="outlined" onClick={() => approve(depositAmount)}
105125
disabled={isApproving || isAppConf || !depositAmount || !disclaimer}>
106126
{isApproving || isAppConf ? <CircularProgress size={16} /> : "Approve"}
107127
</Button>
108-
<Button size="small" variant="contained" onClick={() => addTokens(pid, depositAmount)}
128+
<Button size="small" variant="contained" onClick={() => addTokens(pid, depositAmount, depositRewardType, depositNote)}
109129
disabled={isDepositing || isDepConf || !depositAmount || !disclaimer}>
110130
{isDepositing || isDepConf ? <CircularProgress size={16} /> : "Deposit"}
111131
</Button>
@@ -269,6 +289,7 @@ function BalanceContent() {
269289
<TableCell>Program</TableCell>
270290
<TableCell>Code</TableCell>
271291
<TableCell>Role</TableCell>
292+
<TableCell>Type</TableCell>
272293
<TableCell>Parent</TableCell>
273294
<TableCell>Available</TableCell>
274295
<TableCell>Locked</TableCell>

src/app/members/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
ToggleButton, Alert,
88
} from "@mui/material";
99
import { useReadContract } from "wagmi";
10-
import { CONTRACTS, REWARDS_PROGRAM_ABI, MemberRoleLabels } from "@/config/contracts";
10+
import { CONTRACTS, REWARDS_PROGRAM_ABI, MemberRoleLabels, MemberTypeLabels } from "@/config/contracts";
1111
import { toBytes12, toBytes8, fromBytes12, shortenAddress, formatFula } from "@/lib/utils";
1212
import { useProgramCodeToId } from "@/hooks/useRewardsProgram";
1313
import { QRCodeDisplay } from "@/components/common/QRCodeDisplay";
@@ -120,6 +120,7 @@ export default function MembersPage() {
120120
<TableCell>Member ID</TableCell>
121121
<TableCell>Wallet</TableCell>
122122
<TableCell>Role</TableCell>
123+
<TableCell>Type</TableCell>
123124
<TableCell>Program</TableCell>
124125
<TableCell>Parent</TableCell>
125126
<TableCell>Balance (FULA)</TableCell>
@@ -138,6 +139,9 @@ export default function MembersPage() {
138139
<TableCell>
139140
<Chip label={MemberRoleLabels[Number(memberByID.role)]} size="small" />
140141
</TableCell>
142+
<TableCell>
143+
<Chip label={MemberTypeLabels[Number(memberByID.memberType)] || "Free"} size="small" variant="outlined" />
144+
</TableCell>
141145
<TableCell>{memberByID.programId}</TableCell>
142146
<TableCell>{shortenAddress(memberByID.parent)}</TableCell>
143147
<TableCell>

src/app/page.tsx

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,47 @@
11
"use client";
22

3-
import { Typography, Grid, Paper, Box, Alert } from "@mui/material";
3+
import {
4+
Typography, Grid, Paper, Box, Alert, Table, TableBody, TableCell,
5+
TableContainer, TableHead, TableRow, Chip,
6+
} from "@mui/material";
7+
import Link from "next/link";
48
import { useAccount } from "wagmi";
59
import { useUserRole } from "@/hooks/useUserRole";
6-
import { useProgramCount, useTokenBalance } from "@/hooks/useRewardsProgram";
7-
import { formatFula } from "@/lib/utils";
10+
import { useProgramCount, useProgram, useTokenBalance } from "@/hooks/useRewardsProgram";
11+
import { formatFula, fromBytes8 } from "@/lib/utils";
12+
13+
function ProgramSummaryRow({ programId }: { programId: number }) {
14+
const { data: program } = useProgram(programId);
15+
if (!program) return null;
16+
17+
return (
18+
<TableRow hover>
19+
<TableCell>{program.id}</TableCell>
20+
<TableCell>{fromBytes8(program.code as `0x${string}`)}</TableCell>
21+
<TableCell>
22+
<Link href={`/programs?id=${program.id}`} style={{ color: "#6366f1", textDecoration: "none" }}>
23+
{program.name}
24+
</Link>
25+
</TableCell>
26+
<TableCell>
27+
<Chip
28+
label={program.active ? "Active" : "Inactive"}
29+
color={program.active ? "success" : "default"}
30+
size="small"
31+
/>
32+
</TableCell>
33+
</TableRow>
34+
);
35+
}
836

937
export default function Dashboard() {
1038
const { address } = useAccount();
1139
const { isAdmin, isConnected } = useUserRole();
1240
const { data: programCount } = useProgramCount();
1341
const { data: walletBalance } = useTokenBalance(address);
1442

43+
const count = Number(programCount || 0);
44+
1545
return (
1646
<Box>
1747
<Typography variant="h4" gutterBottom>Dashboard</Typography>
@@ -28,11 +58,11 @@ export default function Dashboard() {
2858
</Alert>
2959
)}
3060

31-
<Grid container spacing={3}>
61+
<Grid container spacing={3} sx={{ mb: 3 }}>
3262
<Grid item xs={12} sm={6} md={3}>
3363
<Paper sx={{ p: 3, textAlign: "center" }}>
3464
<Typography color="text.secondary" variant="body2">Total Programs</Typography>
35-
<Typography variant="h3">{programCount?.toString() || "0"}</Typography>
65+
<Typography variant="h3">{count}</Typography>
3666
</Paper>
3767
</Grid>
3868

@@ -56,6 +86,26 @@ export default function Dashboard() {
5686
</>
5787
)}
5888
</Grid>
89+
90+
{count > 0 && (
91+
<TableContainer component={Paper}>
92+
<Table size="small">
93+
<TableHead>
94+
<TableRow>
95+
<TableCell>ID</TableCell>
96+
<TableCell>Code</TableCell>
97+
<TableCell>Name</TableCell>
98+
<TableCell>Status</TableCell>
99+
</TableRow>
100+
</TableHead>
101+
<TableBody>
102+
{Array.from({ length: count }, (_, i) => (
103+
<ProgramSummaryRow key={i + 1} programId={i + 1} />
104+
))}
105+
</TableBody>
106+
</Table>
107+
</TableContainer>
108+
)}
59109
</Box>
60110
);
61111
}

src/app/programs/page.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
useProgramCount, useProgram, useCreateProgram,
1717
useAssignProgramAdmin, useAddMember, useMemberBalance,
1818
} from "@/hooks/useRewardsProgram";
19-
import { CONTRACTS, REWARDS_PROGRAM_ABI, MemberRoleLabels, MemberRoleEnum } from "@/config/contracts";
19+
import { CONTRACTS, REWARDS_PROGRAM_ABI, MemberRoleLabels, MemberRoleEnum, MemberTypeLabels } from "@/config/contracts";
2020
import { fromBytes8, fromBytes12, shortenAddress, formatFula, isValidAddress, formatContractError } from "@/lib/utils";
2121
import { OnChainDisclaimer } from "@/components/common/OnChainDisclaimer";
2222
import { QRCodeDisplay } from "@/components/common/QRCodeDisplay";
@@ -73,6 +73,9 @@ function MemberRow({ programId, wallet }: { programId: number; wallet: `0x${stri
7373
size="small"
7474
/>
7575
</TableCell>
76+
<TableCell>
77+
<Chip label={MemberTypeLabels[Number(member.memberType)] || "Free"} size="small" variant="outlined" />
78+
</TableCell>
7679
<TableCell>{shortenAddress(member.parent)}</TableCell>
7780
<TableCell>
7881
{balance ? `${formatFula(balance[0])} / ${formatFula(balance[1])} / ${formatFula(balance[2])}` : "-"}
@@ -104,7 +107,9 @@ function ProgramDetail({ programId }: { programId: number }) {
104107
const [mWallet, setMWallet] = useState("");
105108
const [mMemberId, setMMemberId] = useState("");
106109
const [mRole, setMRole] = useState(1);
110+
const [paMemberType, setPaMemberType] = useState(0);
107111
const [paDisclaimer, setPaDisclaimer] = useState(false);
112+
const [mMemberType, setMMemberType] = useState(0);
108113
const [mDisclaimer, setMDisclaimer] = useState(false);
109114

110115
const canAddMembers = isAdmin || role === MemberRoleEnum.ProgramAdmin || role === MemberRoleEnum.TeamLeader;
@@ -115,14 +120,14 @@ function ProgramDetail({ programId }: { programId: number }) {
115120
// Reset and close dialogs on success
116121
useEffect(() => {
117122
if (isSuccessPA) {
118-
const t = setTimeout(() => { setOpenPA(false); setPaWallet(""); setPaMemberId(""); setPaDisclaimer(false); }, 1500);
123+
const t = setTimeout(() => { setOpenPA(false); setPaWallet(""); setPaMemberId(""); setPaMemberType(0); setPaDisclaimer(false); }, 1500);
119124
return () => clearTimeout(t);
120125
}
121126
}, [isSuccessPA]);
122127

123128
useEffect(() => {
124129
if (isSuccessM) {
125-
const t = setTimeout(() => { setOpenMember(false); setMWallet(""); setMMemberId(""); setMRole(1); setMDisclaimer(false); }, 1500);
130+
const t = setTimeout(() => { setOpenMember(false); setMWallet(""); setMMemberId(""); setMRole(1); setMMemberType(0); setMDisclaimer(false); }, 1500);
126131
return () => clearTimeout(t);
127132
}
128133
}, [isSuccessM]);
@@ -193,6 +198,7 @@ function ProgramDetail({ programId }: { programId: number }) {
193198
<TableCell>Member ID</TableCell>
194199
<TableCell>Wallet</TableCell>
195200
<TableCell>Role</TableCell>
201+
<TableCell>Type</TableCell>
196202
<TableCell>Parent</TableCell>
197203
<TableCell>Balance (FULA)</TableCell>
198204
<TableCell>Status</TableCell>
@@ -201,7 +207,7 @@ function ProgramDetail({ programId }: { programId: number }) {
201207
</TableHead>
202208
<TableBody>
203209
<TableRow>
204-
<TableCell colSpan={7} align="center">
210+
<TableCell colSpan={8} align="center">
205211
<Typography variant="body2" color="text.secondary">
206212
Search for members on the Members page by their Member ID.
207213
</Typography>
@@ -221,14 +227,22 @@ function ProgramDetail({ programId }: { programId: number }) {
221227
error={!!paWallet && !paWalletValid} helperText={paWallet && !paWalletValid ? "Invalid wallet address" : "Leave empty to create walletless member"} />
222228
<TextField label="Member ID" value={paMemberId} onChange={(e) => setPaMemberId(e.target.value)}
223229
fullWidth margin="normal" inputProps={{ maxLength: 12 }} />
230+
<FormControl fullWidth margin="normal">
231+
<InputLabel>Member Type</InputLabel>
232+
<Select value={paMemberType} onChange={(e) => setPaMemberType(Number(e.target.value))} label="Member Type">
233+
{Object.entries(MemberTypeLabels).map(([k, v]) => (
234+
<MenuItem key={k} value={Number(k)}>{v}</MenuItem>
235+
))}
236+
</Select>
237+
</FormControl>
224238
{errorPA && <Alert severity="error" sx={{ mt: 2 }}>{formatContractError(errorPA)}</Alert>}
225239
{isSuccessPA && <Alert severity="success" sx={{ mt: 2 }}>Program Admin assigned!</Alert>}
226240
<OnChainDisclaimer accepted={paDisclaimer} onChange={setPaDisclaimer} />
227241
</DialogContent>
228242
<DialogActions>
229243
<Button onClick={() => setOpenPA(false)}>Cancel</Button>
230244
<Button variant="contained"
231-
onClick={() => assignProgramAdmin(programId, (paWallet || "0x0000000000000000000000000000000000000000") as `0x${string}`, paMemberId)}
245+
onClick={() => assignProgramAdmin(programId, (paWallet || "0x0000000000000000000000000000000000000000") as `0x${string}`, paMemberId, "0x0000000000000000000000000000000000000000000000000000000000000000" as `0x${string}`, paMemberType)}
232246
disabled={isPendingPA || isConfirmingPA || !paMemberId || !paDisclaimer || (!!paWallet && !paWalletValid)}>
233247
{isPendingPA || isConfirmingPA ? <CircularProgress size={20} /> : "Assign"}
234248
</Button>
@@ -257,14 +271,22 @@ function ProgramDetail({ programId }: { programId: number }) {
257271
)}
258272
</Select>
259273
</FormControl>
274+
<FormControl fullWidth margin="normal">
275+
<InputLabel>Member Type</InputLabel>
276+
<Select value={mMemberType} onChange={(e) => setMMemberType(Number(e.target.value))} label="Member Type">
277+
{Object.entries(MemberTypeLabels).map(([k, v]) => (
278+
<MenuItem key={k} value={Number(k)}>{v}</MenuItem>
279+
))}
280+
</Select>
281+
</FormControl>
260282
{errorM && <Alert severity="error" sx={{ mt: 2 }}>{formatContractError(errorM)}</Alert>}
261283
{isSuccessM && <Alert severity="success" sx={{ mt: 2 }}>Member added!</Alert>}
262284
<OnChainDisclaimer accepted={mDisclaimer} onChange={setMDisclaimer} />
263285
</DialogContent>
264286
<DialogActions>
265287
<Button onClick={() => setOpenMember(false)}>Cancel</Button>
266288
<Button variant="contained"
267-
onClick={() => addMember(programId, (mWallet || "0x0000000000000000000000000000000000000000") as `0x${string}`, mMemberId, mRole)}
289+
onClick={() => addMember(programId, (mWallet || "0x0000000000000000000000000000000000000000") as `0x${string}`, mMemberId, mRole, "0x0000000000000000000000000000000000000000000000000000000000000000" as `0x${string}`, mMemberType)}
268290
disabled={isPendingM || isConfirmingM || !mMemberId || !mDisclaimer || (!!mWallet && !mWalletValid)}>
269291
{isPendingM || isConfirmingM ? <CircularProgress size={20} /> : "Add"}
270292
</Button>

0 commit comments

Comments
 (0)