feat: scroll to post bubble from recommended card + back-to-top button
Some checks failed
Deploy to Frontend Servers / deploy (push) Failing after 14s

Recommended cards already routed to /browse#post-<id>, but the stream had
no logic to scroll to the target bubble — and the post might not be paged
in yet. MessageStream now resolves the #post-<id> hash, auto-loads more
pages until the bubble renders, scrolls to it, and gives it a brief gold
highlight. Bubbles get scroll-mt so they clear the sticky header.

Also adds a global floating back-to-top button (BackToTop) mounted in
PublicLayout, shown after scrolling past 400px.

Bundles related staging UI work already present in the working tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
TerryM
2026-05-29 11:50:27 +08:00
parent 8e36894851
commit 88a25b6ad4
27 changed files with 748 additions and 139 deletions

View File

@@ -1,13 +1,18 @@
import { useI18n } from "../../i18n";
import { Reveal } from "../../motion";
export function AboutPage() {
const { t } = useI18n();
return (
<div className="mx-auto max-w-2xl space-y-6">
<h1 className="text-2xl font-bold">{t("aboutTitle")}</h1>
<p className="text-neutral-300 leading-relaxed whitespace-pre-line">
{t("aboutIntro")}
</p>
<Reveal>
<h1 className="text-2xl font-bold">{t("aboutTitle")}</h1>
</Reveal>
<Reveal delay={0.08}>
<p className="text-neutral-300 leading-relaxed whitespace-pre-line">
{t("aboutIntro")}
</p>
</Reveal>
</div>
);
}

View File

@@ -3,8 +3,10 @@ import { Link } from "react-router-dom";
import { getJSON, itemsOrEmpty, readJSONCache, type Category } from "../../api";
import { CategoryIcon } from "../../components/CategoryIcon";
import { SectionHeader } from "../../components/SectionHeader";
import { Skeleton } from "../../components/Skeleton";
import { langQuery, useI18n } from "../../i18n";
import { cleanCategoryDisplayName } from "../../utils/categoryDisplay";
import { Reveal } from "../../motion";
const FIGMA_CATEGORY_ORDER = [
"project-ppt",
@@ -70,26 +72,40 @@ export function CategoriesPage() {
);
}
const isLoading = cats.length === 0;
return (
<section>
<SectionHeader title={t("categorySection")} />
<div className="mt-7 grid grid-cols-3 gap-2 md:grid-cols-5 md:gap-3 lg:grid-cols-6 xl:grid-cols-7 xl:gap-4">
{cats.map((category) => (
<Link
key={category.id}
to={`/category/${encodeURIComponent(category.slug)}`}
className="flex h-[88px] min-w-0 flex-col items-center justify-center gap-2 rounded-xl border border-[#27292E] bg-[#1D1E23] px-4 py-3 text-center outline-none transition hover:border-ark-gold/55 hover:bg-[#252630] focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg"
>
<CategoryIcon
iconKey={category.iconKey}
categorySlug={category.slug}
className="h-9 w-9 shrink-0 text-ark-gold"
/>
<div className="w-full text-center text-[13px] font-medium leading-[19.5px] text-white line-clamp-2">
{cleanCategoryDisplayName(category.name)}
</div>
</Link>
))}
{isLoading
? Array.from({ length: 14 }).map((_, i) => (
<Skeleton
key={i}
className="h-[88px] rounded-xl"
/>
))
: cats.map((category, index) => (
<Reveal
key={category.id}
delay={Math.min(index, 8) * 0.05}
className="h-[88px]"
>
<Link
to={`/category/${encodeURIComponent(category.slug)}`}
className="flex h-[88px] min-w-0 flex-col items-center justify-center gap-2 rounded-xl border border-[#27292E] bg-[#1D1E23] px-4 py-3 text-center outline-none transition hover:border-ark-gold/55 hover:bg-[#252630] focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg"
>
<CategoryIcon
iconKey={category.iconKey}
categorySlug={category.slug}
className="h-9 w-9 shrink-0 text-ark-gold"
/>
<div className="w-full text-center text-[13px] font-medium leading-[19.5px] text-white line-clamp-2">
{cleanCategoryDisplayName(category.name)}
</div>
</Link>
</Reveal>
))}
</div>
</section>
);

View File

