Compare commits

..

1 Commits

Author SHA1 Message Date
Henry
9d915319fc fix: ensure publicId is returned from create card 2026-02-01 22:39:48 +00:00
105 changed files with 1624 additions and 11519 deletions

View File

@@ -1,62 +1,18 @@
# Environment
.env
.env.*
!.env.example
# Docker
docker-compose*.yml
docker-compose.override.yml
Dockerfile
./**/*/Dockerfile
.dockerignore
# Dependencies (rebuilt in Docker)
node_modules
**/node_modules
./**/*/node_modules
# Build outputs (rebuilt in Docker)
**/.next
**/dist
**/out
**/.turbo
**/.cache
# Git
.git
.gitignore
.gitattributes
# IDE
.vscode
.idea
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# CI/CD
.github
.husky
.changeset
# Documentation (not needed in build)
*.md
!packages/db/migrations/**
LICENSE
CONTRIBUTING.md
AGENTS.md
CHANGELOG.md
# Test files
**/*.test.ts
**/*.test.tsx
**/*.spec.ts
**/*.spec.tsx
**/__tests__
**/coverage
# Logs
pnpm-debug.log
**/pnpm-debug.log
./**/*/pnpm-debug.log
# Cloud compose (separate deployment)
cloud/
README.md
.next
# .git

View File

