v3: fixed roles propagation bug, fixed inconsistent file drop zone, fixed chat attachments wrongfully pruned, some visual tweaks
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 41s
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 41s
This commit is contained in:
@@ -46,13 +46,17 @@ export async function authMiddleware<R extends string>(
|
|||||||
throw new APIException(APIErrorCode.UNAUTHORIZED, 401, "Invalid token");
|
throw new APIException(APIErrorCode.UNAUTHORIZED, 401, "Invalid token");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let user;
|
||||||
try {
|
try {
|
||||||
getUserById(payload.userId);
|
user = getUserById(payload.userId);
|
||||||
} catch {
|
} catch {
|
||||||
throw new APIException(APIErrorCode.UNAUTHORIZED, 401, "User not found");
|
throw new APIException(APIErrorCode.UNAUTHORIZED, 401, "User not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.state.user = payload;
|
// Trust the live role from the DB, not the role baked into the JWT at login.
|
||||||
|
// Tokens last 7 days, so a role change (e.g. promotion to admin or a demotion)
|
||||||
|
// would otherwise not take effect until the user re-logs in.
|
||||||
|
ctx.state.user = { ...payload, role: user.role };
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,16 +64,16 @@ router.get("/ws", async (ctx) => {
|
|||||||
|
|
||||||
const socket = ctx.upgrade();
|
const socket = ctx.upgrade();
|
||||||
|
|
||||||
const avatarMime = authPayload
|
// Read the live user so the role reflects the DB, not the (possibly stale)
|
||||||
? getUserById(authPayload.userId).avatarMime
|
// role embedded in the 7-day JWT.
|
||||||
: undefined;
|
const user = authPayload ? getUserById(authPayload.userId) : undefined;
|
||||||
|
|
||||||
const client: WsClient = {
|
const client: WsClient = {
|
||||||
socket,
|
socket,
|
||||||
userId: authPayload?.userId,
|
userId: authPayload?.userId,
|
||||||
username: authPayload?.username,
|
username: authPayload?.username,
|
||||||
role: authPayload?.role,
|
role: user?.role,
|
||||||
avatarMime,
|
avatarMime: user?.avatarMime,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use addEventListener — more reliable than onopen= with Deno.serve
|
// Use addEventListener — more reliable than onopen= with Deno.serve
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
type ChatMessage,
|
type ChatMessage,
|
||||||
} from "../model/interfaces.ts";
|
} from "../model/interfaces.ts";
|
||||||
import { chatMessageRowToApi, db, isChatMessageRow } from "../model/db.ts";
|
import { chatMessageRowToApi, db, isChatMessageRow } from "../model/db.ts";
|
||||||
|
import { linkAttachments } from "./attachment-service.ts";
|
||||||
|
|
||||||
export const MAX_CHAT_LENGTH = 2000;
|
export const MAX_CHAT_LENGTH = 2000;
|
||||||
const DEFAULT_LIMIT = 50;
|
const DEFAULT_LIMIT = 50;
|
||||||
@@ -70,6 +71,9 @@ export function createChatMessage(
|
|||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO chat_messages (id, user_id, body, created_at, reply_to_id) VALUES (?, ?, ?, ?, ?);`,
|
`INSERT INTO chat_messages (id, user_id, body, created_at, reply_to_id) VALUES (?, ?, ?, ?, ?);`,
|
||||||
).run(id, userId, trimmed, createdAt, replyTo);
|
).run(id, userId, trimmed, createdAt, replyTo);
|
||||||
|
// Claim any pasted/dropped image attachments so the startup cleanup doesn't
|
||||||
|
// purge them as orphans (mirrors dumps/comments/playlists).
|
||||||
|
linkAttachments(trimmed, id);
|
||||||
return fetchMessage(id);
|
return fetchMessage(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +119,8 @@ export function updateChatMessage(
|
|||||||
}
|
}
|
||||||
db.prepare(`UPDATE chat_messages SET body = ?, updated_at = ? WHERE id = ?;`)
|
db.prepare(`UPDATE chat_messages SET body = ?, updated_at = ? WHERE id = ?;`)
|
||||||
.run(trimmed, new Date().toISOString(), id);
|
.run(trimmed, new Date().toISOString(), id);
|
||||||
|
// An edit may introduce a freshly pasted image; claim its attachment too.
|
||||||
|
linkAttachments(trimmed, id);
|
||||||
return fetchMessage(id);
|
return fetchMessage(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
119
src/App.css
119
src/App.css
@@ -40,7 +40,8 @@
|
|||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
.md ul, .md ol {
|
.md ul,
|
||||||
|
.md ol {
|
||||||
padding-left: 1.5em;
|
padding-left: 1.5em;
|
||||||
margin: 0.4em 0;
|
margin: 0.4em 0;
|
||||||
}
|
}
|
||||||
@@ -53,7 +54,12 @@
|
|||||||
padding: 0.2em 0.75em;
|
padding: 0.2em 0.75em;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
.md h1, .md h2, .md h3, .md h4, .md h5, .md h6 {
|
.md h1,
|
||||||
|
.md h2,
|
||||||
|
.md h3,
|
||||||
|
.md h4,
|
||||||
|
.md h5,
|
||||||
|
.md h6 {
|
||||||
margin: 0.6em 0 0.2em;
|
margin: 0.6em 0 0.2em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
@@ -466,17 +472,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── FileDropZone ── */
|
/* ── FileDropZone ── */
|
||||||
.fdz-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fdz-label {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fdz {
|
.fdz {
|
||||||
border: 2px dashed var(--color-border-subtle);
|
border: 2px dashed var(--color-border-subtle);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -1092,13 +1087,16 @@ body.has-player {
|
|||||||
edge. The lane width is `--fab-lane` (860px for most feeds; the journal
|
edge. The lane width is `--fab-lane` (860px for most feeds; the journal
|
||||||
grid is wider, see below). Falls back to a 1.25rem viewport gutter once
|
grid is wider, see below). Falls back to a 1.25rem viewport gutter once
|
||||||
that margin is too narrow to hold the button. */
|
that margin is too narrow to hold the button. */
|
||||||
right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) - 1rem));
|
right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) -
|
||||||
|
1rem));
|
||||||
/* Anchored to the bottom-right by default, but expressed via `top` (rather
|
/* Anchored to the bottom-right by default, but expressed via `top` (rather
|
||||||
than `bottom`) so the jump to the top-right when a player mounts can
|
than `bottom`) so the jump to the top-right when a player mounts can
|
||||||
animate as a slide. `dvh` tracks the visible viewport on mobile; `vh` is
|
animate as a slide. `dvh` tracks the visible viewport on mobile; `vh` is
|
||||||
the fallback for browsers without it. */
|
the fallback for browsers without it. */
|
||||||
top: calc(100vh - var(--fab-size) - 1.25rem - env(safe-area-inset-bottom, 0px));
|
top: calc(100vh - var(--fab-size) - 1.25rem - env(safe-area-inset-bottom,
|
||||||
top: calc(100dvh - var(--fab-size) - 1.25rem - env(safe-area-inset-bottom, 0px));
|
0px));
|
||||||
|
top: calc(100dvh - var(--fab-size) - 1.25rem - env(safe-area-inset-bottom,
|
||||||
|
0px));
|
||||||
z-index: 900;
|
z-index: 900;
|
||||||
width: var(--fab-size);
|
width: var(--fab-size);
|
||||||
height: var(--fab-size);
|
height: var(--fab-size);
|
||||||
@@ -1112,7 +1110,8 @@ body.has-player {
|
|||||||
background: var(--color-accent);
|
background: var(--color-accent);
|
||||||
color: var(--color-on-accent);
|
color: var(--color-on-accent);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 4px 16px color-mix(in srgb, var(--color-accent) 45%, transparent),
|
box-shadow: 0 4px 16px color-mix(in srgb, var(--color-accent) 45%,
|
||||||
|
transparent),
|
||||||
0 2px 6px rgba(0, 0, 0, 0.3);
|
0 2px 6px rgba(0, 0, 0, 0.3);
|
||||||
/* Hidden state: faded out, nudged down, non-interactive. */
|
/* Hidden state: faded out, nudged down, non-interactive. */
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -1136,7 +1135,8 @@ body.has-player {
|
|||||||
.dump-fab:hover {
|
.dump-fab:hover {
|
||||||
background: var(--color-accent-hover);
|
background: var(--color-accent-hover);
|
||||||
transform: translateY(-2px) scale(1);
|
transform: translateY(-2px) scale(1);
|
||||||
box-shadow: 0 6px 20px color-mix(in srgb, var(--color-accent) 55%, transparent),
|
box-shadow: 0 6px 20px color-mix(in srgb, var(--color-accent) 55%,
|
||||||
|
transparent),
|
||||||
0 2px 6px rgba(0, 0, 0, 0.3);
|
0 2px 6px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1169,10 +1169,13 @@ body.has-fab .page-content {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
--fab-size: 3.5rem;
|
--fab-size: 3.5rem;
|
||||||
/* Share the dump-fab's column. */
|
/* Share the dump-fab's column. */
|
||||||
right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) - 1rem));
|
right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) -
|
||||||
|
1rem));
|
||||||
/* One button-height + gap above the dump-fab's bottom-anchored resting spot. */
|
/* One button-height + gap above the dump-fab's bottom-anchored resting spot. */
|
||||||
top: calc(100vh - var(--fab-size) * 2 - 2rem - env(safe-area-inset-bottom, 0px));
|
top: calc(100vh - var(--fab-size) * 2 - 2rem - env(safe-area-inset-bottom,
|
||||||
top: calc(100dvh - var(--fab-size) * 2 - 2rem - env(safe-area-inset-bottom, 0px));
|
0px));
|
||||||
|
top: calc(100dvh - var(--fab-size) * 2 - 2rem - env(safe-area-inset-bottom,
|
||||||
|
0px));
|
||||||
z-index: 900;
|
z-index: 900;
|
||||||
width: var(--fab-size);
|
width: var(--fab-size);
|
||||||
height: var(--fab-size);
|
height: var(--fab-size);
|
||||||
@@ -1185,7 +1188,8 @@ body.has-fab .page-content {
|
|||||||
background: var(--color-accent);
|
background: var(--color-accent);
|
||||||
color: var(--color-on-accent);
|
color: var(--color-on-accent);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 4px 16px color-mix(in srgb, var(--color-accent) 45%, transparent),
|
box-shadow: 0 4px 16px color-mix(in srgb, var(--color-accent) 45%,
|
||||||
|
transparent),
|
||||||
0 2px 6px rgba(0, 0, 0, 0.3);
|
0 2px 6px rgba(0, 0, 0, 0.3);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(1rem) scale(0.9);
|
transform: translateY(1rem) scale(0.9);
|
||||||
@@ -1208,7 +1212,8 @@ body.has-fab .page-content {
|
|||||||
.chat-fab:hover {
|
.chat-fab:hover {
|
||||||
background: var(--color-accent-hover);
|
background: var(--color-accent-hover);
|
||||||
transform: translateY(-2px) scale(1);
|
transform: translateY(-2px) scale(1);
|
||||||
box-shadow: 0 6px 20px color-mix(in srgb, var(--color-accent) 55%, transparent),
|
box-shadow: 0 6px 20px color-mix(in srgb, var(--color-accent) 55%,
|
||||||
|
transparent),
|
||||||
0 2px 6px rgba(0, 0, 0, 0.3);
|
0 2px 6px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1220,8 +1225,10 @@ body.has-fab .page-content {
|
|||||||
body.has-player .chat-fab {
|
body.has-player .chat-fab {
|
||||||
top: calc(1.25rem + env(safe-area-inset-top, 0px));
|
top: calc(1.25rem + env(safe-area-inset-top, 0px));
|
||||||
/* Stack below the dump-fab in the top-right corner to avoid overlap. */
|
/* Stack below the dump-fab in the top-right corner to avoid overlap. */
|
||||||
right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) - 1rem));
|
right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) -
|
||||||
top: calc(1.25rem + var(--fab-size) + 0.75rem + env(safe-area-inset-top, 0px));
|
1rem));
|
||||||
|
top: calc(1.25rem + var(--fab-size) + 0.75rem + env(safe-area-inset-top,
|
||||||
|
0px));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The badge sits on the circular FAB; nudge it onto the rim. */
|
/* The badge sits on the circular FAB; nudge it onto the rim. */
|
||||||
@@ -2990,7 +2997,8 @@ body.has-player .chat-fab {
|
|||||||
.dump-card-inner:hover .dump-card-preview,
|
.dump-card-inner:hover .dump-card-preview,
|
||||||
.playlist-card-inner:hover .playlist-card-preview {
|
.playlist-card-inner:hover .playlist-card-preview {
|
||||||
transform: scale(1.08);
|
transform: scale(1.08);
|
||||||
box-shadow: inset 0 0 0 1px var(--color-border), 0 4px 12px rgba(0, 0, 0, 0.25);
|
box-shadow: inset 0 0 0 1px var(--color-border), 0 4px 12px rgba(0, 0, 0,
|
||||||
|
0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Shared card preview icon ── */
|
/* ── Shared card preview icon ── */
|
||||||
@@ -3681,7 +3689,9 @@ body.has-player .chat-fab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes chat-typing-bounce {
|
@keyframes chat-typing-bounce {
|
||||||
0%, 60%, 100% {
|
0%,
|
||||||
|
60%,
|
||||||
|
100% {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
@@ -3825,7 +3835,6 @@ body.has-player .chat-fab {
|
|||||||
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
|
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ── Playlist detail page ── */
|
/* ── Playlist detail page ── */
|
||||||
.playlist-detail-header {
|
.playlist-detail-header {
|
||||||
background: var(--color-surface);
|
background: var(--color-surface);
|
||||||
@@ -4092,6 +4101,60 @@ body.has-player .chat-fab {
|
|||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Backlinks are supporting context, not the main event — render them as a
|
||||||
|
tighter, lighter list so they stay scannable but clearly subordinate to
|
||||||
|
the dump's own content. */
|
||||||
|
.related-section .related-section-title {
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-section .dump-feed {
|
||||||
|
padding: 0;
|
||||||
|
gap: 0.4rem;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-section .dump-card {
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
opacity: 0.78;
|
||||||
|
transition: border-color 0.15s, opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-section .dump-card:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-section .dump-card-inner {
|
||||||
|
padding: 0.45rem 0.65rem;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-section .dump-card-preview {
|
||||||
|
width: 76px;
|
||||||
|
height: 43px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related-section .dump-card-title {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep just enough description to orient, but only a single muted line. */
|
||||||
|
.related-section .dump-card-comment {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
line-clamp: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 512px) {
|
||||||
|
.related-section .dump-card-preview {
|
||||||
|
width: 64px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.comment-list {
|
.comment-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { Controller } from "react-hook-form";
|
|
||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
|
|
||||||
@@ -12,10 +11,7 @@ import type {
|
|||||||
RawDump,
|
RawDump,
|
||||||
RawPlaylistMembership,
|
RawPlaylistMembership,
|
||||||
} from "../model.ts";
|
} from "../model.ts";
|
||||||
import {
|
import { deserializeDump, deserializePlaylistMembership } from "../model.ts";
|
||||||
deserializeDump,
|
|
||||||
deserializePlaylistMembership,
|
|
||||||
} from "../model.ts";
|
|
||||||
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 { dumpUrl, normalizeUrl } from "../utils/urls.ts";
|
import { dumpUrl, normalizeUrl } from "../utils/urls.ts";
|
||||||
@@ -23,12 +19,12 @@ import { MAX_FILE_SIZE } from "../config/upload.ts";
|
|||||||
import RichContentCard from "./RichContentCard.tsx";
|
import RichContentCard from "./RichContentCard.tsx";
|
||||||
import { MediaPlayer } from "./MediaPlayer.tsx";
|
import { MediaPlayer } from "./MediaPlayer.tsx";
|
||||||
import type { RichContent } from "../model.ts";
|
import type { RichContent } from "../model.ts";
|
||||||
import { FileDropZone } from "./FileDropZone.tsx";
|
|
||||||
import { Modal } from "./Modal.tsx";
|
import { Modal } from "./Modal.tsx";
|
||||||
import { CategorySelect } from "./CategorySelect.tsx";
|
import { CategorySelect } from "./CategorySelect.tsx";
|
||||||
import { PlaylistMembershipPanel } from "./PlaylistMembershipPanel.tsx";
|
import { PlaylistMembershipPanel } from "./PlaylistMembershipPanel.tsx";
|
||||||
import {
|
import {
|
||||||
expectOk,
|
expectOk,
|
||||||
|
FileField,
|
||||||
FormActions,
|
FormActions,
|
||||||
FormError,
|
FormError,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
@@ -113,7 +109,7 @@ export function DumpCreateModal(
|
|||||||
isPublic: true,
|
isPublic: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { register, setValue, watch, clearErrors, control } = form;
|
const { register, setValue, watch, clearErrors } = form;
|
||||||
const submitting = form.formState.isSubmitting;
|
const submitting = form.formState.isSubmitting;
|
||||||
const url = watch("url");
|
const url = watch("url");
|
||||||
const file = watch("file");
|
const file = watch("file");
|
||||||
@@ -381,16 +377,10 @@ export function DumpCreateModal(
|
|||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
<Controller
|
<FileField<CreateValues>
|
||||||
name="file"
|
name="file"
|
||||||
control={control}
|
onValueChange={selectFile}
|
||||||
render={({ field }) => (
|
disabled={submitting}
|
||||||
<FileDropZone
|
|
||||||
file={field.value}
|
|
||||||
onChange={selectFile}
|
|
||||||
disabled={submitting}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
{file && <LocalFilePreview file={file} />}
|
{file && <LocalFilePreview file={file} />}
|
||||||
{file && (
|
{file && (
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ interface FileDropZoneProps {
|
|||||||
file: File | null;
|
file: File | null;
|
||||||
onChange: (file: File | null) => void;
|
onChange: (file: File | null) => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
label?: string;
|
|
||||||
hint?: string;
|
hint?: string;
|
||||||
showLimit?: boolean;
|
showLimit?: boolean;
|
||||||
}
|
}
|
||||||
@@ -24,11 +23,9 @@ export function FileDropZone({
|
|||||||
file,
|
file,
|
||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
label,
|
|
||||||
hint,
|
hint,
|
||||||
showLimit = true,
|
showLimit = true,
|
||||||
}: FileDropZoneProps) {
|
}: FileDropZoneProps) {
|
||||||
const resolvedLabel = label ?? t`File`;
|
|
||||||
const resolvedHint = hint ?? t`Drop a file here`;
|
const resolvedHint = hint ?? t`Drop a file here`;
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
@@ -72,82 +69,79 @@ export function FileDropZone({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fdz-wrapper">
|
<div
|
||||||
{resolvedLabel && <span className="fdz-label">{resolvedLabel}</span>}
|
className={`fdz${dragging ? " fdz--drag" : ""}${
|
||||||
<div
|
disabled ? " fdz--disabled" : ""
|
||||||
className={`fdz${dragging ? " fdz--drag" : ""}${
|
}${file ? " fdz--filled" : ""}`}
|
||||||
disabled ? " fdz--disabled" : ""
|
onDragOver={handleDragOver}
|
||||||
}${file ? " fdz--filled" : ""}`}
|
onDragLeave={handleDragLeave}
|
||||||
onDragOver={handleDragOver}
|
onDrop={handleDrop}
|
||||||
onDragLeave={handleDragLeave}
|
onClick={file ? undefined : handleClick}
|
||||||
onDrop={handleDrop}
|
role={file ? undefined : "button"}
|
||||||
onClick={file ? undefined : handleClick}
|
tabIndex={file || disabled ? undefined : 0}
|
||||||
role={file ? undefined : "button"}
|
onKeyDown={file || disabled ? undefined : (e) => {
|
||||||
tabIndex={file || disabled ? undefined : 0}
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
onKeyDown={file || disabled ? undefined : (e) => {
|
e.preventDefault();
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
handleClick();
|
||||||
e.preventDefault();
|
}
|
||||||
handleClick();
|
}}
|
||||||
}
|
>
|
||||||
}}
|
<input
|
||||||
>
|
ref={inputRef}
|
||||||
<input
|
type="file"
|
||||||
ref={inputRef}
|
onChange={(e) => onChange(e.target.files?.[0] ?? null)}
|
||||||
type="file"
|
disabled={disabled}
|
||||||
onChange={(e) => onChange(e.target.files?.[0] ?? null)}
|
style={{ display: "none" }}
|
||||||
disabled={disabled}
|
/>
|
||||||
style={{ display: "none" }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{file
|
{file
|
||||||
? (
|
? (
|
||||||
<div className="fdz__file">
|
<div className="fdz__file">
|
||||||
<span className="fdz__file-icon">{fileIcon(file.type)}</span>
|
<span className="fdz__file-icon">{fileIcon(file.type)}</span>
|
||||||
<div className="fdz__file-meta">
|
<div className="fdz__file-meta">
|
||||||
<span className="fdz__file-name">{file.name}</span>
|
<span className="fdz__file-name">{file.name}</span>
|
||||||
<span className="fdz__file-size">{formatBytes(file.size)}</span>
|
<span className="fdz__file-size">{formatBytes(file.size)}</span>
|
||||||
</div>
|
|
||||||
{!disabled && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="fdz__clear"
|
|
||||||
onClick={handleClear}
|
|
||||||
aria-label={t`Remove file`}
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
{!disabled && (
|
||||||
: (
|
<button
|
||||||
<div className="fdz__empty">
|
type="button"
|
||||||
<svg
|
className="fdz__clear"
|
||||||
className="fdz__upload-icon"
|
onClick={handleClear}
|
||||||
viewBox="0 0 24 24"
|
aria-label={t`Remove file`}
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
>
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
✕
|
||||||
<polyline points="17 8 12 3 7 8" />
|
</button>
|
||||||
<line x1="12" y1="3" x2="12" y2="15" />
|
)}
|
||||||
</svg>
|
</div>
|
||||||
<p className="fdz__hint">{resolvedHint}</p>
|
)
|
||||||
<p className="fdz__browse">
|
: (
|
||||||
<Trans>
|
<div className="fdz__empty">
|
||||||
or <span className="fdz__browse-link">browse files</span>
|
<svg
|
||||||
</Trans>
|
className="fdz__upload-icon"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||||
|
<polyline points="17 8 12 3 7 8" />
|
||||||
|
<line x1="12" y1="3" x2="12" y2="15" />
|
||||||
|
</svg>
|
||||||
|
<p className="fdz__hint">{resolvedHint}</p>
|
||||||
|
<p className="fdz__browse">
|
||||||
|
<Trans>
|
||||||
|
or <span className="fdz__browse-link">browse files</span>
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
{showLimit && (
|
||||||
|
<p className="fdz__limit">
|
||||||
|
<Trans>Max 50 MB</Trans>
|
||||||
</p>
|
</p>
|
||||||
{showLimit && (
|
)}
|
||||||
<p className="fdz__limit">
|
</div>
|
||||||
<Trans>Max 50 MB</Trans>
|
)}
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ interface FileFieldProps<T extends FieldValues> {
|
|||||||
showLimit?: boolean;
|
showLimit?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
rules?: RegisterOptions<T, Path<T>>;
|
rules?: RegisterOptions<T, Path<T>>;
|
||||||
|
/** Side effect run after the field value changes (e.g. derive a title). */
|
||||||
|
onValueChange?: (file: File | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `Controller`-wrapped {@link FileDropZone} bound to react-hook-form. */
|
/** `Controller`-wrapped {@link FileDropZone} bound to react-hook-form. */
|
||||||
@@ -25,12 +27,14 @@ export function FileField<T extends FieldValues>({
|
|||||||
showLimit,
|
showLimit,
|
||||||
disabled,
|
disabled,
|
||||||
rules,
|
rules,
|
||||||
|
onValueChange,
|
||||||
}: FileFieldProps<T>) {
|
}: FileFieldProps<T>) {
|
||||||
const { control, formState: { errors } } = useFormContext<T>();
|
const { control, formState: { errors } } = useFormContext<T>();
|
||||||
const error = errors[name];
|
const error = errors[name];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
|
{label && <span className="form-label">{label}</span>}
|
||||||
<Controller
|
<Controller
|
||||||
name={name}
|
name={name}
|
||||||
control={control}
|
control={control}
|
||||||
@@ -38,8 +42,10 @@ export function FileField<T extends FieldValues>({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FileDropZone
|
<FileDropZone
|
||||||
file={(field.value as File | null) ?? null}
|
file={(field.value as File | null) ?? null}
|
||||||
onChange={field.onChange}
|
onChange={(file) => {
|
||||||
label={label}
|
field.onChange(file);
|
||||||
|
onValueChange?.(file);
|
||||||
|
}}
|
||||||
hint={hint}
|
hint={hint}
|
||||||
showLimit={showLimit}
|
showLimit={showLimit}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { type ReactNode, useEffect, useState } from "react";
|
|||||||
|
|
||||||
import { AuthContext, type AuthContextValue } from "./AuthContext.ts";
|
import { AuthContext, type AuthContextValue } from "./AuthContext.ts";
|
||||||
|
|
||||||
import { type AuthResponse, deserializeAuthResponse } from "../model.ts";
|
import {
|
||||||
|
type AuthResponse,
|
||||||
|
deserializeAuthResponse,
|
||||||
|
deserializePublicUser,
|
||||||
|
} from "../model.ts";
|
||||||
import { API_URL } from "../config/api.ts";
|
import { API_URL } from "../config/api.ts";
|
||||||
|
|
||||||
function isTokenExpired(token: string): boolean {
|
function isTokenExpired(token: string): boolean {
|
||||||
@@ -60,6 +64,23 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
localStorage.removeItem("authResponse");
|
localStorage.removeItem("authResponse");
|
||||||
setAuthResponse(null);
|
setAuthResponse(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The stored user is a snapshot from login time; the role (and other
|
||||||
|
// profile fields) can change server-side — e.g. an admin promoting this
|
||||||
|
// user. Sync from the authoritative /me response so the UI reflects role
|
||||||
|
// changes without forcing a logout/login cycle.
|
||||||
|
if (res.ok) {
|
||||||
|
const json = await res.json();
|
||||||
|
if (json?.success && json.data) {
|
||||||
|
const freshUser = deserializePublicUser(json.data);
|
||||||
|
setAuthResponse((prev) => {
|
||||||
|
if (!prev || prev.user.role === freshUser.role) return prev;
|
||||||
|
const updated = { ...prev, user: freshUser };
|
||||||
|
localStorage.setItem("authResponse", JSON.stringify(updated));
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Network error — don't log out an offline user; only an explicit 401 clears the session.
|
// Network error — don't log out an offline user; only an explicit 401 clears the session.
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ msgstr "← Back"
|
|||||||
|
|
||||||
#: src/pages/Dump.tsx:264
|
#: src/pages/Dump.tsx:264
|
||||||
#: src/pages/Dump.tsx:494
|
#: src/pages/Dump.tsx:494
|
||||||
#: src/pages/DumpEdit.tsx:179
|
#: src/pages/DumpEdit.tsx:181
|
||||||
msgid "← Back to all dumps"
|
msgid "← Back to all dumps"
|
||||||
msgstr "← Back to all dumps"
|
msgstr "← Back to all dumps"
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ msgid "Add email…"
|
|||||||
msgstr "Add email…"
|
msgstr "Add email…"
|
||||||
|
|
||||||
#: src/components/AddToPlaylistModal.tsx:64
|
#: src/components/AddToPlaylistModal.tsx:64
|
||||||
#: src/components/DumpCreateModal.tsx:305
|
#: src/components/DumpCreateModal.tsx:301
|
||||||
msgid "Add to playlist"
|
msgid "Add to playlist"
|
||||||
msgstr "Add to playlist"
|
msgstr "Add to playlist"
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ msgstr "Can't connect to the live updates server. Upvotes and notifications may
|
|||||||
#: src/components/ConfirmModal.tsx:32
|
#: src/components/ConfirmModal.tsx:32
|
||||||
#: src/components/form/FormActions.tsx:32
|
#: src/components/form/FormActions.tsx:32
|
||||||
#: src/pages/Dump.tsx:376
|
#: src/pages/Dump.tsx:376
|
||||||
#: src/pages/DumpEdit.tsx:463
|
#: src/pages/DumpEdit.tsx:460
|
||||||
#: src/pages/PlaylistDetail.tsx:920
|
#: src/pages/PlaylistDetail.tsx:920
|
||||||
#: src/pages/UserPublicProfile.tsx:1674
|
#: src/pages/UserPublicProfile.tsx:1674
|
||||||
#: src/pages/UserPublicProfile.tsx:1744
|
#: src/pages/UserPublicProfile.tsx:1744
|
||||||
@@ -291,7 +291,7 @@ msgstr "Could not change password"
|
|||||||
msgid "Could not load."
|
msgid "Could not load."
|
||||||
msgstr "Could not load."
|
msgstr "Could not load."
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:359
|
#: src/pages/DumpEdit.tsx:361
|
||||||
msgid "Could not save"
|
msgid "Could not save"
|
||||||
msgstr "Could not save"
|
msgstr "Could not save"
|
||||||
|
|
||||||
@@ -346,8 +346,8 @@ msgstr "Delete category"
|
|||||||
msgid "Delete category \"{0}\"? This cannot be undone."
|
msgid "Delete category \"{0}\"? This cannot be undone."
|
||||||
msgstr "Delete category \"{0}\"? This cannot be undone."
|
msgstr "Delete category \"{0}\"? This cannot be undone."
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:253
|
#: src/pages/DumpEdit.tsx:255
|
||||||
#: src/pages/DumpEdit.tsx:459
|
#: src/pages/DumpEdit.tsx:456
|
||||||
msgid "Delete dump"
|
msgid "Delete dump"
|
||||||
msgstr "Delete dump"
|
msgstr "Delete dump"
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ msgstr "Delete playlist"
|
|||||||
msgid "Delete this comment?"
|
msgid "Delete this comment?"
|
||||||
msgstr "Delete this comment?"
|
msgstr "Delete this comment?"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:252
|
#: src/pages/DumpEdit.tsx:254
|
||||||
msgid "Delete this dump? This cannot be undone."
|
msgid "Delete this dump? This cannot be undone."
|
||||||
msgstr "Delete this dump? This cannot be undone."
|
msgstr "Delete this dump? This cannot be undone."
|
||||||
|
|
||||||
@@ -383,15 +383,15 @@ msgstr "deleted message"
|
|||||||
msgid "Description (optional)"
|
msgid "Description (optional)"
|
||||||
msgstr "Description (optional)"
|
msgstr "Description (optional)"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:498
|
#: src/components/DumpCreateModal.tsx:488
|
||||||
msgid "Done"
|
msgid "Done"
|
||||||
msgstr "Done"
|
msgstr "Done"
|
||||||
|
|
||||||
#: src/components/FileDropZone.tsx:32
|
#: src/components/FileDropZone.tsx:29
|
||||||
msgid "Drop a file here"
|
msgid "Drop a file here"
|
||||||
msgstr "Drop a file here"
|
msgstr "Drop a file here"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:429
|
#: src/pages/DumpEdit.tsx:428
|
||||||
msgid "Drop a replacement here"
|
msgid "Drop a replacement here"
|
||||||
msgstr "Drop a replacement here"
|
msgstr "Drop a replacement here"
|
||||||
|
|
||||||
@@ -399,11 +399,11 @@ msgstr "Drop a replacement here"
|
|||||||
msgid "Dump"
|
msgid "Dump"
|
||||||
msgstr "Dump"
|
msgstr "Dump"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:468
|
#: src/components/DumpCreateModal.tsx:458
|
||||||
msgid "Dump it"
|
msgid "Dump it"
|
||||||
msgstr "Dump it"
|
msgstr "Dump it"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:479
|
#: src/components/DumpCreateModal.tsx:469
|
||||||
msgid "Dumped!"
|
msgid "Dumped!"
|
||||||
msgstr "Dumped!"
|
msgstr "Dumped!"
|
||||||
|
|
||||||
@@ -436,7 +436,7 @@ msgstr "Edit"
|
|||||||
msgid "Edit {0}"
|
msgid "Edit {0}"
|
||||||
msgstr "Edit {0}"
|
msgstr "Edit {0}"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:430
|
#: src/components/DumpCreateModal.tsx:420
|
||||||
msgid "Edit title"
|
msgid "Edit title"
|
||||||
msgstr "Edit title"
|
msgstr "Edit title"
|
||||||
|
|
||||||
@@ -460,12 +460,12 @@ msgstr "edited {0}"
|
|||||||
msgid "Edited {0}"
|
msgid "Edited {0}"
|
||||||
msgstr "Edited {0}"
|
msgstr "Edited {0}"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:204
|
#: src/pages/DumpEdit.tsx:206
|
||||||
msgid "Editing"
|
msgid "Editing"
|
||||||
msgstr "Editing"
|
msgstr "Editing"
|
||||||
|
|
||||||
#. placeholder {0}: state.dump.title
|
#. placeholder {0}: state.dump.title
|
||||||
#: src/pages/DumpEdit.tsx:49
|
#: src/pages/DumpEdit.tsx:48
|
||||||
msgid "Editing {0}"
|
msgid "Editing {0}"
|
||||||
msgstr "Editing {0}"
|
msgstr "Editing {0}"
|
||||||
|
|
||||||
@@ -506,7 +506,7 @@ msgstr "Failed to generate invite"
|
|||||||
msgid "Failed to load"
|
msgid "Failed to load"
|
||||||
msgstr "Failed to load"
|
msgstr "Failed to load"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:341
|
#: src/components/DumpCreateModal.tsx:337
|
||||||
msgid "Failed to post"
|
msgid "Failed to post"
|
||||||
msgstr "Failed to post"
|
msgstr "Failed to post"
|
||||||
|
|
||||||
@@ -544,20 +544,19 @@ msgstr "Failed to update role"
|
|||||||
msgid "Feeds"
|
msgid "Feeds"
|
||||||
msgstr "Feeds"
|
msgstr "Feeds"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:373
|
#: src/components/DumpCreateModal.tsx:369
|
||||||
msgid "Fetching preview…"
|
msgid "Fetching preview…"
|
||||||
msgstr "Fetching preview…"
|
msgstr "Fetching preview…"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:465
|
#: src/components/DumpCreateModal.tsx:455
|
||||||
msgid "Fetching…"
|
msgid "Fetching…"
|
||||||
msgstr "Fetching…"
|
msgstr "Fetching…"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:335
|
#: src/components/DumpCreateModal.tsx:331
|
||||||
#: src/components/FileDropZone.tsx:31
|
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr "File"
|
msgstr "File"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:243
|
#: src/components/DumpCreateModal.tsx:239
|
||||||
msgid "File too large (max 50 MB)."
|
msgid "File too large (max 50 MB)."
|
||||||
msgstr "File too large (max 50 MB)."
|
msgstr "File too large (max 50 MB)."
|
||||||
|
|
||||||
@@ -681,7 +680,7 @@ msgid "Load older messages"
|
|||||||
msgstr "Load older messages"
|
msgstr "Load older messages"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:240
|
#: src/pages/Dump.tsx:240
|
||||||
#: src/pages/DumpEdit.tsx:155
|
#: src/pages/DumpEdit.tsx:157
|
||||||
msgid "Loading dump…"
|
msgid "Loading dump…"
|
||||||
msgstr "Loading dump…"
|
msgstr "Loading dump…"
|
||||||
|
|
||||||
@@ -758,7 +757,7 @@ msgstr "Login failed"
|
|||||||
msgid "Manage"
|
msgid "Manage"
|
||||||
msgstr "Manage"
|
msgstr "Manage"
|
||||||
|
|
||||||
#: src/components/FileDropZone.tsx:145
|
#: src/components/FileDropZone.tsx:140
|
||||||
msgid "Max 50 MB"
|
msgid "Max 50 MB"
|
||||||
msgstr "Max 50 MB"
|
msgstr "Max 50 MB"
|
||||||
|
|
||||||
@@ -780,7 +779,7 @@ msgstr "new"
|
|||||||
msgid "New"
|
msgid "New"
|
||||||
msgstr "New"
|
msgstr "New"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:305
|
#: src/components/DumpCreateModal.tsx:301
|
||||||
#: src/components/DumpFab.tsx:65
|
#: src/components/DumpFab.tsx:65
|
||||||
#: src/components/DumpFab.tsx:66
|
#: src/components/DumpFab.tsx:66
|
||||||
#: src/pages/UserDumps.tsx:88
|
#: src/pages/UserDumps.tsx:88
|
||||||
@@ -873,7 +872,7 @@ msgstr "Notifications ({unreadNotificationCount} unread)"
|
|||||||
msgid "Open search"
|
msgid "Open search"
|
||||||
msgstr "Open search"
|
msgstr "Open search"
|
||||||
|
|
||||||
#: src/components/FileDropZone.tsx:139
|
#: src/components/FileDropZone.tsx:134
|
||||||
msgid "or <0>browse files</0>"
|
msgid "or <0>browse files</0>"
|
||||||
msgstr "or <0>browse files</0>"
|
msgstr "or <0>browse files</0>"
|
||||||
|
|
||||||
@@ -922,7 +921,7 @@ msgstr "Playlists"
|
|||||||
msgid "Playlists ({0}{1})"
|
msgid "Playlists ({0}{1})"
|
||||||
msgstr "Playlists ({0}{1})"
|
msgstr "Playlists ({0}{1})"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:240
|
#: src/components/DumpCreateModal.tsx:236
|
||||||
msgid "Please select a file."
|
msgid "Please select a file."
|
||||||
msgstr "Please select a file."
|
msgstr "Please select a file."
|
||||||
|
|
||||||
@@ -963,11 +962,11 @@ msgstr "public"
|
|||||||
msgid "Public"
|
msgid "Public"
|
||||||
msgstr "Public"
|
msgstr "Public"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:233
|
#: src/pages/DumpEdit.tsx:235
|
||||||
msgid "Refresh metadata"
|
msgid "Refresh metadata"
|
||||||
msgstr "Refresh metadata"
|
msgstr "Refresh metadata"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:232
|
#: src/pages/DumpEdit.tsx:234
|
||||||
msgid "Refreshing…"
|
msgid "Refreshing…"
|
||||||
msgstr "Refreshing…"
|
msgstr "Refreshing…"
|
||||||
|
|
||||||
@@ -989,7 +988,7 @@ msgstr "Registration failed"
|
|||||||
msgid "Related"
|
msgid "Related"
|
||||||
msgstr "Related"
|
msgstr "Related"
|
||||||
|
|
||||||
#: src/components/FileDropZone.tsx:115
|
#: src/components/FileDropZone.tsx:110
|
||||||
msgid "Remove file"
|
msgid "Remove file"
|
||||||
msgstr "Remove file"
|
msgstr "Remove file"
|
||||||
|
|
||||||
@@ -1005,7 +1004,7 @@ msgstr "Remove like"
|
|||||||
msgid "Remove vote"
|
msgid "Remove vote"
|
||||||
msgstr "Remove vote"
|
msgstr "Remove vote"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:428
|
#: src/pages/DumpEdit.tsx:420
|
||||||
msgid "Replace file"
|
msgid "Replace file"
|
||||||
msgstr "Replace file"
|
msgstr "Replace file"
|
||||||
|
|
||||||
@@ -1031,13 +1030,13 @@ msgstr "Reset failed"
|
|||||||
msgid "Reset password"
|
msgid "Reset password"
|
||||||
msgstr "Reset password"
|
msgstr "Reset password"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:379
|
#: src/pages/DumpEdit.tsx:381
|
||||||
#: src/pages/DumpEdit.tsx:397
|
#: src/pages/DumpEdit.tsx:399
|
||||||
msgid "Reset to default"
|
msgid "Reset to default"
|
||||||
msgstr "Reset to default"
|
msgstr "Reset to default"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:257
|
#: src/pages/Dump.tsx:257
|
||||||
#: src/pages/DumpEdit.tsx:172
|
#: src/pages/DumpEdit.tsx:174
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr "Retry"
|
msgstr "Retry"
|
||||||
|
|
||||||
@@ -1048,7 +1047,7 @@ msgstr "Role"
|
|||||||
#: src/components/ChatModal.tsx:222
|
#: src/components/ChatModal.tsx:222
|
||||||
#: src/components/CommentThread.tsx:328
|
#: src/components/CommentThread.tsx:328
|
||||||
#: src/pages/Dump.tsx:368
|
#: src/pages/Dump.tsx:368
|
||||||
#: src/pages/DumpEdit.tsx:466
|
#: src/pages/DumpEdit.tsx:463
|
||||||
#: src/pages/PlaylistDetail.tsx:927
|
#: src/pages/PlaylistDetail.tsx:927
|
||||||
#: src/pages/UserPublicProfile.tsx:1666
|
#: src/pages/UserPublicProfile.tsx:1666
|
||||||
#: src/pages/UserPublicProfile.tsx:1736
|
#: src/pages/UserPublicProfile.tsx:1736
|
||||||
@@ -1149,17 +1148,17 @@ msgstr "This page does not exist."
|
|||||||
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."
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:363
|
#: src/pages/DumpEdit.tsx:365
|
||||||
msgid "Thumbnail"
|
msgid "Thumbnail"
|
||||||
msgstr "Thumbnail"
|
msgstr "Thumbnail"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:399
|
#: src/components/DumpCreateModal.tsx:389
|
||||||
#: src/components/PlaylistCreateForm.tsx:70
|
#: src/components/PlaylistCreateForm.tsx:70
|
||||||
#: src/pages/DumpEdit.tsx:387
|
#: src/pages/DumpEdit.tsx:389
|
||||||
msgid "Title"
|
msgid "Title"
|
||||||
msgstr "Title"
|
msgstr "Title"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:241
|
#: src/components/DumpCreateModal.tsx:237
|
||||||
msgid "Title is required."
|
msgid "Title is required."
|
||||||
msgstr "Title is required."
|
msgstr "Title is required."
|
||||||
|
|
||||||
@@ -1188,7 +1187,7 @@ msgstr "Unfollow playlist"
|
|||||||
msgid "Upload failed"
|
msgid "Upload failed"
|
||||||
msgstr "Upload failed"
|
msgstr "Upload failed"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:466
|
#: src/components/DumpCreateModal.tsx:456
|
||||||
msgid "Uploading…"
|
msgid "Uploading…"
|
||||||
msgstr "Uploading…"
|
msgstr "Uploading…"
|
||||||
|
|
||||||
@@ -1206,12 +1205,12 @@ msgstr "Upvoted"
|
|||||||
msgid "Upvoted ({0}{1})"
|
msgid "Upvoted ({0}{1})"
|
||||||
msgstr "Upvoted ({0}{1})"
|
msgstr "Upvoted ({0}{1})"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:348
|
#: src/components/DumpCreateModal.tsx:344
|
||||||
#: src/pages/DumpEdit.tsx:410
|
#: src/pages/DumpEdit.tsx:412
|
||||||
msgid "URL"
|
msgid "URL"
|
||||||
msgstr "URL"
|
msgstr "URL"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:227
|
#: src/components/DumpCreateModal.tsx:223
|
||||||
msgid "URL is required."
|
msgid "URL is required."
|
||||||
msgstr "URL is required."
|
msgstr "URL is required."
|
||||||
|
|
||||||
@@ -1248,12 +1247,12 @@ msgstr "Users"
|
|||||||
msgid "View all →"
|
msgid "View all →"
|
||||||
msgstr "View all →"
|
msgstr "View all →"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:481
|
#: src/components/DumpCreateModal.tsx:471
|
||||||
msgid "View dump →"
|
msgid "View dump →"
|
||||||
msgstr "View dump →"
|
msgstr "View dump →"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:444
|
#: src/components/DumpCreateModal.tsx:434
|
||||||
#: src/pages/DumpEdit.tsx:440
|
#: src/pages/DumpEdit.tsx:437
|
||||||
msgid "What makes it worth it?"
|
msgid "What makes it worth it?"
|
||||||
msgstr "What makes it worth it?"
|
msgstr "What makes it worth it?"
|
||||||
|
|
||||||
@@ -1262,8 +1261,8 @@ msgstr "What makes it worth it?"
|
|||||||
msgid "Who am I?"
|
msgid "Who am I?"
|
||||||
msgstr "Who am I?"
|
msgstr "Who am I?"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:443
|
#: src/components/DumpCreateModal.tsx:433
|
||||||
#: src/pages/DumpEdit.tsx:439
|
#: src/pages/DumpEdit.tsx:436
|
||||||
msgid "Why?"
|
msgid "Why?"
|
||||||
msgstr "Why?"
|
msgstr "Why?"
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ msgstr "← Retour"
|
|||||||
|
|
||||||
#: src/pages/Dump.tsx:264
|
#: src/pages/Dump.tsx:264
|
||||||
#: src/pages/Dump.tsx:494
|
#: src/pages/Dump.tsx:494
|
||||||
#: src/pages/DumpEdit.tsx:179
|
#: src/pages/DumpEdit.tsx:181
|
||||||
msgid "← Back to all dumps"
|
msgid "← Back to all dumps"
|
||||||
msgstr "← Retour à toutes les recos"
|
msgstr "← Retour à toutes les recos"
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ msgid "Add email…"
|
|||||||
msgstr "Ajouter un e-mail…"
|
msgstr "Ajouter un e-mail…"
|
||||||
|
|
||||||
#: src/components/AddToPlaylistModal.tsx:64
|
#: src/components/AddToPlaylistModal.tsx:64
|
||||||
#: src/components/DumpCreateModal.tsx:305
|
#: src/components/DumpCreateModal.tsx:301
|
||||||
msgid "Add to playlist"
|
msgid "Add to playlist"
|
||||||
msgstr "Ajouter à la collection"
|
msgstr "Ajouter à la collection"
|
||||||
|
|
||||||
@@ -203,7 +203,7 @@ msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les vo
|
|||||||
#: src/components/ConfirmModal.tsx:32
|
#: src/components/ConfirmModal.tsx:32
|
||||||
#: src/components/form/FormActions.tsx:32
|
#: src/components/form/FormActions.tsx:32
|
||||||
#: src/pages/Dump.tsx:376
|
#: src/pages/Dump.tsx:376
|
||||||
#: src/pages/DumpEdit.tsx:463
|
#: src/pages/DumpEdit.tsx:460
|
||||||
#: src/pages/PlaylistDetail.tsx:920
|
#: src/pages/PlaylistDetail.tsx:920
|
||||||
#: src/pages/UserPublicProfile.tsx:1674
|
#: src/pages/UserPublicProfile.tsx:1674
|
||||||
#: src/pages/UserPublicProfile.tsx:1744
|
#: src/pages/UserPublicProfile.tsx:1744
|
||||||
@@ -291,7 +291,7 @@ msgstr "Impossible de changer le mot de passe"
|
|||||||
msgid "Could not load."
|
msgid "Could not load."
|
||||||
msgstr "Impossible de charger."
|
msgstr "Impossible de charger."
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:359
|
#: src/pages/DumpEdit.tsx:361
|
||||||
msgid "Could not save"
|
msgid "Could not save"
|
||||||
msgstr "Sauvegarde impossible"
|
msgstr "Sauvegarde impossible"
|
||||||
|
|
||||||
@@ -346,8 +346,8 @@ msgstr "Supprimer la catégorie"
|
|||||||
msgid "Delete category \"{0}\"? This cannot be undone."
|
msgid "Delete category \"{0}\"? This cannot be undone."
|
||||||
msgstr "Supprimer la catégorie \"{0}\" ? Cette action est irréversible."
|
msgstr "Supprimer la catégorie \"{0}\" ? Cette action est irréversible."
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:253
|
#: src/pages/DumpEdit.tsx:255
|
||||||
#: src/pages/DumpEdit.tsx:459
|
#: src/pages/DumpEdit.tsx:456
|
||||||
msgid "Delete dump"
|
msgid "Delete dump"
|
||||||
msgstr "Supprimer la reco"
|
msgstr "Supprimer la reco"
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ msgstr "Supprimer la collection"
|
|||||||
msgid "Delete this comment?"
|
msgid "Delete this comment?"
|
||||||
msgstr "Supprimer ce commentaire ?"
|
msgstr "Supprimer ce commentaire ?"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:252
|
#: src/pages/DumpEdit.tsx:254
|
||||||
msgid "Delete this dump? This cannot be undone."
|
msgid "Delete this dump? This cannot be undone."
|
||||||
msgstr "Supprimer cette reco ? Cette action est irréversible."
|
msgstr "Supprimer cette reco ? Cette action est irréversible."
|
||||||
|
|
||||||
@@ -383,15 +383,15 @@ msgstr "message supprimé"
|
|||||||
msgid "Description (optional)"
|
msgid "Description (optional)"
|
||||||
msgstr "Description (facultatif)"
|
msgstr "Description (facultatif)"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:498
|
#: src/components/DumpCreateModal.tsx:488
|
||||||
msgid "Done"
|
msgid "Done"
|
||||||
msgstr "Terminé"
|
msgstr "Terminé"
|
||||||
|
|
||||||
#: src/components/FileDropZone.tsx:32
|
#: src/components/FileDropZone.tsx:29
|
||||||
msgid "Drop a file here"
|
msgid "Drop a file here"
|
||||||
msgstr "Déposez un fichier ici"
|
msgstr "Déposez un fichier ici"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:429
|
#: src/pages/DumpEdit.tsx:428
|
||||||
msgid "Drop a replacement here"
|
msgid "Drop a replacement here"
|
||||||
msgstr "Déposez un fichier de remplacement ici"
|
msgstr "Déposez un fichier de remplacement ici"
|
||||||
|
|
||||||
@@ -399,11 +399,11 @@ msgstr "Déposez un fichier de remplacement ici"
|
|||||||
msgid "Dump"
|
msgid "Dump"
|
||||||
msgstr "Reco"
|
msgstr "Reco"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:468
|
#: src/components/DumpCreateModal.tsx:458
|
||||||
msgid "Dump it"
|
msgid "Dump it"
|
||||||
msgstr "Recommander"
|
msgstr "Recommander"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:479
|
#: src/components/DumpCreateModal.tsx:469
|
||||||
msgid "Dumped!"
|
msgid "Dumped!"
|
||||||
msgstr "Recommandé !"
|
msgstr "Recommandé !"
|
||||||
|
|
||||||
@@ -436,7 +436,7 @@ msgstr "Modifier"
|
|||||||
msgid "Edit {0}"
|
msgid "Edit {0}"
|
||||||
msgstr "Modifier {0}"
|
msgstr "Modifier {0}"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:430
|
#: src/components/DumpCreateModal.tsx:420
|
||||||
msgid "Edit title"
|
msgid "Edit title"
|
||||||
msgstr "Modifier le titre"
|
msgstr "Modifier le titre"
|
||||||
|
|
||||||
@@ -460,12 +460,12 @@ msgstr "modifié {0}"
|
|||||||
msgid "Edited {0}"
|
msgid "Edited {0}"
|
||||||
msgstr "Modifié le {0}"
|
msgstr "Modifié le {0}"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:204
|
#: src/pages/DumpEdit.tsx:206
|
||||||
msgid "Editing"
|
msgid "Editing"
|
||||||
msgstr "Modification"
|
msgstr "Modification"
|
||||||
|
|
||||||
#. placeholder {0}: state.dump.title
|
#. placeholder {0}: state.dump.title
|
||||||
#: src/pages/DumpEdit.tsx:49
|
#: src/pages/DumpEdit.tsx:48
|
||||||
msgid "Editing {0}"
|
msgid "Editing {0}"
|
||||||
msgstr "Édition de {0}"
|
msgstr "Édition de {0}"
|
||||||
|
|
||||||
@@ -506,7 +506,7 @@ msgstr "Impossible de générer une invitation"
|
|||||||
msgid "Failed to load"
|
msgid "Failed to load"
|
||||||
msgstr "Chargement échoué"
|
msgstr "Chargement échoué"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:341
|
#: src/components/DumpCreateModal.tsx:337
|
||||||
msgid "Failed to post"
|
msgid "Failed to post"
|
||||||
msgstr "Publication échouée"
|
msgstr "Publication échouée"
|
||||||
|
|
||||||
@@ -544,20 +544,19 @@ msgstr "Erreur lors de la mise à jour du rôle"
|
|||||||
msgid "Feeds"
|
msgid "Feeds"
|
||||||
msgstr "Flux"
|
msgstr "Flux"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:373
|
#: src/components/DumpCreateModal.tsx:369
|
||||||
msgid "Fetching preview…"
|
msgid "Fetching preview…"
|
||||||
msgstr "Récupération de l'aperçu…"
|
msgstr "Récupération de l'aperçu…"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:465
|
#: src/components/DumpCreateModal.tsx:455
|
||||||
msgid "Fetching…"
|
msgid "Fetching…"
|
||||||
msgstr "Récupération…"
|
msgstr "Récupération…"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:335
|
#: src/components/DumpCreateModal.tsx:331
|
||||||
#: src/components/FileDropZone.tsx:31
|
|
||||||
msgid "File"
|
msgid "File"
|
||||||
msgstr "Fichier"
|
msgstr "Fichier"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:243
|
#: src/components/DumpCreateModal.tsx:239
|
||||||
msgid "File too large (max 50 MB)."
|
msgid "File too large (max 50 MB)."
|
||||||
msgstr "Fichier trop volumineux (max 50 Mo)."
|
msgstr "Fichier trop volumineux (max 50 Mo)."
|
||||||
|
|
||||||
@@ -681,7 +680,7 @@ msgid "Load older messages"
|
|||||||
msgstr "Charger les messages plus anciens"
|
msgstr "Charger les messages plus anciens"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:240
|
#: src/pages/Dump.tsx:240
|
||||||
#: src/pages/DumpEdit.tsx:155
|
#: src/pages/DumpEdit.tsx:157
|
||||||
msgid "Loading dump…"
|
msgid "Loading dump…"
|
||||||
msgstr "Chargement de la reco…"
|
msgstr "Chargement de la reco…"
|
||||||
|
|
||||||
@@ -758,7 +757,7 @@ msgstr "Connexion échouée"
|
|||||||
msgid "Manage"
|
msgid "Manage"
|
||||||
msgstr "Administration"
|
msgstr "Administration"
|
||||||
|
|
||||||
#: src/components/FileDropZone.tsx:145
|
#: src/components/FileDropZone.tsx:140
|
||||||
msgid "Max 50 MB"
|
msgid "Max 50 MB"
|
||||||
msgstr "Max 50 Mo"
|
msgstr "Max 50 Mo"
|
||||||
|
|
||||||
@@ -780,7 +779,7 @@ msgstr "nouveau"
|
|||||||
msgid "New"
|
msgid "New"
|
||||||
msgstr "Nouveau"
|
msgstr "Nouveau"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:305
|
#: src/components/DumpCreateModal.tsx:301
|
||||||
#: src/components/DumpFab.tsx:65
|
#: src/components/DumpFab.tsx:65
|
||||||
#: src/components/DumpFab.tsx:66
|
#: src/components/DumpFab.tsx:66
|
||||||
#: src/pages/UserDumps.tsx:88
|
#: src/pages/UserDumps.tsx:88
|
||||||
@@ -873,7 +872,7 @@ msgstr "Notifications ({unreadNotificationCount} non lues)"
|
|||||||
msgid "Open search"
|
msgid "Open search"
|
||||||
msgstr "Ouvrir la recherche"
|
msgstr "Ouvrir la recherche"
|
||||||
|
|
||||||
#: src/components/FileDropZone.tsx:139
|
#: src/components/FileDropZone.tsx:134
|
||||||
msgid "or <0>browse files</0>"
|
msgid "or <0>browse files</0>"
|
||||||
msgstr "ou <0>parcourir les fichiers</0>"
|
msgstr "ou <0>parcourir les fichiers</0>"
|
||||||
|
|
||||||
@@ -922,7 +921,7 @@ msgstr "Collections"
|
|||||||
msgid "Playlists ({0}{1})"
|
msgid "Playlists ({0}{1})"
|
||||||
msgstr "Collections ({0}{1})"
|
msgstr "Collections ({0}{1})"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:240
|
#: src/components/DumpCreateModal.tsx:236
|
||||||
msgid "Please select a file."
|
msgid "Please select a file."
|
||||||
msgstr "Veuillez sélectionner un fichier."
|
msgstr "Veuillez sélectionner un fichier."
|
||||||
|
|
||||||
@@ -963,11 +962,11 @@ msgstr "public"
|
|||||||
msgid "Public"
|
msgid "Public"
|
||||||
msgstr "Public"
|
msgstr "Public"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:233
|
#: src/pages/DumpEdit.tsx:235
|
||||||
msgid "Refresh metadata"
|
msgid "Refresh metadata"
|
||||||
msgstr "Actualiser les métadonnées"
|
msgstr "Actualiser les métadonnées"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:232
|
#: src/pages/DumpEdit.tsx:234
|
||||||
msgid "Refreshing…"
|
msgid "Refreshing…"
|
||||||
msgstr "Actualisation…"
|
msgstr "Actualisation…"
|
||||||
|
|
||||||
@@ -989,7 +988,7 @@ msgstr "Inscription échouée"
|
|||||||
msgid "Related"
|
msgid "Related"
|
||||||
msgstr "Connexe"
|
msgstr "Connexe"
|
||||||
|
|
||||||
#: src/components/FileDropZone.tsx:115
|
#: src/components/FileDropZone.tsx:110
|
||||||
msgid "Remove file"
|
msgid "Remove file"
|
||||||
msgstr "Supprimer le fichier"
|
msgstr "Supprimer le fichier"
|
||||||
|
|
||||||
@@ -1005,7 +1004,7 @@ msgstr "Retirer le j'aime"
|
|||||||
msgid "Remove vote"
|
msgid "Remove vote"
|
||||||
msgstr "Retirer le vote"
|
msgstr "Retirer le vote"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:428
|
#: src/pages/DumpEdit.tsx:420
|
||||||
msgid "Replace file"
|
msgid "Replace file"
|
||||||
msgstr "Remplacer le fichier"
|
msgstr "Remplacer le fichier"
|
||||||
|
|
||||||
@@ -1031,13 +1030,13 @@ msgstr "Échec de la réinitialisation"
|
|||||||
msgid "Reset password"
|
msgid "Reset password"
|
||||||
msgstr "Réinitialiser le mot de passe"
|
msgstr "Réinitialiser le mot de passe"
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:379
|
#: src/pages/DumpEdit.tsx:381
|
||||||
#: src/pages/DumpEdit.tsx:397
|
#: src/pages/DumpEdit.tsx:399
|
||||||
msgid "Reset to default"
|
msgid "Reset to default"
|
||||||
msgstr "Réinitialiser par défaut"
|
msgstr "Réinitialiser par défaut"
|
||||||
|
|
||||||
#: src/pages/Dump.tsx:257
|
#: src/pages/Dump.tsx:257
|
||||||
#: src/pages/DumpEdit.tsx:172
|
#: src/pages/DumpEdit.tsx:174
|
||||||
msgid "Retry"
|
msgid "Retry"
|
||||||
msgstr "Réessayer"
|
msgstr "Réessayer"
|
||||||
|
|
||||||
@@ -1048,7 +1047,7 @@ msgstr "Rôle"
|
|||||||
#: src/components/ChatModal.tsx:222
|
#: src/components/ChatModal.tsx:222
|
||||||
#: src/components/CommentThread.tsx:328
|
#: src/components/CommentThread.tsx:328
|
||||||
#: src/pages/Dump.tsx:368
|
#: src/pages/Dump.tsx:368
|
||||||
#: src/pages/DumpEdit.tsx:466
|
#: src/pages/DumpEdit.tsx:463
|
||||||
#: src/pages/PlaylistDetail.tsx:927
|
#: src/pages/PlaylistDetail.tsx:927
|
||||||
#: src/pages/UserPublicProfile.tsx:1666
|
#: src/pages/UserPublicProfile.tsx:1666
|
||||||
#: src/pages/UserPublicProfile.tsx:1736
|
#: src/pages/UserPublicProfile.tsx:1736
|
||||||
@@ -1149,17 +1148,17 @@ msgstr "Rien à voir, circulez."
|
|||||||
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é."
|
||||||
|
|
||||||
#: src/pages/DumpEdit.tsx:363
|
#: src/pages/DumpEdit.tsx:365
|
||||||
msgid "Thumbnail"
|
msgid "Thumbnail"
|
||||||
msgstr "Miniature"
|
msgstr "Miniature"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:399
|
#: src/components/DumpCreateModal.tsx:389
|
||||||
#: src/components/PlaylistCreateForm.tsx:70
|
#: src/components/PlaylistCreateForm.tsx:70
|
||||||
#: src/pages/DumpEdit.tsx:387
|
#: src/pages/DumpEdit.tsx:389
|
||||||
msgid "Title"
|
msgid "Title"
|
||||||
msgstr "Titre"
|
msgstr "Titre"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:241
|
#: src/components/DumpCreateModal.tsx:237
|
||||||
msgid "Title is required."
|
msgid "Title is required."
|
||||||
msgstr "Un titre est requis."
|
msgstr "Un titre est requis."
|
||||||
|
|
||||||
@@ -1188,7 +1187,7 @@ msgstr "Ne plus suivre la collection"
|
|||||||
msgid "Upload failed"
|
msgid "Upload failed"
|
||||||
msgstr "Envoi échoué"
|
msgstr "Envoi échoué"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:466
|
#: src/components/DumpCreateModal.tsx:456
|
||||||
msgid "Uploading…"
|
msgid "Uploading…"
|
||||||
msgstr "Envoi…"
|
msgstr "Envoi…"
|
||||||
|
|
||||||
@@ -1206,12 +1205,12 @@ msgstr "Voté"
|
|||||||
msgid "Upvoted ({0}{1})"
|
msgid "Upvoted ({0}{1})"
|
||||||
msgstr "Votés ({0}{1})"
|
msgstr "Votés ({0}{1})"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:348
|
#: src/components/DumpCreateModal.tsx:344
|
||||||
#: src/pages/DumpEdit.tsx:410
|
#: src/pages/DumpEdit.tsx:412
|
||||||
msgid "URL"
|
msgid "URL"
|
||||||
msgstr "URL"
|
msgstr "URL"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:227
|
#: src/components/DumpCreateModal.tsx:223
|
||||||
msgid "URL is required."
|
msgid "URL is required."
|
||||||
msgstr "L'URL est obligatoire."
|
msgstr "L'URL est obligatoire."
|
||||||
|
|
||||||
@@ -1248,12 +1247,12 @@ msgstr "Utilisateurs"
|
|||||||
msgid "View all →"
|
msgid "View all →"
|
||||||
msgstr "Tout voir →"
|
msgstr "Tout voir →"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:481
|
#: src/components/DumpCreateModal.tsx:471
|
||||||
msgid "View dump →"
|
msgid "View dump →"
|
||||||
msgstr "Voir la reco →"
|
msgstr "Voir la reco →"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:444
|
#: src/components/DumpCreateModal.tsx:434
|
||||||
#: src/pages/DumpEdit.tsx:440
|
#: src/pages/DumpEdit.tsx:437
|
||||||
msgid "What makes it worth it?"
|
msgid "What makes it worth it?"
|
||||||
msgstr "Pourquoi on en voudrait ?"
|
msgstr "Pourquoi on en voudrait ?"
|
||||||
|
|
||||||
@@ -1262,8 +1261,8 @@ msgstr "Pourquoi on en voudrait ?"
|
|||||||
msgid "Who am I?"
|
msgid "Who am I?"
|
||||||
msgstr "Qui suis-je ?"
|
msgstr "Qui suis-je ?"
|
||||||
|
|
||||||
#: src/components/DumpCreateModal.tsx:443
|
#: src/components/DumpCreateModal.tsx:433
|
||||||
#: src/pages/DumpEdit.tsx:439
|
#: src/pages/DumpEdit.tsx:436
|
||||||
msgid "Why?"
|
msgid "Why?"
|
||||||
msgstr "Pourquoi ?"
|
msgstr "Pourquoi ?"
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ import { friendlyFetchError } from "../utils/apiError.ts";
|
|||||||
import { ConfirmModal } from "../components/ConfirmModal.tsx";
|
import { ConfirmModal } from "../components/ConfirmModal.tsx";
|
||||||
import RichContentCard from "../components/RichContentCard.tsx";
|
import RichContentCard from "../components/RichContentCard.tsx";
|
||||||
import FilePreview from "../components/FilePreview.tsx";
|
import FilePreview from "../components/FilePreview.tsx";
|
||||||
import { FileDropZone } from "../components/FileDropZone.tsx";
|
|
||||||
import { ImagePicker } from "../components/ImagePicker.tsx";
|
import { ImagePicker } from "../components/ImagePicker.tsx";
|
||||||
import { CategorySelect } from "../components/CategorySelect.tsx";
|
import { CategorySelect } from "../components/CategorySelect.tsx";
|
||||||
import {
|
import {
|
||||||
expectOk,
|
expectOk,
|
||||||
|
FileField,
|
||||||
FormError,
|
FormError,
|
||||||
FormProvider,
|
FormProvider,
|
||||||
RichTextField,
|
RichTextField,
|
||||||
@@ -30,7 +30,6 @@ import {
|
|||||||
VisibilityToggle,
|
VisibilityToggle,
|
||||||
} from "../components/form/index.ts";
|
} from "../components/form/index.ts";
|
||||||
import { useAuth } from "../hooks/useAuth.ts";
|
import { useAuth } from "../hooks/useAuth.ts";
|
||||||
import { Controller } from "react-hook-form";
|
|
||||||
|
|
||||||
type DumpEditState =
|
type DumpEditState =
|
||||||
| { status: "loading" }
|
| { status: "loading" }
|
||||||
@@ -69,7 +68,10 @@ export function DumpEdit() {
|
|||||||
const apiResponse = parseAPIResponse<RawDump>(await res.json());
|
const apiResponse = parseAPIResponse<RawDump>(await res.json());
|
||||||
|
|
||||||
if (apiResponse.success) {
|
if (apiResponse.success) {
|
||||||
setState({ status: "loaded", dump: deserializeDump(apiResponse.data) });
|
setState({
|
||||||
|
status: "loaded",
|
||||||
|
dump: deserializeDump(apiResponse.data),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
setState({ status: "error", error: apiResponse.error.message });
|
setState({ status: "error", error: apiResponse.error.message });
|
||||||
}
|
}
|
||||||
@@ -414,22 +416,17 @@ function DumpEditForm({
|
|||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
<div className="form-field">
|
<div className="form-field">
|
||||||
|
<span className="form-label">
|
||||||
|
<Trans>Replace file</Trans>
|
||||||
|
</span>
|
||||||
<p className="dump-file-notice">
|
<p className="dump-file-notice">
|
||||||
<strong>{dump.fileName}</strong>
|
<strong>{dump.fileName}</strong>
|
||||||
{dump.fileSize != null && ` — ${formatBytes(dump.fileSize)}`}
|
{dump.fileSize != null && ` — ${formatBytes(dump.fileSize)}`}
|
||||||
</p>
|
</p>
|
||||||
<Controller
|
<FileField<EditValues>
|
||||||
name="newFile"
|
name="newFile"
|
||||||
control={form.control}
|
hint={t`Drop a replacement here`}
|
||||||
render={({ field }) => (
|
showLimit={false}
|
||||||
<FileDropZone
|
|
||||||
file={field.value}
|
|
||||||
onChange={field.onChange}
|
|
||||||
label={t`Replace file`}
|
|
||||||
hint={t`Drop a replacement here`}
|
|
||||||
showLimit={false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -569,6 +569,7 @@
|
|||||||
[data-style="nyt"] .notif-icon,
|
[data-style="nyt"] .notif-icon,
|
||||||
[data-style="nyt"] .notifications-empty-icon,
|
[data-style="nyt"] .notifications-empty-icon,
|
||||||
[data-style="nyt"] .notifications-title-bell,
|
[data-style="nyt"] .notifications-title-bell,
|
||||||
|
[data-style="nyt"] .chat-button-icon,
|
||||||
[data-style="nyt"] .dump-card-preview-icon,
|
[data-style="nyt"] .dump-card-preview-icon,
|
||||||
[data-style="nyt"] .playlist-card-icon,
|
[data-style="nyt"] .playlist-card-icon,
|
||||||
[data-style="nyt"] .rich-content-compact-icon,
|
[data-style="nyt"] .rich-content-compact-icon,
|
||||||
|
|||||||
Reference in New Issue
Block a user