Extend lang support

This commit is contained in:
Henry
2025-06-25 13:20:07 +01:00
parent 8a93951464
commit a2f7582999
48 changed files with 4937 additions and 390 deletions

View File

@@ -1,5 +1,7 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { env } from "next-runtime-env";
import { useState } from "react";
@@ -26,7 +28,7 @@ export default function LoginPage() {
return (
<>
<PageHead title="Login | kan.bn" />
<PageHead title={t`Login | kan.bn`} />
<main className="h-screen bg-light-100 pt-20 dark:bg-dark-50 sm:pt-0">
<div className="justify-top flex h-full flex-col items-center px-4 sm:justify-center">
<div className="z-10 flex w-full flex-col items-center">
@@ -36,12 +38,15 @@ export default function LoginPage() {
</h1>
</Link>
<p className="mb-10 text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
{isMagicLinkSent ? "Check your inbox" : "Welcome back"}
{isMagicLinkSent ? t`Check your inbox` : t`Welcome back`}
</p>
{isMagicLinkSent ? (
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
<p className="text-md mt-2 text-center text-light-1000 dark:text-dark-1000">
{`Click on the link we've sent to ${magicLinkRecipient} to sign in.`}
<Trans>
Click on the link we've sent to {magicLinkRecipient} to sign
in.
</Trans>
</p>
</div>
) : (
@@ -53,10 +58,12 @@ export default function LoginPage() {
)}
{!isSignUpDisabled && (
<p className="mt-4 text-sm text-light-1000 dark:text-dark-1000">
Don't have an account?{" "}
<span className="underline">
<Link href="/signup">Sign up</Link>
</span>
<Trans>
Don't have an account?{" "}
<span className="underline">
<Link href="/signup">Sign up</Link>
</span>
</Trans>
</p>
)}
</div>

View File

@@ -1,5 +1,8 @@
import Link from "next/link";
import { useRouter } from "next/navigation";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { env } from "next-runtime-env";
import { useState } from "react";
import { authClient } from "@kan/auth/client";
@@ -7,34 +10,51 @@ import { authClient } from "@kan/auth/client";
import { Auth } from "~/components/AuthForm";
import { PageHead } from "~/components/PageHead";
import PatternedBackground from "~/components/PatternedBackground";
import { env } from "next-runtime-env";
export default function SignupPage() {
export default function SignUpPage() {
const router = useRouter();
const isSignUpDisabled = env("NEXT_PUBLIC_DISABLE_SIGN_UP") === "true";
const [isMagicLinkSent, setIsMagicLinkSent] = useState<boolean>(false);
const [magicLinkRecipient, setMagicLinkRecipient] = useState<string>("");
const { data } = authClient.useSession();
if (data?.user.id) router.push("/boards");
const handleMagicLinkSent = (value: boolean, recipient: string) => {
setIsMagicLinkSent(value);
setMagicLinkRecipient(recipient);
};
const { data } = authClient.useSession();
if (data?.user.id) router.push("/boards");
if (env("NEXT_PUBLIC_DISABLE_SIGN_UP")?.toLowerCase() === "true") {
if (isSignUpDisabled) {
return (
<div className="flex flex-col bg-light-100 dark:bg-dark-50 h-screen items-center justify-center">
<p className="mb-2 text-light-1000 dark:text-dark-1000 font-semibold text-sm">Sign up is disabled</p>
<Link href="/login" className="text-light-1000 dark:text-dark-1000 font-semibold text-sm rounded-md border border-light-900 dark:border-dark-900 px-4 py-2">Login</Link>
</div>
)
<>
<PageHead title={t`Sign up | kan.bn`} />
<main className="h-screen bg-light-100 pt-20 dark:bg-dark-50 sm:pt-0">
<div className="justify-top flex h-full flex-col items-center px-4 sm:justify-center">
<div className="z-10 flex w-full flex-col items-center">
<Link href="/">
<h1 className="mb-6 text-lg font-bold tracking-tight text-light-1000 dark:text-dark-1000">
kan.bn
</h1>
</Link>
<p className="mb-10 text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
{t`Sign up disabled`}
</p>
<p className="text-md text-center text-light-1000 dark:text-dark-1000">
{t`Sign up is currently disabled. Please try again later.`}
</p>
</div>
<PatternedBackground />
</div>
</main>
</>
);
}
return (
<>
<PageHead title="Sign up | kan.bn" />
<PageHead title={t`Sign up | kan.bn`} />
<main className="h-screen bg-light-100 pt-20 dark:bg-dark-50 sm:pt-0">
<div className="justify-top flex h-full flex-col items-center px-4 sm:justify-center">
<div className="z-10 flex w-full flex-col items-center">
@@ -44,12 +64,15 @@ export default function SignupPage() {
</h1>
</Link>
<p className="mb-10 text-3xl font-bold tracking-tight text-light-1000 dark:text-dark-1000">
{isMagicLinkSent ? "Check your inbox" : "Create your account"}
{isMagicLinkSent ? t`Check your inbox` : t`Get started`}
</p>
{isMagicLinkSent ? (
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
<p className="text-md mt-2 text-center text-light-1000 dark:text-dark-1000">
{`Click on the link we've sent to ${magicLinkRecipient} to sign in.`}
<Trans>
Click on the link we've sent to {magicLinkRecipient} to sign
in.
</Trans>
</p>
</div>
) : (
@@ -60,10 +83,12 @@ export default function SignupPage() {
</div>
)}
<p className="mt-4 text-sm text-light-1000 dark:text-dark-1000">
Already have an account?{" "}
<span className="underline">
<Link href="/login">Sign in</Link>
</span>
<Trans>
Already have an account?{" "}
<span className="underline">
<Link href="/login">Sign in</Link>
</span>
</Trans>
</p>
</div>
<PatternedBackground />

View File

@@ -1,3 +1,4 @@
import { t } from "@lingui/core/macro";
import { HiEllipsisHorizontal, HiLink, HiOutlineTrash } from "react-icons/hi2";
import Dropdown from "~/components/Dropdown";
@@ -11,12 +12,12 @@ export default function BoardDropdown({ isLoading }: { isLoading: boolean }) {
disabled={isLoading}
items={[
{
label: "Edit board URL",
label: t`Edit board URL`,
action: () => openModal("UPDATE_BOARD_SLUG"),
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
},
{
label: "Delete board",
label: t`Delete board`,
action: () => openModal("DELETE_BOARD"),
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />,
},

View File

@@ -1,4 +1,5 @@
import { useRouter } from "next/router";
import { t } from "@lingui/core/macro";
import {
HiMiniXMark,
HiOutlineTag,
@@ -89,7 +90,7 @@ const Filters = ({
? [
{
key: "members",
label: "Members",
label: t`Members`,
icon: <HiOutlineUserCircle size={16} />,
items: formattedMembers,
},
@@ -97,7 +98,7 @@ const Filters = ({
: []),
{
key: "labels",
label: "Labels",
label: t`Labels`,
icon: <HiOutlineTag size={16} />,
items: formattedLabels,
},
@@ -145,13 +146,13 @@ const Filters = ({
disabled={isLoading}
iconLeft={<IoFilterOutline />}
>
Filter
{t`Filter`}
</Button>
{numOfFilters > 0 && (
<button
type="button"
onClick={clearFilters}
aria-label="Clear filters"
aria-label={t`Clear filters`}
className="group absolute -right-[8px] -top-[8px] flex h-5 w-5 items-center justify-center rounded-full border-2 border-light-100 bg-light-1000 text-[8px] font-[700] text-light-600 dark:border-dark-50 dark:bg-dark-1000 dark:text-dark-600"
>
<span className="group-hover:hidden">{numOfFilters}</span>

View File

@@ -92,9 +92,14 @@ export function NewCardForm({
args.labelPublicIds.includes(label.publicId),
),
members:
oldBoard.workspace.members.filter((member) =>
args.memberPublicIds.includes(member.publicId),
) ?? [],
oldBoard.workspace.members
.filter((member) =>
args.memberPublicIds.includes(member.publicId),
)
.map((member) => ({
...member,
deletedAt: null,
})) ?? [],
_filteredLabels: labelPublicIds.map((id) => ({ publicId: id })),
_filteredMembers: memberPublicIds.map((id) => ({ publicId: id })),
index: position === "start" ? 0 : list.cards.length,
@@ -168,6 +173,7 @@ export function NewCardForm({
member.user?.name ?? null,
member.user?.email ?? member.email,
),
selected: memberPublicIds.includes(member.publicId),
leftIcon: (
<Avatar
size="xs"
@@ -281,7 +287,7 @@ export function NewCardForm({
>
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
{!memberPublicIds.length ? (
"Members"
t`Members`
) : (
<div className="flex -space-x-1 overflow-hidden">
{memberPublicIds.map((memberPublicId) => {
@@ -322,7 +328,7 @@ export function NewCardForm({
>
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
{!labelPublicIds.length ? (
"Labels"
t`Labels`
) : (
<>
<div

View File

@@ -1,3 +1,4 @@
import { t } from "@lingui/core/macro";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { HiXMark } from "react-icons/hi2";
@@ -73,8 +74,8 @@ export function NewListForm({
onError: (_error, _newList, context) => {
utils.board.byId.setData(queryParams, context?.previousState);
showPopup({
header: "Unable to create list",
message: "Please try again later, or contact customer support.",
header: t`Unable to create list`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -108,7 +109,7 @@ export function NewListForm({
<div className="px-5 pt-5">
<div className="flex w-full items-center justify-between pb-4">
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
New list
{t`New list`}
</h2>
<button
type="button"
@@ -124,7 +125,7 @@ export function NewListForm({
<Input
id="list-name"
placeholder="List name"
placeholder={t`List name`}
{...register("name")}
onKeyDown={async (e) => {
if (e.key === "Enter") {
@@ -136,7 +137,7 @@ export function NewListForm({
</div>
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
<Toggle
label="Create another"
label={t`Create another`}
isChecked={isCreateAnotherEnabled}
onChange={() =>
setValue("isCreateAnotherEnabled", !isCreateAnotherEnabled)
@@ -144,7 +145,7 @@ export function NewListForm({
/>
<div>
<Button type="submit">Create list</Button>
<Button type="submit">{t`Create list`}</Button>
</div>
</div>
</form>

View File

@@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
@@ -12,20 +13,6 @@ import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
const schema = z.object({
slug: z
.string()
.min(3, {
message: "Board URL must be at least 3 characters long",
})
.max(60, { message: "Board URL cannot exceed 60 characters" })
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
message: "Board URL can only contain letters, numbers, and hyphens",
}),
});
type FormValues = z.infer<typeof schema>;
interface QueryParams {
boardPublicId: string;
members: string[];
@@ -47,6 +34,20 @@ export function UpdateBoardSlugForm({
const { showPopup } = usePopup();
const utils = api.useUtils();
const schema = z.object({
slug: z
.string()
.min(3, {
message: t`Board URL must be at least 3 characters long`,
})
.max(60, { message: t`Board URL cannot exceed 60 characters` })
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
message: t`Board URL can only contain letters, numbers, and hyphens`,
}),
});
type FormValues = z.infer<typeof schema>;
const {
register,
handleSubmit,
@@ -67,8 +68,8 @@ export function UpdateBoardSlugForm({
const updateBoardSlug = api.board.update.useMutation({
onError: () => {
showPopup({
header: "Unable to update board URL",
message: "Please try again later, or contact customer support.",
header: t`Unable to update board URL`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -78,17 +79,15 @@ export function UpdateBoardSlugForm({
},
});
const checkBoardSlugAvailability =
api.board.checkSlugAvailability.useQuery(
{
boardSlug: debouncedSlug,
boardPublicId,
},
{
enabled:
!!debouncedSlug && debouncedSlug !== boardSlug && !errors.slug,
},
);
const checkBoardSlugAvailability = api.board.checkSlugAvailability.useQuery(
{
boardSlug: debouncedSlug,
boardPublicId,
},
{
enabled: !!debouncedSlug && debouncedSlug !== boardSlug && !errors.slug,
},
);
const isBoardSlugAvailable = checkBoardSlugAvailability.data;
@@ -100,7 +99,7 @@ export function UpdateBoardSlugForm({
const onSubmit = (data: FormValues) => {
if (!isBoardSlugAvailable) return;
if (isBoardSlugAvailable?.isReserved) return;
if (isBoardSlugAvailable.isReserved) return;
updateBoardSlug.mutate({
slug: data.slug,
@@ -113,7 +112,7 @@ export function UpdateBoardSlugForm({
<div className="px-5 pt-5">
<div className="flex w-full items-center justify-between pb-4">
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
Edit board URL
{t`Edit board URL`}
</h2>
<button
type="button"
@@ -130,9 +129,12 @@ export function UpdateBoardSlugForm({
<Input
id="board-slug"
{...register("slug")}
errorMessage={errors.slug?.message || (isBoardSlugAvailable?.isReserved
? "This board URL has already been taken"
: undefined)}
errorMessage={
errors.slug?.message ||
(isBoardSlugAvailable?.isReserved
? t`This board URL has already been taken`
: undefined)
}
prefix={`${env("NEXT_PUBLIC_BASE_URL")}/${workspaceSlug}/`}
onKeyDown={async (e) => {
if (e.key === "Enter") {
@@ -161,7 +163,7 @@ export function UpdateBoardSlugForm({
isBoardSlugAvailable?.isReserved
}
>
Update
{t`Update`}
</Button>
</div>
</div>

View File

@@ -1,4 +1,7 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { formatDistanceToNow } from "date-fns";
import { de, enGB, es, fr, it, nl } from "date-fns/locale";
import {
HiOutlineArrowLeft,
HiOutlineArrowRight,
@@ -8,24 +11,24 @@ import {
HiOutlineUserMinus,
HiOutlineUserPlus,
} from "react-icons/hi2";
import type { GetCardByIdOutput } from "@kan/api/types";
import { authClient } from "@kan/auth/client";
import Avatar from "~/components/Avatar";
import { useLocalisation } from "~/hooks/useLocalisation";
import Comment from "./Comment";
import { authClient } from "@kan/auth/client";
type ActivityType =
NonNullable<GetCardByIdOutput>["activities"][number]["type"];
const ACTIVITY_TYPE_MAP = {
"card.created": "created the card",
"card.updated.title": "updated the title",
"card.updated.description": "updated the description",
"card.updated.list": "moved the card to another list",
"card.updated.label.added": "added a label to the card",
"card.updated.label.removed": "removed a label from the card",
"card.updated.member.added": "added a member to the card",
"card.updated.member.removed": "removed a member from the card",
const dateLocaleMap = {
en: enGB,
fr: fr,
de: de,
es: es,
it: it,
nl: nl,
} as const;
const getActivityText = ({
@@ -45,6 +48,17 @@ const getActivityText = ({
isSelf: boolean;
label: string | null;
}) => {
const ACTIVITY_TYPE_MAP = {
"card.created": t`created the card`,
"card.updated.title": t`updated the title`,
"card.updated.description": t`updated the description`,
"card.updated.list": t`moved the card to another list`,
"card.updated.label.added": t`added a label to the card`,
"card.updated.label.removed": t`removed a label from the card`,
"card.updated.member.added": t`added a member to the card`,
"card.updated.member.removed": t`removed a member from the card`,
} as const;
if (!(type in ACTIVITY_TYPE_MAP)) return null;
const baseText = ACTIVITY_TYPE_MAP[type as keyof typeof ACTIVITY_TYPE_MAP];
@@ -56,54 +70,54 @@ const getActivityText = ({
if (type === "card.updated.title" && toTitle) {
return (
<>
<Trans>
updated the title to <TextHighlight>{toTitle}</TextHighlight>
</>
</Trans>
);
}
if (type === "card.updated.list" && fromList && toList) {
return (
<>
<Trans>
moved the card from <TextHighlight>{fromList}</TextHighlight> to
<TextHighlight>{toList}</TextHighlight>
</>
</Trans>
);
}
if (type === "card.updated.member.added" && memberName) {
if (isSelf) return <>self-assigned the card</>;
if (isSelf) return <Trans>self-assigned the card</Trans>;
return (
<>
<Trans>
assigned <TextHighlight>{memberName}</TextHighlight> to the card
</>
</Trans>
);
}
if (type === "card.updated.member.removed" && memberName) {
if (isSelf) return <>unassigned themselves from the card</>;
if (isSelf) return <Trans>unassigned themselves from the card</Trans>;
return (
<>
<Trans>
unassigned <TextHighlight>{memberName}</TextHighlight> from the card
</>
</Trans>
);
}
if (type === "card.updated.label.added" && label) {
return (
<>
<Trans>
added label <TextHighlight>{label}</TextHighlight>
</>
</Trans>
);
}
if (type === "card.updated.label.removed" && label) {
return (
<>
<Trans>
removed label <TextHighlight>{label}</TextHighlight>
</>
</Trans>
);
}
@@ -148,6 +162,9 @@ const ActivityList = ({
isAdmin?: boolean;
}) => {
const { data } = authClient.useSession();
const { locale } = useLocalisation();
const currentDateLocale = dateLocaleMap[locale] || enGB;
return (
<div className="flex flex-col space-y-4 pt-4">
@@ -158,7 +175,7 @@ const ActivityList = ({
fromList: activity.fromList?.name ?? null,
toList: activity.toList?.name ?? null,
memberName: activity.member?.user?.name ?? null,
isSelf: activity.member?.user?.id === data?.user?.id,
isSelf: activity.member?.user?.id === data?.user.id,
label: activity.label?.name ?? null,
});
@@ -171,10 +188,10 @@ const ActivityList = ({
name={activity.user?.name ?? ""}
email={activity.user?.email ?? ""}
isLoading={isLoading}
createdAt={activity.createdAt}
createdAt={activity.createdAt.toISOString()}
comment={activity.comment?.comment}
isEdited={!!activity.comment?.updatedAt}
isAuthor={activity.comment?.createdBy === data?.user?.id}
isAuthor={activity.comment?.createdBy === data?.user.id}
isAdmin={isAdmin ?? false}
/>
);
@@ -213,6 +230,7 @@ const ActivityList = ({
<span className="space-x-1 text-light-900 dark:text-dark-800">
{formatDistanceToNow(new Date(activity.createdAt), {
addSuffix: true,
locale: currentDateLocale,
})}
</span>
</p>

View File

@@ -1,3 +1,4 @@
import { t } from "@lingui/core/macro";
import { formatDistanceToNow } from "date-fns";
import { useState } from "react";
import ContentEditable from "react-contenteditable";
@@ -57,8 +58,8 @@ const Comment = ({
},
onError: () => {
showPopup({
header: "Unable to update comment",
message: "Please try again later, or contact customer support.",
header: t`Unable to update comment`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -76,7 +77,7 @@ const Comment = ({
...(isAuthor
? [
{
label: "Edit comment",
label: t`Edit comment`,
action: () => setIsEditing(true),
icon: <HiPencil className="h-[16px] w-[16px] text-dark-900" />,
},
@@ -85,7 +86,7 @@ const Comment = ({
...(isAuthor || isAdmin
? [
{
label: "Delete comment",
label: t`Delete comment`,
action: () => openModal("DELETE_COMMENT", publicId),
icon: <HiTrash className="h-[16px] w-[16px] text-dark-900" />,
},
@@ -117,7 +118,7 @@ const Comment = ({
</span>
{isEdited && (
<span className="text-light-900 dark:text-dark-800">
{" (edited)"}
{t` (edited)`}
</span>
)}
</p>
@@ -140,7 +141,7 @@ const Comment = ({
) : (
<form onSubmit={handleSubmit(onSubmit)}>
<ContentEditable
placeholder="Add a comment..."
placeholder={t`Add a comment...`}
html={watch("comment")}
disabled={false}
onChange={(e) => setValue("comment", e.target.value)}
@@ -152,14 +153,14 @@ const Comment = ({
variant="ghost"
onClick={() => setIsEditing(false)}
>
Cancel
{t`Cancel`}
</Button>
<Button
isLoading={updateCommentMutation.isPending}
type="submit"
size="sm"
>
Save
{t`Save`}
</Button>
</div>
</form>

View File

@@ -1,4 +1,5 @@
import { useRouter } from "next/navigation";
import { t } from "@lingui/core/macro";
import Button from "~/components/Button";
import { useModal } from "~/providers/modal";
@@ -47,8 +48,8 @@ export function DeleteCardConfirmation({
onError: (_error, _newList, context) => {
utils.board.byId.setData(queryParams, context?.previousState);
showPopup({
header: "Unable to delete card",
message: "Please try again later, or contact customer support.",
header: t`Unable to delete card`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -71,10 +72,10 @@ export function DeleteCardConfirmation({
<div className="p-5">
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
Are you sure you want to delete this card?
{t`Are you sure you want to delete this card?`}
</h2>
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
{"This action can't be undone."}
{t`This action can't be undone.`}
</p>
</div>
<div className="mt-5 flex justify-end sm:mt-6">
@@ -82,13 +83,13 @@ export function DeleteCardConfirmation({
className="mr-4 inline-flex justify-center rounded-md border-[1px] border-light-600 bg-light-50 px-3 py-2 text-sm font-semibold text-neutral-900 shadow-sm focus-visible:outline-none dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000"
onClick={() => closeModal()}
>
Cancel
{t`Cancel`}
</button>
<Button
onClick={handleDeleteCard}
isLoading={deleteCardMutation.isPending}
>
Delete
{t`Delete`}
</Button>
</div>
</div>

View File

@@ -1,3 +1,5 @@
import { t } from "@lingui/core/macro";
import Button from "~/components/Button";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
@@ -39,8 +41,8 @@ export function DeleteCommentConfirmation({
onError: (_error, _newList, context) => {
utils.card.byId.setData(queryParams, context?.previousState);
showPopup({
header: "Unable to delete comment",
message: "Please try again later, or contact customer support.",
header: t`Unable to delete comment`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -60,10 +62,10 @@ export function DeleteCommentConfirmation({
<div className="p-5">
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
Are you sure you want to delete this comment?
{t`Are you sure you want to delete this comment?`}
</h2>
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
{"This action can't be undone."}
{t`This action can't be undone.`}
</p>
</div>
<div className="mt-5 flex justify-end sm:mt-6">
@@ -71,13 +73,13 @@ export function DeleteCommentConfirmation({
className="mr-4 inline-flex justify-center rounded-md border-[1px] border-light-600 bg-light-50 px-3 py-2 text-sm font-semibold text-neutral-900 shadow-sm focus-visible:outline-none dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000"
onClick={() => closeModal()}
>
Cancel
{t`Cancel`}
</button>
<Button
onClick={handleDeleteComment}
isLoading={deleteCommentMutation.isPending}
>
Delete
{t`Delete`}
</Button>
</div>
</div>

View File

@@ -1,3 +1,4 @@
import { t } from "@lingui/core/macro";
import { HiEllipsisHorizontal, HiOutlineTrash } from "react-icons/hi2";
import Dropdown from "~/components/Dropdown";
@@ -10,7 +11,7 @@ export default function BoardDropdown() {
<Dropdown
items={[
{
label: "Delete card",
label: t`Delete card`,
action: () => openModal("DELETE_CARD"),
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />,
},

View File

@@ -1,4 +1,5 @@
import { Menu } from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { HiMiniPlus } from "react-icons/hi2";
import Badge from "~/components/Badge";
@@ -68,8 +69,8 @@ export default function LabelSelector({
onError: (_error, _newList, context) => {
utils.card.byId.setData({ cardPublicId }, context?.previousCard);
showPopup({
header: "Unable to update labels",
message: "Please try again later, or contact customer support.",
header: t`Unable to update labels`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -94,7 +95,7 @@ export default function LabelSelector({
}}
handleEdit={(labelPublicId) => openModal("EDIT_LABEL", labelPublicId)}
handleCreate={() => openModal("NEW_LABEL")}
createNewItemLabel="Create new label"
createNewItemLabel={t`Create new label`}
asChild
>
{selectedLabels.length ? (
@@ -105,13 +106,16 @@ export default function LabelSelector({
</Menu.Button>
))}
<Menu.Button>
<Badge value="Add label" iconLeft={<HiMiniPlus size={14} />} />
<Badge
value={t`Add label`}
iconLeft={<HiMiniPlus size={14} />}
/>
</Menu.Button>
</div>
) : (
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-200 pl-2 text-left text-sm text-neutral-900 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
<HiMiniPlus size={22} className="pr-2" />
Add label
{t`Add label`}
</Menu.Button>
)}
</CheckboxDropdown>

View File

@@ -1,4 +1,5 @@
import { Menu } from "@headlessui/react";
import { t } from "@lingui/core/macro";
import CheckboxDropdown from "~/components/CheckboxDropdown";
import { usePopup } from "~/providers/popup";
@@ -48,8 +49,8 @@ export default function ListSelector({
onError: (_error, _newList, context) => {
utils.card.byId.setData({ cardPublicId }, context?.previousCard);
showPopup({
header: "Unable to update list",
message: "Please try again later, or contact customer support.",
header: t`Unable to update list`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},

View File

@@ -1,5 +1,6 @@
import { useRouter } from "next/router";
import { Menu } from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { HiMiniPlus } from "react-icons/hi2";
import Avatar from "~/components/Avatar";
@@ -55,6 +56,8 @@ export default function MemberSelector({
...oldCard.members,
{
publicId: update.workspaceMemberPublicId,
email: memberToAdd?.email ?? "",
deletedAt: null,
user: {
id: memberToAdd?.user?.id ?? "",
name: memberToAdd?.user?.name ?? "",
@@ -73,8 +76,8 @@ export default function MemberSelector({
onError: (_error, _newList, context) => {
utils.card.byId.setData({ cardPublicId }, context?.previousCard);
showPopup({
header: "Unable to update members",
message: "Please try again later, or contact customer support.",
header: t`Unable to update members`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -106,7 +109,7 @@ export default function MemberSelector({
});
}}
handleCreate={handleInviteMember}
createNewItemLabel="Invite member"
createNewItemLabel={t`Invite member`}
asChild
>
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-200 py-1 pl-2 text-left text-sm text-neutral-900 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
@@ -124,7 +127,7 @@ export default function MemberSelector({
) : (
<>
<HiMiniPlus size={22} className="pr-2" />
{"Add member"}
{t`Add member`}
</>
)}
</Menu.Button>

View File

@@ -1,3 +1,4 @@
import { t } from "@lingui/core/macro";
import ContentEditable from "react-contenteditable";
import { useForm } from "react-hook-form";
import { HiOutlineArrowUp } from "react-icons/hi2";
@@ -26,8 +27,8 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
const addCommentMutation = api.card.addComment.useMutation({
onError: (_error, _newList) => {
showPopup({
header: "Unable to add comment",
message: "Please try again later, or contact customer support.",
header: t`Unable to add comment`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -53,7 +54,7 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
className="flex w-full max-w-[1000px] flex-col rounded-xl border border-light-600 bg-light-200 p-4 text-light-900 focus-visible:outline-none dark:border-dark-400 dark:bg-dark-100 dark:text-dark-1000 sm:text-sm sm:leading-6"
>
<ContentEditable
placeholder="Add a comment..."
placeholder={t`Add a comment...`}
html={watch("comment")}
disabled={false}
onChange={(e) => setValue("comment", e.target.value)}

View File

@@ -1,9 +1,11 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { t } from "@lingui/core/macro";
import { useForm } from "react-hook-form";
import { IoChevronForwardSharp } from "react-icons/io5";
import Avatar from "~/components/Avatar";
import Editor from "~/components/Editor";
import { LabelForm } from "~/components/LabelForm";
import LabelIcon from "~/components/LabelIcon";
import Modal from "~/components/modal";
@@ -23,7 +25,6 @@ import LabelSelector from "./components/LabelSelector";
import ListSelector from "./components/ListSelector";
import MemberSelector from "./components/MemberSelector";
import NewCommentForm from "./components/NewCommentForm";
import Editor from "~/components/Editor";
interface FormValues {
cardId: string;
@@ -111,8 +112,8 @@ export default function CardPage() {
const updateCard = api.card.update.useMutation({
onError: () => {
showPopup({
header: "Unable to update card",
message: "Please try again later, or contact customer support.",
header: t`Unable to update card`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -144,7 +145,7 @@ export default function CardPage() {
return (
<>
<PageHead
title={`${card?.title ?? "Card"} | ${board?.name ?? "Board"}`}
title={t`${card?.title ?? "Card"} | ${board?.name ?? "Board"}`}
/>
<div className="flex h-full flex-1 flex-row">
<div className="flex h-full w-full flex-col overflow-hidden">
@@ -189,7 +190,7 @@ export default function CardPage() {
)}
{!card && !isLoading && (
<p className="block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
Card not found
{t`Card not found`}
</p>
)}
</div>
@@ -211,7 +212,7 @@ export default function CardPage() {
</div>
<div className="border-t-[1px] border-light-600 pt-12 dark:border-dark-400">
<h2 className="text-md pb-4 font-medium text-light-900 dark:text-dark-1000">
Activity
{t`Activity`}
</h2>
<div>
<ActivityList
@@ -231,7 +232,7 @@ export default function CardPage() {
</div>
<div className="w-[475px] border-l-[1px] border-light-600 bg-light-200 p-8 text-light-900 dark:border-dark-400 dark:bg-dark-100 dark:text-dark-900">
<div className="mb-4 flex w-full">
<p className="my-2 w-[100px] text-sm">List</p>
<p className="my-2 w-[100px] text-sm">{t`List`}</p>
<ListSelector
cardPublicId={cardId}
lists={formattedLists}
@@ -239,7 +240,7 @@ export default function CardPage() {
/>
</div>
<div className="mb-4 flex w-full">
<p className="my-2 w-[100px] text-sm">Labels</p>
<p className="my-2 w-[100px] text-sm">{t`Labels`}</p>
<LabelSelector
cardPublicId={cardId}
labels={formattedLabels}
@@ -247,7 +248,7 @@ export default function CardPage() {
/>
</div>
<div className="flex w-full">
<p className="my-2 w-[100px] text-sm">Members</p>
<p className="my-2 w-[100px] text-sm">{t`Members`}</p>
<MemberSelector
cardPublicId={cardId}
members={formattedMembers}

View File

@@ -70,7 +70,7 @@ const Header = ({ isLoggedIn }: { isLoggedIn: boolean }) => {
<button
onClick={toggleMenu}
className="z-50 p-2 lg:hidden"
aria-label="Toggle menu"
aria-label={t`Toggle menu`}
>
<div
className={twMerge(

View File

@@ -98,7 +98,7 @@ const Pricing = () => {
</div>
<div className="mt-16 flex justify-center">
<fieldset aria-label="Payment frequency">
<fieldset aria-label={t`Payment frequency`}>
<RadioGroup
value={frequency}
onChange={(value) => setFrequency(value)}
@@ -141,7 +141,7 @@ const Pricing = () => {
>
{tier.name}
</h3>
{tier.highlighted && frequency.value === "annually" ? (
{tier.highlighted && frequency?.value === "annually" ? (
<p className="rounded-full bg-light-50 px-2.5 py-1 text-[12px] font-semibold text-dark-500 dark:bg-dark-1000 dark:text-dark-50">
-20%
</p>
@@ -165,11 +165,11 @@ const Pricing = () => {
!tier.showPrice && "opacity-0",
)}
>
{tier.price[frequency.value]}
{tier.price[frequency?.value || "monthly"]}
</span>
{tier.showPriceSuffix && (
<span className="text-sm/6 font-semibold text-light-50 dark:text-dark-900">
{frequency.priceSuffix}
{frequency?.priceSuffix}
</span>
)}
</p>

View File

@@ -1,3 +1,6 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import Button from "~/components/Button";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
@@ -21,8 +24,8 @@ export function DeleteMemberConfirmation() {
},
onError: () => {
showPopup({
header: "Unable to remove member",
message: "Please try again later, or contact customer support.",
header: t`Unable to remove member`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
closeModal();
@@ -41,18 +44,18 @@ export function DeleteMemberConfirmation() {
<div className="p-5">
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
{`Are you sure want to remove ${entityLabel}?`}
<Trans>Are you sure want to remove {entityLabel}?</Trans>
</h2>
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
{"They won't be able to access this workspace."}
{t`They won't be able to access this workspace.`}
</p>
</div>
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
<Button onClick={() => closeModal()} variant="secondary">
Cancel
{t`Cancel`}
</Button>
<Button onClick={handleDeleteMember} isLoading={deleteMember.isPending}>
Remove
{t`Remove`}
</Button>
</div>
</div>

View File

@@ -1,3 +1,4 @@
import { t } from "@lingui/core/macro";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { HiXMark } from "react-icons/hi2";
@@ -46,7 +47,7 @@ export function InviteMemberForm() {
<form onSubmit={handleSubmit(onSubmit)}>
<div className="px-5 pt-5">
<div className="text-neutral-9000 flex w-full items-center justify-between pb-4 dark:text-dark-1000">
<h2 className="text-sm font-bold">Add member</h2>
<h2 className="text-sm font-bold">{t`Add member`}</h2>
<button
type="button"
className="hover:bg-li ght-300 rounded p-1 focus:outline-none dark:hover:bg-dark-300"
@@ -60,7 +61,7 @@ export function InviteMemberForm() {
</div>
<Input
id="email"
placeholder="Email"
placeholder={t`Email`}
{...register("email", { required: true })}
onKeyDown={async (e) => {
if (e.key === "Enter") {
@@ -78,7 +79,7 @@ export function InviteMemberForm() {
isLoading={createBoard.isPending}
className="inline-flex w-full justify-center rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
>
Invite member
{t`Invite member`}
</Button>
</div>
</div>

View File

@@ -1,3 +1,4 @@
import { t } from "@lingui/core/macro";
import { HiEllipsisHorizontal, HiOutlinePlusSmall } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
@@ -103,7 +104,7 @@ export default function MembersPage() {
</span>
{memberStatus === "invited" && (
<span className="ml-2 inline-flex items-center rounded-md bg-gray-500/10 px-1.5 py-0.5 text-[11px] font-medium text-gray-400 ring-1 ring-inset ring-gray-500/20">
Pending
{t`Pending`}
</span>
)}
</div>
@@ -116,7 +117,7 @@ export default function MembersPage() {
<Dropdown
items={[
{
label: "Remove member",
label: t`Remove member`,
action: () =>
openModal(
"REMOVE_MEMBER",
@@ -140,11 +141,11 @@ export default function MembersPage() {
return (
<>
<PageHead title={`Members | ${workspace.name ?? "Workspace"}`} />
<PageHead title={t`Members | ${workspace.name ?? "Workspace"}`} />
<div className="m-auto max-w-[1600px] px-28 py-12">
<div className="mb-8 flex w-full justify-between">
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
Members
{t`Members`}
</h1>
<div className="flex">
<Button
@@ -152,7 +153,7 @@ export default function MembersPage() {
iconLeft={<HiOutlinePlusSmall className="h-4 w-4" />}
disabled={workspace.role !== "admin"}
>
Invite
{t`Invite`}
</Button>
</div>
</div>
@@ -168,13 +169,13 @@ export default function MembersPage() {
scope="col"
className="w-[65%] rounded-tl-lg py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-light-900 dark:text-dark-900 sm:pl-6"
>
User
{t`User`}
</th>
<th
scope="col"
className="w-[35%] rounded-tr-lg px-3 py-3.5 text-left text-sm font-semibold text-light-900 dark:text-dark-900"
>
Role
{t`Role`}
</th>
</tr>
</thead>

View File

@@ -1,8 +1,10 @@
import { authClient } from "@kan/auth/client";
import { useRouter } from "next/router";
import { t } from "@lingui/core/macro";
import { HiXMark } from "react-icons/hi2";
import Editor from "~/components/Editor";
import { authClient } from "@kan/auth/client";
import Editor from "~/components/Editor";
import { useModal } from "~/providers/modal";
import { api } from "~/utils/api";
import ActivityList from "~/views/card/components/ActivityList";
@@ -99,7 +101,7 @@ export function CardModal({
)}
<div className="border-t-[1px] border-light-600 pb-4 pt-12 dark:border-dark-400">
<h2 className="text-md pb-4 font-medium text-light-900 dark:text-dark-1000">
Activity
{t`Activity`}
</h2>
<div>
{cardPublicId && (

View File

@@ -1,5 +1,6 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { t } from "@lingui/core/macro";
import { keepPreviousData } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { HiLink } from "react-icons/hi2";
@@ -47,9 +48,9 @@ export default function PublicBoardView() {
}
showPopup({
header: "Link copied",
header: t`Link copied`,
icon: "success",
message: "Board URL copied to clipboard",
message: t`Board URL copied to clipboard`,
});
}}
className="rounded p-1.5 transition-all hover:bg-light-200 dark:hover:bg-dark-100"
@@ -82,7 +83,7 @@ export default function PublicBoardView() {
return (
<>
<PageHead
title={`${data?.name ?? "Board"} | ${data?.workspace?.name ?? "Workspace"}`}
title={`${data?.name ?? "Board"} | ${data?.workspace.name ?? "Workspace"}`}
/>
<style jsx global>{`
html {
@@ -137,7 +138,7 @@ export default function PublicBoardView() {
{list.cards.map((card) => (
<Link
key={card.publicId}
href={`/${data.workspace?.slug}/${data.slug}/${card.publicId}`}
href={`/${data.workspace.slug}/${data.slug}/${card.publicId}`}
className={`mb-2 flex !cursor-pointer flex-col`}
shallow={true}
onClick={() => {
@@ -177,7 +178,7 @@ export default function PublicBoardView() {
<Modal modalSize={"md"} positionFromTop={"sm"}>
<CardModal
cardPublicId={cardPublicId}
workspaceSlug={data?.workspace?.slug}
workspaceSlug={data?.workspace.slug}
boardSlug={data?.slug}
/>
</Modal>

View File

@@ -1,4 +1,5 @@
import Image from "next/image";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { useState } from "react";
@@ -20,8 +21,8 @@ export default function Avatar({
const updateUser = api.user.update.useMutation({
onSuccess: async () => {
showPopup({
header: "Profile image updated",
message: "Your profile image has been updated.",
header: t`Profile image updated`,
message: t`Your profile image has been updated.`,
icon: "success",
});
try {
@@ -33,8 +34,8 @@ export default function Avatar({
},
onError: () => {
showPopup({
header: "Error updating profile image",
message: "Please try again later, or contact customer support.",
header: t`Error updating profile image`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -50,8 +51,8 @@ export default function Avatar({
if (!file || !userId) {
return showPopup({
header: "Error uploading profile image",
message: "Please select a file to upload.",
header: t`Error uploading profile image`,
message: t`Please select a file to upload.`,
icon: "error",
});
}
@@ -91,8 +92,8 @@ export default function Avatar({
} catch (error) {
console.error(error);
showPopup({
header: "Error uploading profile image",
message: "Please try again later, or contact customer support.",
header: t`Error uploading profile image`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
} finally {

View File

@@ -1,3 +1,5 @@
import { t } from "@lingui/core/macro";
import { authClient } from "@kan/auth/client";
import Button from "~/components/Button";
@@ -44,12 +46,12 @@ const CreateAPIKeyForm = ({
</div>
<div>
<Button variant="danger" onClick={handleRevokeAPIKey}>
Revoke
{t`Revoke`}
</Button>
</div>
</div>
) : (
<Button onClick={handleCreateAPIKey}>Create new key</Button>
<Button onClick={handleCreateAPIKey}>{t`Create new key`}</Button>
)}
</div>
);

View File

@@ -1,3 +1,5 @@
import { t } from "@lingui/core/macro";
import Button from "~/components/Button";
import { useModal } from "~/providers/modal";
@@ -37,19 +39,17 @@ export function CustomURLConfirmation({
<div className="p-5">
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
{`Confirm URL change`}
{t`Confirm URL change`}
</h2>
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
{
"Custom URLs are a premium feature. You'll be directed to upgrade your account."
}
{t`Custom URLs are a premium feature. You'll be directed to upgrade your account.`}
</p>
</div>
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
<Button onClick={() => closeModal()} variant="secondary">
Cancel
{t`Cancel`}
</Button>
<Button onClick={handleUpgrade}>Upgrade</Button>
<Button onClick={handleUpgrade}>{t`Upgrade`}</Button>
</div>
</div>
);

View File

@@ -1,8 +1,10 @@
import { authClient } from "@kan/auth/client";
import { useMutation } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { t } from "@lingui/core/macro";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { authClient } from "@kan/auth/client";
import Button from "~/components/Button";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
@@ -22,20 +24,20 @@ export function DeleteAccountConfirmation() {
onSuccess: async () => {
closeModal();
showPopup({
header: "Account deleted",
message: "Your account has been deleted.",
header: t`Account deleted`,
message: t`Your account has been deleted.`,
icon: "success",
});
utils.invalidate();
router.push("/");
},
onError: () => {
closeModal();
showPopup({
header: "Error deleting account",
message: "Please try again later, or contact customer support.",
header: t`Error deleting account`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -49,14 +51,13 @@ export function DeleteAccountConfirmation() {
<div className="p-5">
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
{`Are you sure you want to delete your account?`}
{t`Are you sure you want to delete your account?`}
</h2>
<p className="mb-4 text-sm text-light-900 dark:text-dark-900">
Keep in mind that this action is irreversible.
{t`Keep in mind that this action is irreversible.`}
</p>
<p className="text-sm text-light-900 dark:text-dark-900">
This will result in the permanent deletion of all data associated with
your account.
{t`This will result in the permanent deletion of all data associated with your account.`}
</p>
</div>
<div className="relative flex items-start">
@@ -78,14 +79,13 @@ export function DeleteAccountConfirmation() {
id="comments-description"
className="text-light-900 dark:text-dark-1000"
>
I acknowledge that all of my account data will be permanently
deleted and want to proceed.
{t`I acknowledge that all of my account data will be permanently deleted and want to proceed.`}
</p>
</div>
</div>
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
<Button variant="secondary" onClick={() => closeModal()}>
Cancel
{t`Cancel`}
</Button>
<Button
variant="danger"
@@ -93,7 +93,7 @@ export function DeleteAccountConfirmation() {
disabled={!isAcknowledgmentChecked}
isLoading={deleteAccountMutation.isPending}
>
Delete account
{t`Delete account`}
</Button>
</div>
</div>

View File

@@ -1,4 +1,6 @@
import { useRouter } from "next/navigation";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { useState } from "react";
import Button from "~/components/Button";
@@ -21,13 +23,13 @@ export function DeleteWorkspaceConfirmation() {
onSuccess: async () => {
closeModal();
showPopup({
header: "Workspace deleted",
message: "Your workspace has been deleted.",
header: t`Workspace deleted`,
message: t`Your workspace has been deleted.`,
icon: "success",
});
await utils.workspace.all.refetch();
const filteredWorkspaces = availableWorkspaces.filter(
(ws) => ws.publicId !== workspace.publicId,
);
@@ -40,8 +42,8 @@ export function DeleteWorkspaceConfirmation() {
onError: () => {
closeModal();
showPopup({
header: "Error deleting workspace",
message: "Please try again later, or contact customer support.",
header: t`Error deleting workspace`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -57,14 +59,15 @@ export function DeleteWorkspaceConfirmation() {
<div className="p-5">
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
{`Are you sure you want to delete the workspace ${workspace.name}?`}
<Trans>
Are you sure you want to delete the workspace {workspace.name}?
</Trans>
</h2>
<p className="mb-4 text-sm text-light-900 dark:text-dark-900">
Keep in mind that this action is irreversible.
{t`Keep in mind that this action is irreversible.`}
</p>
<p className="text-sm text-light-900 dark:text-dark-900">
This will result in the permanent deletion of all data associated with
this workspace.
{t`This will result in the permanent deletion of all data associated with this workspace.`}
</p>
</div>
<div className="relative flex items-start">
@@ -86,14 +89,13 @@ export function DeleteWorkspaceConfirmation() {
id="comments-description"
className="text-light-900 dark:text-dark-1000"
>
I acknowledge that all of the workspace data will be permanently
deleted and want to proceed.
{t`I acknowledge that all of the workspace data will be permanently deleted and want to proceed.`}
</p>
</div>
</div>
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
<Button variant="secondary" onClick={() => closeModal()}>
Cancel
{t`Cancel`}
</Button>
<Button
variant="danger"
@@ -101,7 +103,7 @@ export function DeleteWorkspaceConfirmation() {
disabled={!isAcknowledgmentChecked}
isLoading={deleteWorkspaceMutation.isPending}
>
Delete workspace
{t`Delete workspace`}
</Button>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/core/macro";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -11,10 +12,10 @@ const schema = z.object({
name: z
.string()
.min(3, {
message: "Display namemust be at least 3 characters long",
message: t`Display name must be at least 3 characters long`,
})
.max(280, {
message: "Display name cannot exceed 280 characters",
message: t`Display name cannot exceed 280 characters`,
}),
});
@@ -37,8 +38,8 @@ const UpdateDisplayNameForm = ({ displayName }: { displayName: string }) => {
const updateDisplayName = api.user.update.useMutation({
onSuccess: async () => {
showPopup({
header: "Display name updated",
message: "Your display name has been updated.",
header: t`Display name updated`,
message: t`Your display name has been updated.`,
icon: "success",
});
try {
@@ -50,8 +51,8 @@ const UpdateDisplayNameForm = ({ displayName }: { displayName: string }) => {
},
onError: () => {
showPopup({
header: "Error updating display name",
message: "Please try again later, or contact customer support.",
header: t`Error updating display name`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -75,7 +76,7 @@ const UpdateDisplayNameForm = ({ displayName }: { displayName: string }) => {
disabled={!isDirty || updateDisplayName.isPending}
isLoading={updateDisplayName.isPending}
>
Update
{t`Update`}
</Button>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/core/macro";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -7,19 +8,6 @@ import Input from "~/components/Input";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
const schema = z.object({
description: z
.string()
.min(3, {
message: "Workspace description must be at least 3 characters long",
})
.max(280, {
message: "Workspace description cannot exceed 280 characters",
}),
});
type FormValues = z.infer<typeof schema>;
const UpdateWorkspaceDescriptionForm = ({
workspacePublicId,
workspaceDescription,
@@ -29,6 +17,20 @@ const UpdateWorkspaceDescriptionForm = ({
}) => {
const utils = api.useUtils();
const { showPopup } = usePopup();
const schema = z.object({
description: z
.string()
.min(3, {
message: t`Workspace description must be at least 3 characters long`,
})
.max(280, {
message: t`Workspace description cannot exceed 280 characters`,
}),
});
type FormValues = z.infer<typeof schema>;
const {
register,
handleSubmit,
@@ -43,8 +45,8 @@ const UpdateWorkspaceDescriptionForm = ({
const updateWorkspaceDescription = api.workspace.update.useMutation({
onSuccess: async () => {
showPopup({
header: "Workspace description updated",
message: "Your workspace description has been updated.",
header: t`Workspace description updated`,
message: t`Your workspace description has been updated.`,
icon: "success",
});
try {
@@ -56,8 +58,8 @@ const UpdateWorkspaceDescriptionForm = ({
},
onError: () => {
showPopup({
header: "Error updating workspace description",
message: "Please try again later, or contact customer support.",
header: t`Error updating workspace description`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -85,7 +87,7 @@ const UpdateWorkspaceDescriptionForm = ({
disabled={!isDirty || updateWorkspaceDescription.isPending}
isLoading={updateWorkspaceDescription.isPending}
>
Update
{t`Update`}
</Button>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/core/macro";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -10,8 +11,8 @@ import { api } from "~/utils/api";
const schema = z.object({
name: z
.string()
.min(3, { message: "Workspace name must be at least 3 characters long" })
.max(24, { message: "Workspace name cannot exceed 24 characters" }),
.min(3, { message: t`Workspace name must be at least 3 characters long` })
.max(24, { message: t`Workspace name cannot exceed 24 characters` }),
});
type FormValues = z.infer<typeof schema>;
@@ -39,8 +40,8 @@ const UpdateWorkspaceNameForm = ({
const updateWorkspaceName = api.workspace.update.useMutation({
onSuccess: async () => {
showPopup({
header: "Workspace name updated",
message: "Your workspace name has been updated.",
header: t`Workspace name updated`,
message: t`Your workspace name has been updated.`,
icon: "success",
});
try {
@@ -52,8 +53,8 @@ const UpdateWorkspaceNameForm = ({
},
onError: () => {
showPopup({
header: "Error updating workspace name",
message: "Please try again later, or contact customer support.",
header: t`Error updating workspace name`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -78,7 +79,7 @@ const UpdateWorkspaceNameForm = ({
disabled={!isDirty || updateWorkspaceName.isPending}
isLoading={updateWorkspaceName.isPending}
>
Update
{t`Update`}
</Button>
</div>
</div>

View File

@@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { useForm } from "react-hook-form";
import { HiCheck, HiMiniStar } from "react-icons/hi2";
@@ -11,20 +12,6 @@ import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
const schema = z.object({
slug: z
.string()
.min(3, {
message: "URL must be at least 3 characters long",
})
.max(24, { message: "URL cannot exceed 24 characters" })
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
message: "URL can only contain letters, numbers, and hyphens",
}),
});
type FormValues = z.infer<typeof schema>;
const UpdateWorkspaceUrlForm = ({
workspacePublicId,
workspaceUrl,
@@ -37,6 +24,21 @@ const UpdateWorkspaceUrlForm = ({
const utils = api.useUtils();
const { showPopup } = usePopup();
const { openModal } = useModal();
const schema = z.object({
slug: z
.string()
.min(3, {
message: t`URL must be at least 3 characters long`,
})
.max(24, { message: t`URL cannot exceed 24 characters` })
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
message: t`URL can only contain letters, numbers, and hyphens`,
}),
});
type FormValues = z.infer<typeof schema>;
const {
register,
handleSubmit,
@@ -55,8 +57,8 @@ const UpdateWorkspaceUrlForm = ({
const updateWorkspaceSlug = api.workspace.update.useMutation({
onSuccess: async () => {
showPopup({
header: "Workspace slug updated",
message: "Your workspace slug has been updated.",
header: t`Workspace slug updated`,
message: t`Your workspace slug has been updated.`,
icon: "success",
});
try {
@@ -68,8 +70,8 @@ const UpdateWorkspaceUrlForm = ({
},
onError: () => {
showPopup({
header: "Error updating workspace URL",
message: "Please try again later, or contact customer support.",
header: t`Error updating workspace URL`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
@@ -118,7 +120,7 @@ const UpdateWorkspaceUrlForm = ({
errorMessage={
errors.slug?.message ||
(isWorkspaceSlugAvailable?.isAvailable === false
? "This workspace username has already been taken"
? t`This workspace username has already been taken`
: undefined)
}
prefix={
@@ -149,7 +151,7 @@ const UpdateWorkspaceUrlForm = ({
}
isLoading={updateWorkspaceSlug.isPending}
>
Update
{t`Update`}
</Button>
</div>
</div>

View File

@@ -1,3 +1,4 @@
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { useEffect } from "react";
import { HiMiniArrowTopRightOnSquare } from "react-icons/hi2";
@@ -65,15 +66,15 @@ export default function SettingsPage() {
refetchIntegrations();
refetchTrelloUrl();
showPopup({
header: "Trello disconnected",
message: "Your Trello account has been disconnected.",
header: t`Trello disconnected`,
message: t`Your Trello account has been disconnected.`,
icon: "success",
});
},
onError: () => {
showPopup({
header: "Error disconnecting Trello",
message: "An error occurred while disconnecting your Trello account.",
header: t`Error disconnecting Trello`,
message: t`An error occurred while disconnecting your Trello account.`,
icon: "error",
});
},
@@ -104,29 +105,29 @@ export default function SettingsPage() {
<>
<div className="flex h-full w-full flex-col overflow-hidden">
<div className="h-full max-h-[calc(100vh-4rem)] overflow-y-auto">
<PageHead title={`Settings | ${workspace.name ?? "Workspace"}`} />
<PageHead title={t`Settings | ${workspace.name ?? "Workspace"}`} />
<div className="px-28 py-12">
<div className="mb-8 flex w-full justify-between">
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
Settings
{t`Settings`}
</h1>
</div>
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
Profile picture
{t`Profile picture`}
</h2>
<Avatar userId={data?.id} userImage={data?.image} />
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
Display name
{t`Display name`}
</h2>
<UpdateDisplayNameForm displayName={data?.name ?? ""} />
</div>
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
Workspace name
{t`Workspace name`}
</h2>
<UpdateWorkspaceNameForm
workspacePublicId={workspace.publicId}
@@ -134,16 +135,16 @@ export default function SettingsPage() {
/>
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
Workspace URL
{t`Workspace URL`}
</h2>
<UpdateWorkspaceUrlForm
workspacePublicId={workspace.publicId}
workspaceUrl={workspace.slug}
workspacePlan={workspace.plan}
workspaceUrl={workspace.slug ?? ""}
workspacePlan={workspace.plan ?? "free"}
/>
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
Workspace description
{t`Workspace description`}
</h2>
<UpdateWorkspaceDescriptionForm
workspacePublicId={workspace.publicId}
@@ -153,10 +154,10 @@ export default function SettingsPage() {
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
Language
{t`Language`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
Change the language of the app.
{t`Change the language of the app.`}
</p>
<LanguageSelector />
</div>
@@ -164,17 +165,17 @@ export default function SettingsPage() {
{env("NEXT_PUBLIC_KAN_ENV") === "cloud" && (
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
Billing
{t`Billing`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
View and manage your billing and subscription.
{t`View and manage your billing and subscription.`}
</p>
<Button
variant="primary"
iconRight={<HiMiniArrowTopRightOnSquare />}
onClick={handleOpenBillingPortal}
>
Billing portal
{t`Billing portal`}
</Button>
</div>
)}
@@ -188,7 +189,7 @@ export default function SettingsPage() {
) && trelloUrl ? (
<>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
Connect your Trello account to import boards.
{t`Connect your Trello account to import boards.`}
</p>
<Button
variant="primary"
@@ -201,32 +202,34 @@ export default function SettingsPage() {
)
}
>
Connect Trello
{t`Connect Trello`}
</Button>
</>
) : (
<>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
You are already connected to Trello.
</p>
<Button
variant="primary"
onClick={() => {
disconnectTrello({ provider: "trello" });
}}
>
Disconnect Trello
</Button>
</>
integrations?.some(
(integration) => integration.provider === "trello",
) && (
<>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`Your Trello account is connected.`}
</p>
<Button
variant="secondary"
onClick={() => disconnectTrello({ provider: "trello" })}
>
{t`Disconnect Trello`}
</Button>
</>
)
)}
</div>
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
API keys
{t`API keys`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
View and manage your API keys.
{t`View and manage your API keys.`}
</p>
<CreateAPIKeyForm
apiKey={data?.apiKey}
@@ -236,36 +239,35 @@ export default function SettingsPage() {
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
Delete workspace
{t`Delete workspace`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
Once you delete your workspace, there is no going back. This
action cannot be undone.
{t`Once you delete your workspace, there is no going back. This action cannot be undone.`}
</p>
<div className="mt-4">
<Button
variant="secondary"
onClick={() => openModal("DELETE_WORKSPACE")}
disabled={workspace.role !== "admin"}
>
Delete workspace
{t`Delete workspace`}
</Button>
</div>
</div>
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] text-neutral-900 dark:text-dark-1000">
Delete account
{t`Delete account`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
Once you delete your account, there is no going back. This
action cannot be undone.
{t`Once you delete your account, there is no going back. This action cannot be undone.`}
</p>
<div className="mt-4">
<Button
variant="secondary"
onClick={() => openModal("DELETE_ACCOUNT")}
>
Delete account
{t`Delete account`}
</Button>
</div>
</div>