From 24e31f2aec70ffa2632b4ca4c3ac79e52cac29ef Mon Sep 17 00:00:00 2001 From: aowheel Date: Wed, 1 Jan 2025 18:59:57 +0900 Subject: [PATCH 1/9] =?UTF-8?q?key=E3=81=AE=E4=BD=8D=E7=BD=AE=E3=82=92?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkgs/frontend/app/routes/$treeId_.member.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkgs/frontend/app/routes/$treeId_.member.tsx b/pkgs/frontend/app/routes/$treeId_.member.tsx index 20f2e92..02745ed 100644 --- a/pkgs/frontend/app/routes/$treeId_.member.tsx +++ b/pkgs/frontend/app/routes/$treeId_.member.tsx @@ -113,9 +113,8 @@ const WorkspaceMember: FC = () => { {m.wearer?.hats?.map((h) => ( - + From eae2f1c2edb8608b14519a77dca212ff0d14aec9 Mon Sep 17 00:00:00 2001 From: aowheel Date: Wed, 8 Jan 2025 01:46:19 +0900 Subject: [PATCH 2/9] =?UTF-8?q?HatsTimeFrameModule=E9=96=A2=E9=80=A3?= =?UTF-8?q?=E3=82=92=E9=99=A4=E3=81=8F=E6=A9=9F=E8=83=BD=E3=81=AE=E5=AE=9F?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/roles/HolderDetail.tsx | 119 ++++++++++++++++++ pkgs/frontend/app/routes/$treeId_.$hatId.tsx | 100 +++++++++++++++ .../app/routes/$treeId_.$hatId_.$address.tsx | 113 +++++++++++++++++ pkgs/frontend/hooks/useFractionToken.ts | 91 ++++++++++++++ 4 files changed, 423 insertions(+) create mode 100644 pkgs/frontend/app/components/roles/HolderDetail.tsx create mode 100644 pkgs/frontend/app/routes/$treeId_.$hatId.tsx create mode 100644 pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx diff --git a/pkgs/frontend/app/components/roles/HolderDetail.tsx b/pkgs/frontend/app/components/roles/HolderDetail.tsx new file mode 100644 index 0000000..ce4fbf7 --- /dev/null +++ b/pkgs/frontend/app/components/roles/HolderDetail.tsx @@ -0,0 +1,119 @@ +import { FC } from "react"; +import { HatsDetailSchama } from "types/hats"; +import { RoleIcon } from "../icon/RoleIcon"; +import { Box, Heading, HStack, Icon, List, Text } from "@chakra-ui/react"; +import { Link } from "@remix-run/react"; +import { UserIcon } from "../icon/UserIcon"; +import { ipfs2https } from "utils/ipfs"; +import { FaChevronLeft, FaLink } from "react-icons/fa6"; +import { abbreviateAddress } from "utils/wallet"; + +interface HolderDetailProps { + detail?: HatsDetailSchama; + imageUri?: string; + treeId?: string; + hatId?: string; + wearerId?: string; + wearerName?: string; + wearerIcon?: string; +} + +export const RoleName: FC = ({ detail, treeId }) => ( + + + + + + {detail?.data.name ? ( + {detail.data.name} + ) : ( + + No name + + )} + + +); + +export const RoleNameWithWearer: FC = ({ + detail, + treeId, + hatId, + wearerId, + wearerName, + wearerIcon, +}) => ( + + + + + + {detail?.data.name ? ( + <> + {detail.data.name} + + + + + + {wearerName ? wearerName : abbreviateAddress(wearerId || "0x")} + + + ) : ( + + No name + + )} + + +); + +export const HolderDetail: FC = ({ detail, imageUri }) => ( + + + + + + + Description + {detail?.data.description ? ( + {detail.data.description} + ) : ( + + No description + + )} + + + Responsibilities + {(detail?.data.responsabilities?.length ?? 0 > 0) ? ( + + {detail?.data.responsabilities?.map((r) => ( + {r.label} + ))} + + ) : ( + + No responsibilities + + )} + + + Authorities + {(detail?.data.authorities?.length ?? 0 > 0) ? ( + + {detail?.data.authorities?.map((a) => ( + {a.label} + ))} + + ) : ( + + No authorities + + )} + + + +); + +export default HolderDetail; diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId.tsx new file mode 100644 index 0000000..758d3a9 --- /dev/null +++ b/pkgs/frontend/app/routes/$treeId_.$hatId.tsx @@ -0,0 +1,100 @@ +import { Box, Heading, HStack, Text, VStack } from "@chakra-ui/react"; +import { useNavigate, useParams } from "@remix-run/react"; +import { useNamesByAddresses } from "hooks/useENS"; +import { useHoldersWithoutWearers } from "hooks/useFractionToken"; +import { useTreeInfo } from "hooks/useHats"; +import { FC, useMemo } from "react"; +import { ipfs2https } from "utils/ipfs"; +import { abbreviateAddress } from "utils/wallet"; +import { BasicButton } from "~/components/BasicButton"; +import { HatsListItemParser } from "~/components/common/HatsListItemParser"; +import { UserIcon } from "~/components/icon/UserIcon"; +import { HolderDetail, RoleName } from "~/components/roles/HolderDetail"; +import { StickyNav } from "~/components/StickyNav"; + +const RoleDetails: FC = () => { + const { treeId, hatId } = useParams(); + + const tree = useTreeInfo(Number(treeId)); + + const hat = useMemo(() => { + if (!tree || !tree.hats) return; + return tree.hats.find((h) => h.id === hatId); + }, [tree, hatId]); + + const wearers = useMemo(() => hat?.wearers, [hat]); + const wearerIds = useMemo( + () => wearers?.map(({ id }) => id) || [], + [wearers] + ); + + // wearer + const { names: wearerNames } = useNamesByAddresses(wearerIds); + + // wearerを除くholder + const holdersWithoutWearers = useHoldersWithoutWearers({ + hatId, + wearers: wearerIds, + }); + const { names: holderNames } = useNamesByAddresses(holdersWithoutWearers); + + const navigate = useNavigate(); + + if (!hat) return; + + return ( + + + + + + + Members + + {wearerNames.flat().map((n, idx) => ( + + + + {n.name + ? `${n.name} (${abbreviateAddress(n.address)})` + : abbreviateAddress(n.address)} + + + Role Holder + + + ))} + {holderNames.flat().map((n, idx) => ( + + + + {n.name + ? `${n.name} (${abbreviateAddress(n.address)})` + : abbreviateAddress(n.address)} + + + Assist + + + ))} + + + navigate(`/${treeId}/${hatId}/assign`)} + > + Assign Member + + + + + ); +}; + +export default RoleDetails; diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx new file mode 100644 index 0000000..41eb66e --- /dev/null +++ b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx @@ -0,0 +1,113 @@ +import { Box, HStack, VStack, Text, Heading } from "@chakra-ui/react"; +import { Link, useParams } from "@remix-run/react"; +import { useNamesByAddresses } from "hooks/useENS"; +import { useHoldersWithBalance } from "hooks/useFractionToken"; +import { useTreeInfo } from "hooks/useHats"; +import { FC, useMemo } from "react"; +import { ipfs2https } from "utils/ipfs"; +import { abbreviateAddress } from "utils/wallet"; +import { HatsListItemParser } from "~/components/common/HatsListItemParser"; +import { UserIcon } from "~/components/icon/UserIcon"; +import { + HolderDetail, + RoleNameWithWearer, +} from "~/components/roles/HolderDetail"; +import { StickyNav } from "~/components/StickyNav"; + +const RoleHolderDetails: FC = () => { + const { treeId, hatId, address } = useParams(); + + const tree = useTreeInfo(Number(treeId)); + + const hat = useMemo(() => { + if (!tree || !tree.hats) return; + return tree.hats.find((h) => h.id === hatId); + }, [tree, hatId]); + + // wearerの名前とアイコンを取得 + const addresses = useMemo(() => (address ? [address] : undefined), [address]); + const { names: wearerNames } = useNamesByAddresses(addresses); + const { wearerName, wearerIcon } = useMemo( + () => + wearerNames.flat().length > 0 + ? { + wearerName: wearerNames.flat()[0].name, + wearerIcon: wearerNames.flat()[0].text_records?.avatar, + } + : {}, + [wearerNames] + ); + + // holderをbalanceとともに取得 + const holdersWithBalance = useHoldersWithBalance({ wearer: address, hatId }); + const holders = useMemo( + () => holdersWithBalance.map(({ holder }) => holder), + [holdersWithBalance] + ); + const { names: holderNames } = useNamesByAddresses(holders); + const holderDetail = useMemo( + () => + holderNames.flat().map((n) => ({ + ...n, + balance: holdersWithBalance.find( + ({ holder }) => holder.toLowerCase() === n.address.toLowerCase() + )?.balance, + })), + [holdersWithBalance, holderNames] + ); + + if (!hat) return; + + return ( + + + + + + + + Assist Credit Holders + + + Send + + + + + + {holderDetail.map((h, idx) => ( + + + + {h.name + ? `${h.name} (${abbreviateAddress(h.address)})` + : abbreviateAddress(h.address)} + + {h.balance !== undefined && ( + {Number(h.balance).toLocaleString()} + )} + + ))} + + + + + ); +}; + +export default RoleHolderDetails; diff --git a/pkgs/frontend/hooks/useFractionToken.ts b/pkgs/frontend/hooks/useFractionToken.ts index dd71b57..0c39d9e 100644 --- a/pkgs/frontend/hooks/useFractionToken.ts +++ b/pkgs/frontend/hooks/useFractionToken.ts @@ -72,6 +72,97 @@ export const useTokenRecipients = ( return recipients; }; +export const useHoldersWithoutWearers = ({ + hatId, + wearers, +}: { + hatId?: string; + wearers: string[]; +}) => { + const [holders, setHolders] = useState([]); + + const { getTokenId, getTokenRecipients } = useFractionToken(); + + useEffect(() => { + const fetch = async () => { + if (!hatId) return; + try { + const fetchedRecipients = await Promise.all( + wearers.map(async (w) => { + const tokenId = await getTokenId({ + hatId: BigInt(hatId), + account: w as Address, + }); + if (!tokenId) return []; + const recipients = (await getTokenRecipients({ tokenId }))?.filter( + (r) => r.toLowerCase() !== w.toLowerCase() + ); + return recipients || []; + }) + ); + + setHolders(fetchedRecipients.flat()); + } catch (error) { + console.error("error occured when fetching tokenRecipients:", error); + } + }; + + fetch(); + }, [getTokenId, getTokenRecipients, hatId, wearers]); + + return holders; +}; + +export const useHoldersWithBalance = ({ + wearer, + hatId, +}: { + wearer?: string; + hatId?: string; +}) => { + const [holders, setHolders] = useState< + { + holder: Address; + balance: bigint; + }[] + >([]); + + const { getTokenId, getTokenRecipients } = useFractionToken(); + + useEffect(() => { + const fetch = async () => { + if (!wearer || !hatId) return; + try { + const tokenId = await getTokenId({ + hatId: BigInt(hatId), + account: wearer as Address, + }); + if (!tokenId) return; + const holders = [...((await getTokenRecipients({ tokenId })) || [])]; + + const holdersWithBalance = await Promise.all( + holders.map(async (holder) => { + const balance = await publicClient.readContract({ + ...fractionTokenBaseConfig, + functionName: "balanceOf", + args: [wearer as Address, holder, BigInt(hatId)], + }); + return { holder, balance }; + }) + ); + + setHolders(holdersWithBalance); + } catch (error) { + console.error("error occured when fetching tokenRecipients:", error); + } + }; + + fetch(); + }, [getTokenId, getTokenRecipients, wearer, hatId]); + + return holders; +}; + export const useBalanceOfFractionToken = ( holder: Address, address: Address, From 131a63a759989bb32c93fd26ee25efe4d2fae18e Mon Sep 17 00:00:00 2001 From: aowheel Date: Thu, 9 Jan 2025 12:33:51 +0900 Subject: [PATCH 3/9] =?UTF-8?q?revoke=E3=82=92=E9=99=A4=E3=81=8F=E6=A9=9F?= =?UTF-8?q?=E8=83=BD=E3=81=AE=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/roles/HolderDetail.tsx | 74 ++++++- pkgs/frontend/app/routes/$treeId_.$hatId.tsx | 45 ++++- .../app/routes/$treeId_.$hatId_.$address.tsx | 142 ++++++++++++-- pkgs/frontend/hooks/useContracts.ts | 7 +- pkgs/frontend/hooks/useHatsTimeFrameModule.ts | 181 +++++++++++++++++- 5 files changed, 407 insertions(+), 42 deletions(-) diff --git a/pkgs/frontend/app/components/roles/HolderDetail.tsx b/pkgs/frontend/app/components/roles/HolderDetail.tsx index ce4fbf7..f5b9300 100644 --- a/pkgs/frontend/app/components/roles/HolderDetail.tsx +++ b/pkgs/frontend/app/components/roles/HolderDetail.tsx @@ -1,4 +1,4 @@ -import { FC } from "react"; +import { FC, useMemo } from "react"; import { HatsDetailSchama } from "types/hats"; import { RoleIcon } from "../icon/RoleIcon"; import { Box, Heading, HStack, Icon, List, Text } from "@chakra-ui/react"; @@ -7,6 +7,7 @@ import { UserIcon } from "../icon/UserIcon"; import { ipfs2https } from "utils/ipfs"; import { FaChevronLeft, FaLink } from "react-icons/fa6"; import { abbreviateAddress } from "utils/wallet"; +import dayjs from "dayjs"; interface HolderDetailProps { detail?: HatsDetailSchama; @@ -16,6 +17,9 @@ interface HolderDetailProps { wearerId?: string; wearerName?: string; wearerIcon?: string; + isActive?: boolean; + woreTime?: number; + wearingElapsedTime?: number; } export const RoleName: FC = ({ detail, treeId }) => ( @@ -44,7 +48,7 @@ export const RoleNameWithWearer: FC = ({ wearerIcon, }) => ( - + @@ -56,7 +60,7 @@ export const RoleNameWithWearer: FC = ({ - {wearerName ? wearerName : abbreviateAddress(wearerId || "0x")} + {wearerName ? wearerName : abbreviateAddress(wearerId || "")} ) : ( @@ -68,7 +72,67 @@ export const RoleNameWithWearer: FC = ({ ); -export const HolderDetail: FC = ({ detail, imageUri }) => ( +export const ActiveState: FC = ({ + isActive, + woreTime, + wearingElapsedTime, +}) => { + const formattedWoreTime = useMemo(() => { + if (!woreTime) return; + return dayjs.unix(woreTime).format("YYYY/MM/DD"); + }, [woreTime]); + + const formattedWearingElapsedTime = useMemo(() => { + if (!wearingElapsedTime) return; + return Math.floor(wearingElapsedTime / 86400); + }, [wearingElapsedTime]); + + return ( + + {isActive ? ( + + Active + + ) : ( + + Inactive + + )} + {formattedWoreTime && formattedWearingElapsedTime && ( + + + Role assigned on{" "} + + {formattedWoreTime} + + + + Active in{" "} + + {formattedWearingElapsedTime}days + + + + )} + + ); +}; + +export const HatDetail: FC = ({ detail, imageUri }) => ( @@ -115,5 +179,3 @@ export const HolderDetail: FC = ({ detail, imageUri }) => ( ); - -export default HolderDetail; diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId.tsx index 758d3a9..4ea3f9d 100644 --- a/pkgs/frontend/app/routes/$treeId_.$hatId.tsx +++ b/pkgs/frontend/app/routes/$treeId_.$hatId.tsx @@ -3,13 +3,15 @@ import { useNavigate, useParams } from "@remix-run/react"; import { useNamesByAddresses } from "hooks/useENS"; import { useHoldersWithoutWearers } from "hooks/useFractionToken"; import { useTreeInfo } from "hooks/useHats"; +import { useWearingElapsedTime } from "hooks/useHatsTimeFrameModule"; +import { useGetWorkspace } from "hooks/useWorkspace"; import { FC, useMemo } from "react"; import { ipfs2https } from "utils/ipfs"; import { abbreviateAddress } from "utils/wallet"; import { BasicButton } from "~/components/BasicButton"; import { HatsListItemParser } from "~/components/common/HatsListItemParser"; import { UserIcon } from "~/components/icon/UserIcon"; -import { HolderDetail, RoleName } from "~/components/roles/HolderDetail"; +import { HatDetail, RoleName } from "~/components/roles/HolderDetail"; import { StickyNav } from "~/components/StickyNav"; const RoleDetails: FC = () => { @@ -38,6 +40,18 @@ const RoleDetails: FC = () => { }); const { names: holderNames } = useNamesByAddresses(holdersWithoutWearers); + // 各wearerのWearingElapsedTimeを取得 + const { data } = useGetWorkspace(treeId); + const hatsTimeFrameModuleAddress = useMemo( + () => data?.workspace?.hatsTimeFrameModule, + [data] + ); + const timeList = useWearingElapsedTime( + hatsTimeFrameModuleAddress, + hatId, + wearerIds + ); + const navigate = useNavigate(); if (!hat) return; @@ -46,22 +60,37 @@ const RoleDetails: FC = () => { - + Members {wearerNames.flat().map((n, idx) => ( - + navigate(`/${treeId}/${hatId}/${n.address}`)} + > - - {n.name - ? `${n.name} (${abbreviateAddress(n.address)})` - : abbreviateAddress(n.address)} - + + + {n.name + ? `${n.name} (${abbreviateAddress(n.address)})` + : abbreviateAddress(n.address)} + + + {Math.floor( + (timeList.find( + ({ wearer }) => + wearer.toLowerCase() === n.address.toLowerCase() + )?.time || 0) / 86400 + )} + days + + Role Holder diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx index 41eb66e..7e4841a 100644 --- a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx +++ b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx @@ -3,13 +3,22 @@ import { Link, useParams } from "@remix-run/react"; import { useNamesByAddresses } from "hooks/useENS"; import { useHoldersWithBalance } from "hooks/useFractionToken"; import { useTreeInfo } from "hooks/useHats"; -import { FC, useMemo } from "react"; +import { + useActiveState, + useDeactivate, + useReactivate, +} from "hooks/useHatsTimeFrameModule"; +import { useActiveWallet } from "hooks/useWallet"; +import { useGetWorkspace } from "hooks/useWorkspace"; +import { FC, useMemo, useState } from "react"; import { ipfs2https } from "utils/ipfs"; import { abbreviateAddress } from "utils/wallet"; +import { BasicButton } from "~/components/BasicButton"; import { HatsListItemParser } from "~/components/common/HatsListItemParser"; import { UserIcon } from "~/components/icon/UserIcon"; import { - HolderDetail, + ActiveState, + HatDetail, RoleNameWithWearer, } from "~/components/roles/HolderDetail"; import { StickyNav } from "~/components/StickyNav"; @@ -17,6 +26,9 @@ import { StickyNav } from "~/components/StickyNav"; const RoleHolderDetails: FC = () => { const { treeId, hatId, address } = useParams(); + const { wallet } = useActiveWallet(); + const me = wallet?.account?.address; + const tree = useTreeInfo(Number(treeId)); const hat = useMemo(() => { @@ -24,6 +36,27 @@ const RoleHolderDetails: FC = () => { return tree.hats.find((h) => h.id === hatId); }, [tree, hatId]); + // ログインユーザーがこのhatの上位のhatのholderであるか + const isAuthorised = useMemo(() => { + if (!me || !hat?.levelAtLocalTree) return false; + + if (hat.wearers?.some((w) => w.id.toLowerCase() === me.toLowerCase())) + return true; + + for (let i = 0; i < hat.levelAtLocalTree; i++) { + const parentHatId = hat.id.slice(0, 10 + 4 * i) + "0".repeat(56 - 4 * i); + + if ( + tree?.hats + ?.find((h) => h.id === parentHatId) + ?.wearers?.some((w) => w.id.toLowerCase() === me.toLowerCase()) + ) + return true; + } + + return false; + }, [me, tree, hat]); + // wearerの名前とアイコンを取得 const addresses = useMemo(() => (address ? [address] : undefined), [address]); const { names: wearerNames } = useNamesByAddresses(addresses); @@ -56,6 +89,28 @@ const RoleHolderDetails: FC = () => { [holdersWithBalance, holderNames] ); + // HatsTimeFrameModuleのアドレスを取得 + const { data } = useGetWorkspace(treeId); + const hatsTimeFrameModuleAddress = useMemo( + () => data?.workspace?.hatsTimeFrameModule, + [data] + ); + + const [count, setCount] = useState(0); + const { isActive, woreTime, wearingElapsedTime } = useActiveState( + hatsTimeFrameModuleAddress, + hatId, + address, + count + ); + + const { reactivate, isLoading: isReactivating } = useReactivate( + hatsTimeFrameModuleAddress + ); + const { deactivate, isLoading: isDeactivating } = useDeactivate( + hatsTimeFrameModuleAddress + ); + if (!hat) return; return ( @@ -68,15 +123,20 @@ const RoleHolderDetails: FC = () => { wearerName={wearerName} wearerIcon={wearerIcon} /> - + + - + Assist Credit Holders { - {holderDetail.map((h, idx) => ( - - - - {h.name - ? `${h.name} (${abbreviateAddress(h.address)})` - : abbreviateAddress(h.address)} - - {h.balance !== undefined && ( - {Number(h.balance).toLocaleString()} - )} - - ))} + {holderDetail.length === 0 ? ( + + No holders + + ) : ( + holderDetail.map((h, idx) => ( + + + + {h.name + ? `${h.name} (${abbreviateAddress(h.address)})` + : abbreviateAddress(h.address)} + + {h.balance !== undefined && ( + {Number(h.balance).toLocaleString()} + )} + + )) + )} + {/* hatについて権限があるかどうかで表示の有無が変わるボタン */} + {isAuthorised && ( + <> + {isActive ? ( + { + await deactivate(hatId, address); + setCount(count + 1); + }} + disabled={isDeactivating} + > + {isDeactivating ? "Deactivating..." : "Deactivate"} + + ) : ( + { + await reactivate(hatId, address); + setCount(count + 1); + }} + disabled={isReactivating} + > + {isReactivating ? "Reactivating..." : "Reactivate"} + + )} + {}}> + Revoke + + + )} + ); diff --git a/pkgs/frontend/hooks/useContracts.ts b/pkgs/frontend/hooks/useContracts.ts index a145e92..fe1b73c 100644 --- a/pkgs/frontend/hooks/useContracts.ts +++ b/pkgs/frontend/hooks/useContracts.ts @@ -15,9 +15,12 @@ export const hatsContractBaseConfig = { abi: HATS_ABI, }; -export const hatsTimeFrameContractBaseConfig = { +export const hatsTimeFrameContractBaseConfig = ( + hatsTimeFrameModuleAddress: Address +) => ({ + address: hatsTimeFrameModuleAddress, abi: HATS_TIME_FRAME_MODULE_ABI, -}; +}); export const fractionTokenBaseConfig = { address: FRACTION_TOKEN_ADDRESS as Address, diff --git a/pkgs/frontend/hooks/useHatsTimeFrameModule.ts b/pkgs/frontend/hooks/useHatsTimeFrameModule.ts index 7cd6492..f2eeecf 100644 --- a/pkgs/frontend/hooks/useHatsTimeFrameModule.ts +++ b/pkgs/frontend/hooks/useHatsTimeFrameModule.ts @@ -1,8 +1,8 @@ -import { HATS_TIME_FRAME_MODULE_ABI } from "abi/hatsTimeFrameModule"; import { useActiveWallet } from "./useWallet"; -import { Address, parseEventLogs } from "viem"; -import { useState } from "react"; +import { Address } from "viem"; +import { useCallback, useEffect, useState } from "react"; import { publicClient } from "./useViem"; +import { hatsTimeFrameContractBaseConfig } from "./useContracts"; export const useMintHatFromTimeFrameModule = ( hatsTimeFrameModuleAddress: Address @@ -18,8 +18,7 @@ export const useMintHatFromTimeFrameModule = ( try { const txHash = await wallet?.writeContract({ - abi: HATS_TIME_FRAME_MODULE_ABI, - address: hatsTimeFrameModuleAddress, + ...hatsTimeFrameContractBaseConfig(hatsTimeFrameModuleAddress), functionName: "mintHat", args: [hatId, wearer, time || BigInt(0)], }); @@ -36,3 +35,175 @@ export const useMintHatFromTimeFrameModule = ( return { mintHat, isLoading }; }; + +export const useReactivate = (hatsTimeFrameModuleAddress?: string) => { + const { wallet } = useActiveWallet(); + + const [isLoading, setIsLoading] = useState(false); + + const reactivate = useCallback( + async (hatId?: string, wearer?: string) => { + if (!hatsTimeFrameModuleAddress || !wallet || !hatId || !wearer) return; + + setIsLoading(true); + + try { + const txHash = await wallet?.writeContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "reactivate", + args: [BigInt(hatId), wearer as Address], + }); + + await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }, + [hatsTimeFrameModuleAddress, wallet] + ); + + return { reactivate, isLoading }; +}; + +export const useDeactivate = (hatsTimeFrameModuleAddress?: string) => { + const { wallet } = useActiveWallet(); + + const [isLoading, setIsLoading] = useState(false); + + const deactivate = useCallback( + async (hatId?: string, wearer?: string) => { + if (!hatsTimeFrameModuleAddress || !wallet || !hatId || !wearer) return; + + setIsLoading(true); + + try { + const txHash = await wallet?.writeContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "deactivate", + args: [BigInt(hatId), wearer as Address], + }); + + await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }, + [hatsTimeFrameModuleAddress, wallet] + ); + + return { deactivate, isLoading }; +}; + +export const useActiveState = ( + hatsTimeFrameModuleAddress?: string, + hatId?: string, + wearer?: string, + count?: number +) => { + const [activeState, setActiveState] = useState({ + isActive: false, + woreTime: 0, + wearingElapsedTime: 0, + }); + + useEffect(() => { + const fetch = async () => { + if (!hatsTimeFrameModuleAddress || !hatId || !wearer) return; + + try { + const [isActive, woreTime, wearingElapsedTime] = await Promise.all([ + publicClient.readContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "isActive", + args: [BigInt(hatId), wearer as Address], + }), + publicClient.readContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "getWoreTime", + args: [wearer as Address, BigInt(hatId)], + }), + publicClient.readContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "getWearingElapsedTime", + args: [wearer as Address, BigInt(hatId)], + }), + ]); + + setActiveState({ + isActive, + woreTime: Number(woreTime), + wearingElapsedTime: Number(wearingElapsedTime), + }); + } catch (error) { + console.error(error); + } + }; + + fetch(); + }, [hatsTimeFrameModuleAddress, hatId, wearer, count]); + + return activeState; +}; + +export const useWearingElapsedTime = ( + hatsTimeFrameModuleAddress?: string, + hatId?: string, + wearers?: string[] +) => { + const [wearingElapsedTimeList, setWearingElapsedTimeList] = useState< + { + wearer: string; + time: number; + }[] + >([]); + + useEffect(() => { + const fetch = async () => { + if (!hatsTimeFrameModuleAddress || !hatId || !wearers) return; + + try { + const list = await Promise.all( + wearers.map(async (w) => { + const time = Number( + await publicClient.readContract({ + ...hatsTimeFrameContractBaseConfig( + hatsTimeFrameModuleAddress as Address + ), + functionName: "getWearingElapsedTime", + args: [w as Address, BigInt(hatId)], + }) + ); + + return { wearer: w, time }; + }) + ); + + setWearingElapsedTimeList(list); + } catch (error) { + console.error(error); + } + }; + + fetch(); + }, [hatsTimeFrameModuleAddress, hatId, wearers]); + + return wearingElapsedTimeList; +}; From 24441056224826e7820082db437d146832b57333 Mon Sep 17 00:00:00 2001 From: aowheel Date: Thu, 9 Jan 2025 17:29:21 +0900 Subject: [PATCH 4/9] =?UTF-8?q?revoke=E3=81=AE=E6=A9=9F=E8=83=BD=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/components/roles/HolderDetail.tsx | 12 +++++---- .../app/routes/$treeId_.$hatId_.$address.tsx | 22 ++++++++++++--- pkgs/frontend/hooks/useHats.ts | 27 +++++++++++++++++++ 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/pkgs/frontend/app/components/roles/HolderDetail.tsx b/pkgs/frontend/app/components/roles/HolderDetail.tsx index f5b9300..b82eb56 100644 --- a/pkgs/frontend/app/components/roles/HolderDetail.tsx +++ b/pkgs/frontend/app/components/roles/HolderDetail.tsx @@ -114,12 +114,14 @@ export const ActiveState: FC = ({ )} {formattedWoreTime && formattedWearingElapsedTime && ( - - Role assigned on{" "} - - {formattedWoreTime} + {isActive && ( + + Activated on{" "} + + {formattedWoreTime} + - + )} Active in{" "} diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx index 7e4841a..c6a24ac 100644 --- a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx +++ b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx @@ -1,8 +1,8 @@ import { Box, HStack, VStack, Text, Heading } from "@chakra-ui/react"; -import { Link, useParams } from "@remix-run/react"; +import { Link, useNavigate, useParams } from "@remix-run/react"; import { useNamesByAddresses } from "hooks/useENS"; import { useHoldersWithBalance } from "hooks/useFractionToken"; -import { useTreeInfo } from "hooks/useHats"; +import { useHats, useTreeInfo } from "hooks/useHats"; import { useActiveState, useDeactivate, @@ -96,6 +96,7 @@ const RoleHolderDetails: FC = () => { [data] ); + // HatsTimeFrameModule関連の情報をボタンクリックの後再取得できるようにカウンターを設置 const [count, setCount] = useState(0); const { isActive, woreTime, wearingElapsedTime } = useActiveState( hatsTimeFrameModuleAddress, @@ -104,12 +105,16 @@ const RoleHolderDetails: FC = () => { count ); + // reactivate, deactivate, renounce const { reactivate, isLoading: isReactivating } = useReactivate( hatsTimeFrameModuleAddress ); const { deactivate, isLoading: isDeactivating } = useDeactivate( hatsTimeFrameModuleAddress ); + const { renounceHat, isLoading: isRenouncing } = useHats(); + + const navigate = useNavigate(); if (!hat) return; @@ -199,8 +204,17 @@ const RoleHolderDetails: FC = () => { {isReactivating ? "Reactivating..." : "Reactivate"} )} - {}}> - Revoke + {/* 現時点では表示されても実際にrevokeできるのはwearerのみ */} + { + await renounceHat(BigInt(hatId || 0)); + navigate(`/${treeId}/${hatId}`); + }} + disabled={isRenouncing} + > + {isRenouncing ? "Revoking..." : "Revoke"} )} diff --git a/pkgs/frontend/hooks/useHats.ts b/pkgs/frontend/hooks/useHats.ts index 5c601af..d68fc97 100644 --- a/pkgs/frontend/hooks/useHats.ts +++ b/pkgs/frontend/hooks/useHats.ts @@ -474,6 +474,32 @@ export const useHats = () => { [wallet] ); + const renounceHat = useCallback( + async (hatId: bigint) => { + if (!wallet) return; + + setIsLoading(true); + + try { + const txHash = await wallet.writeContract({ + abi: HATS_ABI, + address: HATS_ADDRESS, + functionName: "renounceHat", + args: [hatId], + }); + + await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }, + [wallet] + ); + return { isLoading, getTreeInfo, @@ -487,6 +513,7 @@ export const useHats = () => { mintHat, changeHatDetails, changeHatImageURI, + renounceHat, }; }; From 7fa1d8660bca4e883e19940a8746cf63092200fd Mon Sep 17 00:00:00 2001 From: aowheel Date: Thu, 9 Jan 2025 18:09:40 +0900 Subject: [PATCH 5/9] =?UTF-8?q?role=E8=A9=B3=E7=B4=B0=E3=81=AE=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=81=AE=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/app/components/roles/HolderDetail.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pkgs/frontend/app/components/roles/HolderDetail.tsx b/pkgs/frontend/app/components/roles/HolderDetail.tsx index b82eb56..5a30bb8 100644 --- a/pkgs/frontend/app/components/roles/HolderDetail.tsx +++ b/pkgs/frontend/app/components/roles/HolderDetail.tsx @@ -155,7 +155,12 @@ export const HatDetail: FC = ({ detail, imageUri }) => ( {(detail?.data.responsabilities?.length ?? 0 > 0) ? ( {detail?.data.responsabilities?.map((r) => ( - {r.label} + + + {r.label} + {" "} + {r.description} + ))} ) : ( @@ -169,7 +174,12 @@ export const HatDetail: FC = ({ detail, imageUri }) => ( {(detail?.data.authorities?.length ?? 0 > 0) ? ( {detail?.data.authorities?.map((a) => ( - {a.label} + + + {a.label} + {" "} + {a.description} + ))} ) : ( From 3e67eaa802a43647ec1ddfd6456b55ad1d024871 Mon Sep 17 00:00:00 2001 From: yu23ki14 Date: Thu, 9 Jan 2025 18:12:10 +0900 Subject: [PATCH 6/9] use thirdweb smart account --- .../frontend/app/components/icon/UserIcon.tsx | 1 + ...d_.$hatId_.$address_.assistcredit.send.tsx | 2 +- .../app/routes/$treeId_.$hatId_.assign.tsx | 81 ++++++++++--------- .../app/routes/$treeId_.splits.new.tsx | 53 ++++++------ pkgs/frontend/app/routes/signup.tsx | 2 +- pkgs/frontend/hooks/useFractionToken.ts | 3 + pkgs/frontend/hooks/useWallet.ts | 8 +- 7 files changed, 82 insertions(+), 68 deletions(-) diff --git a/pkgs/frontend/app/components/icon/UserIcon.tsx b/pkgs/frontend/app/components/icon/UserIcon.tsx index 89a5c9e..b0c63fc 100644 --- a/pkgs/frontend/app/components/icon/UserIcon.tsx +++ b/pkgs/frontend/app/components/icon/UserIcon.tsx @@ -11,6 +11,7 @@ export const UserIcon = ({ userImageUrl, size = "full" }: UserIconProps) => { { return ( { }, [hatId, resolvedAddress, inputValue, mintHat]); return ( - <> - - - - - - - {/* User name or address input */} - - - ユーザー名 or ウォレットアドレス - - setInputValue(e.target.value)} - /> - {resolvedAddress && !isValidEthAddress(inputValue) && ( - - - {abbreviateAddress(resolvedAddress)} - - )} - - - {/* Date input */} - - - 開始日 - - { - setStartDatetime(e.target.value); - }} - type="datetime-local" - /> + + + + + + + + + + {/* User name or address input */} + + + ユーザー名 or ウォレットアドレス + + setInputValue(e.target.value)} + /> + {resolvedAddress && !isValidEthAddress(inputValue) && ( + + + {abbreviateAddress(resolvedAddress)} + + )} + + + {/* Date input */} + + + 開始日 + + { + setStartDatetime(e.target.value); + }} + type="datetime-local" + /> + {/* Assign Button */} @@ -151,7 +154,7 @@ const AssignRole: FC = () => { > Assign - + ); }; diff --git a/pkgs/frontend/app/routes/$treeId_.splits.new.tsx b/pkgs/frontend/app/routes/$treeId_.splits.new.tsx index 435f544..3f1a587 100644 --- a/pkgs/frontend/app/routes/$treeId_.splits.new.tsx +++ b/pkgs/frontend/app/routes/$treeId_.splits.new.tsx @@ -215,7 +215,7 @@ const SplitterNew: FC = () => { const [splitterName, setSplitterName] = useState(""); const _splitterName = useMemo(() => { - return [`${splitterName}.splitter`]; + return [`${splitterName}.split`]; }, [splitterName]); const { addresses } = useAddressesByNames(_splitterName, true); const availableName = useMemo(() => { @@ -256,29 +256,32 @@ const SplitterNew: FC = () => { const calcParams = () => { const data = getValues(); - return data.roles.map((role) => { - const [multiplierTop, multiplierBottom] = role.multiplier - ? String(role.multiplier).includes(".") - ? [ - BigInt( - role.multiplier * - 10 ** String(role.multiplier).split(".")[1].length - ), - BigInt(10 ** String(role.multiplier).split(".")[1].length), - ] - : [BigInt(role.multiplier), BigInt(1)] - : [BigInt(1), BigInt(1)]; - - return { - hatId: BigInt(role.hatId), - multiplierTop, - multiplierBottom, - wearers: role.wearers, - }; - }); + return data.roles + .filter((r) => r.active) + .map((role) => { + const [multiplierTop, multiplierBottom] = role.multiplier + ? String(role.multiplier).includes(".") + ? [ + BigInt( + role.multiplier * + 10 ** String(role.multiplier).split(".")[1].length + ), + BigInt(10 ** String(role.multiplier).split(".")[1].length), + ] + : [BigInt(role.multiplier), BigInt(1)] + : [BigInt(1), BigInt(1)]; + + return { + hatId: BigInt(role.hatId), + multiplierTop, + multiplierBottom, + wearers: role.wearers, + }; + }); }; - const handlePreview = async () => { + const handlePreview = useCallback(async () => { + if (!availableName) return; const params = calcParams(); const res = await previewSplits(params); @@ -296,7 +299,7 @@ const SplitterNew: FC = () => { }) ) ); - }; + }, [availableName]); const { setName } = useSetName(); const handleCreateSplitter = async () => { @@ -320,7 +323,7 @@ const SplitterNew: FC = () => { return ( { ))} - + プレビュー diff --git a/pkgs/frontend/app/routes/signup.tsx b/pkgs/frontend/app/routes/signup.tsx index cb999f6..2801bdd 100644 --- a/pkgs/frontend/app/routes/signup.tsx +++ b/pkgs/frontend/app/routes/signup.tsx @@ -34,7 +34,7 @@ const Login: FC = () => { }, [userName, addresses]); const handleSubmit = async () => { - if (!wallet) return; + if (!wallet || !availableName) return; const params: { name: string; diff --git a/pkgs/frontend/hooks/useFractionToken.ts b/pkgs/frontend/hooks/useFractionToken.ts index 0dc501e..5b18125 100644 --- a/pkgs/frontend/hooks/useFractionToken.ts +++ b/pkgs/frontend/hooks/useFractionToken.ts @@ -379,6 +379,8 @@ export const useTransferFractionToken = (hatId: bigint, wearer: Address) => { functionName: "safeTransferFrom", args: [wallet.account.address, to, tokenId, amount, "0x"], }); + } catch (_) { + setIsLoading(false); } finally { setIsLoading(false); } @@ -404,6 +406,7 @@ export const useTransferFractionToken = (hatId: bigint, wearer: Address) => { }); } catch (error) { console.error(error); + setIsLoading(false); } finally { await publicClient.waitForTransactionReceipt({ hash: txHash! }); setIsLoading(false); diff --git a/pkgs/frontend/hooks/useWallet.ts b/pkgs/frontend/hooks/useWallet.ts index ae92393..277c896 100644 --- a/pkgs/frontend/hooks/useWallet.ts +++ b/pkgs/frontend/hooks/useWallet.ts @@ -1,6 +1,9 @@ import { ConnectedWallet, usePrivy, useWallets } from "@privy-io/react-auth"; import { createSmartAccountClient, SmartAccountClient } from "permissionless"; -import { toSimpleSmartAccount } from "permissionless/accounts"; +import { + toSimpleSmartAccount, + toThirdwebSmartAccount, +} from "permissionless/accounts"; import { createPimlicoClient } from "permissionless/clients/pimlico"; import { useCallback, useEffect, useMemo, useState } from "react"; import { @@ -62,7 +65,8 @@ export const useSmartAccountClient = (wallets: ConnectedWallet[]) => { const owner = await embeddedWallet?.getEthereumProvider(); if (!owner) return; - const smartAccount = await toSimpleSmartAccount({ + // We are using thirdweb smart account + const smartAccount = await toThirdwebSmartAccount({ owner, client: publicClient as any, entryPoint: { From 250f4156629172632ebc141a710d9e9556ecc674 Mon Sep 17 00:00:00 2001 From: aowheel Date: Thu, 9 Jan 2025 18:41:36 +0900 Subject: [PATCH 7/9] =?UTF-8?q?responsibilities=E3=80=81authorities?= =?UTF-8?q?=E3=81=AELink=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/app/components/roles/HolderDetail.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pkgs/frontend/app/components/roles/HolderDetail.tsx b/pkgs/frontend/app/components/roles/HolderDetail.tsx index 5a30bb8..34ff6cc 100644 --- a/pkgs/frontend/app/components/roles/HolderDetail.tsx +++ b/pkgs/frontend/app/components/roles/HolderDetail.tsx @@ -159,7 +159,12 @@ export const HatDetail: FC = ({ detail, imageUri }) => ( {r.label} {" "} - {r.description} + {r.description}{" "} + {r.link && ( + + ...Link + + )} ))} @@ -178,7 +183,12 @@ export const HatDetail: FC = ({ detail, imageUri }) => ( {a.label} {" "} - {a.description} + {a.description}{" "} + {a.link && ( + + ...Link + + )} ))} From aece4d82f3accb8bafff6ab49829e19a7985cc10 Mon Sep 17 00:00:00 2001 From: yu23ki14 Date: Thu, 9 Jan 2025 19:13:22 +0900 Subject: [PATCH 8/9] modify subgraph readme --- pkgs/subgraph/README.md | 55 +++-------------------------------------- 1 file changed, 4 insertions(+), 51 deletions(-) diff --git a/pkgs/subgraph/README.md b/pkgs/subgraph/README.md index aeec3b7..bf4a7c9 100644 --- a/pkgs/subgraph/README.md +++ b/pkgs/subgraph/README.md @@ -1,6 +1,6 @@ -# POAP Subgraph +# Toban Subgraph -This Subgraph sources events from the POAP contract in different networks. It has been forked from [the original](https://github.com/poap-xyz/poap-subgraph) to demonstrate uses on Goldsky. +This Subgraph sources events from the Toban related contract in different networks. ## Deploying the subgraph: @@ -10,11 +10,7 @@ This Subgraph sources events from the POAP contract in different networks. It ha yarn install ``` -Available networks: mainnet, xdai, chiado, goerli - -**Chiado deployment** - -Chiado is not index by The Graph so we use Goldsky +**deployment** First run: @@ -22,56 +18,13 @@ First run: goldsky login ``` -If you already have an existing Chiado subgraph you will have to delete it to deploy the new one +If you already have an existing subgraph you will have to delete it to deploy the new one **Deploy** -``` ---product hosted-service --access-token {TOKEN} -``` - -as extra parameters just after "graph deploy" in the package json and then execute the following: - ```ssh yarn prepare: yarn codegen yarn build yarn deploy: ``` - -**Good practices** -A good practice to deploy in mainnet or xdai is to have a duplicate/backup subgraph so that if something goes wrong, the traffic can be redirected to the duplicate subgraph instead of having to wait for the subgraph to re-deploy/rollback to a previous version. In Xdai/Gnosis it can take at least 2 days to sync. - -To build a duplicate, you need to create a new subgraph through the-graph profile. Once the new path is provided you can use the next curl to deploy a duplicate WITHOUT NEEDING to resync all over again just by copying the ID of the subgraph you are trying to duplicate. - -```ssh -curl -H "content-type: application/json" -H "authorization: Bearer {TOKEN}" --data '{"jsonrpc": "2.0", "method": "subgraph_deploy", "params": { "name": "poap-xyz/{duplicate_subgraph_path}", "ipfs_hash": "{ID_HASH_FOUND_IN_THE_ORIGINAL_SUBGRAPH}"}, "id": "1"}' https://api.thegraph.com/deploy/ -``` - -## Deployments - -### Mainnet - -Endpoint: [https://api.thegraph.com/subgraphs/name/poap-xyz/poap](https://api.thegraph.com/subgraphs/name/poap-xyz/poap) \ -Subgraph page: [https://thegraph.com/explorer/subgraph/poap-xyz/poap](https://thegraph.com/explorer/subgraph/poap-xyz/poap) - -### XDai - -Endpoint: [https://api.thegraph.com/subgraphs/name/poap-xyz/poap-xdai](https://api.thegraph.com/subgraphs/name/poap-xyz/poap-xdai) \ -Subgraph page: [https://thegraph.com/explorer/subgraph/poap-xyz/poap-xdai](https://thegraph.com/explorer/subgraph/poap-xyz/poap-xdai) - -### Chiado - -Endpoint: [https://api.goldsky.com/api/public/project_clcquosqr8v0k0iwk5rs87x2l/subgraphs/poap-xyz/poap-chiado/gn](https://api.goldsky.com/api/public/project_clcquosqr8v0k0iwk5rs87x2l/subgraphs/poap-xyz/poap-chiado/gn) \ -Subgraph page: [https://api.goldsky.com/api/public/project_clcquosqr8v0k0iwk5rs87x2l/subgraphs/poap-xyz/poap-chiado/gn](https://api.goldsky.com/api/public/project_clcquosqr8v0k0iwk5rs87x2l/subgraphs/poap-xyz/poap-chiado/gn) - -### Goerli - -Endpoint: [https://api.thegraph.com/subgraphs/name/poap-xyz/poap-goerli](https://api.thegraph.com/subgraphs/name/poap-xyz/poap-goerli) \ -Subgraph page: [https://thegraph.com/hosted-service/subgraph/poap-xyz/poap-goerli](https://thegraph.com/hosted-service/subgraph/poap-xyz/poap-goerli) - -## Notes - -### Sokol - -Previously none of the params of EventToken was indexed, due to a change in the ABI, newer events now have one of the params indexed and this may cause some issues with the-graph having to deal with malformed or missing entities for older tokens. From 139873e1c2cae85e87a50521e85594d1ed41be13 Mon Sep 17 00:00:00 2001 From: yu23ki14 Date: Thu, 9 Jan 2025 20:23:25 +0900 Subject: [PATCH 9/9] Fix bugs and modify design --- pkgs/frontend/app/components/PageHeader.tsx | 4 +- .../app/components/common/CommonIcon.tsx | 1 + .../app/components/roles/HolderDetail.tsx | 206 +++++++++--------- .../frontend/app/components/roles/RoleTag.tsx | 4 +- pkgs/frontend/app/routes/$treeId_.$hatId.tsx | 58 +++-- .../app/routes/$treeId_.$hatId_.$address.tsx | 19 +- pkgs/frontend/app/routes/$treeId_.member.tsx | 2 +- pkgs/frontend/hooks/useFractionToken.ts | 20 +- pkgs/frontend/hooks/useHatsTimeFrameModule.ts | 2 +- 9 files changed, 173 insertions(+), 143 deletions(-) diff --git a/pkgs/frontend/app/components/PageHeader.tsx b/pkgs/frontend/app/components/PageHeader.tsx index f389e7d..6bd4193 100644 --- a/pkgs/frontend/app/components/PageHeader.tsx +++ b/pkgs/frontend/app/components/PageHeader.tsx @@ -1,4 +1,4 @@ -import { HStack, IconButton, Text } from "@chakra-ui/react"; +import { Box, HStack, IconButton } from "@chakra-ui/react"; import { useNavigate } from "@remix-run/react"; import { ReactNode, useCallback } from "react"; import { FaChevronLeft } from "react-icons/fa6"; @@ -31,7 +31,7 @@ export const PageHeader: React.FC = ({ title, backLink }) => { > - {title} + {title} ); }; diff --git a/pkgs/frontend/app/components/common/CommonIcon.tsx b/pkgs/frontend/app/components/common/CommonIcon.tsx index bf6e4e0..273a600 100644 --- a/pkgs/frontend/app/components/common/CommonIcon.tsx +++ b/pkgs/frontend/app/components/common/CommonIcon.tsx @@ -22,6 +22,7 @@ export const CommonIcon = ({ return ( = ({ detail, treeId }) => ( - - - - - - {detail?.data.name ? ( - {detail.data.name} + + No name - - )} - - + + ) + } + /> ); export const RoleNameWithWearer: FC = ({ detail, - treeId, - hatId, wearerId, wearerName, wearerIcon, }) => ( - - - - - - {detail?.data.name ? ( - <> - {detail.data.name} - - - - - - {wearerName ? wearerName : abbreviateAddress(wearerId || "")} - - + + + {detail.data.name} + + + {wearerName ? wearerName : abbreviateAddress(wearerId || "")} + ) : ( - + No name - - )} - - + + ) + } + /> ); export const ActiveState: FC = ({ @@ -88,7 +86,7 @@ export const ActiveState: FC = ({ }, [wearingElapsedTime]); return ( - + {isActive ? ( = ({ rounded="md" bgColor="blue.400" fontWeight="medium" + fontSize="sm" > Active @@ -108,12 +107,13 @@ export const ActiveState: FC = ({ rounded="md" bgColor="gray.200" fontWeight="medium" + fontSize="sm" > Inactive )} {formattedWoreTime && formattedWearingElapsedTime && ( - + {isActive && ( Activated on{" "} @@ -136,68 +136,76 @@ export const ActiveState: FC = ({ export const HatDetail: FC = ({ detail, imageUri }) => ( - - - - - - Description - {detail?.data.description ? ( - {detail.data.description} - ) : ( - - No description - - )} - - - Responsibilities - {(detail?.data.responsabilities?.length ?? 0 > 0) ? ( - - {detail?.data.responsabilities?.map((r) => ( - - - {r.label} - {" "} - {r.description}{" "} - {r.link && ( - - ...Link - - )} - - ))} - - ) : ( - - No responsibilities - - )} + + + - - Authorities - {(detail?.data.authorities?.length ?? 0 > 0) ? ( - - {detail?.data.authorities?.map((a) => ( - - - {a.label} - {" "} - {a.description}{" "} - {a.link && ( - - ...Link - - )} - - ))} - - ) : ( - - No authorities + + + 説明 + + + {detail?.data.description ? ( + detail.data.description + ) : ( + + No responsibilities + + )} + {detail?.data.description} - )} - - + + + + 役割 + {(detail?.data.responsabilities?.length ?? 0 > 0) ? ( + + {detail?.data.responsabilities?.map((r) => ( + + {r.label} + {r.description} + {r.link && ( + + + リンク + + + )} + + ))} + + ) : ( + + No responsibilities + + )} + + + + 権限 + {(detail?.data.authorities?.length ?? 0 > 0) ? ( + + {detail?.data.authorities?.map((a) => ( + + {a.label} + {a.description} + {a.link && ( + + + リンク + + + )} + + ))} + + ) : ( + + No authorities + + )} + + + ); diff --git a/pkgs/frontend/app/components/roles/RoleTag.tsx b/pkgs/frontend/app/components/roles/RoleTag.tsx index b740db4..c48c280 100644 --- a/pkgs/frontend/app/components/roles/RoleTag.tsx +++ b/pkgs/frontend/app/components/roles/RoleTag.tsx @@ -15,9 +15,9 @@ export const RoleTag: FC = ({ bgColor = "yellow.400", }) => { return ( - + - + {detail?.data?.name} diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId.tsx index 4ea3f9d..4003ba0 100644 --- a/pkgs/frontend/app/routes/$treeId_.$hatId.tsx +++ b/pkgs/frontend/app/routes/$treeId_.$hatId.tsx @@ -1,5 +1,5 @@ import { Box, Heading, HStack, Text, VStack } from "@chakra-ui/react"; -import { useNavigate, useParams } from "@remix-run/react"; +import { Link, useNavigate, useParams } from "@remix-run/react"; import { useNamesByAddresses } from "hooks/useENS"; import { useHoldersWithoutWearers } from "hooks/useFractionToken"; import { useTreeInfo } from "hooks/useHats"; @@ -8,6 +8,7 @@ import { useGetWorkspace } from "hooks/useWorkspace"; import { FC, useMemo } from "react"; import { ipfs2https } from "utils/ipfs"; import { abbreviateAddress } from "utils/wallet"; +import { Address } from "viem"; import { BasicButton } from "~/components/BasicButton"; import { HatsListItemParser } from "~/components/common/HatsListItemParser"; import { UserIcon } from "~/components/icon/UserIcon"; @@ -26,7 +27,7 @@ const RoleDetails: FC = () => { const wearers = useMemo(() => hat?.wearers, [hat]); const wearerIds = useMemo( - () => wearers?.map(({ id }) => id) || [], + () => wearers?.map(({ id }) => id.toLowerCase()) || [], [wearers] ); @@ -38,7 +39,12 @@ const RoleDetails: FC = () => { hatId, wearers: wearerIds, }); - const { names: holderNames } = useNamesByAddresses(holdersWithoutWearers); + const assistantMembers = useMemo( + () => + holdersWithoutWearers.filter((h) => !wearerIds.includes(h.toLowerCase())), + [holdersWithoutWearers, wearerIds] + ); + const { names: holderNames } = useNamesByAddresses(assistantMembers); // 各wearerのWearingElapsedTimeを取得 const { data } = useGetWorkspace(treeId); @@ -47,7 +53,7 @@ const RoleDetails: FC = () => { [data] ); const timeList = useWearingElapsedTime( - hatsTimeFrameModuleAddress, + hatsTimeFrameModuleAddress as Address, hatId, wearerIds ); @@ -63,7 +69,21 @@ const RoleDetails: FC = () => { - Members + + 貢献メンバー + + + + 役割をわたす + + + + {wearerNames.flat().map((n, idx) => ( { ({ wearer }) => wearer.toLowerCase() === n.address.toLowerCase() )?.time || 0) / 86400 - )} + )}{" "} days - - Role Holder + + 役割保持者 ))} + {holderNames.flat().map((n, idx) => ( { ? `${n.name} (${abbreviateAddress(n.address)})` : abbreviateAddress(n.address)} - + Assist ))} - navigate(`/${treeId}/${hatId}/assign`)} - > - Assign Member - - ); diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx index c6a24ac..cb36853 100644 --- a/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx +++ b/pkgs/frontend/app/routes/$treeId_.$hatId_.$address.tsx @@ -137,17 +137,11 @@ const RoleHolderDetails: FC = () => { - Assist Credit Holders + アシストクレジット - - Send - + + 誰かに送る + @@ -182,7 +176,7 @@ const RoleHolderDetails: FC = () => { {isActive ? ( { await deactivate(hatId, address); setCount(count + 1); @@ -207,7 +201,8 @@ const RoleHolderDetails: FC = () => { {/* 現時点では表示されても実際にrevokeできるのはwearerのみ */} { await renounceHat(BigInt(hatId || 0)); navigate(`/${treeId}/${hatId}`); diff --git a/pkgs/frontend/app/routes/$treeId_.member.tsx b/pkgs/frontend/app/routes/$treeId_.member.tsx index 39862a9..d573c00 100644 --- a/pkgs/frontend/app/routes/$treeId_.member.tsx +++ b/pkgs/frontend/app/routes/$treeId_.member.tsx @@ -118,7 +118,7 @@ const WorkspaceMember: FC = () => { imageUri={h.imageUri} detailUri={h.details} > - + ))} diff --git a/pkgs/frontend/hooks/useFractionToken.ts b/pkgs/frontend/hooks/useFractionToken.ts index 00e90e9..f4b891b 100644 --- a/pkgs/frontend/hooks/useFractionToken.ts +++ b/pkgs/frontend/hooks/useFractionToken.ts @@ -147,16 +147,16 @@ export const useHoldersWithBalance = ({ if (!tokenId) return; const holders = [...((await getTokenRecipients({ tokenId })) || [])]; - const holdersWithBalance = await Promise.all( - holders.map(async (holder) => { - const balance = await publicClient.readContract({ - ...fractionTokenBaseConfig, - functionName: "balanceOf", - args: [wearer as Address, holder, BigInt(hatId)], - }); - return { holder, balance }; - }) - ); + const balances = await publicClient.readContract({ + ...fractionTokenBaseConfig, + functionName: "balanceOfBatch", + args: [holders, holders.map(() => BigInt(tokenId))], + }); + + const holdersWithBalance = holders.map((holder, index) => ({ + holder, + balance: balances[index], + })); setHolders(holdersWithBalance); } catch (error) { diff --git a/pkgs/frontend/hooks/useHatsTimeFrameModule.ts b/pkgs/frontend/hooks/useHatsTimeFrameModule.ts index f2eeecf..647c03f 100644 --- a/pkgs/frontend/hooks/useHatsTimeFrameModule.ts +++ b/pkgs/frontend/hooks/useHatsTimeFrameModule.ts @@ -164,7 +164,7 @@ export const useActiveState = ( }; export const useWearingElapsedTime = ( - hatsTimeFrameModuleAddress?: string, + hatsTimeFrameModuleAddress?: Address, hatId?: string, wearers?: string[] ) => {