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

Implement EditProfile container #1427

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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@emotion/styled": "^11.11.0",
"@fuels/connectors": "^0.5.0",
"@fuels/react": "^0.20.0",
"@guildxyz/types": "^1.9.30",
"@guildxyz/types": "^1.9.33",
"@hcaptcha/react-hcaptcha": "^1.4.4",
"@hookform/resolvers": "^3.3.4",
"@lexical/code": "^0.12.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,11 @@ export const StartProfile: OnboardingChain = () => {
{method === undefined ? (
<>
<ConnectFarcasterButton
className="ml-0 w-full gap-2"
className="ml-0 flex w-full items-center gap-2"
size="md"
disabled={!!farcasterProfile}
>
<div className="size-6">
<FarcasterImage />
</div>
<FarcasterImage />
Connect farcaster
</ConnectFarcasterButton>
<Button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useConfetti } from "@/components/Confetti"
import { useToast } from "@/components/ui/hooks/useToast"
import { profileSchema } from "@/lib/validations/profileSchema"
import { Schemas } from "@guildxyz/types"
import { SignedValidation, useSubmitWithSign } from "hooks/useSubmit"
import { useRouter } from "next/navigation"
import fetcher from "utils/fetcher"
Expand All @@ -17,14 +18,13 @@ export const useCreateProfile = () => {
...signedValidation,
})

