Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add optimistic update for edit contributions and profile #1438

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/app/(marketing)/profile/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<Schemas["Contribution"][]>(
contributionsRequest,
Expand Down
69 changes: 28 additions & 41 deletions src/app/(marketing)/profile/_components/EditContributions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Guild>))
}
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<Guild>(
`/v2/guilds/${contribution.guildId}`,
fetcher
`/v2/guilds/${contribution.guildId}`
)
const memberships = useMemberships()
const editContribution = useUpdateContribution({ contributionId: contribution.id })
Expand Down Expand Up @@ -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<Guild[]>(
(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
Expand Down Expand Up @@ -165,20 +143,7 @@ export const EditContributions = () => {
</SelectTrigger>
<SelectContent>
{guilds?.map((data) => (
<SelectItem key={data.id} value={data.id.toString()}>
<div className="flex gap-2">
<Avatar size="xs">
<AvatarImage
src={data.imageUrl}
width={32}
height={32}
alt="guild avatar"
/>
<AvatarFallback />
</Avatar>
{data.name}
</div>
</SelectItem>
<GuildSelectItem key={data.id} guildId={data.id} />
))}
</SelectContent>
</Select>
Expand Down Expand Up @@ -233,6 +198,28 @@ export const EditContributions = () => {
</Dialog>
)
}

const GuildSelectItem = ({ guildId }: Pick<MembershipResult, "guildId">) => {
const { data } = useSWRImmutable<Guild>(`/v2/guilds/${guildId}`)
if (!data) return
return (
<SelectItem value={data.id.toString()}>
<div className="flex gap-2">
<Avatar size="xs">
<AvatarImage
src={data.imageUrl}
width={32}
height={32}
alt="guild avatar"
/>
<AvatarFallback />
</Avatar>
{data.name}
</div>
</SelectItem>
)
}

const RoleSelectItem = ({
roleId,
guildId,
Expand Down
4 changes: 2 additions & 2 deletions src/app/(marketing)/profile/_components/Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ProfileSkeleton />

Expand Down
8 changes: 5 additions & 3 deletions src/app/(marketing)/profile/_components/ProfileOwnerGuard.tsx
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Schemas["Contribution"][]>(
profileData ? `/v2/profiles/${profileData.username}/contributions` : null
Expand Down
51 changes: 32 additions & 19 deletions src/app/(marketing)/profile/_hooks/useCreateContribution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -24,21 +22,36 @@ export const useCreateContribution = () => {
}

const submitWithSign = useSubmitWithSign<Schemas["Contribution"]>(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({
Expand All @@ -50,7 +63,7 @@ export const useCreateContribution = () => {
})
return {
...submitWithSign,
onSubmit: (payload: EditProfilePayload) =>
onSubmit: (payload: Schemas["ContributionUpdate"]) =>
profile && submitWithSign.onSubmit(payload),
}
}
34 changes: 19 additions & 15 deletions src/app/(marketing)/profile/_hooks/useDeleteContribution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ 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 = ({
contributionId,
}: { contributionId: Schemas["Contribution"]["id"] }) => {
const { toast } = useToast()
const { data: profile } = useProfile()
const contribution = useContribution()
const contributions = useContributions()

const update = async (signedValidation: SignedValidation) => {
return fetcher(
Expand All @@ -24,20 +24,24 @@ export const useDeleteContribution = ({
}

const submitWithSign = useSubmitWithSign<object>(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({
Expand Down
44 changes: 26 additions & 18 deletions src/app/(marketing)/profile/_hooks/useUpdateContribution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ 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 = ({
contributionId,
}: { contributionId: Schemas["Contribution"]["id"] }) => {
const { toast } = useToast()
const { data: profile } = useProfile()
const contribution = useContribution()
const contributions = useContributions()

const update = async (signedValidation: SignedValidation) => {
return fetcher(
Expand All @@ -24,24 +24,32 @@ export const useUpdateContribution = ({
}

const submitWithSign = useSubmitWithSign<Schemas["Contribution"]>(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({
Expand Down
Loading
Loading