style(frontend): Prettier config + one-time repo-wide format pass

Prettier 3 (pinned), printWidth 100, double quotes — chosen to match the existing
hand-formatting and minimise reflow. This commit is ONLY the mechanical format pass
(`prettier --write`) plus the config/ignore/scripts, isolated so it never pollutes a
feature diff. Verified behaviour-neutral: typecheck, eslint (0 errors), and vitest
all green before and after. See .git-blame-ignore-revs — `git blame` skips this sha.
This commit is contained in:
2026-07-21 02:26:40 +02:00
parent 04ecd8b3a7
commit e257e2aca0
101 changed files with 4388 additions and 2641 deletions
+112 -93
View File
@@ -1,6 +1,11 @@
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { keepPreviousData, useInfiniteQuery, useQuery, useQueryClient } from "@tanstack/react-query";
import {
keepPreviousData,
useInfiniteQuery,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import {
ArrowDown,
ArrowUp,
@@ -123,13 +128,13 @@ export default function Feed({
const [savedOverrides, setSavedOverrides] = useState<Record<string, boolean>>({});
// The open player: which video and where to start (null = resume from saved position).
const [activeVideo, setActiveVideo] = useState<{ video: Video; startAt: number | null } | null>(
null
null,
);
const qc = useQueryClient();
const openVideo = useCallback(
(video: Video, startAt: number | null = null) => setActiveVideo({ video, startAt }),
[]
[],
);
const ytActive = !!ytSearch;
@@ -158,7 +163,8 @@ export default function Feed({
// revisited search from re-spending.
const ytQuery = useInfiniteQuery({
queryKey: ["yt-search", ytSearch, ytCount],
queryFn: ({ pageParam }) => api.searchYoutube(ytSearch as string, pageParam as string | null, ytCount),
queryFn: ({ pageParam }) =>
api.searchYoutube(ytSearch as string, pageParam as string | null, ytCount),
initialPageParam: null as string | null,
getNextPageParam: (last) => last.next_cursor ?? undefined,
enabled: ytActive,
@@ -282,10 +288,10 @@ export default function Feed({
const next = { ...o };
delete next[id];
return next;
})
}),
);
},
[qc]
[qc],
);
const onResetState = useCallback(
@@ -301,10 +307,10 @@ export default function Feed({
const next = { ...o };
delete next[id];
return next;
})
}),
);
},
[qc]
[qc],
);
const onToggleSave = useCallback(
@@ -317,7 +323,7 @@ export default function Feed({
})
.catch(() => setSavedOverrides((o) => ({ ...o, [id]: !saved })));
},
[qc]
[qc],
);
const loaded: Video[] = (query.data?.pages ?? []).flatMap((p) => p.items);
@@ -410,7 +416,9 @@ export default function Feed({
const ids = ytItems.map((v) => v.id);
api
.clearSearch(ids)
.then((r) => notify({ message: i18n.t("feed.yt.cleared", { count: r.removed }) }))
.then((r) =>
notify({ message: i18n.t("feed.yt.cleared", { count: r.removed }) }),
)
.catch(() => {});
setFilters({ ...filters, librarySource: "organic", sort: "newest" });
onExitYtSearch();
@@ -485,40 +493,40 @@ export default function Feed({
// wizard; real users get the onboarding prompt. No toolbar (sort/type are meaningless).
if (items.length === 0 && !canRead && filters.scope === "my")
return (
<div className="p-8 grid place-items-center text-center">
<div className="max-w-sm">
<h2 className="text-lg font-semibold">
{isDemo ? t("feed.demoEmptyTitle") : t("feed.emptyTitle")}
</h2>
<p className="text-sm text-muted mt-2 mb-4">
{isDemo ? t("feed.demoEmptyBody") : t("feed.emptyBody")}
</p>
{isDemo ? (
<div className="p-8 grid place-items-center text-center">
<div className="max-w-sm">
<h2 className="text-lg font-semibold">
{isDemo ? t("feed.demoEmptyTitle") : t("feed.emptyTitle")}
</h2>
<p className="text-sm text-muted mt-2 mb-4">
{isDemo ? t("feed.demoEmptyBody") : t("feed.emptyBody")}
</p>
{isDemo ? (
<button
onClick={() => setFilters({ ...filters, scope: "all" })}
className="inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition"
>
{t("feed.browseLibrary")}
</button>
) : (
<>
<button
onClick={() => setFilters({ ...filters, scope: "all" })}
onClick={onOpenWizard}
className="inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition"
>
{t("feed.browseLibrary")}
{t("feed.setUp")}
</button>
) : (
<>
<button
onClick={onOpenWizard}
className="inline-flex items-center justify-center gap-2 px-5 py-2.5 rounded-xl font-semibold bg-accent text-accent-fg hover:opacity-90 transition"
>
{t("feed.setUp")}
</button>
<button
onClick={() => setFilters({ ...filters, scope: "all" })}
className="block mx-auto mt-3 text-sm text-muted hover:text-accent transition"
>
{t("feed.browseShared")}
</button>
</>
)}
</div>
<button
onClick={() => setFilters({ ...filters, scope: "all" })}
className="block mx-auto mt-3 text-sm text-muted hover:text-accent transition"
>
{t("feed.browseShared")}
</button>
</>
)}
</div>
);
</div>
);
const { key: sortKey, dir: sortDir } = parseSort(filters.sort);
@@ -597,61 +605,72 @@ export default function Feed({
</span>
<div className="ml-auto flex items-center gap-2">
<span className="text-xs text-muted">{t("feed.sortLabel")}</span>
<select
value={sortKey}
onChange={(e) => {
const key = e.target.value as SortKey;
const dir = DIRECTIONLESS.includes(key)
? "desc"
: SORT_DEFAULT_DIR[key as Exclude<SortKey, "shuffle" | "relevance">];
setFilters({
...filters,
sort: buildSort(key, dir),
seed: key === "shuffle" ? rollSeed() : undefined,
});
}}
aria-label={t("feed.sortLabel")}
className="bg-card border border-border rounded-lg px-2 py-1.5 text-sm outline-none focus:border-accent"
>
{/* Relevance only ranks meaningfully when there's a search term — offer it then. */}
{(filters.q.trim() ? (["relevance", ...sortKeys] as SortKey[]) : sortKeys).map((k) => (
<option key={k} value={k}>
{t("feed.sortKey." + k)}
</option>
))}
</select>
{!DIRECTIONLESS.includes(sortKey) && (
<button
onClick={() =>
setFilters({ ...filters, sort: buildSort(sortKey, sortDir === "asc" ? "desc" : "asc") })
}
title={sortDir === "asc" ? t("feed.dirAsc") : t("feed.dirDesc")}
aria-label={sortDir === "asc" ? t("feed.dirAsc") : t("feed.dirDesc")}
className="shrink-0 p-1.5 rounded-lg border border-border bg-card text-fg hover:border-accent hover:text-accent active:translate-y-px transition"
<select
value={sortKey}
onChange={(e) => {
const key = e.target.value as SortKey;
const dir = DIRECTIONLESS.includes(key)
? "desc"
: SORT_DEFAULT_DIR[key as Exclude<SortKey, "shuffle" | "relevance">];
setFilters({
...filters,
sort: buildSort(key, dir),
seed: key === "shuffle" ? rollSeed() : undefined,
});
}}
aria-label={t("feed.sortLabel")}
className="bg-card border border-border rounded-lg px-2 py-1.5 text-sm outline-none focus:border-accent"
>
{sortDir === "asc" ? <ArrowUp className="w-4 h-4" /> : <ArrowDown className="w-4 h-4" />}
</button>
)}
{sortKey === "shuffle" && (
<button
onClick={() => setFilters({ ...filters, seed: rollSeed() })}
title={t("sidebar.reshuffle")}
aria-label={t("sidebar.reshuffle")}
className="shrink-0 p-1.5 rounded-lg border border-border bg-card text-fg hover:border-accent hover:text-accent active:translate-y-px transition"
>
<RefreshCw className="w-4 h-4" />
</button>
)}
<span className="mx-0.5 h-5 w-px bg-border" aria-hidden="true" />
<ViewSwitcher
value={view}
options={FEED_VIEWS.map((id) => ({ id, label: t("feed.view." + id), icon: VIEW_ICON[id] }))}
onChange={setView}
label={t("feed.viewLabel")}
// This toolbar is packed — show/content chips, source, count, sort — so the mode's name
// only earns its place once the window is wide.
labelClassName="hidden xl:inline"
/>
{/* Relevance only ranks meaningfully when there's a search term — offer it then. */}
{(filters.q.trim() ? (["relevance", ...sortKeys] as SortKey[]) : sortKeys).map((k) => (
<option key={k} value={k}>
{t("feed.sortKey." + k)}
</option>
))}
</select>
{!DIRECTIONLESS.includes(sortKey) && (
<button
onClick={() =>
setFilters({
...filters,
sort: buildSort(sortKey, sortDir === "asc" ? "desc" : "asc"),
})
}
title={sortDir === "asc" ? t("feed.dirAsc") : t("feed.dirDesc")}
aria-label={sortDir === "asc" ? t("feed.dirAsc") : t("feed.dirDesc")}
className="shrink-0 p-1.5 rounded-lg border border-border bg-card text-fg hover:border-accent hover:text-accent active:translate-y-px transition"
>
{sortDir === "asc" ? (
<ArrowUp className="w-4 h-4" />
) : (
<ArrowDown className="w-4 h-4" />
)}
</button>
)}
{sortKey === "shuffle" && (
<button
onClick={() => setFilters({ ...filters, seed: rollSeed() })}
title={t("sidebar.reshuffle")}
aria-label={t("sidebar.reshuffle")}
className="shrink-0 p-1.5 rounded-lg border border-border bg-card text-fg hover:border-accent hover:text-accent active:translate-y-px transition"
>
<RefreshCw className="w-4 h-4" />
</button>
)}
<span className="mx-0.5 h-5 w-px bg-border" aria-hidden="true" />
<ViewSwitcher
value={view}
options={FEED_VIEWS.map((id) => ({
id,
label: t("feed.view." + id),
icon: VIEW_ICON[id],
}))}
onChange={setView}
label={t("feed.viewLabel")}
// This toolbar is packed — show/content chips, source, count, sort — so the mode's name
// only earns its place once the window is wide.
labelClassName="hidden xl:inline"
/>
</div>
</div>
</div>