terry-wallet-login #15

Merged
terry merged 95 commits from terry-wallet-login into terry-staging 2026-06-05 16:32:43 +00:00
Showing only changes of commit b19486e908 - Show all commits

View File

@@ -1,6 +1,6 @@
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Link, useLocation } from "react-router-dom";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { getJSON, itemsOrEmpty, readJSONCache, type Category } from "../../api";
import { CategoryIcon } from "../../components/CategoryIcon";
import { FigmaBanner } from "../../components/FigmaBanner";
@@ -41,6 +41,60 @@ function figmaCategoryRank(category: Category): number {
return index === -1 ? FIGMA_CATEGORY_ORDER.length : index;
}
type LatestPostColumnItem = {
post: Post;
originalIndex: number;
};
function estimateLatestPostHeight(post: Post): number {
const textLength = (post.text ?? post.title ?? "").length;
const textRows = Math.ceil(textLength / 72);
const textHeight = Math.min(180, Math.max(0, textRows * 22));
const previewHeight = post.linkPreview ? 132 : 0;
const [firstAttachment] = post.attachments;
if (!firstAttachment) return 72 + textHeight + previewHeight;
if (post.attachments.length >= 2) {
const mediaHeight = firstAttachment.kind === "video" ? 340 : 300;
return mediaHeight + textHeight + previewHeight + 42;
}
if (firstAttachment.kind === "video") {
return 300 + textHeight + previewHeight + 42;
}
if (firstAttachment.kind === "image") {
return (post.text ? 300 : 260) + textHeight + previewHeight + 42;
}
return 96 + post.attachments.length * 72 + textHeight + previewHeight;
}
function splitLatestPostsIntoColumns(
posts: Post[],
columnCount: number,
): LatestPostColumnItem[][] {
const safeColumnCount = Math.max(1, columnCount);
const columns = Array.from(
{ length: safeColumnCount },
() => [] as LatestPostColumnItem[],
);
const columnHeights = Array.from({ length: safeColumnCount }, () => 0);
posts.forEach((post, originalIndex) => {
const targetColumn =
originalIndex < safeColumnCount
? originalIndex
: columnHeights.indexOf(Math.min(...columnHeights));
columns[targetColumn].push({ post, originalIndex });
columnHeights[targetColumn] += estimateLatestPostHeight(post) + 16;
});
return columns;
}
export function Home() {
const { t, lang } = useI18n();
const lp = useLocalizedPath();
@@ -64,6 +118,27 @@ export function Home() {
const [activeCategoryPage, setActiveCategoryPage] = useState(0);
const [canScrollRec, setCanScrollRec] = useState(false);
const [recScroll, setRecScroll] = useState({ ratio: 1, progress: 0 });
const [latestDesktopColumnCount, setLatestDesktopColumnCount] = useState(
() =>
typeof window !== "undefined" &&
typeof window.matchMedia === "function" &&
window.matchMedia("(min-width: 1024px)").matches
? 3
: 2,
);
useEffect(() => {
if (typeof window.matchMedia !== "function") return;
const media = window.matchMedia("(min-width: 1024px)");
const updateColumnCount = () => {
setLatestDesktopColumnCount(media.matches ? 3 : 2);
};
updateColumnCount();
media.addEventListener("change", updateColumnCount);
return () => media.removeEventListener("change", updateColumnCount);
}, []);
useEffect(() => {
let cancelled = false;
@@ -73,7 +148,7 @@ export function Home() {
const postQ = `?lang=${langParam}&language=${languageParam}`;
const categoriesUrl = `/api/categories${catQ}`;
const recommendedUrl = `/api/posts/recommended${postQ}&limit=12`;
const latestUrl = `/api/posts${postQ}&sort=latest&limit=12`;
const latestUrl = `/api/posts${postQ}&sort=latest&limit=9`;
const popularUrl = `/api/posts${postQ}&sort=popular&limit=5`;
const applyHomeData = (
@@ -264,6 +339,10 @@ export function Home() {
Math.round(recScroll.progress * (recommendedDotCount - 1)),
)
: 0;
const latestDesktopColumns = useMemo(
() => splitLatestPostsIntoColumns(latestPosts, latestDesktopColumnCount),
[latestPosts, latestDesktopColumnCount],
);
// Hide the arrow that points to an edge we're already at.
const recAtStart = recScroll.progress <= 0.01;
const recAtEnd = recScroll.progress >= 0.99;
@@ -481,18 +560,23 @@ export function Home() {
</Reveal>
))}
</div>
{/* Desktop: masonry that matches the Figma layout. 2 columns at
md (with horizontal padding so cards don't kiss the screen
edge), 3 columns at lg+. */}
<div className="mt-7 hidden gap-4 px-4 md:block md:columns-2 lg:columns-3 lg:px-0">
{latestPosts.map((post, index) => (
<Reveal
key={post.id}
delay={Math.min(index, 8) * 0.05}
className="mb-4 break-inside-avoid"
{/* Desktop: explicit balanced columns avoid the uneven gaps that
CSS multi-column masonry can create with variable-height cards. */}
<div className="mt-7 hidden gap-4 px-4 md:grid md:grid-cols-2 lg:grid-cols-3 lg:px-0">
{latestDesktopColumns.map((column, columnIndex) => (
<div
key={`latest-desktop-column-${columnIndex}`}
className="flex min-w-0 flex-col gap-4"
>
<MessageBubble post={post} fluid />
</Reveal>
{column.map(({ post, originalIndex }) => (
<Reveal
key={post.id}
delay={Math.min(originalIndex, 8) * 0.05}
>
<MessageBubble post={post} fluid />
</Reveal>
))}
</div>
))}
</div>
</div>