diff --git a/package-lock.json b/package-lock.json index ad16d9c9b9..7290570e88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "@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-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", @@ -8423,6 +8424,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", diff --git a/package.json b/package.json index d0afa58b4f..3a9d24e52a 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@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-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", diff --git a/src/app/(marketing)/profile/[username]/atoms.ts b/src/app/(marketing)/profile/[username]/atoms.ts new file mode 100644 index 0000000000..58df821496 --- /dev/null +++ b/src/app/(marketing)/profile/[username]/atoms.ts @@ -0,0 +1,5 @@ +import { Schemas } from "@guildxyz/types" +import { atom } from "jotai" + +// TODO: assertion here prevents WritableAtom type error, handle uninitialized atom with proper types +export const profileAtom = atom(undefined as unknown as Schemas["Profile"]) diff --git a/src/app/(marketing)/profile/[username]/constants.ts b/src/app/(marketing)/profile/[username]/constants.ts new file mode 100644 index 0000000000..34d7780ea0 --- /dev/null +++ b/src/app/(marketing)/profile/[username]/constants.ts @@ -0,0 +1,14 @@ +export const MAX_LEVEL = 100 +export const RANKS = [ + { color: "#78c93d", title: "novice" }, + { color: "#88d525", title: "learner" }, + { color: "#f6ca45", title: "knight" }, + { color: "#78c93d", title: "veteran" }, + { color: "#ec5a53", title: "champion" }, + { color: "#53adf0", title: "hero" }, + { color: "#c385f8", title: "master" }, + { color: "#3e6fc3", title: "grand master" }, + { color: "#be4681", title: "legend" }, + { color: "#000000", title: "mythic" }, + { color: "linear-gradient(to left top, #00cbfa, #b9f2ff)", title: "???" }, +] as const diff --git a/src/app/(marketing)/profile/[username]/page.tsx b/src/app/(marketing)/profile/[username]/page.tsx new file mode 100644 index 0000000000..e72aaaee88 --- /dev/null +++ b/src/app/(marketing)/profile/[username]/page.tsx @@ -0,0 +1,211 @@ +"use client" + +import { CheckMark } from "@/components/CheckMark" +import { Header } from "@/components/Header" +import { + Layout, + LayoutBanner, + LayoutFooter, + LayoutHero, + LayoutMain, +} from "@/components/Layout" +import { Anchor } from "@/components/ui/Anchor" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/Avatar" +import { AvatarGroup } from "@/components/ui/AvatarGroup" +import { Button } from "@/components/ui/Button" +import { Separator } from "@/components/ui/Separator" +import { Skeleton } from "@/components/ui/Skeleton" +import { 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 { CircularProgressBar } from "../_components/CircularProgressBar" +import { ContributionCard } from "../_components/ContributionCard" +import { EditContributions } from "../_components/EditContributions" +import { EditProfile } from "../_components/EditProfile" +import { LevelBadge } from "../_components/LevelBadge" +import { OperatedGuildCard } from "../_components/OperatedGuildCard" +import { ProfileSkeleton } from "../_components/ProfileSkeleton" +import { RecentActivity } from "../_components/RecentActivity" +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 +// } 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 +// } +// } + +const Page = ({ + params: { username }, +}: { + params: { username: string } +}) => { + const { data: fetchedProfile, isLoading } = useSWR( + `/v2/profiles/${username}`, + fetcherForSWR + ) + const [profile, setProfile] = useAtom(profileAtom) + const level = 0 + + useEffect(() => { + setProfile(fetchedProfile) + }, [fetchedProfile, setProfile]) + + if (!profile || isLoading) { + return + } + + return ( + + +
+ +
+
+ + + +
+
+ +
+ + + + + + + + +
+

+ {profile.name} + +

+
@{profile.username}
+

+ {profile.bio} +

+
+
+
3232
+
Guildmates
+
+ +
+
0
+
Followers
+
+ +
+ +
+ Followed by Hoho,
+ Hihi and 22 others +
+
+
+
+ {/*

Experience

+
+ +
+ +
+

Champion

+

1322 / 9999 XP

+
+ +

+ This is a description that perfectly matches the 80 character + description limit. +

+
+
+ +
+

Engagement this month

+ +72 XP +
+ +
+
*/} +

Operated guilds

+ +
+

Top contributions

