diff --git a/pkgs/frontend/app/components/ContentContainer.tsx b/pkgs/frontend/app/components/ContentContainer.tsx new file mode 100644 index 0000000..874e4dd --- /dev/null +++ b/pkgs/frontend/app/components/ContentContainer.tsx @@ -0,0 +1,18 @@ +import { Box } from "@chakra-ui/react"; + +export const ContentContainer = ({ + children, +}: { + children: React.ReactNode; +}) => { + return ( + + {children} + + ); +}; diff --git a/pkgs/frontend/app/components/RoleAttributesList.tsx b/pkgs/frontend/app/components/RoleAttributesList.tsx new file mode 100644 index 0000000..d81456b --- /dev/null +++ b/pkgs/frontend/app/components/RoleAttributesList.tsx @@ -0,0 +1,44 @@ +import { FC } from "react"; +import { Box, Text } from "@chakra-ui/react"; +import { HatsDetailsAttributes } from "types/hats"; +import { EditRoleAttributeDialog } from "~/components/roleAttributeDialog/EditRoleAttributeDialog"; + +export const RoleAttributesList: FC<{ + items: HatsDetailsAttributes; + setItems: (value: HatsDetailsAttributes) => void; +}> = ({ items, setItems }) => { + return ( + + {items.map((_, index) => ( + + + {items[index]?.label} + + + + + + ))} + + ); +}; diff --git a/pkgs/frontend/app/components/common/CommonInput.tsx b/pkgs/frontend/app/components/common/CommonInput.tsx index a92e650..721a8f4 100644 --- a/pkgs/frontend/app/components/common/CommonInput.tsx +++ b/pkgs/frontend/app/components/common/CommonInput.tsx @@ -2,9 +2,9 @@ import { Input, InputProps } from "@chakra-ui/react"; import { FC } from "react"; interface CommonInputProps extends Omit { - minHeight?: string; + minHeight?: InputProps["minHeight"]; value: string | number; - onChange: (event: React.ChangeEvent) => void; + onChange?: (event: React.ChangeEvent) => void; } export const CommonInput: FC = ({ @@ -16,6 +16,7 @@ export const CommonInput: FC = ({ }: CommonInputProps) => { return ( { - minHeight?: string; + minHeight?: TextareaProps["minHeight"]; value: string; onChange: (event: React.ChangeEvent) => void; } diff --git a/pkgs/frontend/app/components/input/InputDescription.tsx b/pkgs/frontend/app/components/input/InputDescription.tsx new file mode 100644 index 0000000..60181b5 --- /dev/null +++ b/pkgs/frontend/app/components/input/InputDescription.tsx @@ -0,0 +1,22 @@ +import { Box, BoxProps } from "@chakra-ui/react"; +import { CommonTextArea } from "../common/CommonTextarea"; + +export const InputDescription = ({ + description, + setDescription, + ...boxProps +}: { + description: string; + setDescription: (description: string) => void; +} & BoxProps) => { + return ( + + setDescription(e.target.value)} + /> + + ); +}; diff --git a/pkgs/frontend/app/components/input/InputImage.tsx b/pkgs/frontend/app/components/input/InputImage.tsx new file mode 100644 index 0000000..9292e82 --- /dev/null +++ b/pkgs/frontend/app/components/input/InputImage.tsx @@ -0,0 +1,71 @@ +import { Box, Input, Text } from "@chakra-ui/react"; +import { CommonIcon } from "../common/CommonIcon"; +import { HiOutlinePlus } from "react-icons/hi2"; + +const EmptyImage = () => { + return ( + + + + + 画像を選択 + + ); +}; + +export const InputImage = ({ + imageFile, + setImageFile, + previousImageUrl, +}: { + imageFile: File | null; + setImageFile: (file: File | null) => void; + previousImageUrl?: string; +}) => { + const imageUrl = imageFile + ? URL.createObjectURL(imageFile) + : previousImageUrl + ? previousImageUrl + : undefined; + + return ( + + { + const file = e.target.files?.[0]; + if (file && file.type.startsWith("image/")) { + setImageFile(file); + } else { + alert("画像ファイルを選択してください"); + } + }} + /> + } + size={200} + borderRadius="3xl" + /> + + ); +}; diff --git a/pkgs/frontend/app/components/input/InputLink.tsx b/pkgs/frontend/app/components/input/InputLink.tsx new file mode 100644 index 0000000..e2becee --- /dev/null +++ b/pkgs/frontend/app/components/input/InputLink.tsx @@ -0,0 +1,20 @@ +import { Box, BoxProps } from "@chakra-ui/react"; +import { CommonInput } from "../common/CommonInput"; + +interface InputLinkProps extends BoxProps { + link: string; + setLink: (link: string) => void; +} + +export const InputLink = ({ link, setLink, ...boxProps }: InputLinkProps) => { + return ( + + setLink(e.target.value)} + /> + + ); +}; diff --git a/pkgs/frontend/app/components/input/InputName.tsx b/pkgs/frontend/app/components/input/InputName.tsx new file mode 100644 index 0000000..9d76526 --- /dev/null +++ b/pkgs/frontend/app/components/input/InputName.tsx @@ -0,0 +1,23 @@ +import { Box, BoxProps } from "@chakra-ui/react"; +import { CommonInput } from "../common/CommonInput"; + +export const InputName = ({ + name, + setName, + ...boxProps +}: { + name: string; + setName: (name: string) => void; +} & BoxProps) => { + return ( + + setName(e.target.value)} + w="100%" + /> + + ); +}; diff --git a/pkgs/frontend/app/components/roleAttributeDialog/AddRoleAttributeDialog.tsx b/pkgs/frontend/app/components/roleAttributeDialog/AddRoleAttributeDialog.tsx new file mode 100644 index 0000000..d87e803 --- /dev/null +++ b/pkgs/frontend/app/components/roleAttributeDialog/AddRoleAttributeDialog.tsx @@ -0,0 +1,47 @@ +import { Button } from "@chakra-ui/react"; +import { DialogTrigger } from "../ui/dialog"; +import { BaseRoleAttributeDialog } from "./BaseRoleAttributeDialog"; +import { HatsDetailsAttributes } from "types/hats"; + +const PlusButton = () => { + return ( + + + + ); +}; + +interface AddRoleAttributeDialogProps { + type: "responsibility" | "authority"; + attributes: HatsDetailsAttributes; + setAttributes: (attributes: HatsDetailsAttributes) => void; +} + +export const AddRoleAttributeDialog = ({ + type, + attributes, + setAttributes, +}: AddRoleAttributeDialogProps) => { + const onClick = (name: string, description: string, link: string) => { + setAttributes([...attributes, { label: name, description, link }]); + }; + + return ( + <> + } + onClick={onClick} + /> + + ); +}; diff --git a/pkgs/frontend/app/components/roleAttributeDialog/BaseRoleAttributeDialog.tsx b/pkgs/frontend/app/components/roleAttributeDialog/BaseRoleAttributeDialog.tsx new file mode 100644 index 0000000..29d2478 --- /dev/null +++ b/pkgs/frontend/app/components/roleAttributeDialog/BaseRoleAttributeDialog.tsx @@ -0,0 +1,156 @@ +import { Box, VStack, Button } from "@chakra-ui/react"; +import { + DialogRoot, + DialogContent, + DialogHeader, + DialogTitle, + DialogBody, + DialogFooter, + DialogActionTrigger, +} from "../ui/dialog"; +import { InputName } from "../input/InputName"; +import { InputDescription } from "../input/InputDescription"; +import { InputLink } from "../input/InputLink"; +import { useEffect, useState } from "react"; +import { HatsDetailsAttributes } from "types/hats"; + +const BUTTON_TEXT_MAP = { + add: "Add", + edit: "Save", +} as const; + +const DIALOG_TITLE_MAP = { + responsibility: { + add: "Add a responsibility", + edit: "Edit responsibility", + }, + authority: { + add: "Add an authority", + edit: "Edit authority", + }, +} as const; + +type RoleAttribute = HatsDetailsAttributes[number]; + +interface BaseRoleAttributeDialogProps { + attribute?: RoleAttribute; + type: "responsibility" | "authority"; + mode: "add" | "edit"; + TriggerButton: React.ReactNode; + onClick: (name: string, description: string, link: string) => void; + onClickDelete?: () => void; +} + +export const BaseRoleAttributeDialog = ({ + attribute, + type, + mode, + TriggerButton, + onClick, + onClickDelete, +}: BaseRoleAttributeDialogProps) => { + const [isOpen, setIsOpen] = useState(false); + const [name, setName] = useState(attribute?.label ?? ""); + const [description, setDescription] = useState(attribute?.description ?? ""); + const [link, setLink] = useState(attribute?.link ?? ""); + + const resetFormValues = () => { + setName(""); + setDescription(""); + setLink(""); + }; + + const setAttribute = (attribute: RoleAttribute) => { + setName(attribute.label); + setDescription(attribute.description ?? ""); + setLink(attribute.link ?? ""); + }; + + useEffect(() => { + if (mode === "edit" && attribute) { + setAttribute(attribute); + } + }, [attribute, mode]); + + return ( + <> + { + setIsOpen(details.open); + if (!details.open) { + resetFormValues(); + } else { + if (mode === "edit" && attribute) { + setAttribute(attribute); + } + } + }} + > + {TriggerButton} + + + + {DIALOG_TITLE_MAP[type][mode]} + + + + + + + + + + + + + + + {mode === "edit" && onClickDelete && ( + + + + )} + + + + + + ); +}; diff --git a/pkgs/frontend/app/components/roleAttributeDialog/EditRoleAttributeDialog.tsx b/pkgs/frontend/app/components/roleAttributeDialog/EditRoleAttributeDialog.tsx new file mode 100644 index 0000000..30499b1 --- /dev/null +++ b/pkgs/frontend/app/components/roleAttributeDialog/EditRoleAttributeDialog.tsx @@ -0,0 +1,61 @@ +import { Button } from "@chakra-ui/react"; +import { DialogTrigger } from "../ui/dialog"; +import { BaseRoleAttributeDialog } from "./BaseRoleAttributeDialog"; +import { HatsDetailsAttributes } from "types/hats"; +import { GrEdit } from "react-icons/gr"; + +const PencilButton = () => { + return ( + + + + ); +}; + +interface EditRoleAttributeDialogProps { + type: "responsibility" | "authority"; + attributes: HatsDetailsAttributes; + setAttributes: (attributes: HatsDetailsAttributes) => void; + attributeIndex: number; +} + +export const EditRoleAttributeDialog = ({ + type, + attributes, + setAttributes, + attributeIndex, +}: EditRoleAttributeDialogProps) => { + const onClick = (name: string, description: string, link: string) => { + const newAttributes = [ + ...attributes.slice(0, attributeIndex), + { ...attributes[attributeIndex], label: name, description, link }, + ...attributes.slice(attributeIndex + 1), + ]; + setAttributes(newAttributes); + }; + + const onClickDelete = () => { + setAttributes(attributes.filter((_, index) => index !== attributeIndex)); + }; + + return ( + <> + } + onClick={onClick} + onClickDelete={onClickDelete} + /> + + ); +}; diff --git a/pkgs/frontend/app/root.tsx b/pkgs/frontend/app/root.tsx index 794a806..a0ccf80 100644 --- a/pkgs/frontend/app/root.tsx +++ b/pkgs/frontend/app/root.tsx @@ -9,7 +9,7 @@ import { import { ChakraProvider } from "./components/chakra-provider"; import { useInjectStyles } from "./emotion/emotion-client"; import { PrivyProvider } from "@privy-io/react-auth"; -import { Box, Container } from "@chakra-ui/react"; +import { Container } from "@chakra-ui/react"; import { Header } from "./components/Header"; import { ApolloProvider } from "@apollo/client/react"; import { goldskyClient } from "utils/apollo"; diff --git a/pkgs/frontend/app/routes/$treeId.tsx b/pkgs/frontend/app/routes/$treeId._index.tsx similarity index 100% rename from pkgs/frontend/app/routes/$treeId.tsx rename to pkgs/frontend/app/routes/$treeId._index.tsx diff --git a/pkgs/frontend/app/routes/$treeId_.$hatId_.edit.tsx b/pkgs/frontend/app/routes/$treeId_.$hatId_.edit.tsx new file mode 100644 index 0000000..8c319b3 --- /dev/null +++ b/pkgs/frontend/app/routes/$treeId_.$hatId_.edit.tsx @@ -0,0 +1,229 @@ +import { FC, useEffect, useState } from "react"; +import { useNavigate, useParams } from "@remix-run/react"; +import { Box, Text } from "@chakra-ui/react"; +import { InputImage } from "~/components/input/InputImage"; +import { + useUploadImageFileToIpfs, + useUploadHatsDetailsToIpfs, +} from "hooks/useIpfs"; +import { ContentContainer } from "~/components/ContentContainer"; +import { InputName } from "~/components/input/InputName"; +import { InputDescription } from "~/components/input/InputDescription"; +import { BasicButton } from "~/components/BasicButton"; +import { useActiveWallet } from "hooks/useWallet"; +import { useHats } from "hooks/useHats"; +import { + HatsDetailsAttributes, + HatsDetailsAuthorities, + HatsDetailSchama, + HatsDetailsResponsabilities, +} from "types/hats"; +import { AddRoleAttributeDialog } from "~/components/roleAttributeDialog/AddRoleAttributeDialog"; +import { ipfs2https, ipfs2httpsJson } from "utils/ipfs"; +import { Hat } from "@hatsprotocol/sdk-v1-subgraph"; +import { RoleAttributesList } from "~/components/RoleAttributesList"; + +const SectionHeading: FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} +); + +const EditRole: FC = () => { + const { treeId, hatId } = useParams(); + + const { uploadImageFileToIpfs, imageFile, setImageFile } = + useUploadImageFileToIpfs(); + + const [roleName, setRoleName] = useState(""); + const [roleDescription, setRoleDescription] = useState(""); + + const [responsibilities, setResponsibilities] = useState< + NonNullable + >([]); + + const [authorities, setAuthorities] = useState< + NonNullable + >([]); + const { wallet } = useActiveWallet(); + const [isLoading, setIsLoading] = useState(false); + const { changeHatDetails, changeHatImageURI } = useHats(); + const { uploadHatsDetailsToIpfs } = useUploadHatsDetailsToIpfs(); + const navigate = useNavigate(); + const { getHat } = useHats(); + const [hat, setHat] = useState(undefined); + const [details, setDetails] = useState( + undefined + ); + + useEffect(() => { + const fetchHat = async () => { + if (!hatId) return; + const resHat = await getHat(hatId); + console.log("hat", resHat); + if (resHat && resHat !== hat) { + setHat(resHat); + } + }; + fetchHat(); + }, [hatId]); + + useEffect(() => { + const setStates = async () => { + if (!hat) return; + const detailsJson: HatsDetailSchama = hat.details + ? await ipfs2httpsJson(hat.details) + : undefined; + console.log("detailsJson", detailsJson); + setDetails(detailsJson); + setRoleName(detailsJson?.data.name ?? ""); + setRoleDescription(detailsJson?.data.description ?? ""); + setResponsibilities(detailsJson?.data.responsabilities ?? []); + setAuthorities(detailsJson?.data.authorities ?? []); + }; + setStates(); + }, [hat]); + + const areArraysEqual = ( + arr1: HatsDetailsAttributes, + arr2: HatsDetailsAttributes + ) => { + if (arr1.length !== arr2.length) return false; + return JSON.stringify(arr1) === JSON.stringify(arr2); + }; + + const isChangedDetails = () => { + if (!details) return false; + + return ( + details.data.name !== roleName || + details.data.description !== roleDescription || + !areArraysEqual(details.data.responsabilities ?? [], responsibilities) || + !areArraysEqual(details.data.authorities ?? [], authorities) + ); + }; + + const changeDetails = async () => { + if (!hatId) return; + + const isChanged = isChangedDetails(); + if (!isChanged) return; + + const resUploadHatsDetails = await uploadHatsDetailsToIpfs({ + name: roleName, + description: roleDescription, + responsabilities: responsibilities, + authorities: authorities, + }); + if (!resUploadHatsDetails) + throw new Error("Failed to upload metadata to ipfs"); + const ipfsUri = resUploadHatsDetails.ipfsUri; + const parsedLog = await changeHatDetails({ + hatId: BigInt(hatId), + newDetails: ipfsUri, + }); + if (!parsedLog) throw new Error("Failed to change hat details"); + console.log("parsedLog", parsedLog); + }; + + const changeImage = async () => { + if (!hatId || !hat || !imageFile) return; + const resUploadImage = await uploadImageFileToIpfs(); + if (!resUploadImage) throw new Error("Failed to upload image to ipfs"); + const ipfsUri = resUploadImage.ipfsUri; + const parsedLog = await changeHatImageURI({ + hatId: BigInt(hatId), + newImageURI: ipfsUri, + }); + if (!parsedLog) throw new Error("Failed to change hat image"); + console.log("parsedLog", parsedLog); + }; + + const handleSubmit = async () => { + if (!wallet) { + alert("ウォレットを接続してください。"); + return; + } + if (!roleName || !roleDescription) { + alert("全ての項目を入力してください。"); + return; + } + + try { + setIsLoading(true); + + await Promise.all([changeDetails(), changeImage()]); + + navigate(`/${treeId}/roles`); + } catch (error) { + console.error(error); + alert("エラーが発生しました。" + error); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + + ロールを編集 + + + + + + Responsibilities + + + + + Authorities + + + + + + + 保存 + + + + + ); +}; + +export default EditRole; diff --git a/pkgs/frontend/app/routes/$treeId_.roles_.new.tsx b/pkgs/frontend/app/routes/$treeId_.roles_.new.tsx new file mode 100644 index 0000000..9bca4c9 --- /dev/null +++ b/pkgs/frontend/app/routes/$treeId_.roles_.new.tsx @@ -0,0 +1,164 @@ +import { FC, useState } from "react"; +import { useNavigate, useParams } from "@remix-run/react"; +import { Box, Text } from "@chakra-ui/react"; +import { InputImage } from "~/components/input/InputImage"; +import { + useUploadImageFileToIpfs, + useUploadHatsDetailsToIpfs, +} from "hooks/useIpfs"; +import { ContentContainer } from "~/components/ContentContainer"; +import { InputName } from "~/components/input/InputName"; +import { InputDescription } from "~/components/input/InputDescription"; +import { BasicButton } from "~/components/BasicButton"; +import { useActiveWallet } from "hooks/useWallet"; +import { useHats } from "hooks/useHats"; +import { + HatsDetailsAuthorities, + HatsDetailsResponsabilities, +} from "types/hats"; +import { AddRoleAttributeDialog } from "~/components/roleAttributeDialog/AddRoleAttributeDialog"; +import { RoleAttributesList } from "~/components/RoleAttributesList"; +import { PageHeader } from "~/components/PageHeader"; + +const SectionHeading: FC<{ children: React.ReactNode }> = ({ children }) => ( + {children} +); + +const NewRole: FC = () => { + const { treeId } = useParams(); + + const { uploadImageFileToIpfs, imageFile, setImageFile } = + useUploadImageFileToIpfs(); + + const [roleName, setRoleName] = useState(""); + const [roleDescription, setRoleDescription] = useState(""); + + const [responsibilities, setResponsibilities] = useState< + NonNullable + >([]); + + const [authorities, setAuthorities] = useState< + NonNullable + >([]); + const { wallet } = useActiveWallet(); + const [isLoading, setIsLoading] = useState(false); + const { createHat } = useHats(); + const { uploadHatsDetailsToIpfs } = useUploadHatsDetailsToIpfs(); + const { getTreeInfo } = useHats(); + const navigate = useNavigate(); + + const handleSubmit = async () => { + if (!wallet) { + alert("ウォレットを接続してください。"); + return; + } + if (!roleName || !roleDescription || !imageFile) { + alert("全ての項目を入力してください。"); + return; + } + + try { + setIsLoading(true); + + const [resUploadHatsDetails, resUploadImage, treeInfo] = + await Promise.all([ + uploadHatsDetailsToIpfs({ + name: roleName, + description: roleDescription, + responsabilities: responsibilities, + authorities: authorities, + }), + uploadImageFileToIpfs(), + getTreeInfo({ treeId: Number(treeId) }), + ]); + + if (!resUploadHatsDetails) + throw new Error("Failed to upload metadata to ipfs"); + if (!resUploadImage) throw new Error("Failed to upload image to ipfs"); + + const hatterHatId = treeInfo?.hats?.[1]?.id; + if (!hatterHatId) throw new Error("Hat ID is required"); + + console.log("resUploadHatsDetails", resUploadHatsDetails); + console.log("resUploadImage", resUploadImage); + console.log("hatterHatId", hatterHatId); + + const parsedLog = await createHat({ + parentHatId: BigInt(hatterHatId), + details: resUploadHatsDetails?.ipfsUri, + imageURI: resUploadImage?.ipfsUri, + }); + if (!parsedLog) throw new Error("Failed to create hat transaction"); + console.log("parsedLog", parsedLog); + + navigate(`/${treeId}/roles`); + } catch (error) { + console.error(error); + alert("エラーが発生しました。" + error); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + + + + + + + + Responsibilities + + + + + Authorities + + + + + + + 作成 + + + + + ); +}; + +export default NewRole; diff --git a/pkgs/frontend/app/routes/_index.tsx b/pkgs/frontend/app/routes/_index.tsx index dabd1c3..db7a5f6 100644 --- a/pkgs/frontend/app/routes/_index.tsx +++ b/pkgs/frontend/app/routes/_index.tsx @@ -3,7 +3,7 @@ import type { MetaFunction } from "@remix-run/node"; import { CommonButton } from "~/components/common/CommonButton"; import { useBigBang } from "hooks/useBigBang"; import { - useUploadMetadataToIpfs, + useUploadHatsDetailsToIpfs, useUploadImageFileToIpfs, } from "hooks/useIpfs"; @@ -16,8 +16,8 @@ export const meta: MetaFunction = () => { export default function Index() { const { bigbang, isLoading } = useBigBang(); - const { uploadMetadataToIpfs, isLoading: isUploadingMetadataToIpfs } = - useUploadMetadataToIpfs(); + const { uploadHatsDetailsToIpfs, isLoading: isUploadingHatsDetailsToIpfs } = + useUploadHatsDetailsToIpfs(); const { uploadImageFileToIpfs, setImageFile, @@ -31,7 +31,6 @@ export default function Index() { topHatImageURI: "https://example.com/top-hat.png", hatterHatDetails: "Hatter Hat Details", hatterHatImageURI: "https://example.com/hatter-hat.png", - trustedForwarder: "0x1234567890123456789012345678901234567890", }); console.log(res); @@ -40,10 +39,8 @@ export default function Index() { const metadata = { name: "Toban test", description: "Toban test", - responsibilities: "Toban test", - authorities: "Toban test", - eligibility: true, - toggle: true, + responsabilities: [], + authorities: [], }; return ( @@ -52,8 +49,8 @@ export default function Index() { BigBang uploadMetadataToIpfs(metadata)} + loading={isUploadingHatsDetailsToIpfs} + onClick={() => uploadHatsDetailsToIpfs(metadata)} > Upload Metadata to IPFS diff --git a/pkgs/frontend/app/routes/workspace.new.tsx b/pkgs/frontend/app/routes/workspace.new.tsx index 64ede70..7f7de86 100644 --- a/pkgs/frontend/app/routes/workspace.new.tsx +++ b/pkgs/frontend/app/routes/workspace.new.tsx @@ -1,26 +1,25 @@ import { FC, useState } from "react"; -import { Box, Float, Grid, Input, Text } from "@chakra-ui/react"; -import { HiOutlinePlus } from "react-icons/hi2"; -import { CommonInput } from "~/components/common/CommonInput"; +import { Box, Grid } from "@chakra-ui/react"; import { BasicButton } from "~/components/BasicButton"; -import { CommonTextArea } from "~/components/common/CommonTextarea"; import { - useUploadMetadataToIpfs, + useUploadHatsDetailsToIpfs, useUploadImageFileToIpfs, } from "hooks/useIpfs"; import { useNavigate } from "@remix-run/react"; -import { CommonIcon } from "~/components/common/CommonIcon"; import { useBigBang } from "hooks/useBigBang"; import { useActiveWallet } from "hooks/useWallet"; import { Address } from "viem"; import { hatIdToTreeId } from "@hatsprotocol/sdk-v1-core"; import { PageHeader } from "~/components/PageHeader"; +import { InputImage } from "~/components/input/InputImage"; +import { InputName } from "~/components/input/InputName"; +import { InputDescription } from "~/components/input/InputDescription"; const WorkspaceNew: FC = () => { const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [isLoading, setIsLoading] = useState(false); - const { uploadMetadataToIpfs } = useUploadMetadataToIpfs(); + const { uploadHatsDetailsToIpfs } = useUploadHatsDetailsToIpfs(); const { uploadImageFileToIpfs, imageFile, setImageFile } = useUploadImageFileToIpfs(); const { bigbang } = useBigBang(); @@ -39,13 +38,11 @@ const WorkspaceNew: FC = () => { setIsLoading(true); try { - const resUploadMetadata = await uploadMetadataToIpfs({ + const resUploadMetadata = await uploadHatsDetailsToIpfs({ name, description, - responsibilities: "", - authorities: "", - eligibility: true, - toggle: true, + responsabilities: [], + authorities: [], }); if (!resUploadMetadata) throw new Error("Failed to upload metadata to ipfs"); @@ -82,34 +79,6 @@ const WorkspaceNew: FC = () => { } }; - const EmptyImage = () => { - return ( - - - - - 画像を選択 - - ); - }; - return ( @@ -121,43 +90,13 @@ const WorkspaceNew: FC = () => { mt={10} alignItems="center" > - - { - const file = e.target.files?.[0]; - if (file && file.type.startsWith("image/")) { - setImageFile(file); - } else { - alert("画像ファイルを選択してください"); - } - }} - /> - } - size={200} - borderRadius="3xl" - /> - - - setName(e.target.value)} - /> - - - setDescription(e.target.value)} - /> - + + + { hash: txHash, }); - const log = receipt.logs.find((log) => { - try { - const decodedLog = decodeEventLog({ - abi: HATS_ABI, - data: log.data, - topics: log.topics, - }); - return decodedLog.eventName === "HatCreated"; - } catch (error) { - console.error("error occured when creating Hats :", error); - } - })!; - - if (log) { - const decodedLog = decodeEventLog({ - abi: HATS_ABI, - data: log.data, - topics: log.topics, - }); - console.log({ decodedLog }); - } - return txHash; + const parsedLog = parseEventLogs({ + abi: HATS_ABI, + eventName: "HatCreated", + logs: receipt.logs, + strict: false, + }); + + return parsedLog; } catch (error) { console.error("error occured when creating Hats:", error); } finally { @@ -418,6 +404,76 @@ export const useHats = () => { [wallet] ); + const changeHatDetails = useCallback( + async (params: { hatId: bigint; newDetails: string }) => { + if (!wallet) return; + + setIsLoading(true); + + try { + const txHash = await wallet.writeContract({ + abi: HATS_ABI, + address: HATS_ADDRESS, + functionName: "changeHatDetails", + args: [params.hatId, params.newDetails], + }); + + const receipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + + const parsedLog = parseEventLogs({ + abi: HATS_ABI, + eventName: "HatDetailsChanged", + logs: receipt.logs, + strict: false, + }); + + return parsedLog; + } catch (error) { + console.error("error occured when creating Hats:", error); + } finally { + setIsLoading(false); + } + }, + [wallet] + ); + + const changeHatImageURI = useCallback( + async (params: { hatId: bigint; newImageURI: string }) => { + if (!wallet) return; + + setIsLoading(true); + + try { + const txHash = await wallet.writeContract({ + abi: HATS_ABI, + address: HATS_ADDRESS, + functionName: "changeHatImageURI", + args: [params.hatId, params.newImageURI], + }); + + const receipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + + const parsedLog = parseEventLogs({ + abi: HATS_ABI, + eventName: "HatImageURIChanged", + logs: receipt.logs, + strict: false, + }); + + return parsedLog; + } catch (error) { + console.error("error occured when creating Hats:", error); + } finally { + setIsLoading(false); + } + }, + [wallet] + ); + return { isLoading, getTreeInfo, @@ -429,6 +485,8 @@ export const useHats = () => { getHatsTimeframeModuleAddress, createHat, mintHat, + changeHatDetails, + changeHatImageURI, }; }; diff --git a/pkgs/frontend/hooks/useIpfs.ts b/pkgs/frontend/hooks/useIpfs.ts index 6b18ffa..4ed0c42 100644 --- a/pkgs/frontend/hooks/useIpfs.ts +++ b/pkgs/frontend/hooks/useIpfs.ts @@ -1,40 +1,19 @@ import { useState } from "react"; +import { HatsDetailSchama, HatsDetailsData } from "types/hats"; import { ipfsUploadJson, ipfsUploadFile } from "utils/ipfs"; export const useUploadMetadataToIpfs = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const uploadMetadataToIpfs = async ({ - name, - description, - responsibilities, - authorities, - eligibility, - toggle, - }: { - name: string; - description: string; - responsibilities: string; - authorities: string; - eligibility: boolean; - toggle: boolean; - }): Promise<{ ipfsCid: string; ipfsUri: string } | null> => { + const uploadMetadataToIpfs = async ( + metadata: object + ): Promise<{ ipfsCid: string; ipfsUri: string } | null> => { setIsLoading(true); setError(null); try { - const upload = await ipfsUploadJson({ - type: "1.0", - data: { - name, - description, - responsibilities, - authorities, - eligibility, - toggle, - }, - }); + const upload = await ipfsUploadJson(metadata); const ipfsCid = upload.IpfsHash; const ipfsUri = `ipfs://${ipfsCid}`; @@ -57,6 +36,33 @@ export const useUploadMetadataToIpfs = () => { return { uploadMetadataToIpfs, isLoading, error }; }; +export const useUploadHatsDetailsToIpfs = () => { + const { uploadMetadataToIpfs, isLoading, error } = useUploadMetadataToIpfs(); + + const uploadHatsDetailsToIpfs = async ({ + name, + description, + responsabilities, + authorities, + }: HatsDetailsData): Promise<{ ipfsCid: string; ipfsUri: string } | null> => { + const details: HatsDetailSchama = { + type: "1.0", + data: { + name, + description, + responsabilities, + authorities, + }, + }; + + const res = await uploadMetadataToIpfs(details); + + return res; + }; + + return { uploadHatsDetailsToIpfs, isLoading, error }; +}; + export const useUploadImageFileToIpfs = () => { const [isLoading, setIsLoading] = useState(false); const [imageFile, setImageFile] = useState(null); diff --git a/pkgs/frontend/types/hats.ts b/pkgs/frontend/types/hats.ts index ae584cc..dc9b069 100644 --- a/pkgs/frontend/types/hats.ts +++ b/pkgs/frontend/types/hats.ts @@ -18,3 +18,14 @@ export interface HatsDetailSchama { }[]; }; } + +export type HatsDetailsData = HatsDetailSchama["data"]; + +export type HatsDetailsResponsabilities = HatsDetailsData["responsabilities"]; + +export type HatsDetailsAuthorities = HatsDetailsData["authorities"]; + +// 共通の型を作成 +export type HatsDetailsAttributes = NonNullable< + HatsDetailsResponsabilities | HatsDetailsAuthorities +>;