Skip to content

Commit

Permalink
Merge pull request #216 from hackdays-io/issue/201
Browse files Browse the repository at this point in the history
Send Fraction Token
  • Loading branch information
yu23ki14 authored Dec 17, 2024
2 parents cb6cc9d + af46b0f commit d22586a
Show file tree
Hide file tree
Showing 16 changed files with 345 additions and 63 deletions.
37 changes: 37 additions & 0 deletions pkgs/frontend/app/components/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { HStack, IconButton, Text } from "@chakra-ui/react";
import { useNavigate } from "@remix-run/react";
import { ReactNode, useCallback } from "react";
import { FaChevronLeft } from "react-icons/fa6";

interface Props {
title: string | ReactNode;
backLink?: string | (() => void);
}

export const PageHeader: React.FC<Props> = ({ title, backLink }) => {
const navigate = useNavigate();

const handleBack = useCallback(() => {
if (backLink && typeof backLink === "string") {
navigate(backLink);
} else if (backLink && typeof backLink === "function") {
backLink();
} else {
navigate(-1);
}
}, [backLink]);

return (
<HStack>
<IconButton
size="sm"
onClick={handleBack}
bgColor="transparent"
color="black"
>
<FaChevronLeft />
</IconButton>
<Text>{title}</Text>
</HStack>
);
};
2 changes: 1 addition & 1 deletion pkgs/frontend/app/components/common/CommonIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Box, Image } from "@chakra-ui/react";

interface CommonIconProps {
imageUrl: string | undefined;
size: number | "full";
size: number | `${number}px` | "full";
fallbackIconComponent?: ReactNode;
}

Expand Down
1 change: 1 addition & 0 deletions pkgs/frontend/app/components/common/CommonInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const CommonInput = ({
width="100%"
onChange={onChange}
borderColor="gray.800"
backgroundColor="white"
/>
);
};
2 changes: 1 addition & 1 deletion pkgs/frontend/app/components/icon/RoleIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ipfs2https } from "utils/ipfs";

interface RoleIconProps {
roleImageUrl?: string;
size?: number | "full";
size?: number | `${number}px` | "full";
}

export const RoleIcon = ({ roleImageUrl, size = "full" }: RoleIconProps) => {
Expand Down
2 changes: 1 addition & 1 deletion pkgs/frontend/app/components/icon/UserIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CommonIcon } from "../common/CommonIcon";

interface UserIconProps {
userImageUrl: string | undefined;
size?: number | "full";
size?: number | `${number}px` | "full";
}

export const UserIcon = ({ userImageUrl, size = "full" }: UserIconProps) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import {
Box,
Float,
HStack,
Input,
List,
Text,
VStack,
} from "@chakra-ui/react";
import { useParams } from "@remix-run/react";
import {
useActiveWalletIdentity,
useAddressesByNames,
useNamesByAddresses,
} from "hooks/useENS";
import {
useBalanceOfFractionToken,
useFractionToken,
} from "hooks/useFractionToken";
import { useTreeInfo } from "hooks/useHats";
import { NameData, TextRecords } from "namestone-sdk";
import { FC, useCallback, useEffect, useMemo, useState } from "react";
import { FaArrowRight } from "react-icons/fa6";
import { ipfs2https } from "utils/ipfs";
import { abbreviateAddress } from "utils/wallet";
import { Address } from "viem";
import { BasicButton } from "~/components/BasicButton";
import { CommonInput } from "~/components/common/CommonInput";
import { RoleIcon } from "~/components/icon/RoleIcon";
import { UserIcon } from "~/components/icon/UserIcon";
import { PageHeader } from "~/components/PageHeader";
import { Field } from "~/components/ui/field";

