Skip to content

Commit

Permalink
feat: join modal
Browse files Browse the repository at this point in the history
  • Loading branch information
BrickheadJohnny committed Dec 11, 2024
1 parent e01371f commit bbe0179
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 153 deletions.
139 changes: 0 additions & 139 deletions src/app/(dashboard)/[guildUrlName]/components/JoinButton.tsx

This file was deleted.

152 changes: 148 additions & 4 deletions src/app/(dashboard)/[guildUrlName]/components/JoinGuild.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,150 @@
"use client";

import { SignInButton } from "@/components/SignInButton";
import { Button } from "@/components/ui/Button";
import {
ResponsiveDialog,
ResponsiveDialogBody,
ResponsiveDialogContent,
ResponsiveDialogFooter,
ResponsiveDialogHeader,
ResponsiveDialogTitle,
ResponsiveDialogTrigger,
} from "@/components/ui/ResponsiveDialog";
import { IDENTITY_STYLES } from "@/config/constants";
import { cn } from "@/lib/cssUtils";
import { env } from "@/lib/env";
import { guildOptions, userOptions } from "@/lib/options";
import type { IdentityType } from "@/lib/schemas/user";
import type { Schemas } from "@guildxyz/types";
import { CheckCircle } from "@phosphor-icons/react/dist/ssr";
import { Check, CheckCircle, XCircle } from "@phosphor-icons/react/dist/ssr";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { EventSourcePlus } from "event-source-plus";
import { useParams } from "next/navigation";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { type ReactNode, useEffect, useState } from "react";
import { toast } from "sonner";

const JOIN_MODAL_SEARCH_PARAM = "join";

export const JoinGuild = () => {
const searchParams = useSearchParams();
const shouldOpen = searchParams.has(JOIN_MODAL_SEARCH_PARAM);

const [open, onOpenChange] = useState(false);

useEffect(() => {
if (!shouldOpen) return;
onOpenChange(true);
}, [shouldOpen]);

const { data: user } = useQuery(userOptions());

return (
<ResponsiveDialog open={open} onOpenChange={onOpenChange}>
<ResponsiveDialogTrigger asChild>
<Button colorScheme="success" className="rounded-2xl">
Join Guild
</Button>
</ResponsiveDialogTrigger>
<ResponsiveDialogContent>
<ResponsiveDialogHeader>
<ResponsiveDialogTitle>Join guild</ResponsiveDialogTitle>
</ResponsiveDialogHeader>

<ResponsiveDialogBody className="gap-2">
<JoinStep
complete={!!user}
label="Sign in"
button={<SignInButton />}
/>
{/* TODO: add `requiredIdentities` prop to the Guild entity & list only the necessary identity connect buttons here */}
<ConnectIdentityJoinStep identity="DISCORD" />
</ResponsiveDialogBody>

<ResponsiveDialogFooter>
<JoinGuildButton />
</ResponsiveDialogFooter>
</ResponsiveDialogContent>
</ResponsiveDialog>
);
};

const JoinStep = ({
complete,
label,
button,
}: { complete: boolean; label: string; button: ReactNode }) => (
<div className="grid grid-cols-[theme(space.5)_1fr] items-center gap-2">
<div
className={cn(
"flex size-5 items-center justify-center rounded-full border bg-blackAlpha text-white dark:bg-blackAlpha-hard",
{
"border-0 bg-icon-success dark:bg-icon-success": complete,
},
)}
>
{complete && <Check weight="bold" className="size-3" />}
</div>
<div className="flex items-center justify-between gap-2">
<span className="line-clamp-1 font-semibold">{label}</span>
<div className="shrink-0">{button}</div>
</div>
</div>
);

const getReturnToURLWithSearchParams = () => {
if (typeof window === "undefined") return "";

const url = new URL(window.location.href);
const searchParams = new URLSearchParams(url.searchParams);

if (searchParams.get(JOIN_MODAL_SEARCH_PARAM)) {
return url.toString();
}

searchParams.set(JOIN_MODAL_SEARCH_PARAM, "true");

return `${window.location.href.split("?")[0]}?${searchParams.toString()}`;
};

