feat(welcome): landing lightbox, prod-served screenshots & session-end UX
- Serve real files at the SPA root (e.g. /welcome/*.png, favicon) from the built app — the landing screenshots were only ever shown by the Vite dev server, never in production. - Add the Channel-manager screenshot; the two secondary previews are smaller thumbnails that open in a custom full-size lightbox (fit-to-viewport, Esc/backdrop/✕ to close). - Drop a suspended/deleted user to the login page on the next 401 (and poll the session only while signed in, so the public login page no longer flickers); 'suspended' and 'account deleted' confirmation banners; strip redirect query params for a clean address bar.
This commit is contained in:
parent
4571800991
commit
c59695d2b3
7 changed files with 180 additions and 22 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api, HttpError, type FeedFilters } from "./lib/api";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { api, HttpError, setUnauthorizedHandler, type FeedFilters } from "./lib/api";
|
||||
import { setLanguage, isSupported, type LangCode } from "./i18n";
|
||||
import {
|
||||
applyTheme,
|
||||
|
|
@ -264,7 +264,47 @@ export default function App() {
|
|||
return () => window.removeEventListener("popstate", onPop);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const meQuery = useQuery({ queryKey: ["me"], queryFn: api.me });
|
||||
const queryClient = useQueryClient();
|
||||
// Poll the session so an account suspended/deleted by an admin is noticed even with no
|
||||
// interaction (option 1): the refetch 401s → the 401 handler below drops to the login page.
|
||||
// Only poll while signed in (data present) — polling on the public login page is pointless and
|
||||
// its periodic refetch caused a visible flicker there.
|
||||
const meQuery = useQuery({
|
||||
queryKey: ["me"],
|
||||
queryFn: api.me,
|
||||
refetchInterval: (query) => (query.state.data ? 60_000 : false),
|
||||
});
|
||||
|
||||
// Any request that 401s means the session ended server-side. If we were signed in (cached
|
||||
// `me` exists), reload to the login page at once (option 2 — also covers any click that hits
|
||||
// the API). Guarded on cached `me` so the public login page's own /api/me 401 can't loop.
|
||||
useEffect(() => {
|
||||
setUnauthorizedHandler(() => {
|
||||
if (queryClient.getQueryData(["me"])) {
|
||||
queryClient.clear();
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
return () => setUnauthorizedHandler(null);
|
||||
}, [queryClient]);
|
||||
|
||||
// Returning from an in-app Google link/upgrade round-trip (auth.py redirects to ?link=…).
|
||||
// Surface the outcome, refresh `me` (can_read/can_write/has_google may have flipped), land on
|
||||
// Settings → Account so the new state is visible, then strip the param for a clean URL.
|
||||
useEffect(() => {
|
||||
const result = new URLSearchParams(window.location.search).get("link");
|
||||
if (!result) return;
|
||||
if (result === "ok") {
|
||||
notify({ level: "success", message: t("settings.account.googleLink.linked") });
|
||||
void meQuery.refetch();
|
||||
localStorage.setItem("siftlode.settingsTab", "account");
|
||||
setPage("settings");
|
||||
} else {
|
||||
const key = result === "conflict" || result === "mismatch" ? result : "error";
|
||||
notify({ level: "error", message: t(`settings.account.googleLink.${key}`) });
|
||||
}
|
||||
stripUrlParams();
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// First-login onboarding: prompt the user to connect YouTube (and resume the flow after
|
||||
// each consent redirect). Derived from granted scopes + storage flags so it's stable
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue