terry-staging #13
@@ -31,7 +31,7 @@ export function BackToTop() {
|
||||
exit={{ opacity: 0, scale: 0.8, y: 8 }}
|
||||
transition={{ type: "spring", stiffness: 380, damping: 26 }}
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
|
||||
className="fixed bottom-[94px] right-4 z-30 flex h-11 w-11 items-center justify-center rounded-full bg-ark-gold text-black shadow-lg shadow-black/40 outline-none transition hover:bg-ark-gold2 active:scale-95 focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg md:bottom-8 md:right-8"
|
||||
className="fixed bottom-[calc(84px+max(env(safe-area-inset-bottom),0px))] right-4 z-30 flex h-11 w-11 items-center justify-center rounded-full bg-ark-gold text-black shadow-lg shadow-black/40 outline-none transition hover:bg-ark-gold2 active:scale-95 focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg md:bottom-8 md:right-8"
|
||||
aria-label={t("backToTop")}
|
||||
title={t("backToTop")}
|
||||
>
|
||||
|
||||
@@ -112,6 +112,17 @@ function streamKey(params: PostStreamParams): string {
|
||||
return buildRealUrl(params);
|
||||
}
|
||||
|
||||
function cacheFirstPage(
|
||||
params: PostStreamParams,
|
||||
page: PostListResponse,
|
||||
): void {
|
||||
streamCache.set(streamKey(params), {
|
||||
items: itemsOrEmpty(page.items),
|
||||
cursor: page.nextCursor,
|
||||
hasMore: !!page.nextCursor,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Warm the cache for a stream view before the user navigates to it, so opening
|
||||
* the page shows content immediately instead of starting to load on arrival.
|
||||
@@ -119,20 +130,31 @@ function streamKey(params: PostStreamParams): string {
|
||||
*/
|
||||
export function prefetchPostStream(params: PostStreamParams): void {
|
||||
if (USE_MOCK) return;
|
||||
const key = streamKey(params);
|
||||
if (streamCache.has(key)) return;
|
||||
|
||||
const url = buildRealUrl(params);
|
||||
if (readJSONCache<PostListResponse>(url)) return;
|
||||
getJSON<PostListResponse>(url).catch(() => {});
|
||||
const cachedPage = readJSONCache<PostListResponse>(url);
|
||||
if (cachedPage) {
|
||||
cacheFirstPage(params, cachedPage);
|
||||
return;
|
||||
}
|
||||
|
||||
getJSON<PostListResponse>(url)
|
||||
.then((page) => cacheFirstPage(params, page))
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
export function usePostStream(params: PostStreamParams): PostStreamResult {
|
||||
const [items, setItems] = useState<Post[]>([]);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const initialCached = streamCache.get(streamKey(params));
|
||||
const [items, setItems] = useState<Post[]>(() => initialCached?.items ?? []);
|
||||
const [hasMore, setHasMore] = useState(() => initialCached?.hasMore ?? true);
|
||||
const [isLoading, setIsLoading] = useState(() => !initialCached);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const reqIdRef = useRef(0);
|
||||
const cursorRef = useRef<string | undefined>(undefined);
|
||||
const hasMoreRef = useRef(true);
|
||||
const cursorRef = useRef<string | undefined>(initialCached?.cursor);
|
||||
const hasMoreRef = useRef(initialCached?.hasMore ?? true);
|
||||
const loadingRef = useRef(false);
|
||||
|
||||
const fetchPage = useCallback(
|
||||
|
||||
@@ -411,7 +411,7 @@ export function PublicLayout() {
|
||||
}, [mobileSearchOpen]);
|
||||
|
||||
return (
|
||||
<div className="min-h-full flex flex-col">
|
||||
<div className="flex min-h-[100dvh] flex-col bg-ark-bg">
|
||||
<DocumentMeta />
|
||||
<header className="sticky top-0 z-40 select-none bg-[#08070c] backdrop-blur-md md:border-b md:border-ark-line md:bg-ark-nav/98">
|
||||
<div className="flex h-[64px] items-center justify-between bg-[#08070c] px-4 py-3 md:hidden">
|
||||
@@ -694,7 +694,7 @@ export function PublicLayout() {
|
||||
) : null}
|
||||
|
||||
<main
|
||||
className={`mx-auto w-full max-w-[1280px] ${
|
||||
className={`mx-auto w-full max-w-[1280px] max-md:pb-[calc(68px+max(env(safe-area-inset-bottom),0px)+1rem)] ${
|
||||
isHome
|
||||
? "flex-1 px-0 pb-6 pt-0 md:px-9 md:pb-10 md:pt-10 xl:px-0"
|
||||
: footerInContentFlow
|
||||
@@ -715,8 +715,8 @@ export function PublicLayout() {
|
||||
</AnimatePresence>
|
||||
</main>
|
||||
|
||||
<nav className="sticky inset-x-0 bottom-0 z-40 select-none bg-[#0C0D0F]/90 backdrop-blur md:hidden">
|
||||
<div className="grid h-[78px] grid-cols-4 gap-3 px-5 py-4 text-center text-[11px] leading-[17.6px]">
|
||||
<nav className="fixed inset-x-0 bottom-0 z-40 select-none bg-[#0C0D0F]/95 pb-[max(env(safe-area-inset-bottom),0px)] backdrop-blur md:hidden">
|
||||
<div className="grid h-[68px] grid-cols-4 gap-3 px-5 py-[10px] text-center text-[11px] leading-[17.6px]">
|
||||
<BottomNavIcon
|
||||
to="/"
|
||||
label={t("home")}
|
||||
@@ -764,9 +764,8 @@ function BottomNavIcon({
|
||||
icon: "home" | "document" | "bookmark" | "update";
|
||||
active: boolean;
|
||||
}) {
|
||||
const src = active
|
||||
? `${NAVBAR_ICON_BASE}/${icon}-active.svg`
|
||||
: `${NAVBAR_ICON_BASE}/${icon}-inactive.svg`;
|
||||
const activeSrc = `${NAVBAR_ICON_BASE}/${icon}-active.svg`;
|
||||
const inactiveSrc = `${NAVBAR_ICON_BASE}/${icon}-inactive.svg`;
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
@@ -775,15 +774,30 @@ function BottomNavIcon({
|
||||
active ? "text-ark-gold" : "text-[#908F92]",
|
||||
].join(" ")}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
className="mx-auto h-6 w-6 object-contain"
|
||||
width={24}
|
||||
height={24}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<span className="relative h-6 w-6" aria-hidden>
|
||||
<img
|
||||
src={inactiveSrc}
|
||||
alt=""
|
||||
className={`absolute inset-0 h-6 w-6 object-contain ${
|
||||
active ? "opacity-0" : "opacity-100"
|
||||
}`}
|
||||
width={24}
|
||||
height={24}
|
||||
loading="eager"
|
||||
decoding="sync"
|
||||
/>
|
||||
<img
|
||||
src={activeSrc}
|
||||
alt=""
|
||||
className={`absolute inset-0 h-6 w-6 object-contain ${
|
||||
active ? "opacity-100" : "opacity-0"
|
||||
}`}
|
||||
width={24}
|
||||
height={24}
|
||||
loading="eager"
|
||||
decoding="sync"
|
||||
/>
|
||||
</span>
|
||||
<span className="leading-tight">{label}</span>
|
||||
</Link>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user