Merge branch 'main' of https://repo.skywalker-rs.com/terry/Arkie-Library-Frontend into terry-wallet-login
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
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 { getJSON, itemsOrEmpty, readJSONCache, type Category } from "../../api";
|
||||||
import { CategoryIcon } from "../../components/CategoryIcon";
|
import { CategoryIcon } from "../../components/CategoryIcon";
|
||||||
import { FigmaBanner } from "../../components/FigmaBanner";
|
import { FigmaBanner } from "../../components/FigmaBanner";
|
||||||
@@ -41,6 +41,60 @@ function figmaCategoryRank(category: Category): number {
|
|||||||
return index === -1 ? FIGMA_CATEGORY_ORDER.length : index;
|
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() {
|
export function Home() {
|
||||||
const { t, lang } = useI18n();
|
const { t, lang } = useI18n();
|
||||||
const lp = useLocalizedPath();
|
const lp = useLocalizedPath();
|
||||||
@@ -64,6 +118,27 @@ export function Home() {
|
|||||||
const [activeCategoryPage, setActiveCategoryPage] = useState(0);
|
const [activeCategoryPage, setActiveCategoryPage] = useState(0);
|
||||||
const [canScrollRec, setCanScrollRec] = useState(false);
|
const [canScrollRec, setCanScrollRec] = useState(false);
|
||||||
const [recScroll, setRecScroll] = useState({ ratio: 1, progress: 0 });
|
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(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -73,7 +148,7 @@ export function Home() {
|
|||||||
const postQ = `?lang=${langParam}&language=${languageParam}`;
|
const postQ = `?lang=${langParam}&language=${languageParam}`;
|
||||||
const categoriesUrl = `/api/categories${catQ}`;
|
const categoriesUrl = `/api/categories${catQ}`;
|
||||||
const recommendedUrl = `/api/posts/recommended${postQ}&limit=12`;
|
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 popularUrl = `/api/posts${postQ}&sort=popular&limit=5`;
|
||||||
|
|
||||||
const applyHomeData = (
|
const applyHomeData = (
|
||||||
@@ -264,6 +339,10 @@ export function Home() {
|
|||||||
Math.round(recScroll.progress * (recommendedDotCount - 1)),
|
Math.round(recScroll.progress * (recommendedDotCount - 1)),
|
||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
|
const latestDesktopColumns = useMemo(
|
||||||
|
() => splitLatestPostsIntoColumns(latestPosts, latestDesktopColumnCount),
|
||||||
|
[latestPosts, latestDesktopColumnCount],
|
||||||
|
);
|
||||||
// Hide the arrow that points to an edge we're already at.
|
// Hide the arrow that points to an edge we're already at.
|
||||||
const recAtStart = recScroll.progress <= 0.01;
|
const recAtStart = recScroll.progress <= 0.01;
|
||||||
const recAtEnd = recScroll.progress >= 0.99;
|
const recAtEnd = recScroll.progress >= 0.99;
|
||||||
@@ -481,20 +560,25 @@ export function Home() {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* Desktop: masonry that matches the Figma layout. 2 columns at
|
{/* Desktop: explicit balanced columns avoid the uneven gaps that
|
||||||
md (with horizontal padding so cards don't kiss the screen
|
CSS multi-column masonry can create with variable-height cards. */}
|
||||||
edge), 3 columns at lg+. */}
|
<div className="mt-7 hidden gap-4 px-4 md:grid md:grid-cols-2 lg:grid-cols-3 lg:px-0">
|
||||||
<div className="mt-7 hidden gap-4 px-4 md:block md:columns-2 lg:columns-3 lg:px-0">
|
{latestDesktopColumns.map((column, columnIndex) => (
|
||||||
{latestPosts.map((post, index) => (
|
<div
|
||||||
|
key={`latest-desktop-column-${columnIndex}`}
|
||||||
|
className="flex min-w-0 flex-col gap-4"
|
||||||
|
>
|
||||||
|
{column.map(({ post, originalIndex }) => (
|
||||||
<Reveal
|
<Reveal
|
||||||
key={post.id}
|
key={post.id}
|
||||||
delay={Math.min(index, 8) * 0.05}
|
delay={Math.min(originalIndex, 8) * 0.05}
|
||||||
className="mb-4 break-inside-avoid"
|
|
||||||
>
|
>
|
||||||
<MessageBubble post={post} fluid />
|
<MessageBubble post={post} fluid />
|
||||||
</Reveal>
|
</Reveal>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|||||||
Reference in New Issue
Block a user