From efde5c601903e66662715a7e72605d4d08aea3b6 Mon Sep 17 00:00:00 2001 From: BrickheadJohnny Date: Fri, 22 Nov 2024 12:37:57 +0100 Subject: [PATCH] feat: simple create guild page --- guild.d.ts | 33 ------- package.json | 2 + .../components/CreateGuildButton.tsx | 68 +++++++++++++ .../components/CreateGuildForm.tsx | 51 ++++++++++ .../components/CreateGuildFormProvider.tsx | 25 +++++ src/app/create-guild/page.tsx | 38 +++++++ src/app/explorer/components/GuildCard.tsx | 1 + src/app/explorer/fetchers.ts | 2 + src/app/explorer/page.tsx | 2 + src/components/ConfettiProvider.tsx | 99 +++++++++++++++++++ src/components/Header.tsx | 2 +- src/components/SignInButton.tsx | 8 +- src/components/SignOutButton.tsx | 1 - src/lib/fetcher.ts | 39 ++++++++ src/lib/getCookie.ts | 13 +++ src/lib/schemas/guild.ts | 16 +++ src/lib/types.ts | 10 ++ 17 files changed, 368 insertions(+), 42 deletions(-) delete mode 100644 guild.d.ts create mode 100644 src/app/create-guild/components/CreateGuildButton.tsx create mode 100644 src/app/create-guild/components/CreateGuildForm.tsx create mode 100644 src/app/create-guild/components/CreateGuildFormProvider.tsx create mode 100644 src/app/create-guild/page.tsx create mode 100644 src/components/ConfettiProvider.tsx create mode 100644 src/lib/fetcher.ts create mode 100644 src/lib/getCookie.ts create mode 100644 src/lib/schemas/guild.ts create mode 100644 src/lib/types.ts diff --git a/guild.d.ts b/guild.d.ts deleted file mode 100644 index 58eafbad35..0000000000 --- a/guild.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -// dumping here types util it comes down properly - -export {} - -declare global { - type Guild = { - name: string; - id: string; - urlName: string; - createdAt: number; - updatedAt: number; - description: string; - imageUrl: string; - backgroundImageUrl: string; - visibility: Record; - settings: Record; - searchTags: string[]; - categoryTags: string[]; - socialLinks: Record; - owner: string; - }; - - type PaginatedResponse = { - page: number; - pageSize: number; - sortBy: string; - reverse: boolean; - searchQuery: string; - query: string; - items: Item[]; - total: number; - }; -} diff --git a/package.json b/package.json index e18764f217..a2df1715be 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,10 @@ "next": "15.0.3", "next-themes": "^0.4.3", "react": "19.0.0-rc-66855b96-20241106", + "react-canvas-confetti": "^2.0.7", "react-dom": "19.0.0-rc-66855b96-20241106", "react-hook-form": "^7.53.2", + "slugify": "^1.6.6", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.1", diff --git a/src/app/create-guild/components/CreateGuildButton.tsx b/src/app/create-guild/components/CreateGuildButton.tsx new file mode 100644 index 0000000000..dbc2ca3693 --- /dev/null +++ b/src/app/create-guild/components/CreateGuildButton.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { useConfetti } from "@/components/ConfettiProvider"; +import { Button } from "@/components/ui/Button"; +import { GUILD_AUTH_COOKIE_NAME } from "@/config/constants"; +import { env } from "@/lib/env"; +import { fetcher } from "@/lib/fetcher"; +import { getCookie } from "@/lib/getCookie"; +import type { CreateGuildForm, Guild } from "@/lib/schemas/guild"; +import { useMutation } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { useFormContext } from "react-hook-form"; +import slugify from "slugify"; + +const CreateGuildButton = () => { + const { handleSubmit } = useFormContext(); + + const confetti = useConfetti(); + + const router = useRouter(); + + const { mutate: onSubmit, isPending } = useMutation({ + mutationFn: async (data: CreateGuildForm) => { + const token = getCookie(GUILD_AUTH_COOKIE_NAME); + + if (!token) throw new Error("Unauthorized"); // TODO: custom errors? + + const guild = { + ...data, + contact: undefined, + // TODO: I think we should do it on the backend + urlName: slugify(data.name, { + replacement: "-", + lower: true, + strict: true, + }), + }; + + return fetcher(`${env.NEXT_PUBLIC_API}/guild`, { + method: "POST", + headers: { + "X-Auth-Token": token, + }, + body: JSON.stringify(guild), + }); + }, + onError: (error) => console.error(error), + onSuccess: (res) => { + confetti.current(); + router.push(`/${res.urlName}`); + console.log(res); + }, + }); + + return ( + + ); +}; + +export { CreateGuildButton }; diff --git a/src/app/create-guild/components/CreateGuildForm.tsx b/src/app/create-guild/components/CreateGuildForm.tsx new file mode 100644 index 0000000000..ab5e96650e --- /dev/null +++ b/src/app/create-guild/components/CreateGuildForm.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { useFormContext } from "react-hook-form"; + +import { + FormControl, + FormErrorMessage, + FormField, + FormItem, + FormLabel, +} from "@/components/ui/Form"; +import { Input } from "@/components/ui/Input"; +import type { CreateGuildForm as CreateGuildFormType } from "@/lib/schemas/guild"; + +export const CreateGuildForm = () => { + const { control } = useFormContext(); + + return ( + <> + ( + + Guild name + + + + + + + )} + /> + + ( + + E-mail address + + + + + + + )} + /> + + ); +}; diff --git a/src/app/create-guild/components/CreateGuildFormProvider.tsx b/src/app/create-guild/components/CreateGuildFormProvider.tsx new file mode 100644 index 0000000000..9c3b119dd9 --- /dev/null +++ b/src/app/create-guild/components/CreateGuildFormProvider.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { type CreateGuildForm, GuildSchema } from "@/lib/schemas/guild"; +import { zodResolver } from "@hookform/resolvers/zod"; +import type { PropsWithChildren } from "react"; +import { FormProvider, useForm } from "react-hook-form"; + +const defaultValues = { + name: "", + imageUrl: "", + urlName: "test", + contact: "", +} satisfies CreateGuildForm; + +const CreateGuildFormProvider = ({ children }: PropsWithChildren) => { + const methods = useForm({ + mode: "all", + resolver: zodResolver(GuildSchema), + defaultValues, + }); + + return {children}; +}; + +export { CreateGuildFormProvider }; diff --git a/src/app/create-guild/page.tsx b/src/app/create-guild/page.tsx new file mode 100644 index 0000000000..c24a6880af --- /dev/null +++ b/src/app/create-guild/page.tsx @@ -0,0 +1,38 @@ +import { AuthBoundary } from "@/components/AuthBoundary"; +import { ConfettiProvider } from "@/components/ConfettiProvider"; +import { SignInButton } from "@/components/SignInButton"; +import { Card } from "@/components/ui/Card"; +import { CreateGuildButton } from "./components/CreateGuildButton"; +import { CreateGuildForm } from "./components/CreateGuildForm"; +import { CreateGuildFormProvider } from "./components/CreateGuildFormProvider"; + +export const metadata = { + title: "Begin your guild", +}; + +const CreateGuild = () => ( +
+ {/* TODO: make a common layout component & use it here too */} + + + +

