feat: integrate react-responsive-masonry for improved gallery layout

- Added the react-responsive-masonry package to enhance the layout of the GalleryShowcase component, allowing for a responsive grid display of galleries.
- Updated the GalleryShowcase component to utilize Masonry and ResponsiveMasonry for better visual organization of gallery items.
- Included new dependencies in package.json and pnpm-lock.yaml for react-responsive-masonry and its types.

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2025-11-24 21:22:16 +08:00
parent 471c1b11ae
commit 3980e24617
3 changed files with 117 additions and 97 deletions

View File

@@ -38,6 +38,7 @@
"react-error-boundary": "6.0.0",
"react-intersection-observer": "10.0.0",
"react-markdown": "^9.0.1",
"react-responsive-masonry": "2.7.1",
"sonner": "2.0.7",
"tailwind-merge": "3.4.0",
"usehooks-ts": "3.1.1",
@@ -58,6 +59,7 @@
"@types/node": "24.10.1",
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"@types/react-responsive-masonry": "2.6.0",
"autoprefixer": "10.4.22",
"code-inspector-plugin": "1.2.10",
"cross-env": "10.1.0",

View File

@@ -3,6 +3,7 @@
import { useQuery } from '@tanstack/react-query'
import { useLocale, useTranslations } from 'next-intl'
import { useEffect, useState } from 'react'
import Masonry, { ResponsiveMasonry } from 'react-responsive-masonry'
import { API_URL } from '~/constants/env'
@@ -152,109 +153,113 @@ export const GalleryShowcase = () => {
)}
{!isLoading && !error && galleries.length > 0 && (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{galleries.map((gallery) => (
<a
key={gallery.id}
href={buildGalleryUrl(gallery)}
target="_blank"
rel="noopener noreferrer"
className="group relative overflow-hidden rounded-3xl border border-white/10 bg-linear-to-br from-white/8 to-transparent p-6 transition hover:border-white/30 hover:bg-white/10"
>
{/* Author Avatar & Info */}
{gallery.author && (
<div className="mb-4 flex items-center gap-3">
<div className="relative size-10 shrink-0 overflow-hidden rounded-full border border-white/10 bg-white/5">
{gallery.author.avatar ? (
<img
src={gallery.author.avatar}
alt={gallery.author.name}
className="h-full w-full object-cover"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = 'none'
}}
/>
) : null}
{(!gallery.author.avatar ||
gallery.author.avatar === '') && (
<div className="bg-accent-20 text-accent flex h-full w-full items-center justify-center">
<span className="text-sm font-medium">
{gallery.author.name.charAt(0).toUpperCase()}
</span>
</div>
)}
<ResponsiveMasonry
columnsCountBreakPoints={{ 350: 1, 640: 2, 1024: 3 }}
>
<Masonry gutter="1rem">
{galleries.map((gallery) => (
<a
key={gallery.id}
href={buildGalleryUrl(gallery)}
target="_blank"
rel="noopener noreferrer"
className="group relative block w-full overflow-hidden rounded-3xl border border-white/10 bg-linear-to-br from-white/8 to-transparent p-6 transition hover:border-white/30 hover:bg-white/10"
>
{/* Author Avatar & Info */}
{gallery.author && (
<div className="mb-4 flex items-center gap-3">
<div className="relative size-10 shrink-0 overflow-hidden rounded-full border border-white/10 bg-white/5">
{gallery.author.avatar ? (
<img
src={gallery.author.avatar}
alt={gallery.author.name}
className="h-full w-full object-cover"
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = 'none'
}}
/>
) : null}
{(!gallery.author.avatar ||
gallery.author.avatar === '') && (
<div className="bg-accent-20 text-accent flex h-full w-full items-center justify-center">
<span className="text-sm font-medium">
{gallery.author.name.charAt(0).toUpperCase()}
</span>
</div>
)}
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-white">
{gallery.author.name}
</p>
<p className="truncate text-xs text-white/50">
{getDisplayUrl(gallery)}
</p>
</div>
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-white">
{gallery.author.name}
</p>
<p className="truncate text-xs text-white/50">
{getDisplayUrl(gallery)}
</p>
</div>
</div>
)}
)}
{/* Site Name */}
<h3 className="group-hover:text-accent mb-2 font-serif text-xl text-white transition">
{gallery.name}
</h3>
{/* Site Name */}
<h3 className="group-hover:text-accent mb-2 font-serif text-xl text-white transition">
{gallery.name}
</h3>
{/* Description */}
{gallery.description && (
<p className="mb-4 line-clamp-2 text-sm leading-relaxed text-white/70">
{gallery.description}
</p>
)}
{/* Description */}
{gallery.description && (
<p className="mb-4 line-clamp-2 text-sm leading-relaxed text-white/70">
{gallery.description}
</p>
)}
{/* Photo Count & Tags */}
<div className="mb-4 space-y-2">
{gallery.photoCount > 0 && (
<div className="flex items-center gap-2 text-xs text-white/60">
<i className="i-lucide-image size-3.5" />
<span>
{gallery.photoCount}{' '}
{/* Photo Count & Tags */}
<div className="mb-4 space-y-2">
{gallery.photoCount > 0 && (
<div className="flex items-center gap-2 text-xs text-white/60">
<i className="i-lucide-image size-3.5" />
<span>
{gallery.photoCount === 1 ? 'photo' : 'photos'}
{gallery.photoCount}{' '}
<span>
{gallery.photoCount === 1 ? 'photo' : 'photos'}
</span>
</span>
</span>
</div>
)}
{gallery.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{gallery.tags.slice(0, 4).map((tag) => (
<span
key={tag}
className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-xs text-white/70"
>
{tag}
</span>
))}
{gallery.tags.length > 4 && (
<span className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-xs text-white/50">
+{gallery.tags.length - 4}
</span>
)}
</div>
)}
</div>
{/* Divider */}
<div className="mb-4 h-px w-full bg-linear-to-r from-transparent via-white/30 to-transparent opacity-50" />
{/* Footer */}
<div className="flex items-center justify-between">
<div className="text-xs text-white/40">
{formatDate(gallery.createdAt)}
</div>
)}
{gallery.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{gallery.tags.slice(0, 4).map((tag) => (
<span
key={tag}
className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-xs text-white/70"
>
{tag}
</span>
))}
{gallery.tags.length > 4 && (
<span className="rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-xs text-white/50">
+{gallery.tags.length - 4}
</span>
)}
</div>
)}
</div>
<div className="text-white/30 transition group-hover:text-white/60">
<i className="i-lucide-external-link size-4" />
{/* Divider */}
<div className="mb-4 h-px w-full bg-linear-to-r from-transparent via-white/30 to-transparent opacity-50" />
{/* Footer */}
<div className="flex items-center justify-between">
<div className="text-xs text-white/40">
{formatDate(gallery.createdAt)}
</div>
<div className="text-white/30 transition group-hover:text-white/60">
<i className="i-lucide-external-link size-4" />
</div>
</div>
</div>
</a>
))}
</div>
</a>
))}
</Masonry>
</ResponsiveMasonry>
)}
</section>
)

17
pnpm-lock.yaml generated
View File

@@ -103,7 +103,7 @@ importers:
version: 9.39.1(jiti@2.6.1)
eslint-config-hyoban:
specifier: 4.0.10
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
fast-glob:
specifier: 3.3.3
version: 3.3.3
@@ -384,6 +384,9 @@ importers:
react-markdown:
specifier: ^9.0.1
version: 9.1.0(@types/react@19.2.3)(react@19.2.0)
react-responsive-masonry:
specifier: 2.7.1
version: 2.7.1
sonner:
specifier: 2.0.7
version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
@@ -439,6 +442,9 @@ importers:
'@types/react-dom':
specifier: 19.2.3
version: 19.2.3(@types/react@19.2.3)
'@types/react-responsive-masonry':
specifier: 2.6.0
version: 2.6.0
autoprefixer:
specifier: 10.4.22
version: 10.4.22(postcss@8.5.6)
@@ -456,7 +462,7 @@ importers:
version: 9.39.1(jiti@2.6.1)
eslint-config-hyoban:
specifier: 4.0.10
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
version: 4.0.10(@types/estree@1.0.8)(@typescript-eslint/eslint-plugin@8.46.4(@typescript-eslint/parser@8.46.2(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1))(tailwindcss@4.1.17)(ts-api-utils@2.1.0(typescript@5.9.3))(typescript@5.9.3)
husky:
specifier: 9.1.7
version: 9.1.7
@@ -6057,6 +6063,9 @@ packages:
peerDependencies:
'@types/react': '*'
'@types/react-responsive-masonry@2.6.0':
resolution: {integrity: sha512-MF2ql1CjzOoL9fLWp6L3ABoyzBUP/YV71wyb3Fx+cViYNj7+tq3gDCllZHbLg1LQfGOQOEGbV2P7TOcUeGiR6w==}
'@types/react@19.2.3':
resolution: {integrity: sha512-k5dJVszUiNr1DSe8Cs+knKR6IrqhqdhpUwzqhkS8ecQTSf3THNtbfIp/umqHMpX2bv+9dkx3fwDv/86LcSfvSg==}
@@ -17648,6 +17657,10 @@ snapshots:
dependencies:
'@types/react': 19.2.3
'@types/react-responsive-masonry@2.6.0':
dependencies:
'@types/react': 19.2.3
'@types/react@19.2.3':
dependencies:
csstype: 3.1.3