diff --git a/src/app/(marketing)/profile/[username]/page.tsx b/src/app/(marketing)/profile/[username]/page.tsx index 0d19f6e673..c06199516e 100644 --- a/src/app/(marketing)/profile/[username]/page.tsx +++ b/src/app/(marketing)/profile/[username]/page.tsx @@ -10,7 +10,7 @@ import { SWRProvider } from "@/components/SWRProvider" import { Anchor } from "@/components/ui/Anchor" import { Guild, Role, Schemas } from "@guildxyz/types" import { ArrowRight } from "@phosphor-icons/react/dist/ssr" -import { notFound } from "next/navigation" +import { notFound, redirect } from "next/navigation" import { Profile } from "../_components/Profile" // TODO: use env var for this url when it is changed to this value. @@ -30,9 +30,10 @@ const fetchPublicProfileData = async ({ username }: { username: string }) => { revalidate: 600, }, }) - if (profileResponse.status === 404) { - notFound() - } + + if (profileResponse.status === 404) notFound() + if (!profileResponse.ok) redirect("/error") + const profile = (await profileResponse.json()) as Schemas["Profile"] const contributions = await ssrFetcher( contributionsRequest, diff --git a/src/app/(marketing)/profile/_components/EditContributions.tsx b/src/app/(marketing)/profile/_components/EditContributions.tsx index 1a5b7b8a79..e428d5c431 100644 --- a/src/app/(marketing)/profile/_components/EditContributions.tsx +++ b/src/app/(marketing)/profile/_components/EditContributions.tsx @@ -29,31 +29,18 @@ import { AvatarFallback } from "@radix-ui/react-avatar" import { DialogDescription } from "@radix-ui/react-dialog" import { useState } from "react" import useSWRImmutable from "swr/immutable" -import fetcher from "utils/fetcher" -import { useContribution } from "../_hooks/useContribution" +import { useContributions } from "../_hooks/useContributions" import { useCreateContribution } from "../_hooks/useCreateContribution" import { useDeleteContribution } from "../_hooks/useDeleteContribution" import { useMemberships } from "../_hooks/useMemberships" import { useUpdateContribution } from "../_hooks/useUpdateContribution" import { CardWithGuildLabel } from "./CardWithGuildLabel" -const guildFetcher = (urls: string[]) => { - return Promise.all(urls.map((url) => fetcher(url) as Promise)) -} -const useYourVerifiedGuild = () => { - const yourGuilds = useYourGuilds() - const requests = yourGuilds.data - ? yourGuilds.data.map((guild) => `/v2/guilds/${guild.id}`) - : null - return { guilds: useSWRImmutable(requests, guildFetcher), baseGuilds: yourGuilds } -} - const EditContributionCard = ({ contribution, }: { contribution: Schemas["Contribution"] }) => { const { data: guild } = useSWRImmutable( - `/v2/guilds/${contribution.guildId}`, - fetcher + `/v2/guilds/${contribution.guildId}` ) const memberships = useMemberships() const editContribution = useUpdateContribution({ contributionId: contribution.id }) @@ -101,23 +88,14 @@ const EditContributionCard = ({ } export const EditContributions = () => { - const contributions = useContribution() + const contributions = useContributions() const memberships = useMemberships() const [guildId, setGuildId] = useState("") const [roleId, setRoleId] = useState("") const { toast } = useToast() - const { - guilds: { data: guildData }, - baseGuilds, - } = useYourVerifiedGuild() - const guilds = - guildData && - baseGuilds.data?.reduce( - (acc, curr, i) => - curr.tags.includes("VERIFIED") ? [...acc, guildData[i]] : acc, - [] - ) + const { data: baseGuilds } = useYourGuilds() + const guilds = baseGuilds?.filter(({ tags }) => tags.includes("VERIFIED")) const roleIds = memberships.data?.find( (membership) => membership.guildId.toString() === guildId @@ -165,20 +143,7 @@ export const EditContributions = () => { {guilds?.map((data) => ( - -
- - - - - {data.name} -
-
+ ))}
@@ -233,6 +198,28 @@ export const EditContributions = () => { ) } + +const GuildSelectItem = ({ guildId }: Pick) => { + const { data } = useSWRImmutable(`/v2/guilds/${guildId}`) + if (!data) return + return ( + +
+ + + + + {data.name} +
+
+ ) +} + const RoleSelectItem = ({ roleId, guildId, diff --git a/src/app/(marketing)/profile/_components/Profile.tsx b/src/app/(marketing)/profile/_components/Profile.tsx index 99510c1cac..b626107250 100644 --- a/src/app/(marketing)/profile/_components/Profile.tsx +++ b/src/app/(marketing)/profile/_components/Profile.tsx @@ -11,13 +11,13 @@ import { EditProfile } from "../_components/EditProfile" import { OperatedGuildCard } from "../_components/OperatedGuildCard" import { ProfileOwnerGuard } from "../_components/ProfileOwnerGuard" import { RecentActivity } from "../_components/RecentActivity" -import { useContribution } from "../_hooks/useContribution" +import { useContributions } from "../_hooks/useContributions" import { useProfile } from "../_hooks/useProfile" import { ProfileSkeleton } from "./ProfileSkeleton" export const Profile = () => { const { data: profile } = useProfile() - const { data: contributions } = useContribution() + const { data: contributions } = useContributions() if (!profile || !contributions) return diff --git a/src/app/(marketing)/profile/_components/ProfileOwnerGuard.tsx b/src/app/(marketing)/profile/_components/ProfileOwnerGuard.tsx index 6de4e15b1e..1788c4f967 100644 --- a/src/app/(marketing)/profile/_components/ProfileOwnerGuard.tsx +++ b/src/app/(marketing)/profile/_components/ProfileOwnerGuard.tsx @@ -1,14 +1,16 @@ "use client" import { useUserPublic } from "@/hooks/useUserPublic" -import { PropsWithChildren } from "react" +import { PropsWithChildren, useMemo } from "react" import { useProfile } from "../_hooks/useProfile" export const ProfileOwnerGuard = ({ children }: PropsWithChildren) => { const { data: profile } = useProfile() const { id: publicUserId } = useUserPublic() - const isProfileOwner = !!profile?.userId && publicUserId === profile.userId - + const isProfileOwner = useMemo( + () => !!profile?.userId && publicUserId === profile.userId, + [publicUserId] + ) if (!isProfileOwner) return return children } diff --git a/src/app/(marketing)/profile/_hooks/useContribution.tsx b/src/app/(marketing)/profile/_hooks/useContributions.tsx similarity index 88% rename from src/app/(marketing)/profile/_hooks/useContribution.tsx rename to src/app/(marketing)/profile/_hooks/useContributions.tsx index 88c4c01317..6c51947abf 100644 --- a/src/app/(marketing)/profile/_hooks/useContribution.tsx +++ b/src/app/(marketing)/profile/_hooks/useContributions.tsx @@ -2,7 +2,7 @@ import { Schemas } from "@guildxyz/types" import useSWRImmutable from "swr/immutable" import { useProfile } from "./useProfile" -export const useContribution = () => { +export const useContributions = () => { const { data: profileData } = useProfile() return useSWRImmutable( profileData ? `/v2/profiles/${profileData.username}/contributions` : null diff --git a/src/app/(marketing)/profile/_hooks/useCreateContribution.tsx b/src/app/(marketing)/profile/_hooks/useCreateContribution.tsx index abfbc22ee2..e7d840da46 100644 --- a/src/app/(marketing)/profile/_hooks/useCreateContribution.tsx +++ b/src/app/(marketing)/profile/_hooks/useCreateContribution.tsx @@ -2,16 +2,14 @@ import { useToast } from "@/components/ui/hooks/useToast" import { Schemas } from "@guildxyz/types" import { SignedValidation, useSubmitWithSign } from "hooks/useSubmit" import fetcher from "utils/fetcher" -import { revalidateContribution } from "../_server_actions/revalidateContribution" -import { useContribution } from "./useContribution" +import { revalidateContributions } from "../_server_actions/revalidateContributions" +import { useContributions } from "./useContributions" import { useProfile } from "./useProfile" -export type EditProfilePayload = Schemas["ContributionUpdate"] - export const useCreateContribution = () => { const { toast } = useToast() const { data: profile } = useProfile() - const contribution = useContribution() + const contributions = useContributions() const update = async (signedValidation: SignedValidation) => { return fetcher( @@ -24,21 +22,36 @@ export const useCreateContribution = () => { } const submitWithSign = useSubmitWithSign(update, { - onSuccess: (response) => { - contribution.mutate( - (prev) => { - // WARNING: should we validate here? - if (!prev) return - prev.push(response) - return prev + onOptimistic: (response, payload) => { + if (!profile?.userId) return + contributions.mutate( + async () => { + if (!contributions.data) return + const contribution = await response + contributions.data[ + contributions.data.findLastIndex(({ id }) => id === -1) + ] = contribution + return contributions.data.filter(({ id }) => id !== -1) }, - { revalidate: false } + { + revalidate: false, + rollbackOnError: true, + optimisticData: () => { + // @ts-expect-error: incorrect types coming from lib + const fakeContribution: Schemas["Contribution"] = { + ...(payload as Schemas["ContributionUpdate"]), + id: -1, + profileId: profile.userId, + } + if (!contributions.data) return [fakeContribution] + contributions.data.push(fakeContribution) + return contributions.data + }, + } ) - revalidateContribution() - toast({ - variant: "success", - title: "Successfully created contribution", - }) + }, + onSuccess: () => { + revalidateContributions() }, onError: (response) => { toast({ @@ -50,7 +63,7 @@ export const useCreateContribution = () => { }) return { ...submitWithSign, - onSubmit: (payload: EditProfilePayload) => + onSubmit: (payload: Schemas["ContributionUpdate"]) => profile && submitWithSign.onSubmit(payload), } } diff --git a/src/app/(marketing)/profile/_hooks/useDeleteContribution.tsx b/src/app/(marketing)/profile/_hooks/useDeleteContribution.tsx index f69753505e..9a09b2e75e 100644 --- a/src/app/(marketing)/profile/_hooks/useDeleteContribution.tsx +++ b/src/app/(marketing)/profile/_hooks/useDeleteContribution.tsx @@ -2,8 +2,8 @@ import { useToast } from "@/components/ui/hooks/useToast" import { Schemas } from "@guildxyz/types" import { SignedValidation, useSubmitWithSign } from "hooks/useSubmit" import fetcher from "utils/fetcher" -import { revalidateContribution } from "../_server_actions/revalidateContribution" -import { useContribution } from "./useContribution" +import { revalidateContributions } from "../_server_actions/revalidateContributions" +import { useContributions } from "./useContributions" import { useProfile } from "./useProfile" export const useDeleteContribution = ({ @@ -11,7 +11,7 @@ export const useDeleteContribution = ({ }: { contributionId: Schemas["Contribution"]["id"] }) => { const { toast } = useToast() const { data: profile } = useProfile() - const contribution = useContribution() + const contributions = useContributions() const update = async (signedValidation: SignedValidation) => { return fetcher( @@ -24,20 +24,24 @@ export const useDeleteContribution = ({ } const submitWithSign = useSubmitWithSign(update, { - onSuccess: () => { - contribution.mutate( - (prev) => { - if (!prev || !contribution.data) return - // WARNING: should we validate here? - return prev.filter((p) => p.id !== contributionId) + onOptimistic: (response) => { + contributions.mutate( + async () => { + await response + return contributions.data?.filter((p) => p.id !== contributionId) }, - { revalidate: false } + { + revalidate: false, + rollbackOnError: true, + optimisticData: () => { + if (!contributions.data) return [] + return contributions.data.filter((p) => p.id !== contributionId) + }, + } ) - revalidateContribution() - toast({ - variant: "success", - title: "Successfully deleted contribution", - }) + }, + onSuccess: () => { + revalidateContributions() }, onError: (response) => { toast({ diff --git a/src/app/(marketing)/profile/_hooks/useUpdateContribution.tsx b/src/app/(marketing)/profile/_hooks/useUpdateContribution.tsx index febe464054..cfc847d9d8 100644 --- a/src/app/(marketing)/profile/_hooks/useUpdateContribution.tsx +++ b/src/app/(marketing)/profile/_hooks/useUpdateContribution.tsx @@ -2,8 +2,8 @@ import { useToast } from "@/components/ui/hooks/useToast" import { Schemas } from "@guildxyz/types" import { SignedValidation, useSubmitWithSign } from "hooks/useSubmit" import fetcher from "utils/fetcher" -import { revalidateContribution } from "../_server_actions/revalidateContribution" -import { useContribution } from "./useContribution" +import { revalidateContributions } from "../_server_actions/revalidateContributions" +import { useContributions } from "./useContributions" import { useProfile } from "./useProfile" export const useUpdateContribution = ({ @@ -11,7 +11,7 @@ export const useUpdateContribution = ({ }: { contributionId: Schemas["Contribution"]["id"] }) => { const { toast } = useToast() const { data: profile } = useProfile() - const contribution = useContribution() + const contributions = useContributions() const update = async (signedValidation: SignedValidation) => { return fetcher( @@ -24,24 +24,32 @@ export const useUpdateContribution = ({ } const submitWithSign = useSubmitWithSign(update, { - onSuccess: (response) => { - contribution.mutate( - (prev) => { - if (!prev || !contribution.data) return - // WARNING: should we validate here? - return prev.map((p) => - p.id === (contribution.data as unknown as Schemas["Contribution"]).id - ? response - : p + onOptimistic: (response, payload) => { + if (!profile?.userId) return + contributions.mutate( + async () => { + if (!contributions.data) return + const contribution = await response + return contributions.data.map((data) => + data.id === contributionId ? contribution : data ) }, - { revalidate: false } + { + revalidate: false, + rollbackOnError: true, + optimisticData: () => { + if (!contributions.data) return [] + return contributions.data.map((data) => + data.id === contributionId + ? { ...data, ...(payload as Schemas["Contribution"]) } + : data + ) + }, + } ) - revalidateContribution() - toast({ - variant: "success", - title: "Successfully updated contribution", - }) + }, + onSuccess: () => { + revalidateContributions() }, onError: (response) => { toast({ diff --git a/src/app/(marketing)/profile/_hooks/useUpdateProfile.ts b/src/app/(marketing)/profile/_hooks/useUpdateProfile.ts index 8da9a9a059..fcd54d6263 100644 --- a/src/app/(marketing)/profile/_hooks/useUpdateProfile.ts +++ b/src/app/(marketing)/profile/_hooks/useUpdateProfile.ts @@ -3,6 +3,7 @@ import { Schemas } from "@guildxyz/types" import { SignedValidation, useSubmitWithSign } from "hooks/useSubmit" import { useParams, useRouter } from "next/navigation" import fetcher from "utils/fetcher" +import { revalidateContributions } from "../_server_actions/revalidateContributions" import { revalidateProfile } from "../_server_actions/revalidateProfile" import { useProfile } from "./useProfile" @@ -10,7 +11,7 @@ export const useUpdateProfile = () => { const { toast } = useToast() const router = useRouter() const params = useParams<{ username: string }>() - const { mutate } = useProfile() + const { mutate, data: profile } = useProfile() const updateProfile = async (signedValidation: SignedValidation) => { return fetcher(`/v2/profiles/${params?.username}`, { @@ -20,18 +21,21 @@ export const useUpdateProfile = () => { } const submitWithSign = useSubmitWithSign(updateProfile, { - onSuccess: (response) => { - console.log("onSuccess", response) - router.replace(`/profile/${response.username}`) - mutate(() => response, { revalidate: false }) - revalidateProfile() - toast({ - variant: "success", - title: "Successfully updated profile", + onOptimistic: (response, payload) => { + mutate(() => response, { + revalidate: false, + rollbackOnError: true, + optimisticData: () => payload, }) }, + onSuccess: async (response) => { + await revalidateProfile() + if (profile?.username !== response.username) { + await revalidateContributions() + router.replace(`/profile/${response.username}`) + } + }, onError: (response) => { - console.log("onError", response) toast({ variant: "error", title: "Failed to update profile", diff --git a/src/app/(marketing)/profile/_server_actions/revalidateContribution.ts b/src/app/(marketing)/profile/_server_actions/revalidateContributions.ts similarity index 65% rename from src/app/(marketing)/profile/_server_actions/revalidateContribution.ts rename to src/app/(marketing)/profile/_server_actions/revalidateContributions.ts index fa10ae1ed6..cbf800b1f7 100644 --- a/src/app/(marketing)/profile/_server_actions/revalidateContribution.ts +++ b/src/app/(marketing)/profile/_server_actions/revalidateContributions.ts @@ -2,6 +2,6 @@ import { revalidateTag } from "next/cache" -export async function revalidateContribution() { +export async function revalidateContributions() { revalidateTag("contributions") } diff --git a/src/hooks/useSetKeyPair.ts b/src/hooks/useSetKeyPair.ts index b1ebf9503f..4b2cc6b55e 100644 --- a/src/hooks/useSetKeyPair.ts +++ b/src/hooks/useSetKeyPair.ts @@ -94,6 +94,7 @@ const useSetKeyPair = (submitOptions?: UseSubmitOptions) => { const recaptcha = useAtomValue(recaptchaAtom) const setSubmitResponse = useSubmit( + // @ts-ignore async ({ signProps, }: { @@ -175,6 +176,7 @@ const useSetKeyPair = (submitOptions?: UseSubmitOptions) => { return { keyPair: generatedKeys, user: userProfile } }, + // @ts-ignore { ...submitOptions, onError: (error) => { diff --git a/src/hooks/useSubmit/types.ts b/src/hooks/useSubmit/types.ts index cffa07fd2a..6da8497725 100644 --- a/src/hooks/useSubmit/types.ts +++ b/src/hooks/useSubmit/types.ts @@ -40,6 +40,7 @@ export type MessageParams = { export type UseSubmitOptions = { onSuccess?: (response: ResponseType) => void onError?: (error: any) => void + onOptimistic?: (response: Promise, payload: any) => void // Use catefully! If this is set to true, a .onSubmit() call can reject! allowThrow?: boolean diff --git a/src/hooks/useSubmit/useSubmit.ts b/src/hooks/useSubmit/useSubmit.ts index 048e1c2007..75a5f630bb 100644 --- a/src/hooks/useSubmit/useSubmit.ts +++ b/src/hooks/useSubmit/useSubmit.ts @@ -21,7 +21,12 @@ type FetcherFunction = ({ const useSubmit = ( fetch: (data?: DataType) => Promise, - { onSuccess, onError, allowThrow }: UseSubmitOptions = {} + { + onSuccess, + onError, + allowThrow, + onOptimistic, + }: UseSubmitOptions = {} ) => { const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(undefined) @@ -31,7 +36,9 @@ const useSubmit = ( (data?: DataType): Promise => { setIsLoading(true) setError(undefined) - return fetch(data) + const response = fetch(data) + onOptimistic?.(response, data) + return response .then((d) => { onSuccess?.(d) setResponse(d) @@ -47,7 +54,7 @@ const useSubmit = ( }) .finally(() => setIsLoading(false)) }, - [allowThrow, fetch, onError, onSuccess] + [allowThrow, fetch, onError, onSuccess, onOptimistic] ) return {