diff --git a/apps/protocol-frontend/src/components/GuildImportModal.tsx b/apps/protocol-frontend/src/components/GuildImportModal.tsx new file mode 100644 index 000000000..cc64c7cdb --- /dev/null +++ b/apps/protocol-frontend/src/components/GuildImportModal.tsx @@ -0,0 +1,123 @@ +import { Box, Button, Heading, Spinner, Stack } from '@chakra-ui/react'; +import { ControlledSelect, SelectOption as Option } from '@govrn/protocol-ui'; +import { useMemo, useState } from 'react'; +import { useGuildXYZListGuilds } from '../hooks/useGuildXYZListGuilds'; +import { useOverlay } from '../contexts/OverlayContext'; + +interface GuildImportProps { + onSuccess: (guildId: number) => void; +} + +const GuildImportTitle = ({ title }: { title: string }) => { + return ( + + {title} + + ); +}; + +const GuildImportModalError = ({ message }: { message: string }) => { + return ( + + + +

{message}

+
+
+ ); +}; + +export const GuildImportModal = ({ onSuccess }: GuildImportProps) => { + const { setModals } = useOverlay(); + + const [selectedGuild, setSelectedGuild] = useState | null>( + null, + ); + + const { + data: guildList, + isLoading: isGuildListLoading, + isError, + error, + } = useGuildXYZListGuilds(); + + const daoListOptions = useMemo(() => { + return ( + guildList?.map(guild => ({ + value: guild.id, + label: guild.name, + image: guild.imageUrl, + })) || [] + ); + }, [guildList]); + + function handleGuildImport() { + console.log( + `::: publish message ${guildList?.find( + g => g.id === (selectedGuild ?? -1), + )}`, + ); + onSuccess(selectedGuild?.value ?? -1); + setModals({}); + } + + if (isGuildListLoading) { + return ( + + + + + ); + } + + if (isError && error instanceof Error) { + return ( + + ); + } + + if (guildList?.length === 0) { + return ( + + ); + } + + return ( + + + + { + if (dao instanceof Array || !dao) { + return; + } + setSelectedGuild(dao); + }} + value={selectedGuild ?? null} + options={daoListOptions} + isSearchable={false} + isClearable + /> + + + + + ); +}; diff --git a/apps/protocol-frontend/src/components/ProfileDaos.tsx b/apps/protocol-frontend/src/components/ProfileDaos.tsx index 1c93e55a8..f95fb4ce1 100644 --- a/apps/protocol-frontend/src/components/ProfileDaos.tsx +++ b/apps/protocol-frontend/src/components/ProfileDaos.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect } from 'react'; +import React, { useState, useMemo, useEffect } from 'react'; import { Link as RouterLink, useLocation } from 'react-router-dom'; import { Button, @@ -8,6 +8,7 @@ import { Divider, Grid, Text, + Stack, } from '@chakra-ui/react'; import { ControlledSelect, @@ -24,6 +25,10 @@ import { mergeMemberPages } from '../utils/arrays'; import { UIGuildUsers } from '@govrn/ui-types'; import { LEFT_MEMBERSHIP_NAME } from '../utils/constants'; import useUserGet from '../hooks/useUserGet'; +import { GiCastle } from 'react-icons/all'; +import ModalWrapper from './ModalWrapper'; +import { GuildImportModal } from './GuildImportModal'; +import { useOverlay } from '../contexts/OverlayContext'; interface ProfileDaoProps { userId: number | undefined; @@ -31,11 +36,16 @@ interface ProfileDaoProps { } const ProfileDaos = ({ userId, userAddress }: ProfileDaoProps) => { + const localOverlay = useOverlay(); const [selectedDao, setSelectedDao] = useState | null>(null); const { state } = useLocation(); const { targetId } = state || {}; + const showGuildImportModal = () => { + localOverlay.setModals({ guildImportModal: true }); + }; + useEffect(() => { const el = document.getElementById(targetId); if (el) { @@ -155,129 +165,171 @@ const ProfileDaos = ({ userId, userAddress }: ProfileDaoProps) => { if (joinableDaosListLoading) return ; return ( - + <> - - My DAOs - - {data && data.length === 0 ? ( - - You'll need other collaborators to be part of a DAO! - - Select a DAO to join below or press create DAO and make your own. - - - ) : null} - - + + My DAOs + + {data && data.length === 0 ? ( - { - if (dao instanceof Array || !dao) { - return; - } - setSelectedDao(dao); - }} - value={selectedDao ?? null} - options={daoListOptions} - isSearchable={false} - isClearable - /> + You'll need other collaborators to be part of a DAO! + + Select a DAO to join below or press create DAO and make your + own. + - - - + + + + { + if (dao instanceof Array || !dao) { + return; + } + setSelectedDao(dao); + }} + value={selectedDao ?? null} + options={daoListOptions} + isSearchable={false} + isClearable + /> + - - - - {data?.map(daoUser => ( - - ))} - - {hasNextPage && ( - - + + - )} + + {data?.map(daoUser => ( + + ))} + + {hasNextPage && ( + + + + )} + + + + + Guild.xyz Import + + + With Guild.xyz, effortlessly + import the guilds you've joined using +
your associated signed-in address. +
{' '} + +
-
+ { + // TODO: publish guild id to backend to import. + // TODO: maybe move this to the modal, and only reflect success here + console.log( + `TODO: publish guild id to backend to import`, + guildId, + ); + }} + /> + } + /> + ); }; diff --git a/apps/protocol-frontend/src/components/ProfileForm.tsx b/apps/protocol-frontend/src/components/ProfileForm.tsx index fbf3d90a3..9b91d1072 100644 --- a/apps/protocol-frontend/src/components/ProfileForm.tsx +++ b/apps/protocol-frontend/src/components/ProfileForm.tsx @@ -29,6 +29,7 @@ const isLinearConnected = (userData?: UIUser | null) => const ProfileForm = () => { const { userData } = useUser(); + const { mutateAsync: disconnectLinearUser } = useLinearUserDisconnect(); const { mutateAsync: disconnectDiscordUser } = useDiscordUserDisconnect(); const { displayName } = useDisplayName(); @@ -139,7 +140,7 @@ const ProfileForm = () => {
- + ;
{ + const { userData } = useUser(); + + const { isLoading, isFetching, isError, error, data } = useQuery({ + queryKey: ['guildXYZListGuilds'], + queryFn: async () => { + if (userData?.address === undefined) { + throw new Error('User not found'); + } + // fetch user joined guilds' ids. + let guildMembership: GuildMembership[] = []; + try { + guildMembership = await fetchGuildMemberships(userData?.address); + } catch (error) { + throw new Error('Failed to get user joined guilds'); + } + + // fetch & map guilds' ids to guilds' basic info. + let guilds: Guild[] = []; + try { + guilds = await Promise.all( + guildMembership + // TODO: which guilds to show? all or only admin/owner? + .filter(guild => true /*guild.isAdmin || guild.isOwner*/) + .map(async guild => await fetchGuild(guild.guildId)), + ); + } catch (error) { + throw new Error('Failed to get joined guilds info'); + } + + return guilds; + }, + }); + + return { isLoading, isError, isFetching, error, data }; +}; diff --git a/apps/protocol-frontend/src/utils/guild-xyz.ts b/apps/protocol-frontend/src/utils/guild-xyz.ts new file mode 100644 index 000000000..29f7e4dbb --- /dev/null +++ b/apps/protocol-frontend/src/utils/guild-xyz.ts @@ -0,0 +1,51 @@ +import axios from 'axios'; + +export type GuildMembership = { + guildId: number; + roleIds: number[]; + isAdmin: boolean; + isOwner: boolean; +}; + +export type Guild = { + id: number; + name: string; + urlName: string; + description: string; + imageUrl: string; +}; + +/** + * Fetches the guilds the user has joined. The response is an array of guild IDs, + * along with information on whether the user is an admin or owner of the guild. + * + * @param address The user's address associated with their guild.xyz account. + * @see {@link https://docs.guild.xyz/guild/guides/guild-api-alpha#get-guilds-joined-by-a-user Guild.xyz API docs} + */ +export const fetchGuildMemberships = async ( + address: string, +): Promise => { + const response = await axios.get( + `https://api.guild.xyz/v1/user/membership/${address}`, + ); + + if (response.status !== 200) { + throw new Error(`Failed to get user joined guilds.`); + } + return response.data as GuildMembership[]; +}; + +/** + * Fetches basic information about a guild. + * + * @param guildId The guild's ID. + * @see {@link https://docs.guild.xyz/guild/guides/guild-api-alpha#get-a-guild-by-id Guild.xyz API docs} + */ +export const fetchGuild = async (guildId: number): Promise => { + const response = await axios.get(`https://api.guild.xyz/v1/guild/${guildId}`); + + if (response.status !== 200) { + throw new Error(`Failed to get guild.`); + } + return response.data as Guild; +};