@@ -15,6 +15,7 @@ import BlockIcon from "@mui/icons-material/Block";
1515import ExpandMoreIcon from "@mui/icons-material/ExpandMore" ;
1616import DeleteIcon from "@mui/icons-material/Delete" ;
1717import ContentCopyIcon from "@mui/icons-material/ContentCopy" ;
18+ import ContentPasteIcon from "@mui/icons-material/ContentPaste" ;
1819import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined" ;
1920import { useAccount , useReadContract } from "wagmi" ;
2021import { useSearchParams } from "next/navigation" ;
@@ -29,10 +30,10 @@ import {
2930 useAddRewardType , useRemoveRewardType , useRewardTypes ,
3031 useAddSubType , useRemoveSubType , useSubTypes ,
3132 useDepositTokens , useTransferToSubMember , useTransferToParent , useWithdraw ,
32- useTokenBalance ,
33+ useTokenBalance , useProgramLogo , useSetProgramLogo ,
3334} from "@/hooks/useRewardsProgram" ;
3435import { MemberRoleLabels , MemberRoleEnum , MemberTypeLabels , CONTRACTS , REWARDS_PROGRAM_ABI } from "@/config/contracts" ;
35- import { fromBytes8 , fromBytes12 , fromBytes16 , toBytes12 , shortenAddress , formatFula , isValidAddress , formatContractError } from "@/lib/utils" ;
36+ import { fromBytes8 , fromBytes12 , fromBytes16 , toBytes12 , shortenAddress , formatFula , isValidAddress , formatContractError , ipfsLogoUrl , parseCID } from "@/lib/utils" ;
3637import { OnChainDisclaimer } from "@/components/common/OnChainDisclaimer" ;
3738import { QRCodeDisplay } from "@/components/common/QRCodeDisplay" ;
3839
@@ -290,6 +291,13 @@ function ProgramDetail({ programId }: { programId: number }) {
290291 const { deactivateProgram, isPending : isPendingDP , isConfirming : isConfirmingDP , isSuccess : isSuccessDP , error : errorDP } = useDeactivateProgram ( ) ;
291292 const [ openDeactivate , setOpenDeactivate ] = useState ( false ) ;
292293
294+ // Program Logo
295+ const { data : logoCID } = useProgramLogo ( programId ) ;
296+ const logoUrl = logoCID ? ipfsLogoUrl ( logoCID as string ) : "" ;
297+ const { setProgramLogo, isPending : isPendingLogo , isConfirming : isConfirmingLogo , isSuccess : isSuccessLogo , error : errorLogo } = useSetProgramLogo ( ) ;
298+ const [ openLogo , setOpenLogo ] = useState ( false ) ;
299+ const [ logoCIDInput , setLogoCIDInput ] = useState ( "" ) ;
300+
293301 // Show generated editCode
294302 const [ showEditCodeDialog , setShowEditCodeDialog ] = useState ( false ) ;
295303 const [ displayEditCode , setDisplayEditCode ] = useState ( "" ) ;
@@ -414,6 +422,14 @@ function ProgramDetail({ programId }: { programId: number }) {
414422 }
415423 } , [ isSuccessDP , refetchProgram ] ) ;
416424
425+ // After logo set
426+ useEffect ( ( ) => {
427+ if ( isSuccessLogo ) {
428+ const t = setTimeout ( ( ) => setOpenLogo ( false ) , 1200 ) ;
429+ return ( ) => clearTimeout ( t ) ;
430+ }
431+ } , [ isSuccessLogo ] ) ;
432+
417433 const handleAssignPA = ( ) => {
418434 const wallet = ( paWallet || "0x0000000000000000000000000000000000000000" ) as `0x${string } `;
419435 let hash : `0x${string } ` = "0x0000000000000000000000000000000000000000000000000000000000000000" ;
@@ -458,6 +474,20 @@ function ProgramDetail({ programId }: { programId: number }) {
458474 < IconButton component = { Link } href = "/programs" sx = { { mt : 0.5 } } aria-label = "Back to programs" >
459475 < ArrowBackIcon />
460476 </ IconButton >
477+ { logoUrl && (
478+ < Box
479+ component = "img"
480+ src = { logoUrl }
481+ alt = { `${ program . name } logo` }
482+ sx = { {
483+ width : { xs : 56 , sm : 72 } ,
484+ height : { xs : 56 , sm : 72 } ,
485+ borderRadius : 2 ,
486+ objectFit : "contain" ,
487+ flexShrink : 0 ,
488+ } }
489+ />
490+ ) }
461491 < Box >
462492 < Box sx = { { display : "flex" , alignItems : "center" , gap : 1 , flexWrap : "wrap" } } >
463493 < Typography variant = "h5" > { program . name } </ Typography >
@@ -486,6 +516,12 @@ function ProgramDetail({ programId }: { programId: number }) {
486516 </ Tooltip >
487517 </ >
488518 ) }
519+ { canManageProgram && program . active && (
520+ < Button size = "small" variant = "outlined"
521+ onClick = { ( ) => { setLogoCIDInput ( ( logoCID as string ) || "" ) ; setOpenLogo ( true ) ; } } >
522+ { logoUrl ? "Change Logo" : "Set Logo" }
523+ </ Button >
524+ ) }
489525 { canSetTransferLimit && program . active && (
490526 < Button size = "small" variant = "outlined"
491527 onClick = { ( ) => { setTlValue ( String ( transferLimit && Number ( transferLimit ) > 0 ? transferLimit : "" ) ) ; setOpenTL ( true ) ; } } >
@@ -879,6 +915,60 @@ function ProgramDetail({ programId }: { programId: number }) {
879915 </ DialogActions >
880916 </ Dialog >
881917
918+ { /* Set Logo Dialog */ }
919+ < Dialog open = { openLogo } onClose = { ( ) => setOpenLogo ( false ) } maxWidth = "sm" fullWidth >
920+ < DialogTitle > Set Program Logo</ DialogTitle >
921+ < DialogContent >
922+ < Typography variant = "body2" color = "text.secondary" sx = { { mb : 2 } } >
923+ Enter an IPFS CID for the program logo image (recommended 256x256px or larger, square).
924+ </ Typography >
925+ < TextField
926+ label = "IPFS CID or Gateway URL"
927+ value = { logoCIDInput }
928+ onChange = { ( e ) => setLogoCIDInput ( parseCID ( e . target . value ) ) }
929+ fullWidth
930+ margin = "normal"
931+ placeholder = "bafkr4i... or https://ipfs.cloud.fx.land/gateway/bafkr4i..."
932+ inputProps = { { maxLength : 256 } }
933+ InputProps = { {
934+ endAdornment : (
935+ < Tooltip title = "Paste from clipboard" >
936+ < IconButton size = "small" onClick = { async ( ) => {
937+ try {
938+ const text = await navigator . clipboard . readText ( ) ;
939+ if ( text ) setLogoCIDInput ( parseCID ( text ) ) ;
940+ } catch { /* clipboard permission denied */ }
941+ } } >
942+ < ContentPasteIcon fontSize = "small" />
943+ </ IconButton >
944+ </ Tooltip >
945+ ) ,
946+ } }
947+ />
948+ { logoCIDInput && (
949+ < Box sx = { { mt : 2 , textAlign : "center" } } >
950+ < Typography variant = "caption" color = "text.secondary" display = "block" sx = { { mb : 1 } } > Preview:</ Typography >
951+ < Box
952+ component = "img"
953+ src = { ipfsLogoUrl ( logoCIDInput ) }
954+ alt = "Logo preview"
955+ sx = { { maxWidth : 128 , maxHeight : 128 , borderRadius : 2 } }
956+ onError = { ( e : React . SyntheticEvent < HTMLImageElement > ) => { e . currentTarget . style . display = "none" ; } }
957+ />
958+ </ Box >
959+ ) }
960+ { errorLogo && < Alert severity = "error" sx = { { mt : 2 } } > { formatContractError ( errorLogo ) } </ Alert > }
961+ { isSuccessLogo && < Alert severity = "success" sx = { { mt : 2 } } > Logo updated!</ Alert > }
962+ </ DialogContent >
963+ < DialogActions >
964+ < Button onClick = { ( ) => setOpenLogo ( false ) } > Cancel</ Button >
965+ < Button variant = "contained" onClick = { ( ) => setProgramLogo ( programId , logoCIDInput ) }
966+ disabled = { isPendingLogo || isConfirmingLogo || ! logoCIDInput } >
967+ { isPendingLogo || isConfirmingLogo ? < CircularProgress size = { 20 } /> : "Save" }
968+ </ Button >
969+ </ DialogActions >
970+ </ Dialog >
971+
882972 { /* Edit Code Display Dialog (shown after walletless member creation) */ }
883973 < Dialog open = { showEditCodeDialog } onClose = { ( ) => setShowEditCodeDialog ( false ) } maxWidth = "sm" fullWidth >
884974 < DialogTitle > Edit Code Generated</ DialogTitle >
0 commit comments