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
+>;