v3: performance pass, bundle size pass, i18n pass, docker pass
This commit is contained in:
@@ -25,7 +25,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {}
|
"ghcr.io/warrenbuckley/codespace-features/sqlite:1": {},
|
||||||
|
"ghcr.io/devcontainers-extra/features/ffmpeg-apt-get:1": {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
|||||||
13
Dockerfile
13
Dockerfile
@@ -1,14 +1,15 @@
|
|||||||
# ── Stage 1: build Vite frontend ─────────────────────────────────────────────
|
# ── Stage 1: build Vite frontend ─────────────────────────────────────────────
|
||||||
FROM denoland/deno:2.7.5 AS builder
|
FROM denoland/deno:2.7.11 AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY deno.json deno.lock package.json ./
|
COPY deno.json deno.lock package.json ./
|
||||||
COPY tsconfig*.json vite.config.ts ./
|
COPY tsconfig*.json vite.config.ts lingui.config.ts ./
|
||||||
RUN deno install
|
RUN deno install
|
||||||
|
|
||||||
COPY index.html ./
|
COPY index.html ./
|
||||||
COPY public/ ./public/
|
COPY public/ ./public/
|
||||||
|
COPY scripts/ ./scripts/
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
|
|
||||||
# In same-origin deployments (API serves the frontend), no build args are needed
|
# In same-origin deployments (API serves the frontend), no build args are needed
|
||||||
@@ -24,7 +25,9 @@ ENV VITE_API_PROTOCOL=$VITE_API_PROTOCOL \
|
|||||||
RUN deno task build
|
RUN deno task build
|
||||||
|
|
||||||
# ── Stage 2: runtime ──────────────────────────────────────────────────────────
|
# ── Stage 2: runtime ──────────────────────────────────────────────────────────
|
||||||
FROM denoland/deno:2.7.5
|
FROM denoland/deno:2.7.11
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -35,9 +38,9 @@ COPY api/ ./api/
|
|||||||
COPY --from=builder /app/dist/ ./dist/
|
COPY --from=builder /app/dist/ ./dist/
|
||||||
COPY public/ ./public/
|
COPY public/ ./public/
|
||||||
|
|
||||||
# Persistent data: database and user uploads must be mounted as volumes.
|
# Persistent data: database and uploaded/generated files must be mounted as volumes.
|
||||||
VOLUME ["/app/api/sql", "/app/api/uploads"]
|
VOLUME ["/app/api/sql", "/app/api/uploads"]
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
CMD ["sh", "-c", "deno run -A api/sql/init.ts && exec deno run -A api/main.ts"]
|
CMD ["deno", "task", "start"]
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const DUMPS_DIR = `${UPLOADS_DIR}/dumps`;
|
|||||||
export const AVATARS_DIR = `${UPLOADS_DIR}/avatars`;
|
export const AVATARS_DIR = `${UPLOADS_DIR}/avatars`;
|
||||||
export const PLAYLIST_IMAGES_DIR = `${UPLOADS_DIR}/playlist-images`;
|
export const PLAYLIST_IMAGES_DIR = `${UPLOADS_DIR}/playlist-images`;
|
||||||
export const ATTACHMENTS_DIR = `${UPLOADS_DIR}/attachments`;
|
export const ATTACHMENTS_DIR = `${UPLOADS_DIR}/attachments`;
|
||||||
|
export const THUMBNAILS_DIR = `${UPLOADS_DIR}/thumbnails`;
|
||||||
|
|
||||||
export const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB
|
export const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024; // 5 MB
|
||||||
export const ALLOWED_IMAGE_MIMES = new Set([
|
export const ALLOWED_IMAGE_MIMES = new Set([
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { oakCors } from "@tajpouria/cors";
|
|||||||
import attachmentsRouter from "./routes/attachments.ts";
|
import attachmentsRouter from "./routes/attachments.ts";
|
||||||
import dumpsRouter from "./routes/dumps.ts";
|
import dumpsRouter from "./routes/dumps.ts";
|
||||||
import filesRouter from "./routes/files.ts";
|
import filesRouter from "./routes/files.ts";
|
||||||
|
import thumbnailsRouter from "./routes/thumbnails.ts";
|
||||||
import usersRouter from "./routes/users.ts";
|
import usersRouter from "./routes/users.ts";
|
||||||
import avatarsRouter from "./routes/avatars.ts";
|
import avatarsRouter from "./routes/avatars.ts";
|
||||||
import wsRouter from "./routes/ws.ts";
|
import wsRouter from "./routes/ws.ts";
|
||||||
@@ -44,6 +45,10 @@ app.use(
|
|||||||
filesRouter.routes(),
|
filesRouter.routes(),
|
||||||
filesRouter.allowedMethods(),
|
filesRouter.allowedMethods(),
|
||||||
);
|
);
|
||||||
|
app.use(
|
||||||
|
thumbnailsRouter.routes(),
|
||||||
|
thumbnailsRouter.allowedMethods(),
|
||||||
|
);
|
||||||
app.use(
|
app.use(
|
||||||
attachmentsRouter.routes(),
|
attachmentsRouter.routes(),
|
||||||
attachmentsRouter.allowedMethods(),
|
attachmentsRouter.allowedMethods(),
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ import {
|
|||||||
export const db = new DatabaseSync(DB_PATH);
|
export const db = new DatabaseSync(DB_PATH);
|
||||||
db.exec("PRAGMA foreign_keys = ON;");
|
db.exec("PRAGMA foreign_keys = ON;");
|
||||||
|
|
||||||
|
// Purge expired/used password reset tokens on startup
|
||||||
|
db.prepare(
|
||||||
|
`DELETE FROM password_reset_tokens WHERE expires_at < datetime('now') OR used_at IS NOT NULL;`,
|
||||||
|
).run();
|
||||||
|
|
||||||
// Purge expired unused invites on startup
|
// Purge expired unused invites on startup
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`DELETE FROM invites WHERE used_at IS NULL AND created_at < datetime('now', '-${UNUSED_INVITES_RETENTION_DAYS} days');`,
|
`DELETE FROM invites WHERE used_at IS NULL AND created_at < datetime('now', '-${UNUSED_INVITES_RETENTION_DAYS} days');`,
|
||||||
|
|||||||
110
api/routes/thumbnails.ts
Normal file
110
api/routes/thumbnails.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Router } from "@oak/oak";
|
||||||
|
import { APIErrorCode, APIException } from "../model/interfaces.ts";
|
||||||
|
import { getDump } from "../services/dump-service.ts";
|
||||||
|
import { DUMPS_DIR, THUMBNAILS_DIR } from "../config.ts";
|
||||||
|
|
||||||
|
const router = new Router({ prefix: "/api/thumbnails" });
|
||||||
|
|
||||||
|
async function ffmpegAvailable(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const cmd = new Deno.Command("ffmpeg", {
|
||||||
|
args: ["-version"],
|
||||||
|
stderr: "null",
|
||||||
|
stdout: "null",
|
||||||
|
});
|
||||||
|
const { success } = await cmd.output();
|
||||||
|
return success;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateThumbnail(
|
||||||
|
srcPath: string,
|
||||||
|
outPath: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await Deno.mkdir(THUMBNAILS_DIR, { recursive: true });
|
||||||
|
const cmd = new Deno.Command("ffmpeg", {
|
||||||
|
args: [
|
||||||
|
"-ss",
|
||||||
|
"00:00:01",
|
||||||
|
"-i",
|
||||||
|
srcPath,
|
||||||
|
"-frames:v",
|
||||||
|
"1",
|
||||||
|
"-vf",
|
||||||
|
"scale=320:-1",
|
||||||
|
"-f",
|
||||||
|
"image2",
|
||||||
|
"-y",
|
||||||
|
outPath,
|
||||||
|
],
|
||||||
|
stdout: "null",
|
||||||
|
stderr: "null",
|
||||||
|
});
|
||||||
|
const { success } = await cmd.output();
|
||||||
|
if (!success) throw new Error("ffmpeg failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get("/:dumpId", async (ctx) => {
|
||||||
|
const { dumpId } = ctx.params;
|
||||||
|
|
||||||
|
if (!/^[0-9a-f-]{36}$/.test(dumpId)) {
|
||||||
|
throw new APIException(APIErrorCode.BAD_REQUEST, 400, "Invalid dump ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const dump = getDump(dumpId);
|
||||||
|
|
||||||
|
if (dump.kind !== "file" || !dump.fileMime?.startsWith("video/")) {
|
||||||
|
throw new APIException(
|
||||||
|
APIErrorCode.NOT_FOUND,
|
||||||
|
404,
|
||||||
|
"No video for this dump",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbPath = `${THUMBNAILS_DIR}/${dumpId}.jpg`;
|
||||||
|
|
||||||
|
// Serve cached thumbnail if it exists
|
||||||
|
try {
|
||||||
|
const data = await Deno.readFile(thumbPath);
|
||||||
|
ctx.response.headers.set("Content-Type", "image/jpeg");
|
||||||
|
ctx.response.headers.set(
|
||||||
|
"Cache-Control",
|
||||||
|
"public, max-age=31536000, immutable",
|
||||||
|
);
|
||||||
|
ctx.response.body = data;
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// Not cached yet — generate it
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await ffmpegAvailable()) {
|
||||||
|
throw new APIException(
|
||||||
|
APIErrorCode.NOT_FOUND,
|
||||||
|
404,
|
||||||
|
"Thumbnail generation unavailable",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const srcPath = `${DUMPS_DIR}/${dumpId}`;
|
||||||
|
try {
|
||||||
|
await generateThumbnail(srcPath, thumbPath);
|
||||||
|
} catch {
|
||||||
|
throw new APIException(
|
||||||
|
APIErrorCode.NOT_FOUND,
|
||||||
|
404,
|
||||||
|
"Could not generate thumbnail",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await Deno.readFile(thumbPath);
|
||||||
|
ctx.response.headers.set("Content-Type", "image/jpeg");
|
||||||
|
ctx.response.headers.set(
|
||||||
|
"Cache-Control",
|
||||||
|
"public, max-age=31536000, immutable",
|
||||||
|
);
|
||||||
|
ctx.response.body = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"dev": "deno run --env-file -A npm:vite & deno run -A server:start",
|
"dev": "deno run --env-file -A npm:vite & deno task server:start",
|
||||||
"build": "deno run --env-file -A npm:vite build",
|
"build": "deno task i18n:extract && deno task i18n:compile && deno run --env-file -A npm:vite build",
|
||||||
"server:start": "deno run --env-file -A --watch api/main.ts",
|
"server:start": "deno run --env-file -A --watch api/main.ts",
|
||||||
"serve": "deno task build && deno run -A server:start",
|
"serve": "deno task build && deno task server:start",
|
||||||
"i18n:extract": "deno run -A scripts/lingui-extract.ts",
|
"i18n:extract": "deno run -A scripts/lingui-extract.ts",
|
||||||
"i18n:compile": "deno run -A scripts/lingui-compile.ts"
|
"i18n:compile": "deno run -A scripts/lingui-compile.ts",
|
||||||
|
"start": "deno run -A api/sql/init.ts && deno run -A api/main.ts"
|
||||||
},
|
},
|
||||||
"nodeModulesDir": "auto",
|
"nodeModulesDir": "auto",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
|||||||
161
deno.lock
generated
161
deno.lock
generated
@@ -22,22 +22,23 @@
|
|||||||
"jsr:@tajpouria/cors@^1.2.1": "1.2.1",
|
"jsr:@tajpouria/cors@^1.2.1": "1.2.1",
|
||||||
"npm:@eslint/js@^9.39.4": "9.39.4",
|
"npm:@eslint/js@^9.39.4": "9.39.4",
|
||||||
"npm:@lingui/cli@*": "5.9.4_typescript@5.9.3",
|
"npm:@lingui/cli@*": "5.9.4_typescript@5.9.3",
|
||||||
"npm:@lingui/cli@6.0.0-next.2": "6.0.0-next.2",
|
"npm:@lingui/cli@6.0.0-next.3": "6.0.0-next.3",
|
||||||
"npm:@lingui/conf@6.0.0-next.2": "6.0.0-next.2",
|
"npm:@lingui/conf@6.0.0-next.3": "6.0.0-next.3",
|
||||||
"npm:@lingui/core@6.0.0-next.2": "6.0.0-next.2",
|
"npm:@lingui/core@6.0.0-next.3": "6.0.0-next.3",
|
||||||
"npm:@lingui/format-po@6.0.0-next.2": "6.0.0-next.2",
|
"npm:@lingui/format-po@6.0.0-next.3": "6.0.0-next.3",
|
||||||
"npm:@lingui/react@6.0.0-next.2": "6.0.0-next.2_react@19.2.4",
|
"npm:@lingui/react@6.0.0-next.3": "6.0.0-next.3_react@19.2.4",
|
||||||
"npm:@lingui/swc-plugin@6.0.0-next.2": "6.0.0-next.2_@lingui+core@6.0.0-next.2",
|
"npm:@lingui/swc-plugin@6.0.0-next.2": "6.0.0-next.2_@lingui+core@6.0.0-next.3",
|
||||||
"npm:@lingui/vite-plugin@6.0.0-next.2": "6.0.0-next.2_vite@8.0.1__@types+node@24.12.0",
|
"npm:@lingui/vite-plugin@6.0.0-next.3": "6.0.0-next.3_vite@8.0.1__@types+node@24.12.0__jiti@2.6.1_jiti@2.6.1",
|
||||||
"npm:@types/node@^24.12.0": "24.12.0",
|
"npm:@types/node@^24.12.0": "24.12.0",
|
||||||
"npm:@types/react-dom@^19.2.3": "19.2.3_@types+react@19.2.14",
|
"npm:@types/react-dom@^19.2.3": "19.2.3_@types+react@19.2.14",
|
||||||
"npm:@types/react@^19.2.14": "19.2.14",
|
"npm:@types/react@^19.2.14": "19.2.14",
|
||||||
"npm:@vitejs/plugin-react-swc@^4.3.0": "4.3.0_vite@8.0.1__@types+node@24.12.0",
|
"npm:@vitejs/plugin-react-swc@^4.3.0": "4.3.0_vite@8.0.1__@types+node@24.12.0__jiti@2.6.1_jiti@2.6.1",
|
||||||
"npm:eslint-plugin-react-hooks@^7.0.1": "7.0.1_eslint@9.39.4",
|
"npm:eslint-plugin-react-hooks@^7.0.1": "7.0.1_eslint@9.39.4__jiti@2.6.1_jiti@2.6.1",
|
||||||
"npm:eslint-plugin-react-refresh@~0.5.2": "0.5.2_eslint@9.39.4",
|
"npm:eslint-plugin-react-refresh@~0.5.2": "0.5.2_eslint@9.39.4__jiti@2.6.1_jiti@2.6.1",
|
||||||
"npm:eslint@^9.39.4": "9.39.4",
|
"npm:eslint@^9.39.4": "9.39.4_jiti@2.6.1",
|
||||||
"npm:frimousse@0.3": "0.3.0_react@19.2.4_typescript@5.9.3",
|
"npm:frimousse@0.3": "0.3.0_react@19.2.4_typescript@5.9.3",
|
||||||
"npm:globals@^17.4.0": "17.4.0",
|
"npm:globals@^17.4.0": "17.4.0",
|
||||||
|
"npm:jiti@^2.6.1": "2.6.1",
|
||||||
"npm:marked@15": "15.0.12",
|
"npm:marked@15": "15.0.12",
|
||||||
"npm:nodemailer@*": "8.0.4",
|
"npm:nodemailer@*": "8.0.4",
|
||||||
"npm:nodemailer@^8.0.4": "8.0.4",
|
"npm:nodemailer@^8.0.4": "8.0.4",
|
||||||
@@ -47,10 +48,10 @@
|
|||||||
"npm:react-router@^7.13.1": "7.13.1_react@19.2.4_react-dom@19.2.4__react@19.2.4",
|
"npm:react-router@^7.13.1": "7.13.1_react@19.2.4_react-dom@19.2.4__react@19.2.4",
|
||||||
"npm:react@^19.2.4": "19.2.4",
|
"npm:react@^19.2.4": "19.2.4",
|
||||||
"npm:remark-gfm@^4.0.1": "4.0.1",
|
"npm:remark-gfm@^4.0.1": "4.0.1",
|
||||||
"npm:typescript-eslint@^8.56.1": "8.57.1_eslint@9.39.4_typescript@5.9.3",
|
"npm:typescript-eslint@^8.56.1": "8.57.1_eslint@9.39.4__jiti@2.6.1_typescript@5.9.3_jiti@2.6.1",
|
||||||
"npm:typescript@~5.9.3": "5.9.3",
|
"npm:typescript@~5.9.3": "5.9.3",
|
||||||
"npm:vite@*": "8.0.1_@types+node@24.12.0",
|
"npm:vite@*": "8.0.1_@types+node@24.12.0_jiti@2.6.1",
|
||||||
"npm:vite@8": "8.0.1_@types+node@24.12.0"
|
"npm:vite@8": "8.0.1_@types+node@24.12.0_jiti@2.6.1"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
"@db/sqlite@0.13.0": {
|
"@db/sqlite@0.13.0": {
|
||||||
@@ -418,7 +419,7 @@
|
|||||||
"os": ["win32"],
|
"os": ["win32"],
|
||||||
"cpu": ["x64"]
|
"cpu": ["x64"]
|
||||||
},
|
},
|
||||||
"@eslint-community/eslint-utils@4.9.1_eslint@9.39.4": {
|
"@eslint-community/eslint-utils@4.9.1_eslint@9.39.4__jiti@2.6.1_jiti@2.6.1": {
|
||||||
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"eslint",
|
"eslint",
|
||||||
@@ -541,8 +542,8 @@
|
|||||||
"@lingui/babel-plugin-extract-messages@5.9.4": {
|
"@lingui/babel-plugin-extract-messages@5.9.4": {
|
||||||
"integrity": "sha512-sFH5lufIBCOLwjM2hyByMIi7gaGjAPhU7md8XMQYgcEjUVtzjBQvZ9APGDdDQ5BB8xRDyqF2kvaJpJvWZu19zA=="
|
"integrity": "sha512-sFH5lufIBCOLwjM2hyByMIi7gaGjAPhU7md8XMQYgcEjUVtzjBQvZ9APGDdDQ5BB8xRDyqF2kvaJpJvWZu19zA=="
|
||||||
},
|
},
|
||||||
"@lingui/babel-plugin-extract-messages@6.0.0-next.2": {
|
"@lingui/babel-plugin-extract-messages@6.0.0-next.3": {
|
||||||
"integrity": "sha512-lefaO/jHaJabVODRZwBIIXrwFV01kQvsSDBuEQfA0BhNXhYW61iFot//4dsGQ3FBiKXiskjQbcSFHdaBNT4YBw=="
|
"integrity": "sha512-6lr2C5NjGSdnzY5qsWlfteBN4CtN4OlfMB4JceS0+mfatVTGd+koaFAE4rLB6xGO79EOoI3c2PqYHGiED8Vdtw=="
|
||||||
},
|
},
|
||||||
"@lingui/babel-plugin-lingui-macro@5.9.4_typescript@5.9.3": {
|
"@lingui/babel-plugin-lingui-macro@5.9.4_typescript@5.9.3": {
|
||||||
"integrity": "sha512-Gj+H48MQWY6rV40TBVG7U91/KETznbXOJpJsf8U4merBRPZgOMCy6VuWZGy1i+YJZJF/LiberlsCCEiiPbBRqg==",
|
"integrity": "sha512-Gj+H48MQWY6rV40TBVG7U91/KETznbXOJpJsf8U4merBRPZgOMCy6VuWZGy1i+YJZJF/LiberlsCCEiiPbBRqg==",
|
||||||
@@ -555,13 +556,13 @@
|
|||||||
"@lingui/message-utils@5.9.4"
|
"@lingui/message-utils@5.9.4"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@lingui/babel-plugin-lingui-macro@6.0.0-next.2": {
|
"@lingui/babel-plugin-lingui-macro@6.0.0-next.3": {
|
||||||
"integrity": "sha512-aWrHmo6pGEBH6vO8/AClkIQ6hSyzwROz+sO1gTBIJnwqj81VgSzyTvbCWpYW5fXCrB8yQiPbyxqD40kSsaJnzQ==",
|
"integrity": "sha512-vGXSKF4HHuCQCIIk0hc5sgHD+u7mhAZmksMTmT52ghks2lrLvD+DFUwnRCxMVaJoEilQu3TMXIVS4wwzf3hB1g==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@babel/core",
|
"@babel/core",
|
||||||
"@babel/types",
|
"@babel/types",
|
||||||
"@lingui/conf@6.0.0-next.2",
|
"@lingui/conf@6.0.0-next.3",
|
||||||
"@lingui/message-utils@6.0.0-next.2"
|
"@lingui/message-utils@6.0.0-next.3"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@lingui/cli@5.9.4_typescript@5.9.3": {
|
"@lingui/cli@5.9.4_typescript@5.9.3": {
|
||||||
@@ -597,19 +598,19 @@
|
|||||||
],
|
],
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
"@lingui/cli@6.0.0-next.2": {
|
"@lingui/cli@6.0.0-next.3": {
|
||||||
"integrity": "sha512-nZj7EaLUC4YJqgR8GJDOq1hGjwxuDoVGRd6C3qXlJY/yaPDY333CIW9VXFm2RSGozFlnppTOp48ESZo1k0mRZA==",
|
"integrity": "sha512-HbVS5nkqR3/GPZyDAp3d2Km/YdQE9YIn5iKAy/WDOMpVMyR34Mi+O3uFyFoMugnB17qVqjmoAUVVxFlTc6CIKw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@babel/core",
|
"@babel/core",
|
||||||
"@babel/generator",
|
"@babel/generator",
|
||||||
"@babel/parser",
|
"@babel/parser",
|
||||||
"@babel/types",
|
"@babel/types",
|
||||||
"@lingui/babel-plugin-extract-messages@6.0.0-next.2",
|
"@lingui/babel-plugin-extract-messages@6.0.0-next.3",
|
||||||
"@lingui/babel-plugin-lingui-macro@6.0.0-next.2",
|
"@lingui/babel-plugin-lingui-macro@6.0.0-next.3",
|
||||||
"@lingui/conf@6.0.0-next.2",
|
"@lingui/conf@6.0.0-next.3",
|
||||||
"@lingui/core@6.0.0-next.2",
|
"@lingui/core@6.0.0-next.3",
|
||||||
"@lingui/format-po@6.0.0-next.2",
|
"@lingui/format-po@6.0.0-next.3",
|
||||||
"@lingui/message-utils@6.0.0-next.2",
|
"@lingui/message-utils@6.0.0-next.3",
|
||||||
"chokidar@5.0.0",
|
"chokidar@5.0.0",
|
||||||
"cli-table3",
|
"cli-table3",
|
||||||
"commander@14.0.3",
|
"commander@14.0.3",
|
||||||
@@ -635,8 +636,8 @@
|
|||||||
"picocolors"
|
"picocolors"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@lingui/conf@6.0.0-next.2": {
|
"@lingui/conf@6.0.0-next.3": {
|
||||||
"integrity": "sha512-DZGvd0yVOP6bhG+wZei0nPIHiyO3LlgG/u/DFFyeCPT+hr4j9hFRH6efsN5hRgCZ4qf8sxiKWwvOAhfLIcbKsQ==",
|
"integrity": "sha512-GZjVN4sP+WOp9+SRdjvYPn7kOXe2jBUNNETYFxN7hb7pEXS1TmehgruNjDr/td4TRDKBxXLI+OaJMlZWaDQE8g==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jest-validate",
|
"jest-validate",
|
||||||
"jiti",
|
"jiti",
|
||||||
@@ -655,11 +656,11 @@
|
|||||||
"@lingui/babel-plugin-lingui-macro@5.9.4_typescript@5.9.3"
|
"@lingui/babel-plugin-lingui-macro@5.9.4_typescript@5.9.3"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@lingui/core@6.0.0-next.2": {
|
"@lingui/core@6.0.0-next.3": {
|
||||||
"integrity": "sha512-JuGvuIlfW4HyCK2cvagqN9o5AG9TwJQ9wP36scxtobufFC0wNCxh7lC5sfsYz+KJwY62duCdYEiXfbMhBvmW1Q==",
|
"integrity": "sha512-ugIBl1cwxlHg7tY98qT9h1M5v6CurDBre/Vjzfv3F2RXC4J7iB4LGJ2QSbVIYAQVIK/H0pgrv1Lh1L8IwgEPZQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@lingui/babel-plugin-lingui-macro@6.0.0-next.2",
|
"@lingui/babel-plugin-lingui-macro@6.0.0-next.3",
|
||||||
"@lingui/message-utils@6.0.0-next.2"
|
"@lingui/message-utils@6.0.0-next.3"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@lingui/format-po@5.9.4_typescript@5.9.3": {
|
"@lingui/format-po@5.9.4_typescript@5.9.3": {
|
||||||
@@ -671,11 +672,11 @@
|
|||||||
"pofile"
|
"pofile"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@lingui/format-po@6.0.0-next.2": {
|
"@lingui/format-po@6.0.0-next.3": {
|
||||||
"integrity": "sha512-vlQCy0tFYKGLT2VS8rlYjh36CBAcQAdzezo6IphtIeg5E3jQYWukrVsAVKqurUHe3/kKVNyyWEMODeH84pIJiA==",
|
"integrity": "sha512-fFtNFzVaSAefPIJHuoCUSTXy/up9OfJTed3gJm9CgpovxaGVBQfuDQtS2lSghaWiLtGo9UqdkROPrSzYdTqulQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@lingui/conf@6.0.0-next.2",
|
"@lingui/conf@6.0.0-next.3",
|
||||||
"@lingui/message-utils@6.0.0-next.2",
|
"@lingui/message-utils@6.0.0-next.3",
|
||||||
"pofile"
|
"pofile"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -686,33 +687,33 @@
|
|||||||
"js-sha256"
|
"js-sha256"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@lingui/message-utils@6.0.0-next.2": {
|
"@lingui/message-utils@6.0.0-next.3": {
|
||||||
"integrity": "sha512-3vI2La2XLD/mrsdkghMmuuHuCS9+pvGXOKofCNjDqiayvCPV9q5YeOGa3jb+frOLfIuJ/IiHplNO4wIq3y2kuw==",
|
"integrity": "sha512-oXTGhXyFoZua0ahVNDwhs7/3tXVAL8NPz7SOTRYORyxDREQwHYTqYXHijHRGquBvL5QEHy28uYSQvy5Szojn8A==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@messageformat/date-skeleton",
|
"@messageformat/date-skeleton",
|
||||||
"@messageformat/parser",
|
"@messageformat/parser",
|
||||||
"js-sha256"
|
"js-sha256"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@lingui/react@6.0.0-next.2_react@19.2.4": {
|
"@lingui/react@6.0.0-next.3_react@19.2.4": {
|
||||||
"integrity": "sha512-EdzP+8a2UDLwzWPDG2+ctoFrU4oAyWFvijimTm6m6o39s/WHci+yH/eD7tNFr+bOUaynwoTI7azxfJJzbaDZNQ==",
|
"integrity": "sha512-tSu+Y8Xsi5oHdjXZaa5o76B4UHdVmrLdQUVDxgMXx+E4s/D30cTkuVtqoKr3kdY/StiEU7aKb08qUdfE52CWyA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@lingui/babel-plugin-lingui-macro@6.0.0-next.2",
|
"@lingui/babel-plugin-lingui-macro@6.0.0-next.3",
|
||||||
"@lingui/core@6.0.0-next.2",
|
"@lingui/core@6.0.0-next.3",
|
||||||
"react"
|
"react"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@lingui/swc-plugin@6.0.0-next.2_@lingui+core@6.0.0-next.2": {
|
"@lingui/swc-plugin@6.0.0-next.2_@lingui+core@6.0.0-next.3": {
|
||||||
"integrity": "sha512-rD20Y7gpquPUw7e/V33lOcsTa28kuuP2GpIK74iQ46KFWm067DKNUkGfcfeEHG6ORBJck6W+KvqeOZ/B4vID1Q==",
|
"integrity": "sha512-rD20Y7gpquPUw7e/V33lOcsTa28kuuP2GpIK74iQ46KFWm067DKNUkGfcfeEHG6ORBJck6W+KvqeOZ/B4vID1Q==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@lingui/core@6.0.0-next.2"
|
"@lingui/core@6.0.0-next.3"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@lingui/vite-plugin@6.0.0-next.2_vite@8.0.1__@types+node@24.12.0": {
|
"@lingui/vite-plugin@6.0.0-next.3_vite@8.0.1__@types+node@24.12.0__jiti@2.6.1_jiti@2.6.1": {
|
||||||
"integrity": "sha512-dK+imM95tZ7LoJoh8VOp7P20cjgXoC2oWx9dW7zoYz40jZsvIssRYQ4a4jiYM4UNp+fMdaAo5t0GDbqIJ9EZGw==",
|
"integrity": "sha512-Ewmg3jcBTp8VEfqkgsr6/YJJ7cphLtLSW+RGkCJREko6pa/a4V3U7hA/xKo2Z2Q1u95E8ngfWJO3qDXLGDb+DA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@lingui/cli@6.0.0-next.2",
|
"@lingui/cli@6.0.0-next.3",
|
||||||
"@lingui/conf@6.0.0-next.2",
|
"@lingui/conf@6.0.0-next.3",
|
||||||
"vite"
|
"vite"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1000,7 +1001,7 @@
|
|||||||
"@types/yargs-parser"
|
"@types/yargs-parser"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/eslint-plugin@8.57.1_@typescript-eslint+parser@8.57.1__eslint@9.39.4__typescript@5.9.3_eslint@9.39.4_typescript@5.9.3": {
|
"@typescript-eslint/eslint-plugin@8.57.1_@typescript-eslint+parser@8.57.1__eslint@9.39.4___jiti@2.6.1__typescript@5.9.3__jiti@2.6.1_eslint@9.39.4__jiti@2.6.1_typescript@5.9.3_jiti@2.6.1": {
|
||||||
"integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==",
|
"integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@eslint-community/regexpp",
|
"@eslint-community/regexpp",
|
||||||
@@ -1016,7 +1017,7 @@
|
|||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/parser@8.57.1_eslint@9.39.4_typescript@5.9.3": {
|
"@typescript-eslint/parser@8.57.1_eslint@9.39.4__jiti@2.6.1_typescript@5.9.3_jiti@2.6.1": {
|
||||||
"integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==",
|
"integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@typescript-eslint/scope-manager",
|
"@typescript-eslint/scope-manager",
|
||||||
@@ -1050,7 +1051,7 @@
|
|||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/type-utils@8.57.1_eslint@9.39.4_typescript@5.9.3": {
|
"@typescript-eslint/type-utils@8.57.1_eslint@9.39.4__jiti@2.6.1_typescript@5.9.3_jiti@2.6.1": {
|
||||||
"integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==",
|
"integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@typescript-eslint/types",
|
"@typescript-eslint/types",
|
||||||
@@ -1080,7 +1081,7 @@
|
|||||||
"typescript"
|
"typescript"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@typescript-eslint/utils@8.57.1_eslint@9.39.4_typescript@5.9.3": {
|
"@typescript-eslint/utils@8.57.1_eslint@9.39.4__jiti@2.6.1_typescript@5.9.3_jiti@2.6.1": {
|
||||||
"integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==",
|
"integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@eslint-community/eslint-utils",
|
"@eslint-community/eslint-utils",
|
||||||
@@ -1101,7 +1102,7 @@
|
|||||||
"@ungap/structured-clone@1.3.0": {
|
"@ungap/structured-clone@1.3.0": {
|
||||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="
|
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="
|
||||||
},
|
},
|
||||||
"@vitejs/plugin-react-swc@4.3.0_vite@8.0.1__@types+node@24.12.0": {
|
"@vitejs/plugin-react-swc@4.3.0_vite@8.0.1__@types+node@24.12.0__jiti@2.6.1_jiti@2.6.1": {
|
||||||
"integrity": "sha512-mOkXCII839dHyAt/gpoSlm28JIVDwhZ6tnG6wJxUy2bmOx7UaPjvOyIDf3SFv5s7Eo7HVaq6kRcu6YMEzt5Z7w==",
|
"integrity": "sha512-mOkXCII839dHyAt/gpoSlm28JIVDwhZ6tnG6wJxUy2bmOx7UaPjvOyIDf3SFv5s7Eo7HVaq6kRcu6YMEzt5Z7w==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@rolldown/pluginutils@1.0.0-rc.7",
|
"@rolldown/pluginutils@1.0.0-rc.7",
|
||||||
@@ -1165,8 +1166,8 @@
|
|||||||
"base64-js@1.5.1": {
|
"base64-js@1.5.1": {
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||||
},
|
},
|
||||||
"baseline-browser-mapping@2.10.10": {
|
"baseline-browser-mapping@2.10.16": {
|
||||||
"integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==",
|
"integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
"binary-extensions@2.3.0": {
|
"binary-extensions@2.3.0": {
|
||||||
@@ -1199,8 +1200,8 @@
|
|||||||
"fill-range"
|
"fill-range"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"browserslist@4.28.1": {
|
"browserslist@4.28.2": {
|
||||||
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
|
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"baseline-browser-mapping",
|
"baseline-browser-mapping",
|
||||||
"caniuse-lite",
|
"caniuse-lite",
|
||||||
@@ -1223,8 +1224,8 @@
|
|||||||
"camelcase@6.3.0": {
|
"camelcase@6.3.0": {
|
||||||
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="
|
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="
|
||||||
},
|
},
|
||||||
"caniuse-lite@1.0.30001780": {
|
"caniuse-lite@1.0.30001786": {
|
||||||
"integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ=="
|
"integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA=="
|
||||||
},
|
},
|
||||||
"ccount@2.0.1": {
|
"ccount@2.0.1": {
|
||||||
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="
|
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="
|
||||||
@@ -1398,8 +1399,8 @@
|
|||||||
"dequal"
|
"dequal"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"electron-to-chromium@1.5.321": {
|
"electron-to-chromium@1.5.332": {
|
||||||
"integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ=="
|
"integrity": "sha512-7OOtytmh/rINMLwaFTbcMVvYXO3AUm029X0LcyfYk0B557RlPkdpTpnH9+htMlfu5dKwOmT0+Zs2Aw+lnn6TeQ=="
|
||||||
},
|
},
|
||||||
"emoji-regex@8.0.0": {
|
"emoji-regex@8.0.0": {
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
@@ -1452,7 +1453,7 @@
|
|||||||
"escape-string-regexp@5.0.0": {
|
"escape-string-regexp@5.0.0": {
|
||||||
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="
|
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="
|
||||||
},
|
},
|
||||||
"eslint-plugin-react-hooks@7.0.1_eslint@9.39.4": {
|
"eslint-plugin-react-hooks@7.0.1_eslint@9.39.4__jiti@2.6.1_jiti@2.6.1": {
|
||||||
"integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
|
"integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@babel/core",
|
"@babel/core",
|
||||||
@@ -1463,7 +1464,7 @@
|
|||||||
"zod-validation-error"
|
"zod-validation-error"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"eslint-plugin-react-refresh@0.5.2_eslint@9.39.4": {
|
"eslint-plugin-react-refresh@0.5.2_eslint@9.39.4__jiti@2.6.1_jiti@2.6.1": {
|
||||||
"integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
|
"integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"eslint"
|
"eslint"
|
||||||
@@ -1485,7 +1486,7 @@
|
|||||||
"eslint-visitor-keys@5.0.1": {
|
"eslint-visitor-keys@5.0.1": {
|
||||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="
|
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="
|
||||||
},
|
},
|
||||||
"eslint@9.39.4": {
|
"eslint@9.39.4_jiti@2.6.1": {
|
||||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@eslint-community/eslint-utils",
|
"@eslint-community/eslint-utils",
|
||||||
@@ -1517,12 +1518,16 @@
|
|||||||
"ignore@5.3.2",
|
"ignore@5.3.2",
|
||||||
"imurmurhash",
|
"imurmurhash",
|
||||||
"is-glob",
|
"is-glob",
|
||||||
|
"jiti",
|
||||||
"json-stable-stringify-without-jsonify",
|
"json-stable-stringify-without-jsonify",
|
||||||
"lodash.merge",
|
"lodash.merge",
|
||||||
"minimatch@3.1.5",
|
"minimatch@3.1.5",
|
||||||
"natural-compare",
|
"natural-compare",
|
||||||
"optionator"
|
"optionator"
|
||||||
],
|
],
|
||||||
|
"optionalPeers": [
|
||||||
|
"jiti"
|
||||||
|
],
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
"esm@3.2.25": {
|
"esm@3.2.25": {
|
||||||
@@ -2902,7 +2907,7 @@
|
|||||||
"prelude-ls"
|
"prelude-ls"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"typescript-eslint@8.57.1_eslint@9.39.4_typescript@5.9.3": {
|
"typescript-eslint@8.57.1_eslint@9.39.4__jiti@2.6.1_typescript@5.9.3_jiti@2.6.1": {
|
||||||
"integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==",
|
"integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@typescript-eslint/eslint-plugin",
|
"@typescript-eslint/eslint-plugin",
|
||||||
@@ -2965,7 +2970,7 @@
|
|||||||
"unist-util-visit-parents"
|
"unist-util-visit-parents"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"update-browserslist-db@1.2.3_browserslist@4.28.1": {
|
"update-browserslist-db@1.2.3_browserslist@4.28.2": {
|
||||||
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"browserslist",
|
"browserslist",
|
||||||
@@ -2997,10 +3002,11 @@
|
|||||||
"vfile-message"
|
"vfile-message"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"vite@8.0.1_@types+node@24.12.0": {
|
"vite@8.0.1_@types+node@24.12.0_jiti@2.6.1": {
|
||||||
"integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==",
|
"integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/node",
|
"@types/node",
|
||||||
|
"jiti",
|
||||||
"lightningcss",
|
"lightningcss",
|
||||||
"picomatch@4.0.3",
|
"picomatch@4.0.3",
|
||||||
"postcss",
|
"postcss",
|
||||||
@@ -3011,7 +3017,8 @@
|
|||||||
"fsevents"
|
"fsevents"
|
||||||
],
|
],
|
||||||
"optionalPeers": [
|
"optionalPeers": [
|
||||||
"@types/node"
|
"@types/node",
|
||||||
|
"jiti"
|
||||||
],
|
],
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
@@ -3065,12 +3072,13 @@
|
|||||||
"packageJson": {
|
"packageJson": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"npm:@eslint/js@^9.39.4",
|
"npm:@eslint/js@^9.39.4",
|
||||||
"npm:@lingui/cli@6.0.0-next.2",
|
"npm:@lingui/cli@6.0.0-next.3",
|
||||||
"npm:@lingui/core@6.0.0-next.2",
|
"npm:@lingui/conf@6.0.0-next.3",
|
||||||
"npm:@lingui/format-po@6.0.0-next.2",
|
"npm:@lingui/core@6.0.0-next.3",
|
||||||
"npm:@lingui/react@6.0.0-next.2",
|
"npm:@lingui/format-po@6.0.0-next.3",
|
||||||
|
"npm:@lingui/react@6.0.0-next.3",
|
||||||
"npm:@lingui/swc-plugin@6.0.0-next.2",
|
"npm:@lingui/swc-plugin@6.0.0-next.2",
|
||||||
"npm:@lingui/vite-plugin@6.0.0-next.2",
|
"npm:@lingui/vite-plugin@6.0.0-next.3",
|
||||||
"npm:@types/node@^24.12.0",
|
"npm:@types/node@^24.12.0",
|
||||||
"npm:@types/react-dom@^19.2.3",
|
"npm:@types/react-dom@^19.2.3",
|
||||||
"npm:@types/react@^19.2.14",
|
"npm:@types/react@^19.2.14",
|
||||||
@@ -3080,6 +3088,7 @@
|
|||||||
"npm:eslint@^9.39.4",
|
"npm:eslint@^9.39.4",
|
||||||
"npm:frimousse@0.3",
|
"npm:frimousse@0.3",
|
||||||
"npm:globals@^17.4.0",
|
"npm:globals@^17.4.0",
|
||||||
|
"npm:jiti@^2.6.1",
|
||||||
"npm:react-dom@^19.2.4",
|
"npm:react-dom@^19.2.4",
|
||||||
"npm:react-markdown@^10.1.0",
|
"npm:react-markdown@^10.1.0",
|
||||||
"npm:react-router@^7.13.1",
|
"npm:react-router@^7.13.1",
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import js from '@eslint/js'
|
|
||||||
import globals from 'globals'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
import tseslint from 'typescript-eslint'
|
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
globalIgnores(['dist', 'src/locales']),
|
|
||||||
{
|
|
||||||
files: ['**/*.{ts,tsx}'],
|
|
||||||
extends: [
|
|
||||||
js.configs.recommended,
|
|
||||||
tseslint.configs.recommended,
|
|
||||||
reactHooks.configs.flat.recommended,
|
|
||||||
reactRefresh.configs.vite,
|
|
||||||
],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
globals: globals.browser,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-unused-vars': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
argsIgnorePattern: '^_',
|
|
||||||
varsIgnorePattern: '^_',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
32
eslint.config.ts
Normal file
32
eslint.config.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import globals from "globals";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import reactRefresh from "eslint-plugin-react-refresh";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(["dist", "src/locales"]),
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
const { formatter } = require("@lingui/format-po");
|
import { formatter } from "@lingui/format-po";
|
||||||
|
import type { LinguiConfig } from "@lingui/conf";
|
||||||
|
|
||||||
/** @type {import("@lingui/conf").LinguiConfig} */
|
const config: LinguiConfig = {
|
||||||
module.exports = {
|
|
||||||
locales: ["en", "fr"],
|
locales: ["en", "fr"],
|
||||||
sourceLocale: "en",
|
sourceLocale: "en",
|
||||||
catalogs: [
|
catalogs: [
|
||||||
@@ -12,3 +12,5 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
format: formatter(),
|
format: formatter(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
12
package.json
12
package.json
@@ -10,14 +10,16 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lingui/cli": "6.0.0-next.2",
|
"@lingui/cli": "6.0.0-next.3",
|
||||||
"@lingui/core": "6.0.0-next.2",
|
"@lingui/conf": "6.0.0-next.3",
|
||||||
"@lingui/format-po": "6.0.0-next.2",
|
"@lingui/core": "6.0.0-next.3",
|
||||||
"@lingui/react": "6.0.0-next.2",
|
"@lingui/format-po": "6.0.0-next.3",
|
||||||
|
"@lingui/react": "6.0.0-next.3",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@lingui/swc-plugin": "6.0.0-next.2",
|
"@lingui/swc-plugin": "6.0.0-next.2",
|
||||||
"@vitejs/plugin-react-swc": "^4.3.0",
|
"@vitejs/plugin-react-swc": "^4.3.0",
|
||||||
"frimousse": "^0.3.0",
|
"frimousse": "^0.3.0",
|
||||||
|
"jiti": "^2.6.1",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
"remark-gfm": "^4.0.1"
|
"remark-gfm": "^4.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lingui/vite-plugin": "6.0.0-next.2",
|
"@lingui/vite-plugin": "6.0.0-next.3",
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
"@types/node": "^24.12.0",
|
"@types/node": "^24.12.0",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
|||||||
@@ -1807,6 +1807,10 @@ body.has-player .fab-new {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-header--has-center .app-header-nav {
|
||||||
|
grid-column: 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-header-brand {
|
.app-header-brand {
|
||||||
|
|||||||
158
src/App.tsx
158
src/App.tsx
@@ -1,20 +1,8 @@
|
|||||||
|
import { lazy, Suspense } from "react";
|
||||||
import { BrowserRouter, Route, Routes } from "react-router";
|
import { BrowserRouter, Route, Routes } from "react-router";
|
||||||
|
|
||||||
import { Index } from "./pages/Index.tsx";
|
|
||||||
import { RestrictedGuest } from "./pages/RestrictedGuest.tsx";
|
import { RestrictedGuest } from "./pages/RestrictedGuest.tsx";
|
||||||
import { RestrictedLoggedIn } from "./pages/RestrictedLoggedIn.tsx";
|
import { RestrictedLoggedIn } from "./pages/RestrictedLoggedIn.tsx";
|
||||||
import { Dump } from "./pages/Dump.tsx";
|
|
||||||
import { DumpEdit } from "./pages/DumpEdit.tsx";
|
|
||||||
import { UserLogin } from "./pages/UserLogin.tsx";
|
|
||||||
import { UserPublicProfile } from "./pages/UserPublicProfile.tsx";
|
|
||||||
import { UserRegister } from "./pages/UserRegister.tsx";
|
|
||||||
import { UserDumps } from "./pages/UserDumps.tsx";
|
|
||||||
import { UserUpvoted } from "./pages/UserUpvoted.tsx";
|
|
||||||
import { UserPlaylists } from "./pages/UserPlaylists.tsx";
|
|
||||||
import { PlaylistDetail } from "./pages/PlaylistDetail.tsx";
|
|
||||||
import { Notifications } from "./pages/Notifications.tsx";
|
|
||||||
import { Search } from "./pages/Search.tsx";
|
|
||||||
import { ResetPassword } from "./pages/ResetPassword.tsx";
|
|
||||||
|
|
||||||
import { AuthProvider } from "./contexts/AuthProvider.tsx";
|
import { AuthProvider } from "./contexts/AuthProvider.tsx";
|
||||||
import { PlayerProvider } from "./contexts/PlayerProvider.tsx";
|
import { PlayerProvider } from "./contexts/PlayerProvider.tsx";
|
||||||
@@ -25,54 +13,106 @@ import { GlobalPlayer } from "./components/GlobalPlayer.tsx";
|
|||||||
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
|
const Index = lazy(() =>
|
||||||
|
import("./pages/Index.tsx").then((m) => ({ default: m.Index }))
|
||||||
|
);
|
||||||
|
const Dump = lazy(() =>
|
||||||
|
import("./pages/Dump.tsx").then((m) => ({ default: m.Dump }))
|
||||||
|
);
|
||||||
|
const DumpEdit = lazy(() =>
|
||||||
|
import("./pages/DumpEdit.tsx").then((m) => ({ default: m.DumpEdit }))
|
||||||
|
);
|
||||||
|
const UserLogin = lazy(() =>
|
||||||
|
import("./pages/UserLogin.tsx").then((m) => ({ default: m.UserLogin }))
|
||||||
|
);
|
||||||
|
const UserPublicProfile = lazy(() =>
|
||||||
|
import("./pages/UserPublicProfile.tsx").then((m) => ({
|
||||||
|
default: m.UserPublicProfile,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const UserRegister = lazy(() =>
|
||||||
|
import("./pages/UserRegister.tsx").then((m) => ({ default: m.UserRegister }))
|
||||||
|
);
|
||||||
|
const UserDumps = lazy(() =>
|
||||||
|
import("./pages/UserDumps.tsx").then((m) => ({ default: m.UserDumps }))
|
||||||
|
);
|
||||||
|
const UserUpvoted = lazy(() =>
|
||||||
|
import("./pages/UserUpvoted.tsx").then((m) => ({ default: m.UserUpvoted }))
|
||||||
|
);
|
||||||
|
const UserPlaylists = lazy(() =>
|
||||||
|
import("./pages/UserPlaylists.tsx").then((m) => ({
|
||||||
|
default: m.UserPlaylists,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const PlaylistDetail = lazy(() =>
|
||||||
|
import("./pages/PlaylistDetail.tsx").then((m) => ({
|
||||||
|
default: m.PlaylistDetail,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const Notifications = lazy(() =>
|
||||||
|
import("./pages/Notifications.tsx").then((m) => ({
|
||||||
|
default: m.Notifications,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const Search = lazy(() =>
|
||||||
|
import("./pages/Search.tsx").then((m) => ({ default: m.Search }))
|
||||||
|
);
|
||||||
|
const ResetPassword = lazy(() =>
|
||||||
|
import("./pages/ResetPassword.tsx").then((m) => ({
|
||||||
|
default: m.ResetPassword,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
function AppRoutes() {
|
function AppRoutes() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Suspense>
|
||||||
<Route path="/" element={<Index />} />
|
<Routes>
|
||||||
<Route path="/dumps/:selectedDump" element={<Dump />} />
|
<Route path="/" element={<Index />} />
|
||||||
<Route
|
<Route path="/dumps/:selectedDump" element={<Dump />} />
|
||||||
path="/dumps/:selectedDump/edit"
|
<Route
|
||||||
element={
|
path="/dumps/:selectedDump/edit"
|
||||||
<RestrictedLoggedIn>
|
element={
|
||||||
<DumpEdit />
|
<RestrictedLoggedIn>
|
||||||
</RestrictedLoggedIn>
|
<DumpEdit />
|
||||||
}
|
</RestrictedLoggedIn>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path="/register"
|
<Route
|
||||||
element={
|
path="/register"
|
||||||
<RestrictedGuest>
|
element={
|
||||||
<UserRegister />
|
<RestrictedGuest>
|
||||||
</RestrictedGuest>
|
<UserRegister />
|
||||||
}
|
</RestrictedGuest>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path="/login"
|
<Route
|
||||||
element={
|
path="/login"
|
||||||
<RestrictedGuest>
|
element={
|
||||||
<UserLogin />
|
<RestrictedGuest>
|
||||||
</RestrictedGuest>
|
<UserLogin />
|
||||||
}
|
</RestrictedGuest>
|
||||||
/>
|
}
|
||||||
<Route path="/users/:username" element={<UserPublicProfile />} />
|
/>
|
||||||
<Route path="/users/:username/dumps" element={<UserDumps />} />
|
<Route path="/users/:username" element={<UserPublicProfile />} />
|
||||||
<Route path="/users/:username/upvoted" element={<UserUpvoted />} />
|
<Route path="/users/:username/dumps" element={<UserDumps />} />
|
||||||
<Route
|
<Route path="/users/:username/upvoted" element={<UserUpvoted />} />
|
||||||
path="/users/:username/playlists"
|
<Route
|
||||||
element={<UserPlaylists />}
|
path="/users/:username/playlists"
|
||||||
/>
|
element={<UserPlaylists />}
|
||||||
<Route path="/playlists/:playlistId" element={<PlaylistDetail />} />
|
/>
|
||||||
<Route path="/search" element={<Search />} />
|
<Route path="/playlists/:playlistId" element={<PlaylistDetail />} />
|
||||||
<Route path="/reset-password" element={<ResetPassword />} />
|
<Route path="/search" element={<Search />} />
|
||||||
<Route
|
<Route path="/reset-password" element={<ResetPassword />} />
|
||||||
path="/notifications"
|
<Route
|
||||||
element={
|
path="/notifications"
|
||||||
<RestrictedLoggedIn>
|
element={
|
||||||
<Notifications />
|
<RestrictedLoggedIn>
|
||||||
</RestrictedLoggedIn>
|
<Notifications />
|
||||||
}
|
</RestrictedLoggedIn>
|
||||||
/>
|
}
|
||||||
</Routes>
|
/>
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { type ReactNode, useState } from "react";
|
import { lazy, type ReactNode, Suspense, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router";
|
import { Link, useNavigate } from "react-router";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { useAuth } from "../hooks/useAuth.ts";
|
import { useAuth } from "../hooks/useAuth.ts";
|
||||||
import { useWS } from "../hooks/useWS.ts";
|
import { useWS } from "../hooks/useWS.ts";
|
||||||
import { DumpCreateModal } from "./DumpCreateModal.tsx";
|
|
||||||
import { NotificationBell } from "./NotificationBell.tsx";
|
import { NotificationBell } from "./NotificationBell.tsx";
|
||||||
import { UserMenu } from "./UserMenu.tsx";
|
import { UserMenu } from "./UserMenu.tsx";
|
||||||
|
|
||||||
|
const DumpCreateModal = lazy(() =>
|
||||||
|
import("./DumpCreateModal.tsx").then((m) => ({ default: m.DumpCreateModal }))
|
||||||
|
);
|
||||||
|
|
||||||
export function AppHeader(
|
export function AppHeader(
|
||||||
{ centerSlot, disableNew, initialDumpUrl }: {
|
{ centerSlot, disableNew, initialDumpUrl }: {
|
||||||
centerSlot?: ReactNode;
|
centerSlot?: ReactNode;
|
||||||
@@ -88,10 +91,12 @@ export function AppHeader(
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{createModalOpen && (
|
{createModalOpen && (
|
||||||
<DumpCreateModal
|
<Suspense>
|
||||||
onClose={() => setCreateModalOpen(false)}
|
<DumpCreateModal
|
||||||
initialUrl={initialDumpUrl}
|
onClose={() => setCreateModalOpen(false)}
|
||||||
/>
|
initialUrl={initialDumpUrl}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -118,6 +118,21 @@ function AudioFilePreview(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function VideoThumb({ src, fallback }: { src: string; fallback: string }) {
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
if (failed) {
|
||||||
|
return <span className="rich-content-compact-icon">{fallback}</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt=""
|
||||||
|
className="rich-content-compact-thumbnail"
|
||||||
|
onError={() => setFailed(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function mimeIcon(mime: string): string {
|
function mimeIcon(mime: string): string {
|
||||||
if (mime.startsWith("video/")) return "🎬";
|
if (mime.startsWith("video/")) return "🎬";
|
||||||
if (mime.startsWith("audio/")) return "🎵";
|
if (mime.startsWith("audio/")) return "🎵";
|
||||||
@@ -148,6 +163,7 @@ export default function FilePreview(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (mime.startsWith("video/")) {
|
if (mime.startsWith("video/")) {
|
||||||
|
const thumbUrl = `${API_URL}/api/thumbnails/${dump.id}`;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -160,15 +176,7 @@ export default function FilePreview(
|
|||||||
play({ kind: "file", fileUrl, mimeType: mime, title: dump.title });
|
play({ kind: "file", fileUrl, mimeType: mime, title: dump.title });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<video
|
<VideoThumb src={thumbUrl} fallback={mimeIcon(mime)} />
|
||||||
src={fileUrl}
|
|
||||||
preload="metadata"
|
|
||||||
className="rich-content-compact-thumbnail"
|
|
||||||
muted
|
|
||||||
onLoadedMetadata={(e) => {
|
|
||||||
(e.target as HTMLVideoElement).currentTime = 0.1;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="rich-content-play-overlay">▶</span>
|
<span className="rich-content-play-overlay">▶</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -217,12 +225,9 @@ export default function FilePreview(
|
|||||||
>
|
>
|
||||||
<video
|
<video
|
||||||
src={fileUrl}
|
src={fileUrl}
|
||||||
preload="metadata"
|
preload="none"
|
||||||
className="file-preview-video-thumb"
|
className="file-preview-video-thumb"
|
||||||
muted
|
muted
|
||||||
onLoadedMetadata={(e) => {
|
|
||||||
(e.target as HTMLVideoElement).currentTime = 0.1;
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<span className="rich-content-play-overlay">
|
<span className="rich-content-play-overlay">
|
||||||
{videoPlaying ? "⏸" : "▶"}
|
{videoPlaying ? "⏸" : "▶"}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ msgstr "← Back to profile"
|
|||||||
msgid "+ Invite someone"
|
msgid "+ Invite someone"
|
||||||
msgstr "+ Invite someone"
|
msgstr "+ Invite someone"
|
||||||
|
|
||||||
#: src/components/AppHeader.tsx:67
|
#: src/components/AppHeader.tsx:70
|
||||||
msgid "+ New"
|
msgid "+ New"
|
||||||
msgstr "+ New"
|
msgstr "+ New"
|
||||||
|
|
||||||
@@ -176,8 +176,8 @@ msgid "Appearance"
|
|||||||
msgstr "Appearance"
|
msgstr "Appearance"
|
||||||
|
|
||||||
#. placeholder {0}: VALIDATION.PASSWORD_MIN
|
#. placeholder {0}: VALIDATION.PASSWORD_MIN
|
||||||
#: src/components/ChangePasswordModal.tsx:101
|
#: src/components/ChangePasswordModal.tsx:123
|
||||||
#: src/pages/ResetPassword.tsx:113
|
#: src/pages/ResetPassword.tsx:125
|
||||||
msgid "At least {0} characters"
|
msgid "At least {0} characters"
|
||||||
msgstr "At least {0} characters"
|
msgstr "At least {0} characters"
|
||||||
|
|
||||||
@@ -185,8 +185,8 @@ msgstr "At least {0} characters"
|
|||||||
msgid "Auto"
|
msgid "Auto"
|
||||||
msgstr "Auto"
|
msgstr "Auto"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:36
|
#: src/pages/ResetPassword.tsx:44
|
||||||
#: src/pages/ResetPassword.tsx:146
|
#: src/pages/ResetPassword.tsx:159
|
||||||
msgid "Back to login"
|
msgid "Back to login"
|
||||||
msgstr "Back to login"
|
msgstr "Back to login"
|
||||||
|
|
||||||
@@ -195,7 +195,7 @@ msgstr "Back to login"
|
|||||||
msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
|
msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
|
||||||
msgstr "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
|
msgstr "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:132
|
#: src/components/ChangePasswordModal.tsx:168
|
||||||
#: src/components/CommentThread.tsx:281
|
#: src/components/CommentThread.tsx:281
|
||||||
#: src/components/CommentThread.tsx:373
|
#: src/components/CommentThread.tsx:373
|
||||||
#: src/components/CommentThread.tsx:510
|
#: src/components/CommentThread.tsx:510
|
||||||
@@ -221,8 +221,8 @@ msgstr "Cancel removal"
|
|||||||
msgid "Change avatar"
|
msgid "Change avatar"
|
||||||
msgstr "Change avatar"
|
msgstr "Change avatar"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:55
|
#: src/components/ChangePasswordModal.tsx:56
|
||||||
#: src/components/ChangePasswordModal.tsx:142
|
#: src/components/ChangePasswordModal.tsx:178
|
||||||
msgid "Change password"
|
msgid "Change password"
|
||||||
msgstr "Change password"
|
msgstr "Change password"
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ msgstr "Change password…"
|
|||||||
msgid "Checking invite…"
|
msgid "Checking invite…"
|
||||||
msgstr "Checking invite…"
|
msgstr "Checking invite…"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:65
|
#: src/components/ChangePasswordModal.tsx:66
|
||||||
#: src/components/Modal.tsx:45
|
#: src/components/Modal.tsx:45
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr "Close"
|
msgstr "Close"
|
||||||
@@ -247,8 +247,8 @@ msgstr "Color scheme"
|
|||||||
#~ msgid "Comment not found"
|
#~ msgid "Comment not found"
|
||||||
#~ msgstr "Comment not found"
|
#~ msgstr "Comment not found"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:107
|
#: src/components/ChangePasswordModal.tsx:136
|
||||||
#: src/pages/ResetPassword.tsx:120
|
#: src/pages/ResetPassword.tsx:132
|
||||||
msgid "Confirm new password"
|
msgid "Confirm new password"
|
||||||
msgstr "Confirm new password"
|
msgstr "Confirm new password"
|
||||||
|
|
||||||
@@ -260,11 +260,11 @@ msgstr "Copied!"
|
|||||||
msgid "Copy"
|
msgid "Copy"
|
||||||
msgstr "Copy"
|
msgstr "Copy"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:123
|
#: src/components/ChangePasswordModal.tsx:159
|
||||||
msgid "Could not change password"
|
msgid "Could not change password"
|
||||||
msgstr "Could not change password"
|
msgstr "Could not change password"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:84
|
#: src/pages/ResetPassword.tsx:94
|
||||||
#: src/pages/UserLogin.tsx:79
|
#: src/pages/UserLogin.tsx:79
|
||||||
msgid "Could not connect to server"
|
msgid "Could not connect to server"
|
||||||
msgstr "Could not connect to server"
|
msgstr "Could not connect to server"
|
||||||
@@ -293,7 +293,7 @@ msgstr "Created ({0}{1})"
|
|||||||
msgid "Creating…"
|
msgid "Creating…"
|
||||||
msgstr "Creating…"
|
msgstr "Creating…"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:75
|
#: src/components/ChangePasswordModal.tsx:83
|
||||||
msgid "Current password"
|
msgid "Current password"
|
||||||
msgstr "Current password"
|
msgstr "Current password"
|
||||||
|
|
||||||
@@ -413,7 +413,7 @@ msgstr "Email address"
|
|||||||
msgid "Enter a query to search."
|
msgid "Enter a query to search."
|
||||||
msgstr "Enter a query to search."
|
msgstr "Enter a query to search."
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:48
|
#: src/components/ChangePasswordModal.tsx:49
|
||||||
msgid "Failed to change password"
|
msgid "Failed to change password"
|
||||||
msgstr "Failed to change password"
|
msgstr "Failed to change password"
|
||||||
|
|
||||||
@@ -432,7 +432,6 @@ msgstr "Failed to generate invite"
|
|||||||
#: src/pages/index/HotFeed.tsx:36
|
#: src/pages/index/HotFeed.tsx:36
|
||||||
#: src/pages/index/JournalFeed.tsx:48
|
#: src/pages/index/JournalFeed.tsx:48
|
||||||
#: src/pages/index/NewFeed.tsx:36
|
#: src/pages/index/NewFeed.tsx:36
|
||||||
#: src/pages/Notifications.tsx:323
|
|
||||||
#: src/pages/UserPublicProfile.tsx:1106
|
#: src/pages/UserPublicProfile.tsx:1106
|
||||||
#: src/pages/UserPublicProfile.tsx:1148
|
#: src/pages/UserPublicProfile.tsx:1148
|
||||||
#: src/pages/UserPublicProfile.tsx:1193
|
#: src/pages/UserPublicProfile.tsx:1193
|
||||||
@@ -554,7 +553,7 @@ msgstr "From people"
|
|||||||
msgid "From playlists"
|
msgid "From playlists"
|
||||||
msgstr "From playlists"
|
msgstr "From playlists"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:56
|
#: src/pages/ResetPassword.tsx:66
|
||||||
msgid "Go to login"
|
msgid "Go to login"
|
||||||
msgstr "Go to login"
|
msgstr "Go to login"
|
||||||
|
|
||||||
@@ -574,7 +573,7 @@ msgstr "If that address is registered you'll receive a reset link shortly."
|
|||||||
msgid "Invalid invite"
|
msgid "Invalid invite"
|
||||||
msgstr "Invalid invite"
|
msgstr "Invalid invite"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:33
|
#: src/pages/ResetPassword.tsx:34
|
||||||
msgid "Invalid link"
|
msgid "Invalid link"
|
||||||
msgstr "Invalid link"
|
msgstr "Invalid link"
|
||||||
|
|
||||||
@@ -620,11 +619,11 @@ msgstr "Light"
|
|||||||
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||||
msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
|
msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||||
|
|
||||||
#: src/components/AppHeader.tsx:84
|
#: src/components/AppHeader.tsx:87
|
||||||
msgid "Live updates unavailable."
|
msgid "Live updates unavailable."
|
||||||
msgstr "Live updates unavailable."
|
msgstr "Live updates unavailable."
|
||||||
|
|
||||||
#: src/pages/Notifications.tsx:396
|
#: src/pages/Notifications.tsx:390
|
||||||
msgid "Load more"
|
msgid "Load more"
|
||||||
msgstr "Load more"
|
msgstr "Load more"
|
||||||
|
|
||||||
@@ -659,8 +658,8 @@ msgstr "Loading profile…"
|
|||||||
#: src/pages/index/HotFeed.tsx:32
|
#: src/pages/index/HotFeed.tsx:32
|
||||||
#: src/pages/index/JournalFeed.tsx:44
|
#: src/pages/index/JournalFeed.tsx:44
|
||||||
#: src/pages/index/NewFeed.tsx:32
|
#: src/pages/index/NewFeed.tsx:32
|
||||||
#: src/pages/Notifications.tsx:319
|
#: src/pages/Notifications.tsx:313
|
||||||
#: src/pages/Notifications.tsx:395
|
#: src/pages/Notifications.tsx:389
|
||||||
#: src/pages/UserDumps.tsx:51
|
#: src/pages/UserDumps.tsx:51
|
||||||
#: src/pages/UserPlaylists.tsx:342
|
#: src/pages/UserPlaylists.tsx:342
|
||||||
#: src/pages/UserPublicProfile.tsx:1100
|
#: src/pages/UserPublicProfile.tsx:1100
|
||||||
@@ -670,7 +669,7 @@ msgstr "Loading profile…"
|
|||||||
msgid "Loading…"
|
msgid "Loading…"
|
||||||
msgstr "Loading…"
|
msgstr "Loading…"
|
||||||
|
|
||||||
#: src/components/AppHeader.tsx:74
|
#: src/components/AppHeader.tsx:77
|
||||||
#: src/pages/UserLogin.tsx:87
|
#: src/pages/UserLogin.tsx:87
|
||||||
#: src/pages/UserLogin.tsx:117
|
#: src/pages/UserLogin.tsx:117
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
@@ -693,7 +692,7 @@ msgstr "Login failed"
|
|||||||
msgid "Max 50 MB"
|
msgid "Max 50 MB"
|
||||||
msgstr "Max 50 MB"
|
msgstr "Max 50 MB"
|
||||||
|
|
||||||
#: src/pages/Notifications.tsx:312
|
#: src/pages/Notifications.tsx:306
|
||||||
msgid "new"
|
msgid "new"
|
||||||
msgstr "new"
|
msgstr "new"
|
||||||
|
|
||||||
@@ -705,8 +704,8 @@ msgstr "New"
|
|||||||
msgid "New dump"
|
msgid "New dump"
|
||||||
msgstr "New dump"
|
msgstr "New dump"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:88
|
#: src/components/ChangePasswordModal.tsx:103
|
||||||
#: src/pages/ResetPassword.tsx:101
|
#: src/pages/ResetPassword.tsx:113
|
||||||
msgid "New password"
|
msgid "New password"
|
||||||
msgstr "New password"
|
msgstr "New password"
|
||||||
|
|
||||||
@@ -763,7 +762,7 @@ msgstr "No users match \"{q}\"."
|
|||||||
msgid "Not following anyone yet."
|
msgid "Not following anyone yet."
|
||||||
msgstr "Not following anyone yet."
|
msgstr "Not following anyone yet."
|
||||||
|
|
||||||
#: src/pages/Notifications.tsx:330
|
#: src/pages/Notifications.tsx:324
|
||||||
#: src/pages/UserDumps.tsx:123
|
#: src/pages/UserDumps.tsx:123
|
||||||
#: src/pages/UserPublicProfile.tsx:1340
|
#: src/pages/UserPublicProfile.tsx:1340
|
||||||
#: src/pages/UserPublicProfile.tsx:1463
|
#: src/pages/UserPublicProfile.tsx:1463
|
||||||
@@ -772,7 +771,7 @@ msgid "Nothing here yet."
|
|||||||
msgstr "Nothing here yet."
|
msgstr "Nothing here yet."
|
||||||
|
|
||||||
#: src/components/NotificationBell.tsx:42
|
#: src/components/NotificationBell.tsx:42
|
||||||
#: src/pages/Notifications.tsx:308
|
#: src/pages/Notifications.tsx:302
|
||||||
msgid "Notifications"
|
msgid "Notifications"
|
||||||
msgstr "Notifications"
|
msgstr "Notifications"
|
||||||
|
|
||||||
@@ -798,7 +797,7 @@ msgstr "Password"
|
|||||||
msgid "Password (min. {0} characters)"
|
msgid "Password (min. {0} characters)"
|
||||||
msgstr "Password (min. {0} characters)"
|
msgstr "Password (min. {0} characters)"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:60
|
#: src/components/ChangePasswordModal.tsx:61
|
||||||
msgid "Password changed successfully."
|
msgid "Password changed successfully."
|
||||||
msgstr "Password changed successfully."
|
msgstr "Password changed successfully."
|
||||||
|
|
||||||
@@ -810,12 +809,12 @@ msgstr "Password changed successfully."
|
|||||||
#~ msgid "Password must be at most 128 characters"
|
#~ msgid "Password must be at most 128 characters"
|
||||||
#~ msgstr "Password must be at most 128 characters"
|
#~ msgstr "Password must be at most 128 characters"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:47
|
#: src/pages/ResetPassword.tsx:56
|
||||||
msgid "Password updated"
|
msgid "Password updated"
|
||||||
msgstr "Password updated"
|
msgstr "Password updated"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:118
|
#: src/components/ChangePasswordModal.tsx:154
|
||||||
#: src/pages/ResetPassword.tsx:129
|
#: src/pages/ResetPassword.tsx:141
|
||||||
msgid "Passwords do not match"
|
msgid "Passwords do not match"
|
||||||
msgstr "Passwords do not match"
|
msgstr "Passwords do not match"
|
||||||
|
|
||||||
@@ -823,7 +822,7 @@ msgstr "Passwords do not match"
|
|||||||
#~ msgid "Playlist not found"
|
#~ msgid "Playlist not found"
|
||||||
#~ msgstr "Playlist not found"
|
#~ msgstr "Playlist not found"
|
||||||
|
|
||||||
#: src/components/AppHeader.tsx:50
|
#: src/components/AppHeader.tsx:53
|
||||||
#: src/components/UserMenu.tsx:62
|
#: src/components/UserMenu.tsx:62
|
||||||
#: src/pages/Search.tsx:175
|
#: src/pages/Search.tsx:175
|
||||||
#: src/pages/UserPlaylists.tsx:368
|
#: src/pages/UserPlaylists.tsx:368
|
||||||
@@ -922,7 +921,7 @@ msgstr "Reply"
|
|||||||
msgid "Request failed"
|
msgid "Request failed"
|
||||||
msgstr "Request failed"
|
msgstr "Request failed"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:94
|
#: src/pages/ResetPassword.tsx:106
|
||||||
msgid "Reset failed"
|
msgid "Reset failed"
|
||||||
msgstr "Reset failed"
|
msgstr "Reset failed"
|
||||||
|
|
||||||
@@ -939,10 +938,10 @@ msgstr "Retry"
|
|||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr "Save"
|
msgstr "Save"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:141
|
#: src/components/ChangePasswordModal.tsx:177
|
||||||
#: src/components/CommentThread.tsx:269
|
#: src/components/CommentThread.tsx:269
|
||||||
#: src/pages/PlaylistDetail.tsx:673
|
#: src/pages/PlaylistDetail.tsx:673
|
||||||
#: src/pages/ResetPassword.tsx:140
|
#: src/pages/ResetPassword.tsx:152
|
||||||
#: src/pages/UserPublicProfile.tsx:832
|
#: src/pages/UserPublicProfile.tsx:832
|
||||||
#: src/pages/UserPublicProfile.tsx:911
|
#: src/pages/UserPublicProfile.tsx:911
|
||||||
msgid "Saving…"
|
msgid "Saving…"
|
||||||
@@ -972,12 +971,12 @@ msgstr "Send reset link"
|
|||||||
msgid "Sending…"
|
msgid "Sending…"
|
||||||
msgstr "Sending…"
|
msgstr "Sending…"
|
||||||
|
|
||||||
#: src/components/AppHeader.tsx:65
|
#: src/components/AppHeader.tsx:68
|
||||||
msgid "Server unreachable"
|
msgid "Server unreachable"
|
||||||
msgstr "Server unreachable"
|
msgstr "Server unreachable"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:91
|
#: src/pages/ResetPassword.tsx:102
|
||||||
#: src/pages/ResetPassword.tsx:141
|
#: src/pages/ResetPassword.tsx:153
|
||||||
msgid "Set new password"
|
msgid "Set new password"
|
||||||
msgstr "Set new password"
|
msgstr "Set new password"
|
||||||
|
|
||||||
@@ -1014,7 +1013,7 @@ msgstr "This invite link is missing, expired, or already used."
|
|||||||
msgid "This is a mirage."
|
msgid "This is a mirage."
|
||||||
msgstr "This is a mirage."
|
msgstr "This is a mirage."
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:34
|
#: src/pages/ResetPassword.tsx:37
|
||||||
msgid "This reset link is missing or malformed."
|
msgid "This reset link is missing or malformed."
|
||||||
msgstr "This reset link is missing or malformed."
|
msgstr "This reset link is missing or malformed."
|
||||||
|
|
||||||
@@ -1042,8 +1041,8 @@ msgstr "Unfollow {targetUsername}"
|
|||||||
msgid "Unfollow playlist"
|
msgid "Unfollow playlist"
|
||||||
msgstr "Unfollow playlist"
|
msgstr "Unfollow playlist"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:43
|
#: src/components/ChangePasswordModal.tsx:44
|
||||||
#: src/pages/ResetPassword.tsx:80
|
#: src/pages/ResetPassword.tsx:90
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr "Unknown error"
|
msgstr "Unknown error"
|
||||||
|
|
||||||
@@ -1120,7 +1119,7 @@ msgstr "Write a reply…"
|
|||||||
msgid "Yesterday"
|
msgid "Yesterday"
|
||||||
msgstr "Yesterday"
|
msgstr "Yesterday"
|
||||||
|
|
||||||
#: src/pages/Notifications.tsx:333
|
#: src/pages/Notifications.tsx:327
|
||||||
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
||||||
msgstr "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
msgstr "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
||||||
|
|
||||||
@@ -1140,6 +1139,6 @@ msgstr "You've reached the end."
|
|||||||
msgid "Your email address"
|
msgid "Your email address"
|
||||||
msgstr "Your email address"
|
msgstr "Your email address"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:49
|
#: src/pages/ResetPassword.tsx:59
|
||||||
msgid "Your password has been changed. You can now log in."
|
msgid "Your password has been changed. You can now log in."
|
||||||
msgstr "Your password has been changed. You can now log in."
|
msgstr "Your password has been changed. You can now log in."
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ msgstr "← Retour au profil"
|
|||||||
msgid "+ Invite someone"
|
msgid "+ Invite someone"
|
||||||
msgstr "+ Inviter quelqu'un"
|
msgstr "+ Inviter quelqu'un"
|
||||||
|
|
||||||
#: src/components/AppHeader.tsx:67
|
#: src/components/AppHeader.tsx:70
|
||||||
msgid "+ New"
|
msgid "+ New"
|
||||||
msgstr "+ Nouveau"
|
msgstr "+ Nouveau"
|
||||||
|
|
||||||
@@ -172,8 +172,8 @@ msgid "Appearance"
|
|||||||
msgstr "Apparence"
|
msgstr "Apparence"
|
||||||
|
|
||||||
#. placeholder {0}: VALIDATION.PASSWORD_MIN
|
#. placeholder {0}: VALIDATION.PASSWORD_MIN
|
||||||
#: src/components/ChangePasswordModal.tsx:101
|
#: src/components/ChangePasswordModal.tsx:123
|
||||||
#: src/pages/ResetPassword.tsx:113
|
#: src/pages/ResetPassword.tsx:125
|
||||||
msgid "At least {0} characters"
|
msgid "At least {0} characters"
|
||||||
msgstr "Au moins {0} caractères"
|
msgstr "Au moins {0} caractères"
|
||||||
|
|
||||||
@@ -181,8 +181,8 @@ msgstr "Au moins {0} caractères"
|
|||||||
msgid "Auto"
|
msgid "Auto"
|
||||||
msgstr "Auto"
|
msgstr "Auto"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:36
|
#: src/pages/ResetPassword.tsx:44
|
||||||
#: src/pages/ResetPassword.tsx:146
|
#: src/pages/ResetPassword.tsx:159
|
||||||
msgid "Back to login"
|
msgid "Back to login"
|
||||||
msgstr "Retour à la connexion"
|
msgstr "Retour à la connexion"
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ msgstr "Retour à la connexion"
|
|||||||
msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
|
msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
|
||||||
msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion."
|
msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion."
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:132
|
#: src/components/ChangePasswordModal.tsx:168
|
||||||
#: src/components/CommentThread.tsx:281
|
#: src/components/CommentThread.tsx:281
|
||||||
#: src/components/CommentThread.tsx:373
|
#: src/components/CommentThread.tsx:373
|
||||||
#: src/components/CommentThread.tsx:510
|
#: src/components/CommentThread.tsx:510
|
||||||
@@ -213,8 +213,8 @@ msgstr "Annuler la suppression"
|
|||||||
msgid "Change avatar"
|
msgid "Change avatar"
|
||||||
msgstr "Changer l'avatar"
|
msgstr "Changer l'avatar"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:55
|
#: src/components/ChangePasswordModal.tsx:56
|
||||||
#: src/components/ChangePasswordModal.tsx:142
|
#: src/components/ChangePasswordModal.tsx:178
|
||||||
msgid "Change password"
|
msgid "Change password"
|
||||||
msgstr "Changer le mot de passe"
|
msgstr "Changer le mot de passe"
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ msgstr "Changer le mot de passe…"
|
|||||||
msgid "Checking invite…"
|
msgid "Checking invite…"
|
||||||
msgstr "Vérification de l'invitation…"
|
msgstr "Vérification de l'invitation…"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:65
|
#: src/components/ChangePasswordModal.tsx:66
|
||||||
#: src/components/Modal.tsx:45
|
#: src/components/Modal.tsx:45
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr "Fermer"
|
msgstr "Fermer"
|
||||||
@@ -235,8 +235,8 @@ msgstr "Fermer"
|
|||||||
msgid "Color scheme"
|
msgid "Color scheme"
|
||||||
msgstr "Thème de couleur"
|
msgstr "Thème de couleur"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:107
|
#: src/components/ChangePasswordModal.tsx:136
|
||||||
#: src/pages/ResetPassword.tsx:120
|
#: src/pages/ResetPassword.tsx:132
|
||||||
msgid "Confirm new password"
|
msgid "Confirm new password"
|
||||||
msgstr "Confirmer le nouveau mot de passe"
|
msgstr "Confirmer le nouveau mot de passe"
|
||||||
|
|
||||||
@@ -248,11 +248,11 @@ msgstr "Copié !"
|
|||||||
msgid "Copy"
|
msgid "Copy"
|
||||||
msgstr "Copier"
|
msgstr "Copier"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:123
|
#: src/components/ChangePasswordModal.tsx:159
|
||||||
msgid "Could not change password"
|
msgid "Could not change password"
|
||||||
msgstr "Impossible de changer le mot de passe"
|
msgstr "Impossible de changer le mot de passe"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:84
|
#: src/pages/ResetPassword.tsx:94
|
||||||
#: src/pages/UserLogin.tsx:79
|
#: src/pages/UserLogin.tsx:79
|
||||||
msgid "Could not connect to server"
|
msgid "Could not connect to server"
|
||||||
msgstr "Impossible de contacter le serveur"
|
msgstr "Impossible de contacter le serveur"
|
||||||
@@ -281,7 +281,7 @@ msgstr "Créées ({0}{1})"
|
|||||||
msgid "Creating…"
|
msgid "Creating…"
|
||||||
msgstr "Création…"
|
msgstr "Création…"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:75
|
#: src/components/ChangePasswordModal.tsx:83
|
||||||
msgid "Current password"
|
msgid "Current password"
|
||||||
msgstr "Mot de passe actuel"
|
msgstr "Mot de passe actuel"
|
||||||
|
|
||||||
@@ -397,7 +397,7 @@ msgstr "Adresse e-mail"
|
|||||||
msgid "Enter a query to search."
|
msgid "Enter a query to search."
|
||||||
msgstr "Saisissez une recherche."
|
msgstr "Saisissez une recherche."
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:48
|
#: src/components/ChangePasswordModal.tsx:49
|
||||||
msgid "Failed to change password"
|
msgid "Failed to change password"
|
||||||
msgstr "Impossible de changer le mot de passe"
|
msgstr "Impossible de changer le mot de passe"
|
||||||
|
|
||||||
@@ -416,7 +416,6 @@ msgstr "Impossible de générer une invitation"
|
|||||||
#: src/pages/index/HotFeed.tsx:36
|
#: src/pages/index/HotFeed.tsx:36
|
||||||
#: src/pages/index/JournalFeed.tsx:48
|
#: src/pages/index/JournalFeed.tsx:48
|
||||||
#: src/pages/index/NewFeed.tsx:36
|
#: src/pages/index/NewFeed.tsx:36
|
||||||
#: src/pages/Notifications.tsx:323
|
|
||||||
#: src/pages/UserPublicProfile.tsx:1106
|
#: src/pages/UserPublicProfile.tsx:1106
|
||||||
#: src/pages/UserPublicProfile.tsx:1148
|
#: src/pages/UserPublicProfile.tsx:1148
|
||||||
#: src/pages/UserPublicProfile.tsx:1193
|
#: src/pages/UserPublicProfile.tsx:1193
|
||||||
@@ -522,7 +521,7 @@ msgstr "De personnes"
|
|||||||
msgid "From playlists"
|
msgid "From playlists"
|
||||||
msgstr "De collections"
|
msgstr "De collections"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:56
|
#: src/pages/ResetPassword.tsx:66
|
||||||
msgid "Go to login"
|
msgid "Go to login"
|
||||||
msgstr "Aller à la connexion"
|
msgstr "Aller à la connexion"
|
||||||
|
|
||||||
@@ -538,7 +537,7 @@ msgstr "Si cette adresse est enregistrée, vous recevrez un lien de réinitialis
|
|||||||
msgid "Invalid invite"
|
msgid "Invalid invite"
|
||||||
msgstr "Invitation invalide"
|
msgstr "Invitation invalide"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:33
|
#: src/pages/ResetPassword.tsx:34
|
||||||
msgid "Invalid link"
|
msgid "Invalid link"
|
||||||
msgstr "Lien invalide"
|
msgstr "Lien invalide"
|
||||||
|
|
||||||
@@ -567,11 +566,11 @@ msgstr "Clair"
|
|||||||
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||||
msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…"
|
msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…"
|
||||||
|
|
||||||
#: src/components/AppHeader.tsx:84
|
#: src/components/AppHeader.tsx:87
|
||||||
msgid "Live updates unavailable."
|
msgid "Live updates unavailable."
|
||||||
msgstr "Mises à jour en direct indisponibles."
|
msgstr "Mises à jour en direct indisponibles."
|
||||||
|
|
||||||
#: src/pages/Notifications.tsx:396
|
#: src/pages/Notifications.tsx:390
|
||||||
msgid "Load more"
|
msgid "Load more"
|
||||||
msgstr "Charger plus"
|
msgstr "Charger plus"
|
||||||
|
|
||||||
@@ -606,8 +605,8 @@ msgstr "Chargement du profil…"
|
|||||||
#: src/pages/index/HotFeed.tsx:32
|
#: src/pages/index/HotFeed.tsx:32
|
||||||
#: src/pages/index/JournalFeed.tsx:44
|
#: src/pages/index/JournalFeed.tsx:44
|
||||||
#: src/pages/index/NewFeed.tsx:32
|
#: src/pages/index/NewFeed.tsx:32
|
||||||
#: src/pages/Notifications.tsx:319
|
#: src/pages/Notifications.tsx:313
|
||||||
#: src/pages/Notifications.tsx:395
|
#: src/pages/Notifications.tsx:389
|
||||||
#: src/pages/UserDumps.tsx:51
|
#: src/pages/UserDumps.tsx:51
|
||||||
#: src/pages/UserPlaylists.tsx:342
|
#: src/pages/UserPlaylists.tsx:342
|
||||||
#: src/pages/UserPublicProfile.tsx:1100
|
#: src/pages/UserPublicProfile.tsx:1100
|
||||||
@@ -617,7 +616,7 @@ msgstr "Chargement du profil…"
|
|||||||
msgid "Loading…"
|
msgid "Loading…"
|
||||||
msgstr "Chargement…"
|
msgstr "Chargement…"
|
||||||
|
|
||||||
#: src/components/AppHeader.tsx:74
|
#: src/components/AppHeader.tsx:77
|
||||||
#: src/pages/UserLogin.tsx:87
|
#: src/pages/UserLogin.tsx:87
|
||||||
#: src/pages/UserLogin.tsx:117
|
#: src/pages/UserLogin.tsx:117
|
||||||
msgid "Log in"
|
msgid "Log in"
|
||||||
@@ -640,7 +639,7 @@ msgstr "Connexion échouée"
|
|||||||
msgid "Max 50 MB"
|
msgid "Max 50 MB"
|
||||||
msgstr "Max 50 Mo"
|
msgstr "Max 50 Mo"
|
||||||
|
|
||||||
#: src/pages/Notifications.tsx:312
|
#: src/pages/Notifications.tsx:306
|
||||||
msgid "new"
|
msgid "new"
|
||||||
msgstr "nouveau"
|
msgstr "nouveau"
|
||||||
|
|
||||||
@@ -652,8 +651,8 @@ msgstr "Nouveau"
|
|||||||
msgid "New dump"
|
msgid "New dump"
|
||||||
msgstr "Nouvelle reco"
|
msgstr "Nouvelle reco"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:88
|
#: src/components/ChangePasswordModal.tsx:103
|
||||||
#: src/pages/ResetPassword.tsx:101
|
#: src/pages/ResetPassword.tsx:113
|
||||||
msgid "New password"
|
msgid "New password"
|
||||||
msgstr "Nouveau mot de passe"
|
msgstr "Nouveau mot de passe"
|
||||||
|
|
||||||
@@ -706,7 +705,7 @@ msgstr "Aucun utilisateur ne correspond à « {q} »."
|
|||||||
msgid "Not following anyone yet."
|
msgid "Not following anyone yet."
|
||||||
msgstr "Aucun abonnement pour le moment."
|
msgstr "Aucun abonnement pour le moment."
|
||||||
|
|
||||||
#: src/pages/Notifications.tsx:330
|
#: src/pages/Notifications.tsx:324
|
||||||
#: src/pages/UserDumps.tsx:123
|
#: src/pages/UserDumps.tsx:123
|
||||||
#: src/pages/UserPublicProfile.tsx:1340
|
#: src/pages/UserPublicProfile.tsx:1340
|
||||||
#: src/pages/UserPublicProfile.tsx:1463
|
#: src/pages/UserPublicProfile.tsx:1463
|
||||||
@@ -715,7 +714,7 @@ msgid "Nothing here yet."
|
|||||||
msgstr "Rien ici pour l'instant."
|
msgstr "Rien ici pour l'instant."
|
||||||
|
|
||||||
#: src/components/NotificationBell.tsx:42
|
#: src/components/NotificationBell.tsx:42
|
||||||
#: src/pages/Notifications.tsx:308
|
#: src/pages/Notifications.tsx:302
|
||||||
msgid "Notifications"
|
msgid "Notifications"
|
||||||
msgstr "Notifications"
|
msgstr "Notifications"
|
||||||
|
|
||||||
@@ -741,20 +740,20 @@ msgstr "Mot de passe"
|
|||||||
msgid "Password (min. {0} characters)"
|
msgid "Password (min. {0} characters)"
|
||||||
msgstr "Mot de passe (min. {0} caractères)"
|
msgstr "Mot de passe (min. {0} caractères)"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:60
|
#: src/components/ChangePasswordModal.tsx:61
|
||||||
msgid "Password changed successfully."
|
msgid "Password changed successfully."
|
||||||
msgstr "Mot de passe modifié avec succès."
|
msgstr "Mot de passe modifié avec succès."
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:47
|
#: src/pages/ResetPassword.tsx:56
|
||||||
msgid "Password updated"
|
msgid "Password updated"
|
||||||
msgstr "Mot de passe mis à jour"
|
msgstr "Mot de passe mis à jour"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:118
|
#: src/components/ChangePasswordModal.tsx:154
|
||||||
#: src/pages/ResetPassword.tsx:129
|
#: src/pages/ResetPassword.tsx:141
|
||||||
msgid "Passwords do not match"
|
msgid "Passwords do not match"
|
||||||
msgstr "Les mots de passe ne correspondent pas"
|
msgstr "Les mots de passe ne correspondent pas"
|
||||||
|
|
||||||
#: src/components/AppHeader.tsx:50
|
#: src/components/AppHeader.tsx:53
|
||||||
#: src/components/UserMenu.tsx:62
|
#: src/components/UserMenu.tsx:62
|
||||||
#: src/pages/Search.tsx:175
|
#: src/pages/Search.tsx:175
|
||||||
#: src/pages/UserPlaylists.tsx:368
|
#: src/pages/UserPlaylists.tsx:368
|
||||||
@@ -853,7 +852,7 @@ msgstr "Répondre"
|
|||||||
msgid "Request failed"
|
msgid "Request failed"
|
||||||
msgstr "Échec de la demande"
|
msgstr "Échec de la demande"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:94
|
#: src/pages/ResetPassword.tsx:106
|
||||||
msgid "Reset failed"
|
msgid "Reset failed"
|
||||||
msgstr "Échec de la réinitialisation"
|
msgstr "Échec de la réinitialisation"
|
||||||
|
|
||||||
@@ -870,10 +869,10 @@ msgstr "Réessayer"
|
|||||||
msgid "Save"
|
msgid "Save"
|
||||||
msgstr "Enregistrer"
|
msgstr "Enregistrer"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:141
|
#: src/components/ChangePasswordModal.tsx:177
|
||||||
#: src/components/CommentThread.tsx:269
|
#: src/components/CommentThread.tsx:269
|
||||||
#: src/pages/PlaylistDetail.tsx:673
|
#: src/pages/PlaylistDetail.tsx:673
|
||||||
#: src/pages/ResetPassword.tsx:140
|
#: src/pages/ResetPassword.tsx:152
|
||||||
#: src/pages/UserPublicProfile.tsx:832
|
#: src/pages/UserPublicProfile.tsx:832
|
||||||
#: src/pages/UserPublicProfile.tsx:911
|
#: src/pages/UserPublicProfile.tsx:911
|
||||||
msgid "Saving…"
|
msgid "Saving…"
|
||||||
@@ -903,12 +902,12 @@ msgstr "Envoyer le lien de réinitialisation"
|
|||||||
msgid "Sending…"
|
msgid "Sending…"
|
||||||
msgstr "Envoi…"
|
msgstr "Envoi…"
|
||||||
|
|
||||||
#: src/components/AppHeader.tsx:65
|
#: src/components/AppHeader.tsx:68
|
||||||
msgid "Server unreachable"
|
msgid "Server unreachable"
|
||||||
msgstr "Serveur inaccessible"
|
msgstr "Serveur inaccessible"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:91
|
#: src/pages/ResetPassword.tsx:102
|
||||||
#: src/pages/ResetPassword.tsx:141
|
#: src/pages/ResetPassword.tsx:153
|
||||||
msgid "Set new password"
|
msgid "Set new password"
|
||||||
msgstr "Définir un nouveau mot de passe"
|
msgstr "Définir un nouveau mot de passe"
|
||||||
|
|
||||||
@@ -945,7 +944,7 @@ msgstr "Ce lien d'invitation est manquant, expiré ou déjà utilisé."
|
|||||||
msgid "This is a mirage."
|
msgid "This is a mirage."
|
||||||
msgstr "C'est un mirage."
|
msgstr "C'est un mirage."
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:34
|
#: src/pages/ResetPassword.tsx:37
|
||||||
msgid "This reset link is missing or malformed."
|
msgid "This reset link is missing or malformed."
|
||||||
msgstr "Ce lien de réinitialisation est absent ou malformé."
|
msgstr "Ce lien de réinitialisation est absent ou malformé."
|
||||||
|
|
||||||
@@ -969,8 +968,8 @@ msgstr "Ne plus suivre {targetUsername}"
|
|||||||
msgid "Unfollow playlist"
|
msgid "Unfollow playlist"
|
||||||
msgstr "Ne plus suivre la collection"
|
msgstr "Ne plus suivre la collection"
|
||||||
|
|
||||||
#: src/components/ChangePasswordModal.tsx:43
|
#: src/components/ChangePasswordModal.tsx:44
|
||||||
#: src/pages/ResetPassword.tsx:80
|
#: src/pages/ResetPassword.tsx:90
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr "Erreur inconnue"
|
msgstr "Erreur inconnue"
|
||||||
|
|
||||||
@@ -1039,7 +1038,7 @@ msgstr "Écrire une réponse…"
|
|||||||
msgid "Yesterday"
|
msgid "Yesterday"
|
||||||
msgstr "Hier"
|
msgstr "Hier"
|
||||||
|
|
||||||
#: src/pages/Notifications.tsx:333
|
#: src/pages/Notifications.tsx:327
|
||||||
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
||||||
msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu."
|
msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu."
|
||||||
|
|
||||||
@@ -1059,6 +1058,6 @@ msgstr "Vous avez tout lu, tout vu, tout bu."
|
|||||||
msgid "Your email address"
|
msgid "Your email address"
|
||||||
msgstr "Votre adresse e-mail"
|
msgstr "Votre adresse e-mail"
|
||||||
|
|
||||||
#: src/pages/ResetPassword.tsx:49
|
#: src/pages/ResetPassword.tsx:59
|
||||||
msgid "Your password has been changed. You can now log in."
|
msgid "Your password has been changed. You can now log in."
|
||||||
msgstr "Votre mot de passe a été modifié. Vous pouvez maintenant vous connecter."
|
msgstr "Votre mot de passe a été modifié. Vous pouvez maintenant vous connecter."
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
|
|||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
|
import { relativeTime } from "../utils/relativeTime.ts";
|
||||||
|
|
||||||
import { API_URL, NOTIFICATIONS_PAGE_SIZE } from "../config/api.ts";
|
import { API_URL, NOTIFICATIONS_PAGE_SIZE } from "../config/api.ts";
|
||||||
import { useAuth } from "../hooks/useAuth.ts";
|
import { useAuth } from "../hooks/useAuth.ts";
|
||||||
@@ -174,14 +175,8 @@ function notificationContent(n: Notification): React.ReactNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function timeAgo(date: Date): string {
|
function timeAgo(date: Date): string {
|
||||||
const secs = Math.floor((Date.now() - date.getTime()) / 1000);
|
const abs = Math.abs(Date.now() - date.getTime()) / 1000;
|
||||||
if (secs < 60) return t`just now`;
|
if (abs < 7 * 86400) return relativeTime(date);
|
||||||
const mins = Math.floor(secs / 60);
|
|
||||||
if (mins < 60) return t`${mins}m ago`;
|
|
||||||
const hrs = Math.floor(mins / 60);
|
|
||||||
if (hrs < 24) return t`${hrs}h ago`;
|
|
||||||
const days = Math.floor(hrs / 24);
|
|
||||||
if (days < 7) return t`${days}d ago`;
|
|
||||||
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
import { i18n } from "../i18n.ts";
|
||||||
|
|
||||||
export function relativeTime(date: Date): string {
|
export function relativeTime(date: Date): string {
|
||||||
|
const rtf = new Intl.RelativeTimeFormat(i18n.locale, { numeric: "auto" });
|
||||||
const diff = date.getTime() - Date.now(); // negative = past
|
const diff = date.getTime() - Date.now(); // negative = past
|
||||||
const abs = Math.abs(diff) / 1000;
|
const abs = Math.abs(diff) / 1000;
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,20 @@ export default defineConfig({
|
|||||||
ignored: ["**/api/**"],
|
ignored: ["**/api/**"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (id.includes("react-markdown") || id.includes("remark-gfm") || id.includes("remark-parse") || id.includes("mdast") || id.includes("micromark") || id.includes("unist")) {
|
||||||
|
return "vendor-markdown";
|
||||||
|
}
|
||||||
|
if (id.includes("node_modules")) {
|
||||||
|
return "vendor";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
manifestPlugin(),
|
manifestPlugin(),
|
||||||
lingui(),
|
lingui(),
|
||||||
|
|||||||
Reference in New Issue
Block a user