Compare commits

..

21 Commits

Author SHA1 Message Date
Henry
ac9bdaf316 feat: improve pricing tiers and add comparison table 2026-02-18 21:58:58 +00:00
Nick Meinhold
031a42b9eb fix: generate presigned avatar URLs for card members on board view (#388)
The board.byId query was generating presigned S3 URLs for workspace
member avatars but not for card member avatars. This caused card avatars
to silently fall back to initials since the frontend helper returns ""
for raw S3 keys.

Closes #387

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 21:10:23 +00:00
Matt
8a1189d011 feat: Activity log for uploading attachments (closes #354) (#366)
* feat: Activity log for uploading attachments (closes #354)

* chore: fix coding style

* revert: upload spinning icon patch

* refactor: add fallbacks, extended card_activity
2026-02-15 20:53:01 +00:00
Charity
53f6ada4d3 feat: add close button to card view header (#383) 2026-02-11 13:30:05 +00:00
Henry
6ea02b1db1 feat: send email notifications on user mentions (#372)
* feat: add notifications schema

* chore: gen migration

* feat: add mention email template

* feat: add sendMentionEmail func

* feat: add repo funcs

* feat: update card router to send emails on mention

* fix: update the editor suggestion to show all members

* feat: skip pending members in sendMentionEmails

* feat: update comments to use tiptap editor
2026-02-11 13:10:46 +00:00
Matt
737b1383d0 feat: add two types of roadmap templates (#369) 2026-02-11 13:03:18 +00:00
Matt
38d0e9495c Join Workspace Email improvements (#371)
* fix failing on percent-encoding invitation urls

* feat: add the workspace and inviter names

* fix: respect the NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY environment variable
2026-02-08 21:34:17 +00:00
Henry
19798ddbc9 fix: update pnpm-lock 2026-02-08 21:11:26 +00:00
Matt
08ba965921 fix: handle x-forwarded-host and proxy issues behind Cloudflare (#370) 2026-02-08 20:59:01 +00:00
kOaDT
008b5fd1a0 fix: prevent long card titles from overflowing column width (#367) 2026-02-08 20:54:44 +00:00
Henry
97cfd60b59 fix: use multipart upload for attachments to resolve chunk size error (#368) 2026-02-07 22:42:38 +00:00
Ivan Shelepugin
24e283e22c feat: smart typography (#360)
* build(web, deps): add `@tiptap/extension-typography`

* feat(editor): add typography extension

* chore(editor): disable some typographic characters

* chore(editor): disable superscript
2026-02-07 20:04:37 +00:00
Matt
07db7409cb fix: remove phantom spacing below social providers in auth form (closes #334) (#364) 2026-02-07 19:34:06 +00:00
Charity
346ddc16be feat: add functionality to copy card link (#358)
* Implemented link copying feature in CardDropdown and CardModal components.
* Updated Dropdown component to accept new props for card identification.
2026-02-06 23:34:23 +00:00
Charity
f9eee53ed0 fix: board and workspace URL links and copy behavior (#357)
- Use /boards/{id} for private board links so copied link works; keep pretty URL for public boards
- Show matching path in board header (pretty path for public, boards/id for private)
- Copy link button copies URL to clipboard instead of opening in new tab (opening a new tab seems redundant since we're already on it)
- Edit workspace URL button links to /settings/workspace instead of account settings
2026-02-06 23:31:08 +00:00
Henry
1ab4e24c96 docs: add railway deployment template to self hosting docs (#363) 2026-02-05 21:52:40 +00:00
Henry
34165b390f fix: handle missing board in board router 2026-02-04 22:56:17 +00:00
Henry
ef0d53db8f refactor: standardise S3 URL generation (#362)
* refactor: replace presigned URL uploads with backend upload endpoints

* feat: update avatar upload to use new endpoint

* refactor: use createS3Client in auth hooks

* feat: generate presigned URLs for avatars

* fix: show avatar image in user menu

* fix: hide tooltip if content is empty

* fix: support external avatar URLs in generateAvatarUrl

* fix: remove content type restriction on attachments
2026-02-04 22:40:14 +00:00
Henry
78b9de869f fix: prevent validation errors in workspace/card queries (#356)
* fix: prevent workspace.byId validation errors when publicId is empty

* fix: add keys to shortcut elements

* fix: prevent card.byId validation errors when cardPublicId is empty

* chore: remove invalid config option from next.config.js
2026-02-02 22:25:54 +00:00
Henry
81c03b51e2 fix: fallback to email when user name is missing in activity list (#355)
* fix: fallback to email when user name is missing in activity list

* chore: gen translations
2026-02-02 22:00:03 +00:00
Henry
0d3d9a358f fix: prevent ellipses from showing through member dropdown menu (#352) 2026-02-02 20:05:53 +00:00
88 changed files with 10916 additions and 1533 deletions

View File

@@ -47,7 +47,17 @@ See our [roadmap](https://kan.bn/kan/roadmap) for upcoming features.
## Self Hosting 🐳
The easiest way to self-host Kan is with Docker Compose. This will set up everything for you including your postgres database.
### One-click Deployments
The easiest way to deploy Kan is through Railway. We've partnered with Railway to maintain an official template that supports the development of the project.
<a href="https://railway.com/deploy/kan?referralCode=bZPsr2&utm_medium=integration&utm_source=template&utm_campaign=generic">
<img src="https://railway.app/button.svg" alt="Deploy on Railway" height="40" />
</a>
### Docker Compose
Alternatively, you can self-host Kan with Docker Compose. This will set up everything for you including your postgres database.
1. Create a new file called `docker-compose.yml` and paste the following configuration:

View File

@@ -5,6 +5,8 @@ checksums:
"%2C%20or%20see%20our/singular": e8a8f2a078fcaea03481710dcaccce5d
"%7B0%7D/singular": 18782768c8a0b3c2078747b34fbbcd24
"%7B0%7D%20%7C%20%7B1%7D/singular": 4628f9c6ec13c2dc2984a6ab80e19f8b
"%7B0%7D%20has%20been%20added%20to%20your%20favorites./singular": 0f752180183c80703b3f9f39dc97195f
"%7B0%7D%20has%20been%20removed%20from%20your%20favorites./singular": 26e4b4997ad8a711d0b3e46337799454
"%7B0%7D%20labels/singular": 785f92bedf786ff3e285adfc07c69d18
"%7B0%7D%20not%20found/singular": a2665fc66f128fee1a80ffec61c9f1a1
"%7BboardCount%2C%20plural%2C%20one%20%7BImport%20board%20(1)%7D%20other%20%7BImport%20boards%20(%7BboardCount%7D)%7D%7D/singular": 72358c550bd1a99fcb6e8952f0f947ef
@@ -27,6 +29,7 @@ checksums:
Add%20details.../singular: 2f42547fd5d199f173aa7a88c8a9b19a
Add%20label/singular: 0be732d46df263265935fda342097a08
Add%20member/singular: 11979625770516ca287e929381778e02
Add%20to%20favorites/singular: 532155acb0a46f2b6c37e15afb3ad21e
added%20%7B0%7D%20labels%3A%20%3C0%3E%7BlabelList%7D%3C%2F0%3E/singular: 42b0488d570626dc887c58ff669c953c
added%20a%20checklist/singular: 44304f4a5ef3a46378eea243b5d2df79
added%20a%20checklist%20item/singular: 20bc0330dba5a9ec978e04bec174365a
@@ -35,6 +38,7 @@ checksums:
added%20checklist%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: fb6a852c50c1629f47c6f155fc93fdee
added%20checklist%20item%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: bdd202da20b1fffbec21792c5453f90c
added%20label%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: b32be052b3d57de0c9120fa7f9fc86ee
Added%20to%20favorites/singular: 848046ec65d15913112b466c544722eb
Adding%20a%20new%20member%20will%20cost%20an%20additional%20%7Bprice%7D%20(%7BbillingType%7D)%20per%20seat./singular: 12e88573028306110fbc15ef1e714892
Adjust%20the%20square%20crop%20to%20fit%20your%20avatar./singular: a4df26bbce6f14c6962fac1324db00a8
Admin/singular: 90eb20f1400db82ab874744e47836dc6
@@ -106,7 +110,6 @@ checksums:
Can%20edit%20member%20roles%20and%20permissions/singular: c364e8e8866111a471f4081db0730049
Can%20edit%20workspace/singular: 9768f990844e215c06910fed250da23a
Can%20invite%20members/singular: e02e562bcb7f46008d9595e485951413
can%20manage%20workspace%20settings/singular: 78bfe1746f961f37b1cde85e5a0e9a13
Can%20manage%20workspace%20settings/singular: 27eb18d3be5b3d813996b7d5071e0317
Can%20remove%20members/singular: 97a452b0fb4b661eaca7e5b3d5b49dcd
Can%20view%20boards/singular: 14977bbbb566f72fc74e17d99bad09bb
@@ -462,10 +465,12 @@ checksums:
Recruitment/singular: 78084ee4954aa22efe9ad7463d2a0c72
Remote/singular: dc3e4280dfe5c455b38ba6c8884999cf
Remove/singular: dba2fe5fe9f83f8078c687f28cba4b52
Remove%20from%20favorites/singular: 74d9cc2d2f3c7a7dc42a7e62c8ee06d5
Remove%20member/singular: 1d77c2ca3768e486fd1fb5df30cb60d9
removed%20%7B0%7D%20labels%3A%20%3C0%3E%7BlabelList%7D%3C%2F0%3E/singular: 612fcfd4aca6818fc870f81d9687d63f
removed%20a%20label%20from%20the%20card/singular: f83ec18850a2a8a3f8242a740a04df06
removed%20a%20member%20from%20the%20card/singular: 0bd7561bb79358fde3d595c3e60a315a
Removed%20from%20favorites/singular: 540609ab686ed67b1cfd7649467b8bc1
removed%20label%20%3C0%3E%7B0%7D%3C%2F0%3E/singular: 7e0936b15831e65754588b8512f62fcb
removed%20the%20due%20date/singular: 000c084844718540fcad542dd8caf8b4
renamed%20a%20checklist/singular: 4d208de1857740e63469007c5b8b491a
@@ -581,6 +586,7 @@ checksums:
Unable%20to%20reorder%20checklist%20item/singular: dcc238c72b85daf74ebd46adcd914473
Unable%20to%20reset%20permissions/singular: fb09d88ff4eb32afb852d733bafd515b
Unable%20to%20send%20feedback/singular: 656c93265d7e2bef1245b17d85f85ae5
Unable%20to%20update%20board/singular: 62899897d7bffcb1df3af77d460f0881
Unable%20to%20update%20board%20URL/singular: 080746884059142358b58d9a44ff7d93
Unable%20to%20update%20board%20visibility/singular: a76a21d561b8943e9276e027a1e3f70d
Unable%20to%20update%20card/singular: eca6002e57af8ec97324eae64e3b5731

View File

@@ -53,7 +53,6 @@ const config = {
{
protocol: 'https',
hostname: 'cdn.discordapp.com',
pathname: '/avatars/**',
},
];
@@ -95,12 +94,6 @@ const config = {
swcPlugins: [["@lingui/swc-plugin", {}]],
},
api: {
bodyParser: {
sizeLimit: env("NEXT_API_BODY_SIZE_LIMIT") || '1mb',
},
},
async rewrites() {
return [
{

View File

@@ -20,6 +20,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.802.0",
"@aws-sdk/lib-storage": "^3.802.0",
"@aws-sdk/s3-request-presigner": "^3.812.0",
"@headlessui/react": "^2.2.0",
"@hookform/resolvers": "^3.3.4",
@@ -38,6 +39,7 @@
"@tiptap/extension-link": "^2.22.2",
"@tiptap/extension-mention": "^3.0.9",
"@tiptap/extension-placeholder": "^2.14.0",
"@tiptap/extension-typography": "^3.18.0",
"@tiptap/pm": "^2.14.0",
"@tiptap/react": "^2.14.0",
"@tiptap/starter-kit": "^2.14.0",

View File

@@ -361,8 +361,8 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
})}
</div>
)}
<form onSubmit={handleSubmit(onSubmit)}>
{!isCredentialsEnabled && socialProviders?.length === 0 && (
{!(isCredentialsEnabled || isMagicLinkAvailable) &&
socialProviders?.length === 0 && (
<div className="flex w-full items-center gap-4">
<div className="h-[1px] w-1/3 bg-light-600 dark:bg-dark-600" />
<span className="text-center text-sm text-light-900 dark:text-dark-900">
@@ -371,65 +371,62 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
<div className="h-[1px] w-1/3 bg-light-600 dark:bg-dark-600" />
</div>
)}
{!isCredentialsEnabled && socialProviders?.length !== 0 && (
<div className="mb-[1.5rem] flex w-full items-center gap-4">
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
<span className="text-sm text-light-900 dark:text-dark-900">
{t`or`}
</span>
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
</div>
)}
<div className="space-y-2">
{isSignUp && isCredentialsEnabled && (
<div>
<Input
{...register("name", { required: true })}
placeholder={t`Enter your name`}
/>
{errors.name && (
<p className="mt-2 text-xs text-red-400">
{t`Please enter a valid name`}
</p>
)}
{(isCredentialsEnabled || isMagicLinkAvailable) && (
<form onSubmit={handleSubmit(onSubmit)}>
{socialProviders?.length !== 0 && (
<div className="mb-[1.5rem] flex w-full items-center gap-4">
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
<span className="text-sm text-light-900 dark:text-dark-900">
{t`or`}
</span>
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
</div>
)}
{(isCredentialsEnabled || isMagicLinkAvailable) && (
<>
<div className="space-y-2">
{isSignUp && isCredentialsEnabled && (
<div>
<Input
{...register("email", { required: true })}
placeholder={t`Enter your email address`}
{...register("name", { required: true })}
placeholder={t`Enter your name`}
/>
{errors.email && (
{errors.name && (
<p className="mt-2 text-xs text-red-400">
{t`Please enter a valid email address`}
{t`Please enter a valid name`}
</p>
)}
</div>
{isCredentialsEnabled && (
<div>
<Input
type="password"
{...register("password", { required: true })}
placeholder={t`Enter your password`}
/>
{errors.password && (
<p className="mt-2 text-xs text-red-400">
{errors.password.message ??
t`Please enter a valid password`}
</p>
)}
</div>
)}
<div>
<Input
{...register("email", { required: true })}
placeholder={t`Enter your email address`}
/>
{errors.email && (
<p className="mt-2 text-xs text-red-400">
{t`Please enter a valid email address`}
</p>
)}
</>
)}
{loginError && (
<p className="mt-2 text-xs text-red-400">{loginError}</p>
)}
</div>
{(isCredentialsEnabled || isMagicLinkAvailable) && (
</div>
{isCredentialsEnabled && (
<div>
<Input
type="password"
{...register("password", { required: true })}
placeholder={t`Enter your password`}
/>
{errors.password && (
<p className="mt-2 text-xs text-red-400">
{errors.password.message ??
t`Please enter a valid password`}
</p>
)}
</div>
)}
{loginError && (
<p className="mt-2 text-xs text-red-400">{loginError}</p>
)}
</div>
<div className="mt-[1.5rem] flex items-center gap-4">
<Button
isLoading={isLoginWithEmailPending}
@@ -441,8 +438,11 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
{isMagicLinkMode ? t`magic link` : t`email`}
</Button>
</div>
)}
</form>
</form>
)}
{!(isCredentialsEnabled || isMagicLinkAvailable) && loginError && (
<p className="mt-2 text-xs text-red-400">{loginError}</p>
)}
</div>
);
}

View File

@@ -25,7 +25,7 @@ const Avatar = ({
icon?: React.ReactNode;
isLoading?: boolean;
}) => {
const initials = name
const initials = name?.trim()
? getInitialsFromName(name)
: inferInitialsFromEmail(email);

View File

@@ -12,6 +12,7 @@ import { authClient } from "@kan/auth/client";
import { useClickOutside } from "~/hooks/useClickOutside";
import { useModal } from "~/providers/modal";
import { useWorkspace, WorkspaceProvider } from "~/providers/workspace";
import { api } from "~/utils/api";
import SideNavigation from "./SideNavigation";
interface DashboardProps {
@@ -44,6 +45,12 @@ export default function Dashboard({
const { availableWorkspaces, hasLoaded } = useWorkspace();
const { data: session, isPending: sessionLoading } = authClient.useSession();
const { data: user, isLoading: userLoading } = api.user.getUser.useQuery(
undefined,
{
enabled: !!session?.user,
},
);
const [isSideNavOpen, setIsSideNavOpen] = useState(false);
const [isRightPanelOpen, setIsRightPanelOpen] = useState(false);
@@ -155,8 +162,12 @@ export default function Dashboard({
className={`fixed top-12 z-40 h-[calc(100dvh-3rem)] w-[calc(100vw-1.5rem)] transform transition-transform duration-300 ease-in-out md:relative md:top-0 md:h-full md:w-auto md:translate-x-0 ${isSideNavOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"} `}
>
<SideNavigation
user={{ displayName: session?.user.name, email: session?.user.email, image: session?.user.image }}
isLoading={sessionLoading}
user={{
displayName: user?.name ?? session?.user.name,
email: user?.email ?? session?.user.email ?? "",
image: user?.image ?? undefined,
}}
isLoading={sessionLoading || userLoading}
onCloseSideNav={closeSideNav}
/>
</div>

View File

@@ -17,6 +17,7 @@ import {
ReactRenderer,
useEditor,
} from "@tiptap/react";
import Typography from "@tiptap/extension-typography";
import StarterKit from "@tiptap/starter-kit";
import Suggestion from "@tiptap/suggestion";
import {
@@ -386,46 +387,54 @@ export interface SlashNodeAttrs {
label?: string | null;
}
const CommandItems: SlashCommandItem[] = [
{
title: "Heading 1",
icon: <HiH1 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 1 }).run(),
},
{
title: "Heading 2",
icon: <HiH2 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 2 }).run(),
},
{
title: "Heading 3",
icon: <HiH3 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 3 }).run(),
},
{
title: "Bullet List",
icon: <HiOutlineListBullet />,
command: ({ editor }) => editor.chain().focus().toggleBulletList().run(),
},
{
title: "Ordered List",
icon: <HiOutlineNumberedList />,
command: ({ editor }) => editor.chain().focus().toggleOrderedList().run(),
},
{
title: "Blockquote",
icon: <HiOutlineChatBubbleLeftEllipsis />,
command: ({ editor }) => editor.chain().focus().toggleBlockquote().run(),
},
{
title: "Code Block",
icon: <HiOutlineCodeBracketSquare />,
command: ({ editor }) => editor.chain().focus().toggleCodeBlock().run(),
},
];
const getCommandItems = (disableHeadings: boolean): SlashCommandItem[] => {
const headingCommands: SlashCommandItem[] = disableHeadings
? []
: [
{
title: "Heading 1",
icon: <HiH1 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 1 }).run(),
},
{
title: "Heading 2",
icon: <HiH2 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 2 }).run(),
},
{
title: "Heading 3",
icon: <HiH3 />,
command: ({ editor }) =>
editor.chain().focus().setHeading({ level: 3 }).run(),
},
];
return [
...headingCommands,
{
title: "Bullet List",
icon: <HiOutlineListBullet />,
command: ({ editor }) => editor.chain().focus().toggleBulletList().run(),
},
{
title: "Ordered List",
icon: <HiOutlineNumberedList />,
command: ({ editor }) => editor.chain().focus().toggleOrderedList().run(),
},
{
title: "Blockquote",
icon: <HiOutlineChatBubbleLeftEllipsis />,
command: ({ editor }) => editor.chain().focus().toggleBlockquote().run(),
},
{
title: "Code Block",
icon: <HiOutlineCodeBracketSquare />,
command: ({ editor }) => editor.chain().focus().toggleCodeBlock().run(),
},
];
};
export default function Editor({
content,
@@ -434,6 +443,8 @@ export default function Editor({
readOnly = false,
workspaceMembers,
enableYouTubeEmbed = true,
placeholder,
disableHeadings = false,
}: {
content: string | null;
onChange?: (value: string) => void;
@@ -441,18 +452,23 @@ export default function Editor({
readOnly?: boolean;
workspaceMembers: WorkspaceMember[];
enableYouTubeEmbed?: boolean;
placeholder?: string;
disableHeadings?: boolean;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const editor = useEditor(
{
extensions: [
StarterKit,
StarterKit.configure({
heading: disableHeadings ? false : undefined,
}),
Markdown,
Placeholder.configure({
placeholder: readOnly
? ""
: t`Add description... (type '/' to open commands or '@' to mention)`,
: placeholder ??
t`Add description... (type '/' to open commands or '@' to mention)`,
}),
Link.configure({
openOnClick: true,
@@ -465,10 +481,10 @@ export default function Editor({
autolink: true,
}),
SlashCommands.configure({
commandItems: CommandItems,
commandItems: getCommandItems(disableHeadings),
suggestion: {
items: ({ query }: { query: string }) =>
filterSlashCommandItems(CommandItems, query),
filterSlashCommandItems(getCommandItems(disableHeadings), query),
startOfLine: true,
char: "/",
},
@@ -480,20 +496,28 @@ export default function Editor({
suggestion: {
char: "@",
items: ({ query }: { query: string }) => {
const all: MentionItem[] = workspaceMembers.map(
(member: WorkspaceMember) => ({
id: member.publicId,
label: member?.user?.name ?? member.email,
image: member?.user?.image ?? null,
}),
const withEmail = workspaceMembers.filter((member) => member.email);
const mapped = withEmail.map((member: WorkspaceMember) => ({
id: member.publicId,
label: member?.user?.name?.trim() || member.email || "",
image: member?.user?.image ?? null,
}));
const all: MentionItem[] = mapped.filter(
(item) => item.label && item.label.length > 0,
);
const q = query.toLowerCase();
return all.filter(
(u) =>
u.label &&
typeof u.label === "string" &&
u.label.toLowerCase().includes(q),
const q = query.toLowerCase().trim();
if (q === "") {
return all;
}
const filtered = all.filter((u) =>
u.label.toLowerCase().includes(q),
);
return filtered;
},
command: ({ editor, range, props }) => {
const id = props.id ?? "";
@@ -514,6 +538,17 @@ export default function Editor({
return `${options.suggestion.char}${node.attrs.label ?? node.attrs.id}`;
},
}),
Typography.configure({
openDoubleQuote: false,
closeDoubleQuote: false,
openSingleQuote: false,
closeSingleQuote: false,
oneHalf: false,
oneQuarter: false,
threeQuarters: false,
superscriptTwo: false,
superscriptThree: false,
}),
...(enableYouTubeEmbed ? [YouTubeNode] : []),
],
content,

View File

@@ -55,9 +55,10 @@ export default function SideNavigation({
const [isInitialised, setIsInitialised] = useState(false);
const { openModal } = useModal();
const { data: workspaceData } = api.workspace.byId.useQuery({
workspacePublicId: workspace.publicId,
});
const { data: workspaceData } = api.workspace.byId.useQuery(
{ workspacePublicId: workspace.publicId },
{ enabled: !!workspace.publicId && workspace.publicId.length >= 12 },
);
const subscriptions = workspaceData?.subscriptions as
| Subscription[]

View File

@@ -7,7 +7,7 @@ import tippy from "tippy.js";
interface TooltipProps {
children: ReactNode;
content: ReactNode;
content?: ReactNode;
placement?: Placement;
delay?: number | [number, number];
}
@@ -24,6 +24,8 @@ export function Tooltip({
useEffect(() => {
if (!triggerRef.current) return;
if (!content) return;
const container = document.createElement("div");
const root = createRoot(container);
rootRef.current = root;

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} wurde zu Ihren Favoriten hinzugefügt."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} wurde aus Ihren Favoriten entfernt."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Label hinzufügen"
msgid "Add member"
msgstr "Mitglied hinzufügen"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Zu Favoriten hinzufügen"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "{0} Labels hinzugefügt: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "hat eine Checkliste hinzugefügt"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "hat ein Checklistenelement hinzugefügt"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "hat ein Label zur Karte hinzugefügt"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "hat ein Mitglied zur Karte hinzugefügt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "hat Checkliste <0>{0}</0> hinzugefügt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "hat Checklistenelement <0>{0}</0> hinzugefügt"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "hat Label <0>{0}</0> hinzugefügt"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Zu Favoriten hinzugefügt"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Das Hinzufügen eines neuen Mitglieds kostet zusätzlich {price} ({billingType}) pro Platz."
@@ -282,8 +300,8 @@ msgstr "Sind Sie sicher, dass Sie dieses Label löschen möchten?"
msgid "Are you sure you want to delete your account?"
msgstr "Bist du sicher, dass du dein Konto löschen möchtest?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "hat <0>{0}</0> der Karte zugewiesen"
@@ -562,8 +580,8 @@ msgstr "Passwort ändern"
msgid "Change your language preferences."
msgstr "Ändern Sie Ihre Spracheinstellungen."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "hat den Fälligkeitstermin auf <0>{formattedDate}</0> geändert"
@@ -651,12 +669,12 @@ msgstr "Abgeschlossen"
msgid "Complete control and ownership:"
msgstr "Vollständige Kontrolle und Eigentum:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "hat ein Checklistenelement abgeschlossen"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "hat Checklistenelement <0>{0}</0> abgeschlossen"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Liste erstellen"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Neue(n) {0} erstellen"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Neues Label erstellen"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Neue Liste erstellen"
@@ -783,7 +801,7 @@ msgstr "Vorlage erstellen"
msgid "Create workspace"
msgstr "Arbeitsbereich erstellen"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "hat die Karte erstellt"
@@ -857,7 +875,7 @@ msgstr "Löschen"
msgid "Delete account"
msgstr "Konto löschen"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Board löschen"
@@ -873,7 +891,7 @@ msgstr "Kommentar löschen"
msgid "Delete list"
msgstr "Liste löschen"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Vorlage löschen"
@@ -883,21 +901,21 @@ msgstr "Vorlage löschen"
msgid "Delete workspace"
msgstr "Workspace löschen"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "hat eine Checkliste gelöscht"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "hat ein Checklistenelement gelöscht"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "hat Checkliste <0>{0}</0> gelöscht"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "hat Checklistenelement <0>{0}</0> gelöscht"
@@ -993,7 +1011,7 @@ msgstr "Morgen fällig"
msgid "Edit"
msgstr "Bearbeiten"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Board-URL bearbeiten"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Loslegen"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Beginnen Sie, indem Sie eine(n) neue(n) {0} erstellen"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Beginne mit dem Erstellen einer neuen Liste"
@@ -1595,7 +1613,7 @@ msgstr "Listenname"
msgid "Lists"
msgstr "Listen"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Weitere Aktivitäten laden"
@@ -1603,7 +1621,7 @@ msgstr "Weitere Aktivitäten laden"
msgid "Loading permissions..."
msgstr "Berechtigungen werden geladen..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Wird geladen..."
@@ -1631,16 +1649,16 @@ msgstr "Niedrige Priorität"
msgid "magic link"
msgstr "Magic Link"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Als Vorlage speichern"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "hat ein Checklistenelement als unvollständig markiert"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "hat Checklistenelement <0>{0}</0> als unvollständig markiert"
@@ -1648,6 +1666,8 @@ msgstr "hat Checklistenelement <0>{0}</0> als unvollständig markiert"
msgid "Medium Priority"
msgstr "Mittlere Priorität"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Mitglied"
@@ -1676,11 +1696,11 @@ msgstr "monatliche Abrechnung"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "hat die Karte von <0>{0}</0> nach <1>{1}</1> verschoben"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "hat die Karte in eine andere Liste verschoben"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Nächste Schritte"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Keine {0}"
@@ -1790,11 +1810,11 @@ msgstr "Für diesen Anhang ist keine Download-URL verfügbar."
msgid "No keyboard shortcuts registered."
msgstr "Keine Tastaturkürzel registriert."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Keine Listen"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Es wurden noch keine Listen erstellt"
@@ -1969,6 +1989,7 @@ msgstr "Bitte wähle eine Datei zum Hochladen aus."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Remote"
msgid "Remove"
msgstr "Entfernen"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Aus Favoriten entfernen"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Mitglied entfernen"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "{0} Labels entfernt: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "hat ein Label von der Karte entfernt"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "hat ein Mitglied von der Karte entfernt"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Aus Favoriten entfernt"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "hat Label <0>{0}</0> entfernt"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "hat den Fälligkeitstermin entfernt"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "hat eine Checkliste umbenannt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "hat Checkliste <0>{0}</0> umbenannt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "hat Checklistenelement in <0>{0}</0> umbenannt"
@@ -2219,7 +2248,7 @@ msgstr "Selbst hosten"
msgid "Self host with Github"
msgstr "Selbst hosten mit Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "hat sich selbst die Karte zugewiesen"
@@ -2239,7 +2268,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Fälligkeitsdatum festlegen"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "hat den Fälligkeitstermin festgelegt"
@@ -2603,6 +2632,10 @@ msgstr "Berechtigungen können nicht zurückgesetzt werden"
msgid "Unable to send feedback"
msgstr "Feedback konnte nicht gesendet werden"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Tafel konnte nicht aktualisiert werden"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Board-URL kann nicht aktualisiert werden"
@@ -2654,12 +2687,12 @@ msgstr "Berechtigungen können nicht aktualisiert werden"
msgid "Unable to update role"
msgstr "Rolle kann nicht aktualisiert werden"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "hat <0>{0}</0> von der Karte entfernt"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "hat sich selbst von der Karte entfernt"
@@ -2718,24 +2751,24 @@ msgstr "Aktualisieren"
msgid "Update label"
msgstr "Label aktualisieren"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "hat ein Checklistenelement aktualisiert"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "hat die Beschreibung aktualisiert"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "hat den Fälligkeitstermin aktualisiert"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "hat den Titel aktualisiert"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "hat den Titel in <0>{0}</0> geändert"
@@ -2860,7 +2893,7 @@ msgstr "Warum ein open source Trello entwickeln?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Arbeitsbereich"
@@ -2965,8 +2998,8 @@ msgstr "Sie können selbst hosten, indem sie den anweisungen in unserem <0>repo<
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} has been added to your favorites."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} has been removed from your favorites."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -147,39 +157,43 @@ msgstr "Add label"
msgid "Add member"
msgstr "Add member"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Add to favorites"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "added {0} labels: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "added a checklist"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "added a checklist item"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "added a label to the card"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "added a member to the card"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "added checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "added checklist item <0>{0}</0>"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "added label <0>{0}</0>"
@@ -187,6 +201,10 @@ msgstr "added label <0>{0}</0>"
#~ msgid "added label <0>{label}</0>"
#~ msgstr "added label <0>{label}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Added to favorites"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Adding a new member will cost an additional {price} ({billingType}) per seat."
@@ -314,8 +332,8 @@ msgstr "Are you sure you want to delete your account?"
#~ msgid "around the world"
#~ msgstr "around the world"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "assigned <0>{0}</0> to the card"
@@ -623,8 +641,8 @@ msgstr "Change Password"
msgid "Change your language preferences."
msgstr "Change your language preferences."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "changed the due date to <0>{formattedDate}</0>"
@@ -712,12 +730,12 @@ msgstr "Complete"
msgid "Complete control and ownership:"
msgstr "Complete control and ownership:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "completed a checklist item"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "completed checklist item <0>{0}</0>"
@@ -828,7 +846,7 @@ msgid "Create list"
msgstr "Create list"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Create new {0}"
@@ -847,7 +865,7 @@ msgid "Create new label"
msgstr "Create new label"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Create new list"
@@ -864,7 +882,7 @@ msgstr "Create template"
msgid "Create workspace"
msgstr "Create workspace"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "created the card"
@@ -946,7 +964,7 @@ msgstr "Delete"
msgid "Delete account"
msgstr "Delete account"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Delete board"
@@ -962,7 +980,7 @@ msgstr "Delete comment"
msgid "Delete list"
msgstr "Delete list"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Delete template"
@@ -972,21 +990,21 @@ msgstr "Delete template"
msgid "Delete workspace"
msgstr "Delete workspace"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "deleted a checklist"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "deleted a checklist item"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "deleted checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "deleted checklist item <0>{0}</0>"
@@ -1082,7 +1100,7 @@ msgstr "Due tomorrow"
msgid "Edit"
msgstr "Edit"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Edit board URL"
@@ -1378,7 +1396,7 @@ msgid "Get Started"
msgstr "Get Started"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Get started by creating a new {0}"
@@ -1386,7 +1404,7 @@ msgstr "Get started by creating a new {0}"
#~ msgid "Get started by creating a new board"
#~ msgstr "Get started by creating a new board"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Get started by creating a new list"
@@ -1704,7 +1722,7 @@ msgstr "List name"
msgid "Lists"
msgstr "Lists"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Load more activities"
@@ -1712,7 +1730,7 @@ msgstr "Load more activities"
msgid "Loading permissions..."
msgstr "Loading permissions..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Loading..."
@@ -1740,16 +1758,16 @@ msgstr "Low Priority"
msgid "magic link"
msgstr "magic link"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Make template"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "marked a checklist item as incomplete"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "marked checklist item <0>{0}</0> as incomplete"
@@ -1757,6 +1775,8 @@ msgstr "marked checklist item <0>{0}</0> as incomplete"
msgid "Medium Priority"
msgstr "Medium Priority"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Member"
@@ -1785,7 +1805,7 @@ msgstr "monthly billing"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "moved the card from <0>{0}</0> to<1>{1}</1>"
@@ -1793,7 +1813,7 @@ msgstr "moved the card from <0>{0}</0> to<1>{1}</1>"
#~ msgid "moved the card from <0>{fromList}</0> to<1>{toList}</1>"
#~ msgstr "moved the card from <0>{fromList}</0> to<1>{toList}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "moved the card to another list"
@@ -1887,7 +1907,7 @@ msgid "Next Steps"
msgstr "Next Steps"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "No {0}"
@@ -1919,11 +1939,11 @@ msgstr "No download URL available for this attachment."
msgid "No keyboard shortcuts registered."
msgstr "No keyboard shortcuts registered."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "No lists"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "No lists have been created yet"
@@ -2098,6 +2118,7 @@ msgstr "Please select a file to upload."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2225,25 +2246,33 @@ msgstr "Remote"
msgid "Remove"
msgstr "Remove"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Remove from favorites"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Remove member"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "removed {0} labels: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "removed a label from the card"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "removed a member from the card"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Removed from favorites"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "removed label <0>{0}</0>"
@@ -2251,22 +2280,22 @@ msgstr "removed label <0>{0}</0>"
#~ msgid "removed label <0>{label}</0>"
#~ msgstr "removed label <0>{label}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "removed the due date"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "renamed a checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "renamed checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "renamed checklist item to <0>{0}</0>"
@@ -2356,7 +2385,7 @@ msgstr "Self Host"
msgid "Self host with Github"
msgstr "Self host with Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "self-assigned the card"
@@ -2380,7 +2409,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Set due date"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "set the due date"
@@ -2760,6 +2789,10 @@ msgstr "Unable to reset permissions"
msgid "Unable to send feedback"
msgstr "Unable to send feedback"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Unable to update board"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Unable to update board URL"
@@ -2811,8 +2844,8 @@ msgstr "Unable to update permissions"
msgid "Unable to update role"
msgstr "Unable to update role"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "unassigned <0>{0}</0> from the card"
@@ -2820,7 +2853,7 @@ msgstr "unassigned <0>{0}</0> from the card"
#~ msgid "unassigned <0>{memberName}</0> from the card"
#~ msgstr "unassigned <0>{memberName}</0> from the card"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "unassigned themselves from the card"
@@ -2879,24 +2912,24 @@ msgstr "Update"
msgid "Update label"
msgstr "Update label"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "updated a checklist item"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "updated the description"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "updated the due date"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "updated the title"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "updated the title to <0>{0}</0>"
@@ -3041,7 +3074,7 @@ msgstr "Why make an open source Trello?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Workspace"
@@ -3150,8 +3183,8 @@ msgstr "You can self-host by following the instructions in our <0>repo</0>."
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} se ha añadido a tus favoritos."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} se ha eliminado de tus favoritos."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Añadir etiqueta"
msgid "Add member"
msgstr "Añadir miembro"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Añadir a favoritos"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "añadió {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "añadió una lista de verificación"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "añadió un elemento a la lista de verificación"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "añadió una etiqueta a la tarjeta"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "añadió un miembro a la tarjeta"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "añadió la lista de verificación <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "añadió el elemento <0>{0}</0> a la lista de verificación"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "añadió la etiqueta <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Añadido a favoritos"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Añadir un nuevo miembro costará {price} adicionales ({billingType}) por asiento."
@@ -282,8 +300,8 @@ msgstr "¿Estás seguro de que quieres eliminar esta etiqueta?"
msgid "Are you sure you want to delete your account?"
msgstr "¿Estás seguro de que quieres eliminar tu cuenta?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "asignó <0>{0}</0> a la tarjeta"
@@ -562,8 +580,8 @@ msgstr "Cambiar contraseña"
msgid "Change your language preferences."
msgstr "Cambia tus preferencias de idioma."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "cambió la fecha de vencimiento a <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Completado"
msgid "Complete control and ownership:"
msgstr "Control y propiedad completos:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "completó un elemento de la lista de verificación"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "completó el elemento <0>{0}</0> de la lista de verificación"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Crear lista"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Crear nuevo {0}"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Crear nueva etiqueta"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Crear nueva lista"
@@ -783,7 +801,7 @@ msgstr "Crear plantilla"
msgid "Create workspace"
msgstr "Crear espacio de trabajo"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "creó la tarjeta"
@@ -857,7 +875,7 @@ msgstr "Eliminar"
msgid "Delete account"
msgstr "Eliminar cuenta"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Eliminar tablero"
@@ -873,7 +891,7 @@ msgstr "Eliminar comentario"
msgid "Delete list"
msgstr "Eliminar lista"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Eliminar plantilla"
@@ -883,21 +901,21 @@ msgstr "Eliminar plantilla"
msgid "Delete workspace"
msgstr "Eliminar espacio de trabajo"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "eliminó una lista de verificación"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "eliminó un elemento de la lista de verificación"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "eliminó la lista de verificación <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "eliminó el elemento <0>{0}</0> de la lista de verificación"
@@ -993,7 +1011,7 @@ msgstr "Vence mañana"
msgid "Edit"
msgstr "Editar"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Editar URL del tablero"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Comenzar"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Comienza creando un nuevo {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Comienza creando una nueva lista"
@@ -1595,7 +1613,7 @@ msgstr "Nombre de la lista"
msgid "Lists"
msgstr "Listas"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Cargar más actividades"
@@ -1603,7 +1621,7 @@ msgstr "Cargar más actividades"
msgid "Loading permissions..."
msgstr "Cargando permisos..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Cargando..."
@@ -1631,16 +1649,16 @@ msgstr "Prioridad baja"
msgid "magic link"
msgstr "enlace mágico"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Crear plantilla"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "marcó un elemento de la lista de verificación como incompleto"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "marcó el elemento <0>{0}</0> de la lista de verificación como incompleto"
@@ -1648,6 +1666,8 @@ msgstr "marcó el elemento <0>{0}</0> de la lista de verificación como incomple
msgid "Medium Priority"
msgstr "Prioridad media"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Miembro"
@@ -1676,11 +1696,11 @@ msgstr "facturación mensual"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "movió la tarjeta de <0>{0}</0> a<1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "movió la tarjeta a otra lista"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Próximos pasos"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Sin {0}"
@@ -1790,11 +1810,11 @@ msgstr "No hay URL de descarga disponible para este archivo adjunto."
msgid "No keyboard shortcuts registered."
msgstr "No hay atajos de teclado registrados."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Sin listas"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Aún no se han creado listas"
@@ -1969,6 +1989,7 @@ msgstr "Por favor selecciona un archivo para subir."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Remoto"
msgid "Remove"
msgstr "Eliminar"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Eliminar de favoritos"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Eliminar miembro"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "eliminó {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "eliminó una etiqueta de la tarjeta"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "eliminó un miembro de la tarjeta"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Eliminado de favoritos"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "eliminó la etiqueta <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "eliminó la fecha de vencimiento"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "renombró una lista de verificación"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "renombró la lista de verificación <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "renombró el elemento de la lista de verificación a <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Autoalojamiento"
msgid "Self host with Github"
msgstr "Autoalojamiento con Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "se autoasignó la tarjeta"
@@ -2239,7 +2268,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Establecer fecha de vencimiento"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "estableció la fecha de vencimiento"
@@ -2603,6 +2632,10 @@ msgstr "No se pueden restablecer los permisos"
msgid "Unable to send feedback"
msgstr "No se pudieron enviar los comentarios"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "No se pudo actualizar el tablero"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "No se puede actualizar la URL del tablero"
@@ -2654,12 +2687,12 @@ msgstr "No se pueden actualizar los permisos"
msgid "Unable to update role"
msgstr "No se puede actualizar el rol"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "desasignó <0>{0}</0> de la tarjeta"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "se desasignó de la tarjeta"
@@ -2718,24 +2751,24 @@ msgstr "Actualizar"
msgid "Update label"
msgstr "Actualizar etiqueta"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "actualizó un elemento de la lista de verificación"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "actualizó la descripción"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "actualizó la fecha de vencimiento"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "actualizó el título"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "actualizó el título a <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "¿Por qué crear un Trello de código abierto?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Espacio de trabajo"
@@ -2965,8 +2998,8 @@ msgstr "Puedes autoalojar siguiendo las instrucciones en nuestro <0>repositorio<
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} a été ajouté à vos favoris."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} a été retiré de vos favoris."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Ajouter une étiquette"
msgid "Add member"
msgstr "Ajouter un membre"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Ajouter aux favoris"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "a ajouté {0} étiquettes : <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "a ajouté une checklist"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "a ajouté un élément à la checklist"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "a ajouté une étiquette à la carte"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "a ajouté un membre à la carte"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "a ajouté la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "a ajouté l'élément <0>{0}</0> à la checklist"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "a ajouté l'étiquette <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Ajouté aux favoris"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "L'ajout d'un nouveau membre coûtera {price} supplémentaires ({billingType}) par siège."
@@ -282,8 +300,8 @@ msgstr "Êtes-vous sûr de vouloir supprimer cette étiquette ?"
msgid "Are you sure you want to delete your account?"
msgstr "Êtes-vous sûr de vouloir supprimer votre compte ?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "a assigné <0>{0}</0> à la carte"
@@ -562,8 +580,8 @@ msgstr "Modifier le mot de passe"
msgid "Change your language preferences."
msgstr "Modifiez vos préférences linguistiques."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "a modifié la date d'échéance pour <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Terminé"
msgid "Complete control and ownership:"
msgstr "Contrôle et propriété complets :"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "a terminé un élément de la checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "a terminé l'élément <0>{0}</0> de la checklist"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Créer une liste"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Créer un nouveau {0}"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Créer une nouvelle étiquette"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Créer une nouvelle liste"
@@ -783,7 +801,7 @@ msgstr "Créer un modèle"
msgid "Create workspace"
msgstr "Créer un espace de travail"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "a créé la carte"
@@ -857,7 +875,7 @@ msgstr "Supprimer"
msgid "Delete account"
msgstr "Supprimer le compte"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Supprimer le tableau"
@@ -873,7 +891,7 @@ msgstr "Supprimer le commentaire"
msgid "Delete list"
msgstr "Supprimer la liste"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Supprimer le modèle"
@@ -883,21 +901,21 @@ msgstr "Supprimer le modèle"
msgid "Delete workspace"
msgstr "Supprimer l'espace de travail"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "a supprimé une checklist"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "a supprimé un élément de la checklist"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "a supprimé la checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "a supprimé l'élément <0>{0}</0> de la checklist"
@@ -993,7 +1011,7 @@ msgstr "Échéance demain"
msgid "Edit"
msgstr "Modifier"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Modifier l'URL du tableau"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Commencer"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Commencez par créer un nouveau {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Commencez par créer une nouvelle liste"
@@ -1595,7 +1613,7 @@ msgstr "Nom de la liste"
msgid "Lists"
msgstr "Listes"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Charger plus d'activités"
@@ -1603,7 +1621,7 @@ msgstr "Charger plus d'activités"
msgid "Loading permissions..."
msgstr "Chargement des permissions..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Chargement..."
@@ -1631,16 +1649,16 @@ msgstr "Priorité basse"
msgid "magic link"
msgstr "lien magique"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Créer un modèle"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "a marqué un élément de la checklist comme incomplet"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "a marqué l'élément <0>{0}</0> de la checklist comme incomplet"
@@ -1648,6 +1666,8 @@ msgstr "a marqué l'élément <0>{0}</0> de la checklist comme incomplet"
msgid "Medium Priority"
msgstr "Priorité moyenne"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Membre"
@@ -1676,11 +1696,11 @@ msgstr "facturation mensuelle"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "a déplacé la carte de <0>{0}</0> vers <1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "a déplacé la carte vers une autre liste"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Prochaines étapes"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Aucun {0}"
@@ -1790,11 +1810,11 @@ msgstr "Aucune URL de téléchargement disponible pour cette pièce jointe."
msgid "No keyboard shortcuts registered."
msgstr "Aucun raccourci clavier enregistré."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Aucune liste"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Aucune liste n'a encore été créée"
@@ -1969,6 +1989,7 @@ msgstr "Veuillez sélectionner un fichier à télécharger."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "À distance"
msgid "Remove"
msgstr "Supprimer"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Retirer des favoris"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Supprimer le membre"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "a supprimé {0} étiquettes : <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "a supprimé une étiquette de la carte"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "a retiré un membre de la carte"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Retiré des favoris"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "a supprimé l'étiquette <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "a supprimé la date d'échéance"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "a renommé une checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "a renommé la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "a renommé l'élément de la liste de contrôle en <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Auto-hébergement"
msgid "Self host with Github"
msgstr "Auto-hébergement avec Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "s'est auto-assigné la carte"
@@ -2239,7 +2268,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Définir la date d'échéance"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "a défini la date d'échéance"
@@ -2603,6 +2632,10 @@ msgstr "Impossible de réinitialiser les permissions"
msgid "Unable to send feedback"
msgstr "Impossible d'envoyer les commentaires"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Impossible de mettre à jour le tableau"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Impossible de mettre à jour l'URL du tableau"
@@ -2654,12 +2687,12 @@ msgstr "Impossible de mettre à jour les permissions"
msgid "Unable to update role"
msgstr "Impossible de mettre à jour le rôle"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "a désassigné <0>{0}</0> de la carte"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "s'est retiré de la carte"
@@ -2718,24 +2751,24 @@ msgstr "Mettre à jour"
msgid "Update label"
msgstr "Mettre à jour l'étiquette"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "a mis à jour un élément de la liste de contrôle"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "a mis à jour la description"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "a mis à jour la date d'échéance"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "a mis à jour le titre"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "a mis à jour le titre en <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "Pourquoi créer un Trello open source ?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Espace de travail"
@@ -2965,8 +2998,8 @@ msgstr "Vous pouvez auto-héberger en suivant les instructions dans notre <0>dé
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} è stato aggiunto ai tuoi preferiti."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} è stato rimosso dai tuoi preferiti."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Aggiungi etichetta"
msgid "Add member"
msgstr "Aggiungi membro"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Aggiungi ai preferiti"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "ha aggiunto {0} etichette: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "ha aggiunto una checklist"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "ha aggiunto un elemento alla checklist"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "ha aggiunto un'etichetta alla carta"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "ha aggiunto un membro alla carta"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "ha aggiunto la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "ha aggiunto l'elemento <0>{0}</0> alla checklist"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "ha aggiunto l'etichetta <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Aggiunto ai preferiti"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "L'aggiunta di un nuovo membro costerà un supplemento di {price} ({billingType}) per posto."
@@ -282,8 +300,8 @@ msgstr "Sei sicuro di voler eliminare questa etichetta?"
msgid "Are you sure you want to delete your account?"
msgstr "Sei sicuro di voler eliminare il tuo account?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "ha assegnato <0>{0}</0> alla scheda"
@@ -562,8 +580,8 @@ msgstr "Cambia password"
msgid "Change your language preferences."
msgstr "Modifica le tue preferenze di lingua."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "ha cambiato la data di scadenza a <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Completato"
msgid "Complete control and ownership:"
msgstr "Controllo e proprietà completi:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "ha completato un elemento della checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "ha completato l'elemento <0>{0}</0> della checklist"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Crea lista"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Crea nuovo {0}"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Crea nuova etichetta"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Crea nuova lista"
@@ -783,7 +801,7 @@ msgstr "Crea template"
msgid "Create workspace"
msgstr "Crea spazio di lavoro"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "ha creato la carta"
@@ -857,7 +875,7 @@ msgstr "Elimina"
msgid "Delete account"
msgstr "Elimina account"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Elimina bacheca"
@@ -873,7 +891,7 @@ msgstr "Elimina commento"
msgid "Delete list"
msgstr "Elimina lista"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Elimina template"
@@ -883,21 +901,21 @@ msgstr "Elimina template"
msgid "Delete workspace"
msgstr "Elimina spazio di lavoro"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "ha eliminato una checklist"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "ha eliminato un elemento della checklist"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "ha eliminato la checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "ha eliminato l'elemento <0>{0}</0> della checklist"
@@ -993,7 +1011,7 @@ msgstr "Scade domani"
msgid "Edit"
msgstr "Modifica"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Modifica URL della bacheca"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Inizia ora"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Inizia creando un nuovo {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Inizia creando una nuova lista"
@@ -1595,7 +1613,7 @@ msgstr "Nome lista"
msgid "Lists"
msgstr "Liste"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Carica altre attività"
@@ -1603,7 +1621,7 @@ msgstr "Carica altre attività"
msgid "Loading permissions..."
msgstr "Caricamento permessi..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Caricamento in corso..."
@@ -1631,16 +1649,16 @@ msgstr "Bassa priorità"
msgid "magic link"
msgstr "link magico"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Crea template"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "ha segnato un elemento della checklist come incompleto"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "ha segnato l'elemento <0>{0}</0> della checklist come incompleto"
@@ -1648,6 +1666,8 @@ msgstr "ha segnato l'elemento <0>{0}</0> della checklist come incompleto"
msgid "Medium Priority"
msgstr "Media priorità"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Membro"
@@ -1676,11 +1696,11 @@ msgstr "fatturazione mensile"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "ha spostato la scheda da <0>{0}</0> a<1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "ha spostato la scheda in un'altra lista"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Prossimi passi"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Nessun {0}"
@@ -1790,11 +1810,11 @@ msgstr "Nessun URL di download disponibile per questo allegato."
msgid "No keyboard shortcuts registered."
msgstr "Nessuna scorciatoia da tastiera registrata."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Nessuna lista"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Nessuna lista è stata ancora creata"
@@ -1969,6 +1989,7 @@ msgstr "Seleziona un file da caricare."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Remoto"
msgid "Remove"
msgstr "Rimuovi"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Rimuovi dai preferiti"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Rimuovi membro"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "ha rimosso {0} etichette: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "ha rimosso un'etichetta dalla scheda"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "ha rimosso un membro dalla scheda"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Rimosso dai preferiti"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "ha rimosso l'etichetta <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "ha rimosso la data di scadenza"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "ha rinominato una checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "ha rinominato la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "ha rinominato l'elemento della checklist in <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Self Host"
msgid "Self host with Github"
msgstr "Self host con Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "si è auto-assegnato la scheda"
@@ -2239,7 +2268,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Imposta data di scadenza"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "ha impostato la data di scadenza"
@@ -2603,6 +2632,10 @@ msgstr "Impossibile ripristinare i permessi"
msgid "Unable to send feedback"
msgstr "Impossibile inviare feedback"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Impossibile aggiornare la bacheca"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Impossibile aggiornare l'URL della bacheca"
@@ -2654,12 +2687,12 @@ msgstr "Impossibile aggiornare i permessi"
msgid "Unable to update role"
msgstr "Impossibile aggiornare il ruolo"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "ha rimosso l'assegnazione di <0>{0}</0> dalla scheda"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "si è rimosso dall'assegnazione della scheda"
@@ -2718,24 +2751,24 @@ msgstr "Aggiorna"
msgid "Update label"
msgstr "Aggiorna etichetta"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "ha aggiornato un elemento della checklist"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "ha aggiornato la descrizione"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "ha aggiornato la data di scadenza"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "ha aggiornato il titolo"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "ha aggiornato il titolo in <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "Perché creare un Trello open source?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Spazio di lavoro"
@@ -2965,8 +2998,8 @@ msgstr "Puoi effettuare il self-hosting seguendo le istruzioni nel nostro <0>rep
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} is toegevoegd aan je favorieten."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} is verwijderd uit je favorieten."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Label toevoegen"
msgid "Add member"
msgstr "Lid toevoegen"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Toevoegen aan favorieten"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "{0} labels toegevoegd: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "heeft een checklist toegevoegd"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "heeft een checklistitem toegevoegd"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "heeft een label aan de kaart toegevoegd"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "heeft een lid aan de kaart toegevoegd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "heeft checklist <0>{0}</0> toegevoegd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "heeft checklistitem <0>{0}</0> toegevoegd"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "heeft label <0>{0}</0> toegevoegd"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Toegevoegd aan favorieten"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Het toevoegen van een nieuw lid kost een extra {price} ({billingType}) per plaats."
@@ -282,8 +300,8 @@ msgstr "Weet je zeker dat je dit label wilt verwijderen?"
msgid "Are you sure you want to delete your account?"
msgstr "Weet je zeker dat je je account wilt verwijderen?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "heeft <0>{0}</0> aan de kaart toegewezen"
@@ -562,8 +580,8 @@ msgstr "Wachtwoord wijzigen"
msgid "Change your language preferences."
msgstr "Wijzig je taalvoorkeuren."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "heeft de vervaldatum gewijzigd naar <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Voltooid"
msgid "Complete control and ownership:"
msgstr "Volledige controle en eigendom:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "heeft een checklistitem voltooid"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "heeft checklistitem <0>{0}</0> voltooid"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Lijst maken"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Nieuwe {0} maken"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Maak nieuw label"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Maak nieuwe lijst"
@@ -783,7 +801,7 @@ msgstr "Sjabloon maken"
msgid "Create workspace"
msgstr "Werkruimte maken"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "heeft de kaart aangemaakt"
@@ -857,7 +875,7 @@ msgstr "Verwijderen"
msgid "Delete account"
msgstr "Account verwijderen"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Board verwijderen"
@@ -873,7 +891,7 @@ msgstr "Reactie verwijderen"
msgid "Delete list"
msgstr "Lijst verwijderen"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Sjabloon verwijderen"
@@ -883,21 +901,21 @@ msgstr "Sjabloon verwijderen"
msgid "Delete workspace"
msgstr "Werkruimte verwijderen"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "heeft een checklist verwijderd"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "heeft een checklistitem verwijderd"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "heeft checklist <0>{0}</0> verwijderd"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "heeft checklistitem <0>{0}</0> verwijderd"
@@ -993,7 +1011,7 @@ msgstr "Vervalt morgen"
msgid "Edit"
msgstr "Bewerken"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Board URL bewerken"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Aan de slag"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Begin door een nieuwe {0} te maken"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Begin door een nieuwe lijst te maken"
@@ -1595,7 +1613,7 @@ msgstr "Lijstnaam"
msgid "Lists"
msgstr "Lijsten"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Meer activiteiten laden"
@@ -1603,7 +1621,7 @@ msgstr "Meer activiteiten laden"
msgid "Loading permissions..."
msgstr "Rechten laden..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Laden..."
@@ -1631,16 +1649,16 @@ msgstr "Lage prioriteit"
msgid "magic link"
msgstr "magische link"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Sjabloon maken"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "heeft een checklistitem als onvoltooid gemarkeerd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "heeft checklistitem <0>{0}</0> als onvoltooid gemarkeerd"
@@ -1648,6 +1666,8 @@ msgstr "heeft checklistitem <0>{0}</0> als onvoltooid gemarkeerd"
msgid "Medium Priority"
msgstr "Gemiddelde prioriteit"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Lid"
@@ -1676,11 +1696,11 @@ msgstr "maandelijkse facturering"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "heeft de kaart verplaatst van <0>{0}</0> naar<1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "heeft de kaart naar een andere lijst verplaatst"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Volgende stappen"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Geen {0}"
@@ -1790,11 +1810,11 @@ msgstr "Geen download-URL beschikbaar voor deze bijlage."
msgid "No keyboard shortcuts registered."
msgstr "Geen sneltoetsen geregistreerd."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Geen lijsten"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Er zijn nog geen lijsten aangemaakt"
@@ -1969,6 +1989,7 @@ msgstr "Selecteer een bestand om te uploaden."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Op afstand"
msgid "Remove"
msgstr "Verwijderen"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Verwijderen uit favorieten"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Lid verwijderen"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "{0} labels verwijderd: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "heeft een label van de kaart verwijderd"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "heeft een lid van de kaart verwijderd"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Verwijderd uit favorieten"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "heeft label <0>{0}</0> verwijderd"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "heeft de vervaldatum verwijderd"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "heeft een checklist hernoemd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "heeft checklist <0>{0}</0> hernoemd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "checklistitem hernoemd naar <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Zelf hosten"
msgid "Self host with Github"
msgstr "Zelf hosten met Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "heeft de kaart aan zichzelf toegewezen"
@@ -2239,7 +2268,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Vervaldatum instellen"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "heeft de vervaldatum ingesteld"
@@ -2603,6 +2632,10 @@ msgstr "Kan machtigingen niet resetten"
msgid "Unable to send feedback"
msgstr "Feedback kon niet worden verzonden"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Kan bord niet bijwerken"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Kan board URL niet bijwerken"
@@ -2654,12 +2687,12 @@ msgstr "Kan machtigingen niet bijwerken"
msgid "Unable to update role"
msgstr "Kan rol niet bijwerken"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "<0>{0}</0> van de kaart verwijderd"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "zichzelf van de kaart verwijderd"
@@ -2718,24 +2751,24 @@ msgstr "Bijwerken"
msgid "Update label"
msgstr "Label bijwerken"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "een checklistitem bijgewerkt"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "beschrijving bijgewerkt"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "heeft de vervaldatum bijgewerkt"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "titel bijgewerkt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "titel bijgewerkt naar <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "Waarom een open source Trello maken?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Werkruimte"
@@ -2965,8 +2998,8 @@ msgstr "Je kunt zelf hosten door de instructies in onze <0>repo</0> te volgen."
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -40,6 +40,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} zostało dodane do ulubionych."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} zostało usunięte z ulubionych."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -136,42 +146,50 @@ msgstr "Dodaj etykietę"
msgid "Add member"
msgstr "Dodaj członka"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Dodaj do ulubionych"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "dodano {0} etykiety: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "dodał listę kontrolną"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "dodał(a) element listy kontrolnej"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "dodał(a) etykietę do karty"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "dodał(a) członka do karty"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "dodał(a) listę kontrolną <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "dodał(a) element listy kontrolnej <0>{0}</0>"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "dodał(a) etykietę <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Dodano do ulubionych"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Dodanie nowego członka będzie kosztować dodatkowe {price} ({billingType}) za miejsce."
@@ -287,8 +305,8 @@ msgstr "Czy na pewno chcesz usunąć tę etykietę?"
msgid "Are you sure you want to delete your account?"
msgstr "Czy na pewno chcesz usunąć swoje konto?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "przypisano <0>{0}</0> do karty"
@@ -567,8 +585,8 @@ msgstr "Zmień hasło"
msgid "Change your language preferences."
msgstr "Zmień swoje preferencje językowe."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "zmieniono termin na <0>{formattedDate}</0>"
@@ -656,12 +674,12 @@ msgstr "Zakończ"
msgid "Complete control and ownership:"
msgstr "Pełna kontrola i własność:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "ukończono element listy kontrolnej"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "ukończono element listy kontrolnej <0>{0}</0>"
@@ -760,7 +778,7 @@ msgid "Create list"
msgstr "Utwórz listę"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Utwórz nowy {0}"
@@ -775,7 +793,7 @@ msgid "Create new label"
msgstr "Utwórz nową etykietę"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Utwórz nową listę"
@@ -788,7 +806,7 @@ msgstr "Utwórz szablon"
msgid "Create workspace"
msgstr "Utwórz przestrzeń roboczą"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "utworzył(a) kartę"
@@ -862,7 +880,7 @@ msgstr "Usuń"
msgid "Delete account"
msgstr "Usuń konto"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Usuń tablicę"
@@ -878,7 +896,7 @@ msgstr "Usuń komentarz"
msgid "Delete list"
msgstr "Usuń listę"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Usuń szablon"
@@ -888,21 +906,21 @@ msgstr "Usuń szablon"
msgid "Delete workspace"
msgstr "Usuń przestrzeń roboczą"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "usunięto listę kontrolną"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "usunięto element listy kontrolnej"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "usunięto listę kontrolną <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "usunięto element listy kontrolnej <0>{0}</0>"
@@ -998,7 +1016,7 @@ msgstr "Termin na jutro"
msgid "Edit"
msgstr "Edytuj"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Edytuj URL tablicy"
@@ -1286,11 +1304,11 @@ msgid "Get Started"
msgstr "Rozpocznij"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Rozpocznij, tworząc nowy {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Rozpocznij, tworząc nową listę"
@@ -1600,7 +1618,7 @@ msgstr "Nazwa listy"
msgid "Lists"
msgstr "Listy"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Załaduj więcej aktywności"
@@ -1608,7 +1626,7 @@ msgstr "Załaduj więcej aktywności"
msgid "Loading permissions..."
msgstr "Ładowanie uprawnień..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Ładowanie..."
@@ -1636,16 +1654,16 @@ msgstr "Niski priorytet"
msgid "magic link"
msgstr "magiczny link"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Utwórz szablon"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "oznaczył element listy kontrolnej jako nieukończony"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "oznaczył element listy kontrolnej <0>{0}</0> jako nieukończony"
@@ -1653,6 +1671,8 @@ msgstr "oznaczył element listy kontrolnej <0>{0}</0> jako nieukończony"
msgid "Medium Priority"
msgstr "Średni priorytet"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Członek"
@@ -1681,11 +1701,11 @@ msgstr "rozliczenie miesięczne"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "przeniesiono kartę z <0>{0}</0> do<1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "przeniesiono kartę do innej listy"
@@ -1767,7 +1787,7 @@ msgid "Next Steps"
msgstr "Następne kroki"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Brak {0}"
@@ -1795,11 +1815,11 @@ msgstr "Brak dostępnego URL do pobrania tego załącznika."
msgid "No keyboard shortcuts registered."
msgstr "Brak zarejestrowanych skrótów klawiaturowych."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Brak list"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Nie utworzono jeszcze żadnych list"
@@ -1974,6 +1994,7 @@ msgstr "Proszę wybrać plik do przesłania."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2101,44 +2122,52 @@ msgstr "Zdalne"
msgid "Remove"
msgstr "Usuń"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Usuń z ulubionych"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Usuń członka"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "usunięto {0} etykiety: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "usunął etykietę z karty"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "usunął członka z karty"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Usunięto z ulubionych"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "usunięto etykietę <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "usunięto termin"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "zmieniono nazwę listy kontrolnej"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "zmieniono nazwę listy kontrolnej <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "zmieniono nazwę elementu listy kontrolnej na <0>{0}</0>"
@@ -2224,7 +2253,7 @@ msgstr "Hostuj samodzielnie"
msgid "Self host with Github"
msgstr "Hostuj samodzielnie z Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "przypisał(a) sobie kartę"
@@ -2244,7 +2273,7 @@ msgstr "Starszy"
msgid "Set due date"
msgstr "Ustaw termin"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "ustawiono termin"
@@ -2608,6 +2637,10 @@ msgstr "Nie można zresetować uprawnień"
msgid "Unable to send feedback"
msgstr "Nie można wysłać opinii"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Nie można zaktualizować tablicy"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Nie można zaktualizować adresu URL tablicy"
@@ -2659,12 +2692,12 @@ msgstr "Nie można zaktualizować uprawnień"
msgid "Unable to update role"
msgstr "Nie można zaktualizować roli"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "usunięto przypisanie <0>{0}</0> z karty"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "usunęli swoje przypisanie z karty"
@@ -2723,24 +2756,24 @@ msgstr "Aktualizuj"
msgid "Update label"
msgstr "Zaktualizuj etykietę"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "zaktualizowano element listy kontrolnej"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "zaktualizowano opis"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "zaktualizowano termin"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "zaktualizowano tytuł"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "zaktualizowano tytuł na <0>{0}</0>"
@@ -2865,7 +2898,7 @@ msgstr "Dlaczego próbujecie stworzyć Trello w wersji otwartoźródłowej?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Obszar roboczy"
@@ -2970,8 +3003,8 @@ msgstr "Możesz hostować samodzielnie, postępując zgodnie z instrukcjami w na
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} foi adicionado aos seus favoritos."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} foi removido dos seus favoritos."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Adicionar etiqueta"
msgid "Add member"
msgstr "Adicionar membro"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Adicionar aos favoritos"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "adicionou {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "adicionou uma lista de verificação"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "adicionou um item da lista de verificação"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "adicionou uma etiqueta ao cartão"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "adicionou um membro ao cartão"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "adicionou a lista de verificação <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "adicionou o item da lista de verificação <0>{0}</0>"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "adicionou a etiqueta <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Adicionado aos favoritos"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Adicionar um novo membro custará {price} ({billingType}) adicionais por assento."
@@ -282,8 +300,8 @@ msgstr "Tem certeza de que deseja excluir esta etiqueta?"
msgid "Are you sure you want to delete your account?"
msgstr "Tem certeza de que deseja excluir sua conta?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "atribuiu <0>{0}</0> ao card"
@@ -562,8 +580,8 @@ msgstr "Alterar senha"
msgid "Change your language preferences."
msgstr "Altere suas preferências de idioma."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "alterou a data de vencimento para <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Concluir"
msgid "Complete control and ownership:"
msgstr "Controle e propriedade completos:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "concluiu um item da lista de verificação"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "concluiu o item da lista de verificação <0>{0}</0>"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Criar lista"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Criar novo {0}"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Criar nova etiqueta"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Criar nova lista"
@@ -783,7 +801,7 @@ msgstr "Criar modelo"
msgid "Create workspace"
msgstr "Criar workspace"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "criou o cartão"
@@ -857,7 +875,7 @@ msgstr "Excluir"
msgid "Delete account"
msgstr "Excluir conta"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Excluir quadro"
@@ -873,7 +891,7 @@ msgstr "Excluir comentário"
msgid "Delete list"
msgstr "Excluir lista"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Excluir modelo"
@@ -883,21 +901,21 @@ msgstr "Excluir modelo"
msgid "Delete workspace"
msgstr "Excluir workspace"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "excluiu uma checklist"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "excluiu um item da checklist"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "excluiu a checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "excluiu o item da checklist <0>{0}</0>"
@@ -993,7 +1011,7 @@ msgstr "Vence amanhã"
msgid "Edit"
msgstr "Editar"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Editar URL do quadro"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Começar"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Comece criando um novo {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Comece criando uma nova lista"
@@ -1595,7 +1613,7 @@ msgstr "Nome da lista"
msgid "Lists"
msgstr "Listas"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Carregar mais atividades"
@@ -1603,7 +1621,7 @@ msgstr "Carregar mais atividades"
msgid "Loading permissions..."
msgstr "Carregando permissões..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Carregando..."
@@ -1631,16 +1649,16 @@ msgstr "Prioridade baixa"
msgid "magic link"
msgstr "link mágico"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Criar modelo"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "marcou um item da checklist como incompleto"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "marcou o item da checklist <0>{0}</0> como incompleto"
@@ -1648,6 +1666,8 @@ msgstr "marcou o item da checklist <0>{0}</0> como incompleto"
msgid "Medium Priority"
msgstr "Prioridade média"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Membro"
@@ -1676,11 +1696,11 @@ msgstr "cobrança mensal"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "moveu o card de <0>{0}</0> para <1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "moveu o card para outra lista"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Próximos passos"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Nenhum {0}"
@@ -1790,11 +1810,11 @@ msgstr "Nenhuma URL de download disponível para este anexo."
msgid "No keyboard shortcuts registered."
msgstr "Nenhum atalho de teclado registrado."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Nenhuma lista"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Nenhuma lista foi criada ainda"
@@ -1969,6 +1989,7 @@ msgstr "Por favor, selecione um arquivo para fazer upload."
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Remoto"
msgid "Remove"
msgstr "Remover"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Remover dos favoritos"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Remover membro"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "removeu {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "removeu uma etiqueta do cartão"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "removeu um membro do cartão"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Removido dos favoritos"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "removeu a etiqueta <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "removeu a data de vencimento"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "renomeou uma lista de verificação"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "renomeou a checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "renomeou o item da checklist para <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Auto-hospedagem"
msgid "Self host with Github"
msgstr "Auto-hospede com Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "atribuiu o cartão a si mesmo"
@@ -2239,7 +2268,7 @@ msgstr "Sênior"
msgid "Set due date"
msgstr "Definir data de vencimento"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "definiu a data de vencimento"
@@ -2603,6 +2632,10 @@ msgstr "Não foi possível redefinir as permissões"
msgid "Unable to send feedback"
msgstr "Não foi possível enviar feedback"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Não foi possível atualizar o quadro"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Não foi possível atualizar URL do quadro"
@@ -2654,12 +2687,12 @@ msgstr "Não foi possível atualizar as permissões"
msgid "Unable to update role"
msgstr "Não foi possível atualizar a função"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "removeu a atribuição de <0>{0}</0> do cartão"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "removeu a própria atribuição do cartão"
@@ -2718,24 +2751,24 @@ msgstr "Atualizar"
msgid "Update label"
msgstr "Atualizar rótulo"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "atualizou um item da lista de verificação"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "atualizou a descrição"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "atualizou a data de vencimento"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "atualizou o título"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "atualizou o título para <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "Por que criar um Trello de código aberto?"
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Workspace"
@@ -2965,8 +2998,8 @@ msgstr "Você pode fazer self-host seguindo as instruções em nosso <0>reposit
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -35,6 +35,16 @@ msgstr "{0}"
msgid "{0} | {1}"
msgstr "{0} | {1}"
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:54
msgid "{0} has been added to your favorites."
msgstr "{0} добавлено в избранное."
#. placeholder {0}: boardName ?? "Board"
#: src/views/board/components/BoardDropdown.tsx:55
msgid "{0} has been removed from your favorites."
msgstr "{0} удалено из избранного."
#. placeholder {0}: labelPublicIds.length
#: src/views/board/components/NewCardForm.tsx:453
msgid "{0} labels"
@@ -131,42 +141,50 @@ msgstr "Добавить метку"
msgid "Add member"
msgstr "Добавить участника"
#: src/views/board/components/BoardDropdown.tsx:93
msgid "Add to favorites"
msgstr "Добавить в избранное"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:78
#: src/views/card/components/ActivityList.tsx:89
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "добавил {0} метки: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:108
#: src/views/card/components/ActivityList.tsx:119
msgid "added a checklist"
msgstr "добавил(а) чек-лист"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:122
msgid "added a checklist item"
msgstr "добавил(а) элемент чек-листа"
#: src/views/card/components/ActivityList.tsx:104
#: src/views/card/components/ActivityList.tsx:115
msgid "added a label to the card"
msgstr "добавил(а) метку к карточке"
#: src/views/card/components/ActivityList.tsx:106
#: src/views/card/components/ActivityList.tsx:117
msgid "added a member to the card"
msgstr "добавил(а) участника к карточке"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:182
#: src/views/card/components/ActivityList.tsx:193
msgid "added checklist <0>{0}</0>"
msgstr "добавлен контрольный список <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:206
#: src/views/card/components/ActivityList.tsx:217
msgid "added checklist item <0>{0}</0>"
msgstr "добавлен элемент контрольного списка <0>{0}</0>"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:166
#: src/views/card/components/ActivityList.tsx:177
msgid "added label <0>{0}</0>"
msgstr "добавлена метка <0>{0}</0>"
#: src/views/board/components/BoardDropdown.tsx:51
msgid "Added to favorites"
msgstr "Добавлено в избранное"
#: src/views/members/components/InviteMemberForm.tsx:327
msgid "Adding a new member will cost an additional {price} ({billingType}) per seat."
msgstr "Добавление нового участника будет стоить дополнительно {price} ({billingType}) за место."
@@ -282,8 +300,8 @@ msgstr "Вы уверены, что хотите удалить эту метк
msgid "Are you sure you want to delete your account?"
msgstr "Вы уверены, что хотите удалить свой аккаунт?"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
msgid "assigned <0>{0}</0> to the card"
msgstr "назначено <0>{0}</0> к карточке"
@@ -562,8 +580,8 @@ msgstr "Изменить пароль"
msgid "Change your language preferences."
msgstr "Измените настройки языка."
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "изменил(а) дату выполнения на <0>{formattedDate}</0>"
@@ -651,12 +669,12 @@ msgstr "Завершить"
msgid "Complete control and ownership:"
msgstr "Полный контроль и владение:"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:124
msgid "completed a checklist item"
msgstr "завершил элемент контрольного списка"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:223
#: src/views/card/components/ActivityList.tsx:234
msgid "completed checklist item <0>{0}</0>"
msgstr "завершил элемент контрольного списка <0>{0}</0>"
@@ -755,7 +773,7 @@ msgid "Create list"
msgstr "Создать список"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Создать новый {0}"
@@ -770,7 +788,7 @@ msgid "Create new label"
msgstr "Создать новую метку"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:537
#: src/views/board/index.tsx:539
msgid "Create new list"
msgstr "Создать новый список"
@@ -783,7 +801,7 @@ msgstr "Создать шаблон"
msgid "Create workspace"
msgstr "Создать рабочее пространство"
#: src/views/card/components/ActivityList.tsx:100
#: src/views/card/components/ActivityList.tsx:111
msgid "created the card"
msgstr "создал(а) карточку"
@@ -857,7 +875,7 @@ msgstr "Удалить"
msgid "Delete account"
msgstr "Удалить аккаунт"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete board"
msgstr "Удалить доску"
@@ -873,7 +891,7 @@ msgstr "Удалить комментарий"
msgid "Delete list"
msgstr "Удалить список"
#: src/views/board/components/BoardDropdown.tsx:55
#: src/views/board/components/BoardDropdown.tsx:104
msgid "Delete template"
msgstr "Удалить шаблон"
@@ -883,21 +901,21 @@ msgstr "Удалить шаблон"
msgid "Delete workspace"
msgstr "Удалить рабочее пространство"
#: src/views/card/components/ActivityList.tsx:110
#: src/views/card/components/ActivityList.tsx:121
msgid "deleted a checklist"
msgstr "удалил контрольный список"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:126
msgid "deleted a checklist item"
msgstr "удалил элемент контрольного списка"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:198
#: src/views/card/components/ActivityList.tsx:209
msgid "deleted checklist <0>{0}</0>"
msgstr "удалил контрольный список <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:241
#: src/views/card/components/ActivityList.tsx:252
msgid "deleted checklist item <0>{0}</0>"
msgstr "удалил элемент контрольного списка <0>{0}</0>"
@@ -993,7 +1011,7 @@ msgstr "Срок завтра"
msgid "Edit"
msgstr "Редактировать"
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Изменить URL доски"
@@ -1281,11 +1299,11 @@ msgid "Get Started"
msgstr "Начать"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:44
#: src/views/boards/components/BoardsList.tsx:65
msgid "Get started by creating a new {0}"
msgstr "Начните с создания нового {0}"
#: src/views/board/index.tsx:522
#: src/views/board/index.tsx:524
msgid "Get started by creating a new list"
msgstr "Начните с создания нового списка"
@@ -1595,7 +1613,7 @@ msgstr "Название списка"
msgid "Lists"
msgstr "Списки"
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Load more activities"
msgstr "Загрузить больше действий"
@@ -1603,7 +1621,7 @@ msgstr "Загрузить больше действий"
msgid "Loading permissions..."
msgstr "Загрузка разрешений..."
#: src/views/card/components/ActivityList.tsx:533
#: src/views/card/components/ActivityList.tsx:545
msgid "Loading..."
msgstr "Загрузка..."
@@ -1631,16 +1649,16 @@ msgstr "Низкий приоритет"
msgid "magic link"
msgstr "магическая ссылка"
#: src/views/board/components/BoardDropdown.tsx:34
#: src/views/board/components/BoardDropdown.tsx:73
msgid "Make template"
msgstr "Создать шаблон"
#: src/views/card/components/ActivityList.tsx:114
#: src/views/card/components/ActivityList.tsx:125
msgid "marked a checklist item as incomplete"
msgstr "отметил элемент контрольного списка как незавершённый"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:232
#: src/views/card/components/ActivityList.tsx:243
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "отметил элемент контрольного списка <0>{0}</0> как незавершённый"
@@ -1648,6 +1666,8 @@ msgstr "отметил элемент контрольного списка <0>{
msgid "Medium Priority"
msgstr "Средний приоритет"
#: src/views/card/components/ActivityList.tsx:44
#: src/views/card/components/ActivityList.tsx:75
#: src/views/members/index.tsx:201
msgid "Member"
msgstr "Участник"
@@ -1676,11 +1696,11 @@ msgstr "ежемесячная оплата"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:134
#: src/views/card/components/ActivityList.tsx:145
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "переместил карточку из <0>{0}</0> в <1>{1}</1>"
#: src/views/card/components/ActivityList.tsx:103
#: src/views/card/components/ActivityList.tsx:114
msgid "moved the card to another list"
msgstr "переместил карточку в другой список"
@@ -1762,7 +1782,7 @@ msgid "Next Steps"
msgstr "Следующие шаги"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:41
#: src/views/boards/components/BoardsList.tsx:62
msgid "No {0}"
msgstr "Нет {0}"
@@ -1790,11 +1810,11 @@ msgstr "URL для загрузки этого вложения недоступ
msgid "No keyboard shortcuts registered."
msgstr "Горячие клавиши не зарегистрированы."
#: src/views/board/index.tsx:518
#: src/views/board/index.tsx:520
msgid "No lists"
msgstr "Нет списков"
#: src/views/board/index.tsx:523
#: src/views/board/index.tsx:525
msgid "No lists have been created yet"
msgstr "Списки ещё не созданы"
@@ -1969,6 +1989,7 @@ msgstr "Пожалуйста, выберите файл для загрузки.
#: src/components/DeleteLabelConfirmation.tsx:26
#: src/components/FeedbackModal.tsx:41
#: src/components/NewWorkspaceForm.tsx:158
#: src/views/board/components/BoardDropdown.tsx:63
#: src/views/board/components/NewCardForm.tsx:184
#: src/views/board/components/NewListForm.tsx:79
#: src/views/board/components/NewTemplateForm.tsx:61
@@ -2096,44 +2117,52 @@ msgstr "Удалённый"
msgid "Remove"
msgstr "Удалить"
#: src/views/board/components/BoardDropdown.tsx:92
msgid "Remove from favorites"
msgstr "Удалить из избранного"
#: src/views/members/index.tsx:232
msgid "Remove member"
msgstr "Удалить участника"
#. placeholder {0}: mergedLabels.length
#: src/views/card/components/ActivityList.tsx:92
#: src/views/card/components/ActivityList.tsx:103
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "удалил {0} метки: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:105
#: src/views/card/components/ActivityList.tsx:116
msgid "removed a label from the card"
msgstr "удалил метку с карточки"
#: src/views/card/components/ActivityList.tsx:107
#: src/views/card/components/ActivityList.tsx:118
msgid "removed a member from the card"
msgstr "удалил участника из карточки"
#: src/views/board/components/BoardDropdown.tsx:52
msgid "Removed from favorites"
msgstr "Удалено из избранного"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:174
#: src/views/card/components/ActivityList.tsx:185
msgid "removed label <0>{0}</0>"
msgstr "удалил метку <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
msgid "removed the due date"
msgstr "удалил(а) дату выполнения"
#: src/views/card/components/ActivityList.tsx:109
#: src/views/card/components/ActivityList.tsx:120
msgid "renamed a checklist"
msgstr "переименовал контрольный список"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:190
#: src/views/card/components/ActivityList.tsx:201
msgid "renamed checklist <0>{0}</0>"
msgstr "переименовал контрольный список <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:214
#: src/views/card/components/ActivityList.tsx:225
msgid "renamed checklist item to <0>{0}</0>"
msgstr "переименовал элемент контрольного списка в <0>{0}</0>"
@@ -2219,7 +2248,7 @@ msgstr "Самостоятельный хостинг"
msgid "Self host with Github"
msgstr "Самостоятельный хостинг с Github"
#: src/views/card/components/ActivityList.tsx:143
#: src/views/card/components/ActivityList.tsx:154
msgid "self-assigned the card"
msgstr "самостоятельно назначил карточку"
@@ -2239,7 +2268,7 @@ msgstr "Старший"
msgid "Set due date"
msgstr "Установить дату выполнения"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:127
msgid "set the due date"
msgstr "установил(а) дату выполнения"
@@ -2603,6 +2632,10 @@ msgstr "Не удалось сбросить разрешения"
msgid "Unable to send feedback"
msgstr "Не удалось отправить отзыв"
#: src/views/board/components/BoardDropdown.tsx:62
msgid "Unable to update board"
msgstr "Не удалось обновить доску"
#: src/views/board/components/UpdateBoardSlugForm.tsx:72
msgid "Unable to update board URL"
msgstr "Не удалось обновить URL доски"
@@ -2654,12 +2687,12 @@ msgstr "Не удалось обновить разрешения"
msgid "Unable to update role"
msgstr "Не удалось обновить роль"
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
msgid "unassigned <0>{0}</0> from the card"
msgstr "удалил(а) <0>{0}</0> из карточки"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:165
msgid "unassigned themselves from the card"
msgstr "удалил(а) себя из карточки"
@@ -2718,24 +2751,24 @@ msgstr "Обновить"
msgid "Update label"
msgstr "Обновить метку"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:123
msgid "updated a checklist item"
msgstr "обновлен элемент контрольного списка"
#: src/views/card/components/ActivityList.tsx:102
#: src/views/card/components/ActivityList.tsx:113
msgid "updated the description"
msgstr "обновлено описание"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:128
msgid "updated the due date"
msgstr "обновил(а) дату выполнения"
#: src/views/card/components/ActivityList.tsx:101
#: src/views/card/components/ActivityList.tsx:112
msgid "updated the title"
msgstr "обновлен заголовок"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:137
msgid "updated the title to <0>{0}</0>"
msgstr "обновлен заголовок на <0>{0}</0>"
@@ -2860,7 +2893,7 @@ msgstr "Зачем создавать Trello с открытым исходны
#: src/views/boards/index.tsx:35
#: src/views/members/index.tsx:257
#: src/views/public/board/index.tsx:120
#: src/views/public/boards/index.tsx:66
#: src/views/public/boards/index.tsx:65
msgid "Workspace"
msgstr "Рабочее пространство"
@@ -2965,8 +2998,8 @@ msgstr "Вы можете развернуть собственный хости
#: src/views/board/components/UpdateBoardSlugButton.tsx:31
#: src/views/board/components/VisibilityButton.tsx:72
#: src/views/board/index.tsx:472
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: src/views/board/index.tsx:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/boards/index.tsx:46
#: src/views/boards/index.tsx:67
msgid "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -13,6 +13,23 @@ const authHandler = toNodeHandler(auth.handler);
export default withRateLimit(
{ points: 100, duration: 60 },
async (req, res) => {
/**
* Better-auth behind proxies (Nginx/Cloudflare) can sometimes fail to parse the protocol
* if headers are incorrectly set or if there are multiple values in X-Forwarded-Proto.
* We sanitize these headers here to ensure better-auth gets a clean protocol and host.
*/
const forwardedProto = req.headers["x-forwarded-proto"];
if (forwardedProto) {
const p = Array.isArray(forwardedProto) ? forwardedProto[0] : forwardedProto;
req.headers["x-forwarded-proto"] = p?.split(",")[0]?.trim();
}
const forwardedHost = req.headers["x-forwarded-host"];
if (forwardedHost) {
const h = Array.isArray(forwardedHost) ? forwardedHost[0] : forwardedHost;
req.headers["host"] = h?.split(",")[0]?.trim();
}
return await authHandler(req, res);
},
);

View File

@@ -0,0 +1,136 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { Upload } from "@aws-sdk/lib-storage";
import { createNextApiContext } from "@kan/api/trpc";
import { assertPermission } from "@kan/api/utils/permissions";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import * as cardRepo from "@kan/db/repository/card.repo";
import * as cardActivityRepo from "@kan/db/repository/cardActivity.repo";
import * as cardAttachmentRepo from "@kan/db/repository/cardAttachment.repo";
import { createS3Client, generateUID } from "@kan/shared/utils";
import { env } from "~/env";
// FIXME: Respect the environment variable: NEXT_API_BODY_SIZE_LIMIT
const MAX_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
export const config = {
api: {
bodyParser: false,
},
};
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { user, db } = await createNextApiContext(req);
if (!user) {
return res.status(401).json({ error: "Unauthorized" });
}
const bucket = env.NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME;
if (!bucket) {
return res.status(500).json({ error: "Attachments bucket not configured" });
}
const cardPublicId = req.query.cardPublicId;
if (typeof cardPublicId !== "string" || cardPublicId.length < 12) {
return res.status(400).json({ error: "Invalid cardPublicId" });
}
const contentType = req.headers["content-type"];
const contentLengthHeader = req.headers["content-length"];
const contentLength = contentLengthHeader
? Number.parseInt(contentLengthHeader, 10)
: NaN;
if (typeof contentType !== "string") {
return res.status(400).json({ error: "Missing content type" });
}
if (!Number.isFinite(contentLength) || contentLength <= 0) {
return res.status(400).json({ error: "Missing or invalid content length" });
}
if (contentLength > MAX_SIZE_BYTES) {
return res.status(400).json({ error: "File too large" });
}
const originalFilenameHeader =
(req.headers["x-original-filename"] as string | undefined) ?? "file";
const sanitizedFilename = originalFilenameHeader
.replace(/[^a-zA-Z0-9._-]/g, "_")
.substring(0, 200);
// Get card and check permissions
const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
db,
cardPublicId,
);
if (!card) {
return res.status(404).json({ error: "Card not found" });
}
// Check if user has permission to edit the card
try {
await assertPermission(db, user.id, card.workspaceId, "card:edit");
} catch {
return res.status(403).json({ error: "Permission denied" });
}
const s3Key = `${card.workspaceId}/${cardPublicId}/${generateUID()}-${sanitizedFilename}`;
const client = createS3Client();
const upload = new Upload({
client,
params: {
Bucket: bucket,
Key: s3Key,
Body: req,
ContentType: contentType,
ContentLength: contentLength,
},
leavePartsOnError: false,
});
await upload.done();
// Create attachment record and log activity
const attachment = await cardAttachmentRepo.create(db, {
cardId: card.id,
filename: sanitizedFilename,
originalFilename: originalFilenameHeader,
contentType,
size: contentLength,
s3Key,
createdBy: user.id,
});
if (!attachment) {
return res.status(500).json({ error: "Failed to create attachment" });
}
await cardActivityRepo.create(db, {
type: "card.updated.attachment.added",
cardId: card.id,
attachmentId: attachment.id,
toTitle: originalFilenameHeader,
createdBy: user.id,
});
return res.status(200).json({ attachment });
} catch (error) {
console.error("Attachment upload failed", error);
return res.status(500).json({ error: "Internal server error" });
}
},
);

View File

@@ -0,0 +1,101 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { createNextApiContext } from "@kan/api/trpc";
import * as userRepo from "@kan/db/repository/user.repo";
import { env } from "~/env";
import { withRateLimit } from "@kan/api/utils/rateLimit";
import { createS3Client } from "@kan/shared/utils";
const MAX_SIZE_BYTES = 2 * 1024 * 1024; // 2MB
const allowedContentTypes = ["image/jpeg", "image/png", "image/webp"];
export const config = {
api: {
bodyParser: false,
},
};
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { user, db } = await createNextApiContext(req);
if (!user) {
return res.status(401).json({ error: "Unauthorized" });
}
const bucket = env.NEXT_PUBLIC_AVATAR_BUCKET_NAME;
if (!bucket) {
return res.status(500).json({ error: "Avatar bucket not configured" });
}
const contentType = req.headers["content-type"];
const contentLengthHeader = req.headers["content-length"];
const contentLength = contentLengthHeader
? Number.parseInt(contentLengthHeader, 10)
: NaN;
if (typeof contentType !== "string") {
return res.status(400).json({ error: "Missing content type" });
}
if (!allowedContentTypes.includes(contentType)) {
return res.status(400).json({ error: "Invalid content type" });
}
if (!Number.isFinite(contentLength) || contentLength <= 0) {
return res.status(400).json({ error: "Missing or invalid content length" });
}
if (contentLength > MAX_SIZE_BYTES) {
return res.status(400).json({ error: "File too large" });
}
const originalFilenameHeader =
(req.headers["x-original-filename"] as string | undefined) ?? "file";
const sanitizedFilename = originalFilenameHeader
.replace(/[^a-zA-Z0-9._-]/g, "_")
.substring(0, 200);
const s3Key = `${user.id}/${sanitizedFilename}`;
const client = createS3Client();
// Upload the file to S3
await client.send(
new PutObjectCommand({
Bucket: bucket,
Key: s3Key,
Body: req,
ContentType: contentType,
ContentLength: contentLength,
}),
);
// Update user image in database
const updatedUser = await userRepo.update(db, user.id, {
image: s3Key,
});
return res.status(200).json({
key: s3Key,
filename: sanitizedFilename,
contentType,
size: contentLength,
user: updatedUser,
});
} catch (error) {
console.error("Avatar upload failed", error);
return res.status(500).json({ error: "Internal server error" });
}
},
);

View File

@@ -1,75 +0,0 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { env as nextRuntimeEnv } from "next-runtime-env";
import { createNextApiContext } from "@kan/api/trpc";
import { env } from "~/env";
import { withRateLimit } from "@kan/api/utils/rateLimit";
const allowedContentTypes = ["image/jpeg", "image/png"];
export default withRateLimit(
{ points: 100, duration: 60 },
async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
}
try {
const { user } = await createNextApiContext(req);
if (!user) {
return res.status(401).json({ error: "Unauthorized" });
}
const { filename, contentType } = req.body as {
filename: string;
contentType: string;
};
// Specific to avatar uploads for now
const filenameRegex = /^[a-f0-9\-]+\/[a-zA-Z0-9_\-]+(\.jpg|\.jpeg|\.png)$/;
if (!filenameRegex.test(filename)) {
return res.status(400).json({ error: "Invalid filename" });
}
if (
typeof contentType !== "string" ||
!allowedContentTypes.includes(contentType)
) {
return res.status(400).json({ error: "Invalid content type" });
}
const credentials =
env.S3_ACCESS_KEY_ID && env.S3_SECRET_ACCESS_KEY
? {
accessKeyId: env.S3_ACCESS_KEY_ID,
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
}
: undefined;
const client = new S3Client({
region: env.S3_REGION ?? "",
endpoint: env.S3_ENDPOINT ?? "",
forcePathStyle: env.S3_FORCE_PATH_STYLE === "true",
credentials,
});
const signedUrl = await getSignedUrl(
client,
new PutObjectCommand({
Bucket: nextRuntimeEnv("NEXT_PUBLIC_AVATAR_BUCKET_NAME") ?? "",
Key: filename,
ACL: "public-read",
}),
);
return res.status(200).json({ url: signedUrl, key: filename });
} catch (error) {
return res.status(500).json({ error: (error as Error).message });
}
},
);

View File

@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
import React from "react";
import {
Dialog,
DialogBackdrop,
@@ -464,19 +465,37 @@ function FormattedShortcut({ shortcut }: { shortcut: KeyboardShortcut }) {
? stroke.modifiers.map(stringifyModifier)
: [];
modifierStrings.forEach((mod) => {
parts.push(<kbd className={kbdClassName}>{mod}</kbd>);
modifierStrings.forEach((mod, index) => {
parts.push(
<kbd key={`mod-${index}-${mod}`} className={kbdClassName}>
{mod}
</kbd>,
);
});
parts.push(<kbd className={kbdClassName}>{stroke.key.toUpperCase()}</kbd>);
parts.push(
<kbd key={`key-${stroke.key}`} className={kbdClassName}>
{stroke.key.toUpperCase()}
</kbd>,
);
return parts;
};
if (shortcut.type === "SEQUENCE") {
const parts: ReactNode[] = [];
shortcut.strokes.forEach((stroke) => {
parts.push(...formatStroke(stroke));
shortcut.strokes.forEach((stroke, strokeIndex) => {
const strokeParts = formatStroke(stroke);
// Add stroke index to keys to ensure uniqueness across multiple strokes
const keyedParts = strokeParts.map((part, partIndex) => {
if (React.isValidElement(part)) {
return React.cloneElement(part, {
key: `stroke-${strokeIndex}-${part.key || partIndex}`,
});
}
return part;
});
parts.push(...keyedParts);
});
return <span className="flex items-center gap-1 text-[11px]">{parts}</span>;
}

View File

@@ -8,6 +8,8 @@ export async function invalidateCard(
utils: ReturnType<typeof api.useUtils>,
cardPublicId: string,
) {
if (!cardPublicId || cardPublicId.length < 12) return;
await Promise.all([
utils.card.byId.invalidate({ cardPublicId }),
utils.card.getActivities.invalidate({ cardPublicId }),

View File

@@ -1,5 +1,3 @@
import { env } from "next-runtime-env";
export const formatToArray = (
value: string | string[] | undefined,
): string[] => {
@@ -52,14 +50,5 @@ export const getAvatarUrl = (imageOrKey: string | null) => {
return imageOrKey;
}
const bucket = env("NEXT_PUBLIC_AVATAR_BUCKET_NAME");
const useVirtualHostedUrls = env("NEXT_PUBLIC_USE_VIRTUAL_HOSTED_URLS");
const storageDomain = env("NEXT_PUBLIC_STORAGE_DOMAIN");
if (useVirtualHostedUrls === "true" && storageDomain) {
return `https://${bucket}.${storageDomain}/${imageOrKey}`;
}
const storageUrl = env("NEXT_PUBLIC_STORAGE_URL");
return `${storageUrl}/${bucket}/${imageOrKey}`;
return "";
};

View File

@@ -66,8 +66,8 @@ const Card = ({
const hasDueDate = !!dueDate;
return (
<div className="flex flex-col rounded-md border border-light-200 bg-light-50 px-3 py-2 text-sm text-neutral-900 dark:border-dark-200 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300">
<span>{title}</span>
<div className="flex flex-col overflow-hidden rounded-md border border-light-200 bg-light-50 px-3 py-2 text-sm text-neutral-900 dark:border-dark-200 dark:bg-dark-200 dark:text-dark-1000 dark:hover:bg-dark-300">
<span className="break-words">{title}</span>
{labels.length ||
members.length ||
checklists.length > 0 ||

View File

@@ -1,24 +1,40 @@
import Link from "next/link";
import { Fragment } from "react";
import { t } from "@lingui/core/macro";
import { env } from "next-runtime-env";
import { HiLink } from "react-icons/hi";
import { Tooltip } from "~/components/Tooltip";
import { usePopup } from "~/providers/popup";
const displayBaseUrl =
env("NEXT_PUBLIC_KAN_ENV") === "cloud"
? "kan.bn"
: env("NEXT_PUBLIC_BASE_URL");
const linkBaseUrl = env("NEXT_PUBLIC_BASE_URL");
const pathSeparator = (
<div className="mx-1.5 h-4 w-px rotate-[20deg] bg-gray-300 dark:bg-dark-600" />
);
const UpdateBoardSlugButton = ({
handleOnClick,
workspaceSlug,
boardSlug,
boardPublicId,
visibility,
isLoading,
canEdit,
}: {
handleOnClick: () => void;
workspaceSlug: string;
boardSlug: string;
boardPublicId: string;
visibility: "public" | "private";
isLoading: boolean;
canEdit: boolean;
}) => {
if (!isLoading && (!workspaceSlug || !boardSlug)) return <></>;
const { showPopup } = usePopup();
if (isLoading) {
return (
@@ -26,44 +42,55 @@ const UpdateBoardSlugButton = ({
);
}
if (!workspaceSlug || !boardSlug || !boardPublicId) return <></>;
const isPublic = visibility === "public";
const boardUrl = isPublic
? `${linkBaseUrl}/${workspaceSlug}/${boardSlug}`
: `${linkBaseUrl}/boards/${boardPublicId}`;
const pathSegments = isPublic
? [displayBaseUrl, workspaceSlug, boardSlug]
: [displayBaseUrl, "boards", boardPublicId];
return (
<Tooltip
content={!canEdit && !isLoading ? t`You don't have permission` : undefined}
content={!canEdit ? t`You don't have permission` : undefined}
>
<button
onClick={canEdit ? handleOnClick : undefined}
disabled={!canEdit || isLoading}
className="hidden cursor-pointer items-center gap-2 rounded-full border-[1px] bg-light-50 p-1 pl-4 pr-1 text-sm text-light-950 hover:bg-light-100 disabled:cursor-not-allowed disabled:opacity-60 dark:border-dark-600 dark:bg-dark-50 dark:text-dark-900 dark:hover:bg-dark-100 xl:flex"
>
<div className="flex items-center">
<span>
{env("NEXT_PUBLIC_KAN_ENV") === "cloud"
? "kan.bn"
: env("NEXT_PUBLIC_BASE_URL")}
</span>
<div className="mx-1.5 h-4 w-px rotate-[20deg] bg-gray-300 dark:bg-dark-600"></div>
<span>{workspaceSlug}</span>
<div className="mx-1.5 h-4 w-px rotate-[20deg] bg-gray-300 dark:bg-dark-600"></div>
<span>{boardSlug}</span>
</div>
<Link
href={`${env("NEXT_PUBLIC_BASE_URL")}/${workspaceSlug}/${boardSlug}`}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => {
e.stopPropagation();
if (!canEdit) {
e.preventDefault();
}
}}
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-light-200 dark:hover:bg-dark-200"
>
<HiLink className="h-[13px] w-[13px]" />
</Link>
</button>
<div className="flex items-center">
{pathSegments.map((segment, i) => (
<Fragment key={i}>
{i > 0 && pathSeparator}
<span>{segment}</span>
</Fragment>
))}
</div>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(boardUrl).then(
() =>
showPopup({
header: t`Link copied`,
icon: "success",
message: t`Board URL copied to clipboard`,
}),
).catch(() => undefined);
}}
className="flex h-7 w-7 items-center justify-center rounded-full hover:bg-light-200 dark:hover:bg-dark-200"
aria-label={t`Copy board link`}
>
<HiLink className="h-[13px] w-[13px]" />
</button>
</button>
</Tooltip>
);
};
export default UpdateBoardSlugButton;

View File

@@ -156,7 +156,7 @@ export function UpdateBoardSlugForm({
<div className="flex items-center gap-2">
<Button
variant="secondary"
href="/settings?tab=workspace"
href="/settings/workspace"
onClick={closeModal}
>
{t`Edit workspace URL`}

View File

@@ -58,13 +58,14 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
const [selectedPublicListId, setSelectedPublicListId] =
useState<PublicListId>("");
const [isInitialLoading, setIsInitialLoading] = useState(true);
const { ref: scrollRef, onMouseDown } = useDragToScroll({
enabled: true,
direction: "horizontal",
});
const { canCreateList, canEditList, canEditCard, canEditBoard } = usePermissions();
const { canCreateList, canEditList, canEditCard, canEditBoard } =
usePermissions();
const { tooltipContent: createListShortcutTooltipContent } =
useKeyboardShortcut({
@@ -417,10 +418,9 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
{...register("name")}
onBlur={canEditBoard ? handleSubmit(onSubmit) : undefined}
readOnly={!canEditBoard}
className="block border-0 bg-transparent p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 focus:ring-0 focus-visible:outline-none dark:text-dark-1000 sm:text-[1.2rem] disabled:cursor-not-allowed"
className="block border-0 bg-transparent p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 focus:ring-0 focus-visible:outline-none disabled:cursor-not-allowed dark:text-dark-1000 sm:text-[1.2rem]"
/>
</form>
)}
{!boardData && !isLoading && (
<p className="order-2 block p-0 py-0 font-bold leading-[2.3rem] tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem] md:order-1">
@@ -443,6 +443,8 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
isLoading={isLoading}
workspaceSlug={workspace.slug ?? ""}
boardSlug={boardData?.slug ?? ""}
boardPublicId={boardId ?? ""}
visibility={boardData?.visibility ?? "private"}
canEdit={canEditBoard}
/>
<VisibilityButton
@@ -596,12 +598,13 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
? `/templates/${boardId}/cards/${card.publicId}`
: `/cards/${card.publicId}`
}
className={`mb-2 flex !cursor-pointer flex-col ${card.publicId.startsWith(
"PLACEHOLDER",
)
? "pointer-events-none"
: ""
}`}
className={`mb-2 flex !cursor-pointer flex-col ${
card.publicId.startsWith(
"PLACEHOLDER",
)
? "pointer-events-none"
: ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}

View File

@@ -23,6 +23,18 @@ export const getTemplates = (): Template[] => [
lists: [t`Backlog`, t`To Do`, t`In Progress`, t`Code Review`, t`Done`],
labels: [t`Bug`, t`Feature`, t`Enhancement`, t`Critical`, t`Documentation`],
},
{
id: "roadmap-basic",
name: t`Basic Roadmap`,
lists: [t`Requested`, t`Planned`, t`In Progress`, t`Done`],
labels: [t`Feature`, t`Enhancement`, t`Critical`, t`Documentation`],
},
{
id: "roadmap-extended",
name: t`Extended Roadmap`,
lists: [t`Requested`, t`Under Review`, t`Planned`, t`In Progress`, t`Done`, t`Rejected`],
labels: [t`Feature`, t`Enhancement`, t`Critical`, t`Documentation`],
},
{
id: "content-creation",
name: t`Content Creation`,

View File

@@ -8,6 +8,7 @@ import {
HiOutlineArrowRight,
HiOutlineCheckCircle,
HiOutlineClock,
HiOutlinePaperClip,
HiOutlinePencil,
HiOutlinePlus,
HiOutlineTag,
@@ -31,29 +32,50 @@ import Comment from "./Comment";
type ActivityType =
NonNullable<GetCardByIdOutput>["activities"][number]["type"];
type ActivityWithMergedLabels =
GetCardActivitiesOutput["activities"][number] & {
mergedLabels?: string[];
attachment?: {
publicId: string;
filename: string;
originalFilename: string;
} | null;
};
const truncate = (value: string | null, maxLength = 50) => {
if (!value) return value;
return value.length > maxLength ? `${value.slice(0, maxLength - 1)}` : value;
};
const getUserDisplayName = (
user: { name?: string | null; email?: string | null } | null | undefined,
): string => {
if (user?.name?.trim()) return user.name;
if (user?.email) return user.email;
return t`Member`;
};
const getActivityText = ({
type,
toTitle,
fromList,
toList,
memberName,
memberEmail,
isSelf,
label,
fromTitle,
toDueDate,
dateLocale,
mergedLabels,
attachmentName,
}: {
type: ActivityType;
toTitle: string | null;
fromList: string | null;
toList: string | null;
memberName: string | null;
memberEmail: string | null;
isSelf: boolean;
label: string | null;
fromTitle?: string | null;
@@ -61,7 +83,9 @@ const getActivityText = ({
toDueDate?: Date | null;
dateLocale: DateFnsLocale;
mergedLabels?: string[];
attachmentName?: string | null;
}) => {
const displayName = memberName ?? memberEmail ?? t`Member`;
const TextHighlight = ({ children }: { children: React.ReactNode }) => (
<span className="font-medium text-light-1000 dark:text-dark-1000">
{children}
@@ -113,6 +137,8 @@ const getActivityText = ({
"card.updated.checklist.item.completed": t`completed a checklist item`,
"card.updated.checklist.item.uncompleted": t`marked a checklist item as incomplete`,
"card.updated.checklist.item.deleted": t`deleted a checklist item`,
"card.updated.attachment.added": t`added an attachment`,
"card.updated.attachment.removed": t`removed an attachment`,
"card.updated.dueDate.added": t`set the due date`,
"card.updated.dueDate.updated": t`updated the due date`,
"card.updated.dueDate.removed": t`removed the due date`,
@@ -139,23 +165,23 @@ const getActivityText = ({
);
}
if (type === "card.updated.member.added" && memberName) {
if (type === "card.updated.member.added" && displayName) {
if (isSelf) return <Trans>self-assigned the card</Trans>;
return (
<Trans>
assigned <TextHighlight>{truncate(memberName)}</TextHighlight> to the
assigned <TextHighlight>{truncate(displayName)}</TextHighlight> to the
card
</Trans>
);
}
if (type === "card.updated.member.removed" && memberName) {
if (type === "card.updated.member.removed" && displayName) {
if (isSelf) return <Trans>unassigned themselves from the card</Trans>;
return (
<Trans>
unassigned <TextHighlight>{truncate(memberName)}</TextHighlight> from
unassigned <TextHighlight>{truncate(displayName)}</TextHighlight> from
the card
</Trans>
);
@@ -245,6 +271,27 @@ const getActivityText = ({
);
}
if (type === "card.updated.attachment.added") {
const filename = attachmentName ?? toTitle;
if (!filename) return baseText;
return (
<Trans>
added an attachment <TextHighlight>{truncate(filename)}</TextHighlight>
</Trans>
);
}
if (type === "card.updated.attachment.removed") {
const filename = attachmentName ?? fromTitle;
if (!filename) return baseText;
return (
<Trans>
removed an attachment{" "}
<TextHighlight>{truncate(filename)}</TextHighlight>
</Trans>
);
}
if (type === "card.updated.dueDate.added" && toDueDate) {
const showYear = !isSameYear(toDueDate, new Date());
const formattedDate = format(
@@ -297,6 +344,8 @@ const ACTIVITY_ICON_MAP: Partial<Record<ActivityType, React.ReactNode | null>> =
"card.updated.checklist.item.completed": <HiOutlineCheckCircle />,
"card.updated.checklist.item.uncompleted": <HiOutlineCheckCircle />,
"card.updated.checklist.item.deleted": <HiOutlineTrash />,
"card.updated.attachment.added": <HiOutlinePaperClip />,
"card.updated.attachment.removed": <HiOutlinePaperClip />,
"card.updated.dueDate.added": <HiOutlineClock />,
"card.updated.dueDate.updated": <HiOutlineClock />,
"card.updated.dueDate.removed": <HiOutlineClock />,
@@ -330,7 +379,7 @@ const ActivityList = ({
isAdmin?: boolean;
isViewOnly?: boolean;
}) => {
const { dateLocale, locale } = useLocalisation();
const { dateLocale } = useLocalisation();
const { data: sessionData } = authClient.useSession();
const utils = api.useUtils();
const [allActivities, setAllActivities] = useState<
@@ -352,7 +401,7 @@ const ActivityList = ({
limit: ACTIVITIES_PAGE_SIZE,
},
{
enabled: !!cardPublicId,
enabled: !!cardPublicId && cardPublicId.length >= 12,
},
);
@@ -380,25 +429,21 @@ const ActivityList = ({
cursor: nextCursor,
});
if (nextPage) {
const existingIds = new Set(
currentActivities.map((a) => a.publicId),
);
const newActivities = nextPage.activities.filter(
(a: { publicId: string }) => !existingIds.has(a.publicId),
);
currentActivities = [...currentActivities, ...newActivities];
currentHasMore = nextPage.hasMore;
} else {
break;
}
const existingIds = new Set(
currentActivities.map((a) => a.publicId),
);
const newActivities = nextPage.activities.filter(
(a: { publicId: string }) => !existingIds.has(a.publicId),
);
currentActivities = [...currentActivities, ...newActivities];
currentHasMore = nextPage.hasMore;
}
setAllActivities(currentActivities);
setHasMore(false);
};
fetchAllRemaining();
void fetchAllRemaining();
} else {
setAllActivities(firstPageData.activities);
setHasMore(firstPageData.hasMore);
@@ -425,17 +470,15 @@ const ActivityList = ({
cursor: nextCursor,
});
if (nextPage) {
const existingIds = new Set(allActivities.map((a) => a.publicId));
const newActivities = nextPage.activities.filter(
(a: { publicId: string }) => !existingIds.has(a.publicId),
);
setAllActivities((prev) => [...prev, ...newActivities]);
setHasMore(nextPage.hasMore);
const existingIds = new Set(allActivities.map((a) => a.publicId));
const newActivities = nextPage.activities.filter(
(a: { publicId: string }) => !existingIds.has(a.publicId),
);
setAllActivities((prev) => [...prev, ...newActivities]);
setHasMore(nextPage.hasMore);
if (!nextPage.hasMore) {
isFullyExpandedRef.current = true;
}
if (!nextPage.hasMore) {
isFullyExpandedRef.current = true;
}
} finally {
setIsLoadingMore(false);
@@ -455,13 +498,17 @@ const ActivityList = ({
fromList: activity.fromList?.name ?? null,
toList: activity.toList?.name ?? null,
memberName: activity.member?.user?.name ?? null,
memberEmail: activity.member?.user?.email ?? null,
isSelf: activity.member?.user?.id === sessionData?.user.id,
label: activity.label?.name ?? null,
fromTitle: activity.fromTitle ?? null,
fromDueDate: activity.fromDueDate ?? null,
toDueDate: activity.toDueDate ?? null,
dateLocale: dateLocale,
mergedLabels: (activity as any).mergedLabels,
mergedLabels: (activity as ActivityWithMergedLabels).mergedLabels,
attachmentName:
(activity as ActivityWithMergedLabels).attachment?.originalFilename ??
null,
});
if (activity.type === "card.updated.comment.added")
@@ -508,7 +555,7 @@ const ActivityList = ({
)}
</div>
<p className="text-sm">
<span className="font-medium dark:text-dark-1000">{`${activity.user?.name} `}</span>
<span className="font-medium dark:text-dark-1000">{`${getUserDisplayName(activity.user)} `}</span>
<span className="space-x-1 text-light-900 dark:text-dark-800">
{activityText}
</span>

View File

@@ -7,6 +7,7 @@ import { twMerge } from "tailwind-merge";
import Button from "~/components/Button";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { env } from "next-runtime-env";
import { api } from "~/utils/api";
import { invalidateCard } from "~/utils/cardInvalidation";
@@ -18,62 +19,33 @@ export function AttachmentUpload({ cardPublicId }: { cardPublicId: string }) {
const [isDragging, setIsDragging] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null);
const generateUploadUrl = api.attachment.generateUploadUrl.useMutation();
const confirmAttachment = api.attachment.confirm.useMutation({
onSuccess: async () => {
const uploadFile = async (file: File) => {
setUploading(true);
try {
const baseUrl = env("NEXT_PUBLIC_BASE_URL") ?? "";
const response = await fetch(
`${baseUrl}/api/upload/attachment?cardPublicId=${encodeURIComponent(cardPublicId)}`,
{
method: "POST",
headers: {
"Content-Type": file.type,
"x-original-filename": file.name,
},
body: file,
},
);
if (!response.ok) {
throw new Error("Upload failed");
}
await invalidateCard(utils, cardPublicId);
showPopup({
header: t`Attachment uploaded`,
message: t`Your file has been uploaded successfully.`,
icon: "success",
});
},
onError: () => {
showPopup({
header: t`Upload failed`,
message: t`Failed to upload attachment. Please try again.`,
icon: "error",
});
},
onSettled: () => {
setUploading(false);
},
});
const uploadFile = async (file: File) => {
setUploading(true);
try {
// Generate presigned URL
const { url, key } = await generateUploadUrl.mutateAsync({
cardPublicId,
filename: file.name,
contentType: file.type,
size: file.size,
});
// Upload file to S3
const uploadResponse = await fetch(url, {
method: "PUT",
body: file,
headers: {
"Content-Type": file.type,
},
});
if (!uploadResponse.ok) {
throw new Error("Upload failed");
}
// Confirm attachment in database
await confirmAttachment.mutateAsync({
cardPublicId,
s3Key: key,
filename: file.name,
originalFilename: file.name,
contentType: file.type,
size: file.size,
});
} catch {
showPopup({
header: t`Upload failed`,

View File

@@ -1,12 +1,13 @@
import { t } from "@lingui/core/macro";
import { formatDistanceToNow } from "date-fns";
import { useState } from "react";
import ContentEditable from "react-contenteditable";
import { useForm } from "react-hook-form";
import { HiEllipsisHorizontal, HiPencil, HiTrash } from "react-icons/hi2";
import Avatar from "~/components/Avatar";
import Button from "~/components/Button";
import Editor from "~/components/Editor";
import type { WorkspaceMember } from "~/components/Editor";
import Dropdown from "~/components/Dropdown";
import { usePermissions } from "~/hooks/usePermissions";
import { useModal } from "~/providers/modal";
@@ -29,7 +30,6 @@ const Comment = ({
createdAt,
comment,
isAuthor,
isAdmin,
isEdited = false,
isViewOnly = false,
}: {
@@ -42,7 +42,6 @@ const Comment = ({
createdAt: string;
comment: string | undefined;
isAuthor: boolean;
isAdmin: boolean;
isEdited: boolean;
isViewOnly: boolean;
}) => {
@@ -57,6 +56,30 @@ const Comment = ({
},
});
const { data: cardData } = api.card.byId.useQuery(
{
cardPublicId,
},
{
enabled: !!cardPublicId && cardPublicId.length >= 12,
},
);
const workspaceMembers: WorkspaceMember[] =
cardData?.list.board.workspace.members
.filter((member) => member.email)
.map((member) => ({
publicId: member.publicId,
email: member.email,
user: member.user
? {
id: member.user.id,
name: member.user.name ?? null,
image: member.user.image ?? null,
}
: null,
})) ?? [];
if (!publicId) return null;
const updateCommentMutation = api.card.updateComment.useMutation({
@@ -142,21 +165,28 @@ const Comment = ({
)}
</div>
{!isEditing ? (
<ContentEditable
html={comment ?? ""}
disabled={true}
className="break-anywhere mt-2 text-sm"
/>
<div className="mt-2">
<Editor
content={comment ?? null}
readOnly={true}
workspaceMembers={workspaceMembers}
enableYouTubeEmbed={false}
disableHeadings={true}
/>
</div>
) : (
<form onSubmit={handleSubmit(onSubmit)}>
<ContentEditable
placeholder={t`Add a comment...`}
html={watch("comment")}
disabled={false}
onChange={(e) => setValue("comment", e.target.value)}
className="block w-full max-w-[800px] border-0 bg-transparent py-1.5 text-sm text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
/>
<div className="flex justify-end space-x-2">
<div className="mt-2">
<Editor
content={watch("comment")}
onChange={(value) => setValue("comment", value)}
workspaceMembers={workspaceMembers}
enableYouTubeEmbed={false}
placeholder={t`Add comment... (type '/' to open commands or '@' to mention)`}
disableHeadings={true}
/>
</div>
<div className="flex justify-end space-x-2 mt-2">
<Button
size="sm"
variant="ghost"

View File

@@ -1,6 +1,7 @@
import { t } from "@lingui/core/macro";
import {
HiEllipsisHorizontal,
HiLink,
HiOutlineCheckCircle,
HiOutlineTrash,
} from "react-icons/hi2";
@@ -10,18 +11,54 @@ import { authClient } from "@kan/auth/client";
import Dropdown from "~/components/Dropdown";
import { usePermissions } from "~/hooks/usePermissions";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
export default function CardDropdown({
cardPublicId,
isTemplate,
boardPublicId,
cardCreatedBy,
}: {
cardPublicId: string;
isTemplate?: boolean;
boardPublicId?: string;
cardCreatedBy?: string | null;
}) {
const { openModal } = useModal();
const { showPopup } = usePopup();
const { canEditCard, canDeleteCard } = usePermissions();
const { data: session } = authClient.useSession();
const isCreator = cardCreatedBy && session?.user.id === cardCreatedBy;
const handleCopyCardLink = async () => {
const path =
isTemplate && boardPublicId
? `/templates/${boardPublicId}/cards/${cardPublicId}`
: `/cards/${cardPublicId}`;
const url = `${window.location.origin}${path}`;
try {
await navigator.clipboard.writeText(url);
showPopup({
header: t`Link copied`,
icon: "success",
message: t`Card URL copied to clipboard`,
});
} catch (error) {
console.error(error);
showPopup({
header: t`Unable to copy link`,
icon: "error",
message: t`Please try again.`,
});
}
};
const items = [
{
label: t`Copy card link`,
action: handleCopyCardLink,
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
},
...(canEditCard
? [
{

View File

@@ -1,8 +1,9 @@
import { t } from "@lingui/core/macro";
import ContentEditable from "react-contenteditable";
import { useForm } from "react-hook-form";
import { HiOutlineArrowUp } from "react-icons/hi2";
import Editor from "~/components/Editor";
import type { WorkspaceMember } from "~/components/Editor";
import LoadingSpinner from "~/components/LoadingSpinner";
import { usePermissions } from "~/hooks/usePermissions";
import { usePopup } from "~/providers/popup";
@@ -13,7 +14,13 @@ interface FormValues {
comment: string;
}
const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
const NewCommentForm = ({
cardPublicId,
workspaceMembers,
}: {
cardPublicId: string;
workspaceMembers: WorkspaceMember[];
}) => {
const utils = api.useUtils();
const { showPopup } = usePopup();
const { canCreateComment } = usePermissions();
@@ -23,10 +30,6 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
},
});
const queryParams = {
cardPublicId,
};
const addCommentMutation = api.card.addComment.useMutation({
onError: (_error, _newList) => {
showPopup({
@@ -57,18 +60,13 @@ const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
onSubmit={handleSubmit(onSubmit)}
className="flex w-full max-w-[800px] flex-col rounded-xl border border-light-600 bg-light-100 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={t`Add a comment...`}
html={watch("comment")}
disabled={false}
onChange={(e) => setValue("comment", e.target.value)}
className="block w-full border-0 bg-transparent py-1.5 text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
onKeyDown={async (e) => {
if (e.key === "Enter" && e.shiftKey) {
e.preventDefault();
await handleSubmit(onSubmit)();
}
}}
<Editor
content={watch("comment")}
onChange={(value) => setValue("comment", value)}
workspaceMembers={workspaceMembers}
enableYouTubeEmbed={false}
placeholder={t`Add comment... (type '/' to open commands or '@' to mention)`}
disableHeadings={true}
/>
<div className="flex justify-end">
<button

View File

@@ -4,6 +4,9 @@ import { t } from "@lingui/core/macro";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { IoChevronForwardSharp } from "react-icons/io5";
import { HiXMark } from "react-icons/hi2";
import { authClient } from "@kan/auth/client";
import Avatar from "~/components/Avatar";
import Editor from "~/components/Editor";
@@ -14,8 +17,6 @@ import Modal from "~/components/modal";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import { PageHead } from "~/components/PageHead";
import { EditYouTubeModal } from "~/components/YouTubeEmbed/EditYouTubeModal";
import { authClient } from "@kan/auth/client";
import { usePermissions } from "~/hooks/usePermissions";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
@@ -53,9 +54,10 @@ export function CardRightPanel({ isTemplate }: { isTemplate?: boolean }) {
? router.query.cardId[0]
: router.query.cardId;
const { data: card } = api.card.byId.useQuery({
cardPublicId: cardId ?? "",
});
const { data: card } = api.card.byId.useQuery(
{ cardPublicId: cardId ?? "" },
{ enabled: !!cardId && cardId.length >= 12 },
);
const isCreator = card?.createdBy && session?.user.id === card.createdBy;
const canEdit = canEditCard || isCreator;
@@ -166,7 +168,6 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
const {
modalContentType,
entityId,
openModal,
getModalState,
clearModalState,
isOpen,
@@ -184,9 +185,10 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
? router.query.cardId[0]
: router.query.cardId;
const { data: card, isLoading } = api.card.byId.useQuery({
cardPublicId: cardId ?? "",
});
const { data: card, isLoading } = api.card.byId.useQuery(
{ cardPublicId: cardId ?? "" },
{ enabled: !!cardId && cardId.length >= 12 },
);
const isCreator = card?.createdBy && session?.user.id === card.createdBy;
const canEdit = canEditCard || isCreator;
@@ -196,8 +198,24 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
};
const board = card?.list.board;
const workspaceMembers = board?.workspace.members;
const boardId = board?.publicId;
const editorWorkspaceMembers =
workspaceMembers
?.filter((member) => member.email)
.map((member) => ({
publicId: member.publicId,
email: member.email,
user: member.user
? {
id: member.user.id,
name: member.user.name ?? null,
image: member.user.image ?? null,
}
: null,
})) ?? [];
const updateCard = api.card.update.useMutation({
onError: () => {
showPopup({
@@ -284,6 +302,7 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
if (!cardId) return <></>;
return (
<>
<PageHead
@@ -315,7 +334,19 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
</Link>
</div>
<div className="flex items-center gap-2">
<Dropdown cardCreatedBy={card?.createdBy} />
<Dropdown
cardPublicId={cardId}
isTemplate={isTemplate}
boardPublicId={boardId}
cardCreatedBy={card?.createdBy}
/>
<Link
href={`/${isTemplate ? "templates" : "boards"}/${boardId}`}
className="flex h-7 w-7 items-center justify-center rounded-[5px] text-light-900 hover:bg-light-200 dark:text-dark-900 dark:hover:bg-dark-200"
aria-label={t`Close`}
>
<HiXMark className="h-5 w-5" />
</Link>
</div>
</>
)}
@@ -372,9 +403,15 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
<div className="mt-2">
<Editor
content={card.description}
onChange={canEdit ? (e) => setValue("description", e) : undefined}
onBlur={canEdit ? () => handleSubmit(onSubmit)() : undefined}
workspaceMembers={board?.workspace.members ?? []}
onChange={
canEdit
? (e) => setValue("description", e)
: undefined
}
onBlur={
canEdit ? () => handleSubmit(onSubmit)() : undefined
}
workspaceMembers={workspaceMembers ?? []}
readOnly={!canEdit}
/>
</div>
@@ -418,7 +455,10 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
</div>
{!isTemplate && (
<div className="mt-6">
<NewCommentForm cardPublicId={cardId} />
<NewCommentForm
cardPublicId={cardId}
workspaceMembers={editorWorkspaceMembers}
/>
</div>
)}
</div>

View File

@@ -37,7 +37,7 @@ export default function MembersPage() {
const { data, isLoading } = api.workspace.byId.useQuery(
{ workspacePublicId: workspace.publicId },
// { enabled: workspace?.publicId ? true : false },
{ enabled: !!workspace.publicId && workspace.publicId.length >= 12 },
);
const { data: session } = authClient.useSession();
@@ -48,9 +48,11 @@ export default function MembersPage() {
const updateRoleMutation = api.member.updateRole.useMutation({
onSuccess: async () => {
await utils.workspace.byId.invalidate({
workspacePublicId: workspace.publicId,
});
if (workspace.publicId && workspace.publicId.length >= 12) {
await utils.workspace.byId.invalidate({
workspacePublicId: workspace.publicId,
});
}
showPopup({
header: t`Role updated`,
@@ -124,7 +126,6 @@ export default function MembersPage() {
name={memberName ?? ""}
email={memberEmail ?? ""}
imageUrl={memberImage ? getAvatarUrl(memberImage) : undefined}
icon={showPendingIcon ? "?" : undefined}
/>
)}
</div>

View File

@@ -1,293 +1,587 @@
import { t } from "@lingui/core/macro";
import React, { useEffect, useRef, useState } from "react";
import { HiCheckCircle, HiXCircle } from "react-icons/hi2";
interface Feature {
key: string;
label: string;
kan: boolean | string;
trello: boolean | string;
}
interface Section {
name: string;
features: Feature[];
}
import { twMerge } from "tailwind-merge";
type FrequencyValue = "monthly" | "annually";
const CellValue = ({ value }: { value: boolean | string }) => {
if (typeof value === "string") {
return (
<span className="text-sm text-dark-900 dark:text-dark-900">{value}</span>
);
}
return value ? (
<HiCheckCircle className="h-5 w-5 text-light-950 dark:text-dark-1000" />
) : (
<HiXCircle className="h-5 w-5 text-light-700 dark:text-dark-600" />
);
};
interface PlanFeature {
key: string;
label: string;
free: string | boolean | { text: string; highlight?: string };
teams: string | boolean | { text: string; highlight?: string };
pro: string | boolean | { text: string; highlight?: string };
enterprise: string | boolean | { text: string; highlight?: string };
}
interface FeatureSection {
name: string;
features: PlanFeature[];
}
const FeatureComparisonTable = ({
frequencyValue,
frequencyValue: _frequencyValue,
}: {
frequencyValue: FrequencyValue;
}) => {
const sections: Section[] = [
// Keep pricing column headers visible as you scroll
const containerRef = useRef<HTMLDivElement | null>(null);
const [isHeaderFixed, setIsHeaderFixed] = useState(false);
const [headerRect, setHeaderRect] = useState<{ left: number; width: number }>({
left: 0,
width: 0,
});
useEffect(() => {
const HEADER_OFFSET = 64; // px; matches fixed top nav height
const handleScroll = () => {
if (!containerRef.current) return;
// Hide fixed header on mobile (screens smaller than 620px)
if (window.innerWidth < 620) {
setIsHeaderFixed(false);
return;
}
const rect = containerRef.current.getBoundingClientRect();
const pastHeader = rect.top <= HEADER_OFFSET;
const aboveBottom = rect.bottom > HEADER_OFFSET + 40;
const shouldFix = pastHeader && aboveBottom;
setIsHeaderFixed(shouldFix);
if (shouldFix) {
setHeaderRect({
left: rect.left,
width: rect.width,
});
}
};
handleScroll();
window.addEventListener("scroll", handleScroll, { passive: true });
window.addEventListener("resize", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", handleScroll);
};
}, []);
const planSpecificLimits: PlanFeature[] = [
{
name: t`Core features`,
features: [
{
key: "ulimited-workspaces",
label: t`Unlimited workspaces`,
kan: true,
trello: false,
},
{
key: "unlimited-boards",
label: t`Unlimited boards`,
kan: true,
trello: false,
},
{
key: "unlimited-lists",
label: t`Unlimited lists`,
kan: true,
trello: true,
},
{
key: "unlimited-cards",
label: t`Unlimited cards`,
kan: true,
trello: true,
},
{
key: "activity-log",
label: t`Activity log`,
kan: true,
trello: true,
},
{
key: "templates",
label: t`Custom templates`,
kan: true,
trello: false,
},
{ key: "labels", label: t`Labels & filters`, kan: true, trello: true },
{ key: "checklists", label: t`Checklists`, kan: true, trello: true },
{
key: "import",
label: t`Import from Trello`,
kan: true,
trello: false,
},
{
key: "visibility",
label: t`Board visibility`,
kan: true,
trello: true,
},
{
key: "search",
label: t`Intelligent search`,
kan: true,
trello: true,
},
{
key: "workspace-url",
label: t`Custom workspace link`,
kan: true,
trello: false,
},
],
key: "boards",
label: t`Boards`,
free: { text: t`Unlimited boards`, highlight: "Unlimited" },
teams: { text: t`Unlimited boards`, highlight: "Unlimited" },
pro: { text: t`Unlimited boards`, highlight: "Unlimited" },
enterprise: { text: t`Unlimited boards`, highlight: "Unlimited" },
},
{
name: t`Teams`,
features: [
{
key: "pricing",
label: t`Price per user/month`,
kan: frequencyValue === "monthly" ? `$10.00` : `$8.00`,
trello: frequencyValue === "monthly" ? `$12.00` : `$10.00`,
},
{
key: "comments",
label: t`Comments & mentions`,
kan: true,
trello: true,
},
{
key: "assignments",
label: t`Assignees`,
kan: true,
trello: true,
},
{
key: "sharing",
label: t`Board sharing & invites`,
kan: true,
trello: true,
},
{
key: "invite-links",
label: t`Invite links`,
kan: true,
trello: true,
},
],
key: "members",
label: t`Members`,
free: { text: t`1 user`, highlight: "1" },
teams: { text: t`Per-seat pricing`, highlight: "Per-seat" },
pro: { text: t`Unlimited members`, highlight: "Unlimited" },
enterprise: { text: t`Unlimited members`, highlight: "Unlimited" },
},
{
key: "file-uploads",
label: t`File uploads`,
free: { text: t`10mb file uploads`, highlight: "10mb" },
teams: { text: t`Unlimited file uploads`, highlight: "Unlimited" },
pro: { text: t`Unlimited file uploads`, highlight: "Unlimited" },
enterprise: { text: t`Unlimited file uploads`, highlight: "Unlimited" },
},
{
key: "workspace-username",
label: t`Workspace username`,
free: { text: t`Default username`, highlight: "Default" },
teams: { text: t`Default username`, highlight: "Default" },
pro: { text: t`Custom username`, highlight: "Custom" },
enterprise: { text: t`Custom username`, highlight: "Custom" },
},
];
const coreFeatures: PlanFeature[] = [
{
key: "checklists",
label: t`Checklists`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "custom-templates",
label: t`Board templates`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "labels-filters",
label: t`Labels & filters`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "board-visibility",
label: t`Board visibility`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "search",
label: t`Intelligent search`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "activity-log",
label: t`Activity log`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "comments",
label: t`Comments`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "roles-permissions",
label: t`Roles & permissions`,
free: false,
teams: false,
pro: true,
enterprise: true,
},
{
key: "custom-branding",
label: t`Custom branding`,
free: false,
teams: false,
pro: true,
enterprise: true,
},
];
const collaborationFeatures: PlanFeature[] = [
{
key: "mentions",
label: t`Mentions`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "email-notifications",
label: t`Email notifications`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "assignees",
label: t`Assignees`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "invite-links",
label: t`Invite links`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
];
const importsFeatures: PlanFeature[] = [
{
key: "import-export",
label: t`Trello`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
];
const platformFeatures: PlanFeature[] = [
{
key: "open-source",
label: t`Open source`,
free: true,
teams: true,
pro: true,
enterprise: true,
},
{
key: "rest-api",
label: t`API`,
free: { text: t`Limited API access` },
teams: { text: t`Full API access` },
pro: { text: t`Full API access` },
enterprise: { text: t`Full API access` },
},
{
key: "self-hostable",
label: t`On-premise`,
free: false,
teams: false,
pro: false,
enterprise: true,
},
];
const securityFeatures: PlanFeature[] = [
{
key: "sso",
label: t`SSO`,
free: { text: t`Google SSO` },
teams: { text: t`Google SSO` },
pro: { text: t`Google SSO` },
enterprise: { text: t`Google SSO + SAML` },
},
{
key: "admin-roles",
label: t`Admin roles`,
free: { text: t`Admin roles` },
teams: { text: t`Admin roles` },
pro: { text: t`Admin roles` },
enterprise: { text: t`Advanced admin roles` },
},
];
const supportFeatures: PlanFeature[] = [
{
key: "priority-email-support",
label: t`Priority email support`,
free: false,
teams: true,
pro: true,
enterprise: true,
},
{
key: "account-manager",
label: t`Account manager`,
free: false,
teams: false,
pro: false,
enterprise: true,
},
{
key: "sla",
label: t`SLA`,
free: false,
teams: false,
pro: false,
enterprise: true,
},
];
const sections: FeatureSection[] = [
{
name: "",
features: planSpecificLimits,
},
{
name: t`Core features`,
features: coreFeatures,
},
{
name: t`Collaboration`,
features: collaborationFeatures,
},
{
name: t`Imports`,
features: importsFeatures,
},
{
name: t`Platform`,
features: [
{
key: "api",
label: t`REST API`,
kan: true,
trello: true,
},
{
key: "open-source",
label: t`Open source`,
kan: true,
trello: false,
},
{
key: "self-hostable",
label: t`Self-hostable`,
kan: true,
trello: false,
},
{
key: "white-label",
label: t`White label`,
kan: true,
trello: false,
},
{
key: "performance",
label: t`Fast & lightweight`,
kan: true,
trello: false,
},
{
key: "this-is-a-joke",
label: t`Owned by Atlassian`,
kan: false,
trello: true,
},
],
features: platformFeatures,
},
{
name: t`Security`,
features: securityFeatures,
},
{
name: t`Support`,
features: supportFeatures,
},
];
const products = [
{
id: "kan",
name: t`Kan`,
featured: true,
},
{
id: "trello",
name: t`Trello`,
featured: false,
},
const plans = [
{ id: "free", name: t`Free`, tierId: "tier-free" },
{ id: "teams", name: t`Teams`, tierId: "tier-teams" },
{ id: "pro", name: t`Pro`, tierId: "tier-pro" },
{ id: "enterprise", name: t`Enterprise`, tierId: "tier-enterprise" },
];
const isHighlightValue = (
value: string | boolean | { text: string; highlight?: string },
): value is { text: string; highlight?: string } => {
return typeof value === "object" && value !== null && "text" in value;
};
const shouldHighlight = (text: string): boolean => {
const lowerText = text.toLowerCase();
return ["unlimited", "multiple", "per-seat", "custom", "mentions", "full", "sso", "admin"].some(
(keyword) => lowerText.includes(keyword),
);
};
const CellValue = ({
value,
label,
}: {
value: string | boolean | { text: string; highlight?: string };
label: string;
}) => {
// Handle object with text and highlight
if (isHighlightValue(value)) {
const { text, highlight } = value;
const isUnlimited = shouldHighlight(text);
let displayText;
if (highlight) {
const parts = text.split(new RegExp(`(${highlight})`, "gi"));
displayText = (
<>
{parts.map((part, index) =>
part.toLowerCase() === highlight.toLowerCase() ? (
<span key={index} className="text-light-1000 dark:text-dark-1000">
{part}
</span>
) : (
<span key={index} className="text-light-950 dark:text-dark-800">
{part}
</span>
),
)}
</>
);
} else {
displayText = text;
}
return (
<div className="flex items-center gap-2.5">
<HiCheckCircle
className={twMerge(
"mt-0.5 h-4 w-4 shrink-0",
isUnlimited
? "text-light-1000 dark:text-dark-1000"
: "text-light-400 dark:text-dark-600",
)}
/>
<span
className={twMerge(
"text-sm font-medium",
isUnlimited
? "text-light-1000 dark:text-dark-950"
: "text-light-950 dark:text-dark-800",
)}
>
{displayText}
</span>
</div>
);
}
if (typeof value === "string") {
const isUnlimited = shouldHighlight(value);
return (
<div className="flex items-center gap-2.5">
<HiCheckCircle
className={twMerge(
"mt-0.5 h-4 w-4 shrink-0",
isUnlimited
? "text-light-1000 dark:text-dark-1000"
: "text-light-400 dark:text-dark-600",
)}
/>
<span
className={twMerge(
"text-sm font-medium",
isUnlimited
? "text-light-1000 dark:text-dark-950"
: "text-light-950 dark:text-dark-800",
)}
>
{value}
</span>
</div>
);
}
if (value) {
// Boolean true - show checkmark icon
return (
<div className="flex items-center gap-2.5">
<HiCheckCircle className="mt-0.5 h-4 w-4 shrink-0 text-light-1000 dark:text-dark-1000" />
<span className="text-sm font-medium text-light-1000 dark:text-dark-950">
{label}
</span>
</div>
);
}
// Boolean false - show cross icon
return (
<div className="flex items-center gap-2.5">
<HiXCircle className="mt-0.5 h-4 w-4 shrink-0 text-light-400 dark:text-dark-600" />
<span className="text-sm font-medium text-light-800 dark:text-dark-800">
{label}
</span>
</div>
);
};
const getFeatureValue = (
feature: PlanFeature,
planId: string,
): string | boolean | { text: string; highlight?: string } => {
switch (planId) {
case "free":
return feature.free;
case "teams":
return feature.teams;
case "pro":
return feature.pro;
case "enterprise":
return feature.enterprise;
default:
return false;
}
};
return (
<section aria-labelledby="comparison-heading" className="w-full px-4">
<div className="mx-auto max-w-6xl py-8 lg:py-10">
<div className="grid grid-cols-3 gap-x-8 border-t border-light-500 before:block dark:border-dark-300">
{products.map((product) => (
<div key={product.id} aria-hidden="true" className="-mt-px">
<div
className={`${product.featured ? "border-dark-200 dark:border-dark-800" : "border-transparent"} border-t-2 pt-8`}
>
<p
className={`${product.featured ? "text-dark-50 dark:text-dark-1000" : "text-light-1000 dark:text-dark-1000"} text-center text-sm font-semibold`}
>
{product.name}
</p>
<section aria-labelledby="comparison-heading" className="w-full">
<div className="mx-auto max-w-7xl">
<div
ref={containerRef}
className="relative overflow-x-auto rounded-lg border border-light-300 bg-light-50 dark:border-dark-300 dark:bg-dark-50"
>
{isHeaderFixed && (
<div
className="hidden sm:block fixed z-40 border-b border-light-300 bg-light-50/95 dark:border-dark-400 dark:bg-dark-50/95"
style={{
top: 64,
left: headerRect.left,
width: headerRect.width,
}}
>
<div className="grid grid-cols-4 border-x border-light-300 dark:border-dark-300">
{plans.map((plan) => {
const isMostPopular = plan.id === "pro";
return (
<div
key={`fixed-${plan.id}`}
className={twMerge(
"flex items-center px-6 py-3 text-left text-base font-semibold",
isMostPopular
? "bg-light-200 text-light-1000 dark:bg-dark-100 dark:text-dark-1000"
: "bg-light-50 text-dark-50 dark:bg-dark-50 dark:text-dark-1000",
)}
>
{plan.name}
{plan.id === "pro" && <span className="ml-1 text-xl"></span>}
</div>
);
})}
</div>
</div>
))}
</div>
<div className="-mt-6 space-y-12">
{sections.map((section) => (
<div key={section.name}>
<h4 className="text-sm font-semibold text-light-1000 dark:text-dark-1000">
{section.name}
</h4>
<div className="relative -mx-8 mt-8">
<div
aria-hidden="true"
className="absolute inset-x-8 inset-y-0 grid grid-cols-3 gap-x-8 before:block"
>
{products.map((product) => (
<div
key={product.id}
className={`size-full rounded-lg ${product.featured ? "bg-white shadow-sm dark:bg-dark-50 dark:shadow-none" : ""}`}
/>
))}
</div>
<table className="relative w-full border-separate border-spacing-x-8">
<thead>
<tr className="text-left">
<th scope="col">
<span className="sr-only">{t`Feature`}</span>
</th>
{products.map((p) => (
<th key={p.id} scope="col">
<span className="sr-only">{p.name}</span>
</th>
))}
</tr>
</thead>
<tbody>
{section.features.map((feature, featureIdx) => (
<tr key={feature.key}>
<th
scope="row"
className="w-1/3 py-3 pr-4 text-left text-sm font-normal text-light-1000 dark:text-dark-1000"
>
{feature.label}
{featureIdx !== section.features.length - 1 ? (
<div className="absolute inset-x-8 mt-3 h-px bg-light-500 dark:bg-dark-300" />
) : null}
</th>
{products.map((p) => (
)}
<table className="w-full border-separate border-spacing-0">
<thead>
<tr>
{plans.map((plan) => {
const isMostPopular = plan.id === "pro";
return (
<th
key={plan.id}
scope="col"
className={twMerge(
"w-1/4 px-6 py-4 text-left text-base font-semibold",
isMostPopular
? "bg-light-200 text-light-1000 dark:bg-dark-100 dark:text-dark-1000"
: "bg-light-50 text-dark-50 dark:bg-dark-50 dark:text-dark-1000",
)}
>
<span className="flex items-center">
{plan.name}
{plan.id === "pro" && <span className="ml-1 text-xl"></span>}
</span>
</th>
);
})}
</tr>
</thead>
<tbody>
{sections.map((section, sectionIdx) => (
<React.Fragment key={section.name || `section-${sectionIdx}`}>
{section.name && (
<tr>
{plans.map((plan) => {
const isPro = plan.id === "pro";
return (
<td
key={p.id}
className="relative w-1/3 px-4 py-0 text-center"
key={plan.id}
className={twMerge(
"border-t px-6 py-3 text-left text-sm font-semibold text-dark-900 dark:text-dark-900",
isPro
? "border-light-400 bg-light-200 dark:border-dark-400 dark:bg-dark-100"
: "border-light-300 bg-light-50 dark:border-dark-400 dark:bg-dark-50",
)}
>
<span className="relative inline-flex w-full items-center justify-center py-3">
<CellValue
value={
p.id === "kan" ? feature.kan : feature.trello
}
/>
</span>
{plan.id === "free" ? section.name : ""}
</td>
))}
</tr>
))}
</tbody>
</table>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-x-8 inset-y-0 grid grid-cols-3 gap-x-8 before:block"
>
{products.map((product) => (
<div
key={product.id}
className={`${product.featured ? "ring-1 ring-light-500 dark:ring-dark-800" : "ring-0"} rounded-2xl`}
/>
);
})}
</tr>
)}
{section.features.map((feature) => (
<tr key={feature.key}>
{plans.map((plan) => {
const isPro = plan.id === "pro";
return (
<td
key={plan.id}
className={twMerge(
"w-1/4 border-t px-6 py-3 text-left",
isPro
? "border-light-400 bg-light-200 dark:border-dark-400 dark:bg-dark-100"
: "border-light-300 dark:border-dark-400",
)}
>
<CellValue
value={getFeatureValue(feature, plan.id)}
label={feature.label}
/>
</td>
);
})}
</tr>
))}
</div>
</div>
</div>
))}
</React.Fragment>
))}
</tbody>
</table>
</div>
</div>
</section>

View File

@@ -23,81 +23,115 @@ const Pricing = ({
}) => {
const tiers = [
{
name: t`Individuals`,
id: "tier-individuals",
name: t`Free`,
id: "tier-free",
href: "signup",
buttonText: t`Get Started`,
price: { monthly: t`Free`, annually: t`Free` },
description: t`Everything you need, free forever. Unlimited boards, unlimited lists, unlimited cards. Upgrade any time.`,
featureHeader: t`Free, forever`,
buttonText: t`Get started`,
price: { monthly: t`$0`, annually: t`$0` },
description: t`Free for everyone`,
bestFor: t`Best for individuals and solo creators getting started`,
featureHeader: undefined,
features: [
t`1 user`,
t`Unlimited boards`,
t`Unlimited cards`,
t`Custom board templates`,
t`Checklists`,
{ text: t`1 user` },
{ text: t`Unlimited boards` },
{ text: t`Unlimited cards` },
{ text: t`Custom board templates` },
{ text: t`Checklists` },
],
showPrice: true,
ctaSubtext: undefined,
mostPopular: false,
highlighted: false,
},
{
name: t`Teams`,
id: "tier-teams",
href: "signup",
buttonText: t`Get Started`,
price: { monthly: "$10.00", annually: "$8.00" },
description: t`Kan is better with a team. Perfect for small and growing teams looking to collaborate.`,
featureHeader: t`Everything in the free plan, plus:`,
buttonText: t`Get started`,
price: { monthly: "$10", annually: "$8" },
description: t`Perfect for small and growing teams looking to collaborate.`,
bestFor: t`Best for small and growing teams that need collaboration`,
featureHeader: undefined,
features: [
t`Workspace members`,
t`Admin roles`,
t`Priority email support`,
t`Support the development of the project`,
{ text: t`All Free features +` },
{ text: t`Workspace members` },
{ text: t`Invite links` },
{ text: t`Unlimited file uploads` },
{ text: t`Email notifications for mentions` },
{ text: t`Priority email support` },
],
highlighted: false,
showPrice: true,
showPriceSuffix: true,
ctaSubtext: t`14 day free trial · Switch plans or cancel anytime`,
mostPopular: false,
},
{
name: t`Pro`,
id: "tier-pro",
href: "signup",
buttonText: t`Get started`,
price: { monthly: "$29", annually: "$24" },
description: t`Unlimited seats and advanced features for growing teams.`,
bestFor: t`Best for teams that want to scale without limits`,
featureHeader: undefined,
features: [
{ text: t`All Teams features +` },
{ text: t`Unlimited members` },
{ text: t`Custom workspace username` },
{ text: t`Configurable user roles and permissions` },
{ text: t`Custom branding` },
],
highlighted: true,
showPrice: true,
showPriceSuffix: true,
showPriceSuffix: false,
ctaSubtext: t`14 day free trial · Switch plans or cancel anytime`,
mostPopular: true,
},
{
name: t`Self Host`,
id: "tier-self-host",
href: "https://github.com/kanbn/kan",
buttonText: t`View docs`,
price: { monthly: "-", annually: "-" },
description: t`Host Kan on your own infrastructure. Ideal for organisations that need complete control over their data.`,
featureHeader: t`Complete control and ownership:`,
name: t`Enterprise`,
id: "tier-enterprise",
href: "mailto:support@kan.bn?subject=Enterprise Inquiry",
buttonText: t`Contact Sales`,
price: { monthly: t`Contact us`, annually: t`Contact us` },
description: t`Advanced security, compliance, and dedicated support for large organizations.`,
bestFor: t`Best for large organizations with advanced needs`,
featureHeader: undefined,
features: [
t`Run on your own infrastructure`,
t`Own your data`,
t`Custom domain`,
{ text: t`All Pro features +` },
{ text: t`SAML SSO` },
{ text: t`Advanced admin controls` },
{ text: t`Dedicated account manager` },
{ text: t`Custom SLA` },
{ text: t`On-premise deployment option` },
],
highlighted: false,
showPrice: true,
showPriceSuffix: false,
ctaSubtext: undefined,
mostPopular: false,
showPrice: false,
},
];
return (
<>
<div className="flex flex-col items-center justify-center px-4 pb-10">
<div className="mt-14 flex flex-col items-center justify-center">
<div className="mb-8 flex items-center gap-2 rounded-full border bg-white px-4 py-1.5 text-center text-xs font-bold text-gray-800 dark:border-dark-300 dark:bg-dark-1000 dark:text-gray-800 lg:text-sm">
<HiBolt />
<p>{t`Launch offer: unlimited seats for just $29/month with Pro`}</p>
</div>
<div className="mx-auto max-w-7xl px-4">
<div className="mb-8 flex items-center justify-center">
<fieldset aria-label={t`Payment frequency`}>
<RadioGroup
value={frequency}
onChange={(value) => setFrequency(value)}
className="grid grid-cols-2 gap-x-1 rounded-full p-1 text-center text-xs/5 font-semibold ring-1 ring-inset ring-light-600 dark:ring-dark-600"
className="flex gap-1 rounded-lg border border-light-300 bg-light-50 p-1 dark:border-dark-300 dark:bg-dark-50"
>
{frequencies.map((option) => (
<Radio
key={option.value}
value={option}
className={twMerge(
"cursor-pointer rounded-full px-2.5 py-1 text-xs transition-colors lg:text-sm",
"cursor-pointer rounded-md px-4 py-1.5 text-sm font-medium transition-colors",
frequency?.value === option.value
? "bg-dark-50 text-white dark:bg-light-50 dark:text-dark-50"
: "text-light-900 hover:bg-light-100 dark:hover:bg-dark-100",
? "bg-white text-light-1000 shadow-sm dark:bg-dark-100 dark:text-dark-1000"
: "text-light-600 dark:text-dark-700",
)}
>
{option.label}
@@ -106,103 +140,129 @@ const Pricing = ({
</RadioGroup>
</fieldset>
</div>
</div>
<div className="isolate mx-auto mb-10 grid max-w-md grid-cols-1 gap-8 px-4 lg:mx-0 lg:max-w-none lg:grid-cols-3">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{tiers.map((tier) => (
<div
key={tier.id}
className={twMerge(
tier.highlighted
? "bg-dark-50 ring-1 ring-dark-50 dark:ring-dark-800"
: "bg-light-50 ring-1 ring-light-300 dark:bg-dark-50 dark:ring-dark-300",
"rounded-3xl p-8 xl:p-10",
"relative flex h-full flex-col rounded-lg border p-6",
tier.mostPopular
? "border-light-400 bg-light-200 dark:border-dark-400 dark:bg-dark-100"
: "border-light-300 bg-light-50 dark:border-dark-300 dark:bg-dark-50",
)}
>
<div className="flex items-center justify-between gap-x-4">
<h3
id={tier.id}
className={twMerge(
tier.highlighted
? "text-dark-1000 dark:text-dark-1000"
: "text-dark-50 dark:text-dark-1000",
"text-lg/8 font-semibold",
{tier.id === "tier-pro" && (
<div className="absolute right-2 top-2 z-10">
<span className="inline-flex items-center gap-1 rounded-full border border-light-400 bg-light-100 px-3 py-1 text-center text-xs text-dark-600 dark:border-dark-400 dark:bg-dark-100 dark:text-dark-900">
<HiBolt className="h-3 w-3 -ml-0.5" />
{t`Launch offer`}
</span>
</div>
)}
<h3
id={tier.id}
className={twMerge(
"mb-4 flex items-center text-base font-semibold",
tier.mostPopular
? "text-light-1000 dark:text-dark-1000"
: "text-light-1000 dark:text-dark-1000",
)}
>
{tier.name}
{tier.id === "tier-pro" && <span className="ml-1 text-xl leading-none"></span>}
</h3>
<div className="mb-4">
<p className="flex items-baseline gap-x-1">
<span
className={twMerge(
"text-2xl font-semibold",
tier.mostPopular
? "text-light-1000 dark:text-dark-1000"
: "text-light-1000 dark:text-dark-1000",
!tier.showPrice && "opacity-0",
)}
>
{tier.price[frequency?.value ?? "monthly"]}
</span>
{tier.id === "tier-pro" && (
<span
className={twMerge(
"text-2xl font-semibold line-through",
tier.mostPopular
? "text-light-600 dark:text-dark-600"
: "text-light-600 dark:text-dark-600",
)}
>
{frequency?.value === "annually" ? "$79" : "$100"}
</span>
)}
>
{tier.name}
</h3>
{tier.highlighted && (
<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">
{t`14 day free trial`}
</p>
{tier.showPriceSuffix && (
<span className="text-sm font-normal text-dark-400 dark:text-dark-700">
{frequency?.priceSuffix}
</span>
)}
{tier.id === "tier-pro" && (
<span className="text-sm font-normal text-dark-400 dark:text-dark-700">
{t`/month`}
</span>
)}
</p>
</div>
{tier.bestFor && (
<p className="mb-6 text-sm text-dark-400 dark:text-dark-700">
{tier.bestFor}
</p>
)}
<div className="mb-6">
{tier.id === "tier-enterprise" ? (
<a
href={tier.href}
aria-describedby={tier.id}
className="block w-full rounded-md border border-dark-200 bg-transparent px-4 py-2 text-center text-sm font-medium text-dark-50 transition-colors hover:bg-dark-50 hover:text-light-50 dark:border-dark-300 dark:text-dark-1000 dark:hover:bg-dark-200"
>
{tier.buttonText}
</a>
) : (
<>
<Link
href={tier.href}
aria-describedby={tier.id}
className={twMerge(
"block w-full rounded-md px-4 py-2 text-center text-sm font-medium transition-colors",
tier.mostPopular
? "bg-dark-50 text-light-50 shadow-sm hover:bg-dark-100 dark:bg-dark-1000 dark:text-dark-50 dark:hover:bg-dark-900"
: "border border-dark-200 bg-transparent text-dark-50 transition-colors hover:bg-dark-50 hover:text-light-50 dark:border-dark-300 dark:text-dark-1000 dark:hover:bg-dark-200",
)}
>
{tier.buttonText}
</Link>
</>
)}
</div>
<p
className={twMerge(
"mt-4 text-sm/6 text-dark-950 dark:text-dark-900",
tier.highlighted ? "text-light-100" : "text-dark-50",
)}
>
{tier.description}
</p>
<p className="mt-6 flex items-baseline gap-x-1">
<span
className={twMerge(
"text-3xl font-semibold tracking-tight text-light-100",
tier.highlighted
? "text-light-50 dark:text-dark-1000"
: "text-gray-900 dark:text-dark-1000",
!tier.showPrice && "opacity-0",
)}
>
{tier.price[frequency?.value ?? "monthly"]}
</span>
{tier.showPriceSuffix && (
<span className="text-sm/6 font-semibold text-light-50 dark:text-dark-900">
{frequency?.priceSuffix}
</span>
)}
</p>
<Link
href={tier.href}
aria-describedby={tier.id}
className={twMerge(
tier.highlighted
? "bg-light-50 text-dark-50 shadow-sm dark:bg-dark-1000 dark:text-dark-50"
: "bg-dark-50 text-light-50 dark:bg-dark-200 dark:text-dark-1000",
"mt-6 block rounded-md px-3 py-2 text-center text-sm/6 font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600",
)}
>
{tier.buttonText}
</Link>
<p
className={twMerge(
"mt-8 text-sm/6 font-bold",
tier.highlighted
? "text-light-100 dark:text-dark-1000"
: "text-dark-50 dark:text-dark-1000",
)}
>
{tier.featureHeader}
</p>
<ul
role="list"
className={twMerge(
"mt-2 space-y-3 text-sm/6 text-light-600",
tier.highlighted
? "text-light-100 dark:text-dark-1000"
: "text-dark-50 dark:text-dark-1000",
)}
>
{tier.features.map((feature) => (
<li key={feature} className="flex items-center gap-x-3">
<HiCheckCircle className="h-5 w-5" />
{feature}
<ul role="list" className="space-y-2.5">
{tier.features.map((feature, index) => (
<li
key={`${tier.id}-feature-${index}`}
className="flex items-start gap-x-2.5"
>
<HiCheckCircle
className="mt-0.5 h-4 w-4 shrink-0 text-light-1000 dark:text-dark-1000"
/>
<span className="text-sm text-dark-600 dark:text-dark-800">
{feature.text}
</span>
</li>
))}
</ul>
</div>
))}
</div>
<div className="mx-auto mt-8 max-w-3xl px-4 text-center">
<div className="inline-flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-xs text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900">
<p>{t`14 day free trial · Switch plans or cancel anytime`}</p>
</div>
</div>
</div>
</>
);

View File

@@ -43,8 +43,8 @@ export default function PricingView() {
<p className="mt-4 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-5xl">
{t`Simple pricing`}
</p>
<p className="text:md lg:text-md mt-6 max-w-[500px] text-center text-light-950 dark:text-dark-900">
{t`Get started for free, with no usage limits. For collaboration, upgrade to a plan that fits the size of your team.`}
<p className="text:md lg:text-md mt-6 max-w-[300px] text-center text-light-950 dark:text-dark-900">
{t`Start free. Upgrade as your team grows. No hidden fees, no contracts.`}
</p>
</div>
<PricingTiers
@@ -59,23 +59,9 @@ export default function PricingView() {
</div>
<div className="pb-22 flex flex-col items-center justify-center px-4">
<div className="flex items-center gap-2 rounded-full border bg-light-50 px-4 py-1 text-center text-xs text-light-1000 dark:border-dark-300 dark:bg-dark-50 dark:text-dark-900 lg:text-sm">
<p>{t`Features`}</p>
</div>
<p className="mt-4 text-center text-3xl font-bold text-light-1000 dark:text-dark-1000 lg:text-4xl">
{t`Feature breakdown`}
</p>
<p className="text-md lg:text-md my-4 max-w-[500px] text-center text-light-950 dark:text-dark-900">
{t`Compare our features to see why Kan is the best choice.`}
</p>
<div className="mt-2">
<Button variant="primary" href="/signup">
{t`Get started`}
</Button>
</div>
<div className="mt-10 w-full max-w-[900px]">
<div className="mt-10 w-full">
<FeatureComparisonTable
frequencyValue={frequency?.value ?? "annually"}
/>

View File

@@ -1,12 +1,13 @@
import { useRouter } from "next/router";
import { t } from "@lingui/core/macro";
import { useEffect, useRef, useState } from "react";
import { HiXMark } from "react-icons/hi2";
import { HiLink, HiXMark } from "react-icons/hi2";
import Badge from "~/components/Badge";
import Editor from "~/components/Editor";
import LabelIcon from "~/components/LabelIcon";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
import ActivityList from "~/views/card/components/ActivityList";
import { AttachmentThumbnails } from "~/views/card/components/AttachmentThumbnails";
@@ -23,16 +24,35 @@ export function CardModal({
}) {
const router = useRouter();
const { closeModal, isOpen } = useModal();
const { showPopup } = usePopup();
const [showFade, setShowFade] = useState(false);
const [showTopFade, setShowTopFade] = useState(false);
const scrollRef = useRef<HTMLDivElement>(null);
const handleCopyCardLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
showPopup({
header: t`Link copied`,
icon: "success",
message: t`Card URL copied to clipboard`,
});
} catch (error) {
console.error(error);
showPopup({
header: t`Unable to copy link`,
icon: "error",
message: t`Please try again.`,
});
}
};
const { data, isLoading } = api.card.byId.useQuery(
{
cardPublicId: cardPublicId ?? "",
},
{
enabled: isOpen && !!cardPublicId,
enabled: isOpen && !!cardPublicId && cardPublicId.length >= 12,
},
);
@@ -65,33 +85,44 @@ export function CardModal({
<div className="h-full p-8">
<div className="mb-6">
<div className="flex w-full items-center justify-between">
<button
className="absolute right-[2rem] top-[2rem] rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
onClick={(e) => {
e.preventDefault();
closeModal();
<div className="absolute right-[2rem] top-[2rem] flex items-center gap-1">
<button
type="button"
onClick={handleCopyCardLink}
className="rounded p-1.5 transition-all hover:bg-light-200 focus:outline-none dark:hover:bg-dark-100"
aria-label="Copy card link"
>
<HiLink className="h-4 w-4 text-light-900 dark:text-dark-900" />
</button>
<button
type="button"
className="rounded p-1.5 transition-all hover:bg-light-200 focus:outline-none dark:hover:bg-dark-100"
onClick={(e) => {
e.preventDefault();
closeModal();
setTimeout(() => {
void router.replace(
{
pathname: router.pathname,
query: {
...router.query,
workspaceSlug,
boardSlug: [boardSlug],
setTimeout(() => {
void router.replace(
{
pathname: router.pathname,
query: {
...router.query,
workspaceSlug: workspaceSlug ?? "",
boardSlug: [boardSlug ?? ""],
},
},
},
undefined,
{ shallow: true },
);
}, 400);
}}
>
<HiXMark
size={18}
className="dark:text-dark-9000 text-light-900"
/>
</button>
undefined,
{ shallow: true },
);
}, 400);
}}
>
<HiXMark
size={18}
className="text-light-900 dark:text-dark-900"
/>
</button>
</div>
{isLoading ? (
<div className="flex space-x-2">
<div className="h-[2.3rem] w-[300px] animate-pulse rounded-[5px] bg-light-300 dark:bg-dark-300" />

View File

@@ -30,7 +30,7 @@ export default function PublicBoardView() {
const { showPopup } = usePopup();
const [isRouteLoaded, setIsRouteLoaded] = useState(false);
const { openModal } = useModal();
const { ref: scrollRef, onMouseDown } = useDragToScroll({
enabled: true,
direction: "horizontal",
@@ -70,30 +70,35 @@ export default function PublicBoardView() {
},
);
const CopyBoardLink = () => {
return (
<button
onClick={async () => {
try {
await navigator.clipboard.writeText(window.location.href);
} catch (error) {
console.error(error);
}
showPopup({
header: t`Link copied`,
icon: "success",
message: t`Board URL copied to clipboard`,
});
}}
className="rounded p-1.5 transition-all hover:bg-light-200 dark:hover:bg-dark-100"
aria-label={`Copy board URL`}
>
<HiLink className={`h-4 w-4 text-light-900 dark:text-dark-900`} />
</button>
);
const handleCopyBoardLink = async () => {
try {
await navigator.clipboard.writeText(window.location.href);
showPopup({
header: t`Link copied`,
icon: "success",
message: t`Board URL copied to clipboard`,
});
} catch (error) {
console.error(error);
showPopup({
header: t`Unable to copy link`,
icon: "error",
message: t`Please try again.`,
});
}
};
const CopyBoardLink = () => (
<button
type="button"
onClick={handleCopyBoardLink}
className="rounded p-1.5 transition-all hover:bg-light-200 focus:outline-none dark:hover:bg-dark-100"
aria-label="Copy board URL"
>
<HiLink className="h-4 w-4 text-light-900 dark:text-dark-900" />
</button>
);
const pathWithoutQuery = router.asPath.split("?")[0];
const splitPath = pathWithoutQuery?.split("/") ?? [];
const cardPublicId = splitPath.length > 3 ? splitPath[3] : null;

View File

@@ -28,9 +28,11 @@ export default function PermissionsSettings() {
});
// Refresh any relevant workspace data
await utils.workspace.byId.invalidate({
workspacePublicId: workspace.publicId,
});
if (workspace.publicId && workspace.publicId.length >= 12) {
await utils.workspace.byId.invalidate({
workspacePublicId: workspace.publicId,
});
}
},
onError: () => {
showPopup({

View File

@@ -31,9 +31,10 @@ export default function WorkspaceSettings() {
const { data } = api.user.getUser.useQuery();
const [hasOpenedUpgradeModal, setHasOpenedUpgradeModal] = useState(false);
const { data: workspaceData } = api.workspace.byId.useQuery({
workspacePublicId: workspace.publicId,
});
const { data: workspaceData } = api.workspace.byId.useQuery(
{ workspacePublicId: workspace.publicId },
{ enabled: !!workspace.publicId && workspace.publicId.length >= 12 },
);
const subscriptions = workspaceData?.subscriptions as
| Subscription[]

View File

@@ -58,28 +58,6 @@ export default function Avatar({
const [crop, setCrop] = useState<PercentCrop>();
const imgRef = useRef<HTMLImageElement | null>(null);
const updateUser = api.user.update.useMutation({
onSuccess: async () => {
showPopup({
header: t`Profile image updated`,
message: t`Your profile image has been updated.`,
icon: "success",
});
try {
await utils.user.getUser.refetch();
} catch (e) {
console.error(e);
throw e;
}
},
onError: () => {
showPopup({
header: t`Error updating profile image`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
});
const avatarUrl = userImage ? getAvatarUrl(userImage) : undefined;
@@ -187,29 +165,32 @@ export default function Avatar({
const originalExt = selectedFile.name.split(".").pop() ?? "jpg";
const fileName = `${userId}/avatar-${generateUID()}.${originalExt}`;
const baseUrl = env("NEXT_PUBLIC_BASE_URL") ?? "";
const response = await fetch(
env("NEXT_PUBLIC_BASE_URL") + "/api/upload/image",
`${baseUrl}/api/upload/avatar`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Type": blob.type,
"x-original-filename": fileName,
},
body: JSON.stringify({ filename: fileName, contentType: blob.type }),
body: blob,
},
);
if (!response.ok) throw new Error("Failed to get pre-signed URL");
if (!response.ok) {
throw new Error("Failed to upload profile image");
}
const { url } = (await response.json()) as { url: string };
const uploadResponse = await fetch(url, {
method: "PUT",
body: blob,
// User image is updated in the backend, refresh user data
await utils.user.getUser.refetch();
showPopup({
header: t`Profile image updated`,
message: t`Your profile image has been updated.`,
icon: "success",
});
if (!uploadResponse.ok) throw new Error("Failed to upload profile image");
updateUser.mutate({ image: fileName });
setCropDialogOpen(false);
resetCropState();
} catch (error) {
@@ -227,7 +208,7 @@ export default function Avatar({
resetCropState,
selectedFile,
showPopup,
updateUser,
utils.user.getUser,
userId,
]);

View File

@@ -22,9 +22,11 @@ export default function UpdateWorkspaceEmailVisibilityForm({
const updateWorkspace = api.workspace.update.useMutation({
onSuccess: () => {
void utils.workspace.byId.invalidate({
workspacePublicId,
});
if (workspacePublicId && workspacePublicId.length >= 12) {
void utils.workspace.byId.invalidate({
workspacePublicId,
});
}
},
});

View File

@@ -27,6 +27,10 @@
"./utils/rateLimit": {
"types": "./dist/utils/rateLimit.d.ts",
"default": "./src/utils/rateLimit.ts"
},
"./utils/permissions": {
"types": "./dist/utils/permissions.d.ts",
"default": "./src/utils/permissions.ts"
}
},
"license": "GPL-3.0",
@@ -39,8 +43,6 @@
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.802.0",
"@aws-sdk/s3-request-presigner": "^3.812.0",
"@kan/auth": "workspace:*",
"@kan/db": "workspace:*",
"@kan/email": "workspace:^",

View File

@@ -9,7 +9,7 @@ import { generateUID } from "@kan/shared/utils";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { assertPermission } from "../utils/permissions";
import { deleteObject, generateUploadUrl } from "../utils/s3";
import { deleteObject, generateUploadUrl } from "@kan/shared/utils";
export const attachmentRouter = createTRPCRouter({
generateUploadUrl: protectedProcedure
@@ -142,9 +142,18 @@ export const attachmentRouter = createTRPCRouter({
createdBy: userId,
});
if (!attachment) {
throw new TRPCError({
message: "Failed to create attachment",
code: "INTERNAL_SERVER_ERROR",
});
}
await cardActivityRepo.create(ctx.db, {
type: "card.updated.attachment.added",
cardId: card.id,
attachmentId: attachment.id,
toTitle: input.originalFilename,
createdBy: userId,
});
@@ -206,6 +215,8 @@ export const attachmentRouter = createTRPCRouter({
await cardActivityRepo.create(ctx.db, {
type: "card.updated.attachment.removed",
cardId: attachment.cardId,
attachmentId: attachment.id,
fromTitle: attachment.originalFilename,
createdBy: userId,
});

View File

@@ -10,6 +10,7 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { colours } from "@kan/shared/constants";
import {
convertDueDateFiltersToRanges,
generateAvatarUrl,
generateSlug,
generateUID,
} from "@kan/shared/utils";
@@ -142,7 +143,63 @@ export const boardRouter = createTRPCRouter({
},
);
return result;
if (!result) {
throw new TRPCError({
message: `Board with public ID ${input.boardPublicId} not found`,
code: "NOT_FOUND",
});
}
// Generate presigned URLs for workspace member avatars
const workspaceWithAvatarUrls = result.workspace
? {
...result.workspace,
members: await Promise.all(
result.workspace.members.map(async (member) => {
if (!member.user?.image) {
return member;
}
const avatarUrl = await generateAvatarUrl(member.user.image);
return {
...member,
user: {
...member.user,
image: avatarUrl,
},
};
}),
),
}
: result.workspace;
// Generate presigned URLs for card member avatars
const listsWithAvatarUrls = await Promise.all(
result.lists.map(async (list) => ({
...list,
cards: await Promise.all(
list.cards.map(async (card) => ({
...card,
members: await Promise.all(
card.members.map(async (member) => {
if (!member.user?.image) return member;
const avatarUrl = await generateAvatarUrl(member.user.image);
return {
...member,
user: { ...member.user, image: avatarUrl },
};
}),
),
})),
),
})),
);
return {
...result,
lists: listsWithAvatarUrls,
workspace: workspaceWithAvatarUrls,
};
}),
bySlug: publicProcedure
.meta({

View File

@@ -10,8 +10,9 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { mergeActivities } from "../utils/activities";
import { sendMentionEmails } from "../utils/notifications";
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
import { generateDownloadUrl } from "../utils/s3";
import { generateAttachmentUrl, generateAvatarUrl } from "@kan/shared/utils";
export const cardRouter = createTRPCRouter({
create: protectedProcedure
@@ -153,6 +154,17 @@ export const cardRouter = createTRPCRouter({
await cardActivityRepo.bulkCreate(ctx.db, cardActivitesInsert);
}
if (input.description) {
sendMentionEmails({
db: ctx.db,
cardPublicId: newCard.publicId,
commentHtml: input.description,
commenterUserId: userId,
}).catch((error) => {
console.error("Failed to send mention emails:", error);
});
}
return newCard;
}),
addComment: protectedProcedure
@@ -215,6 +227,16 @@ export const cardRouter = createTRPCRouter({
createdBy: userId,
});
sendMentionEmails({
db: ctx.db,
cardPublicId: input.cardPublicId,
commentHtml: input.comment,
commenterUserId: userId,
commentId: newComment.id,
}).catch((error) => {
console.error("Failed to send mention emails:", error);
});
return newComment;
}),
updateComment: protectedProcedure
@@ -295,6 +317,16 @@ export const cardRouter = createTRPCRouter({
createdBy: userId,
});
sendMentionEmails({
db: ctx.db,
cardPublicId: input.cardPublicId,
commentHtml: input.comment,
commenterUserId: userId,
commentId: updatedComment.id,
}).catch((error) => {
console.error("Failed to send mention emails:", error);
});
return updatedComment;
}),
deleteComment: protectedProcedure
@@ -631,45 +663,54 @@ export const cardRouter = createTRPCRouter({
});
// Generate URLs for all attachments
const bucket = process.env.NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME;
if (result.attachments && Array.isArray(result.attachments)) {
const attachments = result.attachments as {
publicId: string;
contentType: string;
s3Key: string;
originalFilename: string | null;
size?: number | null;
}[];
const attachmentsWithUrls = await Promise.all(
result.attachments.map(async (attachment) => {
const url = await generateAttachmentUrl(attachment.s3Key);
return {
publicId: attachment.publicId,
contentType: attachment.contentType,
s3Key: attachment.s3Key,
originalFilename: attachment.originalFilename,
size: attachment.size,
url,
};
}),
);
const attachmentsWithUrls = await Promise.all(
attachments.map(async (attachment) => {
const base = {
publicId: attachment.publicId,
contentType: attachment.contentType,
s3Key: attachment.s3Key,
originalFilename: attachment.originalFilename,
size: attachment.size,
};
if (!bucket || !attachment.s3Key) {
return { ...base, url: null };
}
try {
const url = await generateDownloadUrl(
bucket,
attachment.s3Key,
86400, // 24 hours expiration
);
return { ...base, url };
} catch {
// If URL generation fails, return attachment with url: null
return { ...base, url: null };
}
}),
);
return { ...result, attachments: attachmentsWithUrls };
}
// Generate presigned URLs for workspace member avatars
const workspaceWithAvatarUrls = result.list.board.workspace
? {
...result.list.board.workspace,
members: await Promise.all(
result.list.board.workspace.members.map(async (member) => {
if (!member.user?.image) {
return member;
}
return { ...result, attachments: [] };
const avatarUrl = await generateAvatarUrl(member.user.image);
return {
...member,
user: {
...member.user,
image: avatarUrl,
},
};
}),
),
}
: result.list.board.workspace;
return {
...result,
attachments: attachmentsWithUrls,
list: {
...result.list,
board: {
...result.list.board,
workspace: workspaceWithAvatarUrls,
},
},
};
}),
getActivities: publicProcedure
.meta({
@@ -738,7 +779,39 @@ export const cardRouter = createTRPCRouter({
},
);
const mergedActivities = mergeActivities(result.activities);
// Generate presigned URLs for user avatars in activities
const activitiesWithAvatarUrls = await Promise.all(
result.activities.map(async (activity) => {
const updatedActivity = { ...activity };
// Generate presigned URL for activity user avatar
if (activity.user?.image) {
const userAvatarUrl = await generateAvatarUrl(activity.user.image);
updatedActivity.user = {
...activity.user,
image: userAvatarUrl,
};
}
// Generate presigned URL for member user avatar (if exists)
if (activity.member?.user?.image) {
const memberAvatarUrl = await generateAvatarUrl(
activity.member.user.image,
);
updatedActivity.member = {
...activity.member,
user: {
...activity.member.user,
image: memberAvatarUrl,
},
};
}
return updatedActivity;
}),
);
const mergedActivities = mergeActivities(activitiesWithAvatarUrls);
return {
activities: mergedActivities,
@@ -883,6 +956,15 @@ export const cardRouter = createTRPCRouter({
fromDescription: existingCard.description ?? undefined,
toDescription: input.description,
});
sendMentionEmails({
db: ctx.db,
cardPublicId: input.cardPublicId,
commentHtml: input.description,
commenterUserId: userId,
}).catch((error) => {
console.error("Failed to send mention emails:", error);
});
}
if (

View File

@@ -24,7 +24,7 @@ import {
createTRPCRouter,
publicProcedure,
} from "../trpc";
import { createS3Client } from "../utils/s3";
import { createS3Client } from "@kan/shared/utils";
const checkDatabaseConnection = async (db: dbClient) => {
try {

View File

@@ -4,6 +4,7 @@ import { z } from "zod";
import * as userRepo from "@kan/db/repository/user.repo";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import { generateAvatarUrl } from "@kan/shared/utils";
export const userRouter = createTRPCRouter({
getUser: protectedProcedure
@@ -55,8 +56,12 @@ export const userRouter = createTRPCRouter({
const apiKey = result.apiKeys[0];
// Generate presigned URL for avatar
const imageUrl = await generateAvatarUrl(result.image);
return {
...result,
image: imageUrl,
apiKey: apiKey ?? null,
};
}),
@@ -102,6 +107,12 @@ export const userRouter = createTRPCRouter({
});
}
return result;
// Generate presigned URL for avatar
const imageUrl = await generateAvatarUrl(result.image);
return {
...result,
image: imageUrl,
};
}),
});

View File

@@ -8,6 +8,7 @@ import { generateUID } from "@kan/shared/utils";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { assertPermission } from "../utils/permissions";
import { generateAvatarUrl } from "@kan/shared/utils";
export const workspaceRouter = createTRPCRouter({
all: protectedProcedure
@@ -86,9 +87,27 @@ export const workspaceRouter = createTRPCRouter({
const shouldShowEmails =
isAdmin || result.showEmailsToMembers === true;
// Generate presigned URLs for member avatars
const membersWithAvatarUrls = await Promise.all(
result.members.map(async (member) => {
if (!member.user?.image) {
return member;
}
const avatarUrl = await generateAvatarUrl(member.user.image);
return {
...member,
user: {
...member.user,
image: avatarUrl,
},
};
}),
);
// If emails should be hidden, filter them out
if (!shouldShowEmails) {
const sanitizedMembers = result.members.map((member) => {
const sanitizedMembers = membersWithAvatarUrls.map((member) => {
// If user doesn't have a display name, use anonymous identifier
const displayName =
member.user?.name?.trim() ?? `anonymous_${member.publicId}`;
@@ -120,7 +139,10 @@ export const workspaceRouter = createTRPCRouter({
} as Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>;
}
return result;
return {
...result,
members: membersWithAvatarUrls,
};
}),
bySlug: publicProcedure
.meta({

View File

@@ -0,0 +1,133 @@
import { env } from "next-runtime-env";
import type { dbClient } from "@kan/db/client";
import * as cardRepo from "@kan/db/repository/card.repo";
import * as memberRepo from "@kan/db/repository/member.repo";
import * as notificationRepo from "@kan/db/repository/notification.repo";
import * as userRepo from "@kan/db/repository/user.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { sendEmail } from "@kan/email";
import { parseMentionsFromHTML } from "@kan/shared/utils";
/**
* Sends mention notification emails to mentioned members
* Only sends emails for new mentions (checks notification table to avoid duplicates)
*/
export async function sendMentionEmails({
db,
cardPublicId,
commentHtml,
commenterUserId,
commentId,
}: {
db: dbClient;
cardPublicId: string;
commentHtml: string;
commenterUserId: string;
commentId?: number;
}) {
try {
// Parse mentions from HTML
const mentionPublicIds = parseMentionsFromHTML(commentHtml);
if (mentionPublicIds.length === 0) return;
// Get card with board information
const card = await cardRepo.getWithListAndMembersByPublicId(db, cardPublicId);
if (!card?.list.board) return;
const board = card.list.board;
const boardName = board.name;
const cardTitle = card.title;
const cardId = card.id;
// Get workspace ID from workspace publicId
const workspace = await workspaceRepo.getByPublicId(
db,
board.workspace.publicId,
);
if (!workspace?.id) return;
const workspaceId = workspace.id;
// Get commenter information
const commenter = await userRepo.getById(db, commenterUserId);
if (!commenter) return;
const commenterName = commenter.name ?? commenter.email;
// Get mentioned members with full details (filtered by workspace)
const membersWithDetails = await memberRepo.getByPublicIdsWithUsers(
db,
mentionPublicIds,
workspaceId,
);
// Filter out the commenter
const membersToNotify = membersWithDetails.filter(
(member) => member.user?.id !== commenterUserId,
);
if (membersToNotify.length === 0) return;
const baseUrl = env("NEXT_PUBLIC_BASE_URL");
const cardUrl = `${baseUrl}/cards/${cardPublicId}`;
// Send emails to all mentioned members (only if notification doesn't exist)
await Promise.all(
membersToNotify.map(async (member) => {
const userId = member.user?.id;
const email = member.user?.email ?? member.email;
// Skip pending members (no userId) - they can be mentioned but won't receive emails
if (!userId || !email) return;
try {
// Check if notification already exists for this mention
const notificationExists = await notificationRepo.exists(db, {
userId,
cardId,
type: "mention",
});
// If notification already exists, skip sending email
if (notificationExists) {
return;
}
// Create notification record
await notificationRepo.create(db, {
type: "mention",
userId,
cardId,
commentId,
});
// Send email
await sendEmail(
email,
`${commenterName} mentioned you in a comment on ${cardTitle}`,
"MENTION",
{
commenterName,
boardName,
cardTitle,
cardUrl,
},
);
} catch (error) {
console.error("Failed to send mention email:", {
email,
cardPublicId,
error: error instanceof Error ? error.message : String(error),
});
}
}),
);
} catch (error) {
console.error("Error sending mention emails:", {
cardPublicId,
error: error instanceof Error ? error.message : String(error),
});
}
}

View File

@@ -11,15 +11,16 @@ import { createPlugins } from "./plugins";
import { configuredProviders } from "./providers";
export const initAuth = (db: dbClient) => {
const baseURL = env("NEXT_PUBLIC_BASE_URL") || env("BETTER_AUTH_URL");
const trustedOrigins = env("BETTER_AUTH_TRUSTED_ORIGINS")?.split(",") ?? [];
return betterAuth({
secret: env("BETTER_AUTH_SECRET"),
baseURL: env("NEXT_PUBLIC_BASE_URL"),
trustedOrigins: env("BETTER_AUTH_TRUSTED_ORIGINS")
? [
env("NEXT_PUBLIC_BASE_URL") ?? "",
...(env("BETTER_AUTH_TRUSTED_ORIGINS")?.split(",") ?? []),
]
: [env("NEXT_PUBLIC_BASE_URL") ?? ""],
baseURL,
trustedOrigins: [
...(baseURL ? [baseURL] : []),
...trustedOrigins,
],
database: drizzleAdapter(db, {
provider: "pg",
schema: {

View File

@@ -1,4 +1,4 @@
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { ChatOrPushProviderEnum } from "@novu/api/models/components";
import { createAuthMiddleware } from "better-auth/api";
import { env } from "next-runtime-env";
@@ -7,7 +7,7 @@ import type { dbClient } from "@kan/db/client";
import * as memberRepo from "@kan/db/repository/member.repo";
import * as userRepo from "@kan/db/repository/user.repo";
import { notificationClient } from "@kan/email";
import { createEmailUnsubscribeLink } from "@kan/shared";
import { createEmailUnsubscribeLink, createS3Client } from "@kan/shared";
import { downloadImage } from "./utils";
@@ -61,20 +61,7 @@ export function createDatabaseHooks(db: dbClient) {
!user.image.includes(storageDomain)
) {
try {
const credentials =
env("S3_ACCESS_KEY_ID") && env("S3_SECRET_ACCESS_KEY")
? {
accessKeyId: env("S3_ACCESS_KEY_ID")!,
secretAccessKey: env("S3_SECRET_ACCESS_KEY")!,
}
: undefined;
const client = new S3Client({
region: env("S3_REGION") ?? "",
endpoint: env("S3_ENDPOINT") ?? "",
forcePathStyle: env("S3_FORCE_PATH_STYLE") === "true",
credentials,
});
const client = createS3Client();
const allowedFileExtensions = ["jpg", "jpeg", "png", "webp"];

View File

@@ -5,6 +5,7 @@ import { magicLink } from "better-auth/plugins/magic-link";
import type { dbClient } from "@kan/db/client";
import * as memberRepo from "@kan/db/repository/member.repo";
import * as subscriptionRepo from "@kan/db/repository/subscription.repo";
import * as userRepo from "@kan/db/repository/user.repo";
import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { generateUID } from "@kan/shared/utils";
import { sendEmail } from "@kan/email";
@@ -133,10 +134,10 @@ export function createPlugins(db: dbClient) {
if (workspace?.id) {
await memberRepo.pauseAllMembers(db, workspace.id);
// Reset slug to publicId, or generate a UID if publicId is taken
let newSlug = workspace.publicId;
if (workspace.slug !== workspace.publicId) {
const isPublicIdAvailable = await workspaceRepo.isWorkspaceSlugAvailable(
db,
@@ -146,7 +147,7 @@ export function createPlugins(db: dbClient) {
newSlug = generateUID();
}
}
await workspaceRepo.update(db, subscription.referenceId, {
plan: "free",
slug: newSlug,
@@ -171,19 +172,69 @@ export function createPlugins(db: dbClient) {
magicLink({
expiresIn: 60 * 60 * 24 * 7, // 7 days
sendMagicLink: async ({ email, url }) => {
if (url.includes("type=invite")) {
const decodedUrl = decodeURIComponent(url);
console.log("Sending magic link to:", email, "URL:", url);
console.log(
"Magic link contains invite:",
decodedUrl.includes("type=invite"),
);
if (decodedUrl.includes("type=invite")) {
let inviterName = "";
let workspaceName = "";
try {
const urlObj = new URL(url);
const callbackUrl = urlObj.searchParams.get("callbackURL");
if (callbackUrl) {
const callbackParams = new URL(
callbackUrl,
process.env.NEXT_PUBLIC_BASE_URL,
).searchParams;
const memberPublicId = callbackParams.get("memberPublicId");
if (memberPublicId) {
const member = await memberRepo.getByPublicId(
db,
memberPublicId,
);
if (member) {
const [workspace, inviter] = await Promise.all([
workspaceRepo.getById(db, member.workspaceId),
userRepo.getById(db, member.createdBy),
]);
if (workspace) workspaceName = workspace.name;
if (inviter) inviterName = inviter.name ?? "";
}
}
}
} catch (error) {
console.error("Failed to fetch invite details:", error);
}
await sendEmail(
email,
"Invitation to join workspace",
workspaceName
? `Invitation to join the workspace ${workspaceName}`
: "Invitation to join workspace",
"JOIN_WORKSPACE",
{
magicLoginUrl: url,
inviterName,
workspaceName,
},
);
} else {
await sendEmail(
email,
process.env.NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY === "true"
? "Sign in to your account"
: "Sign in to Kan",
"MAGIC_LINK",
{
magicLoginUrl: url,
},
);
} else {
await sendEmail(email, "Sign in to kan.bn", "MAGIC_LINK", {
magicLoginUrl: url,
});
}
},
}),

View File

@@ -0,0 +1,46 @@
CREATE TYPE "public"."notification_type" AS ENUM('mention', 'workspace.member.added', 'workspace.member.removed', 'workspace.role.changed');--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "notification" (
"id" bigserial PRIMARY KEY NOT NULL,
"publicId" varchar(12) NOT NULL,
"type" "notification_type" NOT NULL,
"userId" uuid NOT NULL,
"cardId" bigint,
"commentId" bigint,
"workspaceId" bigint,
"metadata" text,
"readAt" timestamp,
"createdAt" timestamp DEFAULT now() NOT NULL,
"deletedAt" timestamp,
CONSTRAINT "notification_publicId_unique" UNIQUE("publicId")
);
--> statement-breakpoint
ALTER TABLE "notification" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_cardId_card_id_fk" FOREIGN KEY ("cardId") REFERENCES "public"."card"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_commentId_card_comments_id_fk" FOREIGN KEY ("commentId") REFERENCES "public"."card_comments"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "notification" ADD CONSTRAINT "notification_workspaceId_workspace_id_fk" FOREIGN KEY ("workspaceId") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "notification_user_deleted_idx" ON "notification" USING btree ("userId","deletedAt");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "notification_user_read_deleted_idx" ON "notification" USING btree ("userId","readAt","deletedAt");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "notification_user_type_card_idx" ON "notification" USING btree ("userId","type","cardId");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "notification_user_type_workspace_idx" ON "notification" USING btree ("userId","type","workspaceId");--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "notification_user_created_idx" ON "notification" USING btree ("userId","createdAt");

View File

@@ -0,0 +1,6 @@
ALTER TABLE "card_activity" ADD COLUMN "attachmentId" bigint;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "card_activity" ADD CONSTRAINT "card_activity_attachmentId_card_attachment_id_fk" FOREIGN KEY ("attachmentId") REFERENCES "public"."card_attachment"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -176,6 +176,20 @@
"when": 1769983198190,
"tag": "20260201215958_AddUserBoardFavouritesTable",
"breakpoints": true
},
{
"idx": 25,
"version": "7",
"when": 1770500457005,
"tag": "20260207214056_AddNotificationsTable",
"breakpoints": true
},
{
"idx": 26,
"version": "7",
"when": 1770521594167,
"tag": "20260208033314_AddAttachmentToActivity",
"breakpoints": true
}
]
}

View File

@@ -212,6 +212,7 @@ export const getByPublicId = async (
columns: {
publicId: true,
email: true,
status: true,
},
with: {
user: {

View File

@@ -420,6 +420,7 @@ export const getWithListAndMembersByPublicId = async (
) => {
const card = await db.query.cards.findFirst({
columns: {
id: true,
publicId: true,
title: true,
description: true,
@@ -507,6 +508,7 @@ export const getWithListAndMembersByPublicId = async (
columns: {
publicId: true,
email: true,
status: true,
},
with: {
user: {

View File

@@ -33,6 +33,7 @@ export const create = async (
fromDueDate?: Date;
toDueDate?: Date;
sourceBoardId?: number;
attachmentId?: number;
},
) => {
const [result] = await db
@@ -58,6 +59,7 @@ export const create = async (
fromDueDate: activityInput.fromDueDate,
toDueDate: activityInput.toDueDate,
sourceBoardId: activityInput.sourceBoardId,
attachmentId: activityInput.attachmentId,
})
.returning({ id: cardActivities.id });
@@ -83,6 +85,7 @@ export const bulkCreate = async (
fromDueDate?: Date;
toDueDate?: Date;
sourceBoardId?: number;
attachmentId?: number;
}[],
) => {
const activitiesWithPublicIds = activityInputs.map((activity) => ({
@@ -191,6 +194,13 @@ export const getPaginatedActivities = async (
deletedAt: true,
},
},
attachment: {
columns: {
publicId: true,
filename: true,
originalFilename: true,
},
},
},
orderBy: asc(cardActivities.createdAt), // required for merging and pagination
limit: limit + 1, // fetch one extra to check if there are more

View File

@@ -63,6 +63,36 @@ export const getById = async (db: dbClient, memberId: number) => {
});
};
export const getByPublicIdsWithUsers = async (
db: dbClient,
memberPublicIds: string[],
workspaceId?: number,
) => {
return db.query.workspaceMembers.findMany({
where: (members, { inArray: inArrayFn, eq, and, isNull: isNullFn }) => {
const conditions = [inArrayFn(members.publicId, memberPublicIds)];
if (workspaceId) {
conditions.push(eq(members.workspaceId, workspaceId));
}
conditions.push(eq(members.status, "active"));
conditions.push(isNullFn(members.deletedAt));
return and(...conditions);
},
with: {
user: {
columns: {
id: true,
name: true,
email: true,
},
},
},
});
};
export const getByEmailAndStatus = async (
db: dbClient,
email: string,

View File

@@ -0,0 +1,98 @@
import { and, count, eq, isNull } from "drizzle-orm";
import type { dbClient } from "@kan/db/client";
import type { NotificationType } from "@kan/db/schema";
import { notifications } from "@kan/db/schema";
import { generateUID } from "@kan/shared/utils";
export const create = async (
db: dbClient,
notificationInput: {
type: NotificationType;
userId: string;
cardId?: number;
commentId?: number;
workspaceId?: number;
metadata?: string;
},
) => {
const [result] = await db
.insert(notifications)
.values({
publicId: generateUID(),
type: notificationInput.type,
userId: notificationInput.userId,
cardId: notificationInput.cardId,
commentId: notificationInput.commentId,
workspaceId: notificationInput.workspaceId,
metadata: notificationInput.metadata,
})
.returning();
return result;
};
export const exists = async (
db: dbClient,
args: {
userId: string;
type: NotificationType;
cardId?: number;
workspaceId?: number;
commentId?: number;
},
) => {
const result = await db.query.notifications.findFirst({
where: (notifications, { eq, and, isNull: isNullFn }) => {
const conditions = [
eq(notifications.userId, args.userId),
eq(notifications.type, args.type),
isNullFn(notifications.deletedAt),
];
if (args.cardId) {
conditions.push(eq(notifications.cardId, args.cardId));
}
if (args.workspaceId) {
conditions.push(eq(notifications.workspaceId, args.workspaceId));
}
return and(...conditions);
},
});
return !!result;
};
export const markAsRead = async (
db: dbClient,
notificationId: number,
) => {
const [result] = await db
.update(notifications)
.set({ readAt: new Date() })
.where(eq(notifications.id, notificationId))
.returning();
return result;
};
export const getUnreadCount = async (
db: dbClient,
userId: string,
) => {
const result = await db
.select({ count: count() })
.from(notifications)
.where(
and(
eq(notifications.userId, userId),
isNull(notifications.readAt),
isNull(notifications.deletedAt),
),
);
return result[0]?.count ?? 0;
};

View File

@@ -147,6 +147,10 @@ export const cardActivities = pgTable("card_activity", {
() => boards.id,
{ onDelete: "set null" },
),
attachmentId: bigint("attachmentId", { mode: "number" }).references(
() => cardAttachments.id,
{ onDelete: "cascade" },
),
}).enableRLS();
export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({
@@ -190,6 +194,11 @@ export const cardActivitiesRelations = relations(cardActivities, ({ one }) => ({
references: [comments.id],
relationName: "cardActivitiesComment",
}),
attachment: one(cardAttachments, {
fields: [cardActivities.attachmentId],
references: [cardAttachments.id],
relationName: "cardActivitiesAttachment",
}),
}));
export const cardsToLabels = pgTable(

View File

@@ -13,3 +13,4 @@ export * from "./workspaces";
export * from "./subscriptions";
export * from "./workspaceInviteLinks";
export * from "./permissions";
export * from "./notifications";

View File

@@ -0,0 +1,98 @@
import { relations } from "drizzle-orm";
import {
bigint,
bigserial,
index,
pgEnum,
pgTable,
text,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { cards } from "./cards";
import { comments } from "./cards";
import { users } from "./users";
import { workspaces } from "./workspaces";
export const notificationTypes = [
"mention",
"workspace.member.added",
"workspace.member.removed",
"workspace.role.changed",
] as const;
export type NotificationType = (typeof notificationTypes)[number];
export const notificationTypeEnum = pgEnum("notification_type", notificationTypes);
export const notifications = pgTable(
"notification",
{
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
type: notificationTypeEnum("type").notNull(),
userId: uuid("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
cardId: bigint("cardId", { mode: "number" }).references(() => cards.id, {
onDelete: "cascade",
}),
commentId: bigint("commentId", { mode: "number" }).references(
() => comments.id,
{ onDelete: "cascade" },
),
workspaceId: bigint("workspaceId", { mode: "number" }).references(
() => workspaces.id,
{ onDelete: "cascade" },
),
metadata: text("metadata"),
readAt: timestamp("readAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
deletedAt: timestamp("deletedAt"),
},
(table) => [
index("notification_user_deleted_idx").on(table.userId, table.deletedAt),
index("notification_user_read_deleted_idx").on(
table.userId,
table.readAt,
table.deletedAt,
),
index("notification_user_type_card_idx").on(
table.userId,
table.type,
table.cardId,
),
index("notification_user_type_workspace_idx").on(
table.userId,
table.type,
table.workspaceId,
),
index("notification_user_created_idx").on(table.userId, table.createdAt),
],
).enableRLS();
export const notificationsRelations = relations(notifications, ({ one }) => ({
user: one(users, {
fields: [notifications.userId],
references: [users.id],
relationName: "notificationsUser",
}),
card: one(cards, {
fields: [notifications.cardId],
references: [cards.id],
relationName: "notificationsCard",
}),
comment: one(comments, {
fields: [notifications.commentId],
references: [comments.id],
relationName: "notificationsComment",
}),
workspace: one(workspaces, {
fields: [notifications.workspaceId],
references: [workspaces.id],
relationName: "notificationsWorkspace",
}),
}));

View File

@@ -3,14 +3,16 @@ import nodemailer from "nodemailer";
import JoinWorkspaceTemplate from "./templates/join-workspace";
import MagicLinkTemplate from "./templates/magic-link";
import MentionTemplate from "./templates/mention";
import ResetPasswordTemplate from "./templates/reset-password";
type Templates = "MAGIC_LINK" | "JOIN_WORKSPACE" | "RESET_PASSWORD";
type Templates = "MAGIC_LINK" | "JOIN_WORKSPACE" | "RESET_PASSWORD" | "MENTION";
const emailTemplates: Record<Templates, React.FC> = {
const emailTemplates: Record<Templates, React.ComponentType<any>> = {
MAGIC_LINK: MagicLinkTemplate,
JOIN_WORKSPACE: JoinWorkspaceTemplate,
RESET_PASSWORD: ResetPasswordTemplate,
MENTION: MentionTemplate,
};
const transporter = nodemailer.createTransport({

View File

@@ -13,12 +13,16 @@ import * as React from "react";
export const JoinWorkspaceTemplate = ({
magicLoginUrl,
inviterName,
workspaceName,
}: {
magicLoginUrl?: string;
inviterName?: string;
workspaceName?: string;
}) => (
<Html>
<Head />
<Preview>Log in with this magic link</Preview>
<Preview>Join {workspaceName ?? "workspace"} on kan.bn</Preview>
<Body style={{ backgroundColor: "white" }}>
<Container
style={{
@@ -38,12 +42,16 @@ export const JoinWorkspaceTemplate = ({
color: "#232323",
}}
>
kan.bn
{env("NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY") !== "true" && "Kan"}
</Heading>
<Heading
style={{ fontSize: "24px", fontWeight: "bold", color: "#232323" }}
>
Login to your Kan account
{inviterName
? `${inviterName} invited you to join ${
workspaceName ?? "a workspace"
}`
: `You've been invited to join ${workspaceName ?? "a workspace"}`}
</Heading>
<Text
style={{
@@ -52,7 +60,7 @@ export const JoinWorkspaceTemplate = ({
color: "#232323",
}}
>
Click the button below to instantly login to your account.
Click the button below to add this workspace to your account.
</Text>
<Button
target="_blank"
@@ -71,7 +79,7 @@ export const JoinWorkspaceTemplate = ({
color: "white",
}}
>
Login to your account
Join {workspaceName ?? "workspace"}
</Button>
<Text
style={{
@@ -80,25 +88,29 @@ export const JoinWorkspaceTemplate = ({
color: "#7e7e7e",
}}
>
If you didn&apos;t try to login, you can safely ignore this email.
</Text>
<Hr
style={{
marginTop: "2.5rem",
marginBottom: "2rem",
borderWidth: "1px",
}}
/>
<Text style={{ color: "#7e7e7e" }}>
<Link
href={env("NEXT_PUBLIC_BASE_URL")}
target="_blank"
style={{ color: "#7e7e7e", textDecoration: "underline" }}
>
Kan
</Link>
, the open source Trello alternative.
If you don&apos;t want to join this workspace, you can safely ignore this email.
</Text>
{env("NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY") !== "true" && (
<>
<Hr
style={{
marginTop: "2.5rem",
marginBottom: "2rem",
borderWidth: "1px",
}}
/>
<Text style={{ color: "#7e7e7e" }}>
<Link
href={env("NEXT_PUBLIC_BASE_URL")}
target="_blank"
style={{ color: "#7e7e7e", textDecoration: "underline" }}
>
Kan
</Link>
, the open source Trello alternative.
</Text>
</>
)}
</Container>
</Body>
</Html>

View File

@@ -0,0 +1,108 @@
import { Body } from "@react-email/body";
import { Button } from "@react-email/button";
import { Container } from "@react-email/container";
import { Head } from "@react-email/head";
import { Heading } from "@react-email/heading";
import { Hr } from "@react-email/hr";
import { Html } from "@react-email/html";
import { Link } from "@react-email/link";
import { Preview } from "@react-email/preview";
import { Text } from "@react-email/text";
import { env } from "next-runtime-env";
import * as React from "react";
export const MentionTemplate = ({
commenterName,
boardName,
cardTitle,
cardUrl,
}: {
commenterName: string;
boardName: string;
cardTitle: string;
cardUrl: string;
}) => (
<Html>
<Head />
<Preview>
{commenterName} mentioned you in a comment on {cardTitle}
</Preview>
<Body style={{ backgroundColor: "white" }}>
<Container
style={{
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
margin: "auto",
paddingLeft: "0.75rem",
paddingRight: "0.75rem",
}}
>
<Heading
style={{
marginTop: "2.5rem",
marginBottom: "2.5rem",
fontSize: "24px",
fontWeight: "bold",
color: "#232323",
}}
>
kan.bn
</Heading>
<Heading
style={{ fontSize: "24px", fontWeight: "bold", color: "#232323" }}
>
You were mentioned in a comment
</Heading>
<Text
style={{
fontSize: "0.875rem",
marginBottom: "1rem",
color: "#232323",
}}
>
<strong>{commenterName}</strong> mentioned you in a comment on the
card <strong>{cardTitle}</strong> in the board <strong>{boardName}</strong>.
</Text>
<Button
target="_blank"
href={cardUrl}
style={{
marginBottom: "2rem",
borderRadius: "0.375rem",
backgroundColor: "#282828",
paddingLeft: "1.5rem",
paddingRight: "1.5rem",
paddingTop: "1rem",
paddingBottom: "1rem",
fontSize: "0.875rem",
fontWeight: "500",
lineHeight: "1",
color: "white",
}}
>
View Card
</Button>
<Hr
style={{
marginTop: "2.5rem",
marginBottom: "2rem",
borderWidth: "1px",
}}
/>
<Text style={{ color: "#7e7e7e" }}>
<Link
href={env("NEXT_PUBLIC_BASE_URL")}
target="_blank"
style={{ color: "#7e7e7e", textDecoration: "underline" }}
>
Kan
</Link>
, the open source Trello alternative.
</Text>
</Container>
</Body>
</Html>
);
export default MentionTemplate;

View File

@@ -36,6 +36,8 @@
},
"prettier": "@kan/prettier-config",
"dependencies": {
"@aws-sdk/client-s3": "^3.802.0",
"@aws-sdk/s3-request-presigner": "^3.812.0",
"date-fns": "^4.1.0",
"jose": "^6.1.2",
"nanoid": "^5.0.9",

View File

@@ -3,3 +3,5 @@ export * from "./generateSlug";
export * from "./subscriptions";
export * from "./email";
export * from "./dueDateFilters";
export * from "./s3";
export * from "./mentions";

View File

@@ -0,0 +1,22 @@
/**
* Parses mention data-id attributes from HTML content
* Mentions are stored as: <span data-type="mention" data-id="..." data-label="...">@label</span>
* @param htmlContent - The HTML content to parse
* @returns Array of unique mention public IDs
*/
export function parseMentionsFromHTML(htmlContent: string): string[] {
if (!htmlContent) return [];
// Match all mention spans with data-id attributes
const mentionRegex = /<span[^>]*data-type="mention"[^>]*data-id="([^"]+)"[^>]*>/gi;
const matches = Array.from(htmlContent.matchAll(mentionRegex));
// Extract unique mention IDs
const mentionIds = matches
.map((match) => match[1])
.filter((id): id is string => !!id && id.length >= 12);
// Return unique IDs
return Array.from(new Set(mentionIds));
}

View File

@@ -5,6 +5,7 @@ import {
S3Client,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { env } from "next-runtime-env";
export function createS3Client() {
const credentials =
@@ -67,3 +68,60 @@ export async function deleteObject(bucket: string, key: string) {
}),
);
}
/**
* Generate presigned URL for an avatar image
* Returns the URL as-is if it's already a full URL (external provider)
* Returns presigned URL if it's an S3 key
* Returns null if image key is missing, bucket is not configured, or URL generation fails
*/
export async function generateAvatarUrl(
imageKey: string | null | undefined,
expiresIn = 86400, // 24 hours
): Promise<string | null> {
if (!imageKey) {
return null;
}
if (imageKey.startsWith("http://") || imageKey.startsWith("https://")) {
return imageKey;
}
const bucket = env("NEXT_PUBLIC_AVATAR_BUCKET_NAME");
if (!bucket) {
return null;
}
try {
return await generateDownloadUrl(bucket, imageKey, expiresIn);
} catch {
// If URL generation fails, return null
return null;
}
}
/**
* Generate presigned URL for an attachment
* Returns null if attachment key is missing, bucket is not configured, or URL generation fails
*/
export async function generateAttachmentUrl(
attachmentKey: string | null | undefined,
expiresIn = 86400, // 24 hours
): Promise<string | null> {
if (!attachmentKey) {
return null;
}
const bucket = env("NEXT_PUBLIC_ATTACHMENTS_BUCKET_NAME");
if (!bucket) {
return null;
}
try {
return await generateDownloadUrl(bucket, attachmentKey, expiresIn);
} catch {
// If URL generation fails, return null
return null;
}
}

325
pnpm-lock.yaml generated
View File

@@ -82,6 +82,9 @@ importers:
'@aws-sdk/client-s3':
specifier: ^3.802.0
version: 3.879.0
'@aws-sdk/lib-storage':
specifier: ^3.802.0
version: 3.985.0(@aws-sdk/client-s3@3.879.0)
'@aws-sdk/s3-request-presigner':
specifier: ^3.812.0
version: 3.879.0
@@ -136,6 +139,9 @@ importers:
'@tiptap/extension-placeholder':
specifier: ^2.14.0
version: 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1)
'@tiptap/extension-typography':
specifier: ^3.18.0
version: 3.18.0(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))
'@tiptap/pm':
specifier: ^2.14.0
version: 2.26.1
@@ -293,12 +299,6 @@ importers:
packages/api:
dependencies:
'@aws-sdk/client-s3':
specifier: ^3.802.0
version: 3.879.0
'@aws-sdk/s3-request-presigner':
specifier: ^3.812.0
version: 3.879.0
'@kan/auth':
specifier: workspace:*
version: link:../auth
@@ -483,6 +483,12 @@ importers:
packages/shared:
dependencies:
'@aws-sdk/client-s3':
specifier: ^3.802.0
version: 3.879.0
'@aws-sdk/s3-request-presigner':
specifier: ^3.812.0
version: 3.879.0
date-fns:
specifier: ^4.1.0
version: 4.1.0
@@ -734,6 +740,12 @@ packages:
resolution: {integrity: sha512-Jy4uPFfGzHk1Mxy+/Wr43vuw9yXsE2yiF4e4598vc3aJfO0YtA2nSfbKD3PNKRORwXbeKqWPfph9SCKQpWoxEg==}
engines: {node: '>=18.0.0'}
'@aws-sdk/lib-storage@3.985.0':
resolution: {integrity: sha512-EnqXf2E+5+7e5zuz8mPl+3Dk/DI0ObSL8s7Tsf6H8FL/p2BJw2LhgtxPnFZUmpanpoMO0hC9/qjXNd+aGPV0/w==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@aws-sdk/client-s3': ^3.985.0
'@aws-sdk/middleware-bucket-endpoint@3.873.0':
resolution: {integrity: sha512-b4bvr0QdADeTUs+lPc9Z48kXzbKHXQKgTvxx/jXDgSW9tv4KmYPO1gIj6Z9dcrBkRWQuUtSW3Tu2S5n6pe+zeg==}
engines: {node: '>=18.0.0'}
@@ -3300,6 +3312,10 @@ packages:
resolution: {integrity: sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==}
engines: {node: '>=18.0.0'}
'@smithy/abort-controller@4.2.8':
resolution: {integrity: sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==}
engines: {node: '>=18.0.0'}
'@smithy/chunked-blob-reader-native@4.0.0':
resolution: {integrity: sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==}
engines: {node: '>=18.0.0'}
@@ -3312,6 +3328,10 @@ packages:
resolution: {integrity: sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==}
engines: {node: '>=18.0.0'}
'@smithy/core@3.22.1':
resolution: {integrity: sha512-x3ie6Crr58MWrm4viHqqy2Du2rHYZjwu8BekasrQx4ca+Y24dzVAwq3yErdqIbc2G3I0kLQA13PQ+/rde+u65g==}
engines: {node: '>=18.0.0'}
'@smithy/core@3.9.0':
resolution: {integrity: sha512-B/GknvCfS3llXd/b++hcrwIuqnEozQDnRL4sBmOac5/z/dr0/yG1PURNPOyU4Lsiy1IyTj8scPxVqRs5dYWf6A==}
engines: {node: '>=18.0.0'}
@@ -3344,6 +3364,10 @@ packages:
resolution: {integrity: sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==}
engines: {node: '>=18.0.0'}
'@smithy/fetch-http-handler@5.3.9':
resolution: {integrity: sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==}
engines: {node: '>=18.0.0'}
'@smithy/hash-blob-browser@4.0.5':
resolution: {integrity: sha512-F7MmCd3FH/Q2edhcKd+qulWkwfChHbc9nhguBlVjSUE6hVHhec3q6uPQ+0u69S6ppvLtR3eStfCuEKMXBXhvvA==}
engines: {node: '>=18.0.0'}
@@ -3368,6 +3392,10 @@ packages:
resolution: {integrity: sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==}
engines: {node: '>=18.0.0'}
'@smithy/is-array-buffer@4.2.0':
resolution: {integrity: sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==}
engines: {node: '>=18.0.0'}
'@smithy/md5-js@4.0.5':
resolution: {integrity: sha512-8n2XCwdUbGr8W/XhMTaxILkVlw2QebkVTn5tm3HOcbPbOpWg89zr6dPXsH8xbeTsbTXlJvlJNTQsKAIoqQGbdA==}
engines: {node: '>=18.0.0'}
@@ -3380,6 +3408,10 @@ packages:
resolution: {integrity: sha512-EAlEPncqo03siNZJ9Tm6adKCQ+sw5fNU8ncxWwaH0zTCwMPsgmERTi6CEKaermZdgJb+4Yvh0NFm36HeO4PGgQ==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-endpoint@4.4.13':
resolution: {integrity: sha512-x6vn0PjYmGdNuKh/juUJJewZh7MoQ46jYaJ2mvekF4EesMuFfrl4LaW/k97Zjf8PTCPQmPgMvwewg7eNoH9n5w==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-retry@4.1.20':
resolution: {integrity: sha512-T3maNEm3Masae99eFdx1Q7PIqBBEVOvRd5hralqKZNeIivnoGNx5OFtI3DiZ5gCjUkl0mNondlzSXeVxkinh7Q==}
engines: {node: '>=18.0.0'}
@@ -3388,34 +3420,66 @@ packages:
resolution: {integrity: sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-serde@4.2.9':
resolution: {integrity: sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-stack@4.0.5':
resolution: {integrity: sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==}
engines: {node: '>=18.0.0'}
'@smithy/middleware-stack@4.2.8':
resolution: {integrity: sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==}
engines: {node: '>=18.0.0'}
'@smithy/node-config-provider@4.1.4':
resolution: {integrity: sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==}
engines: {node: '>=18.0.0'}
'@smithy/node-config-provider@4.3.8':
resolution: {integrity: sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==}
engines: {node: '>=18.0.0'}
'@smithy/node-http-handler@4.1.1':
resolution: {integrity: sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==}
engines: {node: '>=18.0.0'}
'@smithy/node-http-handler@4.4.9':
resolution: {integrity: sha512-KX5Wml5mF+luxm1szW4QDz32e3NObgJ4Fyw+irhph4I/2geXwUy4jkIMUs5ZPGflRBeR6BUkC2wqIab4Llgm3w==}
engines: {node: '>=18.0.0'}
'@smithy/property-provider@4.0.5':
resolution: {integrity: sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==}
engines: {node: '>=18.0.0'}
'@smithy/property-provider@4.2.8':
resolution: {integrity: sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==}
engines: {node: '>=18.0.0'}
'@smithy/protocol-http@5.1.3':
resolution: {integrity: sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==}
engines: {node: '>=18.0.0'}
'@smithy/protocol-http@5.3.8':
resolution: {integrity: sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==}
engines: {node: '>=18.0.0'}
'@smithy/querystring-builder@4.0.5':
resolution: {integrity: sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==}
engines: {node: '>=18.0.0'}
'@smithy/querystring-builder@4.2.8':
resolution: {integrity: sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==}
engines: {node: '>=18.0.0'}
'@smithy/querystring-parser@4.0.5':
resolution: {integrity: sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==}
engines: {node: '>=18.0.0'}
'@smithy/querystring-parser@4.2.8':
resolution: {integrity: sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==}
engines: {node: '>=18.0.0'}
'@smithy/service-error-classification@4.0.7':
resolution: {integrity: sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==}
engines: {node: '>=18.0.0'}
@@ -3424,14 +3488,26 @@ packages:
resolution: {integrity: sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==}
engines: {node: '>=18.0.0'}
'@smithy/shared-ini-file-loader@4.4.3':
resolution: {integrity: sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==}
engines: {node: '>=18.0.0'}
'@smithy/signature-v4@5.1.3':
resolution: {integrity: sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==}
engines: {node: '>=18.0.0'}
'@smithy/smithy-client@4.11.2':
resolution: {integrity: sha512-SCkGmFak/xC1n7hKRsUr6wOnBTJ3L22Qd4e8H1fQIuKTAjntwgU8lrdMe7uHdiT2mJAOWA/60qaW9tiMu69n1A==}
engines: {node: '>=18.0.0'}
'@smithy/smithy-client@4.5.0':
resolution: {integrity: sha512-ZSdE3vl0MuVbEwJBxSftm0J5nL/gw76xp5WF13zW9cN18MFuFXD5/LV0QD8P+sCU5bSWGyy6CTgUupE1HhOo1A==}
engines: {node: '>=18.0.0'}
'@smithy/types@4.12.0':
resolution: {integrity: sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==}
engines: {node: '>=18.0.0'}
'@smithy/types@4.3.2':
resolution: {integrity: sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==}
engines: {node: '>=18.0.0'}
@@ -3440,14 +3516,26 @@ packages:
resolution: {integrity: sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==}
engines: {node: '>=18.0.0'}
'@smithy/url-parser@4.2.8':
resolution: {integrity: sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==}
engines: {node: '>=18.0.0'}
'@smithy/util-base64@4.0.0':
resolution: {integrity: sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==}
engines: {node: '>=18.0.0'}
'@smithy/util-base64@4.3.0':
resolution: {integrity: sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==}
engines: {node: '>=18.0.0'}
'@smithy/util-body-length-browser@4.0.0':
resolution: {integrity: sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==}
engines: {node: '>=18.0.0'}
'@smithy/util-body-length-browser@4.2.0':
resolution: {integrity: sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==}
engines: {node: '>=18.0.0'}
'@smithy/util-body-length-node@4.0.0':
resolution: {integrity: sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==}
engines: {node: '>=18.0.0'}
@@ -3460,6 +3548,10 @@ packages:
resolution: {integrity: sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==}
engines: {node: '>=18.0.0'}
'@smithy/util-buffer-from@4.2.0':
resolution: {integrity: sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==}
engines: {node: '>=18.0.0'}
'@smithy/util-config-provider@4.0.0':
resolution: {integrity: sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==}
engines: {node: '>=18.0.0'}
@@ -3480,10 +3572,18 @@ packages:
resolution: {integrity: sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==}
engines: {node: '>=18.0.0'}
'@smithy/util-hex-encoding@4.2.0':
resolution: {integrity: sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==}
engines: {node: '>=18.0.0'}
'@smithy/util-middleware@4.0.5':
resolution: {integrity: sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==}
engines: {node: '>=18.0.0'}
'@smithy/util-middleware@4.2.8':
resolution: {integrity: sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==}
engines: {node: '>=18.0.0'}
'@smithy/util-retry@4.0.7':
resolution: {integrity: sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==}
engines: {node: '>=18.0.0'}
@@ -3492,10 +3592,18 @@ packages:
resolution: {integrity: sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==}
engines: {node: '>=18.0.0'}
'@smithy/util-stream@4.5.11':
resolution: {integrity: sha512-lKmZ0S/3Qj2OF5H1+VzvDLb6kRxGzZHq6f3rAsoSu5cTLGsn3v3VQBA8czkNNXlLjoFEtVu3OQT2jEeOtOE2CA==}
engines: {node: '>=18.0.0'}
'@smithy/util-uri-escape@4.0.0':
resolution: {integrity: sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==}
engines: {node: '>=18.0.0'}
'@smithy/util-uri-escape@4.2.0':
resolution: {integrity: sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==}
engines: {node: '>=18.0.0'}
'@smithy/util-utf8@2.3.0':
resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==}
engines: {node: '>=14.0.0'}
@@ -3504,10 +3612,18 @@ packages:
resolution: {integrity: sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==}
engines: {node: '>=18.0.0'}
'@smithy/util-utf8@4.2.0':
resolution: {integrity: sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==}
engines: {node: '>=18.0.0'}
'@smithy/util-waiter@4.0.7':
resolution: {integrity: sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==}
engines: {node: '>=18.0.0'}
'@smithy/uuid@1.1.0':
resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
engines: {node: '>=18.0.0'}
'@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
@@ -3779,6 +3895,11 @@ packages:
peerDependencies:
'@tiptap/core': ^2.7.0
'@tiptap/extension-typography@3.18.0':
resolution: {integrity: sha512-zTNGJjhJG3lObUhhTbDC1IOyi1DCiCd6i11xsnJDPy5BODYc7t7ZP6VMOWU0LIuMsK9kX02dXoNU7OarJgLpCg==}
peerDependencies:
'@tiptap/core': ^3.18.0
'@tiptap/pm@2.26.1':
resolution: {integrity: sha512-8aF+mY/vSHbGFqyG663ds84b+vca5Lge3tHdTMTKazxCnhXR9dn2oQJMnZ78YZvdRbkPkMJJHti9h3K7u2UQvw==}
@@ -4478,6 +4599,9 @@ packages:
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
buffer@5.6.0:
resolution: {integrity: sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==}
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
@@ -7752,6 +7876,9 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
stream-browserify@3.0.0:
resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@@ -8894,6 +9021,17 @@ snapshots:
transitivePeerDependencies:
- aws-crt
'@aws-sdk/lib-storage@3.985.0(@aws-sdk/client-s3@3.879.0)':
dependencies:
'@aws-sdk/client-s3': 3.879.0
'@smithy/abort-controller': 4.2.8
'@smithy/middleware-endpoint': 4.4.13
'@smithy/smithy-client': 4.11.2
buffer: 5.6.0
events: 3.3.0
stream-browserify: 3.0.0
tslib: 2.8.1
'@aws-sdk/middleware-bucket-endpoint@3.873.0':
dependencies:
'@aws-sdk/types': 3.862.0
@@ -11328,6 +11466,11 @@ snapshots:
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/abort-controller@4.2.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/chunked-blob-reader-native@4.0.0':
dependencies:
'@smithy/util-base64': 4.0.0
@@ -11345,6 +11488,19 @@ snapshots:
'@smithy/util-middleware': 4.0.5
tslib: 2.8.1
'@smithy/core@3.22.1':
dependencies:
'@smithy/middleware-serde': 4.2.9
'@smithy/protocol-http': 5.3.8
'@smithy/types': 4.12.0
'@smithy/util-base64': 4.3.0
'@smithy/util-body-length-browser': 4.2.0
'@smithy/util-middleware': 4.2.8
'@smithy/util-stream': 4.5.11
'@smithy/util-utf8': 4.2.0
'@smithy/uuid': 1.1.0
tslib: 2.8.1
'@smithy/core@3.9.0':
dependencies:
'@smithy/middleware-serde': 4.0.9
@@ -11405,6 +11561,14 @@ snapshots:
'@smithy/util-base64': 4.0.0
tslib: 2.8.1
'@smithy/fetch-http-handler@5.3.9':
dependencies:
'@smithy/protocol-http': 5.3.8
'@smithy/querystring-builder': 4.2.8
'@smithy/types': 4.12.0
'@smithy/util-base64': 4.3.0
tslib: 2.8.1
'@smithy/hash-blob-browser@4.0.5':
dependencies:
'@smithy/chunked-blob-reader': 5.0.0
@@ -11438,6 +11602,10 @@ snapshots:
dependencies:
tslib: 2.8.1
'@smithy/is-array-buffer@4.2.0':
dependencies:
tslib: 2.8.1
'@smithy/md5-js@4.0.5':
dependencies:
'@smithy/types': 4.3.2
@@ -11461,6 +11629,17 @@ snapshots:
'@smithy/util-middleware': 4.0.5
tslib: 2.8.1
'@smithy/middleware-endpoint@4.4.13':
dependencies:
'@smithy/core': 3.22.1
'@smithy/middleware-serde': 4.2.9
'@smithy/node-config-provider': 4.3.8
'@smithy/shared-ini-file-loader': 4.4.3
'@smithy/types': 4.12.0
'@smithy/url-parser': 4.2.8
'@smithy/util-middleware': 4.2.8
tslib: 2.8.1
'@smithy/middleware-retry@4.1.20':
dependencies:
'@smithy/node-config-provider': 4.1.4
@@ -11480,11 +11659,22 @@ snapshots:
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/middleware-serde@4.2.9':
dependencies:
'@smithy/protocol-http': 5.3.8
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/middleware-stack@4.0.5':
dependencies:
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/middleware-stack@4.2.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/node-config-provider@4.1.4':
dependencies:
'@smithy/property-provider': 4.0.5
@@ -11492,6 +11682,13 @@ snapshots:
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/node-config-provider@4.3.8':
dependencies:
'@smithy/property-provider': 4.2.8
'@smithy/shared-ini-file-loader': 4.4.3
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/node-http-handler@4.1.1':
dependencies:
'@smithy/abort-controller': 4.0.5
@@ -11500,27 +11697,56 @@ snapshots:
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/node-http-handler@4.4.9':
dependencies:
'@smithy/abort-controller': 4.2.8
'@smithy/protocol-http': 5.3.8
'@smithy/querystring-builder': 4.2.8
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/property-provider@4.0.5':
dependencies:
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/property-provider@4.2.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/protocol-http@5.1.3':
dependencies:
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/protocol-http@5.3.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/querystring-builder@4.0.5':
dependencies:
'@smithy/types': 4.3.2
'@smithy/util-uri-escape': 4.0.0
tslib: 2.8.1
'@smithy/querystring-builder@4.2.8':
dependencies:
'@smithy/types': 4.12.0
'@smithy/util-uri-escape': 4.2.0
tslib: 2.8.1
'@smithy/querystring-parser@4.0.5':
dependencies:
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/querystring-parser@4.2.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/service-error-classification@4.0.7':
dependencies:
'@smithy/types': 4.3.2
@@ -11530,6 +11756,11 @@ snapshots:
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/shared-ini-file-loader@4.4.3':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/signature-v4@5.1.3':
dependencies:
'@smithy/is-array-buffer': 4.0.0
@@ -11541,6 +11772,16 @@ snapshots:
'@smithy/util-utf8': 4.0.0
tslib: 2.8.1
'@smithy/smithy-client@4.11.2':
dependencies:
'@smithy/core': 3.22.1
'@smithy/middleware-endpoint': 4.4.13
'@smithy/middleware-stack': 4.2.8
'@smithy/protocol-http': 5.3.8
'@smithy/types': 4.12.0
'@smithy/util-stream': 4.5.11
tslib: 2.8.1
'@smithy/smithy-client@4.5.0':
dependencies:
'@smithy/core': 3.9.0
@@ -11551,6 +11792,10 @@ snapshots:
'@smithy/util-stream': 4.2.4
tslib: 2.8.1
'@smithy/types@4.12.0':
dependencies:
tslib: 2.8.1
'@smithy/types@4.3.2':
dependencies:
tslib: 2.8.1
@@ -11561,16 +11806,32 @@ snapshots:
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/url-parser@4.2.8':
dependencies:
'@smithy/querystring-parser': 4.2.8
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/util-base64@4.0.0':
dependencies:
'@smithy/util-buffer-from': 4.0.0
'@smithy/util-utf8': 4.0.0
tslib: 2.8.1
'@smithy/util-base64@4.3.0':
dependencies:
'@smithy/util-buffer-from': 4.2.0
'@smithy/util-utf8': 4.2.0
tslib: 2.8.1
'@smithy/util-body-length-browser@4.0.0':
dependencies:
tslib: 2.8.1
'@smithy/util-body-length-browser@4.2.0':
dependencies:
tslib: 2.8.1
'@smithy/util-body-length-node@4.0.0':
dependencies:
tslib: 2.8.1
@@ -11585,6 +11846,11 @@ snapshots:
'@smithy/is-array-buffer': 4.0.0
tslib: 2.8.1
'@smithy/util-buffer-from@4.2.0':
dependencies:
'@smithy/is-array-buffer': 4.2.0
tslib: 2.8.1
'@smithy/util-config-provider@4.0.0':
dependencies:
tslib: 2.8.1
@@ -11617,11 +11883,20 @@ snapshots:
dependencies:
tslib: 2.8.1
'@smithy/util-hex-encoding@4.2.0':
dependencies:
tslib: 2.8.1
'@smithy/util-middleware@4.0.5':
dependencies:
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/util-middleware@4.2.8':
dependencies:
'@smithy/types': 4.12.0
tslib: 2.8.1
'@smithy/util-retry@4.0.7':
dependencies:
'@smithy/service-error-classification': 4.0.7
@@ -11639,10 +11914,25 @@ snapshots:
'@smithy/util-utf8': 4.0.0
tslib: 2.8.1
'@smithy/util-stream@4.5.11':
dependencies:
'@smithy/fetch-http-handler': 5.3.9
'@smithy/node-http-handler': 4.4.9
'@smithy/types': 4.12.0
'@smithy/util-base64': 4.3.0
'@smithy/util-buffer-from': 4.2.0
'@smithy/util-hex-encoding': 4.2.0
'@smithy/util-utf8': 4.2.0
tslib: 2.8.1
'@smithy/util-uri-escape@4.0.0':
dependencies:
tslib: 2.8.1
'@smithy/util-uri-escape@4.2.0':
dependencies:
tslib: 2.8.1
'@smithy/util-utf8@2.3.0':
dependencies:
'@smithy/util-buffer-from': 2.2.0
@@ -11653,12 +11943,21 @@ snapshots:
'@smithy/util-buffer-from': 4.0.0
tslib: 2.8.1
'@smithy/util-utf8@4.2.0':
dependencies:
'@smithy/util-buffer-from': 4.2.0
tslib: 2.8.1
'@smithy/util-waiter@4.0.7':
dependencies:
'@smithy/abort-controller': 4.0.5
'@smithy/types': 4.3.2
tslib: 2.8.1
'@smithy/uuid@1.1.0':
dependencies:
tslib: 2.8.1
'@socket.io/component-emitter@3.1.2': {}
'@standard-schema/spec@1.0.0': {}
@@ -11919,6 +12218,10 @@ snapshots:
dependencies:
'@tiptap/core': 2.26.1(@tiptap/pm@2.26.1)
'@tiptap/extension-typography@3.18.0(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))':
dependencies:
'@tiptap/core': 2.26.1(@tiptap/pm@2.26.1)
'@tiptap/pm@2.26.1':
dependencies:
prosemirror-changeset: 2.3.1
@@ -12778,6 +13081,11 @@ snapshots:
buffer-from@1.1.2: {}
buffer@5.6.0:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
buffer@5.7.1:
dependencies:
base64-js: 1.5.1
@@ -16898,6 +17206,11 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
stream-browserify@3.0.0:
dependencies:
inherits: 2.0.4
readable-stream: 3.6.2
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0