feat: switch to workspace on invite success
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
@@ -46,6 +46,8 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
);
|
);
|
||||||
const [hasLoaded, setHasLoaded] = useState(false);
|
const [hasLoaded, setHasLoaded] = useState(false);
|
||||||
|
|
||||||
|
const workspacePublicId = useSearchParams().get("workspacePublicId");
|
||||||
|
|
||||||
const { data, isLoading } = api.workspace.all.useQuery();
|
const { data, isLoading } = api.workspace.all.useQuery();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
@@ -67,7 +69,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const storedWorkspaceId: string | null =
|
const storedWorkspaceId: string | null =
|
||||||
localStorage.getItem("workspacePublicId");
|
workspacePublicId ?? localStorage.getItem("workspacePublicId");
|
||||||
|
|
||||||
if (data.length) {
|
if (data.length) {
|
||||||
const workspaces = data.map(({ workspace, role }) => ({
|
const workspaces = data.map(({ workspace, role }) => ({
|
||||||
@@ -99,6 +101,11 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
description: selectedWorkspace.workspace.description,
|
description: selectedWorkspace.workspace.description,
|
||||||
role: selectedWorkspace.role,
|
role: selectedWorkspace.role,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (workspacePublicId) {
|
||||||
|
router.push(`/boards`);
|
||||||
|
localStorage.setItem("workspacePublicId", workspacePublicId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const primaryWorkspace = data[0]?.workspace;
|
const primaryWorkspace = data[0]?.workspace;
|
||||||
const primaryWorkspaceRole = data[0]?.role;
|
const primaryWorkspaceRole = data[0]?.role;
|
||||||
@@ -114,7 +121,7 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
role: primaryWorkspaceRole,
|
role: primaryWorkspaceRole,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, isLoading]);
|
}, [data, isLoading, workspacePublicId, router]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkspaceContext.Provider
|
<WorkspaceContext.Provider
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
|
import { env } from "next-runtime-env";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { authClient } from "@kan/auth/client";
|
import { authClient } from "@kan/auth/client";
|
||||||
@@ -15,19 +16,30 @@ export default function InvitePage() {
|
|||||||
const { code } = router.query;
|
const { code } = router.query;
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [invitationInfo, setInvitationInfo] = useState<any>(null);
|
|
||||||
const { data: session, isPending: isSessionLoading } =
|
const { data: session, isPending: isSessionLoading } =
|
||||||
authClient.useSession();
|
authClient.useSession();
|
||||||
const { switchWorkspace } = useWorkspace();
|
|
||||||
|
const isCloudEnv = env("NEXT_PUBLIC_KAN_ENV") === "cloud";
|
||||||
|
|
||||||
|
const inviteCode = Array.isArray(code) ? code[0] : code;
|
||||||
|
|
||||||
const acceptInviteMutation = api.member.acceptInviteLink.useMutation({
|
const acceptInviteMutation = api.member.acceptInviteLink.useMutation({
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
router.push(`/boards`);
|
return router.push(
|
||||||
|
`/boards?workspacePublicId=${result.workspacePublicId}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setError(error.message || t`Failed to accept invitation`);
|
if (error.data?.code === "CONFLICT") {
|
||||||
|
return router.push(`/boards`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(
|
||||||
|
error.message ||
|
||||||
|
t`Failed to accept invitation. Please try again later, or contact customer support.`,
|
||||||
|
);
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -36,40 +48,32 @@ export default function InvitePage() {
|
|||||||
data: inviteInfo,
|
data: inviteInfo,
|
||||||
isLoading: isInviteInfoLoading,
|
isLoading: isInviteInfoLoading,
|
||||||
isError: isInviteInfoError,
|
isError: isInviteInfoError,
|
||||||
error: inviteInfoError,
|
} = api.member.getInviteByCode.useQuery(
|
||||||
} = api.member.getInviteByCode.useQuery({ inviteCode: code ?? "" });
|
{ inviteCode: inviteCode ?? "" },
|
||||||
|
{
|
||||||
// Set invite info when loaded
|
enabled: !!inviteCode,
|
||||||
useEffect(() => {
|
retry: false,
|
||||||
if (inviteInfo) {
|
},
|
||||||
setInvitationInfo(inviteInfo);
|
);
|
||||||
} else if (isInviteInfoError) {
|
|
||||||
setError(t`Invalid or expired invitation link`);
|
|
||||||
}
|
|
||||||
}, [inviteInfo, isInviteInfoError]);
|
|
||||||
|
|
||||||
// Auto accept invite if user is logged in
|
// Auto accept invite if user is logged in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session?.user.id && code && typeof code === "string" && inviteInfo) {
|
if (session?.user.id && inviteCode && inviteInfo && !error) {
|
||||||
handleAcceptInvite();
|
setIsProcessing(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
acceptInviteMutation.mutate({
|
||||||
|
inviteCode,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [session, code, inviteInfo]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [session?.user.id, inviteCode, inviteInfo, error]);
|
||||||
|
|
||||||
const handleAcceptInvite = async () => {
|
if (
|
||||||
if (!code || typeof code !== "string" || !session?.user.id) {
|
!isInviteInfoError &&
|
||||||
return;
|
!error &&
|
||||||
}
|
(session?.user.id || isInviteInfoLoading || isSessionLoading)
|
||||||
|
) {
|
||||||
setIsProcessing(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
acceptInviteMutation.mutate({
|
|
||||||
inviteCode: code,
|
|
||||||
userId: session.user.id,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (session?.user.id || isInviteInfoLoading || isSessionLoading) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={t`Join workspace`} />
|
<PageHead title={t`Join workspace`} />
|
||||||
@@ -81,13 +85,21 @@ export default function InvitePage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInviteInfoError || !inviteInfo) {
|
const PageWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={t`Join workspace`} />
|
<PageHead title={t`Join workspace | kan.bn`} />
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isInviteInfoError || (!isInviteInfoLoading && !inviteInfo)) {
|
||||||
|
return (
|
||||||
|
<PageWrapper>
|
||||||
<div className="relative flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
|
<div className="relative flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
|
||||||
<PatternedBackground />
|
<PatternedBackground />
|
||||||
<div className="w-full max-w-md space-y-8">
|
<div className="z-10 w-full max-w-md space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
||||||
{t`Invalid invitation`}
|
{t`Invalid invitation`}
|
||||||
@@ -103,46 +115,57 @@ export default function InvitePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper>
|
||||||
<PageHead title={t`Join workspace`} />
|
|
||||||
|
|
||||||
<div className="relative flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
|
<div className="relative flex min-h-screen items-center justify-center px-4 py-12 sm:px-6 lg:px-8">
|
||||||
<PatternedBackground />
|
<PatternedBackground />
|
||||||
<div className="z-10 w-full max-w-md space-y-8">
|
<div className="z-10 w-full max-w-[400px] space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
|
||||||
{t`Join workspace`}
|
{t`Join workspace`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-center text-sm text-light-600 dark:text-dark-800">
|
{!error ? (
|
||||||
{t`You've been invited to join ${invitationInfo?.workspaceName || "a workspace"} on kan.bn`}
|
<p className="mt-4 text-center text-sm text-light-600 dark:text-dark-800">
|
||||||
</p>
|
{isCloudEnv
|
||||||
|
? t`You've been invited to join a workspace on kan.bn.`
|
||||||
|
: t`You've been invited to join a workspace.`}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="mt-4 text-center text-sm text-red-500">{error}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-center gap-2">
|
<div className="flex justify-center gap-2">
|
||||||
<Button
|
{session?.user.id ? (
|
||||||
href={`/login?next=/invite/${code}`}
|
<Button href={`/boards`} variant="primary" size="md">
|
||||||
disabled={isProcessing}
|
{t`Go to app`}
|
||||||
variant="primary"
|
</Button>
|
||||||
size="md"
|
) : (
|
||||||
>
|
<>
|
||||||
{t`Sign In`}
|
<Button
|
||||||
</Button>
|
href={`/login?next=/invite/${inviteCode}`}
|
||||||
<Button
|
disabled={isProcessing}
|
||||||
href={`/signup?next=/invite/${code}`}
|
variant="primary"
|
||||||
disabled={isProcessing}
|
size="md"
|
||||||
variant="primary"
|
>
|
||||||
size="md"
|
{t`Sign In`}
|
||||||
>
|
</Button>
|
||||||
{t`Sign Up`}
|
<Button
|
||||||
</Button>
|
href={`/signup?next=/invite/${inviteCode}`}
|
||||||
|
disabled={isProcessing}
|
||||||
|
variant="primary"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
{t`Sign Up`}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user