diff --git a/.env b/.env index d466608671..14a18012aa 100644 --- a/.env +++ b/.env @@ -1,15 +1,15 @@ -NEXT_PUBLIC_API=https://api.guild.xyz/v1 -NEXT_PUBLIC_DISCORD_CLIENT_ID=868172385000509460 +NEXT_PUBLIC_API=https://api.dev.guild-api.xyz/v1 +NEXT_PUBLIC_DISCORD_CLIENT_ID=1118173473676722258 NEXT_PUBLIC_IPFS_GATEWAY=https://guild-xyz.mypinata.cloud/ipfs/ NEXT_PUBLIC_PINATA_API=https://api.pinata.cloud NEXT_PUBLIC_BALANCY_API=https://balancy.guild.xyz/api -NEXT_PUBLIC_TG_BOT_USERNAME=guildxyz_bot -NEXT_PUBLIC_GOOGLE_CLIENT_ID=639132320574-9v9b8d9mq7rjctmjmolsjeklkl2rlcsh.apps.googleusercontent.com -NEXT_PUBLIC_GOOGLE_SERVICE_ACCOUNT_EMAIL=guild-xyz@guildxyz.iam.gserviceaccount.com +NEXT_PUBLIC_TG_BOT_USERNAME=Guildxyz_dev_gcp_bot +NEXT_PUBLIC_GOOGLE_CLIENT_ID=829004986756-f5b265m0hscpaa0ah9cgqht02o8qea15.apps.googleusercontent.com +NEXT_PUBLIC_GOOGLE_SERVICE_ACCOUNT_EMAIL=bvz-test-service-account@bvz-test-project.iam.gserviceaccount.com NEXT_PUBLIC_POSTHOG_KEY=phc_Pu6Xv72B95fHVTAKT5Xs2FPgNxrsNP4LecBqPiVAAxi NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=d851f25304d67fc8e2dd3b354223e4fa NEXT_PUBLIC_EDGE_CONFIG_ID=ecfg_buc5l6124c4koymyvseasbd1k3hs NEXT_PUBLIC_EDGE_CONFIG_READ_ACCESS_TOKEN=8b337e65-3aa6-4949-97b8-c7eab7151128 -NEXT_PUBLIC_RECAPTCHA_SITE_KEY=6LcQm4onAAAAAOcoqkw9A5txg5SbuddONchMZKrF +NEXT_PUBLIC_RECAPTCHA_SITE_KEY=6LcMe3knAAAAAJjUyeMh1LbUcrh5k0aG0fJIZaJR NEXT_PUBLIC_POLYGONID_API=https://guild-privacy.s.guild.xyz -NEXT_PUBLIC_BUGSNAG_KEY=4bd5799ac2cb4a34887513b80b845554 \ No newline at end of file +NEXT_PUBLIC_BUGSNAG_KEY=4bd5799ac2cb4a34887513b80b845554 diff --git a/package-lock.json b/package-lock.json index f5b9e2153b..562a94fbe2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@emotion/styled": "^11.11.0", "@fuels/connectors": "^0.5.0", "@fuels/react": "^0.20.0", - "@guildxyz/types": "^1.9.38", + "@guildxyz/types": "^1.9.39", "@hcaptcha/react-hcaptcha": "^1.4.4", "@hookform/resolvers": "^3.3.4", "@lexical/code": "^0.12.0", @@ -41,6 +41,8 @@ "@radix-ui/react-focus-scope": "^1.1.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", @@ -5473,10 +5475,9 @@ } }, "node_modules/@guildxyz/types": { - "version": "1.9.38", - "resolved": "https://registry.npmjs.org/@guildxyz/types/-/types-1.9.38.tgz", - "integrity": "sha512-cyGwit9QVnjoqQXf/XhiSOnevjDWnu4qeQQEVK319P1vvXxF/0zKDUBqYdgp62gkLG3wENDTKOaE5O4AB9dDMQ==", - "license": "ISC", + "version": "1.9.39", + "resolved": "https://registry.npmjs.org/@guildxyz/types/-/types-1.9.39.tgz", + "integrity": "sha512-7tKwioSR6cTEYBbkqS0Q6QFR+nlk5Y0yIweMnPu/lJM9B9DzUlTgnMzR/2AydYKHXxUXrWZT7sLipdIqnJPJcg==", "dependencies": { "zod": "^3.22.4" } @@ -7798,6 +7799,11 @@ "url": "https://opencollective.com/preact" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", @@ -8415,6 +8421,29 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz", + "integrity": "sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==", + "dependencies": { + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", @@ -8445,6 +8474,72 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz", + "integrity": "sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/react-remove-scroll": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.4", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz", @@ -8664,6 +8759,20 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", diff --git a/package.json b/package.json index a44e7ea90f..4659c8df4f 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@emotion/styled": "^11.11.0", "@fuels/connectors": "^0.5.0", "@fuels/react": "^0.20.0", - "@guildxyz/types": "^1.9.38", + "@guildxyz/types": "^1.9.39", "@hcaptcha/react-hcaptcha": "^1.4.4", "@hookform/resolvers": "^3.3.4", "@lexical/code": "^0.12.0", @@ -53,6 +53,8 @@ "@radix-ui/react-focus-scope": "^1.1.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", diff --git a/src/app/(marketing)/create-profile/_components/StartProfile.tsx b/src/app/(marketing)/create-profile/_components/StartProfile.tsx index 3a0f974276..195d3f210a 100644 --- a/src/app/(marketing)/create-profile/_components/StartProfile.tsx +++ b/src/app/(marketing)/create-profile/_components/StartProfile.tsx @@ -14,8 +14,7 @@ import { import { Input } from "@/components/ui/Input" import { useToast } from "@/components/ui/hooks/useToast" import { cn } from "@/lib/utils" -import { profileSchema } from "@/lib/validations/profileSchema" -import { Schemas } from "@guildxyz/types" +import { Schemas, schemas } from "@guildxyz/types" import { zodResolver } from "@hookform/resolvers/zod" import { Spinner, UploadSimple, User } from "@phosphor-icons/react" import { ArrowRight } from "@phosphor-icons/react/dist/ssr" @@ -54,7 +53,7 @@ export const StartProfile: OnboardingChain = () => { }, [farcasterProfile]) const form = useForm({ - resolver: zodResolver(profileSchema), + resolver: zodResolver(schemas.ProfileCreationSchema), defaultValues: { name: "", username: "", diff --git a/src/app/(marketing)/create-profile/_hooks/useCreateProfile.tsx b/src/app/(marketing)/create-profile/_hooks/useCreateProfile.tsx index 80d2a75220..f406185e01 100644 --- a/src/app/(marketing)/create-profile/_hooks/useCreateProfile.tsx +++ b/src/app/(marketing)/create-profile/_hooks/useCreateProfile.tsx @@ -16,14 +16,13 @@ export const useCreateProfile = () => { ...signedValidation, }) - const submitWithSign = useSubmitWithSign(createProfile, { + const submitWithSign = useSubmitWithSign(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) => { diff --git a/src/app/(marketing)/create-profile/page.tsx b/src/app/(marketing)/create-profile/page.tsx index 45b71c987b..9c5a0710bb 100644 --- a/src/app/(marketing)/create-profile/page.tsx +++ b/src/app/(marketing)/create-profile/page.tsx @@ -9,6 +9,7 @@ export const metadata: Metadata = { } const Page = () => { + // todo: get finetuned layout from new create-guild return (
{ />
- +
diff --git a/src/app/(marketing)/create-profile/layout.tsx b/src/app/(marketing)/layout.tsx similarity index 100% rename from src/app/(marketing)/create-profile/layout.tsx rename to src/app/(marketing)/layout.tsx diff --git a/src/app/(marketing)/profile/[username]/not-found.tsx b/src/app/(marketing)/profile/[username]/not-found.tsx new file mode 100644 index 0000000000..9fd2f43c83 --- /dev/null +++ b/src/app/(marketing)/profile/[username]/not-found.tsx @@ -0,0 +1,26 @@ +import { Button } from "@/components/ui/Button" +import { House } from "@phosphor-icons/react/dist/ssr" +import GuildGhost from "static/avatars/58.svg" + +const NotFound = () => { + return ( +
+ + +

Profile not found

+ +

+ + +
+ ) +} + +export default NotFound diff --git a/src/app/(marketing)/profile/[username]/page.tsx b/src/app/(marketing)/profile/[username]/page.tsx new file mode 100644 index 0000000000..69239ef3ec --- /dev/null +++ b/src/app/(marketing)/profile/[username]/page.tsx @@ -0,0 +1,159 @@ +import { Header } from "@/components/Header" +import { + Layout, + LayoutBanner, + LayoutFooter, + LayoutHero, + LayoutMain, +} from "@/components/Layout" +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 { ActivityLogAction } from "components/[guild]/activity/constants" +import { env } from "env" +import Image from "next/image" +import { notFound, redirect } from "next/navigation" +import { Profile } from "../_components/Profile" +import { ProfileColorBanner } from "../_components/ProfileColorBanner" + +const api = env.NEXT_PUBLIC_API + +async function ssrFetcher(...args: Parameters) { + return (await fetch(...args)).json() as T +} + +const fetchPublicProfileData = async ({ username }: { username: string }) => { + const activitiesRequest = new URL(`v2/profiles/${username}/activity`, api) + const contributionsRequest = new URL(`v2/profiles/${username}/contributions`, api) + const profileRequest = new URL(`v2/profiles/${username}`, api) + const profileResponse = await fetch(profileRequest, { + next: { + tags: ["profile"], + revalidate: 3600, + }, + }) + + if (profileResponse.status === 404) notFound() + if (!profileResponse.ok) redirect("/error") + + const profile = (await profileResponse.json()) as Schemas["Profile"] + const contributions = await ssrFetcher( + contributionsRequest, + { + next: { + tags: ["contributions"], + revalidate: 3600, + }, + } + ) + const activities = await ssrFetcher(activitiesRequest, { + next: { + tags: ["contributions"], + revalidate: 60, + }, + }) + 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(req, { + next: { + revalidate: 3 * 3600, + }, + }) + ) + ) + const roles = await Promise.all( + roleRequests.map((req) => + ssrFetcher(req, { + next: { + revalidate: 3 * 3600, + }, + }) + ) + ) + const guildsZipped = Object.fromEntries( + guildRequests.map(({ pathname }, i) => [pathname, guilds[i]]) + ) + const rolesZipped = Object.fromEntries( + roleRequests.map(({ pathname }, i) => [pathname, roles[i]]) + ) + return { + profile, + fallback: { + [activitiesRequest.pathname]: activities, + [profileRequest.pathname]: profile, + [contributionsRequest.pathname]: contributions, + ...guildsZipped, + ...rolesZipped, + }, + } +} + +const Page = async ({ params: { username } }: { params: { username: string } }) => { + const { fallback, profile } = await fetchPublicProfileData({ username }) + + const isBgColor = profile.backgroundImageUrl?.startsWith("#") + + return ( + + + +
+ + {isBgColor ? ( + + ) : ( + profile.backgroundImageUrl && ( + profile background image + ) + )} +
+ + + + + + +

+ Guild Profiles are currently in invite only early access, only available + to{" "} + + Subscribers + + +

+
+ + + ) +} + +// biome-ignore lint/style/noDefaultExport: page route +export default Page diff --git a/src/app/(marketing)/profile/_components/CardWithGuildLabel.tsx b/src/app/(marketing)/profile/_components/CardWithGuildLabel.tsx new file mode 100644 index 0000000000..aa221d9494 --- /dev/null +++ b/src/app/(marketing)/profile/_components/CardWithGuildLabel.tsx @@ -0,0 +1,41 @@ +import { CheckMark } from "@/components/CheckMark" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/Avatar" +import { Card } from "@/components/ui/Card" +import { Guild } from "@guildxyz/types" +import { PropsWithChildren } from "react" + +export const CardWithGuildLabel = ({ + guild, + children, +}: PropsWithChildren<{ guild: Guild }>) => { + return ( + +
+
+ + + + +
+ {guild.name} +
+ +
+
+
+ {children} +
+
+ ) +} diff --git a/src/app/(marketing)/profile/_components/ContributionCard.tsx b/src/app/(marketing)/profile/_components/ContributionCard.tsx new file mode 100644 index 0000000000..08347d0bde --- /dev/null +++ b/src/app/(marketing)/profile/_components/ContributionCard.tsx @@ -0,0 +1,16 @@ +"use client" + +import { Guild, Role, Schemas } from "@guildxyz/types" +import useSWRImmutable from "swr/immutable" +import { ContributionCardView } from "./ContributionCardView" + +export const ContributionCard = ({ + contribution, +}: { contribution: Schemas["Contribution"] }) => { + const guild = useSWRImmutable(`/v2/guilds/${contribution.guildId}`) + const role = useSWRImmutable( + `/v2/guilds/${contribution.guildId}/roles/${contribution.roleId}` + ) + if (!role.data || !guild.data) return + return +} diff --git a/src/app/(marketing)/profile/_components/ContributionCardView.tsx b/src/app/(marketing)/profile/_components/ContributionCardView.tsx new file mode 100644 index 0000000000..a2db59727d --- /dev/null +++ b/src/app/(marketing)/profile/_components/ContributionCardView.tsx @@ -0,0 +1,44 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/Avatar" +import { AvatarGroup } from "@/components/ui/AvatarGroup" +import { Separator } from "@/components/ui/Separator" +import { Guild, Role } from "@guildxyz/types" +import { Users } from "@phosphor-icons/react/dist/ssr" +import { CardWithGuildLabel } from "./CardWithGuildLabel" + +export const ContributionCardView = ({ + guild, + role, +}: { guild: Guild; role: Role }) => { + return ( + +
+ + + + +
+
+ TOP ROLE +
+

