diff --git a/api/lib/jwt.ts b/api/lib/jwt.ts index af40ef3..4235617 100644 --- a/api/lib/jwt.ts +++ b/api/lib/jwt.ts @@ -3,9 +3,7 @@ import { jwtVerify, SignJWT } from "@panva/jose"; import { type AuthPayload, - InvitePayload, isAuthPayload, - isInvitePayload, isPasswordResetPayload, type PasswordResetPayload, } from "../model/interfaces.ts"; @@ -18,28 +16,6 @@ if (!JWT_SECRET) { } const JWT_KEY = new TextEncoder().encode(JWT_SECRET); -// ── Invite tokens ───────────────────────────────────────────────────────────── - -export async function createInviteToken(inviterId: string): Promise { - return await new SignJWT({ purpose: "invite", inviterId }) - .setProtectedHeader({ alg: "HS256" }) - .setJti(crypto.randomUUID()) - .setExpirationTime("7d") - .sign(JWT_KEY); -} - -export async function verifyInviteToken( - token: string, -): Promise { - try { - const { payload } = await jwtVerify(token, JWT_KEY); - if (!isInvitePayload(payload)) return null; - return payload as InvitePayload; - } catch { - return null; - } -} - // ── Password reset tokens ───────────────────────────────────────────────────── export async function createPasswordResetToken( diff --git a/api/model/interfaces.ts b/api/model/interfaces.ts index 2900a75..1bb50e8 100644 --- a/api/model/interfaces.ts +++ b/api/model/interfaces.ts @@ -165,19 +165,6 @@ export function isAuthPayload(obj: unknown): obj is AuthPayload { "exp" in obj && typeof obj.exp === "number"; } -export interface InvitePayload { - purpose: "invite"; - inviterId: string; - exp: number; -} - -export function isInvitePayload(obj: unknown): obj is InvitePayload { - return !!obj && typeof obj === "object" && - "purpose" in obj && (obj as Record).purpose === "invite" && - "inviterId" in obj && - typeof (obj as Record).inviterId === "string"; -} - export interface PasswordResetPayload { purpose: "password-reset"; userId: string; diff --git a/api/routes/invites.ts b/api/routes/invites.ts index 5e043e6..9077ae9 100644 --- a/api/routes/invites.ts +++ b/api/routes/invites.ts @@ -6,19 +6,19 @@ import { createInvite, validateInvite } from "../services/invite-service.ts"; const router = new Router({ prefix: "/api/invites" }); // Create a new invite link (any authenticated user) -router.post("/", authMiddleware, async (ctx: AuthContext) => { +router.post("/", authMiddleware, (ctx: AuthContext) => { if (!ctx.state.user) { throw new APIException(APIErrorCode.UNAUTHORIZED, 401, "Not authenticated"); } - const token = await createInvite(ctx.state.user.userId); + const token = createInvite(ctx.state.user.userId); ctx.response.status = 201; ctx.response.body = { success: true, data: { token } }; }); // Validate an invite token (used by the register page before showing the form) -router.get("/:token", async (ctx) => { +router.get("/:token", (ctx) => { try { - await validateInvite(ctx.params.token); + validateInvite(ctx.params.token); ctx.response.body = { success: true, data: null }; } catch { throw new APIException( diff --git a/api/routes/users.ts b/api/routes/users.ts index ed4bfda..e4a0166 100644 --- a/api/routes/users.ts +++ b/api/routes/users.ts @@ -58,7 +58,7 @@ router.post("/register", async (ctx) => { } // Validate invite — throws 404/409 if bad - const inviterId = await validateInvite(body.inviteToken); + const inviterId = validateInvite(body.inviteToken); const user = await createUser(body, inviterId); diff --git a/api/services/invite-service.ts b/api/services/invite-service.ts index c28ac37..fc3813c 100644 --- a/api/services/invite-service.ts +++ b/api/services/invite-service.ts @@ -1,9 +1,15 @@ +import { randomBytes } from "node:crypto"; import { APIErrorCode, APIException } from "../model/interfaces.ts"; import { db, isInviteRow } from "../model/db.ts"; -import { createInviteToken, verifyInviteToken } from "../lib/jwt.ts"; +import { UNUSED_INVITES_RETENTION_DAYS } from "../config.ts"; -export async function createInvite(inviterId: string): Promise { - const token = await createInviteToken(inviterId); +/** Short, opaque, URL-safe invite token (128 bits of entropy → 22 chars). */ +function generateInviteToken(): string { + return randomBytes(16).toString("base64url"); +} + +export function createInvite(inviterId: string): string { + const token = generateInviteToken(); db.prepare( `INSERT INTO invites (token, inviter_id, created_at) VALUES (?, ?, ?);`, ).run(token, inviterId, new Date().toISOString()); @@ -11,25 +17,25 @@ export async function createInvite(inviterId: string): Promise { } /** - * Verifies the JWT signature + expiry and checks the token exists and has not - * been used. Returns the inviterId on success; throws APIException otherwise. + * Looks the token up, checks it has not been used and has not expired (same + * 7-day window as the startup cleanup). Returns the inviterId on success; + * throws APIException otherwise. + * + * Tokens are opaque random strings stored as the row's primary key. Invites + * created before this scheme (full JWTs) are still stored verbatim in the + * token column, so they continue to validate by direct lookup. */ -export async function validateInvite(token: string): Promise { - const payload = await verifyInviteToken(token); - if (!payload) { - throw new APIException( - APIErrorCode.NOT_FOUND, - 404, - "Invalid or expired invite", - ); - } - +export function validateInvite(token: string): string { const row = db.prepare( `SELECT token, inviter_id, used_at, created_at FROM invites WHERE token = ?;`, ).get(token); if (!row || !isInviteRow(row)) { - throw new APIException(APIErrorCode.NOT_FOUND, 404, "Invite not found"); + throw new APIException( + APIErrorCode.NOT_FOUND, + 404, + "Invalid or expired invite", + ); } if (row.used_at !== null) { @@ -40,7 +46,17 @@ export async function validateInvite(token: string): Promise { ); } - return payload.inviterId; + const expiresAt = new Date(row.created_at).getTime() + + UNUSED_INVITES_RETENTION_DAYS * 24 * 60 * 60 * 1000; + if (Date.now() > expiresAt) { + throw new APIException( + APIErrorCode.NOT_FOUND, + 404, + "Invalid or expired invite", + ); + } + + return row.inviter_id; } /** diff --git a/deno.lock b/deno.lock index 4d29c03..933334b 100644 --- a/deno.lock +++ b/deno.lock @@ -35,6 +35,7 @@ "npm:@vitejs/plugin-react-swc@^4.3.1": "4.3.1_vite@8.0.16__@types+node@26.0.0__jiti@2.7.0_@types+node@26.0.0_jiti@2.7.0", "npm:eslint-plugin-react-hooks@^7.1.1": "7.1.1_eslint@10.5.0__jiti@2.7.0_jiti@2.7.0", "npm:eslint-plugin-react-refresh@~0.5.3": "0.5.3_eslint@10.5.0__jiti@2.7.0_jiti@2.7.0", + "npm:eslint@*": "10.5.0_jiti@2.7.0", "npm:eslint@^10.5.0": "10.5.0_jiti@2.7.0", "npm:frimousse@0.3": "0.3.0_react@19.2.7_typescript@6.0.3", "npm:globals@^17.6.0": "17.6.0", @@ -49,6 +50,7 @@ "npm:react@^19.2.7": "19.2.7", "npm:remark-gfm@^4.0.1": "4.0.1", "npm:typescript-eslint@^8.61.1": "8.61.1_eslint@10.5.0__jiti@2.7.0_typescript@6.0.3_jiti@2.7.0", + "npm:typescript@*": "6.0.3", "npm:typescript@~6.0.3": "6.0.3", "npm:vite@*": "8.0.16_@types+node@26.0.0_jiti@2.7.0", "npm:vite@^8.0.16": "8.0.16_@types+node@26.0.0_jiti@2.7.0" diff --git a/src/App.css b/src/App.css index 4242b57..fdcad92 100644 --- a/src/App.css +++ b/src/App.css @@ -207,6 +207,26 @@ color: var(--color-danger); } +/* On the narrowest viewports the fixed-width action column squeezes the title + into a sliver. Stack the block: title takes the full width on top, the + vote/playlist actions sit in a row beneath it. */ +@media (max-width: 512px) { + .dump-header-block { + flex-direction: column; + align-items: stretch; + } + + .dump-header-left { + flex-direction: row; + align-items: center; + order: 2; + } + + .dump-header-right { + order: 1; + } +} + .dump-comment { font-size: 1.05rem; line-height: 1.72; @@ -1051,6 +1071,90 @@ body.has-player { padding-bottom: var(--player-height, 0px); } + +/* ── Floating "new dump" button ─────────────────────────────────────── + Revealed once the header scrolls out of view. Bottom-right by default, but + moves to the top-right while the global player is mounted (it owns the + bottom of the screen, full-width on mobile). Sits below the player and + modal backdrop (both z-index 1000). */ +.dump-fab { + position: fixed; + --fab-size: 3.5rem; + /* Sit just outside the right edge of the centered content lane — in the + margin beside it — rather than over the content or pinned to the viewport + edge. The lane width is `--fab-lane` (860px for most feeds; the journal + grid is wider, see below). Falls back to a 1.25rem viewport gutter once + that margin is too narrow to hold the button. */ + right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) - 1rem)); + /* Anchored to the bottom-right by default, but expressed via `top` (rather + than `bottom`) so the jump to the top-right when a player mounts can + animate as a slide. `dvh` tracks the visible viewport on mobile; `vh` is + the fallback for browsers without it. */ + top: calc(100vh - var(--fab-size) - 1.25rem - env(safe-area-inset-bottom, 0px)); + top: calc(100dvh - var(--fab-size) - 1.25rem - env(safe-area-inset-bottom, 0px)); + z-index: 900; + width: var(--fab-size); + height: var(--fab-size); + display: flex; + align-items: center; + justify-content: center; + padding: 0; + border: none; + /* Circle by default; a theme can square it off by setting --fab-radius: 0. */ + border-radius: var(--fab-radius, 50%); + background: var(--color-accent); + color: var(--color-on-accent); + cursor: pointer; + box-shadow: 0 4px 16px color-mix(in srgb, var(--color-accent) 45%, transparent), + 0 2px 6px rgba(0, 0, 0, 0.3); + /* Hidden state: faded out, nudged down, non-interactive. */ + opacity: 0; + transform: translateY(1rem) scale(0.9); + pointer-events: none; + transition: opacity 0.2s ease, transform 0.2s ease, background 0.15s ease, + top 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.dump-fab-icon { + width: 1.6rem; + height: 1.6rem; +} + +.dump-fab--visible { + opacity: 1; + transform: translateY(0) scale(1); + pointer-events: auto; +} + +.dump-fab:hover { + background: var(--color-accent-hover); + transform: translateY(-2px) scale(1); + box-shadow: 0 6px 20px color-mix(in srgb, var(--color-accent) 55%, transparent), + 0 2px 6px rgba(0, 0, 0, 0.3); +} + +.dump-fab:active { + transform: translateY(0) scale(0.96); +} + +/* The journal tab lays out its masonry grid in a wider lane than the other + feeds, so the button must track that wider edge to stay aligned with it. */ +body:has(.journal-grid) { + --fab-lane: 1180px; +} + +/* The player occupies the bottom of the screen (full-width on mobile), so when + one is mounted slide the button up to the top-right corner to stay clear. */ +body.has-player .dump-fab { + top: calc(1.25rem + env(safe-area-inset-top, 0px)); +} + +/* Reserve room at the end of the scroll so the button never hides the last + content when scrolled all the way down. */ +body.has-fab .page-content { + padding-bottom: 5rem; +} + .rich-content-thumbnail-btn { position: relative; padding: 0; @@ -1674,10 +1778,6 @@ body.has-player { display: flex; align-items: center; gap: 0.5rem; - background: var(--color-surface); - border: 1px solid var(--color-border-subtle); - border-radius: 6px; - padding: 0.3rem 0.5rem 0.3rem 0.75rem; max-width: 480px; } .invite-url { @@ -1685,11 +1785,18 @@ body.has-player { font-family: var(--font-mono, monospace); color: var(--color-text-muted); flex: 1; - overflow: hidden; - text-overflow: ellipsis; + min-width: 0; + overflow-x: auto; white-space: nowrap; + background: var(--color-surface); + border: 1px solid var(--color-border-subtle); + border-radius: 6px; + padding: 0.35rem 0.6rem; } +/* Always-visible copy target. Sits first so it's the obvious tap. */ .invite-copy-btn { + order: -1; + flex-shrink: 0; padding: 0.2rem 0.65rem; border: 1px solid var(--color-border-subtle); border-radius: 4px; @@ -1698,7 +1805,6 @@ body.has-player { font-size: 0.75rem; cursor: pointer; white-space: nowrap; - flex-shrink: 0; transition: background 0.12s; } .invite-copy-btn:hover { diff --git a/src/App.tsx b/src/App.tsx index 86d5cc5..c24fadd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,8 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router"; import { RestrictedGuest } from "./pages/RestrictedGuest.tsx"; import { RestrictedLoggedIn } from "./pages/RestrictedLoggedIn.tsx"; +import { useAuth } from "./hooks/useAuth.ts"; +import { useDefaultFeedTab } from "./hooks/useDefaultFeedTab.ts"; import { AuthProvider } from "./contexts/AuthProvider.tsx"; import { PlayerProvider } from "./contexts/PlayerProvider.tsx"; @@ -10,6 +12,7 @@ import { WSProvider } from "./contexts/WSProvider.tsx"; import { FollowProvider } from "./contexts/FollowProvider.tsx"; import { ThemeProvider } from "./contexts/ThemeProvider.tsx"; import { GlobalPlayer } from "./components/GlobalPlayer.tsx"; +import { DumpFab } from "./components/DumpFab.tsx"; import "./App.css"; @@ -66,63 +69,74 @@ const NotFound = lazy(() => import("./pages/NotFound.tsx").then((m) => ({ default: m.NotFound })) ); +function IndexRedirect() { + const { user } = useAuth(); + const [preferredTab] = useDefaultFeedTab(); + // "followed" requires a session, so fall back to "hot" for guests. + const tab = preferredTab === "followed" && !user ? "hot" : preferredTab; + return ; +} + function AppRoutes() { return ( - - - } /> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - } - /> - } /> - } /> - } - /> - } /> - } /> - } /> - } /> - - - - } - /> - } /> - - + <> + + + } /> + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + } + /> + } /> + } /> + } + /> + } /> + } /> + } /> + } /> + + + + } + /> + } /> + + + + ); } diff --git a/src/components/DumpFab.tsx b/src/components/DumpFab.tsx new file mode 100644 index 0000000..a64b789 --- /dev/null +++ b/src/components/DumpFab.tsx @@ -0,0 +1,93 @@ +import { lazy, Suspense, useEffect, useState } from "react"; +import { useLocation } from "react-router"; +import { t } from "@lingui/core/macro"; +import { useAuth } from "../hooks/useAuth.ts"; + +const DumpCreateModal = lazy(() => + import("./DumpCreateModal.tsx").then((m) => ({ default: m.DumpCreateModal })) +); + +/** + * Floating "new dump" button shown once the header has scrolled out of view. + * Sits in the bottom-right corner and lifts above the global player (which is + * full-width on mobile) so the two never overlap. Only rendered for signed-in + * users — dump creation requires an account. + * + * Mounted once at the app root (not per-page) since some pages render their own + * header instead of going through PageShell. Visibility is derived from a live + * read of the current page's `.app-header`, so it survives client-side + * navigation and lazily-mounted route headers. + */ +export function DumpFab() { + const { user } = useAuth(); + const location = useLocation(); + const [headerHidden, setHeaderHidden] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + + // Reserve clearance at the end of the scroll so the floating button never + // hides the last content when scrolled all the way down. + useEffect(() => { + if (!user) return; + document.body.classList.add("has-fab"); + return () => document.body.classList.remove("has-fab"); + }, [user]); + + // Reveal the button once the header's bottom edge scrolls above the viewport. + // Re-reading the header on every tick keeps this correct across navigation + // and lazily-mounted pages, where the header element is replaced. + useEffect(() => { + if (!user) return; + const update = () => { + const header = document.querySelector(".app-header"); + setHeaderHidden(!!header && header.getBoundingClientRect().bottom <= 0); + }; + update(); + globalThis.addEventListener("scroll", update, { passive: true }); + globalThis.addEventListener("resize", update); + // Body height changes when a lazy route finishes mounting — re-check then, + // since no scroll/resize event fires in that case. + const ro = new ResizeObserver(update); + ro.observe(document.body); + return () => { + globalThis.removeEventListener("scroll", update); + globalThis.removeEventListener("resize", update); + ro.disconnect(); + }; + }, [user, location.pathname]); + + if (!user) return null; + + return ( + <> + + {modalOpen && ( + + setModalOpen(false)} /> + + )} + + ); +} diff --git a/src/components/FeedTabBar.tsx b/src/components/FeedTabBar.tsx index 1c9cf9d..f1f1d77 100644 --- a/src/components/FeedTabBar.tsx +++ b/src/components/FeedTabBar.tsx @@ -2,11 +2,16 @@ import { Trans } from "@lingui/react/macro"; import { useAuth } from "../hooks/useAuth.ts"; import { FEED_TABS, type FeedTab } from "../config/feedTabs.ts"; import { useTabParam } from "../hooks/useTabParam.ts"; +import { useDefaultFeedTab } from "../hooks/useDefaultFeedTab.ts"; import { TabBar } from "./TabBar.tsx"; export function FeedTabBar() { const { user } = useAuth(); - const [tab, setTab] = useTabParam(FEED_TABS, "hot"); + const [preferredTab] = useDefaultFeedTab(); + const defaultTab: FeedTab = preferredTab === "followed" && !user + ? "hot" + : preferredTab; + const [tab, setTab] = useTabParam(FEED_TABS, defaultTab); const tabs = [ { key: "hot" as FeedTab, label: Hot }, diff --git a/src/components/GlobalPlayer.tsx b/src/components/GlobalPlayer.tsx index fdf8588..befe47e 100644 --- a/src/components/GlobalPlayer.tsx +++ b/src/components/GlobalPlayer.tsx @@ -10,8 +10,16 @@ function itemKey( } export function GlobalPlayer() { - const { current, stop, seekRef, toggleRef, onPlayStateChange, onTimeUpdate } = - useContext(PlayerContext); + const { + current, + startTime, + autoplay, + stop, + seekRef, + toggleRef, + onPlayStateChange, + onTimeUpdate, + } = useContext(PlayerContext); const ref = useRef(null); const [reduced, setReduced] = useState(false); const [prevKey, setPrevKey] = useState(itemKey(current)); @@ -98,7 +106,8 @@ export function GlobalPlayer() { src={current.fileUrl} kind={current.mimeType.startsWith("video/") ? "video" : "audio"} mime={current.mimeType} - autoplay + autoplay={autoplay} + startTime={startTime} onPlayStateChange={onPlayStateChange} onTimeUpdate={onTimeUpdate} seekRef={seekRef} diff --git a/src/components/MediaPlayer.tsx b/src/components/MediaPlayer.tsx index e6d6d2d..dde5d9e 100644 --- a/src/components/MediaPlayer.tsx +++ b/src/components/MediaPlayer.tsx @@ -129,6 +129,7 @@ interface MediaPlayerProps { mime?: string; poster?: string; autoplay?: boolean; + startTime?: number; onPlayStateChange?: (playing: boolean) => void; onTimeUpdate?: (time: number, duration: number) => void; seekRef?: { current: ((t: number) => void) | null }; @@ -142,6 +143,7 @@ export function MediaPlayer( mime, poster, autoplay, + startTime, onPlayStateChange, onTimeUpdate, seekRef, @@ -209,6 +211,21 @@ export function MediaPlayer( // ── Effects ────────────────────────────────────────────────────────────────── + // Resume to a restored offset once metadata (and thus seekability) is available. + // Runs only on mount for the initial offset — later seeks go through seekTo. + useEffect(() => { + if (!startTime) return; + const a = mediaRef.current!; + const apply = () => { + a.currentTime = startTime; + setCurrent(startTime); + }; + if (a.readyState >= 1) apply(); + else a.addEventListener("loadedmetadata", apply, { once: true }); + return () => a.removeEventListener("loadedmetadata", apply); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // Autoplay on mount (e.g. triggered by play() in PlayerContext) useEffect(() => { if (!autoplay) return; diff --git a/src/contexts/PlayerContext.ts b/src/contexts/PlayerContext.ts index dc27909..1b9e8bf 100644 --- a/src/contexts/PlayerContext.ts +++ b/src/contexts/PlayerContext.ts @@ -11,6 +11,12 @@ export interface PlayerContextValue { currentTime: number; duration: number; + // Initial seek offset for the active item (non-zero only for an item restored + // from a previous session) and whether the active MediaPlayer should autoplay. + // A restored item appears paused; a fresh play() autoplays on the user gesture. + startTime: number; + autoplay: boolean; + // Control — callable by any consumer play(item: PlayerItem): void; stop(): void; @@ -32,6 +38,8 @@ export const PlayerContext = createContext({ playing: false, currentTime: 0, duration: 0, + startTime: 0, + autoplay: false, play: () => {}, stop: () => {}, seekTo: () => {}, diff --git a/src/contexts/PlayerProvider.tsx b/src/contexts/PlayerProvider.tsx index 5edb7ce..420e568 100644 --- a/src/contexts/PlayerProvider.tsx +++ b/src/contexts/PlayerProvider.tsx @@ -1,11 +1,39 @@ -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { PlayerContext, type PlayerItem } from "./PlayerContext.ts"; +const STORAGE_KEY = "player"; + +// Snapshot persisted across reloads: which item was playing and how far in. +interface StoredSession { + item: PlayerItem; + time: number; +} + +function readSession(): StoredSession | null { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + const parsed = JSON.parse(raw) as Partial; + if (!parsed.item || (parsed.item.kind !== "embed" && parsed.item.kind !== "file")) { + return null; + } + return { item: parsed.item as PlayerItem, time: Number(parsed.time) || 0 }; + } catch { + return null; + } +} + export function PlayerProvider({ children }: { children: React.ReactNode }) { - const [current, setCurrent] = useState(null); + const restored = useRef(readSession()).current; + + const [current, setCurrent] = useState(restored?.item ?? null); const [playing, setPlaying] = useState(false); - const [currentTime, setCurrentTime] = useState(0); + const [currentTime, setCurrentTime] = useState(restored?.time ?? 0); const [duration, setDuration] = useState(0); + // Resume offset for the active item — only the restored item carries one. + const [startTime, setStartTime] = useState(restored?.time ?? 0); + // Restored items start paused (no user gesture); fresh play() autoplays. + const [autoplay, setAutoplay] = useState(false); // GlobalPlayer registers the active MediaPlayer's imperative handles here const seekRef = useRef<((t: number) => void) | null>(null); @@ -20,6 +48,8 @@ export function PlayerProvider({ children }: { children: React.ReactNode }) { setCurrent(item); setCurrentTime(0); setDuration(0); + setStartTime(0); + setAutoplay(true); setPlaying(false); }, []); @@ -28,6 +58,7 @@ export function PlayerProvider({ children }: { children: React.ReactNode }) { setPlaying(false); setCurrentTime(0); setDuration(0); + setStartTime(0); }, []); const seekTo = useCallback((t: number) => { @@ -50,11 +81,45 @@ export function PlayerProvider({ children }: { children: React.ReactNode }) { setDuration(d); }, []); + // ── Persistence ──────────────────────────────────────────────────────────────── + // Write the item to storage as soon as it changes (survives crashes), then refresh + // the playback position on pagehide — which also fires on a normal reload — so the + // resume offset is accurate without thrashing localStorage on every timeupdate. + useEffect(() => { + if (current) { + localStorage.setItem( + STORAGE_KEY, + JSON.stringify({ item: current, time: currentTime } satisfies StoredSession), + ); + } else { + localStorage.removeItem(STORAGE_KEY); + } + // currentTime intentionally omitted from deps — see pagehide writer below. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [current]); + + const sessionRef = useRef(null); + sessionRef.current = current ? { item: current, time: currentTime } : null; + + useEffect(() => { + const persist = () => { + if (sessionRef.current) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(sessionRef.current)); + } else { + localStorage.removeItem(STORAGE_KEY); + } + }; + globalThis.addEventListener("pagehide", persist); + return () => globalThis.removeEventListener("pagehide", persist); + }, []); + const value = useMemo(() => ({ current, playing, currentTime, duration, + startTime, + autoplay, play, stop, seekTo, @@ -68,6 +133,8 @@ export function PlayerProvider({ children }: { children: React.ReactNode }) { playing, currentTime, duration, + startTime, + autoplay, play, stop, seekTo, diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 644c78f..e0ef633 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -1,4 +1,4 @@ -import { useContext } from "react"; +import { useCallback, useContext } from "react"; import { AuthContext } from "../contexts/AuthContext.ts"; @@ -16,17 +16,20 @@ function isTokenExpired(token: string): boolean { export const useAuth = () => { const { authResponse, setAuthResponse } = useContext(AuthContext); - const login = (response: AuthResponse) => { + const login = useCallback((response: AuthResponse) => { setAuthResponse(response); localStorage.setItem("authResponse", JSON.stringify(response)); - }; + }, [setAuthResponse]); - const logout = () => { + const logout = useCallback(() => { setAuthResponse(null); localStorage.removeItem("authResponse"); - }; + }, [setAuthResponse]); - const authFetch = async (input: RequestInfo, init: RequestInit = {}) => { + const authFetch = useCallback(async ( + input: RequestInfo, + init: RequestInit = {}, + ) => { const token = authResponse?.token; if (token && isTokenExpired(token)) { @@ -54,7 +57,7 @@ export const useAuth = () => { } return res; - }; + }, [authResponse?.token, logout]); return { user: authResponse?.user ?? null, diff --git a/src/hooks/useDefaultFeedTab.ts b/src/hooks/useDefaultFeedTab.ts new file mode 100644 index 0000000..be7e3e1 --- /dev/null +++ b/src/hooks/useDefaultFeedTab.ts @@ -0,0 +1,28 @@ +import { useCallback, useState } from "react"; +import { FEED_TABS, type FeedTab } from "../config/feedTabs.ts"; + +const STORAGE_KEY = "default-feed-tab"; + +function readStored(): FeedTab { + const stored = localStorage.getItem(STORAGE_KEY); + return stored && (FEED_TABS as readonly string[]).includes(stored) + ? stored as FeedTab + : "hot"; +} + +/** + * User preference for which feed tab the index opens on, persisted to + * localStorage (defaults to "hot"). Returns the stored tab and a setter that + * persists the choice. Consumers that only need to honour the preference (e.g. + * the index default) can ignore the setter. + */ +export function useDefaultFeedTab(): [FeedTab, (tab: FeedTab) => void] { + const [tab, setTabState] = useState(readStored); + + const setTab = useCallback((t: FeedTab) => { + localStorage.setItem(STORAGE_KEY, t); + setTabState(t); + }, []); + + return [tab, setTab]; +} diff --git a/src/locales/en.js b/src/locales/en.js index 4f5bfa4..effc96e 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -1 +1 @@ -/*eslint-disable*/module.exports={messages:JSON.parse("{\"-K9EZb\":[\"Add email…\"],\"-OxI15\":[\"Followed playlists\"],\"-Ya-b9\":[\"Failed to save\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Search dumps, users, playlists…\"],\"1cbYY_\":[\"Upvoted (\",[\"0\"],[\"1\"],\")\"],\"1njn7W\":[\"Light\"],\"1utXA6\":[\"Dumps\"],\"26iNma\":[\"Post comment\"],\"2Hlmdt\":[\"Write a reply…\"],\"2ygf_L\":[\"← Back\"],\"3KKSM4\":[\"private\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" followed your playlist <1>\",[\"1\"],\"\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Dumped!\"],\"4GKuCs\":[\"Login failed\"],\"4HH9iB\":[\"Not following anyone yet.\"],\"4RtQ1k\":[\"Unfollow \",[\"targetUsername\"]],\"4c-qBx\":[\"Your password has been changed. You can now log in.\"],\"4yj9xV\":[\"Live updates are temporarily disconnected. Trying to reconnect…\"],\"5TviPn\":[\"Cancel search\"],\"5cC8f2\":[\"Edited \",[\"0\"]],\"5oD9f_\":[\"Earlier\"],\"6Qly-0\":[\"a comment\"],\"6gRgw8\":[\"Retry\"],\"7PHCIN\":[\"Cancel removal\"],\"7d1a0d\":[\"Public\"],\"7sNhEz\":[\"Username\"],\"8Ug9jB\":[\"Related\"],\"8ZsakT\":[\"Password\"],\"8pxhI8\":[\"Please select a file.\"],\"9BruTc\":[\"Why?\"],\"9l4qcT\":[\"Drop a replacement here\"],\"9uI_rE\":[\"Undo\"],\"9xDZu_\":[\"Could not load.\"],\"A0y396\":[\"+ Invite someone\"],\"A1taO8\":[\"Search\"],\"AQbgNR\":[\"Follow \",[\"targetUsername\"]],\"ATGYL1\":[\"Email address\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"Ade-6d\":[\"Live updates unavailable.\"],\"AeXO77\":[\"Account\"],\"CI50ct\":[\"Upvoted\"],\"Cj24wt\":[\"Registering…\"],\"DPfwMq\":[\"Done\"],\"DdeHXH\":[\"Delete this dump? This cannot be undone.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" upvoted <1>\",[\"1\"],\"\"],\"ECiS12\":[\"View dump →\"],\"ExR0Fr\":[\"URL is required.\"],\"F5Js1v\":[\"Unfollow playlist\"],\"FgAxTj\":[\"Log out\"],\"Fxf4jq\":[\"Description (optional)\"],\"GNSsCc\":[\"Failed to create playlist\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" characters: letters, numbers, or underscores\"],\"GptGxg\":[\"Change password\"],\"H4o4sk\":[\"No one yet.\"],\"H8pzW-\":[\"Failed to update avatar\"],\"HTLDA4\":[\"Loading more…\"],\"I-x669\":[\"Invitees\"],\"IZX7TO\":[\"Failed to generate invite\"],\"IagCbF\":[\"URL\"],\"ImOQa9\":[\"Reply\"],\"J2eKUI\":[\"File\"],\"JJ-Bhk\":[\"Your email address\"],\"JRQitQ\":[\"Confirm new password\"],\"JXr41k\":[\"<0>\",[\"0\"],\" commented on <1>\",[\"1\"],\"\"],\"Jd58Fo\":[\"Hot\"],\"Jf0PuK\":[\"Go to login\"],\"KDGWg5\":[\"Remove from playlist\"],\"K_F6pa\":[\"Saving…\"],\"L-rMC9\":[\"Reset to default\"],\"LLyMkV\":[\"Followed (\",[\"0\"],[\"1\"],\")\"],\"MHrjPM\":[\"Title\"],\"MKEPCY\":[\"Follow\"],\"Oprv1v\":[\"Password (min. \",[\"0\"],\" characters)\"],\"Oz0N9s\":[\"new\"],\"PiH3UR\":[\"Copied!\"],\"Pn2B7_\":[\"Current password\"],\"Pwqkdw\":[\"Loading…\"],\"Q6n4F4\":[\"Refresh metadata\"],\"QKsaQr\":[\"or <0>browse files\"],\"QLtPBd\":[\"No dumps in this playlist yet.\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Delete this playlist? This cannot be undone.\"],\"RTksSy\":[\"<0>\",[\"0\"],\" started following you\"],\"RaKjrM\":[\"Failed to save edit\"],\"RcUHRT\":[\"Followed\"],\"Rrp6-J\":[\"Log in to like\"],\"SBTElJ\":[\"Searching…\"],\"Sad2tK\":[\"Sending…\"],\"StovX6\":[\"This page does not exist.\"],\"Sxm8rQ\":[\"Users\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" was added to <1>\",[\"1\"],\"\"],\"TM1ZbA\":[\"Playlists (\",[\"0\"],[\"1\"],\")\"],\"TN382O\":[\"Invalid link\"],\"Tv9vbB\":[\"Follow playlist\"],\"Tz0i8g\":[\"Settings\"],\"UNMVei\":[\"Forgot password?\"],\"UOZith\":[\"Failed to post\"],\"URAieT\":[\"Log in to vote\"],\"UTiUFs\":[\"Fetching…\"],\"VCoEm-\":[\"Back to login\"],\"V_e7nf\":[\"Set new password\"],\"VnNJbN\":[\"From playlists\"],\"VyTYmS\":[\"Change avatar\"],\"W9FRBT\":[\"Like\"],\"WhimMi\":[\"Reset failed\"],\"WpXcBJ\":[\"Nothing here yet.\"],\"XJy2oN\":[\"Logging in…\"],\"Xan6QP\":[\"New dump\"],\"XgRtUf\":[\"Could not change password\"],\"Xi0Mn4\":[\"← Back to profile\"],\"XnL-Eu\":[\"No users match \\\"\",[\"q\"],\"\\\".\"],\"Xs2Lez\":[\"This reset link is missing or malformed.\"],\"YK1Dhc\":[\"a post\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" liked your comment on <1>\",[\"1\"],\"\"],\"YpkCca\":[\"No followed playlists yet.\"],\"ZBdbv9\":[\"Edit title\"],\"ZCpU0u\":[\"No playlists match \\\"\",[\"q\"],\"\\\".\"],\"ZmD2o6\":[\"Create & Add\"],\"_3O5R_\":[\"Request failed\"],\"_DwR-n\":[\"Creating…\"],\"_R_sGB\":[\"Password changed successfully.\"],\"_aept4\":[\"Post reply\"],\"_nT6AE\":[\"New password\"],\"_t4W-i\":[\"From people\"],\"aAIQg2\":[\"Appearance\"],\"aDvLhk\":[\"Add a comment…\"],\"b3Thhd\":[\"Upload failed\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"bQhwn-\":[\"Loading playlist…\"],\"cILfnJ\":[\"Remove file\"],\"cYP9Sb\":[\"+ Playlist\"],\"cbeBbZ\":[\"At least \",[\"0\"],\" characters\"],\"cnGeoo\":[\"Delete\"],\"d8DZWS\":[\"Open search\"],\"dAs22m\":[\"Replace file\"],\"dEgA5A\":[\"Cancel\"],\"dMizp8\":[\"New playlist\"],\"eFSqvc\":[\"Failed to post reply\"],\"eOfXq3\":[\"Title is required.\"],\"ePK91l\":[\"Edit\"],\"eaUTwS\":[\"Send reset link\"],\"ecUA8p\":[\"Today\"],\"ef9nPf\":[\"Loading dump…\"],\"en9o7K\":[\"Failed to post comment\"],\"etFQQS\":[\"What makes it worth it?\"],\"fC6mXb\":[\"Upvote\"],\"fI-mNw\":[\"Playlists\"],\"f_akpP\":[\"Max 50 MB\"],\"fgLNSM\":[\"Register\"],\"gANddk\":[\"Uploading…\"],\"gGx5tM\":[\"Editing\"],\"gIQQwD\":[\"Failed to load\"],\"gLfZlz\":[\"Add to playlist\"],\"gjJ-sb\":[\"Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects.\"],\"hBuUKa\":[\"Change password…\"],\"hD7w09\":[\"You've reached the end.\"],\"hJSliC\":[\"<0>\",[\"0\"],\" posted <1>\",[\"1\"],\"\"],\"hYgDIe\":[\"Create\"],\"he3ygx\":[\"Copy\"],\"i7K_Te\":[\"Who am I?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Go home\"],\"ipYn7W\":[\"If that address is registered you'll receive a reset link shortly.\"],\"isRobC\":[\"New\"],\"jbernk\":[\"Loading profile…\"],\"joEmfT\":[\"Server unreachable\"],\"jrZTZl\":[\"No dumps yet. Be the first!\"],\"kLttbL\":[\"Registration failed\"],\"lUDifl\":[\"Created (\",[\"0\"],[\"1\"],\")\"],\"lUanmi\":[\"You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}]],\"lcfvr_\":[\"Delete this comment?\"],\"lpIMne\":[\"Passwords do not match\"],\"mt6O6E\":[\"This is a mirage.\"],\"nbm5sI\":[\"No dumps match \\\"\",[\"q\"],\"\\\".\"],\"nrjqON\":[\"Checking invite…\"],\"nwtY4N\":[\"Something went wrong\"],\"nx4kaN\":[\"Could not save\"],\"ogtYkT\":[\"Password updated\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" mentioned you in <1>\",[\"where\"],\"\"],\"pSheLH\":[\"No invitees yet.\"],\"pvnfJD\":[\"Dark\"],\"qIMfNQ\":[\"Delete playlist\"],\"qbDAcy\":[\"Dump it\"],\"qgx_78\":[\"Follow some public playlists to see their dumps here.\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Dump\"],\"rCbqPX\":[\"This invite link is missing, expired, or already used.\"],\"rg9pXu\":[\"Search failed\"],\"rtpJqV\":[\"Dumps (\",[\"0\"],[\"1\"],\")\"],\"sGeXL3\":[\"Thumbnail\"],\"sQia9P\":[\"Log in\"],\"sTiqbm\":[\"invited by\"],\"sdP5Aa\":[\"[deleted]\"],\"shHs8T\":[\"Enter a query to search.\"],\"smeBfS\":[\"Invalid invite\"],\"tfDRzk\":[\"Save\"],\"tvmuQ0\":[\"Color scheme\"],\"u1lDX2\":[\"Fetching preview…\"],\"uD0qXQ\":[\"Drop a file here\"],\"uMGUnV\":[\"No playlists yet.\"],\"ub1EEL\":[\"edited \",[\"0\"]],\"vJBF1r\":[\"Posting…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" unread)\"],\"vQMkHu\":[\"Remove vote\"],\"vuosjb\":[\"User menu\"],\"wbXKOv\":[\"File too large (max 50 MB).\"],\"wixIgH\":[\"Already have an account? <0>Log in\"],\"xEWkgZ\":[\"← Back to all dumps\"],\"xPHtx0\":[\"Submit search\"],\"xVuNgt\":[\"+ New playlist\"],\"xc9O_u\":[\"Delete dump\"],\"y6sq5j\":[\"Following\"],\"y7oaHj\":[\"Playlist title\"],\"yA_6BX\":[\"View all →\"],\"yBBtRm\":[\"Follow some users to see their dumps here.\"],\"yQ2kGp\":[\"Load more\"],\"y_0uwd\":[\"Yesterday\"],\"yz7wBu\":[\"Close\"],\"z0ROB3\":[\"Remove like\"],\"z1uNN0\":[\"No emoji found.\"],\"zVuxvN\":[\"Refreshing…\"],\"zwBp5t\":[\"Private\"]}")}; \ No newline at end of file +/*eslint-disable*/module.exports={messages:JSON.parse("{\"-K9EZb\":[\"Add email…\"],\"-OxI15\":[\"Followed playlists\"],\"-Ya-b9\":[\"Failed to save\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Search dumps, users, playlists…\"],\"1cbYY_\":[\"Upvoted (\",[\"0\"],[\"1\"],\")\"],\"1njn7W\":[\"Light\"],\"1utXA6\":[\"Dumps\"],\"26iNma\":[\"Post comment\"],\"2DoBvq\":[\"Feeds\"],\"2Hlmdt\":[\"Write a reply…\"],\"2ygf_L\":[\"← Back\"],\"3KKSM4\":[\"private\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" followed your playlist <1>\",[\"1\"],\"\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Dumped!\"],\"4GKuCs\":[\"Login failed\"],\"4HH9iB\":[\"Not following anyone yet.\"],\"4RtQ1k\":[\"Unfollow \",[\"targetUsername\"]],\"4c-qBx\":[\"Your password has been changed. You can now log in.\"],\"4yj9xV\":[\"Live updates are temporarily disconnected. Trying to reconnect…\"],\"5TviPn\":[\"Cancel search\"],\"5cC8f2\":[\"Edited \",[\"0\"]],\"5oD9f_\":[\"Earlier\"],\"6Qly-0\":[\"a comment\"],\"6gRgw8\":[\"Retry\"],\"7PHCIN\":[\"Cancel removal\"],\"7d1a0d\":[\"Public\"],\"7sNhEz\":[\"Username\"],\"8Ug9jB\":[\"Related\"],\"8ZsakT\":[\"Password\"],\"8pxhI8\":[\"Please select a file.\"],\"9BruTc\":[\"Why?\"],\"9l4qcT\":[\"Drop a replacement here\"],\"9uI_rE\":[\"Undo\"],\"9xDZu_\":[\"Could not load.\"],\"A0y396\":[\"+ Invite someone\"],\"A1taO8\":[\"Search\"],\"AQbgNR\":[\"Follow \",[\"targetUsername\"]],\"ATGYL1\":[\"Email address\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"Ade-6d\":[\"Live updates unavailable.\"],\"AeXO77\":[\"Account\"],\"CI50ct\":[\"Upvoted\"],\"Cj24wt\":[\"Registering…\"],\"DPfwMq\":[\"Done\"],\"DdeHXH\":[\"Delete this dump? This cannot be undone.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" upvoted <1>\",[\"1\"],\"\"],\"ECiS12\":[\"View dump →\"],\"ExR0Fr\":[\"URL is required.\"],\"F5Js1v\":[\"Unfollow playlist\"],\"FgAxTj\":[\"Log out\"],\"Fxf4jq\":[\"Description (optional)\"],\"GNSsCc\":[\"Failed to create playlist\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" characters: letters, numbers, or underscores\"],\"GptGxg\":[\"Change password\"],\"H4o4sk\":[\"No one yet.\"],\"H8pzW-\":[\"Failed to update avatar\"],\"HTLDA4\":[\"Loading more…\"],\"I-x669\":[\"Invitees\"],\"IZX7TO\":[\"Failed to generate invite\"],\"IagCbF\":[\"URL\"],\"ImOQa9\":[\"Reply\"],\"J2eKUI\":[\"File\"],\"JJ-Bhk\":[\"Your email address\"],\"JRQitQ\":[\"Confirm new password\"],\"JXr41k\":[\"<0>\",[\"0\"],\" commented on <1>\",[\"1\"],\"\"],\"Jd58Fo\":[\"Hot\"],\"Jf0PuK\":[\"Go to login\"],\"KDGWg5\":[\"Remove from playlist\"],\"K_F6pa\":[\"Saving…\"],\"L-rMC9\":[\"Reset to default\"],\"LLyMkV\":[\"Followed (\",[\"0\"],[\"1\"],\")\"],\"MHrjPM\":[\"Title\"],\"MKEPCY\":[\"Follow\"],\"Oprv1v\":[\"Password (min. \",[\"0\"],\" characters)\"],\"Oz0N9s\":[\"new\"],\"PiH3UR\":[\"Copied!\"],\"Pn2B7_\":[\"Current password\"],\"Pwqkdw\":[\"Loading…\"],\"Q6n4F4\":[\"Refresh metadata\"],\"QKsaQr\":[\"or <0>browse files\"],\"QLtPBd\":[\"No dumps in this playlist yet.\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Delete this playlist? This cannot be undone.\"],\"RTksSy\":[\"<0>\",[\"0\"],\" started following you\"],\"RaKjrM\":[\"Failed to save edit\"],\"RcUHRT\":[\"Followed\"],\"Rrp6-J\":[\"Log in to like\"],\"SBTElJ\":[\"Searching…\"],\"Sad2tK\":[\"Sending…\"],\"StovX6\":[\"This page does not exist.\"],\"Sxm8rQ\":[\"Users\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" was added to <1>\",[\"1\"],\"\"],\"TM1ZbA\":[\"Playlists (\",[\"0\"],[\"1\"],\")\"],\"TN382O\":[\"Invalid link\"],\"Tv9vbB\":[\"Follow playlist\"],\"Tz0i8g\":[\"Settings\"],\"UNMVei\":[\"Forgot password?\"],\"UOZith\":[\"Failed to post\"],\"URAieT\":[\"Log in to vote\"],\"UTiUFs\":[\"Fetching…\"],\"VCoEm-\":[\"Back to login\"],\"V_e7nf\":[\"Set new password\"],\"VnNJbN\":[\"From playlists\"],\"VyTYmS\":[\"Change avatar\"],\"W9FRBT\":[\"Like\"],\"WhimMi\":[\"Reset failed\"],\"WpXcBJ\":[\"Nothing here yet.\"],\"XJy2oN\":[\"Logging in…\"],\"Xan6QP\":[\"New dump\"],\"XgRtUf\":[\"Could not change password\"],\"Xi0Mn4\":[\"← Back to profile\"],\"XnL-Eu\":[\"No users match \\\"\",[\"q\"],\"\\\".\"],\"Xs2Lez\":[\"This reset link is missing or malformed.\"],\"YK1Dhc\":[\"a post\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" liked your comment on <1>\",[\"1\"],\"\"],\"YpkCca\":[\"No followed playlists yet.\"],\"ZBdbv9\":[\"Edit title\"],\"ZCpU0u\":[\"No playlists match \\\"\",[\"q\"],\"\\\".\"],\"ZmD2o6\":[\"Create & Add\"],\"_3O5R_\":[\"Request failed\"],\"_DwR-n\":[\"Creating…\"],\"_R_sGB\":[\"Password changed successfully.\"],\"_aept4\":[\"Post reply\"],\"_nT6AE\":[\"New password\"],\"_t4W-i\":[\"From people\"],\"aAIQg2\":[\"Appearance\"],\"aDvLhk\":[\"Add a comment…\"],\"b3Thhd\":[\"Upload failed\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" comment\"],\"other\":[\"#\",\" comments\"]}]],\"bQhwn-\":[\"Loading playlist…\"],\"cILfnJ\":[\"Remove file\"],\"cYP9Sb\":[\"+ Playlist\"],\"cbeBbZ\":[\"At least \",[\"0\"],\" characters\"],\"cnGeoo\":[\"Delete\"],\"d8DZWS\":[\"Open search\"],\"dAs22m\":[\"Replace file\"],\"dEgA5A\":[\"Cancel\"],\"dMizp8\":[\"New playlist\"],\"eFSqvc\":[\"Failed to post reply\"],\"eOfXq3\":[\"Title is required.\"],\"ePK91l\":[\"Edit\"],\"eaUTwS\":[\"Send reset link\"],\"ecUA8p\":[\"Today\"],\"ef9nPf\":[\"Loading dump…\"],\"en9o7K\":[\"Failed to post comment\"],\"etFQQS\":[\"What makes it worth it?\"],\"fC6mXb\":[\"Upvote\"],\"fI-mNw\":[\"Playlists\"],\"f_akpP\":[\"Max 50 MB\"],\"fgLNSM\":[\"Register\"],\"gANddk\":[\"Uploading…\"],\"gGx5tM\":[\"Editing\"],\"gIQQwD\":[\"Failed to load\"],\"gLfZlz\":[\"Add to playlist\"],\"gjJ-sb\":[\"Can't connect to the live updates server. Upvotes and notifications may not sync until it reconnects.\"],\"hBuUKa\":[\"Change password…\"],\"hD7w09\":[\"You've reached the end.\"],\"hJSliC\":[\"<0>\",[\"0\"],\" posted <1>\",[\"1\"],\"\"],\"hYgDIe\":[\"Create\"],\"he3ygx\":[\"Copy\"],\"i7K_Te\":[\"Who am I?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Go home\"],\"ipYn7W\":[\"If that address is registered you'll receive a reset link shortly.\"],\"isRobC\":[\"New\"],\"jbernk\":[\"Loading profile…\"],\"joEmfT\":[\"Server unreachable\"],\"jrZTZl\":[\"No dumps yet. Be the first!\"],\"kLttbL\":[\"Registration failed\"],\"lUDifl\":[\"Created (\",[\"0\"],[\"1\"],\")\"],\"lUanmi\":[\"You'll be notified when someone follows your playlists, upvotes your dumps, or posts new content.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" dump\"],\"other\":[\"#\",\" dumps\"]}]],\"lcfvr_\":[\"Delete this comment?\"],\"lpIMne\":[\"Passwords do not match\"],\"mt6O6E\":[\"This is a mirage.\"],\"nbm5sI\":[\"No dumps match \\\"\",[\"q\"],\"\\\".\"],\"nrjqON\":[\"Checking invite…\"],\"nwtY4N\":[\"Something went wrong\"],\"nx4kaN\":[\"Could not save\"],\"ogtYkT\":[\"Password updated\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" mentioned you in <1>\",[\"where\"],\"\"],\"pSheLH\":[\"No invitees yet.\"],\"pvnfJD\":[\"Dark\"],\"qIMfNQ\":[\"Delete playlist\"],\"qbDAcy\":[\"Dump it\"],\"qgx_78\":[\"Follow some public playlists to see their dumps here.\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Dump\"],\"rCbqPX\":[\"This invite link is missing, expired, or already used.\"],\"rg9pXu\":[\"Search failed\"],\"rtpJqV\":[\"Dumps (\",[\"0\"],[\"1\"],\")\"],\"sGeXL3\":[\"Thumbnail\"],\"sQia9P\":[\"Log in\"],\"sTiqbm\":[\"invited by\"],\"sdP5Aa\":[\"[deleted]\"],\"shHs8T\":[\"Enter a query to search.\"],\"smeBfS\":[\"Invalid invite\"],\"tfDRzk\":[\"Save\"],\"tvmuQ0\":[\"Color scheme\"],\"u1lDX2\":[\"Fetching preview…\"],\"uD0qXQ\":[\"Drop a file here\"],\"uMGUnV\":[\"No playlists yet.\"],\"ub1EEL\":[\"edited \",[\"0\"]],\"vJBF1r\":[\"Posting…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" unread)\"],\"vQMkHu\":[\"Remove vote\"],\"vuosjb\":[\"User menu\"],\"wbXKOv\":[\"File too large (max 50 MB).\"],\"wixIgH\":[\"Already have an account? <0>Log in\"],\"x6tjuK\":[\"Default tab\"],\"xEWkgZ\":[\"← Back to all dumps\"],\"xPHtx0\":[\"Submit search\"],\"xVuNgt\":[\"+ New playlist\"],\"xc9O_u\":[\"Delete dump\"],\"y6sq5j\":[\"Following\"],\"y7oaHj\":[\"Playlist title\"],\"yA_6BX\":[\"View all →\"],\"yBBtRm\":[\"Follow some users to see their dumps here.\"],\"yQ2kGp\":[\"Load more\"],\"y_0uwd\":[\"Yesterday\"],\"yz7wBu\":[\"Close\"],\"z0ROB3\":[\"Remove like\"],\"z1uNN0\":[\"No emoji found.\"],\"zVuxvN\":[\"Refreshing…\"],\"zwBp5t\":[\"Private\"]}")}; \ No newline at end of file diff --git a/src/locales/en.po b/src/locales/en.po index e39887a..cac6f0c 100644 --- a/src/locales/en.po +++ b/src/locales/en.po @@ -43,7 +43,7 @@ msgid "{visibleCount, plural, one {# comment} other {# comments}}" msgstr "{visibleCount, plural, one {# comment} other {# comments}}" #: src/pages/PlaylistDetail.tsx:560 -#: src/pages/UserPublicProfile.tsx:717 +#: src/pages/UserPublicProfile.tsx:719 msgid "← Back" msgstr "← Back" @@ -59,7 +59,7 @@ msgstr "← Back to all dumps" msgid "← Back to profile" msgstr "← Back to profile" -#: src/pages/UserPublicProfile.tsx:110 +#: src/pages/UserPublicProfile.tsx:111 msgid "+ Invite someone" msgstr "+ Invite someone" @@ -125,7 +125,7 @@ msgstr "a comment" msgid "a post" msgstr "a post" -#: src/pages/UserPublicProfile.tsx:1093 +#: src/pages/UserPublicProfile.tsx:1095 msgid "Account" msgstr "Account" @@ -133,7 +133,7 @@ msgstr "Account" msgid "Add a comment…" msgstr "Add a comment…" -#: src/pages/UserPublicProfile.tsx:792 +#: src/pages/UserPublicProfile.tsx:794 msgid "Add email…" msgstr "Add email…" @@ -146,7 +146,7 @@ msgstr "Add to playlist" msgid "Already have an account? <0>Log in" msgstr "Already have an account? <0>Log in" -#: src/pages/UserPublicProfile.tsx:1112 +#: src/pages/UserPublicProfile.tsx:1114 msgid "Appearance" msgstr "Appearance" @@ -157,7 +157,7 @@ msgstr "Appearance" msgid "At least {0} characters" msgstr "At least {0} characters" -#: src/pages/UserPublicProfile.tsx:1160 +#: src/pages/UserPublicProfile.tsx:1162 msgid "Auto" msgstr "Auto" @@ -177,8 +177,8 @@ msgstr "Can't connect to the live updates server. Upvotes and notifications may #: src/pages/Dump.tsx:357 #: src/pages/DumpEdit.tsx:433 #: src/pages/PlaylistDetail.tsx:908 -#: src/pages/UserPublicProfile.tsx:1496 -#: src/pages/UserPublicProfile.tsx:1566 +#: src/pages/UserPublicProfile.tsx:1540 +#: src/pages/UserPublicProfile.tsx:1610 msgid "Cancel" msgstr "Cancel" @@ -190,7 +190,7 @@ msgstr "Cancel removal" msgid "Cancel search" msgstr "Cancel search" -#: src/pages/UserPublicProfile.tsx:744 +#: src/pages/UserPublicProfile.tsx:746 msgid "Change avatar" msgstr "Change avatar" @@ -199,7 +199,7 @@ msgstr "Change avatar" msgid "Change password" msgstr "Change password" -#: src/pages/UserPublicProfile.tsx:1105 +#: src/pages/UserPublicProfile.tsx:1107 msgid "Change password…" msgstr "Change password…" @@ -212,7 +212,7 @@ msgstr "Checking invite…" msgid "Close" msgstr "Close" -#: src/pages/UserPublicProfile.tsx:1152 +#: src/pages/UserPublicProfile.tsx:1154 msgid "Color scheme" msgstr "Color scheme" @@ -221,11 +221,11 @@ msgstr "Color scheme" msgid "Confirm new password" msgstr "Confirm new password" -#: src/pages/UserPublicProfile.tsx:101 +#: src/pages/UserPublicProfile.tsx:102 msgid "Copied!" msgstr "Copied!" -#: src/pages/UserPublicProfile.tsx:101 +#: src/pages/UserPublicProfile.tsx:102 msgid "Copy" msgstr "Copy" @@ -263,10 +263,14 @@ msgstr "Creating…" msgid "Current password" msgstr "Current password" -#: src/pages/UserPublicProfile.tsx:1174 +#: src/pages/UserPublicProfile.tsx:1176 msgid "Dark" msgstr "Dark" +#: src/pages/UserPublicProfile.tsx:1189 +msgid "Default tab" +msgstr "Default tab" + #: src/components/CommentThread.tsx:374 #: src/components/CommentThread.tsx:380 #: src/components/ConfirmModal.tsx:16 @@ -329,13 +333,13 @@ msgstr "Dumped!" #: src/pages/Search.tsx:172 #: src/pages/UserDumps.tsx:107 -#: src/pages/UserPublicProfile.tsx:865 +#: src/pages/UserPublicProfile.tsx:867 msgid "Dumps" msgstr "Dumps" #. placeholder {0}: dumps.items.length #. placeholder {1}: dumps.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:882 +#: src/pages/UserPublicProfile.tsx:884 msgid "Dumps ({0}{1})" msgstr "Dumps ({0}{1})" @@ -387,9 +391,9 @@ msgstr "Enter a query to search." msgid "Failed to create playlist" msgstr "Failed to create playlist" -#: src/pages/UserPublicProfile.tsx:82 -#: src/pages/UserPublicProfile.tsx:85 -#: src/pages/UserPublicProfile.tsx:113 +#: src/pages/UserPublicProfile.tsx:83 +#: src/pages/UserPublicProfile.tsx:86 +#: src/pages/UserPublicProfile.tsx:114 msgid "Failed to generate invite" msgstr "Failed to generate invite" @@ -398,9 +402,9 @@ msgstr "Failed to generate invite" #: src/pages/index/JournalFeed.tsx:33 #: src/pages/index/NewFeed.tsx:36 #: src/pages/Notifications.tsx:367 -#: src/pages/UserPublicProfile.tsx:984 -#: src/pages/UserPublicProfile.tsx:1026 -#: src/pages/UserPublicProfile.tsx:1071 +#: src/pages/UserPublicProfile.tsx:986 +#: src/pages/UserPublicProfile.tsx:1028 +#: src/pages/UserPublicProfile.tsx:1073 msgid "Failed to load" msgstr "Failed to load" @@ -417,8 +421,8 @@ msgid "Failed to post reply" msgstr "Failed to post reply" #: src/pages/PlaylistDetail.tsx:896 -#: src/pages/UserPublicProfile.tsx:1499 -#: src/pages/UserPublicProfile.tsx:1568 +#: src/pages/UserPublicProfile.tsx:1543 +#: src/pages/UserPublicProfile.tsx:1612 msgid "Failed to save" msgstr "Failed to save" @@ -426,10 +430,14 @@ msgstr "Failed to save" msgid "Failed to save edit" msgstr "Failed to save edit" -#: src/pages/UserPublicProfile.tsx:801 +#: src/pages/UserPublicProfile.tsx:803 msgid "Failed to update avatar" msgstr "Failed to update avatar" +#: src/pages/UserPublicProfile.tsx:1184 +msgid "Feeds" +msgstr "Feeds" + #: src/components/DumpCreateModal.tsx:360 msgid "Fetching preview…" msgstr "Fetching preview…" @@ -468,8 +476,9 @@ msgstr "Follow some public playlists to see their dumps here." msgid "Follow some users to see their dumps here." msgstr "Follow some users to see their dumps here." -#: src/components/FeedTabBar.tsx:16 -#: src/pages/UserPublicProfile.tsx:867 +#: src/components/FeedTabBar.tsx:21 +#: src/pages/UserPublicProfile.tsx:869 +#: src/pages/UserPublicProfile.tsx:1218 msgid "Followed" msgstr "Followed" @@ -479,13 +488,13 @@ msgstr "Followed" msgid "Followed ({0}{1})" msgstr "Followed ({0}{1})" -#: src/pages/UserPublicProfile.tsx:1015 +#: src/pages/UserPublicProfile.tsx:1017 msgid "Followed playlists" msgstr "Followed playlists" #: src/components/FollowButton.tsx:37 #: src/components/FollowButton.tsx:64 -#: src/pages/UserPublicProfile.tsx:973 +#: src/pages/UserPublicProfile.tsx:975 msgid "Following" msgstr "Following" @@ -509,7 +518,8 @@ msgstr "Go home" msgid "Go to login" msgstr "Go to login" -#: src/components/FeedTabBar.tsx:12 +#: src/components/FeedTabBar.tsx:17 +#: src/pages/UserPublicProfile.tsx:1197 msgid "Hot" msgstr "Hot" @@ -525,20 +535,21 @@ msgstr "Invalid invite" msgid "Invalid link" msgstr "Invalid link" -#: src/pages/UserPublicProfile.tsx:763 +#: src/pages/UserPublicProfile.tsx:765 msgid "invited by" msgstr "invited by" -#: src/pages/UserPublicProfile.tsx:868 -#: src/pages/UserPublicProfile.tsx:1060 +#: src/pages/UserPublicProfile.tsx:870 +#: src/pages/UserPublicProfile.tsx:1062 msgid "Invitees" msgstr "Invitees" -#: src/components/FeedTabBar.tsx:14 +#: src/components/FeedTabBar.tsx:19 +#: src/pages/UserPublicProfile.tsx:1211 msgid "Journal" msgstr "Journal" -#: src/pages/UserPublicProfile.tsx:1167 +#: src/pages/UserPublicProfile.tsx:1169 msgid "Light" msgstr "Light" @@ -580,7 +591,7 @@ msgstr "Loading more…" msgid "Loading playlist…" msgstr "Loading playlist…" -#: src/pages/UserPublicProfile.tsx:700 +#: src/pages/UserPublicProfile.tsx:702 msgid "Loading profile…" msgstr "Loading profile…" @@ -596,9 +607,9 @@ msgstr "Loading profile…" #: src/pages/Notifications.tsx:439 #: src/pages/UserDumps.tsx:51 #: src/pages/UserPlaylists.tsx:342 -#: src/pages/UserPublicProfile.tsx:978 -#: src/pages/UserPublicProfile.tsx:1020 -#: src/pages/UserPublicProfile.tsx:1065 +#: src/pages/UserPublicProfile.tsx:980 +#: src/pages/UserPublicProfile.tsx:1022 +#: src/pages/UserPublicProfile.tsx:1067 #: src/pages/UserUpvoted.tsx:122 msgid "Loading…" msgstr "Loading…" @@ -617,8 +628,8 @@ msgstr "Log in to like" msgid "Log in to vote" msgstr "Log in to vote" -#: src/pages/UserPublicProfile.tsx:721 -#: src/pages/UserPublicProfile.tsx:815 +#: src/pages/UserPublicProfile.tsx:723 +#: src/pages/UserPublicProfile.tsx:817 msgid "Log out" msgstr "Log out" @@ -638,13 +649,16 @@ msgstr "Max 50 MB" msgid "new" msgstr "new" -#: src/components/FeedTabBar.tsx:13 +#: src/components/FeedTabBar.tsx:18 +#: src/pages/UserPublicProfile.tsx:1204 msgid "New" msgstr "New" #: src/components/DumpCreateModal.tsx:292 +#: src/components/DumpFab.tsx:65 +#: src/components/DumpFab.tsx:66 #: src/pages/UserDumps.tsx:115 -#: src/pages/UserPublicProfile.tsx:1223 +#: src/pages/UserPublicProfile.tsx:1267 msgid "New dump" msgstr "New dump" @@ -677,11 +691,11 @@ msgid "No emoji found." msgstr "No emoji found." #: src/pages/UserPlaylists.tsx:439 -#: src/pages/UserPublicProfile.tsx:1033 +#: src/pages/UserPublicProfile.tsx:1035 msgid "No followed playlists yet." msgstr "No followed playlists yet." -#: src/pages/UserPublicProfile.tsx:1078 +#: src/pages/UserPublicProfile.tsx:1080 msgid "No invitees yet." msgstr "No invitees yet." @@ -695,7 +709,7 @@ msgstr "No playlists match \"{q}\"." #: src/components/PlaylistMembershipPanel.tsx:34 #: src/pages/UserPlaylists.tsx:397 -#: src/pages/UserPublicProfile.tsx:944 +#: src/pages/UserPublicProfile.tsx:946 msgid "No playlists yet." msgstr "No playlists yet." @@ -703,14 +717,14 @@ msgstr "No playlists yet." msgid "No users match \"{q}\"." msgstr "No users match \"{q}\"." -#: src/pages/UserPublicProfile.tsx:991 +#: src/pages/UserPublicProfile.tsx:993 msgid "Not following anyone yet." msgstr "Not following anyone yet." #: src/pages/Notifications.tsx:374 #: src/pages/UserDumps.tsx:125 -#: src/pages/UserPublicProfile.tsx:1234 -#: src/pages/UserPublicProfile.tsx:1356 +#: src/pages/UserPublicProfile.tsx:1278 +#: src/pages/UserPublicProfile.tsx:1400 #: src/pages/UserUpvoted.tsx:194 msgid "Nothing here yet." msgstr "Nothing here yet." @@ -733,7 +747,7 @@ msgid "or <0>browse files" msgstr "or <0>browse files" #: src/pages/UserLogin.tsx:72 -#: src/pages/UserPublicProfile.tsx:1098 +#: src/pages/UserPublicProfile.tsx:1100 msgid "Password" msgstr "Password" @@ -763,13 +777,13 @@ msgstr "Playlist title" #: src/components/UserMenu.tsx:62 #: src/pages/Search.tsx:175 #: src/pages/UserPlaylists.tsx:368 -#: src/pages/UserPublicProfile.tsx:866 +#: src/pages/UserPublicProfile.tsx:868 msgid "Playlists" msgstr "Playlists" #. placeholder {0}: playlists.items.length #. placeholder {1}: playlists.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:913 +#: src/pages/UserPublicProfile.tsx:915 msgid "Playlists ({0}{1})" msgstr "Playlists ({0}{1})" @@ -885,8 +899,8 @@ msgstr "Retry" #: src/pages/Dump.tsx:349 #: src/pages/DumpEdit.tsx:436 #: src/pages/PlaylistDetail.tsx:915 -#: src/pages/UserPublicProfile.tsx:1488 -#: src/pages/UserPublicProfile.tsx:1558 +#: src/pages/UserPublicProfile.tsx:1532 +#: src/pages/UserPublicProfile.tsx:1602 msgid "Save" msgstr "Save" @@ -895,8 +909,8 @@ msgstr "Save" #: src/pages/Dump.tsx:348 #: src/pages/PlaylistDetail.tsx:911 #: src/pages/ResetPassword.tsx:124 -#: src/pages/UserPublicProfile.tsx:1485 -#: src/pages/UserPublicProfile.tsx:1555 +#: src/pages/UserPublicProfile.tsx:1529 +#: src/pages/UserPublicProfile.tsx:1599 msgid "Saving…" msgstr "Saving…" @@ -934,7 +948,7 @@ msgstr "Server unreachable" msgid "Set new password" msgstr "Set new password" -#: src/pages/UserPublicProfile.tsx:870 +#: src/pages/UserPublicProfile.tsx:872 msgid "Settings" msgstr "Settings" @@ -943,7 +957,7 @@ msgstr "Settings" msgid "Something went wrong" msgstr "Something went wrong" -#: src/pages/UserPublicProfile.tsx:1117 +#: src/pages/UserPublicProfile.tsx:1119 msgid "Style" msgstr "Style" @@ -997,7 +1011,7 @@ msgstr "Unfollow {targetUsername}" msgid "Unfollow playlist" msgstr "Unfollow playlist" -#: src/pages/UserPublicProfile.tsx:671 +#: src/pages/UserPublicProfile.tsx:673 msgid "Upload failed" msgstr "Upload failed" @@ -1015,7 +1029,7 @@ msgstr "Upvoted" #. placeholder {0}: votes.items.length #. placeholder {1}: votes.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:893 +#: src/pages/UserPublicProfile.tsx:895 msgid "Upvoted ({0}{1})" msgstr "Upvoted ({0}{1})" @@ -1041,11 +1055,11 @@ msgstr "Username" msgid "Users" msgstr "Users" -#: src/pages/UserPublicProfile.tsx:963 -#: src/pages/UserPublicProfile.tsx:1006 -#: src/pages/UserPublicProfile.tsx:1048 -#: src/pages/UserPublicProfile.tsx:1255 -#: src/pages/UserPublicProfile.tsx:1386 +#: src/pages/UserPublicProfile.tsx:965 +#: src/pages/UserPublicProfile.tsx:1008 +#: src/pages/UserPublicProfile.tsx:1050 +#: src/pages/UserPublicProfile.tsx:1299 +#: src/pages/UserPublicProfile.tsx:1430 msgid "View all →" msgstr "View all →" @@ -1058,8 +1072,8 @@ msgstr "View dump →" msgid "What makes it worth it?" msgstr "What makes it worth it?" -#: src/pages/UserPublicProfile.tsx:850 -#: src/pages/UserPublicProfile.tsx:1547 +#: src/pages/UserPublicProfile.tsx:852 +#: src/pages/UserPublicProfile.tsx:1591 msgid "Who am I?" msgstr "Who am I?" diff --git a/src/locales/fr.js b/src/locales/fr.js index 7668382..51e89e1 100644 --- a/src/locales/fr.js +++ b/src/locales/fr.js @@ -1 +1 @@ -/*eslint-disable*/module.exports={messages:JSON.parse("{\"-K9EZb\":[\"Ajouter un e-mail…\"],\"-OxI15\":[\"Collections suivies\"],\"-Ya-b9\":[\"Enregistrement échoué\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Rechercher des recos, utilisateurs, collections…\"],\"1cbYY_\":[\"Votés (\",[\"0\"],[\"1\"],\")\"],\"1njn7W\":[\"Clair\"],\"1utXA6\":[\"Recos\"],\"26iNma\":[\"Publier le commentaire\"],\"2Hlmdt\":[\"Écrire une réponse…\"],\"2ygf_L\":[\"← Retour\"],\"3KKSM4\":[\"privé\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" a suivi votre collection <1>\",[\"1\"],\"\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Recommandé !\"],\"4GKuCs\":[\"Connexion échouée\"],\"4HH9iB\":[\"Aucun abonnement pour le moment.\"],\"4RtQ1k\":[\"Ne plus suivre \",[\"targetUsername\"]],\"4c-qBx\":[\"Votre mot de passe a été modifié. Vous pouvez maintenant vous connecter.\"],\"4yj9xV\":[\"Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…\"],\"5TviPn\":[\"Annuler la recherche\"],\"5cC8f2\":[\"Modifié le \",[\"0\"]],\"5oD9f_\":[\"Plus tôt\"],\"6Qly-0\":[\"un commentaire\"],\"6gRgw8\":[\"Réessayer\"],\"7PHCIN\":[\"Annuler la suppression\"],\"7d1a0d\":[\"Public\"],\"7sNhEz\":[\"Nom d'utilisateur\"],\"8Ug9jB\":[\"Connexe\"],\"8ZsakT\":[\"Mot de passe\"],\"8pxhI8\":[\"Veuillez sélectionner un fichier.\"],\"9BruTc\":[\"Pourquoi ?\"],\"9l4qcT\":[\"Déposez un fichier de remplacement ici\"],\"9uI_rE\":[\"Annuler\"],\"9xDZu_\":[\"Impossible de charger.\"],\"A0y396\":[\"+ Inviter quelqu'un\"],\"A1taO8\":[\"Rechercher\"],\"AQbgNR\":[\"Suivre \",[\"targetUsername\"]],\"ATGYL1\":[\"Adresse e-mail\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"Ade-6d\":[\"Mises à jour en direct indisponibles.\"],\"AeXO77\":[\"Compte\"],\"CI50ct\":[\"Voté\"],\"Cj24wt\":[\"Inscription…\"],\"DPfwMq\":[\"Terminé\"],\"DdeHXH\":[\"Supprimer cette reco ? Cette action est irréversible.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" a voté pour <1>\",[\"1\"],\"\"],\"ECiS12\":[\"Voir la reco →\"],\"ExR0Fr\":[\"L'URL est obligatoire.\"],\"F5Js1v\":[\"Ne plus suivre la collection\"],\"FgAxTj\":[\"Se déconnecter\"],\"Fxf4jq\":[\"Description (facultatif)\"],\"GNSsCc\":[\"Impossible de créer la collection\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" caractères : lettres, chiffres ou tirets bas\"],\"GptGxg\":[\"Changer le mot de passe\"],\"H4o4sk\":[\"Personne pour le moment.\"],\"H8pzW-\":[\"Impossible de mettre à jour l'avatar\"],\"HTLDA4\":[\"Chargement…\"],\"I-x669\":[\"Invités\"],\"IZX7TO\":[\"Impossible de générer une invitation\"],\"IagCbF\":[\"URL\"],\"ImOQa9\":[\"Répondre\"],\"J2eKUI\":[\"Fichier\"],\"JJ-Bhk\":[\"Votre adresse e-mail\"],\"JRQitQ\":[\"Confirmer le nouveau mot de passe\"],\"JXr41k\":[\"<0>\",[\"0\"],\" a commenté sur <1>\",[\"1\"],\"\"],\"Jd58Fo\":[\"Tendances\"],\"Jf0PuK\":[\"Aller à la connexion\"],\"KDGWg5\":[\"Retirer de la collection\"],\"K_F6pa\":[\"Enregistrement…\"],\"L-rMC9\":[\"Réinitialiser par défaut\"],\"LLyMkV\":[\"Suivies (\",[\"0\"],[\"1\"],\")\"],\"MHrjPM\":[\"Titre\"],\"MKEPCY\":[\"Suivre\"],\"Oprv1v\":[\"Mot de passe (min. \",[\"0\"],\" caractères)\"],\"Oz0N9s\":[\"nouveau\"],\"PiH3UR\":[\"Copié !\"],\"Pn2B7_\":[\"Mot de passe actuel\"],\"Pwqkdw\":[\"Chargement…\"],\"Q6n4F4\":[\"Actualiser les métadonnées\"],\"QKsaQr\":[\"ou <0>parcourir les fichiers\"],\"QLtPBd\":[\"Aucune reco dans cette collection pour l'instant.\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Supprimer cette collection ? Cette action est irréversible.\"],\"RTksSy\":[\"<0>\",[\"0\"],\" a commencé à vous suivre\"],\"RaKjrM\":[\"Impossible d'enregistrer la modification\"],\"RcUHRT\":[\"Suivi\"],\"Rrp6-J\":[\"Se connecter pour aimer\"],\"SBTElJ\":[\"Recherche…\"],\"Sad2tK\":[\"Envoi…\"],\"StovX6\":[\"Rien à voir, circulez.\"],\"Sxm8rQ\":[\"Utilisateurs\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" a été ajouté à <1>\",[\"1\"],\"\"],\"TM1ZbA\":[\"Collections (\",[\"0\"],[\"1\"],\")\"],\"TN382O\":[\"Lien invalide\"],\"Tv9vbB\":[\"Suivre la collection\"],\"Tz0i8g\":[\"Paramètres\"],\"UNMVei\":[\"Mot de passe oublié ?\"],\"UOZith\":[\"Publication échouée\"],\"URAieT\":[\"Se connecter pour voter\"],\"UTiUFs\":[\"Récupération…\"],\"VCoEm-\":[\"Retour à la connexion\"],\"V_e7nf\":[\"Définir un nouveau mot de passe\"],\"VnNJbN\":[\"De collections\"],\"VyTYmS\":[\"Changer l'avatar\"],\"W9FRBT\":[\"Aimer\"],\"WhimMi\":[\"Échec de la réinitialisation\"],\"WpXcBJ\":[\"Rien ici pour l'instant.\"],\"XJy2oN\":[\"Connexion…\"],\"Xan6QP\":[\"Nouvelle reco\"],\"XgRtUf\":[\"Impossible de changer le mot de passe\"],\"Xi0Mn4\":[\"← Retour au profil\"],\"XnL-Eu\":[\"Aucun utilisateur ne correspond à « \",[\"q\"],\" ».\"],\"Xs2Lez\":[\"Ce lien de réinitialisation est absent ou malformé.\"],\"YK1Dhc\":[\"une publication\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" a aimé votre commentaire sur <1>\",[\"1\"],\"\"],\"YpkCca\":[\"Pas encore de collections suivies.\"],\"ZBdbv9\":[\"Modifier le titre\"],\"ZCpU0u\":[\"Aucune collection ne correspond à « \",[\"q\"],\" ».\"],\"ZmD2o6\":[\"Créer et ajouter\"],\"_3O5R_\":[\"Échec de la demande\"],\"_DwR-n\":[\"Création…\"],\"_R_sGB\":[\"Mot de passe modifié avec succès.\"],\"_aept4\":[\"Publier la réponse\"],\"_nT6AE\":[\"Nouveau mot de passe\"],\"_t4W-i\":[\"De personnes\"],\"aAIQg2\":[\"Apparence\"],\"aDvLhk\":[\"Ajouter un commentaire…\"],\"b3Thhd\":[\"Envoi échoué\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"bQhwn-\":[\"Chargement de la collection…\"],\"cILfnJ\":[\"Supprimer le fichier\"],\"cYP9Sb\":[\"+ Collection\"],\"cbeBbZ\":[\"Au moins \",[\"0\"],\" caractères\"],\"cnGeoo\":[\"Supprimer\"],\"d8DZWS\":[\"Ouvrir la recherche\"],\"dAs22m\":[\"Remplacer le fichier\"],\"dEgA5A\":[\"Annuler\"],\"dMizp8\":[\"Nouvelle collection\"],\"eFSqvc\":[\"Impossible de publier la réponse\"],\"eOfXq3\":[\"Un titre est requis.\"],\"ePK91l\":[\"Modifier\"],\"eaUTwS\":[\"Envoyer le lien de réinitialisation\"],\"ecUA8p\":[\"Aujourd'hui\"],\"ef9nPf\":[\"Chargement de la reco…\"],\"en9o7K\":[\"Impossible de publier le commentaire\"],\"etFQQS\":[\"Pourquoi on en voudrait ?\"],\"fC6mXb\":[\"Voter\"],\"fI-mNw\":[\"Collections\"],\"f_akpP\":[\"Max 50 Mo\"],\"fgLNSM\":[\"S'inscrire\"],\"gANddk\":[\"Envoi…\"],\"gGx5tM\":[\"Modification\"],\"gIQQwD\":[\"Chargement échoué\"],\"gLfZlz\":[\"Ajouter à la collection\"],\"gjJ-sb\":[\"Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion.\"],\"hBuUKa\":[\"Changer le mot de passe…\"],\"hD7w09\":[\"Vous avez tout lu, tout vu, tout bu.\"],\"hJSliC\":[\"<0>\",[\"0\"],\" a publié <1>\",[\"1\"],\"\"],\"hYgDIe\":[\"Créer\"],\"he3ygx\":[\"Copier\"],\"i7K_Te\":[\"Qui suis-je ?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Accueil\"],\"ipYn7W\":[\"Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu.\"],\"isRobC\":[\"Nouveau\"],\"jbernk\":[\"Chargement du profil…\"],\"joEmfT\":[\"Serveur inaccessible\"],\"jrZTZl\":[\"Pas encore de recos. Soyez le premier !\"],\"kLttbL\":[\"Inscription échouée\"],\"lUDifl\":[\"Créées (\",[\"0\"],[\"1\"],\")\"],\"lUanmi\":[\"Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}]],\"lcfvr_\":[\"Supprimer ce commentaire ?\"],\"lpIMne\":[\"Les mots de passe ne correspondent pas\"],\"mt6O6E\":[\"C'est un mirage.\"],\"nbm5sI\":[\"Aucune reco ne correspond à « \",[\"q\"],\" ».\"],\"nrjqON\":[\"Vérification de l'invitation…\"],\"nwtY4N\":[\"Une erreur est survenue\"],\"nx4kaN\":[\"Sauvegarde impossible\"],\"ogtYkT\":[\"Mot de passe mis à jour\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" vous a mentionné dans <1>\",[\"where\"],\"\"],\"pSheLH\":[\"Aucun invité pour le moment.\"],\"pvnfJD\":[\"Sombre\"],\"qIMfNQ\":[\"Supprimer la collection\"],\"qbDAcy\":[\"Recommander\"],\"qgx_78\":[\"Suivez des collections publiques pour voir leurs recos ici.\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Reco\"],\"rCbqPX\":[\"Ce lien d'invitation est manquant, expiré ou déjà utilisé.\"],\"rg9pXu\":[\"Recherche échouée\"],\"rtpJqV\":[\"Recos (\",[\"0\"],[\"1\"],\")\"],\"sGeXL3\":[\"Miniature\"],\"sQia9P\":[\"Se connecter\"],\"sTiqbm\":[\"invité par\"],\"sdP5Aa\":[\"[supprimé]\"],\"shHs8T\":[\"Saisissez une recherche.\"],\"smeBfS\":[\"Invitation invalide\"],\"tfDRzk\":[\"Enregistrer\"],\"tvmuQ0\":[\"Thème de couleur\"],\"u1lDX2\":[\"Récupération de l'aperçu…\"],\"uD0qXQ\":[\"Déposez un fichier ici\"],\"uMGUnV\":[\"Pas encore de collections.\"],\"ub1EEL\":[\"modifié \",[\"0\"]],\"vJBF1r\":[\"Publication…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" non lues)\"],\"vQMkHu\":[\"Retirer le vote\"],\"vuosjb\":[\"Menu utilisateur\"],\"wbXKOv\":[\"Fichier trop volumineux (max 50 Mo).\"],\"wixIgH\":[\"Vous avez déjà un compte ? <0>Se connecter\"],\"xEWkgZ\":[\"← Retour à toutes les recos\"],\"xPHtx0\":[\"Lancer la recherche\"],\"xVuNgt\":[\"+ Nouvelle collection\"],\"xc9O_u\":[\"Supprimer la reco\"],\"y6sq5j\":[\"Abonné\"],\"y7oaHj\":[\"Titre de la collection\"],\"yA_6BX\":[\"Tout voir →\"],\"yBBtRm\":[\"Suivez des utilisateurs pour voir leurs recos ici.\"],\"yQ2kGp\":[\"Charger plus\"],\"y_0uwd\":[\"Hier\"],\"yz7wBu\":[\"Fermer\"],\"z0ROB3\":[\"Retirer le j'aime\"],\"z1uNN0\":[\"Aucun emoji trouvé.\"],\"zVuxvN\":[\"Actualisation…\"],\"zwBp5t\":[\"Privé\"]}")}; \ No newline at end of file +/*eslint-disable*/module.exports={messages:JSON.parse("{\"-K9EZb\":[\"Ajouter un e-mail…\"],\"-OxI15\":[\"Collections suivies\"],\"-Ya-b9\":[\"Enregistrement échoué\"],\"-siMqD\":[\"Journal\"],\"1CalO6\":[\"Style\"],\"1HfJWf\":[\"Rechercher des recos, utilisateurs, collections…\"],\"1cbYY_\":[\"Votés (\",[\"0\"],[\"1\"],\")\"],\"1njn7W\":[\"Clair\"],\"1utXA6\":[\"Recos\"],\"26iNma\":[\"Publier le commentaire\"],\"2DoBvq\":[\"Flux\"],\"2Hlmdt\":[\"Écrire une réponse…\"],\"2ygf_L\":[\"← Retour\"],\"3KKSM4\":[\"privé\"],\"3yfh3D\":[\"<0>\",[\"0\"],\" a suivi votre collection <1>\",[\"1\"],\"\"],\"49voTZ\":[[\"label\"],\" (\",[\"count\"],\")\"],\"4B6w_o\":[\"Recommandé !\"],\"4GKuCs\":[\"Connexion échouée\"],\"4HH9iB\":[\"Aucun abonnement pour le moment.\"],\"4RtQ1k\":[\"Ne plus suivre \",[\"targetUsername\"]],\"4c-qBx\":[\"Votre mot de passe a été modifié. Vous pouvez maintenant vous connecter.\"],\"4yj9xV\":[\"Les mises à jour en direct sont temporairement interrompues. Tentative de reconnexion…\"],\"5TviPn\":[\"Annuler la recherche\"],\"5cC8f2\":[\"Modifié le \",[\"0\"]],\"5oD9f_\":[\"Plus tôt\"],\"6Qly-0\":[\"un commentaire\"],\"6gRgw8\":[\"Réessayer\"],\"7PHCIN\":[\"Annuler la suppression\"],\"7d1a0d\":[\"Public\"],\"7sNhEz\":[\"Nom d'utilisateur\"],\"8Ug9jB\":[\"Connexe\"],\"8ZsakT\":[\"Mot de passe\"],\"8pxhI8\":[\"Veuillez sélectionner un fichier.\"],\"9BruTc\":[\"Pourquoi ?\"],\"9l4qcT\":[\"Déposez un fichier de remplacement ici\"],\"9uI_rE\":[\"Annuler\"],\"9xDZu_\":[\"Impossible de charger.\"],\"A0y396\":[\"+ Inviter quelqu'un\"],\"A1taO8\":[\"Rechercher\"],\"AQbgNR\":[\"Suivre \",[\"targetUsername\"]],\"ATGYL1\":[\"Adresse e-mail\"],\"AZctoV\":[[\"0\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"Ade-6d\":[\"Mises à jour en direct indisponibles.\"],\"AeXO77\":[\"Compte\"],\"CI50ct\":[\"Voté\"],\"Cj24wt\":[\"Inscription…\"],\"DPfwMq\":[\"Terminé\"],\"DdeHXH\":[\"Supprimer cette reco ? Cette action est irréversible.\"],\"Dp1JhP\":[\"<0>\",[\"0\"],\" a voté pour <1>\",[\"1\"],\"\"],\"ECiS12\":[\"Voir la reco →\"],\"ExR0Fr\":[\"L'URL est obligatoire.\"],\"F5Js1v\":[\"Ne plus suivre la collection\"],\"FgAxTj\":[\"Se déconnecter\"],\"Fxf4jq\":[\"Description (facultatif)\"],\"GNSsCc\":[\"Impossible de créer la collection\"],\"GbqhrN\":[[\"0\"],\"–\",[\"1\"],\" caractères : lettres, chiffres ou tirets bas\"],\"GptGxg\":[\"Changer le mot de passe\"],\"H4o4sk\":[\"Personne pour le moment.\"],\"H8pzW-\":[\"Impossible de mettre à jour l'avatar\"],\"HTLDA4\":[\"Chargement…\"],\"I-x669\":[\"Invités\"],\"IZX7TO\":[\"Impossible de générer une invitation\"],\"IagCbF\":[\"URL\"],\"ImOQa9\":[\"Répondre\"],\"J2eKUI\":[\"Fichier\"],\"JJ-Bhk\":[\"Votre adresse e-mail\"],\"JRQitQ\":[\"Confirmer le nouveau mot de passe\"],\"JXr41k\":[\"<0>\",[\"0\"],\" a commenté sur <1>\",[\"1\"],\"\"],\"Jd58Fo\":[\"Tendances\"],\"Jf0PuK\":[\"Aller à la connexion\"],\"KDGWg5\":[\"Retirer de la collection\"],\"K_F6pa\":[\"Enregistrement…\"],\"L-rMC9\":[\"Réinitialiser par défaut\"],\"LLyMkV\":[\"Suivies (\",[\"0\"],[\"1\"],\")\"],\"MHrjPM\":[\"Titre\"],\"MKEPCY\":[\"Suivre\"],\"Oprv1v\":[\"Mot de passe (min. \",[\"0\"],\" caractères)\"],\"Oz0N9s\":[\"nouveau\"],\"PiH3UR\":[\"Copié !\"],\"Pn2B7_\":[\"Mot de passe actuel\"],\"Pwqkdw\":[\"Chargement…\"],\"Q6n4F4\":[\"Actualiser les métadonnées\"],\"QKsaQr\":[\"ou <0>parcourir les fichiers\"],\"QLtPBd\":[\"Aucune reco dans cette collection pour l'instant.\"],\"R9Khdg\":[\"Auto\"],\"RCcPrX\":[\"Supprimer cette collection ? Cette action est irréversible.\"],\"RTksSy\":[\"<0>\",[\"0\"],\" a commencé à vous suivre\"],\"RaKjrM\":[\"Impossible d'enregistrer la modification\"],\"RcUHRT\":[\"Suivi\"],\"Rrp6-J\":[\"Se connecter pour aimer\"],\"SBTElJ\":[\"Recherche…\"],\"Sad2tK\":[\"Envoi…\"],\"StovX6\":[\"Rien à voir, circulez.\"],\"Sxm8rQ\":[\"Utilisateurs\"],\"T9bjWt\":[\"<0>\",[\"0\"],\" a été ajouté à <1>\",[\"1\"],\"\"],\"TM1ZbA\":[\"Collections (\",[\"0\"],[\"1\"],\")\"],\"TN382O\":[\"Lien invalide\"],\"Tv9vbB\":[\"Suivre la collection\"],\"Tz0i8g\":[\"Paramètres\"],\"UNMVei\":[\"Mot de passe oublié ?\"],\"UOZith\":[\"Publication échouée\"],\"URAieT\":[\"Se connecter pour voter\"],\"UTiUFs\":[\"Récupération…\"],\"VCoEm-\":[\"Retour à la connexion\"],\"V_e7nf\":[\"Définir un nouveau mot de passe\"],\"VnNJbN\":[\"De collections\"],\"VyTYmS\":[\"Changer l'avatar\"],\"W9FRBT\":[\"Aimer\"],\"WhimMi\":[\"Échec de la réinitialisation\"],\"WpXcBJ\":[\"Rien ici pour l'instant.\"],\"XJy2oN\":[\"Connexion…\"],\"Xan6QP\":[\"Nouvelle reco\"],\"XgRtUf\":[\"Impossible de changer le mot de passe\"],\"Xi0Mn4\":[\"← Retour au profil\"],\"XnL-Eu\":[\"Aucun utilisateur ne correspond à « \",[\"q\"],\" ».\"],\"Xs2Lez\":[\"Ce lien de réinitialisation est absent ou malformé.\"],\"YK1Dhc\":[\"une publication\"],\"Ye9RMF\":[\"<0>\",[\"0\"],\" a aimé votre commentaire sur <1>\",[\"1\"],\"\"],\"YpkCca\":[\"Pas encore de collections suivies.\"],\"ZBdbv9\":[\"Modifier le titre\"],\"ZCpU0u\":[\"Aucune collection ne correspond à « \",[\"q\"],\" ».\"],\"ZmD2o6\":[\"Créer et ajouter\"],\"_3O5R_\":[\"Échec de la demande\"],\"_DwR-n\":[\"Création…\"],\"_R_sGB\":[\"Mot de passe modifié avec succès.\"],\"_aept4\":[\"Publier la réponse\"],\"_nT6AE\":[\"Nouveau mot de passe\"],\"_t4W-i\":[\"De personnes\"],\"aAIQg2\":[\"Apparence\"],\"aDvLhk\":[\"Ajouter un commentaire…\"],\"b3Thhd\":[\"Envoi échoué\"],\"b8XMJ8\":[[\"visibleCount\",\"plural\",{\"one\":[\"#\",\" commentaire\"],\"other\":[\"#\",\" commentaires\"]}]],\"bQhwn-\":[\"Chargement de la collection…\"],\"cILfnJ\":[\"Supprimer le fichier\"],\"cYP9Sb\":[\"+ Collection\"],\"cbeBbZ\":[\"Au moins \",[\"0\"],\" caractères\"],\"cnGeoo\":[\"Supprimer\"],\"d8DZWS\":[\"Ouvrir la recherche\"],\"dAs22m\":[\"Remplacer le fichier\"],\"dEgA5A\":[\"Annuler\"],\"dMizp8\":[\"Nouvelle collection\"],\"eFSqvc\":[\"Impossible de publier la réponse\"],\"eOfXq3\":[\"Un titre est requis.\"],\"ePK91l\":[\"Modifier\"],\"eaUTwS\":[\"Envoyer le lien de réinitialisation\"],\"ecUA8p\":[\"Aujourd'hui\"],\"ef9nPf\":[\"Chargement de la reco…\"],\"en9o7K\":[\"Impossible de publier le commentaire\"],\"etFQQS\":[\"Pourquoi on en voudrait ?\"],\"fC6mXb\":[\"Voter\"],\"fI-mNw\":[\"Collections\"],\"f_akpP\":[\"Max 50 Mo\"],\"fgLNSM\":[\"S'inscrire\"],\"gANddk\":[\"Envoi…\"],\"gGx5tM\":[\"Modification\"],\"gIQQwD\":[\"Chargement échoué\"],\"gLfZlz\":[\"Ajouter à la collection\"],\"gjJ-sb\":[\"Impossible de se connecter au serveur de mises à jour en direct. Les votes et les notifications pourraient ne pas se synchroniser avant la reconnexion.\"],\"hBuUKa\":[\"Changer le mot de passe…\"],\"hD7w09\":[\"Vous avez tout lu, tout vu, tout bu.\"],\"hJSliC\":[\"<0>\",[\"0\"],\" a publié <1>\",[\"1\"],\"\"],\"hYgDIe\":[\"Créer\"],\"he3ygx\":[\"Copier\"],\"i7K_Te\":[\"Qui suis-je ?\"],\"iDNBZe\":[\"Notifications\"],\"iWpEwy\":[\"Accueil\"],\"ipYn7W\":[\"Si cette adresse est enregistrée, vous recevrez un lien de réinitialisation sous peu.\"],\"isRobC\":[\"Nouveau\"],\"jbernk\":[\"Chargement du profil…\"],\"joEmfT\":[\"Serveur inaccessible\"],\"jrZTZl\":[\"Pas encore de recos. Soyez le premier !\"],\"kLttbL\":[\"Inscription échouée\"],\"lUDifl\":[\"Créées (\",[\"0\"],[\"1\"],\")\"],\"lUanmi\":[\"Vous serez notifié lorsque quelqu'un suit vos collections, vote pour vos recos ou publie du nouveau contenu.\"],\"lY5h1V\":[[\"0\",\"plural\",{\"one\":[\"#\",\" reco\"],\"other\":[\"#\",\" recos\"]}]],\"lcfvr_\":[\"Supprimer ce commentaire ?\"],\"lpIMne\":[\"Les mots de passe ne correspondent pas\"],\"mt6O6E\":[\"C'est un mirage.\"],\"nbm5sI\":[\"Aucune reco ne correspond à « \",[\"q\"],\" ».\"],\"nrjqON\":[\"Vérification de l'invitation…\"],\"nwtY4N\":[\"Une erreur est survenue\"],\"nx4kaN\":[\"Sauvegarde impossible\"],\"ogtYkT\":[\"Mot de passe mis à jour\"],\"pCpd9p\":[\"<0>\",[\"0\"],\" vous a mentionné dans <1>\",[\"where\"],\"\"],\"pSheLH\":[\"Aucun invité pour le moment.\"],\"pvnfJD\":[\"Sombre\"],\"qIMfNQ\":[\"Supprimer la collection\"],\"qbDAcy\":[\"Recommander\"],\"qgx_78\":[\"Suivez des collections publiques pour voir leurs recos ici.\"],\"qvFa8r\":[\"public\"],\"qvz_Pp\":[\"Reco\"],\"rCbqPX\":[\"Ce lien d'invitation est manquant, expiré ou déjà utilisé.\"],\"rg9pXu\":[\"Recherche échouée\"],\"rtpJqV\":[\"Recos (\",[\"0\"],[\"1\"],\")\"],\"sGeXL3\":[\"Miniature\"],\"sQia9P\":[\"Se connecter\"],\"sTiqbm\":[\"invité par\"],\"sdP5Aa\":[\"[supprimé]\"],\"shHs8T\":[\"Saisissez une recherche.\"],\"smeBfS\":[\"Invitation invalide\"],\"tfDRzk\":[\"Enregistrer\"],\"tvmuQ0\":[\"Thème de couleur\"],\"u1lDX2\":[\"Récupération de l'aperçu…\"],\"uD0qXQ\":[\"Déposez un fichier ici\"],\"uMGUnV\":[\"Pas encore de collections.\"],\"ub1EEL\":[\"modifié \",[\"0\"]],\"vJBF1r\":[\"Publication…\"],\"vLhLLO\":[\"Notifications (\",[\"unreadNotificationCount\"],\" non lues)\"],\"vQMkHu\":[\"Retirer le vote\"],\"vuosjb\":[\"Menu utilisateur\"],\"wbXKOv\":[\"Fichier trop volumineux (max 50 Mo).\"],\"wixIgH\":[\"Vous avez déjà un compte ? <0>Se connecter\"],\"x6tjuK\":[\"Onglet par défaut\"],\"xEWkgZ\":[\"← Retour à toutes les recos\"],\"xPHtx0\":[\"Lancer la recherche\"],\"xVuNgt\":[\"+ Nouvelle collection\"],\"xc9O_u\":[\"Supprimer la reco\"],\"y6sq5j\":[\"Abonné\"],\"y7oaHj\":[\"Titre de la collection\"],\"yA_6BX\":[\"Tout voir →\"],\"yBBtRm\":[\"Suivez des utilisateurs pour voir leurs recos ici.\"],\"yQ2kGp\":[\"Charger plus\"],\"y_0uwd\":[\"Hier\"],\"yz7wBu\":[\"Fermer\"],\"z0ROB3\":[\"Retirer le j'aime\"],\"z1uNN0\":[\"Aucun emoji trouvé.\"],\"zVuxvN\":[\"Actualisation…\"],\"zwBp5t\":[\"Privé\"]}")}; \ No newline at end of file diff --git a/src/locales/fr.po b/src/locales/fr.po index db1a1e1..1bd61b0 100644 --- a/src/locales/fr.po +++ b/src/locales/fr.po @@ -43,7 +43,7 @@ msgid "{visibleCount, plural, one {# comment} other {# comments}}" msgstr "{visibleCount, plural, one {# commentaire} other {# commentaires}}" #: src/pages/PlaylistDetail.tsx:560 -#: src/pages/UserPublicProfile.tsx:717 +#: src/pages/UserPublicProfile.tsx:719 msgid "← Back" msgstr "← Retour" @@ -59,7 +59,7 @@ msgstr "← Retour à toutes les recos" msgid "← Back to profile" msgstr "← Retour au profil" -#: src/pages/UserPublicProfile.tsx:110 +#: src/pages/UserPublicProfile.tsx:111 msgid "+ Invite someone" msgstr "+ Inviter quelqu'un" @@ -125,7 +125,7 @@ msgstr "un commentaire" msgid "a post" msgstr "une publication" -#: src/pages/UserPublicProfile.tsx:1093 +#: src/pages/UserPublicProfile.tsx:1095 msgid "Account" msgstr "Compte" @@ -133,7 +133,7 @@ msgstr "Compte" msgid "Add a comment…" msgstr "Ajouter un commentaire…" -#: src/pages/UserPublicProfile.tsx:792 +#: src/pages/UserPublicProfile.tsx:794 msgid "Add email…" msgstr "Ajouter un e-mail…" @@ -146,7 +146,7 @@ msgstr "Ajouter à la collection" msgid "Already have an account? <0>Log in" msgstr "Vous avez déjà un compte ? <0>Se connecter" -#: src/pages/UserPublicProfile.tsx:1112 +#: src/pages/UserPublicProfile.tsx:1114 msgid "Appearance" msgstr "Apparence" @@ -157,7 +157,7 @@ msgstr "Apparence" msgid "At least {0} characters" msgstr "Au moins {0} caractères" -#: src/pages/UserPublicProfile.tsx:1160 +#: src/pages/UserPublicProfile.tsx:1162 msgid "Auto" msgstr "Auto" @@ -177,8 +177,8 @@ msgstr "Impossible de se connecter au serveur de mises à jour en direct. Les vo #: src/pages/Dump.tsx:357 #: src/pages/DumpEdit.tsx:433 #: src/pages/PlaylistDetail.tsx:908 -#: src/pages/UserPublicProfile.tsx:1496 -#: src/pages/UserPublicProfile.tsx:1566 +#: src/pages/UserPublicProfile.tsx:1540 +#: src/pages/UserPublicProfile.tsx:1610 msgid "Cancel" msgstr "Annuler" @@ -190,7 +190,7 @@ msgstr "Annuler la suppression" msgid "Cancel search" msgstr "Annuler la recherche" -#: src/pages/UserPublicProfile.tsx:744 +#: src/pages/UserPublicProfile.tsx:746 msgid "Change avatar" msgstr "Changer l'avatar" @@ -199,7 +199,7 @@ msgstr "Changer l'avatar" msgid "Change password" msgstr "Changer le mot de passe" -#: src/pages/UserPublicProfile.tsx:1105 +#: src/pages/UserPublicProfile.tsx:1107 msgid "Change password…" msgstr "Changer le mot de passe…" @@ -212,7 +212,7 @@ msgstr "Vérification de l'invitation…" msgid "Close" msgstr "Fermer" -#: src/pages/UserPublicProfile.tsx:1152 +#: src/pages/UserPublicProfile.tsx:1154 msgid "Color scheme" msgstr "Thème de couleur" @@ -221,11 +221,11 @@ msgstr "Thème de couleur" msgid "Confirm new password" msgstr "Confirmer le nouveau mot de passe" -#: src/pages/UserPublicProfile.tsx:101 +#: src/pages/UserPublicProfile.tsx:102 msgid "Copied!" msgstr "Copié !" -#: src/pages/UserPublicProfile.tsx:101 +#: src/pages/UserPublicProfile.tsx:102 msgid "Copy" msgstr "Copier" @@ -263,10 +263,14 @@ msgstr "Création…" msgid "Current password" msgstr "Mot de passe actuel" -#: src/pages/UserPublicProfile.tsx:1174 +#: src/pages/UserPublicProfile.tsx:1176 msgid "Dark" msgstr "Sombre" +#: src/pages/UserPublicProfile.tsx:1189 +msgid "Default tab" +msgstr "Onglet par défaut" + #: src/components/CommentThread.tsx:374 #: src/components/CommentThread.tsx:380 #: src/components/ConfirmModal.tsx:16 @@ -329,13 +333,13 @@ msgstr "Recommandé !" #: src/pages/Search.tsx:172 #: src/pages/UserDumps.tsx:107 -#: src/pages/UserPublicProfile.tsx:865 +#: src/pages/UserPublicProfile.tsx:867 msgid "Dumps" msgstr "Recos" #. placeholder {0}: dumps.items.length #. placeholder {1}: dumps.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:882 +#: src/pages/UserPublicProfile.tsx:884 msgid "Dumps ({0}{1})" msgstr "Recos ({0}{1})" @@ -387,9 +391,9 @@ msgstr "Saisissez une recherche." msgid "Failed to create playlist" msgstr "Impossible de créer la collection" -#: src/pages/UserPublicProfile.tsx:82 -#: src/pages/UserPublicProfile.tsx:85 -#: src/pages/UserPublicProfile.tsx:113 +#: src/pages/UserPublicProfile.tsx:83 +#: src/pages/UserPublicProfile.tsx:86 +#: src/pages/UserPublicProfile.tsx:114 msgid "Failed to generate invite" msgstr "Impossible de générer une invitation" @@ -398,9 +402,9 @@ msgstr "Impossible de générer une invitation" #: src/pages/index/JournalFeed.tsx:33 #: src/pages/index/NewFeed.tsx:36 #: src/pages/Notifications.tsx:367 -#: src/pages/UserPublicProfile.tsx:984 -#: src/pages/UserPublicProfile.tsx:1026 -#: src/pages/UserPublicProfile.tsx:1071 +#: src/pages/UserPublicProfile.tsx:986 +#: src/pages/UserPublicProfile.tsx:1028 +#: src/pages/UserPublicProfile.tsx:1073 msgid "Failed to load" msgstr "Chargement échoué" @@ -417,8 +421,8 @@ msgid "Failed to post reply" msgstr "Impossible de publier la réponse" #: src/pages/PlaylistDetail.tsx:896 -#: src/pages/UserPublicProfile.tsx:1499 -#: src/pages/UserPublicProfile.tsx:1568 +#: src/pages/UserPublicProfile.tsx:1543 +#: src/pages/UserPublicProfile.tsx:1612 msgid "Failed to save" msgstr "Enregistrement échoué" @@ -426,10 +430,14 @@ msgstr "Enregistrement échoué" msgid "Failed to save edit" msgstr "Impossible d'enregistrer la modification" -#: src/pages/UserPublicProfile.tsx:801 +#: src/pages/UserPublicProfile.tsx:803 msgid "Failed to update avatar" msgstr "Impossible de mettre à jour l'avatar" +#: src/pages/UserPublicProfile.tsx:1184 +msgid "Feeds" +msgstr "Flux" + #: src/components/DumpCreateModal.tsx:360 msgid "Fetching preview…" msgstr "Récupération de l'aperçu…" @@ -468,8 +476,9 @@ msgstr "Suivez des collections publiques pour voir leurs recos ici." msgid "Follow some users to see their dumps here." msgstr "Suivez des utilisateurs pour voir leurs recos ici." -#: src/components/FeedTabBar.tsx:16 -#: src/pages/UserPublicProfile.tsx:867 +#: src/components/FeedTabBar.tsx:21 +#: src/pages/UserPublicProfile.tsx:869 +#: src/pages/UserPublicProfile.tsx:1218 msgid "Followed" msgstr "Suivi" @@ -479,13 +488,13 @@ msgstr "Suivi" msgid "Followed ({0}{1})" msgstr "Suivies ({0}{1})" -#: src/pages/UserPublicProfile.tsx:1015 +#: src/pages/UserPublicProfile.tsx:1017 msgid "Followed playlists" msgstr "Collections suivies" #: src/components/FollowButton.tsx:37 #: src/components/FollowButton.tsx:64 -#: src/pages/UserPublicProfile.tsx:973 +#: src/pages/UserPublicProfile.tsx:975 msgid "Following" msgstr "Abonné" @@ -509,7 +518,8 @@ msgstr "Accueil" msgid "Go to login" msgstr "Aller à la connexion" -#: src/components/FeedTabBar.tsx:12 +#: src/components/FeedTabBar.tsx:17 +#: src/pages/UserPublicProfile.tsx:1197 msgid "Hot" msgstr "Tendances" @@ -525,20 +535,21 @@ msgstr "Invitation invalide" msgid "Invalid link" msgstr "Lien invalide" -#: src/pages/UserPublicProfile.tsx:763 +#: src/pages/UserPublicProfile.tsx:765 msgid "invited by" msgstr "invité par" -#: src/pages/UserPublicProfile.tsx:868 -#: src/pages/UserPublicProfile.tsx:1060 +#: src/pages/UserPublicProfile.tsx:870 +#: src/pages/UserPublicProfile.tsx:1062 msgid "Invitees" msgstr "Invités" -#: src/components/FeedTabBar.tsx:14 +#: src/components/FeedTabBar.tsx:19 +#: src/pages/UserPublicProfile.tsx:1211 msgid "Journal" msgstr "Journal" -#: src/pages/UserPublicProfile.tsx:1167 +#: src/pages/UserPublicProfile.tsx:1169 msgid "Light" msgstr "Clair" @@ -580,7 +591,7 @@ msgstr "Chargement…" msgid "Loading playlist…" msgstr "Chargement de la collection…" -#: src/pages/UserPublicProfile.tsx:700 +#: src/pages/UserPublicProfile.tsx:702 msgid "Loading profile…" msgstr "Chargement du profil…" @@ -596,9 +607,9 @@ msgstr "Chargement du profil…" #: src/pages/Notifications.tsx:439 #: src/pages/UserDumps.tsx:51 #: src/pages/UserPlaylists.tsx:342 -#: src/pages/UserPublicProfile.tsx:978 -#: src/pages/UserPublicProfile.tsx:1020 -#: src/pages/UserPublicProfile.tsx:1065 +#: src/pages/UserPublicProfile.tsx:980 +#: src/pages/UserPublicProfile.tsx:1022 +#: src/pages/UserPublicProfile.tsx:1067 #: src/pages/UserUpvoted.tsx:122 msgid "Loading…" msgstr "Chargement…" @@ -617,8 +628,8 @@ msgstr "Se connecter pour aimer" msgid "Log in to vote" msgstr "Se connecter pour voter" -#: src/pages/UserPublicProfile.tsx:721 -#: src/pages/UserPublicProfile.tsx:815 +#: src/pages/UserPublicProfile.tsx:723 +#: src/pages/UserPublicProfile.tsx:817 msgid "Log out" msgstr "Se déconnecter" @@ -638,13 +649,16 @@ msgstr "Max 50 Mo" msgid "new" msgstr "nouveau" -#: src/components/FeedTabBar.tsx:13 +#: src/components/FeedTabBar.tsx:18 +#: src/pages/UserPublicProfile.tsx:1204 msgid "New" msgstr "Nouveau" #: src/components/DumpCreateModal.tsx:292 +#: src/components/DumpFab.tsx:65 +#: src/components/DumpFab.tsx:66 #: src/pages/UserDumps.tsx:115 -#: src/pages/UserPublicProfile.tsx:1223 +#: src/pages/UserPublicProfile.tsx:1267 msgid "New dump" msgstr "Nouvelle reco" @@ -677,11 +691,11 @@ msgid "No emoji found." msgstr "Aucun emoji trouvé." #: src/pages/UserPlaylists.tsx:439 -#: src/pages/UserPublicProfile.tsx:1033 +#: src/pages/UserPublicProfile.tsx:1035 msgid "No followed playlists yet." msgstr "Pas encore de collections suivies." -#: src/pages/UserPublicProfile.tsx:1078 +#: src/pages/UserPublicProfile.tsx:1080 msgid "No invitees yet." msgstr "Aucun invité pour le moment." @@ -695,7 +709,7 @@ msgstr "Aucune collection ne correspond à « {q} »." #: src/components/PlaylistMembershipPanel.tsx:34 #: src/pages/UserPlaylists.tsx:397 -#: src/pages/UserPublicProfile.tsx:944 +#: src/pages/UserPublicProfile.tsx:946 msgid "No playlists yet." msgstr "Pas encore de collections." @@ -703,14 +717,14 @@ msgstr "Pas encore de collections." msgid "No users match \"{q}\"." msgstr "Aucun utilisateur ne correspond à « {q} »." -#: src/pages/UserPublicProfile.tsx:991 +#: src/pages/UserPublicProfile.tsx:993 msgid "Not following anyone yet." msgstr "Aucun abonnement pour le moment." #: src/pages/Notifications.tsx:374 #: src/pages/UserDumps.tsx:125 -#: src/pages/UserPublicProfile.tsx:1234 -#: src/pages/UserPublicProfile.tsx:1356 +#: src/pages/UserPublicProfile.tsx:1278 +#: src/pages/UserPublicProfile.tsx:1400 #: src/pages/UserUpvoted.tsx:194 msgid "Nothing here yet." msgstr "Rien ici pour l'instant." @@ -733,7 +747,7 @@ msgid "or <0>browse files" msgstr "ou <0>parcourir les fichiers" #: src/pages/UserLogin.tsx:72 -#: src/pages/UserPublicProfile.tsx:1098 +#: src/pages/UserPublicProfile.tsx:1100 msgid "Password" msgstr "Mot de passe" @@ -763,13 +777,13 @@ msgstr "Titre de la collection" #: src/components/UserMenu.tsx:62 #: src/pages/Search.tsx:175 #: src/pages/UserPlaylists.tsx:368 -#: src/pages/UserPublicProfile.tsx:866 +#: src/pages/UserPublicProfile.tsx:868 msgid "Playlists" msgstr "Collections" #. placeholder {0}: playlists.items.length #. placeholder {1}: playlists.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:913 +#: src/pages/UserPublicProfile.tsx:915 msgid "Playlists ({0}{1})" msgstr "Collections ({0}{1})" @@ -885,8 +899,8 @@ msgstr "Réessayer" #: src/pages/Dump.tsx:349 #: src/pages/DumpEdit.tsx:436 #: src/pages/PlaylistDetail.tsx:915 -#: src/pages/UserPublicProfile.tsx:1488 -#: src/pages/UserPublicProfile.tsx:1558 +#: src/pages/UserPublicProfile.tsx:1532 +#: src/pages/UserPublicProfile.tsx:1602 msgid "Save" msgstr "Enregistrer" @@ -895,8 +909,8 @@ msgstr "Enregistrer" #: src/pages/Dump.tsx:348 #: src/pages/PlaylistDetail.tsx:911 #: src/pages/ResetPassword.tsx:124 -#: src/pages/UserPublicProfile.tsx:1485 -#: src/pages/UserPublicProfile.tsx:1555 +#: src/pages/UserPublicProfile.tsx:1529 +#: src/pages/UserPublicProfile.tsx:1599 msgid "Saving…" msgstr "Enregistrement…" @@ -934,7 +948,7 @@ msgstr "Serveur inaccessible" msgid "Set new password" msgstr "Définir un nouveau mot de passe" -#: src/pages/UserPublicProfile.tsx:870 +#: src/pages/UserPublicProfile.tsx:872 msgid "Settings" msgstr "Paramètres" @@ -943,7 +957,7 @@ msgstr "Paramètres" msgid "Something went wrong" msgstr "Une erreur est survenue" -#: src/pages/UserPublicProfile.tsx:1117 +#: src/pages/UserPublicProfile.tsx:1119 msgid "Style" msgstr "Style" @@ -997,7 +1011,7 @@ msgstr "Ne plus suivre {targetUsername}" msgid "Unfollow playlist" msgstr "Ne plus suivre la collection" -#: src/pages/UserPublicProfile.tsx:671 +#: src/pages/UserPublicProfile.tsx:673 msgid "Upload failed" msgstr "Envoi échoué" @@ -1015,7 +1029,7 @@ msgstr "Voté" #. placeholder {0}: votes.items.length #. placeholder {1}: votes.hasMore ? "+" : "" -#: src/pages/UserPublicProfile.tsx:893 +#: src/pages/UserPublicProfile.tsx:895 msgid "Upvoted ({0}{1})" msgstr "Votés ({0}{1})" @@ -1041,11 +1055,11 @@ msgstr "Nom d'utilisateur" msgid "Users" msgstr "Utilisateurs" -#: src/pages/UserPublicProfile.tsx:963 -#: src/pages/UserPublicProfile.tsx:1006 -#: src/pages/UserPublicProfile.tsx:1048 -#: src/pages/UserPublicProfile.tsx:1255 -#: src/pages/UserPublicProfile.tsx:1386 +#: src/pages/UserPublicProfile.tsx:965 +#: src/pages/UserPublicProfile.tsx:1008 +#: src/pages/UserPublicProfile.tsx:1050 +#: src/pages/UserPublicProfile.tsx:1299 +#: src/pages/UserPublicProfile.tsx:1430 msgid "View all →" msgstr "Tout voir →" @@ -1058,8 +1072,8 @@ msgstr "Voir la reco →" msgid "What makes it worth it?" msgstr "Pourquoi on en voudrait ?" -#: src/pages/UserPublicProfile.tsx:850 -#: src/pages/UserPublicProfile.tsx:1547 +#: src/pages/UserPublicProfile.tsx:852 +#: src/pages/UserPublicProfile.tsx:1591 msgid "Who am I?" msgstr "Qui suis-je ?" diff --git a/src/pages/Index.tsx b/src/pages/Index.tsx index aae518b..10d9d6b 100644 --- a/src/pages/Index.tsx +++ b/src/pages/Index.tsx @@ -13,6 +13,7 @@ import { PresenceRow } from "../components/PresenceRow.tsx"; import { FeedTabBar } from "../components/FeedTabBar.tsx"; import { FEED_TABS, type FeedTab } from "../config/feedTabs.ts"; import { useTabParam } from "../hooks/useTabParam.ts"; +import { useDefaultFeedTab } from "../hooks/useDefaultFeedTab.ts"; import { API_URL, DEFAULT_PAGE_SIZE } from "../config/api.ts"; @@ -83,7 +84,13 @@ export function Index() { ); const mainFetchDone = useRef(false); - const [tab] = useTabParam(FEED_TABS, "hot"); + // The preferred default tab decides which feed `/` opens on. "followed" + // requires a session, so fall back to "hot" for guests. + const [preferredTab] = useDefaultFeedTab(); + const defaultTab: FeedTab = preferredTab === "followed" && !user + ? "hot" + : preferredTab; + const [tab] = useTabParam(FEED_TABS, defaultTab); // Web Share Target: Android share sheet navigates to /?share_url=... const searchParams = new URLSearchParams(location.search); diff --git a/src/pages/UserPublicProfile.tsx b/src/pages/UserPublicProfile.tsx index 3bbe6d6..f2f24ab 100644 --- a/src/pages/UserPublicProfile.tsx +++ b/src/pages/UserPublicProfile.tsx @@ -38,6 +38,7 @@ import { PageShell } from "../components/PageShell.tsx"; import { PageError } from "../components/PageError.tsx"; import { useAuth } from "../hooks/useAuth.ts"; import { useTheme } from "../hooks/useTheme.ts"; +import { useDefaultFeedTab } from "../hooks/useDefaultFeedTab.ts"; import { useWS } from "../hooks/useWS.ts"; import { useDumpListSync } from "../hooks/useDumpListSync.ts"; import { useFading } from "../hooks/useFading.ts"; @@ -178,6 +179,7 @@ export function UserPublicProfile() { const navigate = useNavigate(); const { user: me, authFetch, login, logout, token } = useAuth(); const { style, colorScheme, setStyle, setColorScheme } = useTheme(); + const [defaultFeedTab, setDefaultFeedTab] = useDefaultFeedTab(); const { voteCounts, myVotes, @@ -1177,6 +1179,48 @@ export function UserPublicProfile() { +
+

