v3: performance pass, bundle size pass, i18n pass, docker pass
This commit is contained in:
@@ -1807,6 +1807,10 @@ body.has-player .fab-new {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
}
|
||||
|
||||
.app-header--has-center .app-header-nav {
|
||||
grid-column: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.app-header-brand {
|
||||
|
||||
158
src/App.tsx
158
src/App.tsx
@@ -1,20 +1,8 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { BrowserRouter, Route, Routes } from "react-router";
|
||||
|
||||
import { Index } from "./pages/Index.tsx";
|
||||
import { RestrictedGuest } from "./pages/RestrictedGuest.tsx";
|
||||
import { RestrictedLoggedIn } from "./pages/RestrictedLoggedIn.tsx";
|
||||
import { Dump } from "./pages/Dump.tsx";
|
||||
import { DumpEdit } from "./pages/DumpEdit.tsx";
|
||||
import { UserLogin } from "./pages/UserLogin.tsx";
|
||||
import { UserPublicProfile } from "./pages/UserPublicProfile.tsx";
|
||||
import { UserRegister } from "./pages/UserRegister.tsx";
|
||||
import { UserDumps } from "./pages/UserDumps.tsx";
|
||||
import { UserUpvoted } from "./pages/UserUpvoted.tsx";
|
||||
import { UserPlaylists } from "./pages/UserPlaylists.tsx";
|
||||
import { PlaylistDetail } from "./pages/PlaylistDetail.tsx";
|
||||
import { Notifications } from "./pages/Notifications.tsx";
|
||||
import { Search } from "./pages/Search.tsx";
|
||||
import { ResetPassword } from "./pages/ResetPassword.tsx";
|
||||
|
||||
import { AuthProvider } from "./contexts/AuthProvider.tsx";
|
||||
import { PlayerProvider } from "./contexts/PlayerProvider.tsx";
|
||||
@@ -25,54 +13,106 @@ import { GlobalPlayer } from "./components/GlobalPlayer.tsx";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
const Index = lazy(() =>
|
||||
import("./pages/Index.tsx").then((m) => ({ default: m.Index }))
|
||||
);
|
||||
const Dump = lazy(() =>
|
||||
import("./pages/Dump.tsx").then((m) => ({ default: m.Dump }))
|
||||
);
|
||||
const DumpEdit = lazy(() =>
|
||||
import("./pages/DumpEdit.tsx").then((m) => ({ default: m.DumpEdit }))
|
||||
);
|
||||
const UserLogin = lazy(() =>
|
||||
import("./pages/UserLogin.tsx").then((m) => ({ default: m.UserLogin }))
|
||||
);
|
||||
const UserPublicProfile = lazy(() =>
|
||||
import("./pages/UserPublicProfile.tsx").then((m) => ({
|
||||
default: m.UserPublicProfile,
|
||||
}))
|
||||
);
|
||||
const UserRegister = lazy(() =>
|
||||
import("./pages/UserRegister.tsx").then((m) => ({ default: m.UserRegister }))
|
||||
);
|
||||
const UserDumps = lazy(() =>
|
||||
import("./pages/UserDumps.tsx").then((m) => ({ default: m.UserDumps }))
|
||||
);
|
||||
const UserUpvoted = lazy(() =>
|
||||
import("./pages/UserUpvoted.tsx").then((m) => ({ default: m.UserUpvoted }))
|
||||
);
|
||||
const UserPlaylists = lazy(() =>
|
||||
import("./pages/UserPlaylists.tsx").then((m) => ({
|
||||
default: m.UserPlaylists,
|
||||
}))
|
||||
);
|
||||
const PlaylistDetail = lazy(() =>
|
||||
import("./pages/PlaylistDetail.tsx").then((m) => ({
|
||||
default: m.PlaylistDetail,
|
||||
}))
|
||||
);
|
||||
const Notifications = lazy(() =>
|
||||
import("./pages/Notifications.tsx").then((m) => ({
|
||||
default: m.Notifications,
|
||||
}))
|
||||
);
|
||||
const Search = lazy(() =>
|
||||
import("./pages/Search.tsx").then((m) => ({ default: m.Search }))
|
||||
);
|
||||
const ResetPassword = lazy(() =>
|
||||
import("./pages/ResetPassword.tsx").then((m) => ({
|
||||
default: m.ResetPassword,
|
||||
}))
|
||||
);
|
||||
|
||||
function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/dumps/:selectedDump" element={<Dump />} />
|
||||
<Route
|
||||
path="/dumps/:selectedDump/edit"
|
||||
element={
|
||||
<RestrictedLoggedIn>
|
||||
<DumpEdit />
|
||||
</RestrictedLoggedIn>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/register"
|
||||
element={
|
||||
<RestrictedGuest>
|
||||
<UserRegister />
|
||||
</RestrictedGuest>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
<RestrictedGuest>
|
||||
<UserLogin />
|
||||
</RestrictedGuest>
|
||||
}
|
||||
/>
|
||||
<Route path="/users/:username" element={<UserPublicProfile />} />
|
||||
<Route path="/users/:username/dumps" element={<UserDumps />} />
|
||||
<Route path="/users/:username/upvoted" element={<UserUpvoted />} />
|
||||
<Route
|
||||
path="/users/:username/playlists"
|
||||
element={<UserPlaylists />}
|
||||
/>
|
||||
<Route path="/playlists/:playlistId" element={<PlaylistDetail />} />
|
||||
<Route path="/search" element={<Search />} />
|
||||
<Route path="/reset-password" element={<ResetPassword />} />
|
||||
<Route
|
||||
path="/notifications"
|
||||
element={
|
||||
<RestrictedLoggedIn>
|
||||
<Notifications />
|
||||
</RestrictedLoggedIn>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<Suspense>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/dumps/:selectedDump" element={<Dump />} />
|
||||
<Route
|
||||
path="/dumps/:selectedDump/edit"
|
||||
element={
|
||||
<RestrictedLoggedIn>
|
||||
<DumpEdit />
|
||||
</RestrictedLoggedIn>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/register"
|
||||
element={
|
||||
<RestrictedGuest>
|
||||
<UserRegister />
|
||||
</RestrictedGuest>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
<RestrictedGuest>
|
||||
<UserLogin />
|
||||
</RestrictedGuest>
|
||||
}
|
||||
/>
|
||||
<Route path="/users/:username" element={<UserPublicProfile />} />
|
||||
<Route path="/users/:username/dumps" element={<UserDumps />} />
|
||||
<Route path="/users/:username/upvoted" element={<UserUpvoted />} />
|
||||
<Route
|
||||
path="/users/:username/playlists"
|
||||
element={<UserPlaylists />}
|
||||
/>
|
||||
<Route path="/playlists/:playlistId" element={<PlaylistDetail />} />
|
||||
<Route path="/search" element={<Search />} />
|
||||
<Route path="/reset-password" element={<ResetPassword />} />
|
||||
<Route
|
||||
path="/notifications"
|
||||
element={
|
||||
<RestrictedLoggedIn>
|
||||
<Notifications />
|
||||
</RestrictedLoggedIn>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { lazy, type ReactNode, Suspense, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { useWS } from "../hooks/useWS.ts";
|
||||
import { DumpCreateModal } from "./DumpCreateModal.tsx";
|
||||
import { NotificationBell } from "./NotificationBell.tsx";
|
||||
import { UserMenu } from "./UserMenu.tsx";
|
||||
|
||||
const DumpCreateModal = lazy(() =>
|
||||
import("./DumpCreateModal.tsx").then((m) => ({ default: m.DumpCreateModal }))
|
||||
);
|
||||
|
||||
export function AppHeader(
|
||||
{ centerSlot, disableNew, initialDumpUrl }: {
|
||||
centerSlot?: ReactNode;
|
||||
@@ -88,10 +91,12 @@ export function AppHeader(
|
||||
)}
|
||||
|
||||
{createModalOpen && (
|
||||
<DumpCreateModal
|
||||
onClose={() => setCreateModalOpen(false)}
|
||||
initialUrl={initialDumpUrl}
|
||||
/>
|
||||
<Suspense>
|
||||
<DumpCreateModal
|
||||
onClose={() => setCreateModalOpen(false)}
|
||||
initialUrl={initialDumpUrl}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -118,6 +118,21 @@ function AudioFilePreview(
|
||||
);
|
||||
}
|
||||
|
||||
function VideoThumb({ src, fallback }: { src: string; fallback: string }) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
if (failed) {
|
||||
return <span className="rich-content-compact-icon">{fallback}</span>;
|
||||
}
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
className="rich-content-compact-thumbnail"
|
||||
onError={() => setFailed(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function mimeIcon(mime: string): string {
|
||||
if (mime.startsWith("video/")) return "🎬";
|
||||
if (mime.startsWith("audio/")) return "🎵";
|
||||
@@ -148,6 +163,7 @@ export default function FilePreview(
|
||||
);
|
||||
}
|
||||
if (mime.startsWith("video/")) {
|
||||
const thumbUrl = `${API_URL}/api/thumbnails/${dump.id}`;
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
@@ -160,15 +176,7 @@ export default function FilePreview(
|
||||
play({ kind: "file", fileUrl, mimeType: mime, title: dump.title });
|
||||
}}
|
||||
>
|
||||
<video
|
||||
src={fileUrl}
|
||||
preload="metadata"
|
||||
className="rich-content-compact-thumbnail"
|
||||
muted
|
||||
onLoadedMetadata={(e) => {
|
||||
(e.target as HTMLVideoElement).currentTime = 0.1;
|
||||
}}
|
||||
/>
|
||||
<VideoThumb src={thumbUrl} fallback={mimeIcon(mime)} />
|
||||
<span className="rich-content-play-overlay">▶</span>
|
||||
</button>
|
||||
);
|
||||
@@ -217,12 +225,9 @@ export default function FilePreview(
|
||||
>
|
||||
<video
|
||||
src={fileUrl}
|
||||
preload="metadata"
|
||||
preload="none"
|
||||
className="file-preview-video-thumb"
|
||||
muted
|
||||
onLoadedMetadata={(e) => {
|
||||
(e.target as HTMLVideoElement).currentTime = 0.1;
|
||||
}}
|
||||
/>
|
||||
<span className="rich-content-play-overlay">
|
||||
{videoPlaying ? "⏸" : "▶"}
|
||||
|
||||
@@ -74,7 +74,7 @@ msgstr "← Back to profile"
|
||||
msgid "+ Invite someone"
|
||||
msgstr "+ Invite someone"
|
||||
|
||||
#: src/components/AppHeader.tsx:67
|
||||
#: src/components/AppHeader.tsx:70
|
||||
msgid "+ New"
|
||||
msgstr "+ New"
|
||||
|
||||
@@ -176,8 +176,8 @@ msgid "Appearance"
|
||||
msgstr "Appearance"
|
||||
|
||||
#. placeholder {0}: VALIDATION.PASSWORD_MIN
|
||||
#: src/components/ChangePasswordModal.tsx:101
|
||||
#: src/pages/ResetPassword.tsx:113
|
||||
#: src/components/ChangePasswordModal.tsx:123
|
||||
#: src/pages/ResetPassword.tsx:125
|
||||
msgid "At least {0} characters"
|
||||
msgstr "At least {0} characters"
|
||||
|
||||
@@ -185,8 +185,8 @@ msgstr "At least {0} characters"
|
||||
msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:36
|
||||
#: src/pages/ResetPassword.tsx:146
|
||||
#: src/pages/ResetPassword.tsx:44
|
||||
#: src/pages/ResetPassword.tsx:159
|
||||
msgid "Back to login"
|
||||
msgstr "Back to login"
|
||||
|
||||
@@ -195,7 +195,7 @@ msgstr "Back to login"
|
||||
msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
|
||||
msgstr "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:132
|
||||
#: src/components/ChangePasswordModal.tsx:168
|
||||
#: src/components/CommentThread.tsx:281
|
||||
#: src/components/CommentThread.tsx:373
|
||||
#: src/components/CommentThread.tsx:510
|
||||
@@ -221,8 +221,8 @@ msgstr "Cancel removal"
|
||||
msgid "Change avatar"
|
||||
msgstr "Change avatar"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:55
|
||||
#: src/components/ChangePasswordModal.tsx:142
|
||||
#: src/components/ChangePasswordModal.tsx:56
|
||||
#: src/components/ChangePasswordModal.tsx:178
|
||||
msgid "Change password"
|
||||
msgstr "Change password"
|
||||
|
||||
@@ -234,7 +234,7 @@ msgstr "Change password…"
|
||||
msgid "Checking invite…"
|
||||
msgstr "Checking invite…"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:65
|
||||
#: src/components/ChangePasswordModal.tsx:66
|
||||
#: src/components/Modal.tsx:45
|
||||
msgid "Close"
|
||||
msgstr "Close"
|
||||
@@ -247,8 +247,8 @@ msgstr "Color scheme"
|
||||
#~ msgid "Comment not found"
|
||||
#~ msgstr "Comment not found"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:107
|
||||
#: src/pages/ResetPassword.tsx:120
|
||||
#: src/components/ChangePasswordModal.tsx:136
|
||||
#: src/pages/ResetPassword.tsx:132
|
||||
msgid "Confirm new password"
|
||||
msgstr "Confirm new password"
|
||||
|
||||
@@ -260,11 +260,11 @@ msgstr "Copied!"
|
||||
msgid "Copy"
|
||||
msgstr "Copy"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:123
|
||||
#: src/components/ChangePasswordModal.tsx:159
|
||||
msgid "Could not change password"
|
||||
msgstr "Could not change password"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:84
|
||||
#: src/pages/ResetPassword.tsx:94
|
||||
#: src/pages/UserLogin.tsx:79
|
||||
msgid "Could not connect to server"
|
||||
msgstr "Could not connect to server"
|
||||
@@ -293,7 +293,7 @@ msgstr "Created ({0}{1})"
|
||||
msgid "Creating…"
|
||||
msgstr "Creating…"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:75
|
||||
#: src/components/ChangePasswordModal.tsx:83
|
||||
msgid "Current password"
|
||||
msgstr "Current password"
|
||||
|
||||
@@ -413,7 +413,7 @@ msgstr "Email address"
|
||||
msgid "Enter a query to search."
|
||||
msgstr "Enter a query to search."
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:48
|
||||
#: src/components/ChangePasswordModal.tsx:49
|
||||
msgid "Failed to change password"
|
||||
msgstr "Failed to change password"
|
||||
|
||||
@@ -432,7 +432,6 @@ msgstr "Failed to generate invite"
|
||||
#: src/pages/index/HotFeed.tsx:36
|
||||
#: src/pages/index/JournalFeed.tsx:48
|
||||
#: src/pages/index/NewFeed.tsx:36
|
||||
#: src/pages/Notifications.tsx:323
|
||||
#: src/pages/UserPublicProfile.tsx:1106
|
||||
#: src/pages/UserPublicProfile.tsx:1148
|
||||
#: src/pages/UserPublicProfile.tsx:1193
|
||||
@@ -554,7 +553,7 @@ msgstr "From people"
|
||||
msgid "From playlists"
|
||||
msgstr "From playlists"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:56
|
||||
#: src/pages/ResetPassword.tsx:66
|
||||
msgid "Go to login"
|
||||
msgstr "Go to login"
|
||||
|
||||
@@ -574,7 +573,7 @@ msgstr "If that address is registered you'll receive a reset link shortly."
|
||||
msgid "Invalid invite"
|
||||
msgstr "Invalid invite"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:33
|
||||
#: src/pages/ResetPassword.tsx:34
|
||||
msgid "Invalid link"
|
||||
msgstr "Invalid link"
|
||||
|
||||
@@ -620,11 +619,11 @@ msgstr "Light"
|
||||
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||
msgstr "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||
|
||||
#: src/components/AppHeader.tsx:84
|
||||
#: src/components/AppHeader.tsx:87
|
||||
msgid "Live updates unavailable."
|
||||
msgstr "Live updates unavailable."
|
||||
|
||||
#: src/pages/Notifications.tsx:396
|
||||
#: src/pages/Notifications.tsx:390
|
||||
msgid "Load more"
|
||||
msgstr "Load more"
|
||||
|
||||
@@ -659,8 +658,8 @@ msgstr "Loading profile…"
|
||||
#: src/pages/index/HotFeed.tsx:32
|
||||
#: src/pages/index/JournalFeed.tsx:44
|
||||
#: src/pages/index/NewFeed.tsx:32
|
||||
#: src/pages/Notifications.tsx:319
|
||||
#: src/pages/Notifications.tsx:395
|
||||
#: src/pages/Notifications.tsx:313
|
||||
#: src/pages/Notifications.tsx:389
|
||||
#: src/pages/UserDumps.tsx:51
|
||||
#: src/pages/UserPlaylists.tsx:342
|
||||
#: src/pages/UserPublicProfile.tsx:1100
|
||||
@@ -670,7 +669,7 @@ msgstr "Loading profile…"
|
||||
msgid "Loading…"
|
||||
msgstr "Loading…"
|
||||
|
||||
#: src/components/AppHeader.tsx:74
|
||||
#: src/components/AppHeader.tsx:77
|
||||
#: src/pages/UserLogin.tsx:87
|
||||
#: src/pages/UserLogin.tsx:117
|
||||
msgid "Log in"
|
||||
@@ -693,7 +692,7 @@ msgstr "Login failed"
|
||||
msgid "Max 50 MB"
|
||||
msgstr "Max 50 MB"
|
||||
|
||||
#: src/pages/Notifications.tsx:312
|
||||
#: src/pages/Notifications.tsx:306
|
||||
msgid "new"
|
||||
msgstr "new"
|
||||
|
||||
@@ -705,8 +704,8 @@ msgstr "New"
|
||||
msgid "New dump"
|
||||
msgstr "New dump"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:88
|
||||
#: src/pages/ResetPassword.tsx:101
|
||||
#: src/components/ChangePasswordModal.tsx:103
|
||||
#: src/pages/ResetPassword.tsx:113
|
||||
msgid "New password"
|
||||
msgstr "New password"
|
||||
|
||||
@@ -763,7 +762,7 @@ msgstr "No users match \"{q}\"."
|
||||
msgid "Not following anyone yet."
|
||||
msgstr "Not following anyone yet."
|
||||
|
||||
#: src/pages/Notifications.tsx:330
|
||||
#: src/pages/Notifications.tsx:324
|
||||
#: src/pages/UserDumps.tsx:123
|
||||
#: src/pages/UserPublicProfile.tsx:1340
|
||||
#: src/pages/UserPublicProfile.tsx:1463
|
||||
@@ -772,7 +771,7 @@ msgid "Nothing here yet."
|
||||
msgstr "Nothing here yet."
|
||||
|
||||
#: src/components/NotificationBell.tsx:42
|
||||
#: src/pages/Notifications.tsx:308
|
||||
#: src/pages/Notifications.tsx:302
|
||||
msgid "Notifications"
|
||||
msgstr "Notifications"
|
||||
|
||||
@@ -798,7 +797,7 @@ msgstr "Password"
|
||||
msgid "Password (min. {0} characters)"
|
||||
msgstr "Password (min. {0} characters)"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:60
|
||||
#: src/components/ChangePasswordModal.tsx:61
|
||||
msgid "Password changed successfully."
|
||||
msgstr "Password changed successfully."
|
||||
|
||||
@@ -810,12 +809,12 @@ msgstr "Password changed successfully."
|
||||
#~ msgid "Password must be at most 128 characters"
|
||||
#~ msgstr "Password must be at most 128 characters"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:47
|
||||
#: src/pages/ResetPassword.tsx:56
|
||||
msgid "Password updated"
|
||||
msgstr "Password updated"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:118
|
||||
#: src/pages/ResetPassword.tsx:129
|
||||
#: src/components/ChangePasswordModal.tsx:154
|
||||
#: src/pages/ResetPassword.tsx:141
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Passwords do not match"
|
||||
|
||||
@@ -823,7 +822,7 @@ msgstr "Passwords do not match"
|
||||
#~ msgid "Playlist not found"
|
||||
#~ msgstr "Playlist not found"
|
||||
|
||||
#: src/components/AppHeader.tsx:50
|
||||
#: src/components/AppHeader.tsx:53
|
||||
#: src/components/UserMenu.tsx:62
|
||||
#: src/pages/Search.tsx:175
|
||||
#: src/pages/UserPlaylists.tsx:368
|
||||
@@ -922,7 +921,7 @@ msgstr "Reply"
|
||||
msgid "Request failed"
|
||||
msgstr "Request failed"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:94
|
||||
#: src/pages/ResetPassword.tsx:106
|
||||
msgid "Reset failed"
|
||||
msgstr "Reset failed"
|
||||
|
||||
@@ -939,10 +938,10 @@ msgstr "Retry"
|
||||
msgid "Save"
|
||||
msgstr "Save"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:141
|
||||
#: src/components/ChangePasswordModal.tsx:177
|
||||
#: src/components/CommentThread.tsx:269
|
||||
#: src/pages/PlaylistDetail.tsx:673
|
||||
#: src/pages/ResetPassword.tsx:140
|
||||
#: src/pages/ResetPassword.tsx:152
|
||||
#: src/pages/UserPublicProfile.tsx:832
|
||||
#: src/pages/UserPublicProfile.tsx:911
|
||||
msgid "Saving…"
|
||||
@@ -972,12 +971,12 @@ msgstr "Send reset link"
|
||||
msgid "Sending…"
|
||||
msgstr "Sending…"
|
||||
|
||||
#: src/components/AppHeader.tsx:65
|
||||
#: src/components/AppHeader.tsx:68
|
||||
msgid "Server unreachable"
|
||||
msgstr "Server unreachable"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:91
|
||||
#: src/pages/ResetPassword.tsx:141
|
||||
#: src/pages/ResetPassword.tsx:102
|
||||
#: src/pages/ResetPassword.tsx:153
|
||||
msgid "Set new password"
|
||||
msgstr "Set new password"
|
||||
|
||||
@@ -1014,7 +1013,7 @@ msgstr "This invite link is missing, expired, or already used."
|
||||
msgid "This is a mirage."
|
||||
msgstr "This is a mirage."
|
||||
|
||||
#: src/pages/ResetPassword.tsx:34
|
||||
#: src/pages/ResetPassword.tsx:37
|
||||
msgid "This reset link is missing or malformed."
|
||||
msgstr "This reset link is missing or malformed."
|
||||
|
||||
@@ -1042,8 +1041,8 @@ msgstr "Unfollow {targetUsername}"
|
||||
msgid "Unfollow playlist"
|
||||
msgstr "Unfollow playlist"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:43
|
||||
#: src/pages/ResetPassword.tsx:80
|
||||
#: src/components/ChangePasswordModal.tsx:44
|
||||
#: src/pages/ResetPassword.tsx:90
|
||||
msgid "Unknown error"
|
||||
msgstr "Unknown error"
|
||||
|
||||
@@ -1120,7 +1119,7 @@ msgstr "Write a reply…"
|
||||
msgid "Yesterday"
|
||||
msgstr "Yesterday"
|
||||
|
||||
#: src/pages/Notifications.tsx:333
|
||||
#: src/pages/Notifications.tsx:327
|
||||
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
||||
msgstr "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
||||
|
||||
@@ -1140,6 +1139,6 @@ msgstr "You've reached the end."
|
||||
msgid "Your email address"
|
||||
msgstr "Your email address"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:49
|
||||
#: src/pages/ResetPassword.tsx:59
|
||||
msgid "Your password has been changed. You can now log in."
|
||||
msgstr "Your password has been changed. You can now log in."
|
||||
|
||||
@@ -74,7 +74,7 @@ msgstr "← Retour au profil"
|
||||
msgid "+ Invite someone"
|
||||
msgstr "+ Inviter quelqu'un"
|
||||
|
||||
#: src/components/AppHeader.tsx:67
|
||||
#: src/components/AppHeader.tsx:70
|
||||
msgid "+ New"
|
||||
msgstr "+ Nouveau"
|
||||
|
||||
@@ -172,8 +172,8 @@ msgid "Appearance"
|
||||
msgstr "Apparence"
|
||||
|
||||
#. placeholder {0}: VALIDATION.PASSWORD_MIN
|
||||
#: src/components/ChangePasswordModal.tsx:101
|
||||
#: src/pages/ResetPassword.tsx:113
|
||||
#: src/components/ChangePasswordModal.tsx:123
|
||||
#: src/pages/ResetPassword.tsx:125
|
||||
msgid "At least {0} characters"
|
||||
msgstr "Au moins {0} caractères"
|
||||
|
||||
@@ -181,8 +181,8 @@ msgstr "Au moins {0} caractères"
|
||||
msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:36
|
||||
#: src/pages/ResetPassword.tsx:146
|
||||
#: src/pages/ResetPassword.tsx:44
|
||||
#: src/pages/ResetPassword.tsx:159
|
||||
msgid "Back to login"
|
||||
msgstr "Retour à la connexion"
|
||||
|
||||
@@ -191,7 +191,7 @@ msgstr "Retour à la connexion"
|
||||
msgid "Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects."
|
||||
msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion."
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:132
|
||||
#: src/components/ChangePasswordModal.tsx:168
|
||||
#: src/components/CommentThread.tsx:281
|
||||
#: src/components/CommentThread.tsx:373
|
||||
#: src/components/CommentThread.tsx:510
|
||||
@@ -213,8 +213,8 @@ msgstr "Annuler la suppression"
|
||||
msgid "Change avatar"
|
||||
msgstr "Changer l'avatar"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:55
|
||||
#: src/components/ChangePasswordModal.tsx:142
|
||||
#: src/components/ChangePasswordModal.tsx:56
|
||||
#: src/components/ChangePasswordModal.tsx:178
|
||||
msgid "Change password"
|
||||
msgstr "Changer le mot de passe"
|
||||
|
||||
@@ -226,7 +226,7 @@ msgstr "Changer le mot de passe…"
|
||||
msgid "Checking invite…"
|
||||
msgstr "Vérification de l'invitation…"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:65
|
||||
#: src/components/ChangePasswordModal.tsx:66
|
||||
#: src/components/Modal.tsx:45
|
||||
msgid "Close"
|
||||
msgstr "Fermer"
|
||||
@@ -235,8 +235,8 @@ msgstr "Fermer"
|
||||
msgid "Color scheme"
|
||||
msgstr "Thème de couleur"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:107
|
||||
#: src/pages/ResetPassword.tsx:120
|
||||
#: src/components/ChangePasswordModal.tsx:136
|
||||
#: src/pages/ResetPassword.tsx:132
|
||||
msgid "Confirm new password"
|
||||
msgstr "Confirmer le nouveau mot de passe"
|
||||
|
||||
@@ -248,11 +248,11 @@ msgstr "Copié !"
|
||||
msgid "Copy"
|
||||
msgstr "Copier"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:123
|
||||
#: src/components/ChangePasswordModal.tsx:159
|
||||
msgid "Could not change password"
|
||||
msgstr "Impossible de changer le mot de passe"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:84
|
||||
#: src/pages/ResetPassword.tsx:94
|
||||
#: src/pages/UserLogin.tsx:79
|
||||
msgid "Could not connect to server"
|
||||
msgstr "Impossible de contacter le serveur"
|
||||
@@ -281,7 +281,7 @@ msgstr "Créées ({0}{1})"
|
||||
msgid "Creating…"
|
||||
msgstr "Création…"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:75
|
||||
#: src/components/ChangePasswordModal.tsx:83
|
||||
msgid "Current password"
|
||||
msgstr "Mot de passe actuel"
|
||||
|
||||
@@ -397,7 +397,7 @@ msgstr "Adresse e-mail"
|
||||
msgid "Enter a query to search."
|
||||
msgstr "Saisissez une recherche."
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:48
|
||||
#: src/components/ChangePasswordModal.tsx:49
|
||||
msgid "Failed to change password"
|
||||
msgstr "Impossible de changer le mot de passe"
|
||||
|
||||
@@ -416,7 +416,6 @@ msgstr "Impossible de générer une invitation"
|
||||
#: src/pages/index/HotFeed.tsx:36
|
||||
#: src/pages/index/JournalFeed.tsx:48
|
||||
#: src/pages/index/NewFeed.tsx:36
|
||||
#: src/pages/Notifications.tsx:323
|
||||
#: src/pages/UserPublicProfile.tsx:1106
|
||||
#: src/pages/UserPublicProfile.tsx:1148
|
||||
#: src/pages/UserPublicProfile.tsx:1193
|
||||
@@ -522,7 +521,7 @@ msgstr "De personnes"
|
||||
msgid "From playlists"
|
||||
msgstr "De collections"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:56
|
||||
#: src/pages/ResetPassword.tsx:66
|
||||
msgid "Go to login"
|
||||
msgstr "Aller à la connexion"
|
||||
|
||||
@@ -538,7 +537,7 @@ msgstr "Si cette adresse est enregistrée, vous recevrez un lien de réinitialis
|
||||
msgid "Invalid invite"
|
||||
msgstr "Invitation invalide"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:33
|
||||
#: src/pages/ResetPassword.tsx:34
|
||||
msgid "Invalid link"
|
||||
msgstr "Lien invalide"
|
||||
|
||||
@@ -567,11 +566,11 @@ msgstr "Clair"
|
||||
msgid "Live updates are temporarily disconnected. Trying to reconnect…"
|
||||
msgstr "Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…"
|
||||
|
||||
#: src/components/AppHeader.tsx:84
|
||||
#: src/components/AppHeader.tsx:87
|
||||
msgid "Live updates unavailable."
|
||||
msgstr "Mises à jour en direct indisponibles."
|
||||
|
||||
#: src/pages/Notifications.tsx:396
|
||||
#: src/pages/Notifications.tsx:390
|
||||
msgid "Load more"
|
||||
msgstr "Charger plus"
|
||||
|
||||
@@ -606,8 +605,8 @@ msgstr "Chargement du profil…"
|
||||
#: src/pages/index/HotFeed.tsx:32
|
||||
#: src/pages/index/JournalFeed.tsx:44
|
||||
#: src/pages/index/NewFeed.tsx:32
|
||||
#: src/pages/Notifications.tsx:319
|
||||
#: src/pages/Notifications.tsx:395
|
||||
#: src/pages/Notifications.tsx:313
|
||||
#: src/pages/Notifications.tsx:389
|
||||
#: src/pages/UserDumps.tsx:51
|
||||
#: src/pages/UserPlaylists.tsx:342
|
||||
#: src/pages/UserPublicProfile.tsx:1100
|
||||
@@ -617,7 +616,7 @@ msgstr "Chargement du profil…"
|
||||
msgid "Loading…"
|
||||
msgstr "Chargement…"
|
||||
|
||||
#: src/components/AppHeader.tsx:74
|
||||
#: src/components/AppHeader.tsx:77
|
||||
#: src/pages/UserLogin.tsx:87
|
||||
#: src/pages/UserLogin.tsx:117
|
||||
msgid "Log in"
|
||||
@@ -640,7 +639,7 @@ msgstr "Connexion échouée"
|
||||
msgid "Max 50 MB"
|
||||
msgstr "Max 50 Mo"
|
||||
|
||||
#: src/pages/Notifications.tsx:312
|
||||
#: src/pages/Notifications.tsx:306
|
||||
msgid "new"
|
||||
msgstr "nouveau"
|
||||
|
||||
@@ -652,8 +651,8 @@ msgstr "Nouveau"
|
||||
msgid "New dump"
|
||||
msgstr "Nouvelle reco"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:88
|
||||
#: src/pages/ResetPassword.tsx:101
|
||||
#: src/components/ChangePasswordModal.tsx:103
|
||||
#: src/pages/ResetPassword.tsx:113
|
||||
msgid "New password"
|
||||
msgstr "Nouveau mot de passe"
|
||||
|
||||
@@ -706,7 +705,7 @@ msgstr "Aucun utilisateur ne correspond à « {q} »."
|
||||
msgid "Not following anyone yet."
|
||||
msgstr "Aucun abonnement pour le moment."
|
||||
|
||||
#: src/pages/Notifications.tsx:330
|
||||
#: src/pages/Notifications.tsx:324
|
||||
#: src/pages/UserDumps.tsx:123
|
||||
#: src/pages/UserPublicProfile.tsx:1340
|
||||
#: src/pages/UserPublicProfile.tsx:1463
|
||||
@@ -715,7 +714,7 @@ msgid "Nothing here yet."
|
||||
msgstr "Rien ici pour l'instant."
|
||||
|
||||
#: src/components/NotificationBell.tsx:42
|
||||
#: src/pages/Notifications.tsx:308
|
||||
#: src/pages/Notifications.tsx:302
|
||||
msgid "Notifications"
|
||||
msgstr "Notifications"
|
||||
|
||||
@@ -741,20 +740,20 @@ msgstr "Mot de passe"
|
||||
msgid "Password (min. {0} characters)"
|
||||
msgstr "Mot de passe (min. {0} caractères)"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:60
|
||||
#: src/components/ChangePasswordModal.tsx:61
|
||||
msgid "Password changed successfully."
|
||||
msgstr "Mot de passe modifié avec succès."
|
||||
|
||||
#: src/pages/ResetPassword.tsx:47
|
||||
#: src/pages/ResetPassword.tsx:56
|
||||
msgid "Password updated"
|
||||
msgstr "Mot de passe mis à jour"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:118
|
||||
#: src/pages/ResetPassword.tsx:129
|
||||
#: src/components/ChangePasswordModal.tsx:154
|
||||
#: src/pages/ResetPassword.tsx:141
|
||||
msgid "Passwords do not match"
|
||||
msgstr "Les mots de passe ne correspondent pas"
|
||||
|
||||
#: src/components/AppHeader.tsx:50
|
||||
#: src/components/AppHeader.tsx:53
|
||||
#: src/components/UserMenu.tsx:62
|
||||
#: src/pages/Search.tsx:175
|
||||
#: src/pages/UserPlaylists.tsx:368
|
||||
@@ -853,7 +852,7 @@ msgstr "Répondre"
|
||||
msgid "Request failed"
|
||||
msgstr "Échec de la demande"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:94
|
||||
#: src/pages/ResetPassword.tsx:106
|
||||
msgid "Reset failed"
|
||||
msgstr "Échec de la réinitialisation"
|
||||
|
||||
@@ -870,10 +869,10 @@ msgstr "Réessayer"
|
||||
msgid "Save"
|
||||
msgstr "Enregistrer"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:141
|
||||
#: src/components/ChangePasswordModal.tsx:177
|
||||
#: src/components/CommentThread.tsx:269
|
||||
#: src/pages/PlaylistDetail.tsx:673
|
||||
#: src/pages/ResetPassword.tsx:140
|
||||
#: src/pages/ResetPassword.tsx:152
|
||||
#: src/pages/UserPublicProfile.tsx:832
|
||||
#: src/pages/UserPublicProfile.tsx:911
|
||||
msgid "Saving…"
|
||||
@@ -903,12 +902,12 @@ msgstr "Envoyer le lien de réinitialisation"
|
||||
msgid "Sending…"
|
||||
msgstr "Envoi…"
|
||||
|
||||
#: src/components/AppHeader.tsx:65
|
||||
#: src/components/AppHeader.tsx:68
|
||||
msgid "Server unreachable"
|
||||
msgstr "Serveur inaccessible"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:91
|
||||
#: src/pages/ResetPassword.tsx:141
|
||||
#: src/pages/ResetPassword.tsx:102
|
||||
#: src/pages/ResetPassword.tsx:153
|
||||
msgid "Set new password"
|
||||
msgstr "Définir un nouveau mot de passe"
|
||||
|
||||
@@ -945,7 +944,7 @@ msgstr "Ce lien d'invitation est manquant, expiré ou déjà utilisé."
|
||||
msgid "This is a mirage."
|
||||
msgstr "C'est un mirage."
|
||||
|
||||
#: src/pages/ResetPassword.tsx:34
|
||||
#: src/pages/ResetPassword.tsx:37
|
||||
msgid "This reset link is missing or malformed."
|
||||
msgstr "Ce lien de réinitialisation est absent ou malformé."
|
||||
|
||||
@@ -969,8 +968,8 @@ msgstr "Ne plus suivre {targetUsername}"
|
||||
msgid "Unfollow playlist"
|
||||
msgstr "Ne plus suivre la collection"
|
||||
|
||||
#: src/components/ChangePasswordModal.tsx:43
|
||||
#: src/pages/ResetPassword.tsx:80
|
||||
#: src/components/ChangePasswordModal.tsx:44
|
||||
#: src/pages/ResetPassword.tsx:90
|
||||
msgid "Unknown error"
|
||||
msgstr "Erreur inconnue"
|
||||
|
||||
@@ -1039,7 +1038,7 @@ msgstr "Écrire une réponse…"
|
||||
msgid "Yesterday"
|
||||
msgstr "Hier"
|
||||
|
||||
#: src/pages/Notifications.tsx:333
|
||||
#: src/pages/Notifications.tsx:327
|
||||
msgid "You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content."
|
||||
msgstr "Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu."
|
||||
|
||||
@@ -1059,6 +1058,6 @@ msgstr "Vous avez tout lu, tout vu, tout bu."
|
||||
msgid "Your email address"
|
||||
msgstr "Votre adresse e-mail"
|
||||
|
||||
#: src/pages/ResetPassword.tsx:49
|
||||
#: src/pages/ResetPassword.tsx:59
|
||||
msgid "Your password has been changed. You can now log in."
|
||||
msgstr "Votre mot de passe a été modifié. Vous pouvez maintenant vous connecter."
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { relativeTime } from "../utils/relativeTime.ts";
|
||||
|
||||
import { API_URL, NOTIFICATIONS_PAGE_SIZE } from "../config/api.ts";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
@@ -174,14 +175,8 @@ function notificationContent(n: Notification): React.ReactNode {
|
||||
}
|
||||
|
||||
function timeAgo(date: Date): string {
|
||||
const secs = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
if (secs < 60) return t`just now`;
|
||||
const mins = Math.floor(secs / 60);
|
||||
if (mins < 60) return t`${mins}m ago`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
if (hrs < 24) return t`${hrs}h ago`;
|
||||
const days = Math.floor(hrs / 24);
|
||||
if (days < 7) return t`${days}d ago`;
|
||||
const abs = Math.abs(Date.now() - date.getTime()) / 1000;
|
||||
if (abs < 7 * 86400) return relativeTime(date);
|
||||
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
import { i18n } from "../i18n.ts";
|
||||
|
||||
export function relativeTime(date: Date): string {
|
||||
const rtf = new Intl.RelativeTimeFormat(i18n.locale, { numeric: "auto" });
|
||||
const diff = date.getTime() - Date.now(); // negative = past
|
||||
const abs = Math.abs(diff) / 1000;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user