+ {role.name} +

+
+ +

+ Only {((role.memberCount / guild.memberCount) * 100).toFixed(1)}% of + members have this role +

+
+
+
+ +
+ COLLECTION: +
+ +
+
+
+ ) +} diff --git a/src/app/(marketing)/profile/_components/EditContributions.tsx b/src/app/(marketing)/profile/_components/EditContributions.tsx new file mode 100644 index 0000000000..e428d5c431 --- /dev/null +++ b/src/app/(marketing)/profile/_components/EditContributions.tsx @@ -0,0 +1,249 @@ +"use client" + +import { Avatar, AvatarImage } from "@/components/ui/Avatar" +import { Button } from "@/components/ui/Button" +import { Card } from "@/components/ui/Card" +import { + Dialog, + DialogBody, + DialogCloseButton, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/Dialog" +import { Label } from "@/components/ui/Label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/Select" +import { useToast } from "@/components/ui/hooks/useToast" +import { useYourGuilds } from "@/hooks/useYourGuilds" +import { Guild, MembershipResult, Role, Schemas } from "@guildxyz/types" +import { X } from "@phosphor-icons/react" +import { PencilSimple } from "@phosphor-icons/react" +import { AvatarFallback } from "@radix-ui/react-avatar" +import { DialogDescription } from "@radix-ui/react-dialog" +import { useState } from "react" +import useSWRImmutable from "swr/immutable" +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 EditContributionCard = ({ + contribution, +}: { contribution: Schemas["Contribution"] }) => { + const { data: guild } = useSWRImmutable( + `/v2/guilds/${contribution.guildId}` + ) + const memberships = useMemberships() + const editContribution = useUpdateContribution({ contributionId: contribution.id }) + const deleteContribution = useDeleteContribution({ + contributionId: contribution.id, + }) + if (!guild || !memberships.data) return + const roleIds = memberships.data.find( + (membership) => membership.guildId === guild.id + )?.roleIds + + return ( + +
+ + + +
+
+ ) +} + +export const EditContributions = () => { + const contributions = useContributions() + const memberships = useMemberships() + const [guildId, setGuildId] = useState("") + const [roleId, setRoleId] = useState("") + const { toast } = useToast() + + const { data: baseGuilds } = useYourGuilds() + const guilds = baseGuilds?.filter(({ tags }) => tags.includes("VERIFIED")) + + const roleIds = memberships.data?.find( + (membership) => membership.guildId.toString() === guildId + )?.roleIds + const createContribution = useCreateContribution() + + return ( + + + + + + + Edit top contributions + + + + +
+ {contributions.data?.slice(0, 3).map((contribution) => ( + + ))} +
+
+

+ Add contribution +

+ +
+ + +
+
+ + +
+ +
+
+
+
+
+ ) +} + +const GuildSelectItem = ({ guildId }: Pick) => { + const { data } = useSWRImmutable(`/v2/guilds/${guildId}`) + if (!data) return + return ( + +
+ + + + + {data.name} +
+
+ ) +} + +const RoleSelectItem = ({ + roleId, + guildId, +}: Pick & { + roleId: MembershipResult["roleIds"][number] +}) => { + const { data: data } = useSWRImmutable( + `/v2/guilds/${guildId}/roles/${roleId}` + ) + if (!data) return + return ( + +
+ + + + + {data.name} +
+
+ ) +} diff --git a/src/app/(marketing)/profile/_components/EditProfile/EditProfile.tsx b/src/app/(marketing)/profile/_components/EditProfile/EditProfile.tsx new file mode 100644 index 0000000000..79ae4d22ca --- /dev/null +++ b/src/app/(marketing)/profile/_components/EditProfile/EditProfile.tsx @@ -0,0 +1,219 @@ +"use client" + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/Avatar" +import { Button } from "@/components/ui/Button" +import { Card } from "@/components/ui/Card" +import { + Dialog, + DialogBody, + DialogCloseButton, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/Dialog" +import { + FormControl, + FormErrorMessage, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/Form" +import { Input } from "@/components/ui/Input" +import { Separator } from "@/components/ui/Separator" +import { Textarea } from "@/components/ui/Textarea" +import { toast } from "@/components/ui/hooks/useToast" +import { useDisclosure } from "@/hooks/useDisclosure" +import { cn } from "@/lib/utils" +import { Schemas, schemas } from "@guildxyz/types" +import { zodResolver } from "@hookform/resolvers/zod" +import { Pencil, User } from "@phosphor-icons/react" +import useDropzone from "hooks/useDropzone" +import usePinata from "hooks/usePinata" +import { useState } from "react" +import { FormProvider, useForm } from "react-hook-form" +import { useDeleteProfile } from "../../_hooks/useDeleteProfile" +import { useProfile } from "../../_hooks/useProfile" +import { useUpdateProfile } from "../../_hooks/useUpdateProfile" +import { EditProfileBanner } from "./EditProfileBanner" + +export const EditProfile = () => { + const { data: profile } = useProfile() + const form = useForm({ + resolver: zodResolver(schemas.ProfileUpdateSchema), + defaultValues: { + ...schemas.ProfileUpdateSchema.parse(profile), + }, + mode: "onTouched", + }) + const disclosure = useDisclosure() + const editProfile = useUpdateProfile() + + async function onSubmit(values: Schemas["Profile"]) { + await editProfile.onSubmit(schemas.ProfileUpdateSchema.parse(values)) + if (editProfile.error) return + disclosure.onClose() + } + + const { isUploading, onUpload } = usePinata({ + control: form.control, + fieldToSetOnSuccess: "profileImageUrl", + onError: (error) => { + toast({ + variant: "error", + title: "Failed to upload file", + description: error, + }) + }, + }) + + const [uploadProgress, setUploadProgress] = useState(0) + const { isDragActive, getRootProps } = useDropzone({ + multiple: false, + noClick: false, + onDrop: (acceptedFiles) => { + if (!acceptedFiles[0]) return + onUpload({ + data: [acceptedFiles[0]], + onProgress: setUploadProgress, + }) + }, + onError: (error) => { + toast({ + variant: "error", + title: `Failed to upload file`, + description: error.message, + }) + }, + }) + + const deleteProfile = useDeleteProfile() + + return ( + + + + + + + + + + Edit profile + + + +
+ + ( + + )} + /> +
+ + ( + + Name + + + + + + )} + /> + ( + + Username + + + + + + )} + /> + ( + + Bio + +