@@ -1,12 +1,13 @@
import { Heart } from "lucide-react";
import { Link } from "react-router-dom";
import { useI18n } from "../../i18n";
import { Reveal } from "../../motion";
export default function Favorites() {
const { t } = useI18n();
return (
<div className="flex min-h-[60vh] flex-col items-center justify-center gap-5 px-4 py-12 text-center">
<Reveal className="flex min-h-[60vh] flex-col items-center justify-center gap-5 px-4 py-12 text-center">
<div className="flex h-20 w-20 items-center justify-center rounded-full border border-ark-gold/30 bg-ark-gold/5">
<Heart
className="h-10 w-10 text-ark-gold/70"
@@ -33,6 +34,6 @@ export default function Favorites() {
>
{t("backToHome")}
</Link>
</div>
</Reveal>
);
}

View File

@@ -19,6 +19,7 @@ import {
type PostBackedResource,
} from "../../utils/postResourceAdapter";
import type { Post } from "../../types/post";
import { Reveal } from "../../motion";
const FIGMA_CATEGORY_ORDER = [
"project-ppt",
@@ -269,6 +270,7 @@ export function Home() {
<FigmaBanner />
</section>
<Reveal delay={0}>
<section id="categories" className="scroll-mt-16 md:scroll-mt-24">
<div className="px-4 md:px-0">
<SectionHeader
@@ -342,25 +344,28 @@ export function Home() {
</div>
<div className="mt-7 hidden grid-cols-3 gap-3 min-[440px]:gap-3.5 md:grid md:grid-cols-5 md:gap-3 lg:grid-cols-6 xl:grid-cols-7 xl:gap-4">
{figmaOrderedCategories.map((c) => (
<Link
key={c.id}
to={`/category/${encodeURIComponent(c.slug)}`}
className="flex h-[88px] min-w-0 flex-col items-center justify-center gap-2 rounded-xl border border-[#27292E] bg-[#1D1E23] px-4 py-3 text-center outline-none transition hover:border-ark-gold/55 hover:bg-[#252630] focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg"
>
<CategoryIcon
iconKey={c.iconKey}
categorySlug={c.slug}
className="h-9 w-9 shrink-0 text-ark-gold"
/>
<div className="w-full text-center text-[13px] font-medium leading-[19.5px] text-white line-clamp-2">
{cleanCategoryDisplayName(c.name)}
</div>
</Link>
{figmaOrderedCategories.map((c, index) => (
<Reveal key={c.id} delay={Math.min(index, 8) * 0.05}>
<Link
to={`/category/${encodeURIComponent(c.slug)}`}
className="flex h-[88px] min-w-0 flex-col items-center justify-center gap-2 rounded-xl border border-[#27292E] bg-[#1D1E23] px-4 py-3 text-center outline-none transition hover:border-ark-gold/55 hover:bg-[#252630] focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg"
>
<CategoryIcon
iconKey={c.iconKey}
categorySlug={c.slug}
className="h-9 w-9 shrink-0 text-ark-gold"
/>
<div className="w-full text-center text-[13px] font-medium leading-[19.5px] text-white line-clamp-2">
{cleanCategoryDisplayName(c.name)}
</div>
</Link>
</Reveal>
))}
</div>
</section>
</Reveal>
<Reveal>
<section id="official" className="scroll-mt-16 md:scroll-mt-24">
<div className="px-4 md:px-0">
<SectionHeader
@@ -376,7 +381,9 @@ export function Home() {
>
{rec.map((r, index) => (
<div key={r.id}>
<RecommendedCard r={r} visualIndex={index} useFigmaDesign />
<Reveal delay={Math.min(index, 8) * 0.05}>
<RecommendedCard r={r} visualIndex={index} useFigmaDesign />
</Reveal>
</div>
))}
</div>
@@ -435,7 +442,9 @@ export function Home() {
) : null}
</div>
</section>
</Reveal>
<Reveal>
<section id="latest" className="scroll-mt-16 md:scroll-mt-24">
<div className="px-4 md:px-0">
<SectionHeader
@@ -490,8 +499,10 @@ export function Home() {
) : null}
</div>
</section>
</Reveal>
{hasPopular ? (
<Reveal>
<section id="popular" className="scroll-mt-16 md:scroll-mt-24">
<div className="px-4 md:px-0">
<SectionHeader
@@ -521,6 +532,7 @@ export function Home() {
))}
</div>
</section>
</Reveal>
) : null}
</div>
);