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:
parent
fb45250a27
commit
9c0afa2054
1 changed files with 48 additions and 20 deletions
|
|
@ -190,9 +190,10 @@ export default function MemoriesPage() {
|
|||
};
|
||||
|
||||
const baseList = searchResults !== null ? searchResults : memories;
|
||||
const displayMemories = activeFolder
|
||||
const displayMemories = (activeFolder
|
||||
? baseList.filter(m => m.description === activeFolder)
|
||||
: baseList;
|
||||
: baseList
|
||||
).filter(m => m.url || m.thumbnailUrl);
|
||||
|
||||
const folderCounts = memories.reduce<Record<string, number>>((acc, m) => {
|
||||
if (m.description) acc[m.description] = (acc[m.description] || 0) + 1;
|
||||
|
|
@ -269,9 +270,14 @@ export default function MemoriesPage() {
|
|||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-0.5">
|
||||
<div className="grid grid-cols-4 gap-1 pt-1">
|
||||
{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>
|
||||
)}
|
||||
|
|
@ -389,26 +395,48 @@ export default function MemoriesPage() {
|
|||
|
||||
// ─── Grid tile ────────────────────────────────────────────────────────────────
|
||||
|
||||
function MemoryTile({ memory, onClick }: { memory: Memory; onClick: () => void }) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
function MemoryTile({ memory, folder, onClick }: { memory: Memory; folder: Folder | null; onClick: () => void }) {
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [imgError, setImgError] = useState(false);
|
||||
const src = memory.thumbnailUrl || memory.url;
|
||||
|
||||
if (imgError) return null;
|
||||
|
||||
return (
|
||||
<button className="relative w-full aspect-square bg-gray-100 dark:bg-gray-800 overflow-hidden" onClick={onClick}>
|
||||
<img
|
||||
src={src}
|
||||
alt={memory.title || "Memory"}
|
||||
className={`w-full h-full object-cover transition-opacity duration-300 ${loaded ? "opacity-100" : "opacity-0"}`}
|
||||
onLoad={() => setLoaded(true)}
|
||||
loading="lazy"
|
||||
/>
|
||||
{!loaded && <div className="absolute inset-0 bg-gray-200 dark:bg-gray-700 animate-pulse" />}
|
||||
{memory.processingStatus === "processing" && (
|
||||
<div className="absolute inset-0 bg-black/30 flex items-center justify-center">
|
||||
<span className="text-white text-xs bg-black/50 px-2 py-0.5 rounded-full">Processing…</span>
|
||||
<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
|
||||
src={src}
|
||||
alt={memory.title || "Memory"}
|
||||
className={`w-full h-full object-cover transition-all duration-300 group-hover:scale-110 ${loaded ? "opacity-100" : "opacity-0"}`}
|
||||
onLoad={() => setLoaded(true)}
|
||||
onError={() => setImgError(true)}
|
||||
loading="lazy"
|
||||
/>
|
||||
{!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" && (
|
||||
<div className="absolute inset-0 bg-black/40 flex items-center justify-center rounded-xl">
|
||||
<span className="text-white text-[9px] bg-black/50 px-1.5 py-0.5 rounded-full">Processing…</span>
|
||||
</div>
|
||||
)}
|
||||
{memory.isPrivate && <span className="absolute top-1 right-1 text-[9px]">🔒</span>}
|
||||
</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>
|
||||
)}
|
||||
{memory.isPrivate && <div className="absolute top-1 right-1 text-xs">🔒</div>}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue