feat(memories): 4-col grid, hover effects, folder label per tile, no blank tiles

- grid-cols-4 (~40% smaller than 3-col) with gap-1 and rounded-xl corners
- Hover: scale-110 image + dark overlay + expand icon (⤢)
- Subtle shadow + ring border on each tile
- Folder emoji + name shown below each tile (when assigned)
- Filter out tiles with no URL and remove tiles that fail to load (onError → null)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-18 22:45:23 +05:30
parent fb45250a27
commit 9c0afa2054

View file

@ -190,9 +190,10 @@ export default function MemoriesPage() {
}; };
const baseList = searchResults !== null ? searchResults : memories; const baseList = searchResults !== null ? searchResults : memories;
const displayMemories = activeFolder const displayMemories = (activeFolder
? baseList.filter(m => m.description === activeFolder) ? baseList.filter(m => m.description === activeFolder)
: baseList; : baseList
).filter(m => m.url || m.thumbnailUrl);
const folderCounts = memories.reduce<Record<string, number>>((acc, m) => { const folderCounts = memories.reduce<Record<string, number>>((acc, m) => {
if (m.description) acc[m.description] = (acc[m.description] || 0) + 1; if (m.description) acc[m.description] = (acc[m.description] || 0) + 1;
@ -269,9 +270,14 @@ export default function MemoriesPage() {
</p> </p>
</div> </div>
) : ( ) : (
<div className="grid grid-cols-3 gap-0.5"> <div className="grid grid-cols-4 gap-1 pt-1">
{displayMemories.map(mem => ( {displayMemories.map(mem => (
<MemoryTile key={mem.id} memory={mem} onClick={() => setSelected(mem)} /> <MemoryTile
key={mem.id}
memory={mem}
folder={allFolders.find(f => f.id === mem.description) ?? null}
onClick={() => setSelected(mem)}
/>
))} ))}
</div> </div>
)} )}
@ -389,26 +395,48 @@ export default function MemoriesPage() {
// ─── Grid tile ──────────────────────────────────────────────────────────────── // ─── Grid tile ────────────────────────────────────────────────────────────────
function MemoryTile({ memory, onClick }: { memory: Memory; onClick: () => void }) { function MemoryTile({ memory, folder, onClick }: { memory: Memory; folder: Folder | null; onClick: () => void }) {
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
const [imgError, setImgError] = useState(false);
const src = memory.thumbnailUrl || memory.url; const src = memory.thumbnailUrl || memory.url;
if (imgError) return null;
return ( return (
<button className="relative w-full aspect-square bg-gray-100 dark:bg-gray-800 overflow-hidden" onClick={onClick}> <div className="flex flex-col gap-0.5">
<button
className="relative w-full aspect-square rounded-xl overflow-hidden group bg-gray-100 dark:bg-gray-800 shadow-sm ring-1 ring-black/5 dark:ring-white/5"
onClick={onClick}
>
<img <img
src={src} src={src}
alt={memory.title || "Memory"} alt={memory.title || "Memory"}
className={`w-full h-full object-cover transition-opacity duration-300 ${loaded ? "opacity-100" : "opacity-0"}`} className={`w-full h-full object-cover transition-all duration-300 group-hover:scale-110 ${loaded ? "opacity-100" : "opacity-0"}`}
onLoad={() => setLoaded(true)} onLoad={() => setLoaded(true)}
onError={() => setImgError(true)}
loading="lazy" loading="lazy"
/> />
{!loaded && <div className="absolute inset-0 bg-gray-200 dark:bg-gray-700 animate-pulse" />} {!loaded && <div className="absolute inset-0 bg-gray-200 dark:bg-gray-700 animate-pulse rounded-xl" />}
{/* hover overlay */}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/25 transition-colors duration-200" />
{/* hover expand icon */}
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<span className="text-white text-lg drop-shadow"></span>
</div>
{memory.processingStatus === "processing" && ( {memory.processingStatus === "processing" && (
<div className="absolute inset-0 bg-black/30 flex items-center justify-center"> <div className="absolute inset-0 bg-black/40 flex items-center justify-center rounded-xl">
<span className="text-white text-xs bg-black/50 px-2 py-0.5 rounded-full">Processing</span> <span className="text-white text-[9px] bg-black/50 px-1.5 py-0.5 rounded-full">Processing</span>
</div> </div>
)} )}
{memory.isPrivate && <div className="absolute top-1 right-1 text-xs">🔒</div>} {memory.isPrivate && <span className="absolute top-1 right-1 text-[9px]">🔒</span>}
</button> </button>
{/* Folder label */}
{folder && folder.id && (
<p className="text-[9px] text-center text-gray-400 dark:text-gray-500 truncate leading-tight px-0.5">
{folder.emoji} {folder.label}
</p>
)}
</div>
); );
} }