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 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue