feat: chat
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* The media tiles inside a bubble.
|
||||
*
|
||||
* One item fills the bubble; several tile into a 2×2. Beyond four, the fourth
|
||||
* tile carries a "+N more" overlay rather than growing the bubble — a message
|
||||
* with twenty photos should still be one glanceable block.
|
||||
*
|
||||
* Every tile is square so the grid stays even regardless of what was sent;
|
||||
* portrait and landscape both crop to the same cell.
|
||||
*/
|
||||
|
||||
import { Play } from "lucide-react";
|
||||
import { attachmentUrl, type Attachment } from "@/lib/chat";
|
||||
|
||||
/** Tiles shown before the rest collapse into the "+N more" overlay. */
|
||||
const MAX_TILES = 4;
|
||||
|
||||
export function MediaGrid({
|
||||
items,
|
||||
onOpen,
|
||||
}: {
|
||||
items: Attachment[];
|
||||
/** Opens the lightbox at this index. */
|
||||
onOpen: (index: number) => void;
|
||||
}) {
|
||||
if (items.length === 0) return null;
|
||||
|
||||
const visible = items.slice(0, MAX_TILES);
|
||||
const hidden = items.length - visible.length;
|
||||
|
||||
if (items.length === 1) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpen(0)}
|
||||
className="block overflow-hidden rounded-lg"
|
||||
>
|
||||
<Tile item={items[0]} className="max-h-72 w-auto" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// Two columns throughout: two items make a row, three or four make the
|
||||
// 2×2. A three-item grid deliberately leaves the fourth cell empty rather
|
||||
// than stretching one tile — an uneven crop reads worse than a gap.
|
||||
<div className="grid w-56 grid-cols-2 gap-0.5 overflow-hidden rounded-lg sm:w-64">
|
||||
{visible.map((item, i) => {
|
||||
const isLast = i === visible.length - 1;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => onOpen(i)}
|
||||
className="relative aspect-square overflow-hidden"
|
||||
>
|
||||
<Tile item={item} className="size-full object-cover" />
|
||||
{isLast && hidden > 0 && (
|
||||
<span className="absolute inset-0 flex items-center justify-center bg-black/60 text-sm font-medium text-white">
|
||||
+{hidden} more
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Tile({ item, className }: { item: Attachment; className?: string }) {
|
||||
if (item.kind === "video") {
|
||||
return (
|
||||
<span className="relative block size-full">
|
||||
<video
|
||||
src={attachmentUrl(item.id)}
|
||||
// Just the poster frame: the lightbox is where it plays.
|
||||
preload="metadata"
|
||||
muted
|
||||
playsInline
|
||||
className={className}
|
||||
/>
|
||||
<span className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="rounded-full bg-black/50 p-2">
|
||||
<Play className="size-4 fill-white text-white" />
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={attachmentUrl(item.id)}
|
||||
alt={item.file_name ?? "Shared media"}
|
||||
loading="lazy"
|
||||
width={item.width ?? undefined}
|
||||
height={item.height ?? undefined}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user