const AssistCreditSend: FC = () => {
const { treeId, hatId, address } = useParams();
const me = useActiveWalletIdentity();
const balanceOfToken = useBalanceOfFractionToken(
me.identity?.address as Address,
address as Address,
BigInt(hatId!)
);

// 送信先取得
const tree = useTreeInfo(Number(treeId));
const [searchText, setSearchText] = useState<string>("");

const members = useMemo(() => {
if (!tree || !tree.hats) return [];
return tree.hats
.filter((h) => h.levelAtLocalTree && h.levelAtLocalTree >= 0)
.map((h) => h.wearers)
.flat()
.filter((w) => w)
.map((w) => w!.id);
}, [tree]);

const { names: defaultNames } = useNamesByAddresses(members);
const { names, fetchNames } = useNamesByAddresses();
const { addresses, fetchAddresses } = useAddressesByNames();

const isSearchAddress = useMemo(() => {
return searchText.startsWith("0x") && searchText.length === 42;
}, [searchText]);

useEffect(() => {
if (isSearchAddress) {
fetchNames([searchText]);
} else {
fetchAddresses([searchText]);
}
}, [searchText, isSearchAddress]);

const users = useMemo(() => {
if (!searchText) {
const unresolvedMembers = Array.from(
new Set(
members.filter((m) => !defaultNames[0].find((n) => n.address === m))
)
);
return [
...defaultNames,
...unresolvedMembers.map((m) => [
{
address: m,
name: "",
domain: "",
text_records: {
avatar: "",
} as TextRecords,
},
]),
];
}

return isSearchAddress ? names : addresses;
}, [defaultNames, names, addresses, isSearchAddress]);

// 送信先選択後
const [receiver, setReceiver] = useState<NameData>();
const [amount, setAmount] = useState<number>(0);

const { sendFractionToken } = useFractionToken();
const send = useCallback(async () => {
if (!receiver || !hatId || !me) return;
await sendFractionToken({
hatId: BigInt(hatId),
account: me.identity?.address as Address,
to: receiver.address as Address,
amount: BigInt(amount),
});
}, [sendFractionToken, receiver, amount, hatId, address]);

return (
<Box>
<PageHeader
title={
receiver
? `${receiver.name || `${abbreviateAddress(receiver.address)}に送信`}`
: "アシストクレジット送信"
}
backLink={
receiver &&
(() => {
setReceiver(undefined);
setAmount(0);
})
}
/>

<HStack my={2}>
<RoleIcon size="50px" />
<Text>掃除当番(残高: {balanceOfToken?.toLocaleString()}</Text>
</HStack>

{!receiver ? (
<>
<Field label="ユーザー名 or ウォレットアドレスで検索">
<CommonInput
value={searchText}
onChange={(e) => {
setSearchText(e.target.value);
}}
placeholder="ユーザー名 or ウォレットアドレス"
/>
</Field>

<List.Root listStyle="none" my={10} gap={3}>
{users?.flat().map((user, index) => (
<List.Item key={index} onClick={() => setReceiver(user)}>
<HStack>
<UserIcon
userImageUrl={ipfs2https(user.text_records?.avatar)}
size={10}
/>
<Text lineBreak="anywhere">
{user.name
? `${user.name} (${user.address.slice(0, 6)}...${user.address.slice(-4)})`
: user.address}
</Text>
</HStack>
</List.Item>
))}
</List.Root>
</>
) : (
<>
<Field
label="送信量"
mt="calc(50vh - 230px)"
alignItems="center"
justifyContent="center"
>
<Input
p={2}
pb={4}
fontSize="60px"
size="2xl"
border="none"
borderBottom="2px solid"
borderRadius="0"
w="auto"
type="number"
textAlign="center"
min={0}
max={9999}
style={{
WebkitAppearance: "none",
}}
value={amount}
onChange={(e) => setAmount(Number(e.target.value))}
/>
</Field>

<Float
placement="bottom-center"
mb="7vh"
width="100%"
display="flex"
flexDirection="column"
alignItems="center"
>
<HStack columnGap={3} mb={4}>
<Box textAlign="center">
<UserIcon
size={10}
userImageUrl={ipfs2https(me.identity?.text_records?.avatar)}
/>
<Text fontSize="xs">{me.identity?.name}</Text>
</Box>
<VStack textAlign="center">
<Text>{amount}</Text>
<FaArrowRight size="20px" />
</VStack>
<Box>
<UserIcon
size={10}
userImageUrl={ipfs2https(receiver.text_records?.avatar)}
/>
<Text fontSize="xs">
{receiver.name || abbreviateAddress(receiver.address)}
</Text>
</Box>
</HStack>
<BasicButton onClick={send}>送信</BasicButton>
</Float>
</>
)}
</Box>
);
};

export default AssistCreditSend;
13 changes: 10 additions & 3 deletions pkgs/frontend/app/routes/api.namestone.$action.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,32 @@ const domain = "toban.eth";

export const loader: LoaderFunction = async ({ request, params }) => {
const { action } = params;
const searchParams = new URL(request.url).searchParams;

switch (action) {
case "resolve-names":
const addresses = new URL(request.url).searchParams.get("addresses");
const addresses = searchParams.get("addresses");
if (!addresses) return Response.json([]);

const resolvedNames = await Promise.all(
addresses.split(",").map((address) => ns.getNames({ domain, address }))
);
return Response.json(resolvedNames);
case "resolve-addresses":
const names = new URL(request.url).searchParams.get("names");
const names = searchParams.get("names");
if (!names) return Response.json([]);

const exactMatch = searchParams.get("exact_match");

const resolvedAddresses = await Promise.all(
names
.split(",")
.map((name) =>
ns.searchNames({ domain, name, exact_match: 1 as any })
ns.searchNames({
domain,
name,
exact_match: exactMatch === "true" ? 1 : (0 as any),
})
)
);
return Response.json(resolvedAddresses);
Expand Down
2 changes: 1 addition & 1 deletion pkgs/frontend/app/routes/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const Login: FC = () => {
const names = useMemo(() => {
return userName ? [userName] : [];
}, [userName]);
const { addresses } = useAddressesByNames(names);
const { addresses } = useAddressesByNames(names, true);

const availableName = useMemo(() => {
if (!userName) return false;
Expand Down
4 changes: 2 additions & 2 deletions pkgs/frontend/hooks/useENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ export const useNamesByAddresses = (addresses?: string[]) => {
return { names, fetchNames };
};

export const useAddressesByNames = (names?: string[]) => {
export const useAddressesByNames = (names?: string[], exactMatch?: boolean) => {
const [addresses, setAddresses] = useState<NameData[][]>([]);

const fetchAddresses = useCallback(async (resolveNames: string[]) => {
try {
const { data } = await axios.get("/api/namestone/resolve-addresses", {
params: { names: resolveNames.join(",") },
params: { names: resolveNames.join(","), exact_match: exactMatch },
});
setAddresses(data);
return data as NameData[][];
Expand Down
Loading

0 comments on commit d22586a

Please sign in to comment.