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

This commit is contained in:
2026-06-30 13:41:36 +02:00
parent d2d086831e
commit 7773a8a501
12 changed files with 317 additions and 237 deletions

View File

@@ -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();
} }

View File

@@ -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

View File

@@ -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);
} }

View File

@@ -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;

View File

@@ -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 && (

View 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>
); );
} }

View File

@@ -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}

View File

@@ -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.

View File

@@ -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?"

View File

@@ -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 ?"

View File

@@ -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>
)} )}

View File

@@ -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,