+ Feeds +

+
+
+ + Default tab + +
+ + + + +
+
+
+
)} diff --git a/src/themes/brutalist.css b/src/themes/brutalist.css index 46da2f6..af3a187 100644 --- a/src/themes/brutalist.css +++ b/src/themes/brutalist.css @@ -149,7 +149,13 @@ } /* ── Buttons — square corners, modest hard shadow ────────────────── */ -[data-style="brutalist"] button { +/* `.dump-fab` is exempt from the generic button chrome — it keeps its own + shape, shadow and animations — but this hard-edged theme squares it off. */ +[data-style="brutalist"] .dump-fab { + --fab-radius: 0; +} + +[data-style="brutalist"] button:not(.dump-fab) { border-radius: 0; border: 2px solid var(--color-border); font-weight: 700; @@ -157,7 +163,7 @@ transition: box-shadow 0.08s ease, transform 0.08s ease, border-color 0.1s; } -[data-style="brutalist"] button:hover:not(:disabled) { +[data-style="brutalist"] button:not(.dump-fab):hover:not(:disabled) { border-color: var(--color-border); box-shadow: none; transform: translate(2px, 2px); diff --git a/src/themes/geocities.css b/src/themes/geocities.css index e11093e..2108e28 100644 --- a/src/themes/geocities.css +++ b/src/themes/geocities.css @@ -252,7 +252,13 @@ } /* ── Buttons — chunky Win95 outset bevel, press = inset ──────────── */ -[data-style="geocities"] button { +/* `.dump-fab` is exempt from the generic button chrome — it keeps its own + shape, glow and animations — but this squared-off retro theme squares it. */ +[data-style="geocities"] .dump-fab { + --fab-radius: 0; +} + +[data-style="geocities"] button:not(.dump-fab) { border-radius: 0; border: 3px outset var(--color-border); box-shadow: none; @@ -282,6 +288,14 @@ box-shadow: none; } +/* The shared .vote-btn declares `background: transparent`, which leaves the + chunky outset bevel see-through against the card. Give the resting state a + solid raised-panel fill; the voted (--active) state keeps its accent + background, so scope this to non-active buttons. */ +[data-style="geocities"] .vote-btn:not(.vote-btn--active) { + background: var(--color-surface); +} + /* Ghost / icon-only buttons stay flat */ [data-style="geocities"] .modal-close-btn, [data-style="geocities"] .playlist-remove-btn, @@ -326,6 +340,10 @@ box-shadow: 0 0 9px color-mix(in srgb, var(--color-border) 38%, transparent); } +[data-style="geocities"] .dump-card-comment { + padding-bottom: .1rem; +} + /* Comments nest deeply, so the full frame piled up. Keep the squared retro edge but lighten it: thin flat border, just a whisper of the same halo. */ [data-style="geocities"] .comment-node-inner, diff --git a/src/themes/nyt.css b/src/themes/nyt.css index a3c9182..7e31088 100644 --- a/src/themes/nyt.css +++ b/src/themes/nyt.css @@ -150,7 +150,7 @@ } /* Sharp, rectilinear corners throughout — newsprint has no rounding. */ -[data-style="nyt"] button, +[data-style="nyt"] button:not(.dump-fab), [data-style="nyt"] .dump-card, [data-style="nyt"] .playlist-card, [data-style="nyt"] .journal-card, @@ -182,6 +182,12 @@ border-radius: 0; } +/* This theme squares everything off — opt the FAB into a square too (it keeps + its animations; only the corners change). */ +[data-style="nyt"] .dump-fab { + --fab-radius: 0; +} + /* ── Headlines — display serif ───────────────────────────────────── */ [data-style="nyt"] h1, [data-style="nyt"] .app-header-brand, @@ -364,8 +370,9 @@ box-shadow: none; } -/* Page-coloured strip over the grid's content-left edge, hiding the first - column's left rule (left value tracks the grid's padding-left). */ +/* Page-coloured strips over the grid's content-left and content-top edges, + hiding the first column's left rule and the first row's top rule (offsets + track the grid's padding-left / padding-top). */ [data-style="nyt"] .journal-grid::before { content: ""; position: absolute; @@ -377,10 +384,24 @@ z-index: 1; pointer-events: none; } +[data-style="nyt"] .journal-grid::after { + content: ""; + position: absolute; + top: 1.25rem; + left: 0; + right: 0; + height: 1px; + background: var(--color-bg); + z-index: 1; + pointer-events: none; +} @media (max-width: 460px) { [data-style="nyt"] .journal-grid::before { left: 1rem; } + [data-style="nyt"] .journal-grid::after { + top: 0.85rem; + } } [data-style="nyt"] .journal-card:hover { border-color: var(--color-border);