Extend lang support
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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" />,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user