feat: apply figma browse mobile redesign
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { ArrowDownToLine, LoaderCircle, X } from "lucide-react";
|
||||
import { LoaderCircle, X } from "lucide-react";
|
||||
import { DownloadCloudIcon } from "../../icons/DownloadCloudIcon";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useI18n } from "../../../i18n";
|
||||
@@ -12,8 +13,15 @@ import { postDisplayText } from "../utils/postText";
|
||||
|
||||
const MAX_VISIBLE = 4;
|
||||
|
||||
function imageRatio(att: Attachment) {
|
||||
return att.width && att.height ? `${att.width} / ${att.height}` : "4 / 3";
|
||||
function albumGridClass(count: number) {
|
||||
const height = "h-[230px] min-[440px]:h-[250px] md:h-[300px] lg:h-[340px]";
|
||||
if (count === 2) return `${height} grid grid-cols-1 grid-rows-2`;
|
||||
return `${height} grid grid-cols-2 grid-rows-2`;
|
||||
}
|
||||
|
||||
function albumItemClass(index: number, count: number) {
|
||||
if (count === 3 && index === 0) return "row-span-2";
|
||||
return "";
|
||||
}
|
||||
|
||||
function ImageListDownloadButton({
|
||||
@@ -51,7 +59,7 @@ function ImageListDownloadButton({
|
||||
{isDownloading ? (
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" strokeWidth={2.3} />
|
||||
) : (
|
||||
<ArrowDownToLine className="h-4 w-4" strokeWidth={2.3} />
|
||||
<DownloadCloudIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
@@ -142,54 +150,22 @@ export function AlbumBubble({ post }: { post: Post }) {
|
||||
const [listOpen, setListOpen] = useState(false);
|
||||
const images = post.attachments;
|
||||
const text = postDisplayText(post, lang);
|
||||
const shouldMerge = images.length > MAX_VISIBLE;
|
||||
|
||||
if (!shouldMerge) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{images.map((att, i) => (
|
||||
<div
|
||||
key={att.id}
|
||||
className="relative max-h-[180px] w-full overflow-hidden rounded-xl min-[440px]:max-h-[200px] md:max-h-[240px] lg:max-h-[280px]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openLightbox(images, i, text, post.id)}
|
||||
className="block w-full"
|
||||
aria-label={att.filename}
|
||||
>
|
||||
<img
|
||||
src={att.url}
|
||||
alt={att.filename}
|
||||
loading="lazy"
|
||||
className="h-full w-full object-cover"
|
||||
style={{ aspectRatio: imageRatio(att) }}
|
||||
/>
|
||||
</button>
|
||||
<AttachmentDownloadPill postId={post.id} attachment={att} />
|
||||
</div>
|
||||
))}
|
||||
{text ? (
|
||||
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-snug text-neutral-100">
|
||||
{autolink(text)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const visible = images.slice(0, MAX_VISIBLE);
|
||||
const extra = images.length - MAX_VISIBLE;
|
||||
const layoutCount = Math.min(images.length, MAX_VISIBLE);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="grid h-[220px] grid-cols-2 grid-rows-2 gap-[2px] overflow-hidden rounded-xl min-[440px]:h-[250px] md:h-[300px] lg:h-[340px]">
|
||||
<div className="flex flex-col">
|
||||
<div className={`${albumGridClass(layoutCount)} gap-px overflow-hidden`}>
|
||||
{visible.map((att, i) => {
|
||||
const isLastSlot = i === MAX_VISIBLE - 1 && extra > 0;
|
||||
return (
|
||||
<div
|
||||
key={att.id}
|
||||
className="relative h-full w-full overflow-hidden"
|
||||
className={`relative h-full w-full overflow-hidden ${albumItemClass(
|
||||
i,
|
||||
layoutCount,
|
||||
)}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -204,12 +180,10 @@ export function AlbumBubble({ post }: { post: Post }) {
|
||||
src={att.thumbnailUrl ?? att.url}
|
||||
alt={att.filename}
|
||||
loading="lazy"
|
||||
className={`h-full w-full object-cover ${
|
||||
isLastSlot ? "blur-sm scale-105" : ""
|
||||
}`}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
{isLastSlot ? (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/45 text-3xl font-semibold text-white">
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/55 text-4xl font-bold text-white">
|
||||
+{extra}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -222,7 +196,7 @@ export function AlbumBubble({ post }: { post: Post }) {
|
||||
})}
|
||||
</div>
|
||||
{text ? (
|
||||
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-snug text-neutral-100">
|
||||
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words px-4 pt-3 text-[14px] leading-6 text-neutral-100">
|
||||
{autolink(text)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArrowDownToLine, LoaderCircle } from "lucide-react";
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
import { DownloadCloudIcon } from "../../icons/DownloadCloudIcon";
|
||||
import { useState } from "react";
|
||||
import { useI18n } from "../../../i18n";
|
||||
import type { Attachment, Post } from "../../../types/post";
|
||||
@@ -56,7 +57,7 @@ function AttachmentRow({ postId, att }: { postId: string; att: Attachment }) {
|
||||
{isDownloading ? (
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" strokeWidth={2.3} />
|
||||
) : (
|
||||
<ArrowDownToLine className="h-4 w-4" strokeWidth={2.3} />
|
||||
<DownloadCloudIcon className="h-4 w-4" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -6,15 +6,12 @@ export function ImageBubble({ post }: { post: Post }) {
|
||||
const { openLightbox } = useLightbox();
|
||||
const att = post.attachments[0];
|
||||
if (!att) return null;
|
||||
const ratio =
|
||||
att.width && att.height ? `${att.width} / ${att.height}` : "4 / 3";
|
||||
|
||||
return (
|
||||
<div className="relative w-full overflow-hidden rounded-xl max-h-[240px] min-[440px]:max-h-[270px] md:max-h-[320px] lg:max-h-[360px]">
|
||||
<div className="relative h-[180px] w-full overflow-hidden bg-black min-[440px]:h-[210px] md:h-[260px] lg:h-[300px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openLightbox([att], 0, undefined, post.id)}
|
||||
className="block w-full"
|
||||
className="block h-full w-full"
|
||||
aria-label={att.filename}
|
||||
>
|
||||
<img
|
||||
@@ -22,7 +19,6 @@ export function ImageBubble({ post }: { post: Post }) {
|
||||
alt={att.filename}
|
||||
loading="lazy"
|
||||
className="h-full w-full object-cover"
|
||||
style={{ aspectRatio: ratio }}
|
||||
/>
|
||||
</button>
|
||||
<AttachmentDownloadPill postId={post.id} attachment={att} />
|
||||
|
||||
@@ -11,31 +11,27 @@ export function ImageWithTextBubble({ post }: { post: Post }) {
|
||||
const att = post.attachments[0];
|
||||
const text = postDisplayText(post, lang);
|
||||
if (!att) return null;
|
||||
const ratio =
|
||||
att.width && att.height ? `${att.width} / ${att.height}` : "4 / 3";
|
||||
|
||||
return (
|
||||
<div className="relative overflow-hidden rounded-xl bg-black/20">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openLightbox([att], 0, text, post.id)}
|
||||
className="block w-full"
|
||||
aria-label={att.filename}
|
||||
>
|
||||
<img
|
||||
src={att.url}
|
||||
alt={att.filename}
|
||||
loading="lazy"
|
||||
className="block h-auto w-full"
|
||||
style={{ aspectRatio: ratio }}
|
||||
/>
|
||||
</button>
|
||||
<AttachmentDownloadPill postId={post.id} attachment={att} />
|
||||
<div className="flex flex-col">
|
||||
<div className="relative h-[180px] w-full overflow-hidden bg-black min-[440px]:h-[210px] md:h-[260px] lg:h-[300px]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openLightbox([att], 0, text, post.id)}
|
||||
className="block h-full w-full"
|
||||
aria-label={att.filename}
|
||||
>
|
||||
<img
|
||||
src={att.url}
|
||||
alt={att.filename}
|
||||
loading="lazy"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
<AttachmentDownloadPill postId={post.id} attachment={att} />
|
||||
</div>
|
||||
{text ? (
|
||||
<div className="bg-gradient-to-b from-ark-panel/90 to-ark-panel px-4 py-3 text-[14px] leading-snug text-neutral-100">
|
||||
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words">
|
||||
{autolink(text)}
|
||||
</div>
|
||||
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words px-4 pt-3 text-[14px] leading-6 text-neutral-100">
|
||||
{autolink(text)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ArrowDownToLine, LoaderCircle, Play, X } from "lucide-react";
|
||||
import { LoaderCircle, Play, X } from "lucide-react";
|
||||
import { DownloadCloudIcon } from "../../icons/DownloadCloudIcon";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useI18n } from "../../../i18n";
|
||||
@@ -12,6 +13,17 @@ import { postDisplayText } from "../utils/postText";
|
||||
|
||||
const MAX_VISIBLE = 4;
|
||||
|
||||
function videoGridClass(count: number) {
|
||||
const height = "h-[230px] min-[440px]:h-[250px] md:h-[300px] lg:h-[340px]";
|
||||
if (count === 2) return `${height} grid grid-cols-1 grid-rows-2`;
|
||||
return `${height} grid grid-cols-2 grid-rows-2`;
|
||||
}
|
||||
|
||||
function videoItemClass(index: number, count: number) {
|
||||
if (count === 3 && index === 0) return "row-span-2";
|
||||
return "";
|
||||
}
|
||||
|
||||
function formatDuration(sec: number | undefined): string {
|
||||
if (!sec || sec <= 0) return "";
|
||||
const m = Math.floor(sec / 60);
|
||||
@@ -54,7 +66,7 @@ function VideoAttachmentCard({
|
||||
className={`relative w-full overflow-hidden bg-black ${
|
||||
compact
|
||||
? "h-full"
|
||||
: "max-h-[220px] rounded-xl min-[440px]:max-h-[250px] md:max-h-[300px] lg:max-h-[340px]"
|
||||
: "h-[180px] min-[440px]:h-[210px] md:h-[260px] lg:h-[300px]"
|
||||
}`}
|
||||
style={compact ? undefined : { aspectRatio: videoRatio(attachment) }}
|
||||
onClick={() => {
|
||||
@@ -180,7 +192,7 @@ function AttachmentListDownloadButton({
|
||||
{isDownloading ? (
|
||||
<LoaderCircle className="h-4 w-4 animate-spin" strokeWidth={2.3} />
|
||||
) : (
|
||||
<ArrowDownToLine className="h-4 w-4" strokeWidth={2.3} />
|
||||
<DownloadCloudIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
@@ -297,32 +309,38 @@ export function VideoBubble({ post }: { post: Post }) {
|
||||
const [listOpen, setListOpen] = useState(false);
|
||||
const videos = post.attachments.filter(isVideoAttachment);
|
||||
const text = postDisplayText(post, lang);
|
||||
const shouldMerge = videos.length > MAX_VISIBLE;
|
||||
if (!videos.length) return null;
|
||||
|
||||
if (shouldMerge) {
|
||||
if (videos.length >= 2) {
|
||||
const visible = videos.slice(0, MAX_VISIBLE);
|
||||
const extra = videos.length - MAX_VISIBLE;
|
||||
const layoutCount = Math.min(videos.length, MAX_VISIBLE);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="grid h-[220px] grid-cols-2 grid-rows-2 gap-[2px] overflow-hidden rounded-xl bg-black min-[440px]:h-[250px] md:h-[300px] lg:h-[340px]">
|
||||
<div className="flex flex-col">
|
||||
<div
|
||||
className={`${videoGridClass(layoutCount)} gap-px overflow-hidden bg-black`}
|
||||
>
|
||||
{visible.map((att, i) => {
|
||||
const isLastSlot = i === MAX_VISIBLE - 1 && extra > 0;
|
||||
return (
|
||||
<VideoAttachmentCard
|
||||
<div
|
||||
key={att.id}
|
||||
postId={post.id}
|
||||
attachment={att}
|
||||
compact
|
||||
overlayCount={isLastSlot ? extra : undefined}
|
||||
onMoreClick={() => setListOpen(true)}
|
||||
/>
|
||||
className={`h-full w-full ${videoItemClass(i, layoutCount)}`}
|
||||
>
|
||||
<VideoAttachmentCard
|
||||
postId={post.id}
|
||||
attachment={att}
|
||||
compact
|
||||
overlayCount={isLastSlot ? extra : undefined}
|
||||
onMoreClick={() => setListOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{text ? (
|
||||
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-snug text-neutral-100">
|
||||
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words px-4 pt-3 text-[14px] leading-6 text-neutral-100">
|
||||
{autolink(text)}
|
||||
</div>
|
||||
) : null}
|
||||
@@ -342,12 +360,10 @@ export function VideoBubble({ post }: { post: Post }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{videos.map((att) => (
|
||||
<VideoAttachmentCard key={att.id} postId={post.id} attachment={att} />
|
||||
))}
|
||||
<div className="flex flex-col">
|
||||
<VideoAttachmentCard postId={post.id} attachment={videos[0]} />
|
||||
{text ? (
|
||||
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words text-[14px] leading-snug text-neutral-100">
|
||||
<div className="message-stream-copyable-text select-text whitespace-pre-wrap break-words px-4 pt-3 text-[14px] leading-6 text-neutral-100">
|
||||
{autolink(text)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user