Files
whyrating/apps/mobile/src/modules/organization/invitations/invitation.tsx
2026-02-04 01:55:00 +01:00

142 lines
4.3 KiB
TypeScript

"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { router } from "expo-router";
import { View } from "react-native";
import { Trans, useTranslation } from "@turbostarter/i18n";
import { Button } from "@turbostarter/ui-mobile/button";
import { Icons } from "@turbostarter/ui-mobile/icons";
import { Spin } from "@turbostarter/ui-mobile/spin";
import { Text } from "@turbostarter/ui-mobile/text";
import { pathsConfig } from "~/config/paths";
import { authClient } from "~/lib/auth";
import { AuthLayout } from "~/modules/auth/layout/base";
import { AuthHeader } from "~/modules/auth/layout/header";
import { Link } from "~/modules/common/styled";
import { organization } from "~/modules/organization/lib/api";
import { user } from "~/modules/user/lib/api";
import { InvitationSummaryCard } from "./invitation-summary-card";
import type { Invitation as InvitationType } from "@turbostarter/auth";
dayjs.extend(relativeTime);
interface InvitationProps {
readonly invitation: InvitationType & {
inviterEmail: string;
};
readonly organization: {
slug: string | null;
name: string;
logo: string | null;
};
}
export const Invitation = (props: InvitationProps) => {
const { t } = useTranslation(["common", "organization"]);
const activeOrganization = authClient.useActiveOrganization();
const activeMember = authClient.useActiveMember();
const queryClient = useQueryClient();
const setActive = useMutation({
...organization.mutations.setActive,
onSuccess: async () => {
await activeOrganization.refetch();
await activeMember.refetch();
},
});
const acceptInvitation = useMutation({
...organization.mutations.invitations.accept,
onSuccess: async () => {
await queryClient.invalidateQueries(user.queries.invitations.getAll);
await setActive.mutateAsync({
organizationId: props.invitation.organizationId,
});
router.replace(pathsConfig.index);
},
});
const rejectInvitation = useMutation({
...organization.mutations.invitations.reject,
onSuccess: async () => {
await queryClient.invalidateQueries(user.queries.invitations.getAll);
router.replace(pathsConfig.index);
},
});
return (
<AuthLayout>
<AuthHeader
title={t("invitations.invitation.title", {
organizationName: props.organization.name,
})}
description={
<Trans
i18nKey="invitations.invitation.description"
ns="organization"
values={{
inviterEmail: props.invitation.inviterEmail,
organizationName: props.organization.name,
}}
components={{ bold: <Text className="font-sans-medium text-sm" /> }}
/>
}
/>
<InvitationSummaryCard
invitation={props.invitation}
organization={props.organization}
/>
<View className="flex-row gap-2">
<Button
variant="outline"
className="grow"
disabled={rejectInvitation.isPending || acceptInvitation.isPending}
onPress={() =>
rejectInvitation.mutate({ invitationId: props.invitation.id })
}
>
{rejectInvitation.isPending ? (
<Spin>
<Icons.Loader2 className="text-foreground" size={16} />
</Spin>
) : (
<Icons.X className="text-foreground" size={16} />
)}
<Text>{t("reject")}</Text>
</Button>
<Button
className="grow"
onPress={() =>
acceptInvitation.mutate({ invitationId: props.invitation.id })
}
disabled={rejectInvitation.isPending || acceptInvitation.isPending}
>
{acceptInvitation.isPending ? (
<Spin>
<Icons.Loader2 className="text-primary-foreground" size={16} />
</Spin>
) : (
<Icons.Check className="text-primary-foreground" size={16} />
)}
<Text>{t("accept")}</Text>
</Button>
</View>
<Link
href={pathsConfig.index}
className="text-muted-foreground font-sans-medium self-center text-sm underline underline-offset-4"
>
{t("invitations.invitation.skip")}
</Link>
</AuthLayout>
);
};