v3: localization fixes, char counters & limits on all text fields, ux fixes

This commit is contained in:
khannurien
2026-04-03 19:47:37 +00:00
parent 0ce80398a4
commit a69788c15b
48 changed files with 1133 additions and 305 deletions

View File

@@ -1,9 +1,9 @@
import { useEffect, useRef, useState } from "react";
import { Link } from "react-router";
import { t } from "@lingui/core/macro"
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { API_URL } from "../config/api.ts";
import { API_URL, VALIDATION } from "../config/api.ts";
import type {
CreateUrlDumpRequest,
Dump,
@@ -51,16 +51,20 @@ type UrlPreview =
| { status: "done"; richContent: RichContent | null };
function LocalFilePreview({ file }: { file: File }) {
// useRef instead of useMemo+useEffect: StrictMode double-invokes effect
// cleanups, which would revoke the blob URL before the video element can use it.
const blobRef = useRef<{ file: File; url: string } | null>(null);
if (blobRef.current?.file !== file) {
if (blobRef.current) URL.revokeObjectURL(blobRef.current.url);
blobRef.current = { file, url: URL.createObjectURL(file) };
}
const src = blobRef.current.url;
const mime = file.type;
const [src, setSrc] = useState<string | null>(null);
useEffect(() => {
const url = URL.createObjectURL(file);
// Blob URL lifecycle requires setState in effect — no synchronous alternative.
// eslint-disable-next-line react-hooks/set-state-in-effect
setSrc(url);
return () => {
URL.revokeObjectURL(url);
};
}, [file]);
if (!src) return null;
const mime = file.type;
if (mime.startsWith("image/")) {
return <img src={src} alt={file.name} className="local-preview-image" />;
}
@@ -75,9 +79,12 @@ function LocalFilePreview({ file }: { file: File }) {
interface DumpCreateModalProps {
onClose: () => void;
initialUrl?: string;
}
export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
export function DumpCreateModal(
{ onClose, initialUrl = "" }: DumpCreateModalProps,
) {
const { authFetch } = useAuth();
const { injectDump } = useWS();
@@ -86,7 +93,7 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
// Create phase state
const [mode, setMode] = useState<Mode>("url");
const [url, setUrl] = useState("");
const [url, setUrl] = useState(initialUrl);
const [file, setFile] = useState<File | null>(null);
const [comment, setComment] = useState("");
const [isPrivate, setIsPrivate] = useState(false);
@@ -166,6 +173,7 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => {
e.preventDefault();
if (comment.length > VALIDATION.DUMP_COMMENT_MAX) return;
setSubmitState({ status: "submitting" });
try {
@@ -320,7 +328,9 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
? (
<>
<div className="form-group">
<label htmlFor="dc-url"><Trans>URL</Trans></label>
<label htmlFor="dc-url">
<Trans>URL</Trans>
</label>
<input
id="dc-url"
type="url"
@@ -345,7 +355,9 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
/>
</div>
{urlPreview.status === "loading" && (
<p className="preview-loading"><Trans>Fetching preview</Trans></p>
<p className="preview-loading">
<Trans>Fetching preview</Trans>
</p>
)}
{urlPreview.status === "done" &&
urlPreview.richContent && (
@@ -377,6 +389,7 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
disabled={submitting}
placeholder={t`Tell the community what makes this worth their time...`}
rows={3}
maxLength={VALIDATION.DUMP_COMMENT_MAX}
/>
</div>
@@ -411,7 +424,8 @@ export function DumpCreateModal({ onClose }: DumpCreateModalProps) {
<button
type="submit"
className="btn-primary"
disabled={submitting}
disabled={submitting ||
comment.length > VALIDATION.DUMP_COMMENT_MAX}
>
{submitting
? (mode === "url"