NODEDC_TASKMANAGER/plane-src/apps/web/app/(all)/workspace-invitations/page.tsx

186 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { observer } from "mobx-react";
import type { ReactNode } from "react";
import { useSearchParams } from "next/navigation";
import useSWR from "swr";
import { ArrowRight, Check, MailCheck, X } from "lucide-react";
import { Button } from "@plane/propel/button";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { NodeDCStandaloneShell } from "@/components/nodedc/standalone-shell";
// constants
import { WORKSPACE_INVITATION } from "@/constants/fetch-keys";
// helpers
import { EPageTypes } from "@/helpers/authentication.helper";
// hooks
import { useUser } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
// wrappers
import { AuthenticationWrapper } from "@/lib/wrappers/authentication-wrapper";
import { WorkspaceService } from "@/services/workspace.service";
// services
// service initialization
const workspaceService = new WorkspaceService();
function WorkspaceInvitationPage() {
// router
const router = useAppRouter();
// query params
const searchParams = useSearchParams();
const invitation_id = searchParams.get("invitation_id");
const slug = searchParams.get("slug");
const token = searchParams.get("token");
// store hooks
const { data: currentUser } = useUser();
const { data: invitationDetail, error } = useSWR(
invitation_id && slug && WORKSPACE_INVITATION(invitation_id.toString()),
invitation_id && slug
? () => workspaceService.getWorkspaceInvitation(slug.toString(), invitation_id.toString())
: null
);
const handleAccept = async () => {
if (!invitationDetail) return;
try {
await workspaceService.joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, {
accepted: true,
token: token,
});
router.push(invitationDetail.email === currentUser?.email ? `/${invitationDetail.workspace.slug}` : "/");
} catch (err: unknown) {
console.error(err);
}
};
const handleReject = async () => {
if (!invitationDetail || !token) return;
try {
await workspaceService.joinWorkspace(invitationDetail.workspace.slug, invitationDetail.id, {
accepted: false,
token: token,
});
router.push("/");
} catch (err: unknown) {
console.error(err);
}
};
return (
<AuthenticationWrapper pageType={EPageTypes.PUBLIC}>
<NodeDCStandaloneShell showUserControls={!!currentUser}>
{invitationDetail && !invitationDetail.responded_at ? (
error ? (
<InvitationShell
title="Приглашение не найдено"
description="Ссылка устарела или была отозвана администратором workspace."
action={<HomeButton routerPush={() => router.push("/")} />}
/>
) : (
<InvitationShell
eyebrow="Workspace invite"
title={`Вас пригласили в ${invitationDetail.workspace.name}`}
description="Примите приглашение, чтобы получить доступ к workspace и связанным проектам Tasker."
action={
<div className="flex flex-wrap justify-center gap-3">
<Button
variant="primary"
size="lg"
onClick={handleAccept}
className="nodedc-empty-state-primary"
prependIcon={<Check className="size-4" />}
>
Принять
</Button>
<Button
variant="secondary"
size="lg"
onClick={handleReject}
className="nodedc-empty-state-secondary"
prependIcon={<X className="size-4" />}
>
Отклонить
</Button>
</div>
}
/>
)
) : error || invitationDetail?.responded_at ? (
invitationDetail?.accepted ? (
<InvitationShell
title={`Вы уже участник ${invitationDetail.workspace.name}`}
description="Приглашение принято. Можно вернуться в Tasker и продолжить работу."
action={<HomeButton routerPush={() => router.push("/")} />}
/>
) : (
<InvitationShell
title="Ссылка приглашения больше не активна"
description={
currentUser
? "Вернитесь на главную страницу Tasker или запросите новое приглашение."
: "Войдите через NODE.DC и запросите новое приглашение, если доступ всё ещё нужен."
}
action={<HomeButton routerPush={() => router.push("/")} />}
/>
)
) : (
<div className="relative z-[1] flex h-full w-full items-center justify-center">
<LogoSpinner />
</div>
)}
</NodeDCStandaloneShell>
</AuthenticationWrapper>
);
}
export default observer(WorkspaceInvitationPage);
function InvitationShell({
action,
description,
eyebrow = "NODE.DC Tasker",
title,
}: {
action: ReactNode;
description: string;
eyebrow?: string;
title: string;
}) {
return (
<div className="nodedc-glass-surface relative z-[1] w-full max-w-[36rem] overflow-hidden rounded-[2.2rem] px-8 py-9 text-center">
<div className="pointer-events-none absolute inset-x-8 top-0 h-px bg-gradient-to-r from-transparent via-[rgb(var(--nodedc-accent-rgb))]/55 to-transparent" />
<div className="mx-auto flex size-24 items-center justify-center rounded-[2rem] bg-white/[0.035] text-[rgb(var(--nodedc-accent-rgb))]">
<MailCheck className="size-11" />
</div>
<div className="mt-6 space-y-2">
<div className="text-11 font-semibold tracking-[0.18em] text-[rgb(var(--nodedc-accent-rgb))] uppercase">
{eyebrow}
</div>
<h1 className="text-24 font-semibold tracking-[-0.03em]">{title}</h1>
<p className="mx-auto max-w-sm text-13 leading-6 text-secondary">{description}</p>
</div>
<div className="mt-7">{action}</div>
</div>
);
}
function HomeButton({ routerPush }: { routerPush: () => void }) {
return (
<Button
variant="primary"
size="lg"
onClick={routerPush}
className="nodedc-empty-state-primary"
appendIcon={<ArrowRight className="size-4" />}
>
Вернуться на главную
</Button>
);
}