@@ -61,6 +61,21 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
# Extract version from git tag or ref
# Uses git describe to get latest tag + commit hash in SemVer format: 1.2.3+abc1234
- name: Extract version
@@ -97,39 +112,6 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
labels: |
org.opencontainers.image.version=${{ steps.version.outputs.version }}
# Extract metadata for migrate image
- name: Extract Docker metadata for migrate
id: meta-migrate
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-migrate
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
labels: |
org.opencontainers.image.version=${{ steps.version.outputs.version }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
@@ -147,24 +129,6 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
# Build and push migrate Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push migrate Docker image
id: build-and-push-migrate
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
file: apps/web/Dockerfile
target: migrate
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta-migrate.outputs.tags }}
labels: ${{ steps.meta-migrate.outputs.labels }}
build-args: |
APP_VERSION=${{ steps.version.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
@@ -179,11 +143,3 @@ jobs:
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
# Sign the migrate Docker image digest except on PRs.
- name: Sign the published migrate Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
TAGS: ${{ steps.meta-migrate.outputs.tags }}
DIGEST: ${{ steps.build-and-push-migrate.outputs.digest }}
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}

View File

@@ -47,17 +47,7 @@ See our [roadmap](https://kan.bn/kan/roadmap) for upcoming features.
## Self Hosting 🐳
### 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.
The easiest way to self-host Kan is 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

@@ -1,23 +1,25 @@
# syntax=docker/dockerfile:1.7
ARG NODE_VERSION=20
ARG DISTROLESS_NODE_IMAGE=gcr.io/distroless/nodejs${NODE_VERSION}-debian12
ARG APP_DIRNAME=web
ARG PROJECT=@kan/web
# ============================================
# Stage 1: Alpine base with pnpm and turbo
# ============================================
# 1. Alpine image
FROM node:${NODE_VERSION}-alpine AS alpine
RUN apk update && \
apk add --no-cache libc6-compat && \
rm -rf /var/cache/apk/* /tmp/* || true && \
corepack enable && \
npm install turbo@2.3.1 --global && \
pnpm config set store-dir ~/.pnpm-store
apk add --no-cache --virtual .build-deps libc6-compat python3 make g++ && \
rm -rf /var/cache/apk/* /tmp/* || true
# ============================================
# Stage 2: Prune the monorepo
# ============================================
FROM alpine AS pruner
# Setup pnpm and turbo on the alpine base
FROM alpine AS base
RUN corepack enable
# Replace <your-major-version> with the major version installed in your repository. For example:
# RUN npm install turbo@2.1.3 --global
RUN npm install turbo@2.3.1 --global
RUN pnpm config set store-dir ~/.pnpm-store
# 2. Prune projects
FROM base AS pruner
ARG PROJECT
RUN apk add --no-cache git
@@ -33,92 +35,53 @@ RUN git fetch --tags --unshallow 2>/dev/null || git fetch --tags 2>/dev/null ||
echo "unknown") && \
echo "$AUTO_VERSION" > /app/AUTO_VERSION
RUN turbo prune --scope=@kan/web --scope=@kan/db --docker
RUN turbo prune --scope=${PROJECT} --scope=@kan/db --docker
# 3. Build the project
FROM base AS builder
ARG PROJECT
ARG APP_VERSION
# ============================================
# Stage 3: Install dependencies
# ============================================
FROM alpine AS deps
WORKDIR /app
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=pruner /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
COPY --from=pruner /app/out/json/ .
ENV CI=true
RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install --frozen-lockfile
# ============================================
# Stage 4: Build the web application
# ============================================
FROM alpine AS builder
ARG APP_VERSION
WORKDIR /app
COPY --from=deps /app/ ./
COPY --from=pruner /app/out/full/ .
COPY --from=pruner /app/AUTO_VERSION /tmp/AUTO_VERSION
# Force standalone output for production Docker image
ENV NEXT_PUBLIC_USE_STANDALONE_OUTPUT=true
ENV CI=true
RUN --mount=type=cache,id=pnpm,target=~/.pnpm-store pnpm install --frozen-lockfile
COPY --from=pruner /app/out/full/ .
# Use provided APP_VERSION or auto-generated from pruner stage
RUN VERSION="${APP_VERSION:-$(cat /tmp/AUTO_VERSION 2>/dev/null | tr -d '\n\r' || echo 'unknown')}" && \
NEXT_PUBLIC_APP_VERSION="$VERSION" pnpm build --filter=@kan/web
NEXT_PUBLIC_APP_VERSION="$VERSION" pnpm build --filter=${PROJECT}
# ============================================
# Stage 5: Migration image (run-once container)
# Note: Use a Debian-based Node image instead of Alpine here because
# Alpine under QEMU has caused Illegal instruction (signal 4) crashes
# during npm installs on arm64 multi-arch builds.
# ============================================
FROM node:${NODE_VERSION}-bullseye-slim AS migrate
WORKDIR /db
# # Copy static files to standalone directory
# RUN mkdir -p apps/web/.next/standalone/.next && \
# mv apps/web/public apps/web/.next/standalone/ && \
# mv apps/web/.next/static apps/web/.next/standalone/.next/
COPY packages/db/drizzle.config.ts ./drizzle.config.ts
COPY packages/db/migrations/ ./migrations/
# 4. Final image - runner stage to run the application
FROM base AS runner
ARG APP_DIRNAME
# Don't run production as root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs
RUN npm init -y && \
npm install drizzle-kit drizzle-orm pg --save-exact && \
# Strip unnecessary files from node_modules
find node_modules -type f \( \
-name '*.d.ts' -o \
-name '*.d.mts' -o \
-name '*.d.cts' -o \
-name '*.map' -o \
-name '*.md' -o \
-name '*.txt' -o \
-name 'LICENSE*' -o \
-name 'CHANGELOG*' -o \
-name 'README*' -o \
-name '.eslint*' -o \
-name '.prettier*' -o \
-name 'tsconfig*.json' \
\) -delete && \
find node_modules -type d -empty -delete && \
rm -rf /root/.npm /tmp/*
CMD ["npx", "drizzle-kit", "migrate"]
# ============================================
# Stage 6: Production web image (distroless)
# ============================================
FROM ${DISTROLESS_NODE_IMAGE} AS web
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
# Copy the standalone Next.js server
COPY --from=builder /app/apps/web/.next/standalone/ ./
# Copy static assets and public files
COPY --from=builder /app/apps/web/.next/static/ ./apps/web/.next/static/
COPY --from=builder /app/apps/web/public/ ./apps/web/public/
COPY --chown=nextjs:nodejs --from=builder /app/ ./
WORKDIR /app/apps/${APP_DIRNAME}
# Copy bootstrap script for runtime env var injection
COPY apps/web/bootstrap.cjs ./bootstrap.cjs
ARG PORT=3000
ENV PORT=${PORT}
EXPOSE ${PORT}
EXPOSE 3000
CMD ["bootstrap.cjs"]
CMD ["sh", "-c", "if [ -n \"$POSTGRES_URL\" ]; then cd /app && pnpm db:migrate && cd /app/apps/web; fi && pnpm start"]

View File

@@ -1,44 +0,0 @@
/**
* Bootstrap script for the distroless production image.
*
* Distroless images have no shell, so this Node.js script handles two tasks
* that would normally be done in an entrypoint.sh:
*
* 1. Regenerate `public/__ENV.js` with the current runtime NEXT_PUBLIC_*
* environment variables. The file was originally created at build time by
* next-runtime-env's `configureRuntimeEnv()`, but in a Docker deployment the
* env vars are provided at *run* time via docker-compose / docker run.
*
* 2. Start the Next.js standalone server.
*/
const { writeFileSync, existsSync, mkdirSync } = require("fs");
const path = require("path");
// ---------------------------------------------------------------------------
// 1. Inject runtime NEXT_PUBLIC_* env vars into __ENV.js
// ---------------------------------------------------------------------------
const publicDir = path.join(__dirname, "apps", "web", "public");
if (!existsSync(publicDir)) {
mkdirSync(publicDir, { recursive: true });
}
const envVars = {};
for (const [key, value] of Object.entries(process.env)) {
if (key.startsWith("NEXT_PUBLIC_")) {
envVars[key] = value;
}
}
writeFileSync(
path.join(publicDir, "__ENV.js"),
`self.__ENV = ${JSON.stringify(envVars)};`,
);
// ---------------------------------------------------------------------------
// 2. Start the Next.js standalone server
// ---------------------------------------------------------------------------
require("./apps/web/server.js");

View File

@@ -5,8 +5,6 @@ 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
@@ -29,7 +27,6 @@ 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
@@ -38,7 +35,6 @@ 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
@@ -110,6 +106,7 @@ 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
@@ -465,12 +462,10 @@ 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
@@ -586,7 +581,6 @@ 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

@@ -16,18 +16,6 @@ const config = {
: undefined,
reactStrictMode: true,
/** Exclude build tools and dev-only packages from the standalone output */
outputFileTracingExcludes: {
"**/*": [
"@esbuild/**",
"esbuild/**",
"typescript/**",
"webpack/**",
"uglify-js/**",
"terser/**",
],
},
/** Enables hot reloading for local packages without a build step */
transpilePackages: [
"@kan/api",
@@ -63,8 +51,9 @@ const config = {
hostname: "*.googleusercontent.com",
},
{
protocol: "https",
hostname: "cdn.discordapp.com",
protocol: 'https',
hostname: 'cdn.discordapp.com',
pathname: '/avatars/**',
},
];
@@ -106,6 +95,12 @@ const config = {
swcPlugins: [["@lingui/swc-plugin", {}]],
},
api: {
bodyParser: {
sizeLimit: env("NEXT_API_BODY_SIZE_LIMIT") || '1mb',
},
},
async rewrites() {
return [
{

View File

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

View File

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

View File

@@ -12,7 +12,6 @@ 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 {
@@ -45,12 +44,6 @@ 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);
@@ -162,12 +155,8 @@ 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: user?.name ?? session?.user.name,
email: user?.email ?? session?.user.email ?? "",
image: user?.image ?? undefined,
}}
isLoading={sessionLoading || userLoading}
user={{ displayName: session?.user.name, email: session?.user.email, image: session?.user.image }}
isLoading={sessionLoading}
onCloseSideNav={closeSideNav}
/>
</div>

View File

@@ -30,7 +30,7 @@ export default function Dropdown({
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-[100] isolate mt-2 w-56 origin-top-right rounded-md border border-light-200 bg-white p-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
<Menu.Items className="absolute right-0 z-50 mt-2 w-56 origin-top-right rounded-md border border-light-200 bg-light-50 p-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
<div className="flex flex-col">
{items.map((item) => (
<Menu.Item key={item.label} disabled={item.disabled}>

View File

@@ -17,7 +17,6 @@ import {
ReactRenderer,
useEditor,
} from "@tiptap/react";
import Typography from "@tiptap/extension-typography";
import StarterKit from "@tiptap/starter-kit";
import Suggestion from "@tiptap/suggestion";
import {
@@ -387,54 +386,46 @@ export interface SlashNodeAttrs {
label?: string | null;
}
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(),
},
];
};
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(),
},
];
export default function Editor({
content,
@@ -443,8 +434,6 @@ export default function Editor({
readOnly = false,
workspaceMembers,
enableYouTubeEmbed = true,
placeholder,
disableHeadings = false,
}: {
content: string | null;
onChange?: (value: string) => void;
@@ -452,23 +441,18 @@ export default function Editor({
readOnly?: boolean;
workspaceMembers: WorkspaceMember[];
enableYouTubeEmbed?: boolean;
placeholder?: string;
disableHeadings?: boolean;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const editor = useEditor(
{
extensions: [
StarterKit.configure({
heading: disableHeadings ? false : undefined,
}),
StarterKit,
Markdown,
Placeholder.configure({
placeholder: readOnly
? ""
: placeholder ??
t`Add description... (type '/' to open commands or '@' to mention)`,
: t`Add description... (type '/' to open commands or '@' to mention)`,
}),
Link.configure({
openOnClick: true,
@@ -481,10 +465,10 @@ export default function Editor({
autolink: true,
}),
SlashCommands.configure({
commandItems: getCommandItems(disableHeadings),
commandItems: CommandItems,
suggestion: {
items: ({ query }: { query: string }) =>
filterSlashCommandItems(getCommandItems(disableHeadings), query),
filterSlashCommandItems(CommandItems, query),
startOfLine: true,
char: "/",
},
@@ -496,28 +480,20 @@ export default function Editor({
suggestion: {
char: "@",
items: ({ query }: { query: string }) => {
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 all: MentionItem[] = workspaceMembers.map(
(member: WorkspaceMember) => ({
id: member.publicId,
label: member?.user?.name ?? member.email,
image: member?.user?.image ?? null,
}),
);
const q = query.toLowerCase().trim();
if (q === "") {
return all;
}
const filtered = all.filter((u) =>
u.label.toLowerCase().includes(q),
const q = query.toLowerCase();
return all.filter(
(u) =>
u.label &&
typeof u.label === "string" &&
u.label.toLowerCase().includes(q),
);
return filtered;
},
command: ({ editor, range, props }) => {
const id = props.id ?? "";
@@ -538,17 +514,6 @@ 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

@@ -33,7 +33,7 @@ const schema = z.object({
.min(3, {
message: t`URL must be at least 3 characters long`,
})
.max(64, { message: t`URL cannot exceed 64 characters` })
.max(24, { message: t`URL cannot exceed 24 characters` })
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
message: t`URL can only contain letters, numbers, and hyphens`,
})

View File

@@ -55,10 +55,9 @@ export default function SideNavigation({
const [isInitialised, setIsInitialised] = useState(false);
const { openModal } = useModal();
const { data: workspaceData } = api.workspace.byId.useQuery(
{ workspacePublicId: workspace.publicId },
{ enabled: !!workspace.publicId && workspace.publicId.length >= 12 },
);
const { data: workspaceData } = api.workspace.byId.useQuery({
workspacePublicId: workspace.publicId,
});
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,8 +24,6 @@ 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

@@ -1,7 +1,6 @@
import type { Permission } from "@kan/shared";
import { useContext } from "react";
import { WorkspaceContext } from "~/providers/workspace";
import { useWorkspace } from "~/providers/workspace";
import { api } from "~/utils/api";
interface UsePermissionsResult {
@@ -31,40 +30,7 @@ interface UsePermissionsResult {
}
export function usePermissions(): UsePermissionsResult {
// Check if WorkspaceProvider is available (for public board views, it may not be)
const workspaceContext = useContext(WorkspaceContext);
// If WorkspaceProvider is not available, return safe defaults
if (!workspaceContext) {
const emptyPermissions: UsePermissionsResult = {
permissions: [],
role: null,
isLoading: false,
hasPermission: () => false,
canViewCard: false,
canCreateCard: false,
canEditCard: false,
canDeleteCard: false,
canCreateList: false,
canEditList: false,
canDeleteList: false,
canCreateBoard: false,
canEditBoard: false,
canDeleteBoard: false,
canViewComment: false,
canCreateComment: false,
canEditComment: false,
canDeleteComment: false,
canInviteMember: false,
canEditMember: false,
canRemoveMember: false,
canViewWorkspace: false,
canEditWorkspace: false,
};
return emptyPermissions;
}
const { workspace } = workspaceContext;
const { workspace } = useWorkspace();
const { data, isLoading } = api.permission.getMyPermissions.useQuery(
{ workspacePublicId: workspace.publicId },

View File

@@ -35,16 +35,6 @@ 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"
@@ -141,50 +131,42 @@ 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:89
#: src/views/card/components/ActivityList.tsx:78
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "{0} Labels hinzugefügt: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:119
#: src/views/card/components/ActivityList.tsx:108
msgid "added a checklist"
msgstr "hat eine Checkliste hinzugefügt"
#: src/views/card/components/ActivityList.tsx:122
#: src/views/card/components/ActivityList.tsx:111
msgid "added a checklist item"
msgstr "hat ein Checklistenelement hinzugefügt"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:104
msgid "added a label to the card"
msgstr "hat ein Label zur Karte hinzugefügt"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:106
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:193
#: src/views/card/components/ActivityList.tsx:182
msgid "added checklist <0>{0}</0>"
msgstr "hat Checkliste <0>{0}</0> hinzugefügt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:217
#: src/views/card/components/ActivityList.tsx:206
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:177
#: src/views/card/components/ActivityList.tsx:166
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."
@@ -300,8 +282,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(displayName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
msgid "assigned <0>{0}</0> to the card"
msgstr "hat <0>{0}</0> der Karte zugewiesen"
@@ -580,8 +562,8 @@ msgstr "Passwort ändern"
msgid "Change your language preferences."
msgstr "Ändern Sie Ihre Spracheinstellungen."
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "hat den Fälligkeitstermin auf <0>{formattedDate}</0> geändert"
@@ -669,12 +651,12 @@ msgstr "Abgeschlossen"
msgid "Complete control and ownership:"
msgstr "Vollständige Kontrolle und Eigentum:"
#: src/views/card/components/ActivityList.tsx:124
#: src/views/card/components/ActivityList.tsx:113
msgid "completed a checklist item"
msgstr "hat ein Checklistenelement abgeschlossen"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:234
#: src/views/card/components/ActivityList.tsx:223
msgid "completed checklist item <0>{0}</0>"
msgstr "hat Checklistenelement <0>{0}</0> abgeschlossen"
@@ -773,7 +755,7 @@ msgid "Create list"
msgstr "Liste erstellen"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Neue(n) {0} erstellen"
@@ -788,7 +770,7 @@ msgid "Create new label"
msgstr "Neues Label erstellen"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:539
#: src/views/board/index.tsx:537
msgid "Create new list"
msgstr "Neue Liste erstellen"
@@ -801,7 +783,7 @@ msgstr "Vorlage erstellen"
msgid "Create workspace"
msgstr "Arbeitsbereich erstellen"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:100
msgid "created the card"
msgstr "hat die Karte erstellt"
@@ -875,7 +857,7 @@ msgstr "Löschen"
msgid "Delete account"
msgstr "Konto löschen"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete board"
msgstr "Board löschen"
@@ -891,7 +873,7 @@ msgstr "Kommentar löschen"
msgid "Delete list"
msgstr "Liste löschen"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete template"
msgstr "Vorlage löschen"
@@ -901,21 +883,21 @@ msgstr "Vorlage löschen"
msgid "Delete workspace"
msgstr "Workspace löschen"
#: src/views/card/components/ActivityList.tsx:121
#: src/views/card/components/ActivityList.tsx:110
msgid "deleted a checklist"
msgstr "hat eine Checkliste gelöscht"
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:115
msgid "deleted a checklist item"
msgstr "hat ein Checklistenelement gelöscht"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:209
#: src/views/card/components/ActivityList.tsx:198
msgid "deleted checklist <0>{0}</0>"
msgstr "hat Checkliste <0>{0}</0> gelöscht"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:252
#: src/views/card/components/ActivityList.tsx:241
msgid "deleted checklist item <0>{0}</0>"
msgstr "hat Checklistenelement <0>{0}</0> gelöscht"
@@ -1011,7 +993,7 @@ msgstr "Morgen fällig"
msgid "Edit"
msgstr "Bearbeiten"
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Board-URL bearbeiten"
@@ -1299,11 +1281,11 @@ msgid "Get Started"
msgstr "Loslegen"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:65
#: src/views/boards/components/BoardsList.tsx:44
msgid "Get started by creating a new {0}"
msgstr "Beginnen Sie, indem Sie eine(n) neue(n) {0} erstellen"
#: src/views/board/index.tsx:524
#: src/views/board/index.tsx:522
msgid "Get started by creating a new list"
msgstr "Beginne mit dem Erstellen einer neuen Liste"
@@ -1613,7 +1595,7 @@ msgstr "Listenname"
msgid "Lists"
msgstr "Listen"
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Load more activities"
msgstr "Weitere Aktivitäten laden"
@@ -1621,7 +1603,7 @@ msgstr "Weitere Aktivitäten laden"
msgid "Loading permissions..."
msgstr "Berechtigungen werden geladen..."
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Loading..."
msgstr "Wird geladen..."
@@ -1649,16 +1631,16 @@ msgstr "Niedrige Priorität"
msgid "magic link"
msgstr "Magic Link"
#: src/views/board/components/BoardDropdown.tsx:73
#: src/views/board/components/BoardDropdown.tsx:34
msgid "Make template"
msgstr "Als Vorlage speichern"
#: src/views/card/components/ActivityList.tsx:125
#: src/views/card/components/ActivityList.tsx:114
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:243
#: src/views/card/components/ActivityList.tsx:232
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "hat Checklistenelement <0>{0}</0> als unvollständig markiert"
@@ -1666,8 +1648,6 @@ 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"
@@ -1696,11 +1676,11 @@ msgstr "monatliche Abrechnung"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:145
#: src/views/card/components/ActivityList.tsx:134
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:114
#: src/views/card/components/ActivityList.tsx:103
msgid "moved the card to another list"
msgstr "hat die Karte in eine andere Liste verschoben"
@@ -1782,7 +1762,7 @@ msgid "Next Steps"
msgstr "Nächste Schritte"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:62
#: src/views/boards/components/BoardsList.tsx:41
msgid "No {0}"
msgstr "Keine {0}"
@@ -1810,11 +1790,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:520
#: src/views/board/index.tsx:518
msgid "No lists"
msgstr "Keine Listen"
#: src/views/board/index.tsx:525
#: src/views/board/index.tsx:523
msgid "No lists have been created yet"
msgstr "Es wurden noch keine Listen erstellt"
@@ -1989,7 +1969,6 @@ 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
@@ -2117,52 +2096,44 @@ 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:103
#: src/views/card/components/ActivityList.tsx:92
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "{0} Labels entfernt: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:105
msgid "removed a label from the card"
msgstr "hat ein Label von der Karte entfernt"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:107
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:185
#: src/views/card/components/ActivityList.tsx:174
msgid "removed label <0>{0}</0>"
msgstr "hat Label <0>{0}</0> entfernt"
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
msgid "removed the due date"
msgstr "hat den Fälligkeitstermin entfernt"
#: src/views/card/components/ActivityList.tsx:120
#: src/views/card/components/ActivityList.tsx:109
msgid "renamed a checklist"
msgstr "hat eine Checkliste umbenannt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:201
#: src/views/card/components/ActivityList.tsx:190
msgid "renamed checklist <0>{0}</0>"
msgstr "hat Checkliste <0>{0}</0> umbenannt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:225
#: src/views/card/components/ActivityList.tsx:214
msgid "renamed checklist item to <0>{0}</0>"
msgstr "hat Checklistenelement in <0>{0}</0> umbenannt"
@@ -2248,7 +2219,7 @@ msgstr "Selbst hosten"
msgid "Self host with Github"
msgstr "Selbst hosten mit Github"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:143
msgid "self-assigned the card"
msgstr "hat sich selbst die Karte zugewiesen"
@@ -2268,7 +2239,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Fälligkeitsdatum festlegen"
#: src/views/card/components/ActivityList.tsx:127
#: src/views/card/components/ActivityList.tsx:116
msgid "set the due date"
msgstr "hat den Fälligkeitstermin festgelegt"
@@ -2632,10 +2603,6 @@ 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"
@@ -2687,12 +2654,12 @@ msgstr "Berechtigungen können nicht aktualisiert werden"
msgid "Unable to update role"
msgstr "Rolle kann nicht aktualisiert werden"
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
msgid "unassigned <0>{0}</0> from the card"
msgstr "hat <0>{0}</0> von der Karte entfernt"
#: src/views/card/components/ActivityList.tsx:165
#: src/views/card/components/ActivityList.tsx:154
msgid "unassigned themselves from the card"
msgstr "hat sich selbst von der Karte entfernt"
@@ -2751,24 +2718,24 @@ msgstr "Aktualisieren"
msgid "Update label"
msgstr "Label aktualisieren"
#: src/views/card/components/ActivityList.tsx:123
#: src/views/card/components/ActivityList.tsx:112
msgid "updated a checklist item"
msgstr "hat ein Checklistenelement aktualisiert"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:102
msgid "updated the description"
msgstr "hat die Beschreibung aktualisiert"
#: src/views/card/components/ActivityList.tsx:128
#: src/views/card/components/ActivityList.tsx:117
msgid "updated the due date"
msgstr "hat den Fälligkeitstermin aktualisiert"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:101
msgid "updated the title"
msgstr "hat den Titel aktualisiert"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:137
#: src/views/card/components/ActivityList.tsx:126
msgid "updated the title to <0>{0}</0>"
msgstr "hat den Titel in <0>{0}</0> geändert"
@@ -2893,7 +2860,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:65
#: src/views/public/boards/index.tsx:66
msgid "Workspace"
msgstr "Arbeitsbereich"
@@ -2998,8 +2965,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:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: 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,16 +35,6 @@ 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"
@@ -157,43 +147,39 @@ 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:89
#: src/views/card/components/ActivityList.tsx:78
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "added {0} labels: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:119
#: src/views/card/components/ActivityList.tsx:108
msgid "added a checklist"
msgstr "added a checklist"
#: src/views/card/components/ActivityList.tsx:122
#: src/views/card/components/ActivityList.tsx:111
msgid "added a checklist item"
msgstr "added a checklist item"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:104
msgid "added a label to the card"
msgstr "added a label to the card"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:106
msgid "added a member to the card"
msgstr "added a member to the card"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:193
#: src/views/card/components/ActivityList.tsx:182
msgid "added checklist <0>{0}</0>"
msgstr "added checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:217
#: src/views/card/components/ActivityList.tsx:206
msgid "added checklist item <0>{0}</0>"
msgstr "added checklist item <0>{0}</0>"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:177
#: src/views/card/components/ActivityList.tsx:166
msgid "added label <0>{0}</0>"
msgstr "added label <0>{0}</0>"
@@ -201,10 +187,6 @@ 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."
@@ -332,8 +314,8 @@ msgstr "Are you sure you want to delete your account?"
#~ msgid "around the world"
#~ msgstr "around the world"
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
msgid "assigned <0>{0}</0> to the card"
msgstr "assigned <0>{0}</0> to the card"
@@ -641,8 +623,8 @@ msgstr "Change Password"
msgid "Change your language preferences."
msgstr "Change your language preferences."
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "changed the due date to <0>{formattedDate}</0>"
@@ -730,12 +712,12 @@ msgstr "Complete"
msgid "Complete control and ownership:"
msgstr "Complete control and ownership:"
#: src/views/card/components/ActivityList.tsx:124
#: src/views/card/components/ActivityList.tsx:113
msgid "completed a checklist item"
msgstr "completed a checklist item"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:234
#: src/views/card/components/ActivityList.tsx:223
msgid "completed checklist item <0>{0}</0>"
msgstr "completed checklist item <0>{0}</0>"
@@ -846,7 +828,7 @@ msgid "Create list"
msgstr "Create list"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Create new {0}"
@@ -865,7 +847,7 @@ msgid "Create new label"
msgstr "Create new label"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:539
#: src/views/board/index.tsx:537
msgid "Create new list"
msgstr "Create new list"
@@ -882,7 +864,7 @@ msgstr "Create template"
msgid "Create workspace"
msgstr "Create workspace"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:100
msgid "created the card"
msgstr "created the card"
@@ -964,7 +946,7 @@ msgstr "Delete"
msgid "Delete account"
msgstr "Delete account"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete board"
msgstr "Delete board"
@@ -980,7 +962,7 @@ msgstr "Delete comment"
msgid "Delete list"
msgstr "Delete list"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete template"
msgstr "Delete template"
@@ -990,21 +972,21 @@ msgstr "Delete template"
msgid "Delete workspace"
msgstr "Delete workspace"
#: src/views/card/components/ActivityList.tsx:121
#: src/views/card/components/ActivityList.tsx:110
msgid "deleted a checklist"
msgstr "deleted a checklist"
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:115
msgid "deleted a checklist item"
msgstr "deleted a checklist item"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:209
#: src/views/card/components/ActivityList.tsx:198
msgid "deleted checklist <0>{0}</0>"
msgstr "deleted checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:252
#: src/views/card/components/ActivityList.tsx:241
msgid "deleted checklist item <0>{0}</0>"
msgstr "deleted checklist item <0>{0}</0>"
@@ -1100,7 +1082,7 @@ msgstr "Due tomorrow"
msgid "Edit"
msgstr "Edit"
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Edit board URL"
@@ -1396,7 +1378,7 @@ msgid "Get Started"
msgstr "Get Started"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:65
#: src/views/boards/components/BoardsList.tsx:44
msgid "Get started by creating a new {0}"
msgstr "Get started by creating a new {0}"
@@ -1404,7 +1386,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:524
#: src/views/board/index.tsx:522
msgid "Get started by creating a new list"
msgstr "Get started by creating a new list"
@@ -1722,7 +1704,7 @@ msgstr "List name"
msgid "Lists"
msgstr "Lists"
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Load more activities"
msgstr "Load more activities"
@@ -1730,7 +1712,7 @@ msgstr "Load more activities"
msgid "Loading permissions..."
msgstr "Loading permissions..."
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Loading..."
msgstr "Loading..."
@@ -1758,16 +1740,16 @@ msgstr "Low Priority"
msgid "magic link"
msgstr "magic link"
#: src/views/board/components/BoardDropdown.tsx:73
#: src/views/board/components/BoardDropdown.tsx:34
msgid "Make template"
msgstr "Make template"
#: src/views/card/components/ActivityList.tsx:125
#: src/views/card/components/ActivityList.tsx:114
msgid "marked a checklist item as incomplete"
msgstr "marked a checklist item as incomplete"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:243
#: src/views/card/components/ActivityList.tsx:232
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "marked checklist item <0>{0}</0> as incomplete"
@@ -1775,8 +1757,6 @@ 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"
@@ -1805,7 +1785,7 @@ msgstr "monthly billing"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:145
#: src/views/card/components/ActivityList.tsx:134
msgid "moved the card from <0>{0}</0> to<1>{1}</1>"
msgstr "moved the card from <0>{0}</0> to<1>{1}</1>"
@@ -1813,7 +1793,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:114
#: src/views/card/components/ActivityList.tsx:103
msgid "moved the card to another list"
msgstr "moved the card to another list"
@@ -1907,7 +1887,7 @@ msgid "Next Steps"
msgstr "Next Steps"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:62
#: src/views/boards/components/BoardsList.tsx:41
msgid "No {0}"
msgstr "No {0}"
@@ -1939,11 +1919,11 @@ msgstr "No download URL available for this attachment."
msgid "No keyboard shortcuts registered."
msgstr "No keyboard shortcuts registered."
#: src/views/board/index.tsx:520
#: src/views/board/index.tsx:518
msgid "No lists"
msgstr "No lists"
#: src/views/board/index.tsx:525
#: src/views/board/index.tsx:523
msgid "No lists have been created yet"
msgstr "No lists have been created yet"
@@ -2118,7 +2098,6 @@ 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
@@ -2246,33 +2225,25 @@ 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:103
#: src/views/card/components/ActivityList.tsx:92
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "removed {0} labels: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:105
msgid "removed a label from the card"
msgstr "removed a label from the card"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:107
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:185
#: src/views/card/components/ActivityList.tsx:174
msgid "removed label <0>{0}</0>"
msgstr "removed label <0>{0}</0>"
@@ -2280,22 +2251,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:129
#: src/views/card/components/ActivityList.tsx:288
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
msgid "removed the due date"
msgstr "removed the due date"
#: src/views/card/components/ActivityList.tsx:120
#: src/views/card/components/ActivityList.tsx:109
msgid "renamed a checklist"
msgstr "renamed a checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:201
#: src/views/card/components/ActivityList.tsx:190
msgid "renamed checklist <0>{0}</0>"
msgstr "renamed checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:225
#: src/views/card/components/ActivityList.tsx:214
msgid "renamed checklist item to <0>{0}</0>"
msgstr "renamed checklist item to <0>{0}</0>"
@@ -2385,7 +2356,7 @@ msgstr "Self Host"
msgid "Self host with Github"
msgstr "Self host with Github"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:143
msgid "self-assigned the card"
msgstr "self-assigned the card"
@@ -2409,7 +2380,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Set due date"
#: src/views/card/components/ActivityList.tsx:127
#: src/views/card/components/ActivityList.tsx:116
msgid "set the due date"
msgstr "set the due date"
@@ -2789,10 +2760,6 @@ 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"
@@ -2844,8 +2811,8 @@ msgstr "Unable to update permissions"
msgid "Unable to update role"
msgstr "Unable to update role"
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
msgid "unassigned <0>{0}</0> from the card"
msgstr "unassigned <0>{0}</0> from the card"
@@ -2853,7 +2820,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:165
#: src/views/card/components/ActivityList.tsx:154
msgid "unassigned themselves from the card"
msgstr "unassigned themselves from the card"
@@ -2912,24 +2879,24 @@ msgstr "Update"
msgid "Update label"
msgstr "Update label"
#: src/views/card/components/ActivityList.tsx:123
#: src/views/card/components/ActivityList.tsx:112
msgid "updated a checklist item"
msgstr "updated a checklist item"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:102
msgid "updated the description"
msgstr "updated the description"
#: src/views/card/components/ActivityList.tsx:128
#: src/views/card/components/ActivityList.tsx:117
msgid "updated the due date"
msgstr "updated the due date"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:101
msgid "updated the title"
msgstr "updated the title"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:137
#: src/views/card/components/ActivityList.tsx:126
msgid "updated the title to <0>{0}</0>"
msgstr "updated the title to <0>{0}</0>"
@@ -3074,7 +3041,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:65
#: src/views/public/boards/index.tsx:66
msgid "Workspace"
msgstr "Workspace"
@@ -3183,8 +3150,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:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: 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,16 +35,6 @@ 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"
@@ -141,50 +131,42 @@ 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:89
#: src/views/card/components/ActivityList.tsx:78
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "añadió {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:119
#: src/views/card/components/ActivityList.tsx:108
msgid "added a checklist"
msgstr "añadió una lista de verificación"
#: src/views/card/components/ActivityList.tsx:122
#: src/views/card/components/ActivityList.tsx:111
msgid "added a checklist item"
msgstr "añadió un elemento a la lista de verificación"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:104
msgid "added a label to the card"
msgstr "añadió una etiqueta a la tarjeta"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:106
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:193
#: src/views/card/components/ActivityList.tsx:182
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:217
#: src/views/card/components/ActivityList.tsx:206
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:177
#: src/views/card/components/ActivityList.tsx:166
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."
@@ -300,8 +282,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(displayName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
msgid "assigned <0>{0}</0> to the card"
msgstr "asignó <0>{0}</0> a la tarjeta"
@@ -580,8 +562,8 @@ msgstr "Cambiar contraseña"
msgid "Change your language preferences."
msgstr "Cambia tus preferencias de idioma."
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "cambió la fecha de vencimiento a <0>{formattedDate}</0>"
@@ -669,12 +651,12 @@ msgstr "Completado"
msgid "Complete control and ownership:"
msgstr "Control y propiedad completos:"
#: src/views/card/components/ActivityList.tsx:124
#: src/views/card/components/ActivityList.tsx:113
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:234
#: src/views/card/components/ActivityList.tsx:223
msgid "completed checklist item <0>{0}</0>"
msgstr "completó el elemento <0>{0}</0> de la lista de verificación"
@@ -773,7 +755,7 @@ msgid "Create list"
msgstr "Crear lista"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Crear nuevo {0}"
@@ -788,7 +770,7 @@ msgid "Create new label"
msgstr "Crear nueva etiqueta"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:539
#: src/views/board/index.tsx:537
msgid "Create new list"
msgstr "Crear nueva lista"
@@ -801,7 +783,7 @@ msgstr "Crear plantilla"
msgid "Create workspace"
msgstr "Crear espacio de trabajo"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:100
msgid "created the card"
msgstr "creó la tarjeta"
@@ -875,7 +857,7 @@ msgstr "Eliminar"
msgid "Delete account"
msgstr "Eliminar cuenta"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete board"
msgstr "Eliminar tablero"
@@ -891,7 +873,7 @@ msgstr "Eliminar comentario"
msgid "Delete list"
msgstr "Eliminar lista"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete template"
msgstr "Eliminar plantilla"
@@ -901,21 +883,21 @@ msgstr "Eliminar plantilla"
msgid "Delete workspace"
msgstr "Eliminar espacio de trabajo"
#: src/views/card/components/ActivityList.tsx:121
#: src/views/card/components/ActivityList.tsx:110
msgid "deleted a checklist"
msgstr "eliminó una lista de verificación"
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:115
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:209
#: src/views/card/components/ActivityList.tsx:198
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:252
#: src/views/card/components/ActivityList.tsx:241
msgid "deleted checklist item <0>{0}</0>"
msgstr "eliminó el elemento <0>{0}</0> de la lista de verificación"
@@ -1011,7 +993,7 @@ msgstr "Vence mañana"
msgid "Edit"
msgstr "Editar"
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Editar URL del tablero"
@@ -1299,11 +1281,11 @@ msgid "Get Started"
msgstr "Comenzar"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:65
#: src/views/boards/components/BoardsList.tsx:44
msgid "Get started by creating a new {0}"
msgstr "Comienza creando un nuevo {0}"
#: src/views/board/index.tsx:524
#: src/views/board/index.tsx:522
msgid "Get started by creating a new list"
msgstr "Comienza creando una nueva lista"
@@ -1613,7 +1595,7 @@ msgstr "Nombre de la lista"
msgid "Lists"
msgstr "Listas"
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Load more activities"
msgstr "Cargar más actividades"
@@ -1621,7 +1603,7 @@ msgstr "Cargar más actividades"
msgid "Loading permissions..."
msgstr "Cargando permisos..."
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Loading..."
msgstr "Cargando..."
@@ -1649,16 +1631,16 @@ msgstr "Prioridad baja"
msgid "magic link"
msgstr "enlace mágico"
#: src/views/board/components/BoardDropdown.tsx:73
#: src/views/board/components/BoardDropdown.tsx:34
msgid "Make template"
msgstr "Crear plantilla"
#: src/views/card/components/ActivityList.tsx:125
#: src/views/card/components/ActivityList.tsx:114
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:243
#: src/views/card/components/ActivityList.tsx:232
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "marcó el elemento <0>{0}</0> de la lista de verificación como incompleto"
@@ -1666,8 +1648,6 @@ 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"
@@ -1696,11 +1676,11 @@ msgstr "facturación mensual"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:145
#: src/views/card/components/ActivityList.tsx:134
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:114
#: src/views/card/components/ActivityList.tsx:103
msgid "moved the card to another list"
msgstr "movió la tarjeta a otra lista"
@@ -1782,7 +1762,7 @@ msgid "Next Steps"
msgstr "Próximos pasos"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:62
#: src/views/boards/components/BoardsList.tsx:41
msgid "No {0}"
msgstr "Sin {0}"
@@ -1810,11 +1790,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:520
#: src/views/board/index.tsx:518
msgid "No lists"
msgstr "Sin listas"
#: src/views/board/index.tsx:525
#: src/views/board/index.tsx:523
msgid "No lists have been created yet"
msgstr "Aún no se han creado listas"
@@ -1989,7 +1969,6 @@ 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
@@ -2117,52 +2096,44 @@ 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:103
#: src/views/card/components/ActivityList.tsx:92
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "eliminó {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:105
msgid "removed a label from the card"
msgstr "eliminó una etiqueta de la tarjeta"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:107
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:185
#: src/views/card/components/ActivityList.tsx:174
msgid "removed label <0>{0}</0>"
msgstr "eliminó la etiqueta <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
msgid "removed the due date"
msgstr "eliminó la fecha de vencimiento"
#: src/views/card/components/ActivityList.tsx:120
#: src/views/card/components/ActivityList.tsx:109
msgid "renamed a checklist"
msgstr "renombró una lista de verificación"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:201
#: src/views/card/components/ActivityList.tsx:190
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:225
#: src/views/card/components/ActivityList.tsx:214
msgid "renamed checklist item to <0>{0}</0>"
msgstr "renombró el elemento de la lista de verificación a <0>{0}</0>"
@@ -2248,7 +2219,7 @@ msgstr "Autoalojamiento"
msgid "Self host with Github"
msgstr "Autoalojamiento con Github"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:143
msgid "self-assigned the card"
msgstr "se autoasignó la tarjeta"
@@ -2268,7 +2239,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Establecer fecha de vencimiento"
#: src/views/card/components/ActivityList.tsx:127
#: src/views/card/components/ActivityList.tsx:116
msgid "set the due date"
msgstr "estableció la fecha de vencimiento"
@@ -2632,10 +2603,6 @@ 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"
@@ -2687,12 +2654,12 @@ msgstr "No se pueden actualizar los permisos"
msgid "Unable to update role"
msgstr "No se puede actualizar el rol"
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
msgid "unassigned <0>{0}</0> from the card"
msgstr "desasignó <0>{0}</0> de la tarjeta"
#: src/views/card/components/ActivityList.tsx:165
#: src/views/card/components/ActivityList.tsx:154
msgid "unassigned themselves from the card"
msgstr "se desasignó de la tarjeta"
@@ -2751,24 +2718,24 @@ msgstr "Actualizar"
msgid "Update label"
msgstr "Actualizar etiqueta"
#: src/views/card/components/ActivityList.tsx:123
#: src/views/card/components/ActivityList.tsx:112
msgid "updated a checklist item"
msgstr "actualizó un elemento de la lista de verificación"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:102
msgid "updated the description"
msgstr "actualizó la descripción"
#: src/views/card/components/ActivityList.tsx:128
#: src/views/card/components/ActivityList.tsx:117
msgid "updated the due date"
msgstr "actualizó la fecha de vencimiento"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:101
msgid "updated the title"
msgstr "actualizó el título"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:137
#: src/views/card/components/ActivityList.tsx:126
msgid "updated the title to <0>{0}</0>"
msgstr "actualizó el título a <0>{0}</0>"
@@ -2893,7 +2860,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:65
#: src/views/public/boards/index.tsx:66
msgid "Workspace"
msgstr "Espacio de trabajo"
@@ -2998,8 +2965,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:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: 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,16 +35,6 @@ 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"
@@ -141,50 +131,42 @@ 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:89
#: src/views/card/components/ActivityList.tsx:78
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "a ajouté {0} étiquettes : <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:119
#: src/views/card/components/ActivityList.tsx:108
msgid "added a checklist"
msgstr "a ajouté une checklist"
#: src/views/card/components/ActivityList.tsx:122
#: src/views/card/components/ActivityList.tsx:111
msgid "added a checklist item"
msgstr "a ajouté un élément à la checklist"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:104
msgid "added a label to the card"
msgstr "a ajouté une étiquette à la carte"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:106
msgid "added a member to the card"
msgstr "a ajouté un membre à la carte"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:193
#: src/views/card/components/ActivityList.tsx:182
msgid "added checklist <0>{0}</0>"
msgstr "a ajouté la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:217
#: src/views/card/components/ActivityList.tsx:206
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:177
#: src/views/card/components/ActivityList.tsx:166
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."
@@ -300,8 +282,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(displayName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
msgid "assigned <0>{0}</0> to the card"
msgstr "a assigné <0>{0}</0> à la carte"
@@ -580,8 +562,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:267
#: src/views/card/components/ActivityList.tsx:281
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "a modifié la date d'échéance pour <0>{formattedDate}</0>"
@@ -669,12 +651,12 @@ msgstr "Terminé"
msgid "Complete control and ownership:"
msgstr "Contrôle et propriété complets :"
#: src/views/card/components/ActivityList.tsx:124
#: src/views/card/components/ActivityList.tsx:113
msgid "completed a checklist item"
msgstr "a terminé un élément de la checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:234
#: src/views/card/components/ActivityList.tsx:223
msgid "completed checklist item <0>{0}</0>"
msgstr "a terminé l'élément <0>{0}</0> de la checklist"
@@ -773,7 +755,7 @@ msgid "Create list"
msgstr "Créer une liste"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Créer un nouveau {0}"
@@ -788,7 +770,7 @@ msgid "Create new label"
msgstr "Créer une nouvelle étiquette"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:539
#: src/views/board/index.tsx:537
msgid "Create new list"
msgstr "Créer une nouvelle liste"
@@ -801,7 +783,7 @@ msgstr "Créer un modèle"
msgid "Create workspace"
msgstr "Créer un espace de travail"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:100
msgid "created the card"
msgstr "a créé la carte"
@@ -875,7 +857,7 @@ msgstr "Supprimer"
msgid "Delete account"
msgstr "Supprimer le compte"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete board"
msgstr "Supprimer le tableau"
@@ -891,7 +873,7 @@ msgstr "Supprimer le commentaire"
msgid "Delete list"
msgstr "Supprimer la liste"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete template"
msgstr "Supprimer le modèle"
@@ -901,21 +883,21 @@ msgstr "Supprimer le modèle"
msgid "Delete workspace"
msgstr "Supprimer l'espace de travail"
#: src/views/card/components/ActivityList.tsx:121
#: src/views/card/components/ActivityList.tsx:110
msgid "deleted a checklist"
msgstr "a supprimé une checklist"
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:115
msgid "deleted a checklist item"
msgstr "a supprimé un élément de la checklist"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:209
#: src/views/card/components/ActivityList.tsx:198
msgid "deleted checklist <0>{0}</0>"
msgstr "a supprimé la checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:252
#: src/views/card/components/ActivityList.tsx:241
msgid "deleted checklist item <0>{0}</0>"
msgstr "a supprimé l'élément <0>{0}</0> de la checklist"
@@ -1011,7 +993,7 @@ msgstr "Échéance demain"
msgid "Edit"
msgstr "Modifier"
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Modifier l'URL du tableau"
@@ -1299,11 +1281,11 @@ msgid "Get Started"
msgstr "Commencer"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:65
#: src/views/boards/components/BoardsList.tsx:44
msgid "Get started by creating a new {0}"
msgstr "Commencez par créer un nouveau {0}"
#: src/views/board/index.tsx:524
#: src/views/board/index.tsx:522
msgid "Get started by creating a new list"
msgstr "Commencez par créer une nouvelle liste"
@@ -1613,7 +1595,7 @@ msgstr "Nom de la liste"
msgid "Lists"
msgstr "Listes"
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Load more activities"
msgstr "Charger plus d'activités"
@@ -1621,7 +1603,7 @@ msgstr "Charger plus d'activités"
msgid "Loading permissions..."
msgstr "Chargement des permissions..."
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Loading..."
msgstr "Chargement..."
@@ -1649,16 +1631,16 @@ msgstr "Priorité basse"
msgid "magic link"
msgstr "lien magique"
#: src/views/board/components/BoardDropdown.tsx:73
#: src/views/board/components/BoardDropdown.tsx:34
msgid "Make template"
msgstr "Créer un modèle"
#: src/views/card/components/ActivityList.tsx:125
#: src/views/card/components/ActivityList.tsx:114
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:243
#: src/views/card/components/ActivityList.tsx:232
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "a marqué l'élément <0>{0}</0> de la checklist comme incomplet"
@@ -1666,8 +1648,6 @@ 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"
@@ -1696,11 +1676,11 @@ msgstr "facturation mensuelle"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:145
#: src/views/card/components/ActivityList.tsx:134
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:114
#: src/views/card/components/ActivityList.tsx:103
msgid "moved the card to another list"
msgstr "a déplacé la carte vers une autre liste"
@@ -1782,7 +1762,7 @@ msgid "Next Steps"
msgstr "Prochaines étapes"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:62
#: src/views/boards/components/BoardsList.tsx:41
msgid "No {0}"
msgstr "Aucun {0}"
@@ -1810,11 +1790,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:520
#: src/views/board/index.tsx:518
msgid "No lists"
msgstr "Aucune liste"
#: src/views/board/index.tsx:525
#: src/views/board/index.tsx:523
msgid "No lists have been created yet"
msgstr "Aucune liste n'a encore été créée"
@@ -1989,7 +1969,6 @@ 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
@@ -2117,52 +2096,44 @@ 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:103
#: src/views/card/components/ActivityList.tsx:92
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "a supprimé {0} étiquettes : <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:105
msgid "removed a label from the card"
msgstr "a supprimé une étiquette de la carte"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:107
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:185
#: src/views/card/components/ActivityList.tsx:174
msgid "removed label <0>{0}</0>"
msgstr "a supprimé l'étiquette <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
msgid "removed the due date"
msgstr "a supprimé la date d'échéance"
#: src/views/card/components/ActivityList.tsx:120
#: src/views/card/components/ActivityList.tsx:109
msgid "renamed a checklist"
msgstr "a renommé une checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:201
#: src/views/card/components/ActivityList.tsx:190
msgid "renamed checklist <0>{0}</0>"
msgstr "a renommé la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:225
#: src/views/card/components/ActivityList.tsx:214
msgid "renamed checklist item to <0>{0}</0>"
msgstr "a renommé l'élément de la liste de contrôle en <0>{0}</0>"
@@ -2248,7 +2219,7 @@ msgstr "Auto-hébergement"
msgid "Self host with Github"
msgstr "Auto-hébergement avec Github"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:143
msgid "self-assigned the card"
msgstr "s'est auto-assigné la carte"
@@ -2268,7 +2239,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Définir la date d'échéance"
#: src/views/card/components/ActivityList.tsx:127
#: src/views/card/components/ActivityList.tsx:116
msgid "set the due date"
msgstr "a défini la date d'échéance"
@@ -2632,10 +2603,6 @@ 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"
@@ -2687,12 +2654,12 @@ msgstr "Impossible de mettre à jour les permissions"
msgid "Unable to update role"
msgstr "Impossible de mettre à jour le rôle"
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
msgid "unassigned <0>{0}</0> from the card"
msgstr "a désassigné <0>{0}</0> de la carte"
#: src/views/card/components/ActivityList.tsx:165
#: src/views/card/components/ActivityList.tsx:154
msgid "unassigned themselves from the card"
msgstr "s'est retiré de la carte"
@@ -2751,24 +2718,24 @@ msgstr "Mettre à jour"
msgid "Update label"
msgstr "Mettre à jour l'étiquette"
#: src/views/card/components/ActivityList.tsx:123
#: src/views/card/components/ActivityList.tsx:112
msgid "updated a checklist item"
msgstr "a mis à jour un élément de la liste de contrôle"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:102
msgid "updated the description"
msgstr "a mis à jour la description"
#: src/views/card/components/ActivityList.tsx:128
#: src/views/card/components/ActivityList.tsx:117
msgid "updated the due date"
msgstr "a mis à jour la date d'échéance"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:101
msgid "updated the title"
msgstr "a mis à jour le titre"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:137
#: src/views/card/components/ActivityList.tsx:126
msgid "updated the title to <0>{0}</0>"
msgstr "a mis à jour le titre en <0>{0}</0>"
@@ -2893,7 +2860,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:65
#: src/views/public/boards/index.tsx:66
msgid "Workspace"
msgstr "Espace de travail"
@@ -2998,8 +2965,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:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: 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,16 +35,6 @@ 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"
@@ -141,50 +131,42 @@ 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:89
#: src/views/card/components/ActivityList.tsx:78
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "ha aggiunto {0} etichette: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:119
#: src/views/card/components/ActivityList.tsx:108
msgid "added a checklist"
msgstr "ha aggiunto una checklist"
#: src/views/card/components/ActivityList.tsx:122
#: src/views/card/components/ActivityList.tsx:111
msgid "added a checklist item"
msgstr "ha aggiunto un elemento alla checklist"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:104
msgid "added a label to the card"
msgstr "ha aggiunto un'etichetta alla carta"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:106
msgid "added a member to the card"
msgstr "ha aggiunto un membro alla carta"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:193
#: src/views/card/components/ActivityList.tsx:182
msgid "added checklist <0>{0}</0>"
msgstr "ha aggiunto la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:217
#: src/views/card/components/ActivityList.tsx:206
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:177
#: src/views/card/components/ActivityList.tsx:166
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."
@@ -300,8 +282,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(displayName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
msgid "assigned <0>{0}</0> to the card"
msgstr "ha assegnato <0>{0}</0> alla scheda"
@@ -580,8 +562,8 @@ msgstr "Cambia password"
msgid "Change your language preferences."
msgstr "Modifica le tue preferenze di lingua."
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "ha cambiato la data di scadenza a <0>{formattedDate}</0>"
@@ -669,12 +651,12 @@ msgstr "Completato"
msgid "Complete control and ownership:"
msgstr "Controllo e proprietà completi:"
#: src/views/card/components/ActivityList.tsx:124
#: src/views/card/components/ActivityList.tsx:113
msgid "completed a checklist item"
msgstr "ha completato un elemento della checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:234
#: src/views/card/components/ActivityList.tsx:223
msgid "completed checklist item <0>{0}</0>"
msgstr "ha completato l'elemento <0>{0}</0> della checklist"
@@ -773,7 +755,7 @@ msgid "Create list"
msgstr "Crea lista"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Crea nuovo {0}"
@@ -788,7 +770,7 @@ msgid "Create new label"
msgstr "Crea nuova etichetta"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:539
#: src/views/board/index.tsx:537
msgid "Create new list"
msgstr "Crea nuova lista"
@@ -801,7 +783,7 @@ msgstr "Crea template"
msgid "Create workspace"
msgstr "Crea spazio di lavoro"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:100
msgid "created the card"
msgstr "ha creato la carta"
@@ -875,7 +857,7 @@ msgstr "Elimina"
msgid "Delete account"
msgstr "Elimina account"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete board"
msgstr "Elimina bacheca"
@@ -891,7 +873,7 @@ msgstr "Elimina commento"
msgid "Delete list"
msgstr "Elimina lista"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete template"
msgstr "Elimina template"
@@ -901,21 +883,21 @@ msgstr "Elimina template"
msgid "Delete workspace"
msgstr "Elimina spazio di lavoro"
#: src/views/card/components/ActivityList.tsx:121
#: src/views/card/components/ActivityList.tsx:110
msgid "deleted a checklist"
msgstr "ha eliminato una checklist"
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:115
msgid "deleted a checklist item"
msgstr "ha eliminato un elemento della checklist"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:209
#: src/views/card/components/ActivityList.tsx:198
msgid "deleted checklist <0>{0}</0>"
msgstr "ha eliminato la checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:252
#: src/views/card/components/ActivityList.tsx:241
msgid "deleted checklist item <0>{0}</0>"
msgstr "ha eliminato l'elemento <0>{0}</0> della checklist"
@@ -1011,7 +993,7 @@ msgstr "Scade domani"
msgid "Edit"
msgstr "Modifica"
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Modifica URL della bacheca"
@@ -1299,11 +1281,11 @@ msgid "Get Started"
msgstr "Inizia ora"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:65
#: src/views/boards/components/BoardsList.tsx:44
msgid "Get started by creating a new {0}"
msgstr "Inizia creando un nuovo {0}"
#: src/views/board/index.tsx:524
#: src/views/board/index.tsx:522
msgid "Get started by creating a new list"
msgstr "Inizia creando una nuova lista"
@@ -1613,7 +1595,7 @@ msgstr "Nome lista"
msgid "Lists"
msgstr "Liste"
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Load more activities"
msgstr "Carica altre attività"
@@ -1621,7 +1603,7 @@ msgstr "Carica altre attività"
msgid "Loading permissions..."
msgstr "Caricamento permessi..."
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Loading..."
msgstr "Caricamento in corso..."
@@ -1649,16 +1631,16 @@ msgstr "Bassa priorità"
msgid "magic link"
msgstr "link magico"
#: src/views/board/components/BoardDropdown.tsx:73
#: src/views/board/components/BoardDropdown.tsx:34
msgid "Make template"
msgstr "Crea template"
#: src/views/card/components/ActivityList.tsx:125
#: src/views/card/components/ActivityList.tsx:114
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:243
#: src/views/card/components/ActivityList.tsx:232
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "ha segnato l'elemento <0>{0}</0> della checklist come incompleto"
@@ -1666,8 +1648,6 @@ 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"
@@ -1696,11 +1676,11 @@ msgstr "fatturazione mensile"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:145
#: src/views/card/components/ActivityList.tsx:134
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:114
#: src/views/card/components/ActivityList.tsx:103
msgid "moved the card to another list"
msgstr "ha spostato la scheda in un'altra lista"
@@ -1782,7 +1762,7 @@ msgid "Next Steps"
msgstr "Prossimi passi"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:62
#: src/views/boards/components/BoardsList.tsx:41
msgid "No {0}"
msgstr "Nessun {0}"
@@ -1810,11 +1790,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:520
#: src/views/board/index.tsx:518
msgid "No lists"
msgstr "Nessuna lista"
#: src/views/board/index.tsx:525
#: src/views/board/index.tsx:523
msgid "No lists have been created yet"
msgstr "Nessuna lista è stata ancora creata"
@@ -1989,7 +1969,6 @@ 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
@@ -2117,52 +2096,44 @@ 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:103
#: src/views/card/components/ActivityList.tsx:92
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "ha rimosso {0} etichette: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:105
msgid "removed a label from the card"
msgstr "ha rimosso un'etichetta dalla scheda"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:107
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:185
#: src/views/card/components/ActivityList.tsx:174
msgid "removed label <0>{0}</0>"
msgstr "ha rimosso l'etichetta <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
msgid "removed the due date"
msgstr "ha rimosso la data di scadenza"
#: src/views/card/components/ActivityList.tsx:120
#: src/views/card/components/ActivityList.tsx:109
msgid "renamed a checklist"
msgstr "ha rinominato una checklist"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:201
#: src/views/card/components/ActivityList.tsx:190
msgid "renamed checklist <0>{0}</0>"
msgstr "ha rinominato la checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:225
#: src/views/card/components/ActivityList.tsx:214
msgid "renamed checklist item to <0>{0}</0>"
msgstr "ha rinominato l'elemento della checklist in <0>{0}</0>"
@@ -2248,7 +2219,7 @@ msgstr "Self Host"
msgid "Self host with Github"
msgstr "Self host con Github"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:143
msgid "self-assigned the card"
msgstr "si è auto-assegnato la scheda"
@@ -2268,7 +2239,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Imposta data di scadenza"
#: src/views/card/components/ActivityList.tsx:127
#: src/views/card/components/ActivityList.tsx:116
msgid "set the due date"
msgstr "ha impostato la data di scadenza"
@@ -2632,10 +2603,6 @@ 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"
@@ -2687,12 +2654,12 @@ msgstr "Impossibile aggiornare i permessi"
msgid "Unable to update role"
msgstr "Impossibile aggiornare il ruolo"
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
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:165
#: src/views/card/components/ActivityList.tsx:154
msgid "unassigned themselves from the card"
msgstr "si è rimosso dall'assegnazione della scheda"
@@ -2751,24 +2718,24 @@ msgstr "Aggiorna"
msgid "Update label"
msgstr "Aggiorna etichetta"
#: src/views/card/components/ActivityList.tsx:123
#: src/views/card/components/ActivityList.tsx:112
msgid "updated a checklist item"
msgstr "ha aggiornato un elemento della checklist"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:102
msgid "updated the description"
msgstr "ha aggiornato la descrizione"
#: src/views/card/components/ActivityList.tsx:128
#: src/views/card/components/ActivityList.tsx:117
msgid "updated the due date"
msgstr "ha aggiornato la data di scadenza"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:101
msgid "updated the title"
msgstr "ha aggiornato il titolo"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:137
#: src/views/card/components/ActivityList.tsx:126
msgid "updated the title to <0>{0}</0>"
msgstr "ha aggiornato il titolo in <0>{0}</0>"
@@ -2893,7 +2860,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:65
#: src/views/public/boards/index.tsx:66
msgid "Workspace"
msgstr "Spazio di lavoro"
@@ -2998,8 +2965,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:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: 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,16 +35,6 @@ 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"
@@ -141,50 +131,42 @@ 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:89
#: src/views/card/components/ActivityList.tsx:78
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "{0} labels toegevoegd: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:119
#: src/views/card/components/ActivityList.tsx:108
msgid "added a checklist"
msgstr "heeft een checklist toegevoegd"
#: src/views/card/components/ActivityList.tsx:122
#: src/views/card/components/ActivityList.tsx:111
msgid "added a checklist item"
msgstr "heeft een checklistitem toegevoegd"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:104
msgid "added a label to the card"
msgstr "heeft een label aan de kaart toegevoegd"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:106
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:193
#: src/views/card/components/ActivityList.tsx:182
msgid "added checklist <0>{0}</0>"
msgstr "heeft checklist <0>{0}</0> toegevoegd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:217
#: src/views/card/components/ActivityList.tsx:206
msgid "added checklist item <0>{0}</0>"
msgstr "heeft checklistitem <0>{0}</0> toegevoegd"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:177
#: src/views/card/components/ActivityList.tsx:166
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."
@@ -300,8 +282,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(displayName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
msgid "assigned <0>{0}</0> to the card"
msgstr "heeft <0>{0}</0> aan de kaart toegewezen"
@@ -580,8 +562,8 @@ msgstr "Wachtwoord wijzigen"
msgid "Change your language preferences."
msgstr "Wijzig je taalvoorkeuren."
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "heeft de vervaldatum gewijzigd naar <0>{formattedDate}</0>"
@@ -669,12 +651,12 @@ msgstr "Voltooid"
msgid "Complete control and ownership:"
msgstr "Volledige controle en eigendom:"
#: src/views/card/components/ActivityList.tsx:124
#: src/views/card/components/ActivityList.tsx:113
msgid "completed a checklist item"
msgstr "heeft een checklistitem voltooid"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:234
#: src/views/card/components/ActivityList.tsx:223
msgid "completed checklist item <0>{0}</0>"
msgstr "heeft checklistitem <0>{0}</0> voltooid"
@@ -773,7 +755,7 @@ msgid "Create list"
msgstr "Lijst maken"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Nieuwe {0} maken"
@@ -788,7 +770,7 @@ msgid "Create new label"
msgstr "Maak nieuw label"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:539
#: src/views/board/index.tsx:537
msgid "Create new list"
msgstr "Maak nieuwe lijst"
@@ -801,7 +783,7 @@ msgstr "Sjabloon maken"
msgid "Create workspace"
msgstr "Werkruimte maken"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:100
msgid "created the card"
msgstr "heeft de kaart aangemaakt"
@@ -875,7 +857,7 @@ msgstr "Verwijderen"
msgid "Delete account"
msgstr "Account verwijderen"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete board"
msgstr "Board verwijderen"
@@ -891,7 +873,7 @@ msgstr "Reactie verwijderen"
msgid "Delete list"
msgstr "Lijst verwijderen"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete template"
msgstr "Sjabloon verwijderen"
@@ -901,21 +883,21 @@ msgstr "Sjabloon verwijderen"
msgid "Delete workspace"
msgstr "Werkruimte verwijderen"
#: src/views/card/components/ActivityList.tsx:121
#: src/views/card/components/ActivityList.tsx:110
msgid "deleted a checklist"
msgstr "heeft een checklist verwijderd"
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:115
msgid "deleted a checklist item"
msgstr "heeft een checklistitem verwijderd"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:209
#: src/views/card/components/ActivityList.tsx:198
msgid "deleted checklist <0>{0}</0>"
msgstr "heeft checklist <0>{0}</0> verwijderd"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:252
#: src/views/card/components/ActivityList.tsx:241
msgid "deleted checklist item <0>{0}</0>"
msgstr "heeft checklistitem <0>{0}</0> verwijderd"
@@ -1011,7 +993,7 @@ msgstr "Vervalt morgen"
msgid "Edit"
msgstr "Bewerken"
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Board URL bewerken"
@@ -1299,11 +1281,11 @@ msgid "Get Started"
msgstr "Aan de slag"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:65
#: src/views/boards/components/BoardsList.tsx:44
msgid "Get started by creating a new {0}"
msgstr "Begin door een nieuwe {0} te maken"
#: src/views/board/index.tsx:524
#: src/views/board/index.tsx:522
msgid "Get started by creating a new list"
msgstr "Begin door een nieuwe lijst te maken"
@@ -1613,7 +1595,7 @@ msgstr "Lijstnaam"
msgid "Lists"
msgstr "Lijsten"
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Load more activities"
msgstr "Meer activiteiten laden"
@@ -1621,7 +1603,7 @@ msgstr "Meer activiteiten laden"
msgid "Loading permissions..."
msgstr "Rechten laden..."
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Loading..."
msgstr "Laden..."
@@ -1649,16 +1631,16 @@ msgstr "Lage prioriteit"
msgid "magic link"
msgstr "magische link"
#: src/views/board/components/BoardDropdown.tsx:73
#: src/views/board/components/BoardDropdown.tsx:34
msgid "Make template"
msgstr "Sjabloon maken"
#: src/views/card/components/ActivityList.tsx:125
#: src/views/card/components/ActivityList.tsx:114
msgid "marked a checklist item as incomplete"
msgstr "heeft een checklistitem als onvoltooid gemarkeerd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:243
#: src/views/card/components/ActivityList.tsx:232
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "heeft checklistitem <0>{0}</0> als onvoltooid gemarkeerd"
@@ -1666,8 +1648,6 @@ 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"
@@ -1696,11 +1676,11 @@ msgstr "maandelijkse facturering"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:145
#: src/views/card/components/ActivityList.tsx:134
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:114
#: src/views/card/components/ActivityList.tsx:103
msgid "moved the card to another list"
msgstr "heeft de kaart naar een andere lijst verplaatst"
@@ -1782,7 +1762,7 @@ msgid "Next Steps"
msgstr "Volgende stappen"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:62
#: src/views/boards/components/BoardsList.tsx:41
msgid "No {0}"
msgstr "Geen {0}"
@@ -1810,11 +1790,11 @@ msgstr "Geen download-URL beschikbaar voor deze bijlage."
msgid "No keyboard shortcuts registered."
msgstr "Geen sneltoetsen geregistreerd."
#: src/views/board/index.tsx:520
#: src/views/board/index.tsx:518
msgid "No lists"
msgstr "Geen lijsten"
#: src/views/board/index.tsx:525
#: src/views/board/index.tsx:523
msgid "No lists have been created yet"
msgstr "Er zijn nog geen lijsten aangemaakt"
@@ -1989,7 +1969,6 @@ 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
@@ -2117,52 +2096,44 @@ 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:103
#: src/views/card/components/ActivityList.tsx:92
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "{0} labels verwijderd: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:105
msgid "removed a label from the card"
msgstr "heeft een label van de kaart verwijderd"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:107
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:185
#: src/views/card/components/ActivityList.tsx:174
msgid "removed label <0>{0}</0>"
msgstr "heeft label <0>{0}</0> verwijderd"
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
msgid "removed the due date"
msgstr "heeft de vervaldatum verwijderd"
#: src/views/card/components/ActivityList.tsx:120
#: src/views/card/components/ActivityList.tsx:109
msgid "renamed a checklist"
msgstr "heeft een checklist hernoemd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:201
#: src/views/card/components/ActivityList.tsx:190
msgid "renamed checklist <0>{0}</0>"
msgstr "heeft checklist <0>{0}</0> hernoemd"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:225
#: src/views/card/components/ActivityList.tsx:214
msgid "renamed checklist item to <0>{0}</0>"
msgstr "checklistitem hernoemd naar <0>{0}</0>"
@@ -2248,7 +2219,7 @@ msgstr "Zelf hosten"
msgid "Self host with Github"
msgstr "Zelf hosten met Github"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:143
msgid "self-assigned the card"
msgstr "heeft de kaart aan zichzelf toegewezen"
@@ -2268,7 +2239,7 @@ msgstr "Senior"
msgid "Set due date"
msgstr "Vervaldatum instellen"
#: src/views/card/components/ActivityList.tsx:127
#: src/views/card/components/ActivityList.tsx:116
msgid "set the due date"
msgstr "heeft de vervaldatum ingesteld"
@@ -2632,10 +2603,6 @@ 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"
@@ -2687,12 +2654,12 @@ msgstr "Kan machtigingen niet bijwerken"
msgid "Unable to update role"
msgstr "Kan rol niet bijwerken"
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
msgid "unassigned <0>{0}</0> from the card"
msgstr "<0>{0}</0> van de kaart verwijderd"
#: src/views/card/components/ActivityList.tsx:165
#: src/views/card/components/ActivityList.tsx:154
msgid "unassigned themselves from the card"
msgstr "zichzelf van de kaart verwijderd"
@@ -2751,24 +2718,24 @@ msgstr "Bijwerken"
msgid "Update label"
msgstr "Label bijwerken"
#: src/views/card/components/ActivityList.tsx:123
#: src/views/card/components/ActivityList.tsx:112
msgid "updated a checklist item"
msgstr "een checklistitem bijgewerkt"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:102
msgid "updated the description"
msgstr "beschrijving bijgewerkt"
#: src/views/card/components/ActivityList.tsx:128
#: src/views/card/components/ActivityList.tsx:117
msgid "updated the due date"
msgstr "heeft de vervaldatum bijgewerkt"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:101
msgid "updated the title"
msgstr "titel bijgewerkt"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:137
#: src/views/card/components/ActivityList.tsx:126
msgid "updated the title to <0>{0}</0>"
msgstr "titel bijgewerkt naar <0>{0}</0>"
@@ -2893,7 +2860,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:65
#: src/views/public/boards/index.tsx:66
msgid "Workspace"
msgstr "Werkruimte"
@@ -2998,8 +2965,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:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: 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,16 +40,6 @@ 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"
@@ -146,50 +136,42 @@ 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:89
#: src/views/card/components/ActivityList.tsx:78
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "dodano {0} etykiety: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:119
#: src/views/card/components/ActivityList.tsx:108
msgid "added a checklist"
msgstr "dodał listę kontrolną"
#: src/views/card/components/ActivityList.tsx:122
#: src/views/card/components/ActivityList.tsx:111
msgid "added a checklist item"
msgstr "dodał(a) element listy kontrolnej"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:104
msgid "added a label to the card"
msgstr "dodał(a) etykietę do karty"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:106
msgid "added a member to the card"
msgstr "dodał(a) członka do karty"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:193
#: src/views/card/components/ActivityList.tsx:182
msgid "added checklist <0>{0}</0>"
msgstr "dodał(a) listę kontrolną <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:217
#: src/views/card/components/ActivityList.tsx:206
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:177
#: src/views/card/components/ActivityList.tsx:166
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."
@@ -305,8 +287,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(displayName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
msgid "assigned <0>{0}</0> to the card"
msgstr "przypisano <0>{0}</0> do karty"
@@ -585,8 +567,8 @@ msgstr "Zmień hasło"
msgid "Change your language preferences."
msgstr "Zmień swoje preferencje językowe."
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "zmieniono termin na <0>{formattedDate}</0>"
@@ -674,12 +656,12 @@ msgstr "Zakończ"
msgid "Complete control and ownership:"
msgstr "Pełna kontrola i własność:"
#: src/views/card/components/ActivityList.tsx:124
#: src/views/card/components/ActivityList.tsx:113
msgid "completed a checklist item"
msgstr "ukończono element listy kontrolnej"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:234
#: src/views/card/components/ActivityList.tsx:223
msgid "completed checklist item <0>{0}</0>"
msgstr "ukończono element listy kontrolnej <0>{0}</0>"
@@ -778,7 +760,7 @@ msgid "Create list"
msgstr "Utwórz listę"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Utwórz nowy {0}"
@@ -793,7 +775,7 @@ msgid "Create new label"
msgstr "Utwórz nową etykietę"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:539
#: src/views/board/index.tsx:537
msgid "Create new list"
msgstr "Utwórz nową listę"
@@ -806,7 +788,7 @@ msgstr "Utwórz szablon"
msgid "Create workspace"
msgstr "Utwórz przestrzeń roboczą"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:100
msgid "created the card"
msgstr "utworzył(a) kartę"
@@ -880,7 +862,7 @@ msgstr "Usuń"
msgid "Delete account"
msgstr "Usuń konto"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete board"
msgstr "Usuń tablicę"
@@ -896,7 +878,7 @@ msgstr "Usuń komentarz"
msgid "Delete list"
msgstr "Usuń listę"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete template"
msgstr "Usuń szablon"
@@ -906,21 +888,21 @@ msgstr "Usuń szablon"
msgid "Delete workspace"
msgstr "Usuń przestrzeń roboczą"
#: src/views/card/components/ActivityList.tsx:121
#: src/views/card/components/ActivityList.tsx:110
msgid "deleted a checklist"
msgstr "usunięto listę kontrolną"
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:115
msgid "deleted a checklist item"
msgstr "usunięto element listy kontrolnej"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:209
#: src/views/card/components/ActivityList.tsx:198
msgid "deleted checklist <0>{0}</0>"
msgstr "usunięto listę kontrolną <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:252
#: src/views/card/components/ActivityList.tsx:241
msgid "deleted checklist item <0>{0}</0>"
msgstr "usunięto element listy kontrolnej <0>{0}</0>"
@@ -1016,7 +998,7 @@ msgstr "Termin na jutro"
msgid "Edit"
msgstr "Edytuj"
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Edytuj URL tablicy"
@@ -1304,11 +1286,11 @@ msgid "Get Started"
msgstr "Rozpocznij"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:65
#: src/views/boards/components/BoardsList.tsx:44
msgid "Get started by creating a new {0}"
msgstr "Rozpocznij, tworząc nowy {0}"
#: src/views/board/index.tsx:524
#: src/views/board/index.tsx:522
msgid "Get started by creating a new list"
msgstr "Rozpocznij, tworząc nową listę"
@@ -1618,7 +1600,7 @@ msgstr "Nazwa listy"
msgid "Lists"
msgstr "Listy"
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Load more activities"
msgstr "Załaduj więcej aktywności"
@@ -1626,7 +1608,7 @@ msgstr "Załaduj więcej aktywności"
msgid "Loading permissions..."
msgstr "Ładowanie uprawnień..."
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Loading..."
msgstr "Ładowanie..."
@@ -1654,16 +1636,16 @@ msgstr "Niski priorytet"
msgid "magic link"
msgstr "magiczny link"
#: src/views/board/components/BoardDropdown.tsx:73
#: src/views/board/components/BoardDropdown.tsx:34
msgid "Make template"
msgstr "Utwórz szablon"
#: src/views/card/components/ActivityList.tsx:125
#: src/views/card/components/ActivityList.tsx:114
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:243
#: src/views/card/components/ActivityList.tsx:232
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "oznaczył element listy kontrolnej <0>{0}</0> jako nieukończony"
@@ -1671,8 +1653,6 @@ 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"
@@ -1701,11 +1681,11 @@ msgstr "rozliczenie miesięczne"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:145
#: src/views/card/components/ActivityList.tsx:134
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:114
#: src/views/card/components/ActivityList.tsx:103
msgid "moved the card to another list"
msgstr "przeniesiono kartę do innej listy"
@@ -1787,7 +1767,7 @@ msgid "Next Steps"
msgstr "Następne kroki"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:62
#: src/views/boards/components/BoardsList.tsx:41
msgid "No {0}"
msgstr "Brak {0}"
@@ -1815,11 +1795,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:520
#: src/views/board/index.tsx:518
msgid "No lists"
msgstr "Brak list"
#: src/views/board/index.tsx:525
#: src/views/board/index.tsx:523
msgid "No lists have been created yet"
msgstr "Nie utworzono jeszcze żadnych list"
@@ -1994,7 +1974,6 @@ 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
@@ -2122,52 +2101,44 @@ 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:103
#: src/views/card/components/ActivityList.tsx:92
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "usunięto {0} etykiety: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:105
msgid "removed a label from the card"
msgstr "usunął etykietę z karty"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:107
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:185
#: src/views/card/components/ActivityList.tsx:174
msgid "removed label <0>{0}</0>"
msgstr "usunięto etykietę <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
msgid "removed the due date"
msgstr "usunięto termin"
#: src/views/card/components/ActivityList.tsx:120
#: src/views/card/components/ActivityList.tsx:109
msgid "renamed a checklist"
msgstr "zmieniono nazwę listy kontrolnej"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:201
#: src/views/card/components/ActivityList.tsx:190
msgid "renamed checklist <0>{0}</0>"
msgstr "zmieniono nazwę listy kontrolnej <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:225
#: src/views/card/components/ActivityList.tsx:214
msgid "renamed checklist item to <0>{0}</0>"
msgstr "zmieniono nazwę elementu listy kontrolnej na <0>{0}</0>"
@@ -2253,7 +2224,7 @@ msgstr "Hostuj samodzielnie"
msgid "Self host with Github"
msgstr "Hostuj samodzielnie z Github"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:143
msgid "self-assigned the card"
msgstr "przypisał(a) sobie kartę"
@@ -2273,7 +2244,7 @@ msgstr "Starszy"
msgid "Set due date"
msgstr "Ustaw termin"
#: src/views/card/components/ActivityList.tsx:127
#: src/views/card/components/ActivityList.tsx:116
msgid "set the due date"
msgstr "ustawiono termin"
@@ -2637,10 +2608,6 @@ 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"
@@ -2692,12 +2659,12 @@ msgstr "Nie można zaktualizować uprawnień"
msgid "Unable to update role"
msgstr "Nie można zaktualizować roli"
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
msgid "unassigned <0>{0}</0> from the card"
msgstr "usunięto przypisanie <0>{0}</0> z karty"
#: src/views/card/components/ActivityList.tsx:165
#: src/views/card/components/ActivityList.tsx:154
msgid "unassigned themselves from the card"
msgstr "usunęli swoje przypisanie z karty"
@@ -2756,24 +2723,24 @@ msgstr "Aktualizuj"
msgid "Update label"
msgstr "Zaktualizuj etykietę"
#: src/views/card/components/ActivityList.tsx:123
#: src/views/card/components/ActivityList.tsx:112
msgid "updated a checklist item"
msgstr "zaktualizowano element listy kontrolnej"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:102
msgid "updated the description"
msgstr "zaktualizowano opis"
#: src/views/card/components/ActivityList.tsx:128
#: src/views/card/components/ActivityList.tsx:117
msgid "updated the due date"
msgstr "zaktualizowano termin"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:101
msgid "updated the title"
msgstr "zaktualizowano tytuł"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:137
#: src/views/card/components/ActivityList.tsx:126
msgid "updated the title to <0>{0}</0>"
msgstr "zaktualizowano tytuł na <0>{0}</0>"
@@ -2898,7 +2865,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:65
#: src/views/public/boards/index.tsx:66
msgid "Workspace"
msgstr "Obszar roboczy"
@@ -3003,8 +2970,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:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: 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,16 +35,6 @@ 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"
@@ -141,50 +131,42 @@ 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:89
#: src/views/card/components/ActivityList.tsx:78
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "adicionou {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:119
#: src/views/card/components/ActivityList.tsx:108
msgid "added a checklist"
msgstr "adicionou uma lista de verificação"
#: src/views/card/components/ActivityList.tsx:122
#: src/views/card/components/ActivityList.tsx:111
msgid "added a checklist item"
msgstr "adicionou um item da lista de verificação"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:104
msgid "added a label to the card"
msgstr "adicionou uma etiqueta ao cartão"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:106
msgid "added a member to the card"
msgstr "adicionou um membro ao cartão"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:193
#: src/views/card/components/ActivityList.tsx:182
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:217
#: src/views/card/components/ActivityList.tsx:206
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:177
#: src/views/card/components/ActivityList.tsx:166
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."
@@ -300,8 +282,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(displayName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
msgid "assigned <0>{0}</0> to the card"
msgstr "atribuiu <0>{0}</0> ao card"
@@ -580,8 +562,8 @@ msgstr "Alterar senha"
msgid "Change your language preferences."
msgstr "Altere suas preferências de idioma."
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "alterou a data de vencimento para <0>{formattedDate}</0>"
@@ -669,12 +651,12 @@ msgstr "Concluir"
msgid "Complete control and ownership:"
msgstr "Controle e propriedade completos:"
#: src/views/card/components/ActivityList.tsx:124
#: src/views/card/components/ActivityList.tsx:113
msgid "completed a checklist item"
msgstr "concluiu um item da lista de verificação"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:234
#: src/views/card/components/ActivityList.tsx:223
msgid "completed checklist item <0>{0}</0>"
msgstr "concluiu o item da lista de verificação <0>{0}</0>"
@@ -773,7 +755,7 @@ msgid "Create list"
msgstr "Criar lista"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Criar novo {0}"
@@ -788,7 +770,7 @@ msgid "Create new label"
msgstr "Criar nova etiqueta"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:539
#: src/views/board/index.tsx:537
msgid "Create new list"
msgstr "Criar nova lista"
@@ -801,7 +783,7 @@ msgstr "Criar modelo"
msgid "Create workspace"
msgstr "Criar workspace"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:100
msgid "created the card"
msgstr "criou o cartão"
@@ -875,7 +857,7 @@ msgstr "Excluir"
msgid "Delete account"
msgstr "Excluir conta"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete board"
msgstr "Excluir quadro"
@@ -891,7 +873,7 @@ msgstr "Excluir comentário"
msgid "Delete list"
msgstr "Excluir lista"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete template"
msgstr "Excluir modelo"
@@ -901,21 +883,21 @@ msgstr "Excluir modelo"
msgid "Delete workspace"
msgstr "Excluir workspace"
#: src/views/card/components/ActivityList.tsx:121
#: src/views/card/components/ActivityList.tsx:110
msgid "deleted a checklist"
msgstr "excluiu uma checklist"
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:115
msgid "deleted a checklist item"
msgstr "excluiu um item da checklist"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:209
#: src/views/card/components/ActivityList.tsx:198
msgid "deleted checklist <0>{0}</0>"
msgstr "excluiu a checklist <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:252
#: src/views/card/components/ActivityList.tsx:241
msgid "deleted checklist item <0>{0}</0>"
msgstr "excluiu o item da checklist <0>{0}</0>"
@@ -1011,7 +993,7 @@ msgstr "Vence amanhã"
msgid "Edit"
msgstr "Editar"
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Editar URL do quadro"
@@ -1299,11 +1281,11 @@ msgid "Get Started"
msgstr "Começar"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:65
#: src/views/boards/components/BoardsList.tsx:44
msgid "Get started by creating a new {0}"
msgstr "Comece criando um novo {0}"
#: src/views/board/index.tsx:524
#: src/views/board/index.tsx:522
msgid "Get started by creating a new list"
msgstr "Comece criando uma nova lista"
@@ -1613,7 +1595,7 @@ msgstr "Nome da lista"
msgid "Lists"
msgstr "Listas"
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Load more activities"
msgstr "Carregar mais atividades"
@@ -1621,7 +1603,7 @@ msgstr "Carregar mais atividades"
msgid "Loading permissions..."
msgstr "Carregando permissões..."
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Loading..."
msgstr "Carregando..."
@@ -1649,16 +1631,16 @@ msgstr "Prioridade baixa"
msgid "magic link"
msgstr "link mágico"
#: src/views/board/components/BoardDropdown.tsx:73
#: src/views/board/components/BoardDropdown.tsx:34
msgid "Make template"
msgstr "Criar modelo"
#: src/views/card/components/ActivityList.tsx:125
#: src/views/card/components/ActivityList.tsx:114
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:243
#: src/views/card/components/ActivityList.tsx:232
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "marcou o item da checklist <0>{0}</0> como incompleto"
@@ -1666,8 +1648,6 @@ 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"
@@ -1696,11 +1676,11 @@ msgstr "cobrança mensal"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:145
#: src/views/card/components/ActivityList.tsx:134
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:114
#: src/views/card/components/ActivityList.tsx:103
msgid "moved the card to another list"
msgstr "moveu o card para outra lista"
@@ -1782,7 +1762,7 @@ msgid "Next Steps"
msgstr "Próximos passos"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:62
#: src/views/boards/components/BoardsList.tsx:41
msgid "No {0}"
msgstr "Nenhum {0}"
@@ -1810,11 +1790,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:520
#: src/views/board/index.tsx:518
msgid "No lists"
msgstr "Nenhuma lista"
#: src/views/board/index.tsx:525
#: src/views/board/index.tsx:523
msgid "No lists have been created yet"
msgstr "Nenhuma lista foi criada ainda"
@@ -1989,7 +1969,6 @@ 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
@@ -2117,52 +2096,44 @@ 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:103
#: src/views/card/components/ActivityList.tsx:92
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "removeu {0} etiquetas: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:105
msgid "removed a label from the card"
msgstr "removeu uma etiqueta do cartão"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:107
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:185
#: src/views/card/components/ActivityList.tsx:174
msgid "removed label <0>{0}</0>"
msgstr "removeu a etiqueta <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
msgid "removed the due date"
msgstr "removeu a data de vencimento"
#: src/views/card/components/ActivityList.tsx:120
#: src/views/card/components/ActivityList.tsx:109
msgid "renamed a checklist"
msgstr "renomeou uma lista de verificação"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:201
#: src/views/card/components/ActivityList.tsx:190
msgid "renamed checklist <0>{0}</0>"
msgstr "renomeou a checklist <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:225
#: src/views/card/components/ActivityList.tsx:214
msgid "renamed checklist item to <0>{0}</0>"
msgstr "renomeou o item da checklist para <0>{0}</0>"
@@ -2248,7 +2219,7 @@ msgstr "Auto-hospedagem"
msgid "Self host with Github"
msgstr "Auto-hospede com Github"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:143
msgid "self-assigned the card"
msgstr "atribuiu o cartão a si mesmo"
@@ -2268,7 +2239,7 @@ msgstr "Sênior"
msgid "Set due date"
msgstr "Definir data de vencimento"
#: src/views/card/components/ActivityList.tsx:127
#: src/views/card/components/ActivityList.tsx:116
msgid "set the due date"
msgstr "definiu a data de vencimento"
@@ -2632,10 +2603,6 @@ 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"
@@ -2687,12 +2654,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(displayName)
#: src/views/card/components/ActivityList.tsx:168
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
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:165
#: src/views/card/components/ActivityList.tsx:154
msgid "unassigned themselves from the card"
msgstr "removeu a própria atribuição do cartão"
@@ -2751,24 +2718,24 @@ msgstr "Atualizar"
msgid "Update label"
msgstr "Atualizar rótulo"
#: src/views/card/components/ActivityList.tsx:123
#: src/views/card/components/ActivityList.tsx:112
msgid "updated a checklist item"
msgstr "atualizou um item da lista de verificação"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:102
msgid "updated the description"
msgstr "atualizou a descrição"
#: src/views/card/components/ActivityList.tsx:128
#: src/views/card/components/ActivityList.tsx:117
msgid "updated the due date"
msgstr "atualizou a data de vencimento"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:101
msgid "updated the title"
msgstr "atualizou o título"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:137
#: src/views/card/components/ActivityList.tsx:126
msgid "updated the title to <0>{0}</0>"
msgstr "atualizou o título para <0>{0}</0>"
@@ -2893,7 +2860,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:65
#: src/views/public/boards/index.tsx:66
msgid "Workspace"
msgstr "Workspace"
@@ -2998,8 +2965,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:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: 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,16 +35,6 @@ 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"
@@ -141,50 +131,42 @@ 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:89
#: src/views/card/components/ActivityList.tsx:78
msgid "added {0} labels: <0>{labelList}</0>"
msgstr "добавил {0} метки: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:119
#: src/views/card/components/ActivityList.tsx:108
msgid "added a checklist"
msgstr "добавил(а) чек-лист"
#: src/views/card/components/ActivityList.tsx:122
#: src/views/card/components/ActivityList.tsx:111
msgid "added a checklist item"
msgstr "добавил(а) элемент чек-листа"
#: src/views/card/components/ActivityList.tsx:115
#: src/views/card/components/ActivityList.tsx:104
msgid "added a label to the card"
msgstr "добавил(а) метку к карточке"
#: src/views/card/components/ActivityList.tsx:117
#: src/views/card/components/ActivityList.tsx:106
msgid "added a member to the card"
msgstr "добавил(а) участника к карточке"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:193
#: src/views/card/components/ActivityList.tsx:182
msgid "added checklist <0>{0}</0>"
msgstr "добавлен контрольный список <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:217
#: src/views/card/components/ActivityList.tsx:206
msgid "added checklist item <0>{0}</0>"
msgstr "добавлен элемент контрольного списка <0>{0}</0>"
#. placeholder {0}: truncate(label)
#: src/views/card/components/ActivityList.tsx:177
#: src/views/card/components/ActivityList.tsx:166
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}) за место."
@@ -300,8 +282,8 @@ msgstr "Вы уверены, что хотите удалить эту метк
msgid "Are you sure you want to delete your account?"
msgstr "Вы уверены, что хотите удалить свой аккаунт?"
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:157
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:146
msgid "assigned <0>{0}</0> to the card"
msgstr "назначено <0>{0}</0> к карточке"
@@ -580,8 +562,8 @@ msgstr "Изменить пароль"
msgid "Change your language preferences."
msgstr "Измените настройки языка."
#: src/views/card/components/ActivityList.tsx:267
#: src/views/card/components/ActivityList.tsx:281
#: src/views/card/components/ActivityList.tsx:256
#: src/views/card/components/ActivityList.tsx:270
msgid "changed the due date to <0>{formattedDate}</0>"
msgstr "изменил(а) дату выполнения на <0>{formattedDate}</0>"
@@ -669,12 +651,12 @@ msgstr "Завершить"
msgid "Complete control and ownership:"
msgstr "Полный контроль и владение:"
#: src/views/card/components/ActivityList.tsx:124
#: src/views/card/components/ActivityList.tsx:113
msgid "completed a checklist item"
msgstr "завершил элемент контрольного списка"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:234
#: src/views/card/components/ActivityList.tsx:223
msgid "completed checklist item <0>{0}</0>"
msgstr "завершил элемент контрольного списка <0>{0}</0>"
@@ -773,7 +755,7 @@ msgid "Create list"
msgstr "Создать список"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:79
#: src/views/boards/components/BoardsList.tsx:58
#: src/views/boards/index.tsx:28
msgid "Create new {0}"
msgstr "Создать новый {0}"
@@ -788,7 +770,7 @@ msgid "Create new label"
msgstr "Создать новую метку"
#: src/views/board/index.tsx:74
#: src/views/board/index.tsx:539
#: src/views/board/index.tsx:537
msgid "Create new list"
msgstr "Создать новый список"
@@ -801,7 +783,7 @@ msgstr "Создать шаблон"
msgid "Create workspace"
msgstr "Создать рабочее пространство"
#: src/views/card/components/ActivityList.tsx:111
#: src/views/card/components/ActivityList.tsx:100
msgid "created the card"
msgstr "создал(а) карточку"
@@ -875,7 +857,7 @@ msgstr "Удалить"
msgid "Delete account"
msgstr "Удалить аккаунт"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete board"
msgstr "Удалить доску"
@@ -891,7 +873,7 @@ msgstr "Удалить комментарий"
msgid "Delete list"
msgstr "Удалить список"
#: src/views/board/components/BoardDropdown.tsx:104
#: src/views/board/components/BoardDropdown.tsx:55
msgid "Delete template"
msgstr "Удалить шаблон"
@@ -901,21 +883,21 @@ msgstr "Удалить шаблон"
msgid "Delete workspace"
msgstr "Удалить рабочее пространство"
#: src/views/card/components/ActivityList.tsx:121
#: src/views/card/components/ActivityList.tsx:110
msgid "deleted a checklist"
msgstr "удалил контрольный список"
#: src/views/card/components/ActivityList.tsx:126
#: src/views/card/components/ActivityList.tsx:115
msgid "deleted a checklist item"
msgstr "удалил элемент контрольного списка"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:209
#: src/views/card/components/ActivityList.tsx:198
msgid "deleted checklist <0>{0}</0>"
msgstr "удалил контрольный список <0>{0}</0>"
#. placeholder {0}: truncate(fromTitle)
#: src/views/card/components/ActivityList.tsx:252
#: src/views/card/components/ActivityList.tsx:241
msgid "deleted checklist item <0>{0}</0>"
msgstr "удалил элемент контрольного списка <0>{0}</0>"
@@ -1011,7 +993,7 @@ msgstr "Срок завтра"
msgid "Edit"
msgstr "Редактировать"
#: src/views/board/components/BoardDropdown.tsx:84
#: src/views/board/components/BoardDropdown.tsx:44
#: src/views/board/components/UpdateBoardSlugForm.tsx:116
msgid "Edit board URL"
msgstr "Изменить URL доски"
@@ -1299,11 +1281,11 @@ msgid "Get Started"
msgstr "Начать"
#. placeholder {0}: isTemplate ? "template" : "board"
#: src/views/boards/components/BoardsList.tsx:65
#: src/views/boards/components/BoardsList.tsx:44
msgid "Get started by creating a new {0}"
msgstr "Начните с создания нового {0}"
#: src/views/board/index.tsx:524
#: src/views/board/index.tsx:522
msgid "Get started by creating a new list"
msgstr "Начните с создания нового списка"
@@ -1613,7 +1595,7 @@ msgstr "Название списка"
msgid "Lists"
msgstr "Списки"
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Load more activities"
msgstr "Загрузить больше действий"
@@ -1621,7 +1603,7 @@ msgstr "Загрузить больше действий"
msgid "Loading permissions..."
msgstr "Загрузка разрешений..."
#: src/views/card/components/ActivityList.tsx:545
#: src/views/card/components/ActivityList.tsx:533
msgid "Loading..."
msgstr "Загрузка..."
@@ -1649,16 +1631,16 @@ msgstr "Низкий приоритет"
msgid "magic link"
msgstr "магическая ссылка"
#: src/views/board/components/BoardDropdown.tsx:73
#: src/views/board/components/BoardDropdown.tsx:34
msgid "Make template"
msgstr "Создать шаблон"
#: src/views/card/components/ActivityList.tsx:125
#: src/views/card/components/ActivityList.tsx:114
msgid "marked a checklist item as incomplete"
msgstr "отметил элемент контрольного списка как незавершённый"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:243
#: src/views/card/components/ActivityList.tsx:232
msgid "marked checklist item <0>{0}</0> as incomplete"
msgstr "отметил элемент контрольного списка <0>{0}</0> как незавершённый"
@@ -1666,8 +1648,6 @@ 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 "Участник"
@@ -1696,11 +1676,11 @@ msgstr "ежемесячная оплата"
#. placeholder {0}: truncate(fromList)
#. placeholder {1}: truncate(toList)
#: src/views/card/components/ActivityList.tsx:145
#: src/views/card/components/ActivityList.tsx:134
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:114
#: src/views/card/components/ActivityList.tsx:103
msgid "moved the card to another list"
msgstr "переместил карточку в другой список"
@@ -1782,7 +1762,7 @@ msgid "Next Steps"
msgstr "Следующие шаги"
#. placeholder {0}: isTemplate ? "templates" : "boards"
#: src/views/boards/components/BoardsList.tsx:62
#: src/views/boards/components/BoardsList.tsx:41
msgid "No {0}"
msgstr "Нет {0}"
@@ -1810,11 +1790,11 @@ msgstr "URL для загрузки этого вложения недоступ
msgid "No keyboard shortcuts registered."
msgstr "Горячие клавиши не зарегистрированы."
#: src/views/board/index.tsx:520
#: src/views/board/index.tsx:518
msgid "No lists"
msgstr "Нет списков"
#: src/views/board/index.tsx:525
#: src/views/board/index.tsx:523
msgid "No lists have been created yet"
msgstr "Списки ещё не созданы"
@@ -1989,7 +1969,6 @@ 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
@@ -2117,52 +2096,44 @@ 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:103
#: src/views/card/components/ActivityList.tsx:92
msgid "removed {0} labels: <0>{labelList}</0>"
msgstr "удалил {0} метки: <0>{labelList}</0>"
#: src/views/card/components/ActivityList.tsx:116
#: src/views/card/components/ActivityList.tsx:105
msgid "removed a label from the card"
msgstr "удалил метку с карточки"
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:107
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:185
#: src/views/card/components/ActivityList.tsx:174
msgid "removed label <0>{0}</0>"
msgstr "удалил метку <0>{0}</0>"
#: src/views/card/components/ActivityList.tsx:129
#: src/views/card/components/ActivityList.tsx:288
#: src/views/card/components/ActivityList.tsx:118
#: src/views/card/components/ActivityList.tsx:277
msgid "removed the due date"
msgstr "удалил(а) дату выполнения"
#: src/views/card/components/ActivityList.tsx:120
#: src/views/card/components/ActivityList.tsx:109
msgid "renamed a checklist"
msgstr "переименовал контрольный список"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:201
#: src/views/card/components/ActivityList.tsx:190
msgid "renamed checklist <0>{0}</0>"
msgstr "переименовал контрольный список <0>{0}</0>"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:225
#: src/views/card/components/ActivityList.tsx:214
msgid "renamed checklist item to <0>{0}</0>"
msgstr "переименовал элемент контрольного списка в <0>{0}</0>"
@@ -2248,7 +2219,7 @@ msgstr "Самостоятельный хостинг"
msgid "Self host with Github"
msgstr "Самостоятельный хостинг с Github"
#: src/views/card/components/ActivityList.tsx:154
#: src/views/card/components/ActivityList.tsx:143
msgid "self-assigned the card"
msgstr "самостоятельно назначил карточку"
@@ -2268,7 +2239,7 @@ msgstr "Старший"
msgid "Set due date"
msgstr "Установить дату выполнения"
#: src/views/card/components/ActivityList.tsx:127
#: src/views/card/components/ActivityList.tsx:116
msgid "set the due date"
msgstr "установил(а) дату выполнения"
@@ -2632,10 +2603,6 @@ 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 доски"
@@ -2687,12 +2654,12 @@ msgstr "Не удалось обновить разрешения"
msgid "Unable to update role"
msgstr "Не удалось обновить роль"
#. placeholder {0}: truncate(displayName)
#: src/views/card/components/ActivityList.tsx:168
#. placeholder {0}: truncate(memberName)
#: src/views/card/components/ActivityList.tsx:157
msgid "unassigned <0>{0}</0> from the card"
msgstr "удалил(а) <0>{0}</0> из карточки"
#: src/views/card/components/ActivityList.tsx:165
#: src/views/card/components/ActivityList.tsx:154
msgid "unassigned themselves from the card"
msgstr "удалил(а) себя из карточки"
@@ -2751,24 +2718,24 @@ msgstr "Обновить"
msgid "Update label"
msgstr "Обновить метку"
#: src/views/card/components/ActivityList.tsx:123
#: src/views/card/components/ActivityList.tsx:112
msgid "updated a checklist item"
msgstr "обновлен элемент контрольного списка"
#: src/views/card/components/ActivityList.tsx:113
#: src/views/card/components/ActivityList.tsx:102
msgid "updated the description"
msgstr "обновлено описание"
#: src/views/card/components/ActivityList.tsx:128
#: src/views/card/components/ActivityList.tsx:117
msgid "updated the due date"
msgstr "обновил(а) дату выполнения"
#: src/views/card/components/ActivityList.tsx:112
#: src/views/card/components/ActivityList.tsx:101
msgid "updated the title"
msgstr "обновлен заголовок"
#. placeholder {0}: truncate(toTitle)
#: src/views/card/components/ActivityList.tsx:137
#: src/views/card/components/ActivityList.tsx:126
msgid "updated the title to <0>{0}</0>"
msgstr "обновлен заголовок на <0>{0}</0>"
@@ -2893,7 +2860,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:65
#: src/views/public/boards/index.tsx:66
msgid "Workspace"
msgstr "Рабочее пространство"
@@ -2998,8 +2965,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:530
#: src/views/boards/components/BoardsList.tsx:70
#: src/views/board/index.tsx:528
#: src/views/boards/components/BoardsList.tsx:49
#: 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,23 +13,6 @@ 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

@@ -11,7 +11,7 @@ import { withRateLimit } from "@kan/api/utils/rateLimit";
const workspaceSlugSchema = z
.string()
.min(3)
.max(64)
.max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/);
interface CheckoutSessionRequest {

View File

@@ -1,136 +0,0 @@
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

@@ -1,101 +0,0 @@
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

@@ -0,0 +1,75 @@
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,5 +1,4 @@
import type { ReactNode } from "react";
import React from "react";
import {
Dialog,
DialogBackdrop,
@@ -465,37 +464,19 @@ function FormattedShortcut({ shortcut }: { shortcut: KeyboardShortcut }) {
? stroke.modifiers.map(stringifyModifier)
: [];
modifierStrings.forEach((mod, index) => {
parts.push(
<kbd key={`mod-${index}-${mod}`} className={kbdClassName}>
{mod}
</kbd>,
);
modifierStrings.forEach((mod) => {
parts.push(<kbd className={kbdClassName}>{mod}</kbd>);
});
parts.push(
<kbd key={`key-${stroke.key}`} className={kbdClassName}>
{stroke.key.toUpperCase()}
</kbd>,
);
parts.push(<kbd className={kbdClassName}>{stroke.key.toUpperCase()}</kbd>);
return parts;
};
if (shortcut.type === "SEQUENCE") {
const parts: ReactNode[] = [];
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);
shortcut.strokes.forEach((stroke) => {
parts.push(...formatStroke(stroke));
});
return <span className="flex items-center gap-1 text-[11px]">{parts}</span>;
}

View File

@@ -32,7 +32,7 @@ const initialWorkspace: Workspace = {
const initialAvailableWorkspaces: Workspace[] = [];
export const WorkspaceContext = createContext<WorkspaceContextProps | undefined>(
const WorkspaceContext = createContext<WorkspaceContextProps | undefined>(
undefined,
);

View File

@@ -8,8 +8,6 @@ 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,3 +1,5 @@
import { env } from "next-runtime-env";
export const formatToArray = (
value: string | string[] | undefined,
): string[] => {
@@ -50,5 +52,14 @@ export const getAvatarUrl = (imageOrKey: string | null) => {
return imageOrKey;
}
return "";
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}`;
};

View File

@@ -66,8 +66,8 @@ const Card = ({
const hasDueDate = !!dueDate;
return (
<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>
<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>
{labels.length ||
members.length ||
checklists.length > 0 ||

View File

@@ -1,40 +1,24 @@
import { Fragment } from "react";
import Link from "next/link";
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;
}) => {
const { showPopup } = usePopup();
if (!isLoading && (!workspaceSlug || !boardSlug)) return <></>;
if (isLoading) {
return (
@@ -42,55 +26,44 @@ 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 ? t`You don't have permission` : undefined}
content={!canEdit && !isLoading ? 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">
{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>
<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>
</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/workspace"
href="/settings?tab=workspace"
onClick={closeModal}
>
{t`Edit workspace URL`}

View File

@@ -58,14 +58,13 @@ 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({
@@ -418,9 +417,10 @@ 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 disabled:cursor-not-allowed dark:text-dark-1000 sm:text-[1.2rem]"
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"
/>
</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,8 +443,6 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
isLoading={isLoading}
workspaceSlug={workspace.slug ?? ""}
boardSlug={boardData?.slug ?? ""}
boardPublicId={boardId ?? ""}
visibility={boardData?.visibility ?? "private"}
canEdit={canEditBoard}
/>
<VisibilityButton
@@ -598,13 +596,12 @@ 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,18 +23,6 @@ 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,7 +8,6 @@ import {
HiOutlineArrowRight,
HiOutlineCheckCircle,
HiOutlineClock,
HiOutlinePaperClip,
HiOutlinePencil,
HiOutlinePlus,
HiOutlineTag,
@@ -32,50 +31,29 @@ 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;
@@ -83,9 +61,7 @@ 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}
@@ -137,8 +113,6 @@ 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`,
@@ -165,23 +139,23 @@ const getActivityText = ({
);
}
if (type === "card.updated.member.added" && displayName) {
if (type === "card.updated.member.added" && memberName) {
if (isSelf) return <Trans>self-assigned the card</Trans>;
return (
<Trans>
assigned <TextHighlight>{truncate(displayName)}</TextHighlight> to the
assigned <TextHighlight>{truncate(memberName)}</TextHighlight> to the
card
</Trans>
);
}
if (type === "card.updated.member.removed" && displayName) {
if (type === "card.updated.member.removed" && memberName) {
if (isSelf) return <Trans>unassigned themselves from the card</Trans>;
return (
<Trans>
unassigned <TextHighlight>{truncate(displayName)}</TextHighlight> from
unassigned <TextHighlight>{truncate(memberName)}</TextHighlight> from
the card
</Trans>
);
@@ -271,27 +245,6 @@ 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(
@@ -344,8 +297,6 @@ 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 />,
@@ -379,7 +330,7 @@ const ActivityList = ({
isAdmin?: boolean;
isViewOnly?: boolean;
}) => {
const { dateLocale } = useLocalisation();
const { dateLocale, locale } = useLocalisation();
const { data: sessionData } = authClient.useSession();
const utils = api.useUtils();
const [allActivities, setAllActivities] = useState<
@@ -401,7 +352,7 @@ const ActivityList = ({
limit: ACTIVITIES_PAGE_SIZE,
},
{
enabled: !!cardPublicId && cardPublicId.length >= 12,
enabled: !!cardPublicId,
},
);
@@ -429,21 +380,25 @@ const ActivityList = ({
cursor: nextCursor,
});
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;
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;
}
}
setAllActivities(currentActivities);
setHasMore(false);
};
void fetchAllRemaining();
fetchAllRemaining();
} else {
setAllActivities(firstPageData.activities);
setHasMore(firstPageData.hasMore);
@@ -470,15 +425,17 @@ const ActivityList = ({
cursor: nextCursor,
});
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) {
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);
@@ -498,17 +455,13 @@ 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 ActivityWithMergedLabels).mergedLabels,
attachmentName:
(activity as ActivityWithMergedLabels).attachment?.originalFilename ??
null,
mergedLabels: (activity as any).mergedLabels,
});
if (activity.type === "card.updated.comment.added")
@@ -555,7 +508,7 @@ const ActivityList = ({
)}
</div>
<p className="text-sm">
<span className="font-medium dark:text-dark-1000">{`${getUserDisplayName(activity.user)} `}</span>
<span className="font-medium dark:text-dark-1000">{`${activity.user?.name} `}</span>
<span className="space-x-1 text-light-900 dark:text-dark-800">
{activityText}
</span>

View File

@@ -7,7 +7,6 @@ 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";
@@ -19,33 +18,62 @@ export function AttachmentUpload({ cardPublicId }: { cardPublicId: string }) {
const [isDragging, setIsDragging] = useState(false);
const inputRef = useRef<HTMLInputElement | null>(null);
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");
}
const generateUploadUrl = api.attachment.generateUploadUrl.useMutation();
const confirmAttachment = api.attachment.confirm.useMutation({
onSuccess: async () => {
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,13 +1,12 @@
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";
@@ -30,6 +29,7 @@ const Comment = ({
createdAt,
comment,
isAuthor,
isAdmin,
isEdited = false,
isViewOnly = false,
}: {
@@ -42,6 +42,7 @@ const Comment = ({
createdAt: string;
comment: string | undefined;
isAuthor: boolean;
isAdmin: boolean;
isEdited: boolean;
isViewOnly: boolean;
}) => {
@@ -56,30 +57,6 @@ 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({
@@ -165,28 +142,21 @@ const Comment = ({
)}
</div>
{!isEditing ? (
<div className="mt-2">
<Editor
content={comment ?? null}
readOnly={true}
workspaceMembers={workspaceMembers}
enableYouTubeEmbed={false}
disableHeadings={true}
/>
</div>
<ContentEditable
html={comment ?? ""}
disabled={true}
className="break-anywhere mt-2 text-sm"
/>
) : (
<form onSubmit={handleSubmit(onSubmit)}>
<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">
<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">
<Button
size="sm"
variant="ghost"

View File

@@ -1,7 +1,6 @@
import { t } from "@lingui/core/macro";
import {
HiEllipsisHorizontal,
HiLink,
HiOutlineCheckCircle,
HiOutlineTrash,
} from "react-icons/hi2";
@@ -11,54 +10,18 @@ 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,9 +1,8 @@
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";
@@ -14,13 +13,7 @@ interface FormValues {
comment: string;
}
const NewCommentForm = ({
cardPublicId,
workspaceMembers,
}: {
cardPublicId: string;
workspaceMembers: WorkspaceMember[];
}) => {
const NewCommentForm = ({ cardPublicId }: { cardPublicId: string }) => {
const utils = api.useUtils();
const { showPopup } = usePopup();
const { canCreateComment } = usePermissions();
@@ -30,6 +23,10 @@ const NewCommentForm = ({
},
});
const queryParams = {
cardPublicId,
};
const addCommentMutation = api.card.addComment.useMutation({
onError: (_error, _newList) => {
showPopup({
@@ -60,13 +57,18 @@ const NewCommentForm = ({
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"
>
<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}
<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)();
}
}}
/>
<div className="flex justify-end">
<button

View File

@@ -4,9 +4,6 @@ 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";
@@ -17,6 +14,8 @@ 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";
@@ -54,10 +53,9 @@ export function CardRightPanel({ isTemplate }: { isTemplate?: boolean }) {
? router.query.cardId[0]
: router.query.cardId;
const { data: card } = api.card.byId.useQuery(
{ cardPublicId: cardId ?? "" },
{ enabled: !!cardId && cardId.length >= 12 },
);
const { data: card } = api.card.byId.useQuery({
cardPublicId: cardId ?? "",
});
const isCreator = card?.createdBy && session?.user.id === card.createdBy;
const canEdit = canEditCard || isCreator;
@@ -168,6 +166,7 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
const {
modalContentType,
entityId,
openModal,
getModalState,
clearModalState,
isOpen,
@@ -185,10 +184,9 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
? router.query.cardId[0]
: router.query.cardId;
const { data: card, isLoading } = api.card.byId.useQuery(
{ cardPublicId: cardId ?? "" },
{ enabled: !!cardId && cardId.length >= 12 },
);
const { data: card, isLoading } = api.card.byId.useQuery({
cardPublicId: cardId ?? "",
});
const isCreator = card?.createdBy && session?.user.id === card.createdBy;
const canEdit = canEditCard || isCreator;
@@ -198,24 +196,8 @@ 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({
@@ -302,7 +284,6 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
if (!cardId) return <></>;
return (
<>
<PageHead
@@ -334,19 +315,7 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
</Link>
</div>
<div className="flex items-center gap-2">
<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>
<Dropdown cardCreatedBy={card?.createdBy} />
</div>
</>
)}
@@ -403,15 +372,9 @@ 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={workspaceMembers ?? []}
onChange={canEdit ? (e) => setValue("description", e) : undefined}
onBlur={canEdit ? () => handleSubmit(onSubmit)() : undefined}
workspaceMembers={board?.workspace.members ?? []}
readOnly={!canEdit}
/>
</div>
@@ -455,10 +418,7 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
</div>
{!isTemplate && (
<div className="mt-6">
<NewCommentForm
cardPublicId={cardId}
workspaceMembers={editorWorkspaceMembers}
/>
<NewCommentForm cardPublicId={cardId} />
</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 && workspace.publicId.length >= 12 },
// { enabled: workspace?.publicId ? true : false },
);
const { data: session } = authClient.useSession();
@@ -48,11 +48,9 @@ export default function MembersPage() {
const updateRoleMutation = api.member.updateRole.useMutation({
onSuccess: async () => {
if (workspace.publicId && workspace.publicId.length >= 12) {
await utils.workspace.byId.invalidate({
workspacePublicId: workspace.publicId,
});
}
await utils.workspace.byId.invalidate({
workspacePublicId: workspace.publicId,
});
showPopup({
header: t`Role updated`,
@@ -126,6 +124,7 @@ export default function MembersPage() {
name={memberName ?? ""}
email={memberEmail ?? ""}
imageUrl={memberImage ? getAvatarUrl(memberImage) : undefined}
icon={showPendingIcon ? "?" : undefined}
/>
)}
</div>
@@ -213,7 +212,7 @@ export default function MembersPage() {
</div>
<div
className={twMerge(
"relative",
"relative z-50",
(workspace.role !== "admin" || showSkeleton) && "hidden",
)}
>

View File

@@ -1,587 +1,293 @@
import { t } from "@lingui/core/macro";
import React, { useEffect, useRef, useState } from "react";
import { HiCheckCircle, HiXCircle } from "react-icons/hi2";
import { twMerge } from "tailwind-merge";
interface Feature {
key: string;
label: string;
kan: boolean | string;
trello: boolean | string;
}
interface Section {
name: string;
features: Feature[];
}
type FrequencyValue = "monthly" | "annually";
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 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" />
);
};
const FeatureComparisonTable = ({
frequencyValue: _frequencyValue,
frequencyValue,
}: {
frequencyValue: FrequencyValue;
}) => {
// 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[] = [
{
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" },
},
{
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,
},
const sections: Section[] = [
{
name: t`Core features`,
features: coreFeatures,
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,
},
],
},
{
name: t`Collaboration`,
features: collaborationFeatures,
},
{
name: t`Imports`,
features: importsFeatures,
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,
},
],
},
{
name: t`Platform`,
features: platformFeatures,
},
{
name: t`Security`,
features: securityFeatures,
},
{
name: t`Support`,
features: supportFeatures,
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,
},
],
},
];
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 products = [
{
id: "kan",
name: t`Kan`,
featured: true,
},
{
id: "trello",
name: t`Trello`,
featured: false,
},
];
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">
<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>
);
})}
<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>
</div>
</div>
)}
<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={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",
)}
>
{plan.id === "free" ? section.name : ""}
</td>
);
})}
</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 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" : ""}`}
/>
))}
</React.Fragment>
))}
</tbody>
</table>
</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) => (
<td
key={p.id}
className="relative w-1/3 px-4 py-0 text-center"
>
<span className="relative inline-flex w-full items-center justify-center py-3">
<CellValue
value={
p.id === "kan" ? feature.kan : feature.trello
}
/>
</span>
</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`}
/>
))}
</div>
</div>
</div>
))}
</div>
</div>
</section>

View File

@@ -23,115 +23,81 @@ const Pricing = ({
}) => {
const tiers = [
{
name: t`Free`,
id: "tier-free",
name: t`Individuals`,
id: "tier-individuals",
href: "signup",
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,
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`,
features: [
{ text: t`1 user` },
{ text: t`Unlimited boards` },
{ text: t`Unlimited cards` },
{ text: t`Custom board templates` },
{ text: t`Checklists` },
t`1 user`,
t`Unlimited boards`,
t`Unlimited cards`,
t`Custom board templates`,
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", 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,
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:`,
features: [
{ 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` },
t`Workspace members`,
t`Admin roles`,
t`Priority email support`,
t`Support the development of the project`,
],
highlighted: true,
showPrice: true,
showPriceSuffix: false,
ctaSubtext: t`14 day free trial · Switch plans or cancel anytime`,
mostPopular: true,
showPriceSuffix: true,
},
{
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,
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:`,
features: [
{ 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` },
t`Run on your own infrastructure`,
t`Own your data`,
t`Custom domain`,
],
highlighted: false,
showPrice: true,
showPriceSuffix: false,
ctaSubtext: undefined,
mostPopular: false,
showPrice: false,
},
];
return (
<>
<div className="mx-auto max-w-7xl px-4">
<div className="mb-8 flex items-center justify-center">
<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>
<fieldset aria-label={t`Payment frequency`}>
<RadioGroup
value={frequency}
onChange={(value) => setFrequency(value)}
className="flex gap-1 rounded-lg border border-light-300 bg-light-50 p-1 dark:border-dark-300 dark:bg-dark-50"
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"
>
{frequencies.map((option) => (
<Radio
key={option.value}
value={option}
className={twMerge(
"cursor-pointer rounded-md px-4 py-1.5 text-sm font-medium transition-colors",
"cursor-pointer rounded-full px-2.5 py-1 text-xs transition-colors lg:text-sm",
frequency?.value === option.value
? "bg-white text-light-1000 shadow-sm dark:bg-dark-100 dark:text-dark-1000"
: "text-light-600 dark:text-dark-700",
? "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",
)}
>
{option.label}
@@ -140,129 +106,103 @@ const Pricing = ({
</RadioGroup>
</fieldset>
</div>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<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">
{tiers.map((tier) => (
<div
key={tier.id}
className={twMerge(
"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",
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",
)}
>
{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}
<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.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>
)}
</div>
<p
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",
"mt-4 text-sm/6 text-dark-950 dark:text-dark-900",
tier.highlighted ? "text-light-100" : "text-dark-50",
)}
>
{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"]}
{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>
{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.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>
<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>
</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}
</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-[300px] text-center text-light-950 dark:text-dark-900">
{t`Start free. Upgrade as your team grows. No hidden fees, no contracts.`}
<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>
</div>
<PricingTiers
@@ -59,9 +59,23 @@ 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>
<div className="mt-10 w-full">
<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]">
<FeatureComparisonTable
frequencyValue={frequency?.value ?? "annually"}
/>

View File

@@ -1,13 +1,12 @@
import { useRouter } from "next/router";
import { t } from "@lingui/core/macro";
import { useEffect, useRef, useState } from "react";
import { HiLink, HiXMark } from "react-icons/hi2";
import { 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";
@@ -24,35 +23,16 @@ 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 && cardPublicId.length >= 12,
enabled: isOpen && !!cardPublicId,
},
);
@@ -85,44 +65,33 @@ export function CardModal({
<div className="h-full p-8">
<div className="mb-6">
<div className="flex w-full items-center justify-between">
<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();
<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();
setTimeout(() => {
void router.replace(
{
pathname: router.pathname,
query: {
...router.query,
workspaceSlug: workspaceSlug ?? "",
boardSlug: [boardSlug ?? ""],
},
setTimeout(() => {
void router.replace(
{
pathname: router.pathname,
query: {
...router.query,
workspaceSlug,
boardSlug: [boardSlug],
},
undefined,
{ shallow: true },
);
}, 400);
}}
>
<HiXMark
size={18}
className="text-light-900 dark:text-dark-900"
/>
</button>
</div>
},
undefined,
{ shallow: true },
);
}, 400);
}}
>
<HiXMark
size={18}
className="dark:text-dark-9000 text-light-900"
/>
</button>
{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,34 +70,29 @@ export default function PublicBoardView() {
},
);
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 = () => {
return (
<button
onClick={async () => {
try {
await navigator.clipboard.writeText(window.location.href);
} catch (error) {
console.error(error);
}
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>
);
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 pathWithoutQuery = router.asPath.split("?")[0];
const splitPath = pathWithoutQuery?.split("/") ?? [];

View File

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

View File

@@ -31,10 +31,9 @@ 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 },
{ enabled: !!workspace.publicId && workspace.publicId.length >= 12 },
);
const { data: workspaceData } = api.workspace.byId.useQuery({
workspacePublicId: workspace.publicId,
});
const subscriptions = workspaceData?.subscriptions as
| Subscription[]

View File

@@ -58,6 +58,28 @@ 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;
@@ -165,32 +187,29 @@ 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(
`${baseUrl}/api/upload/avatar`,
env("NEXT_PUBLIC_BASE_URL") + "/api/upload/image",
{
method: "POST",
headers: {
"Content-Type": blob.type,
"x-original-filename": fileName,
"Content-Type": "application/json",
},
body: blob,
body: JSON.stringify({ filename: fileName, contentType: blob.type }),
},
);
if (!response.ok) {
throw new Error("Failed to upload profile image");
}
if (!response.ok) throw new Error("Failed to get pre-signed URL");
// 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",
const { url } = (await response.json()) as { url: string };
const uploadResponse = await fetch(url, {
method: "PUT",
body: blob,
});
if (!uploadResponse.ok) throw new Error("Failed to upload profile image");
updateUser.mutate({ image: fileName });
setCropDialogOpen(false);
resetCropState();
} catch (error) {
@@ -208,7 +227,7 @@ export default function Avatar({
resetCropState,
selectedFile,
showPopup,
utils.user.getUser,
updateUser,
userId,
]);

View File

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

View File

@@ -33,7 +33,7 @@ const UpdateWorkspaceUrlForm = ({
.min(3, {
message: t`URL must be at least 3 characters long`,
})
.max(64, { message: t`URL cannot exceed 64 characters` })
.max(24, { message: t`URL cannot exceed 24 characters` })
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
message: t`URL can only contain letters, numbers, and hyphens`,
}),

View File

@@ -1,20 +1,4 @@
services:
migrate:
image: ghcr.io/kanbn/kan-migrate:latest
container_name: ${CONTAINER_NAME:-kan-migrate}
networks:
- kan-network
build:
context: .
dockerfile: ./apps/web/Dockerfile
target: migrate
environment:
- POSTGRES_URL=${POSTGRES_URL}
depends_on:
postgres:
condition: service_healthy
restart: "no"
web:
image: ghcr.io/kanbn/kan:latest
container_name: ${CONTAINER_NAME:-kan-web}
@@ -22,10 +6,10 @@ services:
- "${WEB_PORT:-3000}:3000"
networks:
- kan-network
- dokploy-network
build:
context: .
dockerfile: ./apps/web/Dockerfile
target: web
dockerfile: ./apps/web/Dockerfile.new
env_file:
- .env
environment:
@@ -123,8 +107,7 @@ services:
- APPLE_CLIENT_SECRET=${APPLE_CLIENT_SECRET}
- APPLE_APP_BUNDLE_IDENTIFIER=${APPLE_APP_BUNDLE_IDENTIFIER}
depends_on:
migrate:
condition: service_completed_successfully
- postgres
restart: unless-stopped
postgres:
@@ -138,17 +121,14 @@ services:
- 5432:5432
volumes:
- kan_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U kan -d kan_db"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
networks:
- kan-network
networks:
kan-network:
dokploy-network:
external: true
volumes:
kan_postgres_data:

View File

@@ -27,10 +27,6 @@
"./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",
@@ -43,6 +39,8 @@
"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 "@kan/shared/utils";
import { deleteObject, generateUploadUrl } from "../utils/s3";
export const attachmentRouter = createTRPCRouter({
generateUploadUrl: protectedProcedure
@@ -142,18 +142,9 @@ 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,
});
@@ -215,8 +206,6 @@ 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,7 +10,6 @@ import * as workspaceRepo from "@kan/db/repository/workspace.repo";
import { colours } from "@kan/shared/constants";
import {
convertDueDateFiltersToRanges,
generateAvatarUrl,
generateSlug,
generateUID,
} from "@kan/shared/utils";
@@ -143,63 +142,7 @@ export const boardRouter = createTRPCRouter({
},
);
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,
};
return result;
}),
bySlug: publicProcedure
.meta({
@@ -218,7 +161,7 @@ export const boardRouter = createTRPCRouter({
workspaceSlug: z
.string()
.min(3)
.max(64)
.max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
boardSlug: z
.string()

View File

@@ -10,9 +10,8 @@ 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 { generateAttachmentUrl, generateAvatarUrl } from "@kan/shared/utils";
import { generateDownloadUrl } from "../utils/s3";
export const cardRouter = createTRPCRouter({
create: protectedProcedure
@@ -154,17 +153,6 @@ 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
@@ -227,16 +215,6 @@ 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
@@ -317,16 +295,6 @@ 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
@@ -663,54 +631,45 @@ export const cardRouter = createTRPCRouter({
});
// Generate URLs for all attachments
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 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;
}[];
// 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;
}
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 };
}
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,
},
},
};
return { ...result, attachments: [] };
}),
getActivities: publicProcedure
.meta({
@@ -779,39 +738,7 @@ export const cardRouter = createTRPCRouter({
},
);
// 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);
const mergedActivities = mergeActivities(result.activities);
return {
activities: mergedActivities,
@@ -956,15 +883,6 @@ 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 "@kan/shared/utils";
import { createS3Client } from "../utils/s3";
const checkDatabaseConnection = async (db: dbClient) => {
try {

View File

@@ -4,7 +4,6 @@ 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
@@ -56,12 +55,8 @@ 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,
};
}),
@@ -107,12 +102,6 @@ export const userRouter = createTRPCRouter({
});
}
// Generate presigned URL for avatar
const imageUrl = await generateAvatarUrl(result.image);
return {
...result,
image: imageUrl,
};
return result;
}),
});

View File

@@ -8,7 +8,6 @@ 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
@@ -87,27 +86,9 @@ 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 = membersWithAvatarUrls.map((member) => {
const sanitizedMembers = result.members.map((member) => {
// If user doesn't have a display name, use anonymous identifier
const displayName =
member.user?.name?.trim() ?? `anonymous_${member.publicId}`;
@@ -139,10 +120,7 @@ export const workspaceRouter = createTRPCRouter({
} as Awaited<ReturnType<typeof workspaceRepo.getByPublicIdWithMembers>>;
}
return {
...result,
members: membersWithAvatarUrls,
};
return result;
}),
bySlug: publicProcedure
.meta({
@@ -160,7 +138,7 @@ export const workspaceRouter = createTRPCRouter({
workspaceSlug: z
.string()
.min(3)
.max(64)
.max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
}),
)
@@ -207,7 +185,7 @@ export const workspaceRouter = createTRPCRouter({
slug: z
.string()
.min(3)
.max(64)
.max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/)
.optional(),
}),
@@ -290,7 +268,7 @@ export const workspaceRouter = createTRPCRouter({
slug: z
.string()
.min(3)
.max(64)
.max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/)
.optional(),
description: z.string().min(3).max(280).optional(),
@@ -424,7 +402,7 @@ export const workspaceRouter = createTRPCRouter({
workspaceSlug: z
.string()
.min(3)
.max(64)
.max(24)
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/),
}),
)

View File

@@ -1,133 +0,0 @@
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?.trim() || 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

@@ -5,7 +5,6 @@ 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 =
@@ -68,60 +67,3 @@ 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;
}
}

View File

@@ -11,14 +11,15 @@ 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(",").filter(Boolean) ?? [];
return betterAuth({
secret: env("BETTER_AUTH_SECRET"),
baseURL,
trustedOrigins: [...(baseURL ? [baseURL] : []), ...trustedOrigins],
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") ?? ""],
database: drizzleAdapter(db, {
provider: "pg",
schema: {

View File

@@ -1,4 +1,4 @@
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { PutObjectCommand, S3Client } 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, createS3Client } from "@kan/shared";
import { createEmailUnsubscribeLink } from "@kan/shared";
import { downloadImage } from "./utils";
@@ -61,7 +61,20 @@ export function createDatabaseHooks(db: dbClient) {
!user.image.includes(storageDomain)
) {
try {
const client = createS3Client();
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 allowedFileExtensions = ["jpg", "jpeg", "png", "webp"];

View File

@@ -5,7 +5,6 @@ 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";
@@ -134,10 +133,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,
@@ -147,7 +146,7 @@ export function createPlugins(db: dbClient) {
newSlug = generateUID();
}
}
await workspaceRepo.update(db, subscription.referenceId, {
plan: "free",
slug: newSlug,
@@ -172,69 +171,19 @@ export function createPlugins(db: dbClient) {
magicLink({
expiresIn: 60 * 60 * 24 * 7, // 7 days
sendMagicLink: async ({ email, url }) => {
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);
}
if (url.includes("type=invite")) {
await sendEmail(
email,
workspaceName
? `Invitation to join the workspace ${workspaceName}`
: "Invitation to join workspace",
"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,
},
);
await sendEmail(email, "Sign in to kan.bn", "MAGIC_LINK", {
magicLoginUrl: url,
});
}
},
}),

View File

@@ -1 +1 @@
ALTER TABLE "workspace" ADD COLUMN IF NOT EXISTS "showEmailsToMembers" boolean NOT NULL DEFAULT true;
ALTER TABLE "workspace" ADD COLUMN "showEmailsToMembers" boolean NOT NULL DEFAULT true;

View File

@@ -32,6 +32,7 @@ CREATE TABLE IF NOT EXISTS "workspace_roles" (
--> statement-breakpoint
ALTER TABLE "workspace_roles" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
ALTER TABLE "workspace_members" ADD COLUMN "roleId" bigint;--> statement-breakpoint
ALTER TABLE "workspace" ADD COLUMN "showEmailsToMembers" boolean DEFAULT true NOT NULL;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "workspace_role_permissions" ADD CONSTRAINT "workspace_role_permissions_workspaceRoleId_workspace_roles_id_fk" FOREIGN KEY ("workspaceRoleId") REFERENCES "public"."workspace_roles"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION

View File

@@ -1,28 +0,0 @@
CREATE TABLE IF NOT EXISTS "workspace_webhooks" (
"id" bigserial PRIMARY KEY NOT NULL,
"publicId" varchar(12) NOT NULL,
"workspaceId" bigint NOT NULL,
"name" varchar(255) NOT NULL,
"url" varchar(2048) NOT NULL,
"secret" text,
"events" text NOT NULL,
"active" boolean DEFAULT true NOT NULL,
"createdBy" uuid NOT NULL,
"createdAt" timestamp DEFAULT now() NOT NULL,
"updatedAt" timestamp,
CONSTRAINT "workspace_webhooks_publicId_unique" UNIQUE("publicId")
);
--> statement-breakpoint
ALTER TABLE "workspace_webhooks" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "workspace_webhooks_workspace_idx" ON "workspace_webhooks" USING btree ("workspaceId");--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "workspace_webhooks" ADD CONSTRAINT "workspace_webhooks_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
DO $$ BEGIN
ALTER TABLE "workspace_webhooks" ADD CONSTRAINT "workspace_webhooks_createdBy_user_id_fk" FOREIGN KEY ("createdBy") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -1,46 +0,0 @@
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

@@ -1,6 +0,0 @@
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,27 +176,6 @@
"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
},
{
"idx": 27,
"version": "7",
"when": 1771192000000,
"tag": "20260129210000_AddWorkspaceWebhooks",
"breakpoints": true
}
]
}

View File

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

View File

@@ -420,7 +420,6 @@ export const getWithListAndMembersByPublicId = async (
) => {
const card = await db.query.cards.findFirst({
columns: {
id: true,
publicId: true,
title: true,
description: true,
@@ -508,7 +507,6 @@ export const getWithListAndMembersByPublicId = async (
columns: {
publicId: true,
email: true,
status: true,
},
with: {
user: {
@@ -945,14 +943,12 @@ export const getWorkspaceAndCardIdByCardPublicId = async (
where: and(eq(cards.publicId, cardPublicId), isNull(cards.deletedAt)),
with: {
list: {
columns: { name: true },
columns: {},
with: {
board: {
columns: {
publicId: true,
workspaceId: true,
visibility: true,
name: true,
},
},
},
@@ -966,9 +962,6 @@ export const getWorkspaceAndCardIdByCardPublicId = async (
createdBy: result.createdBy,
workspaceId: result.list.board.workspaceId,
workspaceVisibility: result.list.board.visibility,
listName: result.list.name,
boardPublicId: result.list.board.publicId,
boardName: result.list.board.name,
}
: null;
};

View File

@@ -33,7 +33,6 @@ export const create = async (
fromDueDate?: Date;
toDueDate?: Date;
sourceBoardId?: number;
attachmentId?: number;
},
) => {
const [result] = await db
@@ -59,7 +58,6 @@ export const create = async (
fromDueDate: activityInput.fromDueDate,
toDueDate: activityInput.toDueDate,
sourceBoardId: activityInput.sourceBoardId,
attachmentId: activityInput.attachmentId,
})
.returning({ id: cardActivities.id });
@@ -85,7 +83,6 @@ export const bulkCreate = async (
fromDueDate?: Date;
toDueDate?: Date;
sourceBoardId?: number;
attachmentId?: number;
}[],
) => {
const activitiesWithPublicIds = activityInputs.map((activity) => ({
@@ -194,13 +191,6 @@ 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

@@ -419,14 +419,12 @@ export const getWorkspaceAndListIdByListPublicId = async (
listPublicId: string,
) => {
const result = await db.query.lists.findFirst({
columns: { id: true, name: true, createdBy: true },
columns: { id: true, createdBy: true },
where: and(eq(lists.publicId, listPublicId), isNull(lists.deletedAt)),
with: {
board: {
columns: {
publicId: true,
workspaceId: true,
name: true,
},
},
},
@@ -435,11 +433,8 @@ export const getWorkspaceAndListIdByListPublicId = async (
return result
? {
id: result.id,
name: result.name,
createdBy: result.createdBy,
workspaceId: result.board.workspaceId,
boardPublicId: result.board.publicId,
boardName: result.board.name,
}
: null;
};

View File

@@ -63,36 +63,6 @@ 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

@@ -1,98 +0,0 @@
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

@@ -1,175 +0,0 @@
import { and, eq } from "drizzle-orm";
import type { dbClient } from "@kan/db/client";
import { workspaceWebhooks } from "@kan/db/schema";
import { generateUID } from "@kan/shared/utils";
import type { WebhookEvent } from "../schema/webhooks";
/** Parse JSON-encoded events column into typed array */
function parseEvents(raw: string): WebhookEvent[] {
return JSON.parse(raw) as WebhookEvent[];
}
export const create = async (
db: dbClient,
webhookInput: {
workspaceId: number;
name: string;
url: string;
secret?: string;
events: WebhookEvent[];
createdBy: string;
},
) => {
const [webhook] = await db
.insert(workspaceWebhooks)
.values({
publicId: generateUID(),
workspaceId: webhookInput.workspaceId,
name: webhookInput.name,
url: webhookInput.url,
secret: webhookInput.secret,
events: JSON.stringify(webhookInput.events),
createdBy: webhookInput.createdBy,
})
.returning({
publicId: workspaceWebhooks.publicId,
name: workspaceWebhooks.name,
url: workspaceWebhooks.url,
events: workspaceWebhooks.events,
active: workspaceWebhooks.active,
createdAt: workspaceWebhooks.createdAt,
});
return webhook
? {
...webhook,
events: parseEvents(webhook.events),
}
: null;
};
export const update = async (
db: dbClient,
webhookPublicId: string,
webhookInput: {
name?: string;
url?: string;
secret?: string;
events?: WebhookEvent[];
active?: boolean;
},
) => {
const [result] = await db
.update(workspaceWebhooks)
.set({
name: webhookInput.name,
url: webhookInput.url,
secret: webhookInput.secret,
events: webhookInput.events
? JSON.stringify(webhookInput.events)
: undefined,
active: webhookInput.active,
updatedAt: new Date(),
})
.where(eq(workspaceWebhooks.publicId, webhookPublicId))
.returning({
publicId: workspaceWebhooks.publicId,
name: workspaceWebhooks.name,
url: workspaceWebhooks.url,
events: workspaceWebhooks.events,
active: workspaceWebhooks.active,
createdAt: workspaceWebhooks.createdAt,
updatedAt: workspaceWebhooks.updatedAt,
});
return result
? {
...result,
events: parseEvents(result.events),
}
: null;
};
export const getByPublicId = async (db: dbClient, webhookPublicId: string) => {
const result = await db.query.workspaceWebhooks.findFirst({
columns: {
id: true,
publicId: true,
workspaceId: true,
name: true,
url: true,
secret: true,
events: true,
active: true,
createdAt: true,
updatedAt: true,
},
where: eq(workspaceWebhooks.publicId, webhookPublicId),
});
return result
? {
...result,
events: parseEvents(result.events),
}
: null;
};
export const getAllByWorkspaceId = async (
db: dbClient,
workspaceId: number,
) => {
const results = await db.query.workspaceWebhooks.findMany({
columns: {
publicId: true,
name: true,
url: true,
events: true,
active: true,
createdAt: true,
updatedAt: true,
},
where: eq(workspaceWebhooks.workspaceId, workspaceId),
});
return results.map((webhook) => ({
...webhook,
events: parseEvents(webhook.events),
}));
};
/**
* Returns active webhooks with secrets for server-side delivery (HMAC signing).
* DO NOT expose this via tRPC or any client-facing endpoint.
* Use getAllByWorkspaceId (which omits secrets) for the admin list endpoint.
*/
export const getActiveByWorkspaceId = async (
db: dbClient,
workspaceId: number,
) => {
const results = await db.query.workspaceWebhooks.findMany({
columns: {
publicId: true,
url: true,
secret: true,
events: true,
},
where: and(
eq(workspaceWebhooks.workspaceId, workspaceId),
eq(workspaceWebhooks.active, true),
),
});
return results.map((webhook) => ({
...webhook,
events: parseEvents(webhook.events),
}));
};
export const hardDelete = (db: dbClient, webhookPublicId: string) => {
return db
.delete(workspaceWebhooks)
.where(eq(workspaceWebhooks.publicId, webhookPublicId));
};

View File

@@ -147,10 +147,6 @@ 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 }) => ({
@@ -194,11 +190,6 @@ 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,5 +13,3 @@ export * from "./workspaces";
export * from "./subscriptions";
export * from "./workspaceInviteLinks";
export * from "./permissions";
export * from "./notifications";
export * from "./webhooks";

View File

@@ -1,98 +0,0 @@
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

@@ -1,59 +0,0 @@
import { relations } from "drizzle-orm";
import {
bigint,
bigserial,
boolean,
index,
pgTable,
text,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
import { users } from "./users";
import { workspaces } from "./workspaces";
export const webhookEvents = [
"card.created",
"card.updated",
"card.moved",
"card.deleted",
] as const;
export type WebhookEvent = (typeof webhookEvents)[number];
export const workspaceWebhooks = pgTable("workspace_webhooks", {
id: bigserial("id", { mode: "number" }).primaryKey(),
publicId: varchar("publicId", { length: 12 }).notNull().unique(),
workspaceId: bigint("workspaceId", { mode: "number" })
.notNull()
.references(() => workspaces.id, { onDelete: "cascade" }),
name: varchar("name", { length: 255 }).notNull(),
url: varchar("url", { length: 2048 }).notNull(),
secret: text("secret"),
events: text("events").notNull(), // JSON array of webhook events
active: boolean("active").notNull().default(true),
createdBy: uuid("createdBy")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
}, (table) => [
index("workspace_webhooks_workspace_idx").on(table.workspaceId),
]).enableRLS();
export const workspaceWebhooksRelations = relations(
workspaceWebhooks,
({ one }) => ({
workspace: one(workspaces, {
fields: [workspaceWebhooks.workspaceId],
references: [workspaces.id],
relationName: "workspaceWebhooksWorkspace",
}),
createdByUser: one(users, {
fields: [workspaceWebhooks.createdBy],
references: [users.id],
relationName: "workspaceWebhooksCreatedByUser",
}),
}),
);

View File

@@ -3,16 +3,14 @@ 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" | "MENTION";
type Templates = "MAGIC_LINK" | "JOIN_WORKSPACE" | "RESET_PASSWORD";
const emailTemplates: Record<Templates, React.ComponentType<any>> = {
const emailTemplates: Record<Templates, React.FC> = {
MAGIC_LINK: MagicLinkTemplate,
JOIN_WORKSPACE: JoinWorkspaceTemplate,
RESET_PASSWORD: ResetPasswordTemplate,
MENTION: MentionTemplate,
};
const transporter = nodemailer.createTransport({

View File

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

Some files were not shown because too many files have changed in this diff Show More