diff --git a/pkgs/frontend/app/components/ContentContainer.tsx b/pkgs/frontend/app/components/ContentContainer.tsx new file mode 100644 index 00000000..874e4dda --- /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/Header.tsx b/pkgs/frontend/app/components/Header.tsx index 6cb69869..afb50cb9 100644 --- a/pkgs/frontend/app/components/Header.tsx +++ b/pkgs/frontend/app/components/Header.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, FC } from "react"; import { Box, Flex, Text } from "@chakra-ui/react"; import { WorkspaceIcon } from "./icon/WorkspaceIcon"; import { UserIcon } from "./icon/UserIcon"; @@ -14,7 +14,7 @@ import axios from "axios"; import { HatsDetailSchama } from "types/hats"; import { abbreviateAddress } from "utils/wallet"; -const NO_HEADER_PATHS: string[] = ["/login", "/signup"]; // 適宜ヘッダーが不要なページのパスを追加 +const NO_HEADER_PATHS: string[] = ["/login", "/signup", "/"]; // 適宜ヘッダーが不要なページのパスを追加 const WORKSPACES_PATHS: string[] = ["/workspace", "/workspace/new"]; // 適宜ワークスペースが未選択な状態のページのパスを追加 const headerTextStyle = { @@ -28,7 +28,7 @@ enum HeaderType { WorkspaceAndUserIcons = "WorkspaceAndUserIcons", } -export const Header = () => { +export const Header: FC = () => { const [headerType, setHeaderType] = useState( HeaderType.NonHeader ); diff --git a/pkgs/frontend/app/components/RoleAttributesList.tsx b/pkgs/frontend/app/components/RoleAttributesList.tsx new file mode 100644 index 00000000..d81456ba --- /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 a92e650c..721a8f43 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 00000000..60181b5d --- /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 00000000..9292e826 --- /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 00000000..e2becee5 --- /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 00000000..9d76526e --- /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 00000000..d87e8031 --- /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 00000000..29d24787 --- /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 00000000..30499b14 --- /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 720fbe67..82876a7d 100644 --- a/pkgs/frontend/app/root.tsx +++ b/pkgs/frontend/app/root.tsx @@ -1,21 +1,21 @@ import { withEmotionCache } from "@emotion/react"; import { - json, + data, Links, Meta, Outlet, Scripts, ScrollRestoration, } from "@remix-run/react"; -import { ApolloProvider } from "@apollo/client/react"; -import { Box, Container } from "@chakra-ui/react"; +import { ChakraProvider } from "./components/chakra-provider"; +import { useInjectStyles } from "./emotion/emotion-client"; import { PrivyProvider } from "@privy-io/react-auth"; +import { Header } from "./components/Header"; +import { ApolloProvider } from "@apollo/client/react"; +import { Container } from "@chakra-ui/react"; import type { LoaderFunctionArgs } from "@remix-run/node"; import { goldskyClient } from "utils/apollo"; -import { ChakraProvider } from "./components/chakra-provider"; -import { Header } from "./components/Header"; import i18nServer, { localeCookie } from "./config/i18n.server"; -import { useInjectStyles } from "./emotion/emotion-client"; interface LayoutProps extends React.PropsWithChildren {} @@ -23,7 +23,7 @@ export const handle = { i18n: ["translation"] }; export async function loader({ request }: LoaderFunctionArgs) { const locale = await i18nServer.getLocale(request); - return json( + return data( { locale }, { headers: { "Set-Cookie": await localeCookie.serialize(locale) } } ); 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 00000000..8c319b39 --- /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_.assistcredit-history.tsx b/pkgs/frontend/app/routes/$treeId_.assistcredit-history.tsx new file mode 100644 index 00000000..6deafbc1 --- /dev/null +++ b/pkgs/frontend/app/routes/$treeId_.assistcredit-history.tsx @@ -0,0 +1,38 @@ +import { Box, Heading, List, Text } from "@chakra-ui/react"; +import { useParams } from "@remix-run/react"; +import { useGetTransferFractionTokens } from "hooks/useFractionToken"; +import { FC } from "react"; +import { abbreviateAddress } from "utils/wallet"; +import { StickyNav } from "~/components/StickyNav"; + +const WorkspaceMember: FC = () => { + const { treeId } = useParams(); + + const { data } = useGetTransferFractionTokens({ + where: { + workspaceId: treeId, + }, + }); + + return ( + <> + {/* Members */} + + Transaction History + + {data?.transferFractionTokens.map((token) => ( + + + {abbreviateAddress(token.from)} → {abbreviateAddress(token.to)}{" "} + : {token.amount} + + + ))} + + + + + ); +}; + +export default WorkspaceMember; diff --git a/pkgs/frontend/app/routes/$treeId_.member.tsx b/pkgs/frontend/app/routes/$treeId_.member.tsx index 20f2e921..a03475ac 100644 --- a/pkgs/frontend/app/routes/$treeId_.member.tsx +++ b/pkgs/frontend/app/routes/$treeId_.member.tsx @@ -135,7 +135,7 @@ const WorkspaceMember: FC = () => { All Contributors {assistantMembers.map((m, i) => ( - + = ({ 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 c94136f8..d629bb8a 100644 --- a/pkgs/frontend/app/routes/_index.tsx +++ b/pkgs/frontend/app/routes/_index.tsx @@ -1,11 +1,7 @@ -import { Box, Input } from "@chakra-ui/react"; +import { Box, Container, Heading, Image } from "@chakra-ui/react"; import type { MetaFunction } from "@remix-run/node"; -import { useBigBang } from "hooks/useBigBang"; -import { - useUploadImageFileToIpfs, - useUploadMetadataToIpfs, -} from "hooks/useIpfs"; -import { CommonButton } from "~/components/common/CommonButton"; +import { Link } from "@remix-run/react"; +import CommonButton from "~/components/common/CommonButton"; export const meta: MetaFunction = () => { return [ @@ -15,61 +11,44 @@ export const meta: MetaFunction = () => { }; export default function Index() { - const { bigbang, isLoading } = useBigBang(); - const { uploadMetadataToIpfs, isLoading: isUploadingMetadataToIpfs } = - useUploadMetadataToIpfs(); - const { - uploadImageFileToIpfs, - setImageFile, - isLoading: isUploadingImageFileToIpfs, - } = useUploadImageFileToIpfs(); - - const handleBigBang = async () => { - const res = await bigbang({ - owner: "0xdCb93093424447bF4FE9Df869750950922F1E30B", - topHatDetails: "Top Hat Details", - topHatImageURI: "https://example.com/top-hat.png", - hatterHatDetails: "Hatter Hat Details", - hatterHatImageURI: "https://example.com/hatter-hat.png", - trustedForwarder: "0x1234567890123456789012345678901234567890", - }); - - console.log(res); - }; + return ( + + + logo + + いちばん簡単な +
+ 貢献の記録と報酬の分配 +
+ + Simplest way of contribution recording and rewards distribution. + +
- const metadata = { - name: "Toban test", - description: "Toban test", - responsibilities: "Toban test", - authorities: "Toban test", - eligibility: true, - toggle: true, - }; + + + + - return ( - - - BigBang - - uploadMetadataToIpfs(metadata)} - > - Upload Metadata to IPFS - - ) => - setImageFile(e.target.files?.[0] || null) - } - /> - - Upload Image File to IPFS - + + + + はじめる + + + ); } diff --git a/pkgs/frontend/app/routes/api.namestone.$action.tsx b/pkgs/frontend/app/routes/api.namestone.$action.tsx index 24926d70..99d8662b 100644 --- a/pkgs/frontend/app/routes/api.namestone.$action.tsx +++ b/pkgs/frontend/app/routes/api.namestone.$action.tsx @@ -1,4 +1,4 @@ -import { ActionFunction, LoaderFunction } from "@remix-run/node"; +import { ActionFunction, data, LoaderFunction } from "@remix-run/node"; import NameStone, { NameData } from "namestone-sdk"; const ns = new NameStone(import.meta.env.VITE_NAMESTONE_API_KEY); @@ -11,32 +11,30 @@ export const loader: LoaderFunction = async ({ request, params }) => { switch (action) { case "resolve-names": const addresses = searchParams.get("addresses"); - if (!addresses) return Response.json([]); + if (!addresses) return []; const resolvedNames = await Promise.all( addresses.split(",").map((address) => ns.getNames({ domain, address })) ); - return Response.json(resolvedNames); + return resolvedNames; case "resolve-addresses": const names = searchParams.get("names"); - if (!names) return Response.json([]); + if (!names) return []; const exactMatch = searchParams.get("exact_match"); const resolvedAddresses = await Promise.all( - names - .split(",") - .map((name) => - ns.searchNames({ - domain, - name, - exact_match: exactMatch === "true" ? 1 : (0 as any), - }) - ) + names.split(",").map((name) => + ns.searchNames({ + domain, + name, + exact_match: exactMatch === "true" ? 1 : (0 as any), + }) + ) ); - return Response.json(resolvedAddresses); + return resolvedAddresses; default: - throw Response.json({ message: "Not Found" }, { status: 404 }); + throw data({ message: "Not Found" }, 404); } }; @@ -49,11 +47,11 @@ export const action: ActionFunction = async ({ request, params }) => { case "set-name": const { name, address, text_records } = await request.json(); await ns.setName({ domain, name, address, text_records }); - return Response.json({ message: "OK" }); + return { message: "OK" }; default: - throw Response.json({ message: "Not Found" }, { status: 404 }); + throw data({ message: "Not Found" }, 404); } } - throw Response.json({ message: "Not Found" }, { status: 404 }); + throw data({ message: "Not Found" }, 404); }; diff --git a/pkgs/frontend/app/routes/api_.locales.ts b/pkgs/frontend/app/routes/api_.locales.ts index 0ee46f0a..ffc25f18 100644 --- a/pkgs/frontend/app/routes/api_.locales.ts +++ b/pkgs/frontend/app/routes/api_.locales.ts @@ -1,4 +1,4 @@ -import { type LoaderFunctionArgs } from "@remix-run/node"; +import { data, type LoaderFunctionArgs } from "@remix-run/node"; import { cacheHeader } from "pretty-cache-header"; import { z } from "zod"; import { resources } from "~/config/i18n"; @@ -44,5 +44,5 @@ export async function loader({ request }: LoaderFunctionArgs) { ); } - return Response.json(namespaces[ns], { headers }); + return data(namespaces[ns], { headers }); } diff --git a/pkgs/frontend/app/routes/workspace.new.tsx b/pkgs/frontend/app/routes/workspace.new.tsx index 64ede700..7f7de86e 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)} - /> - + + +
; and?: InputMaybe>>; + blockNumber?: InputMaybe; + blockNumber_gt?: InputMaybe; + blockNumber_gte?: InputMaybe; + blockNumber_in?: InputMaybe>; + blockNumber_lt?: InputMaybe; + blockNumber_lte?: InputMaybe; + blockNumber_not?: InputMaybe; + blockNumber_not_in?: InputMaybe>; + blockTimestamp?: InputMaybe; + blockTimestamp_gt?: InputMaybe; + blockTimestamp_gte?: InputMaybe; + blockTimestamp_in?: InputMaybe>; + blockTimestamp_lt?: InputMaybe; + blockTimestamp_lte?: InputMaybe; + blockTimestamp_not?: InputMaybe; + blockTimestamp_not_in?: InputMaybe>; hatId?: InputMaybe; hatId_gt?: InputMaybe; hatId_gte?: InputMaybe; @@ -104,6 +122,8 @@ export type InitializedFractionToken_Filter = { }; export enum InitializedFractionToken_OrderBy { + BlockNumber = 'blockNumber', + BlockTimestamp = 'blockTimestamp', HatId = 'hatId', Id = 'id', Wearer = 'wearer', @@ -261,6 +281,8 @@ export type SubscriptionWorkspacesArgs = { export type TransferFractionToken = { __typename?: 'TransferFractionToken'; amount: Scalars['BigInt']['output']; + blockNumber: Scalars['BigInt']['output']; + blockTimestamp: Scalars['BigInt']['output']; from: Scalars['String']['output']; hatId: Scalars['BigInt']['output']; id: Scalars['ID']['output']; @@ -282,6 +304,22 @@ export type TransferFractionToken_Filter = { amount_not?: InputMaybe; amount_not_in?: InputMaybe>; and?: InputMaybe>>; + blockNumber?: InputMaybe; + blockNumber_gt?: InputMaybe; + blockNumber_gte?: InputMaybe; + blockNumber_in?: InputMaybe>; + blockNumber_lt?: InputMaybe; + blockNumber_lte?: InputMaybe; + blockNumber_not?: InputMaybe; + blockNumber_not_in?: InputMaybe>; + blockTimestamp?: InputMaybe; + blockTimestamp_gt?: InputMaybe; + blockTimestamp_gte?: InputMaybe; + blockTimestamp_in?: InputMaybe>; + blockTimestamp_lt?: InputMaybe; + blockTimestamp_lte?: InputMaybe; + blockTimestamp_not?: InputMaybe; + blockTimestamp_not_in?: InputMaybe>; from?: InputMaybe; from_contains?: InputMaybe; from_contains_nocase?: InputMaybe; @@ -379,6 +417,8 @@ export type TransferFractionToken_Filter = { export enum TransferFractionToken_OrderBy { Amount = 'amount', + BlockNumber = 'blockNumber', + BlockTimestamp = 'blockTimestamp', From = 'from', HatId = 'hatId', Id = 'id', @@ -390,6 +430,8 @@ export enum TransferFractionToken_OrderBy { export type Workspace = { __typename?: 'Workspace'; + blockNumber: Scalars['BigInt']['output']; + blockTimestamp: Scalars['BigInt']['output']; creator: Scalars['String']['output']; hatsTimeFrameModule: Scalars['String']['output']; hatterHatId: Scalars['BigInt']['output']; @@ -402,6 +444,22 @@ export type Workspace_Filter = { /** Filter for the block changed event. */ _change_block?: InputMaybe; and?: InputMaybe>>; + blockNumber?: InputMaybe; + blockNumber_gt?: InputMaybe; + blockNumber_gte?: InputMaybe; + blockNumber_in?: InputMaybe>; + blockNumber_lt?: InputMaybe; + blockNumber_lte?: InputMaybe; + blockNumber_not?: InputMaybe; + blockNumber_not_in?: InputMaybe>; + blockTimestamp?: InputMaybe; + blockTimestamp_gt?: InputMaybe; + blockTimestamp_gte?: InputMaybe; + blockTimestamp_in?: InputMaybe>; + blockTimestamp_lt?: InputMaybe; + blockTimestamp_lte?: InputMaybe; + blockTimestamp_not?: InputMaybe; + blockTimestamp_not_in?: InputMaybe>; creator?: InputMaybe; creator_contains?: InputMaybe; creator_contains_nocase?: InputMaybe; @@ -490,6 +548,8 @@ export type Workspace_Filter = { }; export enum Workspace_OrderBy { + BlockNumber = 'blockNumber', + BlockTimestamp = 'blockTimestamp', Creator = 'creator', HatsTimeFrameModule = 'hatsTimeFrameModule', HatterHatId = 'hatterHatId', @@ -534,20 +594,28 @@ export enum _SubgraphErrorPolicy_ { Deny = 'deny' } +export type GetTransferFractionTokensQueryVariables = Exact<{ + where?: InputMaybe; +}>; + + +export type GetTransferFractionTokensQuery = { __typename?: 'Query', transferFractionTokens: Array<{ __typename?: 'TransferFractionToken', amount: any, from: string, to: string, tokenId: any, blockNumber: any, blockTimestamp: any, hatId: any, id: string, wearer: string, workspaceId: string }> }; + export type GetWorkspacesQueryVariables = Exact<{ where?: InputMaybe; }>; -export type GetWorkspacesQuery = { __typename?: 'Query', workspaces: Array<{ __typename?: 'Workspace', topHatId: any, splitCreator: string, id: string, hatterHatId: any, hatsTimeFrameModule: string, creator: string }> }; +export type GetWorkspacesQuery = { __typename?: 'Query', workspaces: Array<{ __typename?: 'Workspace', creator: string, topHatId: any, splitCreator: string, id: string, hatterHatId: any, hatsTimeFrameModule: string, blockTimestamp: any, blockNumber: any }> }; export type GetWorkspaceQueryVariables = Exact<{ workspaceId: Scalars['ID']['input']; }>; -export type GetWorkspaceQuery = { __typename?: 'Query', workspace?: { __typename?: 'Workspace', creator: string, hatsTimeFrameModule: string, hatterHatId: any, id: string, splitCreator: string, topHatId: any } | null }; +export type GetWorkspaceQuery = { __typename?: 'Query', workspace?: { __typename?: 'Workspace', creator: string, hatsTimeFrameModule: string, hatterHatId: any, id: string, splitCreator: string, topHatId: any, blockTimestamp: any, blockNumber: any } | null }; -export const GetWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaces"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace_filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"topHatId"}},{"kind":"Field","name":{"kind":"Name","value":"splitCreator"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"hatterHatId"}},{"kind":"Field","name":{"kind":"Name","value":"hatsTimeFrameModule"}},{"kind":"Field","name":{"kind":"Name","value":"creator"}}]}}]}}]} as unknown as DocumentNode; -export const GetWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"creator"}},{"kind":"Field","name":{"kind":"Name","value":"hatsTimeFrameModule"}},{"kind":"Field","name":{"kind":"Name","value":"hatterHatId"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"splitCreator"}},{"kind":"Field","name":{"kind":"Name","value":"topHatId"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const GetTransferFractionTokensDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTransferFractionTokens"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"TransferFractionToken_filter"}},"defaultValue":{"kind":"ObjectValue","fields":[]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"transferFractionTokens"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"from"}},{"kind":"Field","name":{"kind":"Name","value":"to"}},{"kind":"Field","name":{"kind":"Name","value":"tokenId"}},{"kind":"Field","name":{"kind":"Name","value":"blockNumber"}},{"kind":"Field","name":{"kind":"Name","value":"blockTimestamp"}},{"kind":"Field","name":{"kind":"Name","value":"hatId"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"wearer"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}}]}}]}}]} as unknown as DocumentNode; +export const GetWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaces"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"where"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace_filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"where"},"value":{"kind":"Variable","name":{"kind":"Name","value":"where"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"creator"}},{"kind":"Field","name":{"kind":"Name","value":"topHatId"}},{"kind":"Field","name":{"kind":"Name","value":"splitCreator"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"hatterHatId"}},{"kind":"Field","name":{"kind":"Name","value":"hatsTimeFrameModule"}},{"kind":"Field","name":{"kind":"Name","value":"blockTimestamp"}},{"kind":"Field","name":{"kind":"Name","value":"blockNumber"}}]}}]}}]} as unknown as DocumentNode; +export const GetWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"creator"}},{"kind":"Field","name":{"kind":"Name","value":"hatsTimeFrameModule"}},{"kind":"Field","name":{"kind":"Name","value":"hatterHatId"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"splitCreator"}},{"kind":"Field","name":{"kind":"Name","value":"topHatId"}},{"kind":"Field","name":{"kind":"Name","value":"blockTimestamp"}},{"kind":"Field","name":{"kind":"Name","value":"blockNumber"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/pkgs/frontend/hooks/useFractionToken.ts b/pkgs/frontend/hooks/useFractionToken.ts index dd71b572..0dc501ea 100644 --- a/pkgs/frontend/hooks/useFractionToken.ts +++ b/pkgs/frontend/hooks/useFractionToken.ts @@ -7,6 +7,13 @@ import { } from "./useContracts"; import { useActiveWallet } from "./useWallet"; import { publicClient } from "./useViem"; +import { gql } from "@apollo/client/core"; +import { useQuery } from "@apollo/client/react/hooks"; +import { + GetTransferFractionTokensQuery, + GetTransferFractionTokensQueryVariables, + TransferFractionToken_Filter, +} from "gql/graphql"; export const useTokenRecipients = ( params: { @@ -106,7 +113,6 @@ export const useBalanceOfFractionToken = ( */ export const useFractionToken = () => { const { wallet } = useActiveWallet(); - const [isLoading, setIsLoading] = useState(false); /** @@ -115,8 +121,6 @@ export const useFractionToken = () => { */ const getTokenRecipients = useCallback( async (params: { tokenId: bigint }) => { - if (!wallet) return; - setIsLoading(true); try { @@ -133,7 +137,7 @@ export const useFractionToken = () => { setIsLoading(false); } }, - [wallet] + [] ); /** @@ -143,8 +147,6 @@ export const useFractionToken = () => { */ const getTokenId = useCallback( async (params: { hatId: bigint; account: Address }) => { - if (!wallet) return; - setIsLoading(true); try { @@ -161,7 +163,7 @@ export const useFractionToken = () => { setIsLoading(false); } }, - [wallet] + [] ); const mintInitialSupplyFractionToken = useCallback( @@ -417,3 +419,39 @@ export const useTransferFractionToken = (hatId: bigint, wearer: Address) => { return { isLoading, transferFractionToken }; }; + +///////////////////////////////////// +/////// Start subgraph section ////// +///////////////////////////////////// + +const queryGetTransferFractionTokens = gql(` + query GetTransferFractionTokens($where: TransferFractionToken_filter = {}) { + transferFractionTokens(where: $where) { + amount + from + to + tokenId + blockNumber + blockTimestamp + hatId + id + wearer + workspaceId + } + } +`); + +export const useGetTransferFractionTokens = (params: { + where?: TransferFractionToken_Filter; +}) => { + const result = useQuery< + GetTransferFractionTokensQuery, + GetTransferFractionTokensQueryVariables + >(queryGetTransferFractionTokens, { + variables: { + where: params.where, + }, + }); + + return result; +}; diff --git a/pkgs/frontend/hooks/useHats.ts b/pkgs/frontend/hooks/useHats.ts index 19088b33..5c601afe 100644 --- a/pkgs/frontend/hooks/useHats.ts +++ b/pkgs/frontend/hooks/useHats.ts @@ -2,7 +2,7 @@ import { hatIdToTreeId, treeIdHexToDecimal } from "@hatsprotocol/sdk-v1-core"; import { Hat, HatsSubgraphClient, Tree } from "@hatsprotocol/sdk-v1-subgraph"; import { HATS_ABI } from "abi/hats"; import { useCallback, useEffect, useState } from "react"; -import { Address, decodeEventLog } from "viem"; +import { Address, parseEventLogs } from "viem"; import { base, optimism, sepolia } from "viem/chains"; import { HATS_ADDRESS } from "./useContracts"; import { useActiveWallet } from "./useWallet"; @@ -352,28 +352,14 @@ export const useHats = () => { 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 6b18ffae..4ed0c427 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/hooks/useWorkspace.ts b/pkgs/frontend/hooks/useWorkspace.ts index 67ea518d..780d471c 100644 --- a/pkgs/frontend/hooks/useWorkspace.ts +++ b/pkgs/frontend/hooks/useWorkspace.ts @@ -5,12 +5,14 @@ import { GetWorkspaceQuery, GetWorkspaceQueryVariables } from "gql/graphql"; const queryGetWorkspaces = gql(` query GetWorkspaces($where: Workspace_filter) { workspaces(where: $where) { + creator topHatId splitCreator id hatterHatId hatsTimeFrameModule - creator + blockTimestamp + blockNumber } } `); @@ -24,6 +26,8 @@ const queryGetWorkspace = gql(` id splitCreator topHatId + blockTimestamp + blockNumber } } `); diff --git a/pkgs/frontend/package.json b/pkgs/frontend/package.json index 8b47dd0a..2995ba48 100644 --- a/pkgs/frontend/package.json +++ b/pkgs/frontend/package.json @@ -23,7 +23,7 @@ "@emotion/server": "^11.11.0", "@hatsprotocol/sdk-v1-core": "^0.10.0", "@hatsprotocol/sdk-v1-subgraph": "^1.0.0", - "@privy-io/react-auth": "^1.95.0", + "@privy-io/react-auth": "^1.99.0", "@remix-run/node": "^2.15.0", "@remix-run/react": "^2.15.0", "@remix-run/serve": "^2.15.0", diff --git a/pkgs/frontend/public/images/favicon.ico b/pkgs/frontend/public/images/favicon.ico new file mode 100644 index 00000000..b570f989 Binary files /dev/null and b/pkgs/frontend/public/images/favicon.ico differ diff --git a/pkgs/frontend/public/images/lp/people-deco.svg b/pkgs/frontend/public/images/lp/people-deco.svg new file mode 100644 index 00000000..312f237e --- /dev/null +++ b/pkgs/frontend/public/images/lp/people-deco.svg @@ -0,0 +1,670 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pkgs/frontend/public/images/lp/wave-deco.svg b/pkgs/frontend/public/images/lp/wave-deco.svg new file mode 100644 index 00000000..5a6bec2b --- /dev/null +++ b/pkgs/frontend/public/images/lp/wave-deco.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/pkgs/frontend/public/images/toban-logo-text.svg b/pkgs/frontend/public/images/toban-logo-text.svg new file mode 100644 index 00000000..cc1adba6 --- /dev/null +++ b/pkgs/frontend/public/images/toban-logo-text.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/pkgs/frontend/types/hats.ts b/pkgs/frontend/types/hats.ts index ae584ccc..dc9b0693 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 +>; diff --git a/yarn.lock b/yarn.lock index b7240cfb..d7abbf55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4557,10 +4557,10 @@ dependencies: zod "^3.21.4" -"@privy-io/js-sdk-core@0.35.1": - version "0.35.1" - resolved "https://registry.yarnpkg.com/@privy-io/js-sdk-core/-/js-sdk-core-0.35.1.tgz#20e18d03deeb365a5e633c632d5471f67c752a98" - integrity sha512-KuE6bQ1KMw4fgK3nPClTfiC55byu/AVWyBQwrRKDh0Jacw0L8eLRhGoQtRcJOKqwa+LukiSBf6cgutlxdRVZqQ== +"@privy-io/js-sdk-core@0.37.0": + version "0.37.0" + resolved "https://registry.yarnpkg.com/@privy-io/js-sdk-core/-/js-sdk-core-0.37.0.tgz#32b370b6a4ce6d1721511f88ef499d1afd3d7beb" + integrity sha512-9TXaZfCD1+3SW05yYiO/CDiQ/+aRmgrG0jjpl8iilyfge65CEhzQ5Qp0XkOWH7cT6+pGwFO1Dh7VGPuGimlAyw== dependencies: "@ethersproject/abstract-signer" "^5.7.0" "@ethersproject/bignumber" "^5.7.0" @@ -4569,7 +4569,7 @@ "@ethersproject/transactions" "^5.7.0" "@ethersproject/units" "^5.7.0" "@privy-io/api-base" "^1.4.1" - "@privy-io/public-api" "2.15.2" + "@privy-io/public-api" "2.15.9" eventemitter3 "^5.0.1" fetch-retry "^5.0.6" jose "^4.15.5" @@ -4578,10 +4578,10 @@ set-cookie-parser "^2.6.0" uuid ">=8 <10" -"@privy-io/public-api@2.15.2": - version "2.15.2" - resolved "https://registry.yarnpkg.com/@privy-io/public-api/-/public-api-2.15.2.tgz#9e89d3aab6cce4bb568b3da3449b8d87b2b350d5" - integrity sha512-p8D2TPI0A319+tF5R8XvaaaKbXopltcd1d0/zELB6pXY5kkd1sCv9RZy+b52aweQgh3rvGrUo/lmlM11QRNPog== +"@privy-io/public-api@2.15.9": + version "2.15.9" + resolved "https://registry.yarnpkg.com/@privy-io/public-api/-/public-api-2.15.9.tgz#f79877500a3abe3f0a5bb33ff92a2a86509a7702" + integrity sha512-z5iA+A+2ih0evTjrIOdkF0JyrWltr1P32GB+Ar0WBuDgmC35/BA2aOuivO24hxWuEFXaVioNpA4NKrxUO/poJw== dependencies: "@privy-io/api-base" "1.4.1" bs58 "^5.0.0" @@ -4589,10 +4589,10 @@ libphonenumber-js "^1.10.31" zod "^3.22.4" -"@privy-io/react-auth@^1.95.0": - version "1.95.2" - resolved "https://registry.yarnpkg.com/@privy-io/react-auth/-/react-auth-1.95.2.tgz#916cfa3c2ebbfd0f5db43734f5d21ce3576ef8ea" - integrity sha512-JWJmpH16vEN5NlneniCzYmKWDnUSAtQKKwnf149/fJLKBv+AaArrUNsOcuckmspCcamAGBFNB/n8ovGDYQBqYA== +"@privy-io/react-auth@^1.99.0": + version "1.99.0" + resolved "https://registry.yarnpkg.com/@privy-io/react-auth/-/react-auth-1.99.0.tgz#444eeeaf40ad18a5330d0e1dc61dd5212ab6e0f1" + integrity sha512-XZiHfCcuvt3ZmP1Q90jGlwGMF8MZiqBE6ulFhXdkT6m5blzPXOQDHq/OO0faLURJ1oDNISJ/OPbXJYS2AcGZ+g== dependencies: "@coinbase/wallet-sdk" "4.0.3" "@ethersproject/abstract-signer" "^5.7.0" @@ -4610,7 +4610,7 @@ "@heroicons/react" "^2.1.1" "@marsidev/react-turnstile" "^0.4.1" "@metamask/eth-sig-util" "^6.0.0" - "@privy-io/js-sdk-core" "0.35.1" + "@privy-io/js-sdk-core" "0.37.0" "@simplewebauthn/browser" "^9.0.1" "@solana/wallet-adapter-base" "^0.9.23" "@solana/wallet-standard-wallet-adapter-base" "^1.1.2" @@ -4640,6 +4640,7 @@ viem "^2.21.9" web3-core "^1.8.0" web3-core-helpers "^1.8.0" + zustand "^5.0.0" "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" @@ -13169,6 +13170,11 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" +hyperlinker@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" + integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ== + i18next-browser-languagedetector@^8.0.0: version "8.0.2" resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz#037ca25c26877cad778f060a9e177054d9f8eaa3" @@ -13188,12 +13194,6 @@ i18next@^23.12.2: dependencies: "@babel/runtime" "^7.23.2" -hyperlinker@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" - integrity sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ== - - iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -19828,7 +19828,7 @@ string-hash@^1.1.3: resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -19845,6 +19845,15 @@ string-width@^2.1.1: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -19953,7 +19962,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -19981,6 +19990,13 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-bom-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" @@ -20342,10 +20358,6 @@ thunky@^1.0.2: resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== -timestring@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/timestring/-/timestring-6.0.0.tgz#b0c7c331981ecf2066ce88bcfb8ee3ae32e7a0f6" - integrity sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA== timeout-abort-controller@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/timeout-abort-controller/-/timeout-abort-controller-2.0.0.tgz#d6a59209132e520413092dd4b4d71eaaf5887feb" @@ -20355,6 +20367,11 @@ timeout-abort-controller@^2.0.0: native-abort-controller "^1.0.4" retimer "^3.0.0" +timestring@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/timestring/-/timestring-6.0.0.tgz#b0c7c331981ecf2066ce88bcfb8ee3ae32e7a0f6" + integrity sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA== + tiny-invariant@^1.0.2: version "1.3.3" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" @@ -21313,18 +21330,6 @@ viem@^2.20.1, viem@^2.21.15, viem@^2.21.51, viem@^2.21.9: webauthn-p256 "0.0.10" ws "8.18.0" -vite-env-only@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/vite-env-only/-/vite-env-only-3.0.3.tgz#cf43be571af1ed6f71d715b51625a81dc7f9d029" - integrity sha512-iAb7cTXRrvFShaF1n+G8f6Yqq7sRJcxipNYNQQu0DN5N9P55vJMmLG5lNU5moYGpd+ZH1WhBHdkWi5WjrfImHg== - dependencies: - "@babel/core" "^7.23.7" - "@babel/generator" "^7.23.6" - "@babel/parser" "^7.23.6" - "@babel/traverse" "^7.23.7" - "@babel/types" "^7.23.6" - babel-dead-code-elimination "^1.0.6" - micromatch "^4.0.5" viem@^2.21.55: version "2.22.2" resolved "https://registry.yarnpkg.com/viem/-/viem-2.22.2.tgz#ec25affced2491ea3984cc8ce5d3d4b760ae85b1" @@ -21340,6 +21345,19 @@ viem@^2.21.55: webauthn-p256 "0.0.10" ws "8.18.0" +vite-env-only@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/vite-env-only/-/vite-env-only-3.0.3.tgz#cf43be571af1ed6f71d715b51625a81dc7f9d029" + integrity sha512-iAb7cTXRrvFShaF1n+G8f6Yqq7sRJcxipNYNQQu0DN5N9P55vJMmLG5lNU5moYGpd+ZH1WhBHdkWi5WjrfImHg== + dependencies: + "@babel/core" "^7.23.7" + "@babel/generator" "^7.23.6" + "@babel/parser" "^7.23.6" + "@babel/traverse" "^7.23.7" + "@babel/types" "^7.23.6" + babel-dead-code-elimination "^1.0.6" + micromatch "^4.0.5" + vite-node@^1.2.0, vite-node@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f" @@ -21880,7 +21898,7 @@ workerpool@^6.5.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -21898,6 +21916,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -22109,6 +22136,11 @@ zod@^3.21.4, zod@^3.22.4, zod@^3.23.8: resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== +zustand@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.2.tgz#f7595ada55a565f1fd6464f002a91e701ee0cfca" + integrity sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw== + zwitch@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7"