const submitWithSign = useSubmitWithSign<unknown>(createProfile, {
const submitWithSign = useSubmitWithSign<Schemas["Profile"]>(createProfile, {
onSuccess: (response) => {
toast({
variant: "success",
title: "Successfully created profile",
})
confettiPlayer.current("Confetti from left and right")
// @ts-ignore: TODO: either acquire types from backend, or type them here
router.replace(`/profile/${response.username}`)
},
onError: (response) => {
Expand Down
5 changes: 0 additions & 5 deletions src/app/(marketing)/profile/[username]/atoms.ts

This file was deleted.

258 changes: 99 additions & 159 deletions src/app/(marketing)/profile/[username]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
"use client"

import { CheckMark } from "@/components/CheckMark"
import { Header } from "@/components/Header"
import {
Layout,
Expand All @@ -9,170 +6,113 @@ import {
LayoutHero,
LayoutMain,
} from "@/components/Layout"
import { SWRProvider } from "@/components/SWRProvider"
import { Anchor } from "@/components/ui/Anchor"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/Avatar"
import { AvatarGroup } from "@/components/ui/AvatarGroup"
import { Separator } from "@/components/ui/Separator"
import { Skeleton } from "@/components/ui/Skeleton"
import { useUserPublic } from "@/hooks/useUserPublic"
import { Schemas } from "@guildxyz/types"
import { Guild, Role, Schemas } from "@guildxyz/types"
import { ArrowRight } from "@phosphor-icons/react/dist/ssr"
import { useAtom } from "jotai"
import { useEffect } from "react"
import useSWR from "swr"
import { fetcherForSWR } from "utils/fetcher"
import { ContributionCard } from "../_components/ContributionCard"
import { EditContributions } from "../_components/EditContributions"
import { EditProfile } from "../_components/EditProfile"
import { OperatedGuildCard } from "../_components/OperatedGuildCard"
import { ProfileSkeleton } from "../_components/ProfileSkeleton"
import { RecentActivity } from "../_components/RecentActivity"
import { useContribution } from "../_hooks/useContribution"
import { profileAtom } from "./atoms"

// async function getProfileData(username: string) {
// const req = `https://api.guild.xyz/v2/profiles/${username}`
// try {
// const res = await fetch(req)
// if (!res.ok) {
// throw new Error("Failed to fetch profile data")
// }
// return res.json() as Promise<Schemas["Profile"]>
// } catch (e) {
// // mocking for the time being if fetch fails
// console.error(e)
// const res = {
// id: 4,
// userId: 6027190,
// username: "durumm",
// name: "durum",
// bio: null,
// profileImageUrl:
// "https://guild-xyz.mypinata.cloud/ipfs/QmWGdo6FkjSz22oDZFMJysx3hGKoVqtzTWVMx9tTSP7jvi",
// backgroundImageUrl: null,
// createdAt: "2024-07-25T10:04:20.781Z",
// updatedAt: "2024-07-25T10:04:20.781Z",
// }
// return res
// }
// }
import { Profile } from "../_components/Profile"

const Page = ({
params: { username },
}: {
params: { username: string }
}) => {
const { data: fetchedProfile, isLoading } = useSWR<Schemas["Profile"]>(
`/v2/profiles/${username}`,
fetcherForSWR
)
const [profile, setProfile] = useAtom(profileAtom)
const contributions = useContribution()
// TODO: use env var for this url when it is changed to this value.
// next-server throws fetch error if we modify the env var in memory
const api = "https://api.guild.xyz"

const { id: publicUserId } = useUserPublic()
const isProfileOwner = !!profile?.userId && publicUserId === profile.userId
useEffect(() => {
setProfile(fetchedProfile)
}, [fetchedProfile, setProfile])
async function ssrFetcher<T>(...args: Parameters<typeof fetch>) {
return (await fetch(...args)).json() as T
}

if (!profile || isLoading) {
return <ProfileSkeleton />
const fetchPublicProfileData = async ({ username }: { username: string }) => {
const contributionsRequest = new URL(`v2/profiles/${username}/contributions`, api)
const profileRequest = new URL(`v2/profiles/${username}`, api)
const contributions = await ssrFetcher<Schemas["Contribution"][]>(
contributionsRequest,
{
next: {
tags: ["contributions"],
revalidate: 600,
},
}
)
const profile = await ssrFetcher<Schemas["Profile"]>(profileRequest, {
next: {
tags: ["profile"],
revalidate: 600,
},
})
const roleRequests = contributions.map(
({ roleId, guildId }) => new URL(`v2/guilds/${guildId}/roles/${roleId}`, api)
)
const guildRequests = contributions.map(
({ guildId }) => new URL(`v2/guilds/${guildId}`, api)
)
const guilds = await Promise.all(
guildRequests.map((req) =>
ssrFetcher<Guild>(req, {
next: {
revalidate: 3600,
},
})
)
)
const roles = await Promise.all(
roleRequests.map((req) =>
ssrFetcher<Role>(req, {
next: {
revalidate: 3600,
},
})
)
)
const guildsZipped = Object.fromEntries(
guildRequests.map(({ pathname }, i) => [pathname, guilds[i]])
)
const rolesZipped = Object.fromEntries(
roleRequests.map(({ pathname }, i) => [pathname, roles[i]])
)
return {
fallback: {
[profileRequest.pathname]: profile,
[contributionsRequest.pathname]: contributions,
...guildsZipped,
...rolesZipped,
},
}
}

const Page = async ({ params: { username } }: { params: { username: string } }) => {
const { fallback } = await fetchPublicProfileData({ username })
return (
<Layout>
<LayoutHero>
<Header />
<LayoutBanner className="-bottom-[500px]">
<div className="absolute inset-0 bg-[url('/banner.svg')] opacity-10" />
<div className="absolute inset-0 bg-gradient-to-t from-background" />
</LayoutBanner>
</LayoutHero>
<LayoutMain>
<div className="mt-24">
<div className="relative mb-24 flex flex-col items-center">
<EditProfile
profileImageUrl={profile.profileImageUrl}
name={profile.name}
bio={profile.bio ?? undefined}
backgroundImageUrl={profile.backgroundImageUrl ?? undefined}
username={profile.username}
/>
<div className="relative mb-12 flex items-center justify-center">
<Avatar className="size-48">
<AvatarImage
src={profile.profileImageUrl ?? ""}
alt="profile"
width={192}
height={192}
/>
<AvatarFallback>
<Skeleton className="size-full" />
</AvatarFallback>
</Avatar>
</div>
<h1 className="text-center font-bold text-4xl leading-normal tracking-tight">
{profile.name}
<CheckMark className="ml-2 inline size-6 fill-yellow-500" />
</h1>
<div className="text-lg text-muted-foreground">@{profile.username}</div>
<p className="mt-6 max-w-md text-pretty text-center text-lg text-muted-foreground">
{profile.bio}
</p>
<div className="mt-8 grid grid-cols-[repeat(3,auto)] gap-x-8 gap-y-6 sm:grid-cols-[repeat(5,auto)]">
<div className="flex flex-col items-center leading-tight">
<div className="font-bold text-lg">3232</div>
<div className="text-muted-foreground">Guildmates</div>
</div>
<Separator orientation="vertical" className="h-12" />
<div className="flex flex-col items-center leading-tight">
<div className="font-bold text-lg">0</div>
<div className="text-muted-foreground">Followers</div>
</div>
<Separator orientation="vertical" className="hidden h-12 sm:block" />
<div className="col-span-3 flex items-center gap-2 place-self-center sm:col-span-1">
<AvatarGroup imageUrls={["", ""]} count={8} />
<div className="text-muted-foreground leading-tight">
Followed by <span className="font-bold">Hoho</span>,<br />
<span className="font-bold">Hihi</span> and 22 others
</div>
</div>
</div>
</div>
<h2 className="mb-3 font-bold text-lg">Operated guilds</h2>
<OperatedGuildCard />
<div className="mt-8 mb-3 flex items-center justify-between">
<h2 className="font-bold text-lg">Top contributions</h2>
{isProfileOwner && <EditContributions />}
</div>
<div className="grid grid-cols-1 gap-3">
{contributions.data?.map((contribution) => (
<ContributionCard contribution={contribution} key={contribution.id} />
))}
</div>
<div className="mt-8">
<h2 className="mb-3 font-bold text-lg">Recent activity</h2>
<RecentActivity />
<p className="mt-2 font-semibold text-muted-foreground">
&hellip; only last 20 actions are shown
</p>
</div>
</div>
</LayoutMain>
<LayoutFooter>
<p className="mb-12 text-center font-medium text-muted-foreground">
Guild Profiles are currently in invite only early access, only available to{" "}
<Anchor
href={"#"}
className="inline-flex items-center gap-1"
variant="muted"
>
Subscribers
<ArrowRight />
</Anchor>
</p>
</LayoutFooter>
</Layout>
<SWRProvider
value={{
fallback,
}}
>
<Layout>
<LayoutHero>
<Header />
<LayoutBanner className="-bottom-[500px]">
<div className="absolute inset-0 bg-[url('/banner.svg')] opacity-10" />
<div className="absolute inset-0 bg-gradient-to-t from-background" />
</LayoutBanner>
</LayoutHero>
<LayoutMain>
<Profile />
</LayoutMain>
<LayoutFooter>
<p className="mb-12 text-center font-medium text-muted-foreground">
Guild Profiles are currently in invite only early access, only available
to{" "}
<Anchor
href={"#"}
className="inline-flex items-center gap-1"
variant="muted"
>
Subscribers
<ArrowRight />
</Anchor>
</p>
</LayoutFooter>
</Layout>
</SWRProvider>
)
}

Expand Down
10 changes: 6 additions & 4 deletions src/app/(marketing)/profile/_components/ContributionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"use client"

import { Guild, Role, Schemas } from "@guildxyz/types"
import useSWR from "swr"
import useSWRImmutable from "swr/immutable"
import fetcher from "utils/fetcher"
import { ContributionCardView } from "./ContributionCardView"

export const ContributionCard = ({
contribution,
}: { contribution: Schemas["ProfileContribution"] }) => {
const guild = useSWR<Guild>(`/v2/guilds/${contribution.guildId}`, fetcher)
const role = useSWR<Role>(
}: { contribution: Schemas["Contribution"] }) => {
const guild = useSWRImmutable<Guild>(`/v2/guilds/${contribution.guildId}`, fetcher)
const role = useSWRImmutable<Role>(
`/v2/guilds/${contribution.guildId}/roles/${contribution.roleId}`,
fetcher
)
Expand Down
Loading
Loading