v3: consistent tabs url across app, maxy small fixes
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 42s

This commit is contained in:
khannurien
2026-06-22 17:25:40 +00:00
parent dbd2e15158
commit a1b71ad0c8
22 changed files with 295 additions and 236 deletions

View File

@@ -73,7 +73,8 @@ async function loadIndexHtml(): Promise<string | null> {
} }
const DUMP_RE = /^\/dumps\/([^/]+)$/; const DUMP_RE = /^\/dumps\/([^/]+)$/;
const USER_RE = /^\/users\/([^/]+)$/; // Allow an optional `/~/<tab>` suffix so linkable profile tabs keep OG meta.
const USER_RE = /^\/users\/([^/]+)(?:\/~\/[^/]+)?$/;
const PLAYLIST_RE = /^\/playlists\/([^/]+)$/; const PLAYLIST_RE = /^\/playlists\/([^/]+)$/;
export async function ogMiddleware(ctx: Context, next: Next) { export async function ogMiddleware(ctx: Context, next: Next) {

View File

@@ -83,6 +83,20 @@ export interface DumpRow {
[key: string]: SQLOutputValue; // Index signature [key: string]: SQLOutputValue; // Index signature
} }
// Shared `dumps` column list — keep in sync with DumpRow/isDumpRow above.
// Services import these instead of repeating the column list themselves.
export const DUMP_COLUMNS =
"id, kind, title, slug, comment, user_id, created_at, updated_at, url, rich_content, file_name, file_mime, file_size, vote_count, is_private, custom_thumbnail_mime";
export const DUMP_COLUMNS_ALIASED =
"d.id, d.kind, d.title, d.slug, d.comment, d.user_id, d.created_at, d.updated_at, d.url, d.rich_content, d.file_name, d.file_mime, d.file_size, d.vote_count, d.is_private, d.custom_thumbnail_mime";
export const DUMP_SELECT_COLUMNS = `${DUMP_COLUMNS},
(SELECT COUNT(*) FROM comments WHERE dump_id = dumps.id AND deleted = 0) as comment_count`;
export const DUMP_SELECT_COLUMNS_ALIASED = `${DUMP_COLUMNS_ALIASED},
(SELECT COUNT(*) FROM comments WHERE dump_id = d.id AND deleted = 0) as comment_count`;
export interface UserRow { export interface UserRow {
id: string; id: string;
username: string; username: string;

View File

@@ -1,13 +1,12 @@
import { type Dump } from "../model/interfaces.ts"; import { type Dump } from "../model/interfaces.ts";
import { db, dumpRowToApi, isDumpRow } from "../model/db.ts"; import {
db,
DUMP_SELECT_COLUMNS as SELECT_COLS,
dumpRowToApi,
isDumpRow,
} from "../model/db.ts";
import { UUID_RE } from "../lib/slugify.ts"; import { UUID_RE } from "../lib/slugify.ts";
// Mirrors dump-service SELECT_COLS — kept local to avoid a circular import
// (dump-service.ts must import relinkBacklinks from this file).
const SELECT_COLS =
"id, kind, title, slug, comment, user_id, created_at, updated_at, url, rich_content, file_name, file_mime, file_size, vote_count, is_private, custom_thumbnail_mime," +
" (SELECT COUNT(*) FROM comments WHERE dump_id = dumps.id AND deleted = 0) as comment_count";
// Matches http(s) URLs in free text — stops at whitespace or markdown-link // Matches http(s) URLs in free text — stops at whitespace or markdown-link
// delimiters (so `[text](url)` and `<url>` don't swallow the closing char). // delimiters (so `[text](url)` and `<url>` don't swallow the closing char).
const URL_RE = /https?:\/\/[^\s)\]<>"']+/g; const URL_RE = /https?:\/\/[^\s)\]<>"']+/g;

View File

@@ -5,7 +5,14 @@ import {
type Dump, type Dump,
type UpdateDumpRequest, type UpdateDumpRequest,
} from "../model/interfaces.ts"; } from "../model/interfaces.ts";
import { db, dumpApiToRow, dumpRowToApi, isDumpRow } from "../model/db.ts"; import {
db,
DUMP_SELECT_COLUMNS as SELECT_COLS,
DUMP_SELECT_COLUMNS_ALIASED as SELECT_COLS_ALIASED,
dumpApiToRow,
dumpRowToApi,
isDumpRow,
} from "../model/db.ts";
import { fetchRichContent, isValidHttpUrl } from "./rich-content-service.ts"; import { fetchRichContent, isValidHttpUrl } from "./rich-content-service.ts";
import { import {
broadcastDumpDeleted, broadcastDumpDeleted,
@@ -40,16 +47,6 @@ function titleFromUrl(url: string): string {
} }
} }
const BASE_COLS =
"id, kind, title, slug, comment, user_id, created_at, updated_at, url, rich_content, file_name, file_mime, file_size, vote_count, is_private, custom_thumbnail_mime";
const SELECT_COLS = `${BASE_COLS},
(SELECT COUNT(*) FROM comments WHERE dump_id = dumps.id AND deleted = 0) as comment_count`;
const SELECT_COLS_ALIASED =
"d.id, d.kind, d.title, d.slug, d.comment, d.user_id, d.created_at, d.updated_at, d.url, d.rich_content, d.file_name, d.file_mime, d.file_size, d.vote_count, d.is_private, d.custom_thumbnail_mime," +
" (SELECT COUNT(*) FROM comments WHERE dump_id = d.id AND deleted = 0) as comment_count";
export async function createUrlDump( export async function createUrlDump(
request: CreateUrlDumpRequest, request: CreateUrlDumpRequest,
userId: string, userId: string,

View File

@@ -12,6 +12,7 @@ import {
} from "./notification-service.ts"; } from "./notification-service.ts";
import { import {
db, db,
DUMP_SELECT_COLUMNS_ALIASED as SELECT_COLS_ALIASED,
dumpRowToApi, dumpRowToApi,
isDumpRow, isDumpRow,
isFollowRow, isFollowRow,
@@ -21,12 +22,6 @@ import {
userRowToApi, userRowToApi,
} from "../model/db.ts"; } from "../model/db.ts";
// Mirrors dump-service SELECT_COLS_ALIASED — kept local to avoid circular imports
const SELECT_COLS_ALIASED =
"d.id, d.kind, d.title, d.slug, d.comment, d.user_id, d.created_at, d.updated_at, d.url, d.rich_content, " +
"d.file_name, d.file_mime, d.file_size, d.vote_count, d.is_private, d.custom_thumbnail_mime," +
" (SELECT COUNT(*) FROM comments WHERE dump_id = d.id AND deleted = 0) as comment_count";
// ── Follow / unfollow a user ────────────────────────────────────────────────── // ── Follow / unfollow a user ──────────────────────────────────────────────────
export function followUser(followerId: string, followedUserId: string): void { export function followUser(followerId: string, followedUserId: string): void {

View File

@@ -10,6 +10,7 @@ import {
} from "../model/interfaces.ts"; } from "../model/interfaces.ts";
import { import {
db, db,
DUMP_COLUMNS_ALIASED,
dumpRowToApi, dumpRowToApi,
isDumpRow, isDumpRow,
isPlaylistRow, isPlaylistRow,
@@ -28,9 +29,6 @@ import {
import { makeSlug, UUID_RE } from "../lib/slugify.ts"; import { makeSlug, UUID_RE } from "../lib/slugify.ts";
import { linkAttachments } from "./attachment-service.ts"; import { linkAttachments } from "./attachment-service.ts";
const DUMP_SELECT_COLS =
"id, kind, title, slug, comment, user_id, created_at, updated_at, url, rich_content, file_name, file_mime, file_size, vote_count, is_private, custom_thumbnail_mime";
const PLAYLIST_SELECT = `p.*, u.username as owner_username, const PLAYLIST_SELECT = `p.*, u.username as owner_username,
(SELECT COUNT(*) FROM playlist_dumps pd WHERE pd.playlist_id = p.id) as dump_count (SELECT COUNT(*) FROM playlist_dumps pd WHERE pd.playlist_id = p.id) as dump_count
FROM playlists p LEFT JOIN users u ON u.id = p.user_id`; FROM playlists p LEFT JOIN users u ON u.id = p.user_id`;
@@ -91,12 +89,11 @@ export function getPlaylist(
throw new APIException(APIErrorCode.NOT_FOUND, 404, "Playlist not found"); throw new APIException(APIErrorCode.NOT_FOUND, 404, "Playlist not found");
} }
const dumpCols = DUMP_SELECT_COLS.split(", ").map((c) => `d.${c}`).join(", ");
const isOwner = requestingUserId === playlist.userId; const isOwner = requestingUserId === playlist.userId;
// For public playlists (or when viewed by non-owner), filter out private dumps // For public playlists (or when viewed by non-owner), filter out private dumps
const rows = db.prepare( const rows = db.prepare(
`SELECT ${dumpCols}, `SELECT ${DUMP_COLUMNS_ALIASED},
(SELECT COUNT(*) FROM comments WHERE dump_id = d.id AND deleted = 0) as comment_count (SELECT COUNT(*) FROM comments WHERE dump_id = d.id AND deleted = 0) as comment_count
FROM dumps d FROM dumps d
INNER JOIN playlist_dumps pd ON d.id = pd.dump_id INNER JOIN playlist_dumps pd ON d.id = pd.dump_id

View File

@@ -1,5 +1,5 @@
import { lazy, Suspense } from "react"; import { lazy, Suspense } from "react";
import { BrowserRouter, Route, Routes } from "react-router"; import { BrowserRouter, Navigate, Route, Routes } from "react-router";
import { RestrictedGuest } from "./pages/RestrictedGuest.tsx"; import { RestrictedGuest } from "./pages/RestrictedGuest.tsx";
import { RestrictedLoggedIn } from "./pages/RestrictedLoggedIn.tsx"; import { RestrictedLoggedIn } from "./pages/RestrictedLoggedIn.tsx";
@@ -70,7 +70,8 @@ function AppRoutes() {
return ( return (
<Suspense> <Suspense>
<Routes> <Routes>
<Route path="/" element={<Index />} /> <Route path="/" element={<Navigate to="/~/hot" replace />} />
<Route path="/~/:feedTab" element={<Index />} />
<Route path="/dumps/:selectedDump" element={<Dump />} /> <Route path="/dumps/:selectedDump" element={<Dump />} />
<Route <Route
path="/dumps/:selectedDump/edit" path="/dumps/:selectedDump/edit"
@@ -97,6 +98,10 @@ function AppRoutes() {
} }
/> />
<Route path="/users/:username" element={<UserPublicProfile />} /> <Route path="/users/:username" element={<UserPublicProfile />} />
<Route
path="/users/:username/~/:profileTab"
element={<UserPublicProfile />}
/>
<Route path="/users/:username/dumps" element={<UserDumps />} /> <Route path="/users/:username/dumps" element={<UserDumps />} />
<Route path="/users/:username/upvoted" element={<UserUpvoted />} /> <Route path="/users/:username/upvoted" element={<UserUpvoted />} />
<Route <Route
@@ -105,6 +110,7 @@ function AppRoutes() {
/> />
<Route path="/playlists/:playlistId" element={<PlaylistDetail />} /> <Route path="/playlists/:playlistId" element={<PlaylistDetail />} />
<Route path="/search" element={<Search />} /> <Route path="/search" element={<Search />} />
<Route path="/search/~/:searchTab" element={<Search />} />
<Route path="/reset-password" element={<ResetPassword />} /> <Route path="/reset-password" element={<ResetPassword />} />
<Route <Route
path="/notifications" path="/notifications"

View File

@@ -42,7 +42,7 @@ export function AppHeader(
return ( return (
<> <>
<header className="app-header app-header--has-center"> <header className="app-header app-header--has-center">
<Link to="/?tab=hot" className="app-header-brand"> <Link to="/" className="app-header-brand">
🚚<span className="app-header-brand-name"> 🚚<span className="app-header-brand-name">
{" "} {" "}
{document.querySelector<HTMLMetaElement>('meta[name="site-name"]') {document.querySelector<HTMLMetaElement>('meta[name="site-name"]')

View File

@@ -24,7 +24,7 @@ export function ChangePasswordModal({ onClose }: ChangePasswordModalProps) {
const tooShort = newPassword.length > 0 && const tooShort = newPassword.length > 0 &&
newPassword.length < VALIDATION.PASSWORD_MIN; newPassword.length < VALIDATION.PASSWORD_MIN;
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.SubmitEvent) => {
e.preventDefault(); e.preventDefault();
if (mismatch || tooShort || !currentPassword || !newPassword) return; if (mismatch || tooShort || !currentPassword || !newPassword) return;

View File

@@ -188,7 +188,7 @@ export function DumpCreateModal(
return () => globalThis.removeEventListener("paste", handler); return () => globalThis.removeEventListener("paste", handler);
}, []); }, []);
const handleSubmit = async (e: React.SubmitEvent<HTMLFormElement>) => { const handleSubmit = async (e: React.SubmitEvent) => {
e.preventDefault(); e.preventDefault();
if (comment.length > VALIDATION.DUMP_COMMENT_MAX) return; if (comment.length > VALIDATION.DUMP_COMMENT_MAX) return;
setSubmitState({ status: "submitting" }); setSubmitState({ status: "submitting" });

View File

@@ -1,16 +1,12 @@
import { useLocation, useNavigate } from "react-router";
import { Trans } from "@lingui/react/macro"; import { Trans } from "@lingui/react/macro";
import { useAuth } from "../hooks/useAuth.ts"; import { useAuth } from "../hooks/useAuth.ts";
import { type FeedTab, VALID_TABS } from "../config/feedTabs.ts"; import { type FeedTab, FEED_TABS } from "../config/feedTabs.ts";
import { useTabParam } from "../hooks/useTabParam.ts";
import { TabBar } from "./TabBar.tsx"; import { TabBar } from "./TabBar.tsx";
export function FeedTabBar() { export function FeedTabBar() {
const location = useLocation();
const navigate = useNavigate();
const { user } = useAuth(); const { user } = useAuth();
const [tab, setTab] = useTabParam<FeedTab>(FEED_TABS, "hot");
const rawTab = new URLSearchParams(location.search).get("tab") ?? "hot";
const tab: FeedTab = VALID_TABS.has(rawTab) ? (rawTab as FeedTab) : "hot";
const tabs = [ const tabs = [
{ key: "hot" as FeedTab, label: <Trans>Hot</Trans> }, { key: "hot" as FeedTab, label: <Trans>Hot</Trans> },
@@ -25,7 +21,7 @@ export function FeedTabBar() {
<TabBar <TabBar
tabs={tabs} tabs={tabs}
activeTab={tab} activeTab={tab}
onChange={(t) => navigate(`/?tab=${t}`, { replace: true })} onChange={setTab}
/> />
); );
} }

View File

@@ -1,4 +1,4 @@
import { type FormEvent, useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { t } from "@lingui/core/macro"; import { t } from "@lingui/core/macro";
@@ -41,11 +41,11 @@ export function SearchBar(
} }
} }
function handleSubmit(e: FormEvent) { function handleSubmit(e: React.SubmitEvent) {
e.preventDefault(); e.preventDefault();
const q = value.trim(); const q = value.trim();
if (!q) return; if (!q) return;
navigate(`/search?q=${encodeURIComponent(q)}&tab=dumps`); navigate(`/search?q=${encodeURIComponent(q)}`);
if (collapsible || isControlled) { if (collapsible || isControlled) {
setExpanded(false); setExpanded(false);
setValue(""); setValue("");

View File

@@ -1,3 +1,2 @@
export const FEED_TABS = ["hot", "new", "journal", "followed"] as const; export const FEED_TABS = ["hot", "new", "journal", "followed"] as const;
export type FeedTab = (typeof FEED_TABS)[number]; export type FeedTab = (typeof FEED_TABS)[number];
export const VALID_TABS = new Set<string>(FEED_TABS);

47
src/hooks/useTabParam.ts Normal file
View File

@@ -0,0 +1,47 @@
import { useCallback } from "react";
import { useLocation, useNavigate } from "react-router";
const SEP = "/~/";
/**
* Reflects the active tab in the URL path using a `/~/` delimiter, e.g.
* `/users/vincent/~/playlists` or `/~/new`. The segment before `/~/` is the
* base path and the segment after is the tab. When the base is non-empty the
* default tab collapses to the clean base path (`/users/vincent`); at the root
* the base is empty, so the default stays explicit (`/~/hot`). Generic and
* path-agnostic: the base is derived from the current location, so the same
* hook works for any route.
*
* Returns the validated active tab (falling back to `defaultTab`) and a setter
* that navigates to the matching path (replacing history).
*/
export function useTabParam<T extends string>(
validTabs: Iterable<T>,
defaultTab: T,
): [T, (tab: T) => void] {
const location = useLocation();
const navigate = useNavigate();
const valid = new Set<string>(validTabs as Iterable<string>);
const path = location.pathname;
const sepAt = path.indexOf(SEP);
let base = sepAt === -1 ? path : path.slice(0, sepAt);
if (base.endsWith("/")) base = base.slice(0, -1); // root "/" → ""
const raw = sepAt === -1
? null
: path.slice(sepAt + SEP.length).replace(/\/$/, "");
const tab = (raw && valid.has(raw) ? raw : defaultTab) as T;
const setTab = useCallback(
(t: T) => {
const target = t === defaultTab && base ? base : `${base}${SEP}${t}`;
// Preserve any unrelated query string (e.g. the search page's `?q=`).
navigate(`${target}${location.search}`, { replace: true });
},
[navigate, base, defaultTab, location.search],
);
return [tab, setTab];
}

View File

@@ -42,7 +42,7 @@ msgstr "{days}d ago"
msgid "{hrs}h ago" msgid "{hrs}h ago"
msgstr "{hrs}h ago" msgstr "{hrs}h ago"
#: src/pages/Search.tsx:182 #: src/pages/Search.tsx:176
msgid "{label} ({count})" msgid "{label} ({count})"
msgstr "{label} ({count})" msgstr "{label} ({count})"
@@ -55,7 +55,7 @@ msgid "{visibleCount, plural, one {# comment} other {# comments}}"
msgstr "{visibleCount, plural, one {# comment} other {# comments}}" msgstr "{visibleCount, plural, one {# comment} other {# comments}}"
#: src/pages/PlaylistDetail.tsx:617 #: src/pages/PlaylistDetail.tsx:617
#: src/pages/UserPublicProfile.tsx:753 #: src/pages/UserPublicProfile.tsx:766
msgid "← Back" msgid "← Back"
msgstr "← Back" msgstr "← Back"
@@ -71,7 +71,7 @@ msgstr "← Back to all dumps"
msgid "← Back to profile" msgid "← Back to profile"
msgstr "← Back to profile" msgstr "← Back to profile"
#: src/pages/UserPublicProfile.tsx:101 #: src/pages/UserPublicProfile.tsx:102
msgid "+ Invite someone" msgid "+ Invite someone"
msgstr "+ Invite someone" msgstr "+ Invite someone"
@@ -142,7 +142,7 @@ msgstr "a comment"
msgid "a post" msgid "a post"
msgstr "a post" msgstr "a post"
#: src/pages/UserPublicProfile.tsx:1204 #: src/pages/UserPublicProfile.tsx:1217
msgid "Account" msgid "Account"
msgstr "Account" msgstr "Account"
@@ -150,7 +150,7 @@ msgstr "Account"
msgid "Add a comment…" msgid "Add a comment…"
msgstr "Add a comment…" msgstr "Add a comment…"
#: src/pages/UserPublicProfile.tsx:871 #: src/pages/UserPublicProfile.tsx:884
msgid "Add email…" msgid "Add email…"
msgstr "Add email…" msgstr "Add email…"
@@ -171,7 +171,7 @@ msgstr "All {0, plural, one {# upvoted dump} other {# upvoted dumps}} loaded."
msgid "Already have an account? <0>Log in</0>" msgid "Already have an account? <0>Log in</0>"
msgstr "Already have an account? <0>Log in</0>" msgstr "Already have an account? <0>Log in</0>"
#: src/pages/UserPublicProfile.tsx:1223 #: src/pages/UserPublicProfile.tsx:1236
msgid "Appearance" msgid "Appearance"
msgstr "Appearance" msgstr "Appearance"
@@ -181,7 +181,7 @@ msgstr "Appearance"
msgid "At least {0} characters" msgid "At least {0} characters"
msgstr "At least {0} characters" msgstr "At least {0} characters"
#: src/pages/UserPublicProfile.tsx:1257 #: src/pages/UserPublicProfile.tsx:1270
msgid "Auto" msgid "Auto"
msgstr "Auto" msgstr "Auto"
@@ -205,8 +205,8 @@ msgstr "Can't connect to the live updates server. Upvotes and notifications may
#: src/pages/Dump.tsx:355 #: src/pages/Dump.tsx:355
#: src/pages/DumpEdit.tsx:391 #: src/pages/DumpEdit.tsx:391
#: src/pages/PlaylistDetail.tsx:686 #: src/pages/PlaylistDetail.tsx:686
#: src/pages/UserPublicProfile.tsx:850 #: src/pages/UserPublicProfile.tsx:863
#: src/pages/UserPublicProfile.tsx:932 #: src/pages/UserPublicProfile.tsx:945
msgid "Cancel" msgid "Cancel"
msgstr "Cancel" msgstr "Cancel"
@@ -218,7 +218,7 @@ msgstr "Cancel removal"
msgid "Cancel search" msgid "Cancel search"
msgstr "Cancel search" msgstr "Cancel search"
#: src/pages/UserPublicProfile.tsx:780 #: src/pages/UserPublicProfile.tsx:793
msgid "Change avatar" msgid "Change avatar"
msgstr "Change avatar" msgstr "Change avatar"
@@ -227,7 +227,7 @@ msgstr "Change avatar"
msgid "Change password" msgid "Change password"
msgstr "Change password" msgstr "Change password"
#: src/pages/UserPublicProfile.tsx:1216 #: src/pages/UserPublicProfile.tsx:1229
msgid "Change password…" msgid "Change password…"
msgstr "Change password…" msgstr "Change password…"
@@ -240,7 +240,7 @@ msgstr "Checking invite…"
msgid "Close" msgid "Close"
msgstr "Close" msgstr "Close"
#: src/pages/UserPublicProfile.tsx:1249 #: src/pages/UserPublicProfile.tsx:1262
msgid "Color scheme" msgid "Color scheme"
msgstr "Color scheme" msgstr "Color scheme"
@@ -249,11 +249,11 @@ msgstr "Color scheme"
msgid "Confirm new password" msgid "Confirm new password"
msgstr "Confirm new password" msgstr "Confirm new password"
#: src/pages/UserPublicProfile.tsx:92 #: src/pages/UserPublicProfile.tsx:93
msgid "Copied!" msgid "Copied!"
msgstr "Copied!" msgstr "Copied!"
#: src/pages/UserPublicProfile.tsx:92 #: src/pages/UserPublicProfile.tsx:93
msgid "Copy" msgid "Copy"
msgstr "Copy" msgstr "Copy"
@@ -298,7 +298,7 @@ msgstr "Creating…"
msgid "Current password" msgid "Current password"
msgstr "Current password" msgstr "Current password"
#: src/pages/UserPublicProfile.tsx:1271 #: src/pages/UserPublicProfile.tsx:1284
msgid "Dark" msgid "Dark"
msgstr "Dark" msgstr "Dark"
@@ -362,15 +362,15 @@ msgstr "Dump it"
msgid "Dumped!" msgid "Dumped!"
msgstr "Dumped!" msgstr "Dumped!"
#: src/pages/Search.tsx:178 #: src/pages/Search.tsx:172
#: src/pages/UserDumps.tsx:107 #: src/pages/UserDumps.tsx:107
#: src/pages/UserPublicProfile.tsx:976 #: src/pages/UserPublicProfile.tsx:989
msgid "Dumps" msgid "Dumps"
msgstr "Dumps" msgstr "Dumps"
#. placeholder {0}: dumps.items.length #. placeholder {0}: dumps.items.length
#. placeholder {1}: dumps.hasMore ? "+" : "" #. placeholder {1}: dumps.hasMore ? "+" : ""
#: src/pages/UserPublicProfile.tsx:993 #: src/pages/UserPublicProfile.tsx:1006
msgid "Dumps ({0}{1})" msgid "Dumps ({0}{1})"
msgstr "Dumps ({0}{1})" msgstr "Dumps ({0}{1})"
@@ -414,7 +414,7 @@ msgstr "Editing"
msgid "Email address" msgid "Email address"
msgstr "Email address" msgstr "Email address"
#: src/pages/Search.tsx:211 #: src/pages/Search.tsx:205
msgid "Enter a query to search." msgid "Enter a query to search."
msgstr "Enter a query to search." msgstr "Enter a query to search."
@@ -427,9 +427,9 @@ msgstr "Failed to change password"
msgid "Failed to create playlist" msgid "Failed to create playlist"
msgstr "Failed to create playlist" msgstr "Failed to create playlist"
#: src/pages/UserPublicProfile.tsx:73 #: src/pages/UserPublicProfile.tsx:74
#: src/pages/UserPublicProfile.tsx:76 #: src/pages/UserPublicProfile.tsx:77
#: src/pages/UserPublicProfile.tsx:104 #: src/pages/UserPublicProfile.tsx:105
msgid "Failed to generate invite" msgid "Failed to generate invite"
msgstr "Failed to generate invite" msgstr "Failed to generate invite"
@@ -438,9 +438,9 @@ msgstr "Failed to generate invite"
#: src/pages/index/JournalFeed.tsx:48 #: src/pages/index/JournalFeed.tsx:48
#: src/pages/index/NewFeed.tsx:36 #: src/pages/index/NewFeed.tsx:36
#: src/pages/Notifications.tsx:367 #: src/pages/Notifications.tsx:367
#: src/pages/UserPublicProfile.tsx:1095 #: src/pages/UserPublicProfile.tsx:1108
#: src/pages/UserPublicProfile.tsx:1137 #: src/pages/UserPublicProfile.tsx:1150
#: src/pages/UserPublicProfile.tsx:1182 #: src/pages/UserPublicProfile.tsx:1195
msgid "Failed to load" msgid "Failed to load"
msgstr "Failed to load" msgstr "Failed to load"
@@ -457,10 +457,10 @@ msgid "Failed to post reply"
msgstr "Failed to post reply" msgstr "Failed to post reply"
#: src/pages/PlaylistDetail.tsx:795 #: src/pages/PlaylistDetail.tsx:795
#: src/pages/UserPublicProfile.tsx:688 #: src/pages/UserPublicProfile.tsx:701
#: src/pages/UserPublicProfile.tsx:726 #: src/pages/UserPublicProfile.tsx:739
#: src/pages/UserPublicProfile.tsx:855 #: src/pages/UserPublicProfile.tsx:868
#: src/pages/UserPublicProfile.tsx:935 #: src/pages/UserPublicProfile.tsx:948
msgid "Failed to save" msgid "Failed to save"
msgstr "Failed to save" msgstr "Failed to save"
@@ -468,7 +468,7 @@ msgstr "Failed to save"
msgid "Failed to save edit" msgid "Failed to save edit"
msgstr "Failed to save edit" msgstr "Failed to save edit"
#: src/pages/UserPublicProfile.tsx:880 #: src/pages/UserPublicProfile.tsx:893
msgid "Failed to update avatar" msgid "Failed to update avatar"
msgstr "Failed to update avatar" msgstr "Failed to update avatar"
@@ -510,8 +510,8 @@ msgstr "Follow some public playlists to see their dumps here."
msgid "Follow some users to see their dumps here." msgid "Follow some users to see their dumps here."
msgstr "Follow some users to see their dumps here." msgstr "Follow some users to see their dumps here."
#: src/components/FeedTabBar.tsx:20 #: src/components/FeedTabBar.tsx:16
#: src/pages/UserPublicProfile.tsx:978 #: src/pages/UserPublicProfile.tsx:991
msgid "Followed" msgid "Followed"
msgstr "Followed" msgstr "Followed"
@@ -521,13 +521,13 @@ msgstr "Followed"
msgid "Followed ({0}{1})" msgid "Followed ({0}{1})"
msgstr "Followed ({0}{1})" msgstr "Followed ({0}{1})"
#: src/pages/UserPublicProfile.tsx:1126 #: src/pages/UserPublicProfile.tsx:1139
msgid "Followed playlists" msgid "Followed playlists"
msgstr "Followed playlists" msgstr "Followed playlists"
#: src/components/FollowButton.tsx:37 #: src/components/FollowButton.tsx:37
#: src/components/FollowButton.tsx:64 #: src/components/FollowButton.tsx:64
#: src/pages/UserPublicProfile.tsx:1084 #: src/pages/UserPublicProfile.tsx:1097
msgid "Following" msgid "Following"
msgstr "Following" msgstr "Following"
@@ -551,7 +551,7 @@ msgstr "Go home"
msgid "Go to login" msgid "Go to login"
msgstr "Go to login" msgstr "Go to login"
#: src/components/FeedTabBar.tsx:16 #: src/components/FeedTabBar.tsx:12
msgid "Hot" msgid "Hot"
msgstr "Hot" msgstr "Hot"
@@ -567,16 +567,16 @@ msgstr "Invalid invite"
msgid "Invalid link" msgid "Invalid link"
msgstr "Invalid link" msgstr "Invalid link"
#: src/pages/UserPublicProfile.tsx:799 #: src/pages/UserPublicProfile.tsx:812
msgid "invited by" msgid "invited by"
msgstr "invited by" msgstr "invited by"
#: src/pages/UserPublicProfile.tsx:979 #: src/pages/UserPublicProfile.tsx:992
#: src/pages/UserPublicProfile.tsx:1171 #: src/pages/UserPublicProfile.tsx:1184
msgid "Invitees" msgid "Invitees"
msgstr "Invitees" msgstr "Invitees"
#: src/components/FeedTabBar.tsx:18 #: src/components/FeedTabBar.tsx:14
msgid "Journal" msgid "Journal"
msgstr "Journal" msgstr "Journal"
@@ -584,7 +584,7 @@ msgstr "Journal"
msgid "just now" msgid "just now"
msgstr "just now" msgstr "just now"
#: src/pages/UserPublicProfile.tsx:1264 #: src/pages/UserPublicProfile.tsx:1277
msgid "Light" msgid "Light"
msgstr "Light" msgstr "Light"
@@ -614,7 +614,7 @@ msgstr "Loading dump…"
#: src/pages/index/HotFeed.tsx:64 #: src/pages/index/HotFeed.tsx:64
#: src/pages/index/JournalFeed.tsx:77 #: src/pages/index/JournalFeed.tsx:77
#: src/pages/index/NewFeed.tsx:64 #: src/pages/index/NewFeed.tsx:64
#: src/pages/Search.tsx:248 #: src/pages/Search.tsx:242
#: src/pages/UserDumps.tsx:93 #: src/pages/UserDumps.tsx:93
#: src/pages/UserPlaylists.tsx:417 #: src/pages/UserPlaylists.tsx:417
#: src/pages/UserPlaylists.tsx:452 #: src/pages/UserPlaylists.tsx:452
@@ -626,7 +626,7 @@ msgstr "Loading more…"
msgid "Loading playlist…" msgid "Loading playlist…"
msgstr "Loading playlist…" msgstr "Loading playlist…"
#: src/pages/UserPublicProfile.tsx:736 #: src/pages/UserPublicProfile.tsx:749
msgid "Loading profile…" msgid "Loading profile…"
msgstr "Loading profile…" msgstr "Loading profile…"
@@ -642,9 +642,9 @@ msgstr "Loading profile…"
#: src/pages/Notifications.tsx:439 #: src/pages/Notifications.tsx:439
#: src/pages/UserDumps.tsx:51 #: src/pages/UserDumps.tsx:51
#: src/pages/UserPlaylists.tsx:342 #: src/pages/UserPlaylists.tsx:342
#: src/pages/UserPublicProfile.tsx:1089 #: src/pages/UserPublicProfile.tsx:1102
#: src/pages/UserPublicProfile.tsx:1131 #: src/pages/UserPublicProfile.tsx:1144
#: src/pages/UserPublicProfile.tsx:1176 #: src/pages/UserPublicProfile.tsx:1189
#: src/pages/UserUpvoted.tsx:122 #: src/pages/UserUpvoted.tsx:122
msgid "Loading…" msgid "Loading…"
msgstr "Loading…" msgstr "Loading…"
@@ -663,8 +663,8 @@ msgstr "Log in to like"
msgid "Log in to vote" msgid "Log in to vote"
msgstr "Log in to vote" msgstr "Log in to vote"
#: src/pages/UserPublicProfile.tsx:757 #: src/pages/UserPublicProfile.tsx:770
#: src/pages/UserPublicProfile.tsx:894 #: src/pages/UserPublicProfile.tsx:907
msgid "Log out" msgid "Log out"
msgstr "Log out" msgstr "Log out"
@@ -684,13 +684,13 @@ msgstr "Max 50 MB"
msgid "new" msgid "new"
msgstr "new" msgstr "new"
#: src/components/FeedTabBar.tsx:17 #: src/components/FeedTabBar.tsx:13
msgid "New" msgid "New"
msgstr "New" msgstr "New"
#: src/components/DumpCreateModal.tsx:302 #: src/components/DumpCreateModal.tsx:302
#: src/pages/UserDumps.tsx:115 #: src/pages/UserDumps.tsx:115
#: src/pages/UserPublicProfile.tsx:1320 #: src/pages/UserPublicProfile.tsx:1333
msgid "New dump" msgid "New dump"
msgstr "New dump" msgstr "New dump"
@@ -708,7 +708,7 @@ msgstr "New playlist"
msgid "No dumps in this playlist yet." msgid "No dumps in this playlist yet."
msgstr "No dumps in this playlist yet." msgstr "No dumps in this playlist yet."
#: src/pages/Search.tsx:228 #: src/pages/Search.tsx:222
msgid "No dumps match \"{q}\"." msgid "No dumps match \"{q}\"."
msgstr "No dumps match \"{q}\"." msgstr "No dumps match \"{q}\"."
@@ -723,11 +723,11 @@ msgid "No emoji found."
msgstr "No emoji found." msgstr "No emoji found."
#: src/pages/UserPlaylists.tsx:439 #: src/pages/UserPlaylists.tsx:439
#: src/pages/UserPublicProfile.tsx:1144 #: src/pages/UserPublicProfile.tsx:1157
msgid "No followed playlists yet." msgid "No followed playlists yet."
msgstr "No followed playlists yet." msgstr "No followed playlists yet."
#: src/pages/UserPublicProfile.tsx:1189 #: src/pages/UserPublicProfile.tsx:1202
msgid "No invitees yet." msgid "No invitees yet."
msgstr "No invitees yet." msgstr "No invitees yet."
@@ -735,28 +735,28 @@ msgstr "No invitees yet."
msgid "No one yet." msgid "No one yet."
msgstr "No one yet." msgstr "No one yet."
#: src/pages/Search.tsx:287 #: src/pages/Search.tsx:281
msgid "No playlists match \"{q}\"." msgid "No playlists match \"{q}\"."
msgstr "No playlists match \"{q}\"." msgstr "No playlists match \"{q}\"."
#: src/components/PlaylistMembershipPanel.tsx:34 #: src/components/PlaylistMembershipPanel.tsx:34
#: src/pages/UserPlaylists.tsx:397 #: src/pages/UserPlaylists.tsx:397
#: src/pages/UserPublicProfile.tsx:1055 #: src/pages/UserPublicProfile.tsx:1068
msgid "No playlists yet." msgid "No playlists yet."
msgstr "No playlists yet." msgstr "No playlists yet."
#: src/pages/Search.tsx:261 #: src/pages/Search.tsx:255
msgid "No users match \"{q}\"." msgid "No users match \"{q}\"."
msgstr "No users match \"{q}\"." msgstr "No users match \"{q}\"."
#: src/pages/UserPublicProfile.tsx:1102 #: src/pages/UserPublicProfile.tsx:1115
msgid "Not following anyone yet." msgid "Not following anyone yet."
msgstr "Not following anyone yet." msgstr "Not following anyone yet."
#: src/pages/Notifications.tsx:374 #: src/pages/Notifications.tsx:374
#: src/pages/UserDumps.tsx:125 #: src/pages/UserDumps.tsx:125
#: src/pages/UserPublicProfile.tsx:1331 #: src/pages/UserPublicProfile.tsx:1344
#: src/pages/UserPublicProfile.tsx:1453 #: src/pages/UserPublicProfile.tsx:1466
#: src/pages/UserUpvoted.tsx:194 #: src/pages/UserUpvoted.tsx:194
msgid "Nothing here yet." msgid "Nothing here yet."
msgstr "Nothing here yet." msgstr "Nothing here yet."
@@ -779,7 +779,7 @@ msgid "or <0>browse files</0>"
msgstr "or <0>browse files</0>" msgstr "or <0>browse files</0>"
#: src/pages/UserLogin.tsx:106 #: src/pages/UserLogin.tsx:106
#: src/pages/UserPublicProfile.tsx:1209 #: src/pages/UserPublicProfile.tsx:1222
msgid "Password" msgid "Password"
msgstr "Password" msgstr "Password"
@@ -803,15 +803,15 @@ msgstr "Passwords do not match"
#: src/components/AppHeader.tsx:85 #: src/components/AppHeader.tsx:85
#: src/components/UserMenu.tsx:62 #: src/components/UserMenu.tsx:62
#: src/pages/Search.tsx:181 #: src/pages/Search.tsx:175
#: src/pages/UserPlaylists.tsx:368 #: src/pages/UserPlaylists.tsx:368
#: src/pages/UserPublicProfile.tsx:977 #: src/pages/UserPublicProfile.tsx:990
msgid "Playlists" msgid "Playlists"
msgstr "Playlists" msgstr "Playlists"
#. placeholder {0}: playlists.items.length #. placeholder {0}: playlists.items.length
#. placeholder {1}: playlists.hasMore ? "+" : "" #. placeholder {1}: playlists.hasMore ? "+" : ""
#: src/pages/UserPublicProfile.tsx:1024 #: src/pages/UserPublicProfile.tsx:1037
msgid "Playlists ({0}{1})" msgid "Playlists ({0}{1})"
msgstr "Playlists ({0}{1})" msgstr "Playlists ({0}{1})"
@@ -930,8 +930,8 @@ msgstr "Retry"
#: src/pages/Dump.tsx:347 #: src/pages/Dump.tsx:347
#: src/pages/DumpEdit.tsx:400 #: src/pages/DumpEdit.tsx:400
#: src/pages/PlaylistDetail.tsx:679 #: src/pages/PlaylistDetail.tsx:679
#: src/pages/UserPublicProfile.tsx:842 #: src/pages/UserPublicProfile.tsx:855
#: src/pages/UserPublicProfile.tsx:924 #: src/pages/UserPublicProfile.tsx:937
msgid "Save" msgid "Save"
msgstr "Save" msgstr "Save"
@@ -940,8 +940,8 @@ msgstr "Save"
#: src/pages/Dump.tsx:347 #: src/pages/Dump.tsx:347
#: src/pages/PlaylistDetail.tsx:679 #: src/pages/PlaylistDetail.tsx:679
#: src/pages/ResetPassword.tsx:152 #: src/pages/ResetPassword.tsx:152
#: src/pages/UserPublicProfile.tsx:841 #: src/pages/UserPublicProfile.tsx:854
#: src/pages/UserPublicProfile.tsx:924 #: src/pages/UserPublicProfile.tsx:937
msgid "Saving…" msgid "Saving…"
msgstr "Saving…" msgstr "Saving…"
@@ -954,11 +954,11 @@ msgstr "Search"
msgid "Search dumps, users, playlists…" msgid "Search dumps, users, playlists…"
msgstr "Search dumps, users, playlists…" msgstr "Search dumps, users, playlists…"
#: src/pages/Search.tsx:222 #: src/pages/Search.tsx:216
msgid "Search failed" msgid "Search failed"
msgstr "Search failed" msgstr "Search failed"
#: src/pages/Search.tsx:217 #: src/pages/Search.tsx:211
msgid "Searching…" msgid "Searching…"
msgstr "Searching…" msgstr "Searching…"
@@ -979,7 +979,7 @@ msgstr "Server unreachable"
msgid "Set new password" msgid "Set new password"
msgstr "Set new password" msgstr "Set new password"
#: src/pages/UserPublicProfile.tsx:981 #: src/pages/UserPublicProfile.tsx:994
msgid "Settings" msgid "Settings"
msgstr "Settings" msgstr "Settings"
@@ -987,7 +987,7 @@ msgstr "Settings"
msgid "Something went wrong" msgid "Something went wrong"
msgstr "Something went wrong" msgstr "Something went wrong"
#: src/pages/UserPublicProfile.tsx:1228 #: src/pages/UserPublicProfile.tsx:1241
msgid "Style" msgid "Style"
msgstr "Style" msgstr "Style"
@@ -1042,7 +1042,7 @@ msgstr "Unfollow playlist"
msgid "Unknown error" msgid "Unknown error"
msgstr "Unknown error" msgstr "Unknown error"
#: src/pages/UserPublicProfile.tsx:657 #: src/pages/UserPublicProfile.tsx:670
msgid "Upload failed" msgid "Upload failed"
msgstr "Upload failed" msgstr "Upload failed"
@@ -1060,7 +1060,7 @@ msgstr "Upvoted"
#. placeholder {0}: votes.items.length #. placeholder {0}: votes.items.length
#. placeholder {1}: votes.hasMore ? "+" : "" #. placeholder {1}: votes.hasMore ? "+" : ""
#: src/pages/UserPublicProfile.tsx:1004 #: src/pages/UserPublicProfile.tsx:1017
msgid "Upvoted ({0}{1})" msgid "Upvoted ({0}{1})"
msgstr "Upvoted ({0}{1})" msgstr "Upvoted ({0}{1})"
@@ -1082,15 +1082,15 @@ msgstr "User menu"
msgid "Username" msgid "Username"
msgstr "Username" msgstr "Username"
#: src/pages/Search.tsx:180 #: src/pages/Search.tsx:174
msgid "Users" msgid "Users"
msgstr "Users" msgstr "Users"
#: src/pages/UserPublicProfile.tsx:1074 #: src/pages/UserPublicProfile.tsx:1087
#: src/pages/UserPublicProfile.tsx:1117 #: src/pages/UserPublicProfile.tsx:1130
#: src/pages/UserPublicProfile.tsx:1159 #: src/pages/UserPublicProfile.tsx:1172
#: src/pages/UserPublicProfile.tsx:1352 #: src/pages/UserPublicProfile.tsx:1365
#: src/pages/UserPublicProfile.tsx:1483 #: src/pages/UserPublicProfile.tsx:1496
msgid "View all →" msgid "View all →"
msgstr "View all →" msgstr "View all →"
@@ -1103,8 +1103,8 @@ msgstr "View dump →"
msgid "What makes it worth it?" msgid "What makes it worth it?"
msgstr "What makes it worth it?" msgstr "What makes it worth it?"
#: src/pages/UserPublicProfile.tsx:912 #: src/pages/UserPublicProfile.tsx:925
#: src/pages/UserPublicProfile.tsx:961 #: src/pages/UserPublicProfile.tsx:974
msgid "Who am I?" msgid "Who am I?"
msgstr "Who am I?" msgstr "Who am I?"
@@ -1129,7 +1129,7 @@ msgstr "You'll be notified when someone follows your playlists, upvotes your dum
#: src/pages/index/HotFeed.tsx:69 #: src/pages/index/HotFeed.tsx:69
#: src/pages/index/JournalFeed.tsx:82 #: src/pages/index/JournalFeed.tsx:82
#: src/pages/index/NewFeed.tsx:69 #: src/pages/index/NewFeed.tsx:69
#: src/pages/Search.tsx:253 #: src/pages/Search.tsx:247
#: src/pages/UserDumps.tsx:98 #: src/pages/UserDumps.tsx:98
#: src/pages/UserPlaylists.tsx:422 #: src/pages/UserPlaylists.tsx:422
#: src/pages/UserPlaylists.tsx:457 #: src/pages/UserPlaylists.tsx:457

File diff suppressed because one or more lines are too long

View File

@@ -42,7 +42,7 @@ msgstr "il y a {days}j"
msgid "{hrs}h ago" msgid "{hrs}h ago"
msgstr "il y a {hrs}h" msgstr "il y a {hrs}h"
#: src/pages/Search.tsx:182 #: src/pages/Search.tsx:176
msgid "{label} ({count})" msgid "{label} ({count})"
msgstr "{label} ({count})" msgstr "{label} ({count})"
@@ -55,7 +55,7 @@ msgid "{visibleCount, plural, one {# comment} other {# comments}}"
msgstr "{visibleCount, plural, one {# commentaire} other {# commentaires}}" msgstr "{visibleCount, plural, one {# commentaire} other {# commentaires}}"
#: src/pages/PlaylistDetail.tsx:617 #: src/pages/PlaylistDetail.tsx:617
#: src/pages/UserPublicProfile.tsx:753 #: src/pages/UserPublicProfile.tsx:766
msgid "← Back" msgid "← Back"
msgstr "← Retour" msgstr "← Retour"
@@ -71,7 +71,7 @@ msgstr "← Retour à toutes les recos"
msgid "← Back to profile" msgid "← Back to profile"
msgstr "← Retour au profil" msgstr "← Retour au profil"
#: src/pages/UserPublicProfile.tsx:101 #: src/pages/UserPublicProfile.tsx:102
msgid "+ Invite someone" msgid "+ Invite someone"
msgstr "+ Inviter quelqu'un" msgstr "+ Inviter quelqu'un"
@@ -142,7 +142,7 @@ msgstr "un commentaire"
msgid "a post" msgid "a post"
msgstr "une publication" msgstr "une publication"
#: src/pages/UserPublicProfile.tsx:1204 #: src/pages/UserPublicProfile.tsx:1217
msgid "Account" msgid "Account"
msgstr "Compte" msgstr "Compte"
@@ -150,7 +150,7 @@ msgstr "Compte"
msgid "Add a comment…" msgid "Add a comment…"
msgstr "Ajouter un commentaire…" msgstr "Ajouter un commentaire…"
#: src/pages/UserPublicProfile.tsx:871 #: src/pages/UserPublicProfile.tsx:884
msgid "Add email…" msgid "Add email…"
msgstr "Ajouter un e-mail…" msgstr "Ajouter un e-mail…"
@@ -171,7 +171,7 @@ msgstr "Toutes les {0, plural, one {# reco votée} other {# recos votées}} char
msgid "Already have an account? <0>Log in</0>" msgid "Already have an account? <0>Log in</0>"
msgstr "Vous avez déjà un compte ? <0>Se connecter</0>" msgstr "Vous avez déjà un compte ? <0>Se connecter</0>"
#: src/pages/UserPublicProfile.tsx:1223 #: src/pages/UserPublicProfile.tsx:1236
msgid "Appearance" msgid "Appearance"
msgstr "Apparence" msgstr "Apparence"
@@ -181,7 +181,7 @@ msgstr "Apparence"
msgid "At least {0} characters" msgid "At least {0} characters"
msgstr "Au moins {0} caractères" msgstr "Au moins {0} caractères"
#: src/pages/UserPublicProfile.tsx:1257 #: src/pages/UserPublicProfile.tsx:1270
msgid "Auto" msgid "Auto"
msgstr "Auto" msgstr "Auto"
@@ -205,8 +205,8 @@ msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les vo
#: src/pages/Dump.tsx:355 #: src/pages/Dump.tsx:355
#: src/pages/DumpEdit.tsx:391 #: src/pages/DumpEdit.tsx:391
#: src/pages/PlaylistDetail.tsx:686 #: src/pages/PlaylistDetail.tsx:686
#: src/pages/UserPublicProfile.tsx:850 #: src/pages/UserPublicProfile.tsx:863
#: src/pages/UserPublicProfile.tsx:932 #: src/pages/UserPublicProfile.tsx:945
msgid "Cancel" msgid "Cancel"
msgstr "Annuler" msgstr "Annuler"
@@ -218,7 +218,7 @@ msgstr "Annuler la suppression"
msgid "Cancel search" msgid "Cancel search"
msgstr "Annuler la recherche" msgstr "Annuler la recherche"
#: src/pages/UserPublicProfile.tsx:780 #: src/pages/UserPublicProfile.tsx:793
msgid "Change avatar" msgid "Change avatar"
msgstr "Changer l'avatar" msgstr "Changer l'avatar"
@@ -227,7 +227,7 @@ msgstr "Changer l'avatar"
msgid "Change password" msgid "Change password"
msgstr "Changer le mot de passe" msgstr "Changer le mot de passe"
#: src/pages/UserPublicProfile.tsx:1216 #: src/pages/UserPublicProfile.tsx:1229
msgid "Change password…" msgid "Change password…"
msgstr "Changer le mot de passe…" msgstr "Changer le mot de passe…"
@@ -240,7 +240,7 @@ msgstr "Vérification de l'invitation…"
msgid "Close" msgid "Close"
msgstr "Fermer" msgstr "Fermer"
#: src/pages/UserPublicProfile.tsx:1249 #: src/pages/UserPublicProfile.tsx:1262
msgid "Color scheme" msgid "Color scheme"
msgstr "Thème de couleur" msgstr "Thème de couleur"
@@ -249,11 +249,11 @@ msgstr "Thème de couleur"
msgid "Confirm new password" msgid "Confirm new password"
msgstr "Confirmer le nouveau mot de passe" msgstr "Confirmer le nouveau mot de passe"
#: src/pages/UserPublicProfile.tsx:92 #: src/pages/UserPublicProfile.tsx:93
msgid "Copied!" msgid "Copied!"
msgstr "Copié !" msgstr "Copié !"
#: src/pages/UserPublicProfile.tsx:92 #: src/pages/UserPublicProfile.tsx:93
msgid "Copy" msgid "Copy"
msgstr "Copier" msgstr "Copier"
@@ -298,7 +298,7 @@ msgstr "Création…"
msgid "Current password" msgid "Current password"
msgstr "Mot de passe actuel" msgstr "Mot de passe actuel"
#: src/pages/UserPublicProfile.tsx:1271 #: src/pages/UserPublicProfile.tsx:1284
msgid "Dark" msgid "Dark"
msgstr "Sombre" msgstr "Sombre"
@@ -362,15 +362,15 @@ msgstr "Recommander"
msgid "Dumped!" msgid "Dumped!"
msgstr "Recommandé !" msgstr "Recommandé !"
#: src/pages/Search.tsx:178 #: src/pages/Search.tsx:172
#: src/pages/UserDumps.tsx:107 #: src/pages/UserDumps.tsx:107
#: src/pages/UserPublicProfile.tsx:976 #: src/pages/UserPublicProfile.tsx:989
msgid "Dumps" msgid "Dumps"
msgstr "Recos" msgstr "Recos"
#. placeholder {0}: dumps.items.length #. placeholder {0}: dumps.items.length
#. placeholder {1}: dumps.hasMore ? "+" : "" #. placeholder {1}: dumps.hasMore ? "+" : ""
#: src/pages/UserPublicProfile.tsx:993 #: src/pages/UserPublicProfile.tsx:1006
msgid "Dumps ({0}{1})" msgid "Dumps ({0}{1})"
msgstr "Recos ({0}{1})" msgstr "Recos ({0}{1})"
@@ -414,7 +414,7 @@ msgstr "Modification"
msgid "Email address" msgid "Email address"
msgstr "Adresse e-mail" msgstr "Adresse e-mail"
#: src/pages/Search.tsx:211 #: src/pages/Search.tsx:205
msgid "Enter a query to search." msgid "Enter a query to search."
msgstr "Saisissez une recherche." msgstr "Saisissez une recherche."
@@ -427,9 +427,9 @@ msgstr "Impossible de changer le mot de passe"
msgid "Failed to create playlist" msgid "Failed to create playlist"
msgstr "Impossible de créer la collection" msgstr "Impossible de créer la collection"
#: src/pages/UserPublicProfile.tsx:73 #: src/pages/UserPublicProfile.tsx:74
#: src/pages/UserPublicProfile.tsx:76 #: src/pages/UserPublicProfile.tsx:77
#: src/pages/UserPublicProfile.tsx:104 #: src/pages/UserPublicProfile.tsx:105
msgid "Failed to generate invite" msgid "Failed to generate invite"
msgstr "Impossible de générer une invitation" msgstr "Impossible de générer une invitation"
@@ -438,9 +438,9 @@ msgstr "Impossible de générer une invitation"
#: src/pages/index/JournalFeed.tsx:48 #: src/pages/index/JournalFeed.tsx:48
#: src/pages/index/NewFeed.tsx:36 #: src/pages/index/NewFeed.tsx:36
#: src/pages/Notifications.tsx:367 #: src/pages/Notifications.tsx:367
#: src/pages/UserPublicProfile.tsx:1095 #: src/pages/UserPublicProfile.tsx:1108
#: src/pages/UserPublicProfile.tsx:1137 #: src/pages/UserPublicProfile.tsx:1150
#: src/pages/UserPublicProfile.tsx:1182 #: src/pages/UserPublicProfile.tsx:1195
msgid "Failed to load" msgid "Failed to load"
msgstr "Chargement échoué" msgstr "Chargement échoué"
@@ -457,10 +457,10 @@ msgid "Failed to post reply"
msgstr "Impossible de publier la réponse" msgstr "Impossible de publier la réponse"
#: src/pages/PlaylistDetail.tsx:795 #: src/pages/PlaylistDetail.tsx:795
#: src/pages/UserPublicProfile.tsx:688 #: src/pages/UserPublicProfile.tsx:701
#: src/pages/UserPublicProfile.tsx:726 #: src/pages/UserPublicProfile.tsx:739
#: src/pages/UserPublicProfile.tsx:855 #: src/pages/UserPublicProfile.tsx:868
#: src/pages/UserPublicProfile.tsx:935 #: src/pages/UserPublicProfile.tsx:948
msgid "Failed to save" msgid "Failed to save"
msgstr "Enregistrement échoué" msgstr "Enregistrement échoué"
@@ -468,7 +468,7 @@ msgstr "Enregistrement échoué"
msgid "Failed to save edit" msgid "Failed to save edit"
msgstr "Impossible d'enregistrer la modification" msgstr "Impossible d'enregistrer la modification"
#: src/pages/UserPublicProfile.tsx:880 #: src/pages/UserPublicProfile.tsx:893
msgid "Failed to update avatar" msgid "Failed to update avatar"
msgstr "Impossible de mettre à jour l'avatar" msgstr "Impossible de mettre à jour l'avatar"
@@ -510,8 +510,8 @@ msgstr "Suivez des collections publiques pour voir leurs recos ici."
msgid "Follow some users to see their dumps here." msgid "Follow some users to see their dumps here."
msgstr "Suivez des utilisateurs pour voir leurs recos ici." msgstr "Suivez des utilisateurs pour voir leurs recos ici."
#: src/components/FeedTabBar.tsx:20 #: src/components/FeedTabBar.tsx:16
#: src/pages/UserPublicProfile.tsx:978 #: src/pages/UserPublicProfile.tsx:991
msgid "Followed" msgid "Followed"
msgstr "Suivi" msgstr "Suivi"
@@ -521,13 +521,13 @@ msgstr "Suivi"
msgid "Followed ({0}{1})" msgid "Followed ({0}{1})"
msgstr "Suivies ({0}{1})" msgstr "Suivies ({0}{1})"
#: src/pages/UserPublicProfile.tsx:1126 #: src/pages/UserPublicProfile.tsx:1139
msgid "Followed playlists" msgid "Followed playlists"
msgstr "Collections suivies" msgstr "Collections suivies"
#: src/components/FollowButton.tsx:37 #: src/components/FollowButton.tsx:37
#: src/components/FollowButton.tsx:64 #: src/components/FollowButton.tsx:64
#: src/pages/UserPublicProfile.tsx:1084 #: src/pages/UserPublicProfile.tsx:1097
msgid "Following" msgid "Following"
msgstr "Abonné" msgstr "Abonné"
@@ -551,7 +551,7 @@ msgstr "Accueil"
msgid "Go to login" msgid "Go to login"
msgstr "Aller à la connexion" msgstr "Aller à la connexion"
#: src/components/FeedTabBar.tsx:16 #: src/components/FeedTabBar.tsx:12
msgid "Hot" msgid "Hot"
msgstr "Tendances" msgstr "Tendances"
@@ -567,16 +567,16 @@ msgstr "Invitation invalide"
msgid "Invalid link" msgid "Invalid link"
msgstr "Lien invalide" msgstr "Lien invalide"
#: src/pages/UserPublicProfile.tsx:799 #: src/pages/UserPublicProfile.tsx:812
msgid "invited by" msgid "invited by"
msgstr "invité par" msgstr "invité par"
#: src/pages/UserPublicProfile.tsx:979 #: src/pages/UserPublicProfile.tsx:992
#: src/pages/UserPublicProfile.tsx:1171 #: src/pages/UserPublicProfile.tsx:1184
msgid "Invitees" msgid "Invitees"
msgstr "Invités" msgstr "Invités"
#: src/components/FeedTabBar.tsx:18 #: src/components/FeedTabBar.tsx:14
msgid "Journal" msgid "Journal"
msgstr "Journal" msgstr "Journal"
@@ -584,7 +584,7 @@ msgstr "Journal"
msgid "just now" msgid "just now"
msgstr "à l'instant" msgstr "à l'instant"
#: src/pages/UserPublicProfile.tsx:1264 #: src/pages/UserPublicProfile.tsx:1277
msgid "Light" msgid "Light"
msgstr "Clair" msgstr "Clair"
@@ -614,7 +614,7 @@ msgstr "Chargement de la reco…"
#: src/pages/index/HotFeed.tsx:64 #: src/pages/index/HotFeed.tsx:64
#: src/pages/index/JournalFeed.tsx:77 #: src/pages/index/JournalFeed.tsx:77
#: src/pages/index/NewFeed.tsx:64 #: src/pages/index/NewFeed.tsx:64
#: src/pages/Search.tsx:248 #: src/pages/Search.tsx:242
#: src/pages/UserDumps.tsx:93 #: src/pages/UserDumps.tsx:93
#: src/pages/UserPlaylists.tsx:417 #: src/pages/UserPlaylists.tsx:417
#: src/pages/UserPlaylists.tsx:452 #: src/pages/UserPlaylists.tsx:452
@@ -626,7 +626,7 @@ msgstr "Chargement…"
msgid "Loading playlist…" msgid "Loading playlist…"
msgstr "Chargement de la collection…" msgstr "Chargement de la collection…"
#: src/pages/UserPublicProfile.tsx:736 #: src/pages/UserPublicProfile.tsx:749
msgid "Loading profile…" msgid "Loading profile…"
msgstr "Chargement du profil…" msgstr "Chargement du profil…"
@@ -642,9 +642,9 @@ msgstr "Chargement du profil…"
#: src/pages/Notifications.tsx:439 #: src/pages/Notifications.tsx:439
#: src/pages/UserDumps.tsx:51 #: src/pages/UserDumps.tsx:51
#: src/pages/UserPlaylists.tsx:342 #: src/pages/UserPlaylists.tsx:342
#: src/pages/UserPublicProfile.tsx:1089 #: src/pages/UserPublicProfile.tsx:1102
#: src/pages/UserPublicProfile.tsx:1131 #: src/pages/UserPublicProfile.tsx:1144
#: src/pages/UserPublicProfile.tsx:1176 #: src/pages/UserPublicProfile.tsx:1189
#: src/pages/UserUpvoted.tsx:122 #: src/pages/UserUpvoted.tsx:122
msgid "Loading…" msgid "Loading…"
msgstr "Chargement…" msgstr "Chargement…"
@@ -663,8 +663,8 @@ msgstr "Se connecter pour aimer"
msgid "Log in to vote" msgid "Log in to vote"
msgstr "Se connecter pour voter" msgstr "Se connecter pour voter"
#: src/pages/UserPublicProfile.tsx:757 #: src/pages/UserPublicProfile.tsx:770
#: src/pages/UserPublicProfile.tsx:894 #: src/pages/UserPublicProfile.tsx:907
msgid "Log out" msgid "Log out"
msgstr "Se déconnecter" msgstr "Se déconnecter"
@@ -684,13 +684,13 @@ msgstr "Max 50 Mo"
msgid "new" msgid "new"
msgstr "nouveau" msgstr "nouveau"
#: src/components/FeedTabBar.tsx:17 #: src/components/FeedTabBar.tsx:13
msgid "New" msgid "New"
msgstr "Nouveau" msgstr "Nouveau"
#: src/components/DumpCreateModal.tsx:302 #: src/components/DumpCreateModal.tsx:302
#: src/pages/UserDumps.tsx:115 #: src/pages/UserDumps.tsx:115
#: src/pages/UserPublicProfile.tsx:1320 #: src/pages/UserPublicProfile.tsx:1333
msgid "New dump" msgid "New dump"
msgstr "Nouvelle reco" msgstr "Nouvelle reco"
@@ -708,7 +708,7 @@ msgstr "Nouvelle collection"
msgid "No dumps in this playlist yet." msgid "No dumps in this playlist yet."
msgstr "Aucune reco dans cette collection pour l'instant." msgstr "Aucune reco dans cette collection pour l'instant."
#: src/pages/Search.tsx:228 #: src/pages/Search.tsx:222
msgid "No dumps match \"{q}\"." msgid "No dumps match \"{q}\"."
msgstr "Aucune reco ne correspond à « {q} »." msgstr "Aucune reco ne correspond à « {q} »."
@@ -723,11 +723,11 @@ msgid "No emoji found."
msgstr "Aucun emoji trouvé." msgstr "Aucun emoji trouvé."
#: src/pages/UserPlaylists.tsx:439 #: src/pages/UserPlaylists.tsx:439
#: src/pages/UserPublicProfile.tsx:1144 #: src/pages/UserPublicProfile.tsx:1157
msgid "No followed playlists yet." msgid "No followed playlists yet."
msgstr "Pas encore de collections suivies." msgstr "Pas encore de collections suivies."
#: src/pages/UserPublicProfile.tsx:1189 #: src/pages/UserPublicProfile.tsx:1202
msgid "No invitees yet." msgid "No invitees yet."
msgstr "Aucun invité pour le moment." msgstr "Aucun invité pour le moment."
@@ -735,28 +735,28 @@ msgstr "Aucun invité pour le moment."
msgid "No one yet." msgid "No one yet."
msgstr "Personne pour le moment." msgstr "Personne pour le moment."
#: src/pages/Search.tsx:287 #: src/pages/Search.tsx:281
msgid "No playlists match \"{q}\"." msgid "No playlists match \"{q}\"."
msgstr "Aucune collection ne correspond à « {q} »." msgstr "Aucune collection ne correspond à « {q} »."
#: src/components/PlaylistMembershipPanel.tsx:34 #: src/components/PlaylistMembershipPanel.tsx:34
#: src/pages/UserPlaylists.tsx:397 #: src/pages/UserPlaylists.tsx:397
#: src/pages/UserPublicProfile.tsx:1055 #: src/pages/UserPublicProfile.tsx:1068
msgid "No playlists yet." msgid "No playlists yet."
msgstr "Pas encore de collections." msgstr "Pas encore de collections."
#: src/pages/Search.tsx:261 #: src/pages/Search.tsx:255
msgid "No users match \"{q}\"." msgid "No users match \"{q}\"."
msgstr "Aucun utilisateur ne correspond à « {q} »." msgstr "Aucun utilisateur ne correspond à « {q} »."
#: src/pages/UserPublicProfile.tsx:1102 #: src/pages/UserPublicProfile.tsx:1115
msgid "Not following anyone yet." msgid "Not following anyone yet."
msgstr "Aucun abonnement pour le moment." msgstr "Aucun abonnement pour le moment."
#: src/pages/Notifications.tsx:374 #: src/pages/Notifications.tsx:374
#: src/pages/UserDumps.tsx:125 #: src/pages/UserDumps.tsx:125
#: src/pages/UserPublicProfile.tsx:1331 #: src/pages/UserPublicProfile.tsx:1344
#: src/pages/UserPublicProfile.tsx:1453 #: src/pages/UserPublicProfile.tsx:1466
#: src/pages/UserUpvoted.tsx:194 #: src/pages/UserUpvoted.tsx:194
msgid "Nothing here yet." msgid "Nothing here yet."
msgstr "Rien ici pour l'instant." msgstr "Rien ici pour l'instant."
@@ -779,7 +779,7 @@ msgid "or <0>browse files</0>"
msgstr "ou <0>parcourir les fichiers</0>" msgstr "ou <0>parcourir les fichiers</0>"
#: src/pages/UserLogin.tsx:106 #: src/pages/UserLogin.tsx:106
#: src/pages/UserPublicProfile.tsx:1209 #: src/pages/UserPublicProfile.tsx:1222
msgid "Password" msgid "Password"
msgstr "Mot de passe" msgstr "Mot de passe"
@@ -803,15 +803,15 @@ msgstr "Les mots de passe ne correspondent pas"
#: src/components/AppHeader.tsx:85 #: src/components/AppHeader.tsx:85
#: src/components/UserMenu.tsx:62 #: src/components/UserMenu.tsx:62
#: src/pages/Search.tsx:181 #: src/pages/Search.tsx:175
#: src/pages/UserPlaylists.tsx:368 #: src/pages/UserPlaylists.tsx:368
#: src/pages/UserPublicProfile.tsx:977 #: src/pages/UserPublicProfile.tsx:990
msgid "Playlists" msgid "Playlists"
msgstr "Collections" msgstr "Collections"
#. placeholder {0}: playlists.items.length #. placeholder {0}: playlists.items.length
#. placeholder {1}: playlists.hasMore ? "+" : "" #. placeholder {1}: playlists.hasMore ? "+" : ""
#: src/pages/UserPublicProfile.tsx:1024 #: src/pages/UserPublicProfile.tsx:1037
msgid "Playlists ({0}{1})" msgid "Playlists ({0}{1})"
msgstr "Collections ({0}{1})" msgstr "Collections ({0}{1})"
@@ -883,7 +883,7 @@ msgstr "Inscription échouée"
#: src/pages/Dump.tsx:467 #: src/pages/Dump.tsx:467
msgid "Related" msgid "Related"
msgstr "Apparentés" msgstr "Connexe"
#: src/components/FileDropZone.tsx:115 #: src/components/FileDropZone.tsx:115
msgid "Remove file" msgid "Remove file"
@@ -930,8 +930,8 @@ msgstr "Réessayer"
#: src/pages/Dump.tsx:347 #: src/pages/Dump.tsx:347
#: src/pages/DumpEdit.tsx:400 #: src/pages/DumpEdit.tsx:400
#: src/pages/PlaylistDetail.tsx:679 #: src/pages/PlaylistDetail.tsx:679
#: src/pages/UserPublicProfile.tsx:842 #: src/pages/UserPublicProfile.tsx:855
#: src/pages/UserPublicProfile.tsx:924 #: src/pages/UserPublicProfile.tsx:937
msgid "Save" msgid "Save"
msgstr "Enregistrer" msgstr "Enregistrer"
@@ -940,8 +940,8 @@ msgstr "Enregistrer"
#: src/pages/Dump.tsx:347 #: src/pages/Dump.tsx:347
#: src/pages/PlaylistDetail.tsx:679 #: src/pages/PlaylistDetail.tsx:679
#: src/pages/ResetPassword.tsx:152 #: src/pages/ResetPassword.tsx:152
#: src/pages/UserPublicProfile.tsx:841 #: src/pages/UserPublicProfile.tsx:854
#: src/pages/UserPublicProfile.tsx:924 #: src/pages/UserPublicProfile.tsx:937
msgid "Saving…" msgid "Saving…"
msgstr "Enregistrement…" msgstr "Enregistrement…"
@@ -954,11 +954,11 @@ msgstr "Rechercher"
msgid "Search dumps, users, playlists…" msgid "Search dumps, users, playlists…"
msgstr "Rechercher des recos, utilisateurs, collections…" msgstr "Rechercher des recos, utilisateurs, collections…"
#: src/pages/Search.tsx:222 #: src/pages/Search.tsx:216
msgid "Search failed" msgid "Search failed"
msgstr "Recherche échouée" msgstr "Recherche échouée"
#: src/pages/Search.tsx:217 #: src/pages/Search.tsx:211
msgid "Searching…" msgid "Searching…"
msgstr "Recherche…" msgstr "Recherche…"
@@ -979,7 +979,7 @@ msgstr "Serveur inaccessible"
msgid "Set new password" msgid "Set new password"
msgstr "Définir un nouveau mot de passe" msgstr "Définir un nouveau mot de passe"
#: src/pages/UserPublicProfile.tsx:981 #: src/pages/UserPublicProfile.tsx:994
msgid "Settings" msgid "Settings"
msgstr "Paramètres" msgstr "Paramètres"
@@ -987,7 +987,7 @@ msgstr "Paramètres"
msgid "Something went wrong" msgid "Something went wrong"
msgstr "Une erreur est survenue" msgstr "Une erreur est survenue"
#: src/pages/UserPublicProfile.tsx:1228 #: src/pages/UserPublicProfile.tsx:1241
msgid "Style" msgid "Style"
msgstr "Style" msgstr "Style"
@@ -1042,7 +1042,7 @@ msgstr "Ne plus suivre la collection"
msgid "Unknown error" msgid "Unknown error"
msgstr "Erreur inconnue" msgstr "Erreur inconnue"
#: src/pages/UserPublicProfile.tsx:657 #: src/pages/UserPublicProfile.tsx:670
msgid "Upload failed" msgid "Upload failed"
msgstr "Envoi échoué" msgstr "Envoi échoué"
@@ -1060,7 +1060,7 @@ msgstr "Voté"
#. placeholder {0}: votes.items.length #. placeholder {0}: votes.items.length
#. placeholder {1}: votes.hasMore ? "+" : "" #. placeholder {1}: votes.hasMore ? "+" : ""
#: src/pages/UserPublicProfile.tsx:1004 #: src/pages/UserPublicProfile.tsx:1017
msgid "Upvoted ({0}{1})" msgid "Upvoted ({0}{1})"
msgstr "Votés ({0}{1})" msgstr "Votés ({0}{1})"
@@ -1082,15 +1082,15 @@ msgstr "Menu utilisateur"
msgid "Username" msgid "Username"
msgstr "Nom d'utilisateur" msgstr "Nom d'utilisateur"
#: src/pages/Search.tsx:180 #: src/pages/Search.tsx:174
msgid "Users" msgid "Users"
msgstr "Utilisateurs" msgstr "Utilisateurs"
#: src/pages/UserPublicProfile.tsx:1074 #: src/pages/UserPublicProfile.tsx:1087
#: src/pages/UserPublicProfile.tsx:1117 #: src/pages/UserPublicProfile.tsx:1130
#: src/pages/UserPublicProfile.tsx:1159 #: src/pages/UserPublicProfile.tsx:1172
#: src/pages/UserPublicProfile.tsx:1352 #: src/pages/UserPublicProfile.tsx:1365
#: src/pages/UserPublicProfile.tsx:1483 #: src/pages/UserPublicProfile.tsx:1496
msgid "View all →" msgid "View all →"
msgstr "Tout voir →" msgstr "Tout voir →"
@@ -1103,8 +1103,8 @@ msgstr "Voir la reco →"
msgid "What makes it worth it?" msgid "What makes it worth it?"
msgstr "Pourquoi on en voudrait ?" msgstr "Pourquoi on en voudrait ?"
#: src/pages/UserPublicProfile.tsx:912 #: src/pages/UserPublicProfile.tsx:925
#: src/pages/UserPublicProfile.tsx:961 #: src/pages/UserPublicProfile.tsx:974
msgid "Who am I?" msgid "Who am I?"
msgstr "Qui suis-je ?" msgstr "Qui suis-je ?"
@@ -1129,7 +1129,7 @@ msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vo
#: src/pages/index/HotFeed.tsx:69 #: src/pages/index/HotFeed.tsx:69
#: src/pages/index/JournalFeed.tsx:82 #: src/pages/index/JournalFeed.tsx:82
#: src/pages/index/NewFeed.tsx:69 #: src/pages/index/NewFeed.tsx:69
#: src/pages/Search.tsx:253 #: src/pages/Search.tsx:247
#: src/pages/UserDumps.tsx:98 #: src/pages/UserDumps.tsx:98
#: src/pages/UserPlaylists.tsx:422 #: src/pages/UserPlaylists.tsx:422
#: src/pages/UserPlaylists.tsx:457 #: src/pages/UserPlaylists.tsx:457

View File

@@ -11,7 +11,8 @@ import { useLocation } from "react-router";
import { AppHeader } from "../components/AppHeader.tsx"; import { AppHeader } from "../components/AppHeader.tsx";
import { PresenceRow } from "../components/PresenceRow.tsx"; import { PresenceRow } from "../components/PresenceRow.tsx";
import { FeedTabBar } from "../components/FeedTabBar.tsx"; import { FeedTabBar } from "../components/FeedTabBar.tsx";
import { type FeedTab, VALID_TABS } from "../config/feedTabs.ts"; import { type FeedTab, FEED_TABS } from "../config/feedTabs.ts";
import { useTabParam } from "../hooks/useTabParam.ts";
import { API_URL, DEFAULT_PAGE_SIZE } from "../config/api.ts"; import { API_URL, DEFAULT_PAGE_SIZE } from "../config/api.ts";
@@ -82,20 +83,20 @@ export function Index() {
); );
const mainFetchDone = useRef(false); const mainFetchDone = useRef(false);
const searchParams = new URLSearchParams(location.search); const [tab] = useTabParam<FeedTab>(FEED_TABS, "hot");
const rawTab = searchParams.get("tab") ?? "hot";
const tab: FeedTab = VALID_TABS.has(rawTab) ? rawTab as FeedTab : "hot";
// Web Share Target: Android share sheet navigates to /?share_url=... // Web Share Target: Android share sheet navigates to /?share_url=...
const searchParams = new URLSearchParams(location.search);
const shareUrl = searchParams.get("share_url") ?? const shareUrl = searchParams.get("share_url") ??
searchParams.get("share_text") ?? ""; searchParams.get("share_text") ?? "";
useEffect(() => { useEffect(() => {
if (!shareUrl) return; if (!shareUrl) return;
// Clean share params from the URL so a refresh doesn't re-open the modal // Clean share params from the URL so a refresh doesn't re-open the modal.
const clean = tab !== "hot" ? `?tab=${tab}` : ""; // The active tab already lives in the pathname (e.g. /~/new), so just drop
globalThis.history.replaceState({}, "", location.pathname + clean); // the query string.
}, [shareUrl, tab, location.pathname]); globalThis.history.replaceState({}, "", location.pathname);
}, [shareUrl, location.pathname]);
// ── Main feed fetch ── // ── Main feed fetch ──

View File

@@ -70,7 +70,7 @@ export function ResetPassword() {
); );
} }
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.SubmitEvent) => {
e.preventDefault(); e.preventDefault();
if (mismatch || tooShort || !newPassword) return; if (mismatch || tooShort || !newPassword) return;

View File

@@ -10,6 +10,7 @@ import { ErrorCard } from "../components/ErrorCard.tsx";
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 { useInfiniteScroll } from "../hooks/useInfiniteScroll.ts"; import { useInfiniteScroll } from "../hooks/useInfiniteScroll.ts";
import { useTabParam } from "../hooks/useTabParam.ts";
import { import {
deserializeDump, deserializeDump,
deserializePlaylist, deserializePlaylist,
@@ -23,7 +24,8 @@ import {
} from "../model.ts"; } from "../model.ts";
import { DEFAULT_PAGE_SIZE, SEARCH_URL } from "../config/api.ts"; import { DEFAULT_PAGE_SIZE, SEARCH_URL } from "../config/api.ts";
type Tab = "dumps" | "users" | "playlists"; const SEARCH_TABS = ["dumps", "users", "playlists"] as const;
type Tab = (typeof SEARCH_TABS)[number];
type SearchState = type SearchState =
| { status: "idle" } | { status: "idle" }
@@ -44,9 +46,9 @@ type SearchState =
}; };
export function Search() { export function Search() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const q = searchParams.get("q") ?? ""; const q = searchParams.get("q") ?? "";
const tab = (searchParams.get("tab") ?? "dumps") as Tab; const [tab, setTab] = useTabParam<Tab>(SEARCH_TABS, "dumps");
const { token, user } = useAuth(); const { token, user } = useAuth();
const { voteCounts, myVotes, castVote, removeVote } = useWS(); const { voteCounts, myVotes, castVote, removeVote } = useWS();
@@ -159,14 +161,6 @@ export function Search() {
!state.dumps.loadingMore, !state.dumps.loadingMore,
); );
function setTab(tab: Tab) {
setSearchParams((prev) => {
const next = new URLSearchParams(prev);
next.set("tab", tab);
return next;
}, { replace: true });
}
const dumpCount = state.status === "loaded" ? state.dumps.total : null; const dumpCount = state.status === "loaded" ? state.dumps.total : null;
const userCount = state.status === "loaded" ? state.users.length : null; const userCount = state.status === "loaded" ? state.users.length : null;
const playlistCount = state.status === "loaded" const playlistCount = state.status === "loaded"
@@ -188,7 +182,7 @@ export function Search() {
<main className="search-page"> <main className="search-page">
{q && ( {q && (
<TabBar <TabBar
tabs={(["dumps", "users", "playlists"] as Tab[]).map((key) => ({ tabs={SEARCH_TABS.map((key) => ({
key, key,
label: tabLabel( label: tabLabel(
key, key,

View File

@@ -64,7 +64,7 @@ export function UserLogin() {
} }
}; };
const handleResetRequest = async (e: React.FormEvent) => { const handleResetRequest = async (e: React.SubmitEvent) => {
e.preventDefault(); e.preventDefault();
if (!resetEmail.trim()) return; if (!resetEmail.trim()) return;
setResetState({ status: "submitting" }); setResetState({ status: "submitting" });

View File

@@ -54,6 +54,7 @@ import { TextEditor } from "../components/TextEditor.tsx";
import { Markdown } from "../components/Markdown.tsx"; import { Markdown } from "../components/Markdown.tsx";
import { ChangePasswordModal } from "../components/ChangePasswordModal.tsx"; import { ChangePasswordModal } from "../components/ChangePasswordModal.tsx";
import { TabBar } from "../components/TabBar.tsx"; import { TabBar } from "../components/TabBar.tsx";
import { useTabParam } from "../hooks/useTabParam.ts";
function InviteButton() { function InviteButton() {
const { authFetch } = useAuth(); const { authFetch } = useAuth();
@@ -146,7 +147,14 @@ type InviteTreeState =
| { status: "loaded"; entries: InviteTreeEntry[] }; | { status: "loaded"; entries: InviteTreeEntry[] };
type InviteTreeNode = InviteTreeEntry & { children: InviteTreeNode[] }; type InviteTreeNode = InviteTreeEntry & { children: InviteTreeNode[] };
type ProfileTab = "dumps" | "playlists" | "followed" | "invitees" | "settings"; const PROFILE_TABS = [
"dumps",
"playlists",
"followed",
"invitees",
"settings",
] as const;
type ProfileTab = (typeof PROFILE_TABS)[number];
function buildInviteTree( function buildInviteTree(
entries: InviteTreeEntry[], entries: InviteTreeEntry[],
@@ -193,6 +201,13 @@ export function UserPublicProfile() {
const profileUserId = state.status === "loaded" ? state.user.id : null; const profileUserId = state.status === "loaded" ? state.user.id : null;
const isOwnProfile = me?.id === profileUserId; const isOwnProfile = me?.id === profileUserId;
// Active tab is driven by the `/~/<tab>` URL path (linkable / refresh-safe).
// `settings` is only valid on your own profile; otherwise it falls back to "dumps".
const availableTabs: ProfileTab[] = isOwnProfile
? [...PROFILE_TABS]
: PROFILE_TABS.filter((t) => t !== "settings");
const [tab, setTab] = useTabParam<ProfileTab>(availableTabs, "dumps");
const setDumps = useCallback((fn: (prev: Dump[]) => Dump[]) => { const setDumps = useCallback((fn: (prev: Dump[]) => Dump[]) => {
setState((s) => setState((s) =>
s.status !== "loaded" s.status !== "loaded"
@@ -287,7 +302,6 @@ export function UserPublicProfile() {
const [emailError, setEmailError] = useState<string | null>(null); const [emailError, setEmailError] = useState<string | null>(null);
const prevMyVotesRef = useRef<Set<string> | null>(null); const prevMyVotesRef = useRef<Set<string> | null>(null);
const [tab, setTab] = useState<ProfileTab>("dumps");
const [changePasswordOpen, setChangePasswordOpen] = useState(false); const [changePasswordOpen, setChangePasswordOpen] = useState(false);
const [followedState, setFollowedState] = useState<FollowedState>(null); const [followedState, setFollowedState] = useState<FollowedState>(null);
const [inviteTreeState, setInviteTreeState] = useState<InviteTreeState>(null); const [inviteTreeState, setInviteTreeState] = useState<InviteTreeState>(null);
@@ -301,7 +315,6 @@ export function UserPublicProfile() {
if (prevUsername !== username) { if (prevUsername !== username) {
setPrevUsername(username); setPrevUsername(username);
setState({ status: "loading" }); setState({ status: "loading" });
setTab("dumps");
setFollowedState(null); setFollowedState(null);
setInviteTreeState(null); setInviteTreeState(null);
} }