fix(ui): honest tab titles and a download field that names every site
Signed out, the tab read Home's question over the sign-in form; the legal pages read just "Siftlode". The download field asked only for a YouTube link although Facebook and the rest download too.
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
// bookkeeping.
|
// bookkeeping.
|
||||||
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
|
import { describe, it, expect, afterEach, beforeEach, vi } from "vitest";
|
||||||
import { renderHook, act, cleanup } from "@testing-library/react";
|
import { renderHook, act, cleanup } from "@testing-library/react";
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
// Partial: the provider reads `t` AND `i18n.language` (both feed the document-title effect), and the
|
// Partial: the provider reads `t` AND `i18n.language` (both feed the document-title effect), and the
|
||||||
@@ -28,8 +29,14 @@ import { NavigationProvider } from "./NavigationProvider";
|
|||||||
import { useNavigation, useNavigationActions } from "./navigationContext";
|
import { useNavigation, useNavigationActions } from "./navigationContext";
|
||||||
import { useBackToClose } from "../lib/history";
|
import { useBackToClose } from "../lib/history";
|
||||||
|
|
||||||
|
import { qk } from "../lib/queryKeys";
|
||||||
|
|
||||||
|
// The provider reads the signed-in account (the tab title) from the `me` cache.
|
||||||
|
let qc = new QueryClient();
|
||||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
<NavigationProvider>{children}</NavigationProvider>
|
<QueryClientProvider client={qc}>
|
||||||
|
<NavigationProvider>{children}</NavigationProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
const render = () =>
|
const render = () =>
|
||||||
renderHook(() => ({ ...useNavigation(), ...useNavigationActions() }), { wrapper });
|
renderHook(() => ({ ...useNavigation(), ...useNavigationActions() }), { wrapper });
|
||||||
@@ -54,6 +61,8 @@ async function openOverlay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
qc = new QueryClient();
|
||||||
|
qc.setQueryData(qk.me(), { id: 1 });
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
sessionStorage.setItem("siftlode.activeAccount", "1"); // account-scoped page memory
|
sessionStorage.setItem("siftlode.activeAccount", "1"); // account-scoped page memory
|
||||||
window.history.replaceState({ sfPage: "feed" }, "", "/");
|
window.history.replaceState({ sfPage: "feed" }, "", "/");
|
||||||
@@ -65,6 +74,22 @@ afterEach(() => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("NavigationProvider · the tab title", () => {
|
||||||
|
// Prod UAT 2026-09-19: signed out, the tab read the Home page's question over the sign-in form.
|
||||||
|
it("is the brand alone while nobody is signed in, the page's name once someone is", async () => {
|
||||||
|
window.history.replaceState({ sfPage: "home" }, "", "/?page=home");
|
||||||
|
qc.setQueryData(qk.me(), null);
|
||||||
|
render();
|
||||||
|
await settle();
|
||||||
|
expect(document.title).toBe("Siftlode");
|
||||||
|
|
||||||
|
act(() => qc.setQueryData(qk.me(), { id: 1 }));
|
||||||
|
await settle();
|
||||||
|
expect(document.title).not.toBe("Siftlode");
|
||||||
|
expect(document.title.endsWith(" · Siftlode")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("NavigationProvider · setPage's same-page guard", () => {
|
describe("NavigationProvider · setPage's same-page guard", () => {
|
||||||
it("pushes an entry for a real page change", () => {
|
it("pushes an entry for a real page change", () => {
|
||||||
const { result } = render();
|
const { result } = render();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useMe } from "../lib/useMe";
|
||||||
import { urlWithParams, type Page } from "../lib/urlState";
|
import { urlWithParams, type Page } from "../lib/urlState";
|
||||||
import { LOGS_KEYS } from "../lib/logsUrlState";
|
import { LOGS_KEYS } from "../lib/logsUrlState";
|
||||||
import { pageTitleKey } from "../lib/pageMeta";
|
import { pageTitleKey } from "../lib/pageMeta";
|
||||||
@@ -154,16 +155,20 @@ export function NavigationProvider({ children }: { children: ReactNode }) {
|
|||||||
}, [page]);
|
}, [page]);
|
||||||
|
|
||||||
// Reflect the current module in the browser tab title. Feed = brand only; an open channel page
|
// Reflect the current module in the browser tab title. Feed = brand only; an open channel page
|
||||||
// shows the channel name.
|
// shows the channel name. Signed out, the page on screen is the public landing whatever `page`
|
||||||
|
// says — the tab read "What should I watch?" over the sign-in form (prod UAT 2026-09-19).
|
||||||
|
const signedIn = useMe() != null;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const brand = "Siftlode";
|
const brand = "Siftlode";
|
||||||
const label = channelView
|
const label = !signedIn
|
||||||
? channelView.name || t("header.channelManager")
|
? null
|
||||||
: page === "feed"
|
: channelView
|
||||||
? null
|
? channelView.name || t("header.channelManager")
|
||||||
: t(pageTitleKey(page));
|
: page === "feed"
|
||||||
|
? null
|
||||||
|
: t(pageTitleKey(page));
|
||||||
document.title = label ? `${label} · ${brand}` : brand;
|
document.title = label ? `${label} · ${brand}` : brand;
|
||||||
}, [page, channelView, i18n.language, t]);
|
}, [signedIn, page, channelView, i18n.language, t]);
|
||||||
|
|
||||||
// In-app Back/Forward: stamp the initial entry with the current page (mirrored to the address bar),
|
// In-app Back/Forward: stamp the initial entry with the current page (mirrored to the address bar),
|
||||||
// then sync state from history.state on popstate — the browser restores each entry's URL for us, so
|
// then sync state from history.state on popstate — the browser restores each entry's URL for us, so
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ export default function LegalLayout({ title, children }: { title: string; childr
|
|||||||
const id = decodeURIComponent(window.location.hash.slice(1));
|
const id = decodeURIComponent(window.location.hash.slice(1));
|
||||||
if (id) document.getElementById(id)?.scrollIntoView();
|
if (id) document.getElementById(id)?.scrollIntoView();
|
||||||
}, []);
|
}, []);
|
||||||
|
// The tab names the document, like every page of the app — a bookmark or a tab strip read just
|
||||||
|
// "Siftlode" (prod UAT 2026-09-19).
|
||||||
|
useEffect(() => {
|
||||||
|
document.title = `${title} · Siftlode`;
|
||||||
|
}, [title]);
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-bg text-fg">
|
<div className="min-h-screen bg-bg text-fg">
|
||||||
<div className="mx-auto w-[min(92vw,720px)] py-12">
|
<div className="mx-auto w-[min(92vw,720px)] py-12">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"page": {
|
"page": {
|
||||||
"title": "Downloads",
|
"title": "Downloads",
|
||||||
"subtitle": "Download videos to the server, then save them to your device.",
|
"subtitle": "Download videos to the server, then save them to your device.",
|
||||||
"addUrl": "Paste a YouTube link or video id…",
|
"addUrl": "Paste a video link (YouTube, Facebook, …) or a YouTube video id…",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"paste": "Paste link from clipboard",
|
"paste": "Paste link from clipboard",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"page": {
|
"page": {
|
||||||
"title": "Letöltések",
|
"title": "Letöltések",
|
||||||
"subtitle": "Töltsd le a videókat a szerverre, majd mentsd a saját gépedre.",
|
"subtitle": "Töltsd le a videókat a szerverre, majd mentsd a saját gépedre.",
|
||||||
"addUrl": "Illessz be egy YouTube linket vagy videó azonosítót…",
|
"addUrl": "Illessz be egy videólinket (YouTube, Facebook, …) vagy YouTube-videóazonosítót…",
|
||||||
"add": "Hozzáad",
|
"add": "Hozzáad",
|
||||||
"paste": "Link beillesztése a vágólapról",
|
"paste": "Link beillesztése a vágólapról",
|
||||||
"upload": "Feltöltés",
|
"upload": "Feltöltés",
|
||||||
|
|||||||
Reference in New Issue
Block a user