feat: polish figma mobile home and nav
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
} from "../../components/LatestUpdateRow";
|
||||
import { RecommendedCard } from "../../components/RecommendedCard";
|
||||
import { SectionHeader } from "../../components/SectionHeader";
|
||||
import { MessageBubble } from "../../components/messageStream/MessageBubble";
|
||||
import { langQuery, useI18n } from "../../i18n";
|
||||
import { sourceLanguageQuery } from "../../i18nLanguages";
|
||||
import { categoryCardLines } from "../../utils/categoryDisplay";
|
||||
@@ -24,6 +25,7 @@ export function Home() {
|
||||
const [cats, setCats] = useState<Category[]>([]);
|
||||
const [rec, setRec] = useState<PostBackedResource[]>([]);
|
||||
const [latest, setLatest] = useState<PostBackedResource[]>([]);
|
||||
const [latestPosts, setLatestPosts] = useState<Post[]>([]);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const recRowRef = useRef<HTMLDivElement>(null);
|
||||
const categoryRowRef = useRef<HTMLDivElement>(null);
|
||||
@@ -48,8 +50,10 @@ export function Home() {
|
||||
postToResource(post, lang, itemsOrEmpty(c)),
|
||||
),
|
||||
);
|
||||
const latestItems = itemsOrEmpty(l.items);
|
||||
setLatestPosts(latestItems);
|
||||
setLatest(
|
||||
itemsOrEmpty(l.items).map((post) =>
|
||||
latestItems.map((post) =>
|
||||
postToResource(post, lang, itemsOrEmpty(c)),
|
||||
),
|
||||
);
|
||||
@@ -64,6 +68,10 @@ export function Home() {
|
||||
for (let index = 0; index < cats.length; index += 9) {
|
||||
categoryPages.push(cats.slice(index, index + 9));
|
||||
}
|
||||
const activeCategoryCount = categoryPages[activeCategoryPage]?.length ?? 0;
|
||||
const activeCategoryRows = Math.ceil(activeCategoryCount / 3);
|
||||
const mobileCategoryHeight =
|
||||
activeCategoryRows * 88 + Math.max(0, activeCategoryRows - 1) * 8;
|
||||
|
||||
useEffect(() => {
|
||||
const row = categoryRowRef.current;
|
||||
@@ -119,6 +127,11 @@ export function Home() {
|
||||
};
|
||||
|
||||
const latestPlaceholderCount = Math.max(0, 5 - latest.length);
|
||||
const recommendedDotCount = Math.max(1, Math.min(4, rec.length || 4));
|
||||
const activeRecommendedDot = Math.min(
|
||||
recommendedDotCount - 1,
|
||||
Math.round(recScroll.progress * (recommendedDotCount - 1)),
|
||||
);
|
||||
|
||||
if (err) {
|
||||
return (
|
||||
@@ -129,28 +142,31 @@ export function Home() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-[30px] pb-10 md:space-y-10 md:pb-16 xl:space-y-[34px]">
|
||||
<section className="-mt-6 md:mt-0">
|
||||
<div className="pb-4 md:space-y-10 md:pb-16 xl:space-y-[34px]">
|
||||
<section className="md:mt-0">
|
||||
<FigmaBanner />
|
||||
</section>
|
||||
|
||||
<section id="categories" className="scroll-mt-24">
|
||||
<SectionHeader
|
||||
title={t("categorySection")}
|
||||
viewAllTo="/browse"
|
||||
viewAllLabel={t("viewAll")}
|
||||
/>
|
||||
<div className="px-4 md:px-0">
|
||||
<SectionHeader
|
||||
title={t("categorySection")}
|
||||
viewAllTo="/browse"
|
||||
viewAllLabel={t("viewAll")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="md:hidden">
|
||||
<div
|
||||
ref={categoryRowRef}
|
||||
className="flex snap-x snap-mandatory overflow-x-auto overflow-y-hidden scroll-smooth [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
className="flex snap-x snap-mandatory items-start overflow-x-auto overflow-y-hidden scroll-smooth transition-[height] duration-300 ease-out motion-reduce:transition-none [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
style={{ height: `${mobileCategoryHeight}px` }}
|
||||
aria-label={t("categorySection")}
|
||||
>
|
||||
{categoryPages.map((page, pageIndex) => (
|
||||
<div
|
||||
key={`category-page-${pageIndex}`}
|
||||
className="grid w-full shrink-0 snap-start grid-cols-3 gap-2"
|
||||
className="grid w-full shrink-0 snap-start grid-cols-3 gap-2 px-4"
|
||||
>
|
||||
{page.map((c) => (
|
||||
<Link
|
||||
@@ -233,16 +249,18 @@ export function Home() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<SectionHeader
|
||||
title={t("officialSection")}
|
||||
viewAllTo="/browse?sort=recommended"
|
||||
viewAllLabel={t("viewAll")}
|
||||
/>
|
||||
<div className="relative mt-7">
|
||||
<section id="official" className="scroll-mt-24">
|
||||
<div className="px-4 md:px-0">
|
||||
<SectionHeader
|
||||
title={t("officialSection")}
|
||||
viewAllTo="/browse?sort=recommended"
|
||||
viewAllLabel={t("viewAll")}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={recRowRef}
|
||||
className="flex gap-3 overflow-x-auto pb-5 pr-0 scroll-smooth snap-x snap-mandatory [-ms-overflow-style:none] [scrollbar-width:none] md:gap-4 [&::-webkit-scrollbar]:hidden"
|
||||
className="flex snap-x snap-mandatory gap-3 overflow-x-auto overflow-y-hidden px-4 pb-0 pr-4 scroll-smooth [-ms-overflow-style:none] [scrollbar-width:none] md:mt-7 md:gap-4 md:px-0 md:pb-5 [&::-webkit-scrollbar]:hidden"
|
||||
>
|
||||
{rec.map((r, index) => (
|
||||
<div key={r.id} className="snap-start">
|
||||
@@ -250,14 +268,38 @@ export function Home() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="h-1 overflow-hidden rounded-full bg-black/80 md:hidden">
|
||||
<div
|
||||
className="h-full rounded-full bg-ark-gold transition-transform duration-300 ease-out"
|
||||
style={{
|
||||
width: `${Math.round(recScroll.ratio * 100)}%`,
|
||||
transform: `translateX(${recScroll.progress * (100 / recScroll.ratio - 100)}%)`,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="flex h-[30px] items-center justify-center gap-1.5 md:hidden"
|
||||
aria-label="Recommended pagination"
|
||||
>
|
||||
{Array.from({ length: recommendedDotCount }).map((_, index) => (
|
||||
<button
|
||||
key={`recommended-dot-${index}`}
|
||||
type="button"
|
||||
aria-label={`Go to recommendation page ${index + 1}`}
|
||||
aria-current={activeRecommendedDot === index}
|
||||
onClick={() => {
|
||||
const row = recRowRef.current;
|
||||
if (!row) return;
|
||||
const maxScroll = Math.max(
|
||||
0,
|
||||
row.scrollWidth - row.clientWidth,
|
||||
);
|
||||
row.scrollTo({
|
||||
left:
|
||||
recommendedDotCount === 1
|
||||
? 0
|
||||
: (maxScroll * index) / (recommendedDotCount - 1),
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
className={`h-1.5 rounded-full transition-all ${
|
||||
activeRecommendedDot === index
|
||||
? "w-6 bg-ark-gold"
|
||||
: "w-1.5 bg-[#7C7C7C]"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{canScrollRec ? (
|
||||
<button
|
||||
@@ -272,13 +314,20 @@ export function Home() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<SectionHeader
|
||||
title={t("latestSection")}
|
||||
viewAllTo="/browse?sort=latest"
|
||||
viewAllLabel={t("viewAll")}
|
||||
/>
|
||||
<div className="mt-7 grid gap-3 min-[576px]:grid-cols-2 md:gap-4 lg:grid-cols-3 xl:grid-cols-5">
|
||||
<section id="latest" className="scroll-mt-24">
|
||||
<div className="px-4 md:px-0">
|
||||
<SectionHeader
|
||||
title={t("latestSection")}
|
||||
viewAllTo="/browse?sort=latest"
|
||||
viewAllLabel={t("viewAll")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 md:hidden">
|
||||
{latestPosts.slice(0, 4).map((post) => (
|
||||
<MessageBubble key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-7 hidden gap-3 min-[576px]:grid-cols-2 md:grid md:gap-4 lg:grid-cols-3 xl:grid-cols-5">
|
||||
{latest.map((r) => (
|
||||
<LatestUpdateRow key={r.id} r={r} iconKey={iconKeyForResource(r)} />
|
||||
))}
|
||||
@@ -290,6 +339,8 @@ export function Home() {
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<span id="popular" className="block scroll-mt-24" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user