+ Begin your guild +

+ + {/* TODO: */} + +
+ +
+ + }> + + +
+
+
+
+); + +export default CreateGuild; diff --git a/src/app/explorer/components/GuildCard.tsx b/src/app/explorer/components/GuildCard.tsx index cef6659310..7c9ba80f0c 100644 --- a/src/app/explorer/components/GuildCard.tsx +++ b/src/app/explorer/components/GuildCard.tsx @@ -1,6 +1,7 @@ import { Badge } from "@/components/ui/Badge"; import { Card } from "@/components/ui/Card"; import { Skeleton } from "@/components/ui/Skeleton"; +import type { Guild } from "@/lib/schemas/guild"; import { ImageSquare, Users } from "@phosphor-icons/react/dist/ssr"; import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar"; import Link from "next/link"; diff --git a/src/app/explorer/fetchers.ts b/src/app/explorer/fetchers.ts index f2ce54db18..7bfc490ea4 100644 --- a/src/app/explorer/fetchers.ts +++ b/src/app/explorer/fetchers.ts @@ -1,4 +1,6 @@ import { env } from "@/lib/env"; +import type { Guild } from "@/lib/schemas/guild"; +import type { PaginatedResponse } from "@/lib/types"; import { PAGE_SIZE } from "./constants"; export const getGuildSearch = diff --git a/src/app/explorer/page.tsx b/src/app/explorer/page.tsx index 34803c5bed..5c7f511e53 100644 --- a/src/app/explorer/page.tsx +++ b/src/app/explorer/page.tsx @@ -1,6 +1,8 @@ import { AuthBoundary } from "@/components/AuthBoundary"; import { SignInButton } from "@/components/SignInButton"; import { env } from "@/lib/env"; +import type { Guild } from "@/lib/schemas/guild"; +import type { PaginatedResponse } from "@/lib/types"; import { HydrationBoundary, QueryClient, diff --git a/src/components/ConfettiProvider.tsx b/src/components/ConfettiProvider.tsx new file mode 100644 index 0000000000..1878f5f2e3 --- /dev/null +++ b/src/components/ConfettiProvider.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { + type MutableRefObject, + type PropsWithChildren, + createContext, + useContext, + useRef, +} from "react"; +import ReactCanvasConfetti from "react-canvas-confetti/dist"; +import type { TCanvasConfettiInstance } from "react-canvas-confetti/dist/types"; + +const doubleConfetti = (confetti: TCanvasConfettiInstance) => { + const count = 200; + const defaultsPerBarrage: confetti.Options[] = [ + { + origin: { x: -0.05 }, + angle: 50, + }, + { + origin: { x: 1.05 }, + angle: 130, + }, + ] as const; + + const fire = (particleRatio: number, opts: confetti.Options) => { + confetti({ + ...opts, + particleCount: Math.floor(count * particleRatio), + }); + }; + + for (const defaults of defaultsPerBarrage) { + fire(0.25, { + spread: 26, + startVelocity: 55, + ...defaults, + }); + fire(0.2, { + spread: 60, + ...defaults, + }); + fire(0.35, { + spread: 100, + decay: 0.91, + scalar: 0.8, + ...defaults, + }); + fire(0.1, { + spread: 120, + startVelocity: 25, + decay: 0.92, + scalar: 1.2, + ...defaults, + }); + fire(0.1, { + spread: 120, + startVelocity: 45, + ...defaults, + }); + } +}; + +const ConfettiContext = createContext>( + {} as MutableRefObject, +); + +export const useConfetti = () => useContext(ConfettiContext); + +type ConfettiPlayer = () => void; + +export const ConfettiProvider = ({ children }: PropsWithChildren) => { + const confettiRef = useRef(() => { + return; + }); + + const onInitHandler = ({ + confetti, + }: { confetti: TCanvasConfettiInstance }) => { + const confettiClosure: ConfettiPlayer = () => { + doubleConfetti(confetti); + }; + confettiRef.current = confettiClosure; + }; + + return ( + + {children} + + + ); +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 2d18301eb7..b6beb9de72 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -6,7 +6,7 @@ import { Card } from "./ui/Card"; export const Header = () => (
{/* TODO: NavMenu component */} - + }> diff --git a/src/components/SignInButton.tsx b/src/components/SignInButton.tsx index 1731cddce8..9715059b4b 100644 --- a/src/components/SignInButton.tsx +++ b/src/components/SignInButton.tsx @@ -1,22 +1,16 @@ "use client"; import { signInDialogOpenAtom } from "@/config/atoms"; -import { cn } from "@/lib/cssUtils"; import { SignIn } from "@phosphor-icons/react/dist/ssr"; import { useSetAtom } from "jotai"; import type { ComponentProps } from "react"; import { Button } from "./ui/Button"; -export const SignInButton = ({ - className, - ...props -}: ComponentProps) => { +export const SignInButton = (props: ComponentProps) => { const setSignInDialogOpen = useSetAtom(signInDialogOpenAtom); return (