diff --git a/apps/web/src/pages/invite/[code].tsx b/apps/web/src/pages/invite/[code].tsx new file mode 100644 index 00000000..74b45233 --- /dev/null +++ b/apps/web/src/pages/invite/[code].tsx @@ -0,0 +1,5 @@ +import InviteView from "~/views/invite"; + +export default function InvitePage() { + return ; +} diff --git a/apps/web/src/views/invite/index.tsx b/apps/web/src/views/invite/index.tsx new file mode 100644 index 00000000..a489586d --- /dev/null +++ b/apps/web/src/views/invite/index.tsx @@ -0,0 +1,148 @@ +import { useRouter } from "next/router"; +import { t } from "@lingui/core/macro"; +import { useEffect, useState } from "react"; + +import { authClient } from "@kan/auth/client"; + +import Button from "~/components/Button"; +import LoadingSpinner from "~/components/LoadingSpinner"; +import { PageHead } from "~/components/PageHead"; +import PatternedBackground from "~/components/PatternedBackground"; +import { api } from "~/utils/api"; + +export default function InvitePage() { + const router = useRouter(); + const { code } = router.query; + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(null); + const [invitationInfo, setInvitationInfo] = useState(null); + const { data: session, isPending: isSessionLoading } = + authClient.useSession(); + const { switchWorkspace } = useWorkspace(); + + const acceptInviteMutation = api.member.acceptInviteLink.useMutation({ + onSuccess: (result) => { + if (result.success) { + router.push(`/boards`); + } + }, + onError: (error) => { + setError(error.message || t`Failed to accept invitation`); + setIsProcessing(false); + }, + }); + + const { + data: inviteInfo, + isLoading: isInviteInfoLoading, + isError: isInviteInfoError, + error: inviteInfoError, + } = api.member.getInviteByCode.useQuery({ inviteCode: code ?? "" }); + + // Set invite info when loaded + useEffect(() => { + if (inviteInfo) { + setInvitationInfo(inviteInfo); + } else if (isInviteInfoError) { + setError(t`Invalid or expired invitation link`); + } + }, [inviteInfo, isInviteInfoError]); + + // Auto accept invite if user is logged in + useEffect(() => { + if (session?.user.id && code && typeof code === "string" && inviteInfo) { + handleAcceptInvite(); + } + }, [session, code, inviteInfo]); + + const handleAcceptInvite = async () => { + if (!code || typeof code !== "string" || !session?.user.id) { + return; + } + + setIsProcessing(true); + setError(null); + + acceptInviteMutation.mutate({ + inviteCode: code, + userId: session.user.id, + }); + }; + + if (session?.user.id || isInviteInfoLoading || isSessionLoading) { + return ( + <> + + +
+ +
+ + ); + } + + if (isInviteInfoError || !inviteInfo) { + return ( + <> + +
+ +
+
+

+ {t`Invalid invitation`} +

+

+ {t`This invitation link is invalid or has expired.`} +

+
+
+ +
+
+
+ + ); + } + + return ( + <> + + +
+ +
+
+

+ {t`Join workspace`} +

+

+ {t`You've been invited to join ${invitationInfo?.workspaceName || "a workspace"} on kan.bn`} +

+
+ +
+ + +
+
+
+ + ); +}