feat: chat

This commit is contained in:
gosangam
2026-08-29 03:49:58 +05:30
parent 3257efb06a
commit 523b6fa7e8
34 changed files with 5381 additions and 11 deletions
+103
View File
@@ -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}
/>
);
}