diff --git a/packages/client/src/components/gallery/GalleryCard.tsx b/packages/client/src/components/gallery/GalleryCard.tsx
index da3da6f..18462c8 100644
--- a/packages/client/src/components/gallery/GalleryCard.tsx
+++ b/packages/client/src/components/gallery/GalleryCard.tsx
@@ -1,11 +1,12 @@
import type { Document } from "dbtype";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx";
-import TagBadge from "@/components/gallery/TagBadge.tsx";
+import TagBadge, { toPrettyTagname } from "@/components/gallery/TagBadge.tsx";
import { Fragment, useLayoutEffect, useRef, useState } from "react";
import { LazyImage } from "./LazyImage.tsx";
import StyledLink from "./StyledLink.tsx";
import React from "react";
import { Skeleton } from "../ui/skeleton.tsx";
+import { Palette, Users, Trash2, Clock, LayersIcon } from "lucide-react";
function clipTagsWhenOverflow(tags: string[], limit: number) {
let l = 0;
@@ -19,6 +20,7 @@ function clipTagsWhenOverflow(tags: string[], limit: number) {
return tags;
}
+
function GalleryCardImpl({
doc: x
}: { doc: Document; }) {
@@ -36,9 +38,36 @@ function GalleryCardImpl({
const listener = () => {
if (ref.current) {
const { width } = ref.current.getBoundingClientRect();
- const charWidth = 7; // rough estimate
- const newClipCharCount = Math.floor(width / charWidth) * 3;
- setClipCharCount(newClipCharCount);
+ const canvas = document.createElement("canvas");
+ const context = canvas.getContext("2d");
+ if (context) {
+ // 스타일에 맞는 폰트 설정 (Tailwind의 기본 폰트 스타일에 맞게 설정)
+ context.font = getComputedStyle(ref.current).font || "16px sans-serif";
+
+ let totalWidth = 0;
+ let charCount = 0;
+
+ for (const tag of originalTags) {
+ // prefix와 패딩을 고려한 너비 계산
+ const prettyTag = toPrettyTagname(tag); // prefix가 포함된 태그 이름
+ const tagWidth =
+ context.measureText(prettyTag).width + 8; // 양쪽 패딩 4px씩 추가
+ const spaceWidth = context.measureText(" ").width; // 공백 너비
+
+ if (totalWidth + tagWidth + spaceWidth > width * 3) { // 3줄 제한
+ break;
+ }
+ totalWidth += tagWidth + spaceWidth;
+ charCount += tag.length + 1; // 태그 길이 + 공백
+ }
+
+ setClipCharCount(charCount);
+ }
+ else {
+ const charWidth = 7; // rough estimate
+ const newClipCharCount = Math.floor(width / charWidth) * 3;
+ setClipCharCount(newClipCharCount);
+ }
}
};
listener();
@@ -46,46 +75,104 @@ function GalleryCardImpl({
return () => {
window.removeEventListener("resize", listener);
};
- }, []);
+ }, [originalTags]);
- return
- {isDeleted ?
- Deleted
-
:
-
-
- }
-
-
-
-
- {x.title}
-
-
-
- {artists.map((x, i) =>
- {x}
- {i + 1 < artists.length && , }
- )}
- {groups.length > 0 && {" | "}}
- {groups.map((x, i) =>
- {x}
- {i + 1 < groups.length && , }
-
- )}
-
-
-
-
- {clippedTags.map(tag => )}
- {clippedTags.length < originalTags.length && }
-
-
-
- ;
+ return
+ {isDeleted ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {x.title}
+
+
+
+ {artists.length > 0 && (
+
+
+
+ {artists.map((x, i) => (
+
+
+ {x}
+
+ {i + 1 < artists.length && ,}
+
+ ))}
+
+
+ )}
+
+ {groups.length > 0 && (
+
+
+
+ {groups.map((x, i) => (
+
+
+ {x}
+
+ {i + 1 < groups.length && ,}
+
+ ))}
+
+
+ )}
+
+
+
+ {new Date(x.created_at).toLocaleDateString()}
+
+
+
+
+
+
+ {clippedTags.map(tag => (
+
+ ))}
+ {clippedTags.length < originalTags.length && (
+
+ )}
+
+
+
+
}
export function GalleryCardSkeleton({
@@ -104,7 +191,7 @@ export function GalleryCardSkeleton({
{Array.from({ length: tagCount }).map((_, i) => )}
+ className="h-4" />)}
diff --git a/packages/client/src/components/gallery/LazyImage.tsx b/packages/client/src/components/gallery/LazyImage.tsx
index fd6ea7c..ee19a93 100644
--- a/packages/client/src/components/gallery/LazyImage.tsx
+++ b/packages/client/src/components/gallery/LazyImage.tsx
@@ -9,13 +9,26 @@ export function LazyImage({ src, alt, className }: { src: string; alt: string; c
const observer = new IntersectionObserver((entries) => {
if (entries.some(x => x.isIntersecting)) {
setLoaded(true);
- ref.current?.animate([
- { opacity: 0 },
- { opacity: 1 }
- ], {
- duration: 300,
- easing: "ease-in-out"
- });
+ if (ref.current?.complete) {
+ ref.current?.animate([
+ { opacity: 0 },
+ { opacity: 1 }
+ ], {
+ duration: 300,
+ easing: "ease-in-out"
+ });
+ }
+ else {
+ ref.current?.addEventListener("load", () => {
+ ref.current?.animate([
+ { opacity: 0 },
+ { opacity: 1 }
+ ], {
+ duration: 300,
+ easing: "ease-in-out"
+ });
+ });
+ }
observer.disconnect();
}
}, {
@@ -34,5 +47,7 @@ export function LazyImage({ src, alt, className }: { src: string; alt: string; c
src={loaded ? src : undefined}
alt={alt}
className={className}
- loading="lazy" />;
+ loading="lazy"
+
+ />;
}
diff --git a/packages/client/src/page/galleryPage.tsx b/packages/client/src/page/galleryPage.tsx
index 007b28d..3528232 100644
--- a/packages/client/src/page/galleryPage.tsx
+++ b/packages/client/src/page/galleryPage.tsx
@@ -6,8 +6,8 @@ import { useSearchGalleryInfinite } from "../hook/useSearchGallery.ts";
import { Spinner } from "../components/Spinner.tsx";
import TagInput from "@/components/gallery/TagInput.tsx";
import { useEffect, useRef, useState } from "react";
-import { Separator } from "@/components/ui/separator.tsx";
import { useVirtualizer } from "@tanstack/react-virtual";
+import { SearchIcon, TagIcon, AlertCircle, ImageIcon } from "lucide-react";
export default function Gallery() {
const search = useSearch();
@@ -24,13 +24,13 @@ export default function Gallery() {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: size,
- // biome-ignore lint/style/noNonNullAssertion:
+ // biome-ignore lint/style/noNonNullAssertion: could not be null
getScrollElement: () => parentRef.current!,
estimateSize: (index) => {
if (!data) return 8;
const docs = data?.[index];
if (!docs) return 8;
- return docs.data.length * (200 + 8) + 37 + 8;
+ return docs.data.length * (200 + 8) + 32 + 8 + 8 * 2; // 200px for image, 8px for gap, 32px for title, 8px for padding
},
overscan: 1,
});
@@ -51,16 +51,20 @@ export default function Gallery() {
virtualizer.measure();
}, [virtualizer, data]);
-
+
const renderContent = () => {
if (!data) {
return null;
}
const isLoadingMore = data && size > 0 && (data[size - 1] === undefined);
const NoResult = data.reduce((acc, x) => acc + x.data.length, 0) === 0;
-
+
if (NoResult) {
- return No results
+ return
+
+
검색 결과가 없습니다
+
다른 검색어나 태그로 시도해보세요
+
}
else {
return {
const isLoaderRow = item.index === size - 1 && isLoadingMore;
if (isLoaderRow) {
- return
-
+ 컨텐츠를 불러오는 중...
;
}
const docs = data[item.index];
if (!docs) return null;
- return
- {docs.startCursor &&
-
Start with {docs.startCursor}
-
- }
- {docs?.data?.map((x) => {
- return (
-
- );
- })}
+ {docs.startCursor && (
+
+
+ ID {docs.startCursor}이하
+
+
+ )}
+ {docs?.data?.map((x) => (
+
+ ))}
})
}
@@ -102,20 +108,41 @@ export default function Gallery() {
return (
- {(word || tags) &&
-
- {word &&
Search: {word}}
- {tags &&
Tags: }
+
+ {((word ?? "").length > 0 || tags.length > 0) &&
+
+ {word && (
+
+
+ {word}
+
+ )}
+ {tags && tags.length > 0 && (
+
+ )}
}
- {error &&
Error: {String(error)}
}
- {isLoading && <>
-
-
-
- >}
+
+ {error && (
+
+ )}
+
+ {isLoading && (
+
+
+
+
+
+
+ )}
{renderContent()}
);
@@ -127,22 +154,32 @@ function Search() {
const searchParams = new URLSearchParams(search);
const [tags, setTags] = useState(searchParams.get("allow_tag")?.split(",") ?? []);
const [word, setWord] = useState(searchParams.get("word") ?? "");
- return
-
+
-
;
+ if (word) {
+ params.set("word", word);
+ }
+ navigate(`/search?${params.toString()}`);
+ }}
+ >
+
+ 검색
+
+
}