const ConnectIdentityJoinStep = ({ identity }: { identity: IdentityType }) => {
const router = useRouter();
const { data: user } = useQuery(userOptions());

const connected = !!user?.identities?.find((i) => i.platform === identity);

const Icon = IDENTITY_STYLES[identity].icon;

return (
<JoinStep
complete={connected}
label={`Connect ${identity}`}
button={
<Button
onClick={() =>
router.push(
`${env.NEXT_PUBLIC_API}/connect/${identity}?returnTo=${getReturnToURLWithSearchParams()}`,
)
}
leftIcon={
connected ? <Check weight="bold" /> : <Icon weight="fill" />
}
disabled={!user || connected} // TODO: once we allow users to log in with 3rd party accounts, we can remove the "!user" part of this
className={IDENTITY_STYLES[identity].buttonColorsClassName}
>
Connect
</Button>
}
/>
);
};

const JoinGuildButton = () => {
const { guildUrlName } = useParams<{ guildUrlName: string }>();
const guild = useQuery(guildOptions({ guildIdLike: guildUrlName }));

const { data: user } = useQuery(userOptions());

const queryClient = useQueryClient();

if (!guild.data) {
Expand All @@ -32,6 +164,7 @@ export const JoinGuild = () => {
headers: {
"content-type": "application/json",
},
credentials: "include",
});

const { resolve, reject, promise } =
Expand Down Expand Up @@ -65,24 +198,35 @@ export const JoinGuild = () => {
console.warn("JSON parsing failed on join event stream", e);
}
},
onResponseError: (ctx) => {
return reject(ctx.error);
},
});

return promise;
},
onSuccess: async (user) => {
queryClient.setQueryData(userOptions().queryKey, user);
},
onError: (error: Error) => {
toast("Join error", {
description: error.message,
icon: <XCircle weight="fill" className="text-icon-error" />,
});
},
});

return (
<Button
colorScheme="success"
className="rounded-2xl"
className="w-full rounded-2xl"
onClick={() => mutate()}
isLoading={isPending}
disabled={!user}
loadingText="Joining Guild"
size="xl"
>
Join Guild
{user ? "Join Guild" : "Sign in to join"}
</Button>
);
};
11 changes: 4 additions & 7 deletions src/app/(dashboard)/[guildUrlName]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { AuthBoundary } from "@/components/AuthBoundary";
import { GuildImage } from "@/components/GuildImage";
import { getQueryClient } from "@/lib/getQueryClient";
import {
Expand Down Expand Up @@ -73,12 +72,10 @@ const GuildLayout = async ({
{guild.data.name}
</h1>
</div>
{/* TODO: JoinButton should open a modal where the user can sign in and also connect the required platforms. So we won't need an AuthBoundary here. */}
<AuthBoundary fallback={null}>
<Suspense fallback={<ActionButtonSkeleton />}>
<ActionButton />
</Suspense>
</AuthBoundary>

<Suspense fallback={<ActionButtonSkeleton />}>
<ActionButton />
</Suspense>
</div>
<p className="line-clamp-3 max-w-prose text-balance text-lg leading-relaxed">
{guild.data.description}
Expand Down
8 changes: 6 additions & 2 deletions src/components/SignInButton.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
"use client";

import { signInDialogOpenAtom } from "@/config/atoms";
import { SignIn } from "@phosphor-icons/react/dist/ssr";
import { userOptions } from "@/lib/options";
import { Check, SignIn } from "@phosphor-icons/react/dist/ssr";
import { useQuery } from "@tanstack/react-query";
import { useSetAtom } from "jotai";
import type { ComponentProps } from "react";
import { Button } from "./ui/Button";

export const SignInButton = (props: ComponentProps<typeof Button>) => {
const setSignInDialogOpen = useSetAtom(signInDialogOpenAtom);
const { data: user } = useQuery(userOptions());

return (
<Button
{...props}
leftIcon={<SignIn weight="bold" />}
leftIcon={user ? <Check weight="bold" /> : <SignIn weight="bold" />}
disabled={!!user}
onClick={() => setSignInDialogOpen(true)}
>
Sign in
Expand Down
Loading

0 comments on commit bbe0179

Please sign in to comment.