+ +
+
+ + + + +
+
+

Recent activity

+ +

+ … only last 20 actions are shown +

+
+
+
+ +

+ 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/CircularProgressBar.tsx b/src/app/(marketing)/profile/_components/CircularProgressBar.tsx new file mode 100644 index 0000000000..0b590b219d --- /dev/null +++ b/src/app/(marketing)/profile/_components/CircularProgressBar.tsx @@ -0,0 +1,10 @@ +"use client" + +const chartData = [{ experience: 1, fill: "hsl(var(--primary))" }] + +// TODO: use svg clip-path +export const CircularProgressBar = ({ progress }: { progress: number }) => { + const size = 224 + const strokeWidth = 10 + return null +} diff --git a/src/app/(marketing)/profile/_components/ContributionCard.tsx b/src/app/(marketing)/profile/_components/ContributionCard.tsx new file mode 100644 index 0000000000..a30547b55b --- /dev/null +++ b/src/app/(marketing)/profile/_components/ContributionCard.tsx @@ -0,0 +1,48 @@ +import { Avatar, AvatarFallback } from "@/components/ui/Avatar" +import { AvatarGroup } from "@/components/ui/AvatarGroup" +import { Button } from "@/components/ui/Button" +import { Card } from "@/components/ui/Card" +import { Separator } from "@/components/ui/Separator" +import { CaretDown, Users } from "@phosphor-icons/react/dist/ssr" + +export const ContributionCard = () => { + return ( + +
+
+ + # + +
Guild
+
+
+
+ + # + +
+
+ TOP ROLE +
+

+ Enter Farcaster +

+
+ +

Only 3.4% of users 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..91c2d27c52 --- /dev/null +++ b/src/app/(marketing)/profile/_components/EditContributions.tsx @@ -0,0 +1,61 @@ +"use client" +import { Button } from "@/components/ui/Button" +import { + Dialog, + DialogBody, + DialogCloseButton, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/Dialog" +// import { profileSchema } from "@/lib/validations/profileSchema" +import { Schemas } from "@guildxyz/types" +// import { zodResolver } from "@hookform/resolvers/zod" +import { PencilSimple } from "@phosphor-icons/react" +import { FormProvider, useForm } from "react-hook-form" + +export const EditContributions = ( + contribution: Schemas["ProfileContributionUpdate"] +) => { + const form = useForm({ + // resolver: zodResolver(profileSchema), + defaultValues: { + ...contribution, + }, + mode: "onTouched", + }) + + async function onSubmit(values: Schemas["ProfileContributionUpdate"]) { + console.log("edit contributions submit", values) + } + + return ( + + + + + + + Edit profile + + + +
+ + + +
+
+
+
+ ) +} diff --git a/src/app/(marketing)/profile/_components/EditProfile.tsx b/src/app/(marketing)/profile/_components/EditProfile.tsx new file mode 100644 index 0000000000..4f52600eda --- /dev/null +++ b/src/app/(marketing)/profile/_components/EditProfile.tsx @@ -0,0 +1,232 @@ +"use client" + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/Avatar" +import { Button } from "@/components/ui/Button" +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 { cn } from "@/lib/utils" +import { profileSchema } from "@/lib/validations/profileSchema" +import { Schemas } from "@guildxyz/types" +import { zodResolver } from "@hookform/resolvers/zod" +import { Eyedropper, Image as ImageIcon, Pencil, User } from "@phosphor-icons/react" +import useDropzone from "hooks/useDropzone" +import usePinata from "hooks/usePinata" +import Image from "next/image" +import { useState } from "react" +import { FormProvider, useForm } from "react-hook-form" + +export const EditProfile = (profile: Schemas["ProfileUpdate"]) => { + const form = useForm({ + resolver: zodResolver(profileSchema), + defaultValues: { + ...profile, + }, + mode: "onTouched", + }) + + async function onSubmit(values: Schemas["ProfileUpdate"]) { + console.log("edit profile submit", values) + } + + 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, + }) + }, + }) + + return ( + + + + + +
+ + + Edit profile + + + +
+ ( + +
+ {field.value ? ( + profile background + ) : ( +
+ )} +
+
+ + + +
+ + )} + /> + ( + + )} + /> +
+ + ( + + Name + + + + + + )} + /> + ( + + Username + + + + + + )} + /> + ( + + Bio + +