mirror of
https://github.com/documenso/documenso.git
synced 2025-11-26 22:44:41 +10:00
feat: web i18n (#1286)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
|
||||
@@ -11,6 +12,8 @@ export type DocumentPageProps = {
|
||||
};
|
||||
|
||||
export default async function TeamsDocumentEditPage({ params }: DocumentPageProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { teamUrl } = params;
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
|
||||
@@ -11,6 +12,8 @@ export type TeamDocumentsLogsPageProps = {
|
||||
};
|
||||
|
||||
export default async function TeamsDocumentsLogsPage({ params }: TeamDocumentsLogsPageProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { teamUrl } = params;
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
|
||||
@@ -11,6 +12,8 @@ export type DocumentPageProps = {
|
||||
};
|
||||
|
||||
export default async function DocumentPage({ params }: DocumentPageProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { teamUrl } = params;
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
|
||||
@@ -15,6 +16,8 @@ export default async function TeamsDocumentPage({
|
||||
params,
|
||||
searchParams = {},
|
||||
}: TeamsDocumentPageProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { teamUrl } = params;
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
|
||||
import { AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
@@ -13,24 +16,28 @@ type ErrorProps = {
|
||||
};
|
||||
|
||||
export default function ErrorPage({ error }: ErrorProps) {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
let errorMessage = 'Unknown error';
|
||||
let errorDetails = '';
|
||||
let errorMessage = msg`Unknown error`;
|
||||
let errorDetails: MessageDescriptor | null = null;
|
||||
|
||||
if (error.message === AppErrorCode.UNAUTHORIZED) {
|
||||
errorMessage = 'Unauthorized';
|
||||
errorDetails = 'You are not authorized to view this page.';
|
||||
errorMessage = msg`Unauthorized`;
|
||||
errorDetails = msg`You are not authorized to view this page.`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">
|
||||
<div>
|
||||
<p className="text-muted-foreground font-semibold">{errorMessage}</p>
|
||||
<p className="text-muted-foreground font-semibold">{_(errorMessage)}</p>
|
||||
|
||||
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
|
||||
<h1 className="mt-3 text-2xl font-bold md:text-3xl">
|
||||
<Trans>Oops! Something went wrong.</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-sm">{errorDetails}</p>
|
||||
<p className="text-muted-foreground mt-4 text-sm">{errorDetails ? _(errorDetails) : ''}</p>
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button
|
||||
@@ -41,11 +48,13 @@ export default function ErrorPage({ error }: ErrorProps) {
|
||||
}}
|
||||
>
|
||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||
Go Back
|
||||
<Trans>Go Back</Trans>
|
||||
</Button>
|
||||
|
||||
<Button asChild>
|
||||
<Link href="/settings/teams">View teams</Link>
|
||||
<Link href="/settings/teams">
|
||||
<Trans>View teams</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
@@ -31,6 +33,7 @@ export const LayoutBillingBanner = ({
|
||||
teamId,
|
||||
userRole,
|
||||
}: LayoutBillingBannerProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -47,9 +50,10 @@ export const LayoutBillingBanner = ({
|
||||
setIsOpen(false);
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
description:
|
||||
'We are unable to proceed to the billing portal at this time. Please try again, or contact support.',
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(
|
||||
msg`We are unable to proceed to the billing portal at this time. Please try again, or contact support.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
});
|
||||
@@ -75,8 +79,8 @@ export const LayoutBillingBanner = ({
|
||||
<AlertTriangle className="mr-2.5 h-5 w-5" />
|
||||
|
||||
{match(subscription.status)
|
||||
.with(SubscriptionStatus.PAST_DUE, () => 'Payment overdue')
|
||||
.with(SubscriptionStatus.INACTIVE, () => 'Teams restricted')
|
||||
.with(SubscriptionStatus.PAST_DUE, () => <Trans>Payment overdue</Trans>)
|
||||
.with(SubscriptionStatus.INACTIVE, () => <Trans>Teams restricted</Trans>)
|
||||
.exhaustive()}
|
||||
</div>
|
||||
|
||||
@@ -92,26 +96,32 @@ export const LayoutBillingBanner = ({
|
||||
onClick={() => setIsOpen(true)}
|
||||
size="sm"
|
||||
>
|
||||
Resolve
|
||||
<Trans>Resolve</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={(value) => !isLoading && setIsOpen(value)}>
|
||||
<DialogContent>
|
||||
<DialogTitle>Payment overdue</DialogTitle>
|
||||
<DialogTitle>
|
||||
<Trans>Payment overdue</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
{match(subscription.status)
|
||||
.with(SubscriptionStatus.PAST_DUE, () => (
|
||||
<DialogDescription>
|
||||
Your payment for teams is overdue. Please settle the payment to avoid any service
|
||||
disruptions.
|
||||
<Trans>
|
||||
Your payment for teams is overdue. Please settle the payment to avoid any service
|
||||
disruptions.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
))
|
||||
.with(SubscriptionStatus.INACTIVE, () => (
|
||||
<DialogDescription>
|
||||
Due to an unpaid invoice, your team has been restricted. Please settle the payment
|
||||
to restore full access to your team.
|
||||
<Trans>
|
||||
Due to an unpaid invoice, your team has been restricted. Please settle the payment
|
||||
to restore full access to your team.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
))
|
||||
.otherwise(() => null)}
|
||||
@@ -119,7 +129,7 @@ export const LayoutBillingBanner = ({
|
||||
{canExecuteTeamAction('MANAGE_BILLING', userRole) && (
|
||||
<DialogFooter>
|
||||
<Button loading={isLoading} onClick={handleCreatePortal}>
|
||||
Resolve payment
|
||||
<Trans>Resolve payment</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import { RedirectType, redirect } from 'next/navigation';
|
||||
|
||||
import { LimitsProvider } from '@documenso/ee/server-only/limits/provider/server';
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { getTeams } from '@documenso/lib/server-only/team/get-teams';
|
||||
@@ -26,6 +27,8 @@ export default async function AuthenticatedTeamsLayout({
|
||||
children,
|
||||
params,
|
||||
}: AuthenticatedTeamsLayoutProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { session, user } = await getServerComponentSession();
|
||||
|
||||
if (!session || !user) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { ChevronLeft } from 'lucide-react';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -10,19 +11,25 @@ export default function NotFound() {
|
||||
return (
|
||||
<div className="mx-auto flex min-h-[80vh] w-full items-center justify-center py-32">
|
||||
<div>
|
||||
<p className="text-muted-foreground font-semibold">404 Team not found</p>
|
||||
<p className="text-muted-foreground font-semibold">
|
||||
<Trans>404 Team not found</Trans>
|
||||
</p>
|
||||
|
||||
<h1 className="mt-3 text-2xl font-bold md:text-3xl">Oops! Something went wrong.</h1>
|
||||
<h1 className="mt-3 text-2xl font-bold md:text-3xl">
|
||||
<Trans>Oops! Something went wrong.</Trans>
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground mt-4 text-sm">
|
||||
The team you are looking for may have been removed, renamed or may have never existed.
|
||||
<Trans>
|
||||
The team you are looking for may have been removed, renamed or may have never existed.
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex gap-x-2.5 gap-y-4 md:items-center">
|
||||
<Button asChild className="w-32">
|
||||
<Link href="/settings/teams">
|
||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||
Go Back
|
||||
<Trans>Go Back</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Plural, Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import type Stripe from 'stripe';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { stripe } from '@documenso/lib/server-only/stripe';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
@@ -18,6 +21,10 @@ export type TeamsSettingsBillingPageProps = {
|
||||
};
|
||||
|
||||
export default async function TeamsSettingBillingPage({ params }: TeamsSettingsBillingPageProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { _ } = useLingui();
|
||||
|
||||
const session = await getRequiredServerComponentSession();
|
||||
|
||||
const team = await getTeamByUrl({ userId: session.user.id, teamUrl: params.teamUrl });
|
||||
@@ -32,29 +39,34 @@ export default async function TeamsSettingBillingPage({ params }: TeamsSettingsB
|
||||
|
||||
const formatTeamSubscriptionDetails = (subscription: Stripe.Subscription | null) => {
|
||||
if (!subscription) {
|
||||
return 'No payment required';
|
||||
return <Trans>No payment required</Trans>;
|
||||
}
|
||||
|
||||
const numberOfSeats = subscription.items.data[0].quantity ?? 0;
|
||||
|
||||
const formattedTeamMemberQuanity = numberOfSeats > 1 ? `${numberOfSeats} members` : '1 member';
|
||||
const formattedTeamMemberQuanity = (
|
||||
<Plural value={numberOfSeats} one="# member" other="# members" />
|
||||
);
|
||||
|
||||
const formattedDate = DateTime.fromSeconds(subscription.current_period_end).toFormat(
|
||||
'LLL dd, yyyy',
|
||||
);
|
||||
|
||||
return `${formattedTeamMemberQuanity} • Monthly • Renews: ${formattedDate}`;
|
||||
return _(msg`${formattedTeamMemberQuanity} • Monthly • Renews: ${formattedDate}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader title="Billing" subtitle="Your subscription is currently active." />
|
||||
<SettingsHeader
|
||||
title={_(msg`Billing`)}
|
||||
subtitle={_(msg`Your subscription is currently active.`)}
|
||||
/>
|
||||
|
||||
<Card gradient className="shadow-sm">
|
||||
<CardContent className="flex flex-row items-center justify-between p-4">
|
||||
<div className="flex flex-col text-sm">
|
||||
<p className="text-foreground font-semibold">
|
||||
Current plan: {teamSubscription ? 'Team' : 'Early Adopter Team'}
|
||||
<Trans>Current plan: {teamSubscription ? 'Team' : 'Early Adopter Team'}</Trans>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-0.5">
|
||||
@@ -66,8 +78,8 @@ export default async function TeamsSettingBillingPage({ params }: TeamsSettingsB
|
||||
<div
|
||||
title={
|
||||
canManageBilling
|
||||
? 'Manage team subscription.'
|
||||
: 'You must be an admin of this team to manage billing.'
|
||||
? _(msg`Manage team subscription.`)
|
||||
: _(msg`You must be an admin of this team to manage billing.`)
|
||||
}
|
||||
>
|
||||
<TeamBillingPortalButton teamId={team.id} />
|
||||
|
||||
@@ -2,6 +2,9 @@ import React from 'react';
|
||||
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { Trans } from '@lingui/macro';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
@@ -21,6 +24,8 @@ export default async function TeamsSettingsLayout({
|
||||
children,
|
||||
params: { teamUrl },
|
||||
}: TeamSettingsLayoutProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const session = await getRequiredServerComponentSession();
|
||||
|
||||
try {
|
||||
@@ -41,7 +46,9 @@ export default async function TeamsSettingsLayout({
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-screen-xl px-4 md:px-8">
|
||||
<h1 className="text-4xl font-semibold">Team Settings</h1>
|
||||
<h1 className="text-4xl font-semibold">
|
||||
<Trans>Team Settings</Trans>
|
||||
</h1>
|
||||
|
||||
<div className="mt-4 grid grid-cols-12 gap-x-8 md:mt-8">
|
||||
<DesktopNav className="hidden md:col-span-3 md:flex" />
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
|
||||
@@ -12,6 +16,9 @@ export type TeamsSettingsMembersPageProps = {
|
||||
};
|
||||
|
||||
export default async function TeamsSettingsMembersPage({ params }: TeamsSettingsMembersPageProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { teamUrl } = params;
|
||||
|
||||
const session = await getRequiredServerComponentSession();
|
||||
@@ -20,7 +27,10 @@ export default async function TeamsSettingsMembersPage({ params }: TeamsSettings
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader title="Members" subtitle="Manage the members or invite new members.">
|
||||
<SettingsHeader
|
||||
title={_(msg`Members`)}
|
||||
subtitle={_(msg`Manage the members or invite new members.`)}
|
||||
>
|
||||
<InviteTeamMembersDialog
|
||||
teamId={team.id}
|
||||
currentUserTeamRole={team.currentTeamMember.role}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { CheckCircle2, Clock } from 'lucide-react';
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
@@ -26,6 +28,8 @@ export type TeamsSettingsPageProps = {
|
||||
};
|
||||
|
||||
export default async function TeamsSettingsPage({ params }: TeamsSettingsPageProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { teamUrl } = params;
|
||||
|
||||
const session = await getRequiredServerComponentSession();
|
||||
@@ -53,11 +57,15 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
|
||||
<section className="mt-6 space-y-6">
|
||||
{(team.teamEmail || team.emailVerification) && (
|
||||
<Alert className="p-6" variant="neutral">
|
||||
<AlertTitle>Team email</AlertTitle>
|
||||
<AlertTitle>
|
||||
<Trans>Team email</Trans>
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription className="mr-2">
|
||||
You can view documents associated with this email and use this identity when sending
|
||||
documents.
|
||||
<Trans>
|
||||
You can view documents associated with this email and use this identity when sending
|
||||
documents.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
|
||||
<hr className="border-border/50 mt-2" />
|
||||
@@ -90,7 +98,7 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
|
||||
.with({ teamEmail: P.not(null) }, () => (
|
||||
<>
|
||||
<CheckCircle2 className="mr-1.5 text-green-500 dark:text-green-300" />
|
||||
Active
|
||||
<Trans>Active</Trans>
|
||||
</>
|
||||
))
|
||||
.with(
|
||||
@@ -103,14 +111,14 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
|
||||
() => (
|
||||
<>
|
||||
<Clock className="mr-1.5 text-yellow-500 dark:text-yellow-200" />
|
||||
Expired
|
||||
<Trans>Expired</Trans>
|
||||
</>
|
||||
),
|
||||
)
|
||||
.with({ emailVerification: P.not(null) }, () => (
|
||||
<>
|
||||
<Clock className="mr-1.5 text-blue-600 dark:text-blue-300" />
|
||||
Awaiting email confirmation
|
||||
<Trans>Awaiting email confirmation</Trans>
|
||||
</>
|
||||
))
|
||||
.otherwise(() => null)}
|
||||
@@ -128,7 +136,9 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
|
||||
variant="neutral"
|
||||
>
|
||||
<div className="mb-4 sm:mb-0">
|
||||
<AlertTitle>Team email</AlertTitle>
|
||||
<AlertTitle>
|
||||
<Trans>Team email</Trans>
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription className="mr-2">
|
||||
<ul className="text-muted-foreground mt-0.5 list-inside list-disc text-sm">
|
||||
@@ -136,7 +146,9 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
|
||||
{/* <li>Display this name and email when sending documents</li> */}
|
||||
{/* <li>View documents associated with this email</li> */}
|
||||
|
||||
<span>View documents associated with this email</span>
|
||||
<span>
|
||||
<Trans>View documents associated with this email</Trans>
|
||||
</span>
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
@@ -153,10 +165,12 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
|
||||
variant="neutral"
|
||||
>
|
||||
<div className="mb-4 sm:mb-0">
|
||||
<AlertTitle>Transfer team</AlertTitle>
|
||||
<AlertTitle>
|
||||
<Trans>Transfer team</Trans>
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription className="mr-2">
|
||||
Transfer the ownership of the team to another team member.
|
||||
<Trans>Transfer the ownership of the team to another team member.</Trans>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
|
||||
@@ -173,11 +187,15 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
|
||||
variant="neutral"
|
||||
>
|
||||
<div className="mb-4 sm:mb-0">
|
||||
<AlertTitle>Delete team</AlertTitle>
|
||||
<AlertTitle>
|
||||
<Trans>Delete team</Trans>
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription className="mr-2">
|
||||
This team, and any associated data excluding billing invoices will be permanently
|
||||
deleted.
|
||||
<Trans>
|
||||
This team, and any associated data excluding billing invoices will be
|
||||
permanently deleted.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
import { getTeamPublicProfile } from '@documenso/lib/server-only/team/get-team-public-profile';
|
||||
@@ -13,6 +14,8 @@ export type TeamsSettingsPublicProfilePageProps = {
|
||||
export default async function TeamsSettingsPublicProfilePage({
|
||||
params,
|
||||
}: TeamsSettingsPublicProfilePageProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { teamUrl } = params;
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Edit, Loader, Mail, MoreHorizontal, X } from 'lucide-react';
|
||||
|
||||
import type { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
@@ -20,23 +22,24 @@ export type TeamsSettingsPageProps = {
|
||||
};
|
||||
|
||||
export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: resendEmailVerification, isLoading: isResendingEmailVerification } =
|
||||
trpc.team.resendTeamEmailVerification.useMutation({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Email verification has been resent',
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Email verification has been resent`),
|
||||
duration: 5000,
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'Something went wrong',
|
||||
title: _(msg`Something went wrong`),
|
||||
description: _(msg`Unable to resend verification at this time. Please try again.`),
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
description: 'Unable to resend verification at this time. Please try again.',
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -61,7 +64,7 @@ export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
|
||||
) : (
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Resend verification
|
||||
<Trans>Resend verification</Trans>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
@@ -71,7 +74,7 @@ export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
|
||||
trigger={
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
<Trans>Edit</Trans>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
@@ -83,7 +86,7 @@ export const TeamEmailDropdown = ({ team }: TeamsSettingsPageProps) => {
|
||||
trigger={
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Remove
|
||||
<Trans>Remove</Trans>
|
||||
</DropdownMenuItem>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
|
||||
import { canExecuteTeamAction } from '@documenso/lib/utils/teams';
|
||||
@@ -29,6 +31,7 @@ export const TeamTransferStatus = ({
|
||||
}: TeamTransferStatusProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const isExpired = transferVerification && isTokenExpired(transferVerification.expiresAt);
|
||||
@@ -38,8 +41,8 @@ export const TeamTransferStatus = ({
|
||||
onSuccess: () => {
|
||||
if (!isExpired) {
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'The team transfer invitation has been successfully deleted.',
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`The team transfer invitation has been successfully deleted.`),
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
@@ -48,10 +51,11 @@ export const TeamTransferStatus = ({
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
title: 'An unknown error occurred',
|
||||
title: _(msg`An unknown error occurred`),
|
||||
description: _(
|
||||
msg`We encountered an unknown error while attempting to remove this transfer. Please try again or contact support.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
description:
|
||||
'We encountered an unknown error while attempting to remove this transfer. Please try again or contact support.',
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -69,26 +73,36 @@ export const TeamTransferStatus = ({
|
||||
>
|
||||
<div>
|
||||
<AlertTitle>
|
||||
{isExpired ? 'Team transfer request expired' : 'Team transfer in progress'}
|
||||
{isExpired ? (
|
||||
<Trans>Team transfer request expired</Trans>
|
||||
) : (
|
||||
<Trans>Team transfer in progress</Trans>
|
||||
)}
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
{isExpired ? (
|
||||
<p className="text-sm">
|
||||
The team transfer request to <strong>{transferVerification.name}</strong> has
|
||||
expired.
|
||||
<Trans>
|
||||
The team transfer request to <strong>{transferVerification.name}</strong> has
|
||||
expired.
|
||||
</Trans>
|
||||
</p>
|
||||
) : (
|
||||
<section className="text-sm">
|
||||
<p>
|
||||
A request to transfer the ownership of this team has been sent to{' '}
|
||||
<strong>
|
||||
{transferVerification.name} ({transferVerification.email})
|
||||
</strong>
|
||||
<Trans>
|
||||
A request to transfer the ownership of this team has been sent to{' '}
|
||||
<strong>
|
||||
{transferVerification.name} ({transferVerification.email})
|
||||
</strong>
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If they accept this request, the team will be transferred to their account.
|
||||
<Trans>
|
||||
If they accept this request, the team will be transferred to their account.
|
||||
</Trans>
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
@@ -104,7 +118,7 @@ export const TeamTransferStatus = ({
|
||||
'hover:bg-transparent hover:text-blue-800': !isExpired,
|
||||
})}
|
||||
>
|
||||
{isExpired ? 'Close' : 'Cancel'}
|
||||
{isExpired ? <Trans>Close</Trans> : <Trans>Cancel</Trans>}
|
||||
</Button>
|
||||
)}
|
||||
</Alert>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { DateTime } from 'luxon';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
@@ -20,6 +22,8 @@ type ApiTokensPageProps = {
|
||||
};
|
||||
|
||||
export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { teamUrl } = params;
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
@@ -35,7 +39,9 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">API Tokens</h3>
|
||||
<h3 className="text-2xl font-semibold">
|
||||
<Trans>API Tokens</Trans>
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
{match(error.code)
|
||||
.with(AppErrorCode.UNAUTHORIZED, () => error.message)
|
||||
@@ -47,18 +53,22 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold">API Tokens</h3>
|
||||
<h3 className="text-2xl font-semibold">
|
||||
<Trans>API Tokens</Trans>
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
On this page, you can create new API tokens and manage the existing ones. <br />
|
||||
You can view our swagger docs{' '}
|
||||
<a
|
||||
className="text-primary underline"
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/openapi`}
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
<Trans>
|
||||
On this page, you can create new API tokens and manage the existing ones. <br />
|
||||
You can view our swagger docs{' '}
|
||||
<a
|
||||
className="text-primary underline"
|
||||
href={`${NEXT_PUBLIC_WEBAPP_URL()}/api/v1/openapi`}
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<hr className="my-4" />
|
||||
@@ -67,12 +77,14 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
||||
|
||||
<hr className="mb-4 mt-8" />
|
||||
|
||||
<h4 className="text-xl font-medium">Your existing tokens</h4>
|
||||
<h4 className="text-xl font-medium">
|
||||
<Trans>Your existing tokens</Trans>
|
||||
</h4>
|
||||
|
||||
{tokens.length === 0 && (
|
||||
<div className="mb-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm italic">
|
||||
Your tokens will be shown here once you create them.
|
||||
<Trans>Your tokens will be shown here once you create them.</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -86,22 +98,30 @@ export default async function ApiTokensPage({ params }: ApiTokensPageProps) {
|
||||
<h5 className="text-base">{token.name}</h5>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
Created on <LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
|
||||
<Trans>
|
||||
Created on{' '}
|
||||
<LocaleDate date={token.createdAt} format={DateTime.DATETIME_FULL} />
|
||||
</Trans>
|
||||
</p>
|
||||
{token.expires ? (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
Expires on <LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
|
||||
<Trans>
|
||||
Expires on{' '}
|
||||
<LocaleDate date={token.expires} format={DateTime.DATETIME_FULL} />
|
||||
</Trans>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground mt-1 text-xs">
|
||||
Token doesn't have an expiration date
|
||||
<Trans>Token doesn't have an expiration date</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<DeleteTokenDialog token={token} teamId={team.id}>
|
||||
<Button variant="destructive">Delete</Button>
|
||||
<Button variant="destructive">
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
</DeleteTokenDialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
@@ -39,7 +41,9 @@ export type WebhookPageOptions = {
|
||||
};
|
||||
|
||||
export default function WebhookPage({ params }: WebhookPageOptions) {
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
@@ -73,16 +77,18 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Webhook updated',
|
||||
description: 'The webhook has been updated successfully.',
|
||||
title: _(msg`Webhook updated`),
|
||||
description: _(msg`The webhook has been updated successfully.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Failed to update webhook',
|
||||
description: 'We encountered an error while updating the webhook. Please try again later.',
|
||||
title: _(msg`Failed to update webhook`),
|
||||
description: _(
|
||||
msg`We encountered an error while updating the webhook. Please try again later.`,
|
||||
),
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
@@ -91,8 +97,8 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
title="Edit webhook"
|
||||
subtitle="On this page, you can edit the webhook and its settings."
|
||||
title={_(msg`Edit webhook`)}
|
||||
subtitle={_(msg`On this page, you can edit the webhook and its settings.`)}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
@@ -119,7 +125,7 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
The URL for Documenso to send webhook events to.
|
||||
<Trans>The URL for Documenso to send webhook events to.</Trans>
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -132,7 +138,9 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
|
||||
name="enabled"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Enabled</FormLabel>
|
||||
<FormLabel>
|
||||
<Trans>Enabled</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormControl>
|
||||
@@ -155,7 +163,9 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
|
||||
name="eventTriggers"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<FormItem className="flex flex-col gap-2">
|
||||
<FormLabel required>Triggers</FormLabel>
|
||||
<FormLabel required>
|
||||
<Trans>Triggers</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<TriggerMultiSelectCombobox
|
||||
listValues={value}
|
||||
@@ -166,7 +176,7 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
The events that will trigger a webhook to be sent to your URL.
|
||||
<Trans>The events that will trigger a webhook to be sent to your URL.</Trans>
|
||||
</FormDescription>
|
||||
|
||||
<FormMessage />
|
||||
@@ -185,8 +195,10 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
|
||||
</FormControl>
|
||||
|
||||
<FormDescription>
|
||||
A secret that will be sent to your URL so you can verify that the request has
|
||||
been sent by Documenso.
|
||||
<Trans>
|
||||
A secret that will be sent to your URL so you can verify that the request has
|
||||
been sent by Documenso.
|
||||
</Trans>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -195,7 +207,7 @@ export default function WebhookPage({ params }: WebhookPageOptions) {
|
||||
|
||||
<div className="mt-4">
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
Update webhook
|
||||
<Trans>Update webhook</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Trans, msg } from '@lingui/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Loader } from 'lucide-react';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
@@ -18,6 +20,8 @@ import { LocaleDate } from '~/components/formatter/locale-date';
|
||||
import { useCurrentTeam } from '~/providers/team';
|
||||
|
||||
export default function WebhookPage() {
|
||||
const { _ } = useLingui();
|
||||
|
||||
const team = useCurrentTeam();
|
||||
|
||||
const { data: webhooks, isLoading } = trpc.webhook.getTeamWebhooks.useQuery({
|
||||
@@ -27,8 +31,8 @@ export default function WebhookPage() {
|
||||
return (
|
||||
<div>
|
||||
<SettingsHeader
|
||||
title="Webhooks"
|
||||
subtitle="On this page, you can create new Webhooks and manage the existing ones."
|
||||
title={_(msg`Webhooks`)}
|
||||
subtitle={_(msg`On this page, you can create new Webhooks and manage the existing ones.`)}
|
||||
>
|
||||
<CreateWebhookDialog />
|
||||
</SettingsHeader>
|
||||
@@ -43,7 +47,9 @@ export default function WebhookPage() {
|
||||
// TODO: Perhaps add some illustrations here to make the page more engaging
|
||||
<div className="mb-4">
|
||||
<p className="text-muted-foreground mt-2 text-sm italic">
|
||||
You have no webhooks yet. Your webhooks will be shown here once you create them.
|
||||
<Trans>
|
||||
You have no webhooks yet. Your webhooks will be shown here once you create them.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -71,29 +77,37 @@ export default function WebhookPage() {
|
||||
</h5>
|
||||
|
||||
<Badge variant={webhook.enabled ? 'neutral' : 'warning'} size="small">
|
||||
{webhook.enabled ? 'Enabled' : 'Disabled'}
|
||||
{webhook.enabled ? <Trans>Enabled</Trans> : <Trans>Disabled</Trans>}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
Listening to{' '}
|
||||
{webhook.eventTriggers
|
||||
.map((trigger) => toFriendlyWebhookEventName(trigger))
|
||||
.join(', ')}
|
||||
<Trans>
|
||||
Listening to{' '}
|
||||
{webhook.eventTriggers
|
||||
.map((trigger) => toFriendlyWebhookEventName(trigger))
|
||||
.join(', ')}
|
||||
</Trans>
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">
|
||||
Created on{' '}
|
||||
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
|
||||
<Trans>
|
||||
Created on{' '}
|
||||
<LocaleDate date={webhook.createdAt} format={DateTime.DATETIME_FULL} />
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-shrink-0 gap-4 sm:mt-0">
|
||||
<Button asChild variant="outline">
|
||||
<Link href={`/t/${team.url}/settings/webhooks/${webhook.id}`}>Edit</Link>
|
||||
<Link href={`/t/${team.url}/settings/webhooks/${webhook.id}`}>
|
||||
<Trans>Edit</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
<DeleteWebhookDialog webhook={webhook}>
|
||||
<Button variant="destructive">Delete</Button>
|
||||
<Button variant="destructive">
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
</DeleteWebhookDialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
|
||||
@@ -13,6 +14,8 @@ type TeamTemplatePageProps = {
|
||||
};
|
||||
|
||||
export default async function TeamTemplatePage({ params }: TeamTemplatePageProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { teamUrl } = params;
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
|
||||
|
||||
@@ -17,6 +18,8 @@ export default async function TeamTemplatesPage({
|
||||
searchParams = {},
|
||||
params,
|
||||
}: TeamTemplatesPageProps) {
|
||||
setupI18nSSR();
|
||||
|
||||
const { teamUrl } = params;
|
||||
|
||||
const { user } = await getRequiredServerComponentSession();
|
||||
|
||||
Reference in New Issue
Block a user