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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user