style: improve gallery card
This commit is contained in:
parent
f8e2b43e79
commit
94cf46e7f8
3 changed files with 236 additions and 97 deletions
|
@ -1,11 +1,12 @@
|
||||||
import type { Document } from "dbtype";
|
import type { Document } from "dbtype";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx";
|
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 { Fragment, useLayoutEffect, useRef, useState } from "react";
|
||||||
import { LazyImage } from "./LazyImage.tsx";
|
import { LazyImage } from "./LazyImage.tsx";
|
||||||
import StyledLink from "./StyledLink.tsx";
|
import StyledLink from "./StyledLink.tsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Skeleton } from "../ui/skeleton.tsx";
|
import { Skeleton } from "../ui/skeleton.tsx";
|
||||||
|
import { Palette, Users, Trash2, Clock, LayersIcon } from "lucide-react";
|
||||||
|
|
||||||
function clipTagsWhenOverflow(tags: string[], limit: number) {
|
function clipTagsWhenOverflow(tags: string[], limit: number) {
|
||||||
let l = 0;
|
let l = 0;
|
||||||
|
@ -19,6 +20,7 @@ function clipTagsWhenOverflow(tags: string[], limit: number) {
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function GalleryCardImpl({
|
function GalleryCardImpl({
|
||||||
doc: x
|
doc: x
|
||||||
}: { doc: Document; }) {
|
}: { doc: Document; }) {
|
||||||
|
@ -36,9 +38,36 @@ function GalleryCardImpl({
|
||||||
const listener = () => {
|
const listener = () => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const { width } = ref.current.getBoundingClientRect();
|
const { width } = ref.current.getBoundingClientRect();
|
||||||
const charWidth = 7; // rough estimate
|
const canvas = document.createElement("canvas");
|
||||||
const newClipCharCount = Math.floor(width / charWidth) * 3;
|
const context = canvas.getContext("2d");
|
||||||
setClipCharCount(newClipCharCount);
|
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();
|
listener();
|
||||||
|
@ -46,46 +75,104 @@ function GalleryCardImpl({
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("resize", listener);
|
window.removeEventListener("resize", listener);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [originalTags]);
|
||||||
|
|
||||||
return <Card className="flex h-[200px]">
|
return <Card className="flex h-[200px] overflow-hidden transition-all duration-200 hover:shadow-lg hover:shadow-primary/20 group">
|
||||||
{isDeleted ? <div className="bg-primary border flex items-center justify-center h-[200px] w-[142px] rounded-xl">
|
{isDeleted ? (
|
||||||
<span className="text-primary-foreground text-lg font-bold">Deleted</span>
|
<div className="bg-gradient-to-br from-red-500/20 to-red-800/30 flex items-center justify-center h-[200px] w-[142px] rounded-l-xl border-r border-border/50">
|
||||||
</div> : <div className="rounded-xl overflow-hidden h-[200px] w-[142px] flex-none bg-[#272733] flex items-center justify-center">
|
<div className="flex flex-col items-center gap-2 text-primary-foreground">
|
||||||
<LazyImage src={`/api/doc/${x.id}/comic/thumbnail`}
|
<Trash2 className="h-8 w-8 opacity-80" />
|
||||||
alt={x.title}
|
<span className="text-sm font-medium">Deleted</span>
|
||||||
className="max-h-full max-w-full object-cover object-center"
|
</div>
|
||||||
/>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
}
|
<div className="relative rounded-l-xl overflow-hidden h-[200px] w-[142px] flex-none bg-gradient-to-br from-primary/5 to-primary/10 flex items-center justify-center group-hover:from-primary/10 group-hover:to-primary/20 transition-all duration-300">
|
||||||
<div className="flex-1 flex flex-col">
|
<LazyImage
|
||||||
<CardHeader className="flex-none">
|
src={`/api/doc/${x.id}/comic/thumbnail`}
|
||||||
<CardTitle>
|
alt={x.title}
|
||||||
<StyledLink className="line-clamp-2" to={`/doc/${x.id}`}>
|
className="max-h-full max-w-full object-cover object-center transition-transform duration-300 group-hover:scale-105"
|
||||||
{x.title}
|
/>
|
||||||
</StyledLink>
|
<div className="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded-full flex items-center gap-1 opacity-80">
|
||||||
</CardTitle>
|
<LayersIcon className="h-3 w-3" />
|
||||||
<CardDescription>
|
<span>{x.pagenum}</span>
|
||||||
{artists.map((x, i) => <Fragment key={`artist:${x}`}>
|
</div>
|
||||||
<StyledLink to={`/search?allow_tag=artist:${x}`}>{x}</StyledLink>
|
</div>
|
||||||
{i + 1 < artists.length && <span className="opacity-50">, </span>}
|
)}
|
||||||
</Fragment>)}
|
|
||||||
{groups.length > 0 && <span key={"sep"}>{" | "}</span>}
|
<div className="flex-1 flex flex-col">
|
||||||
{groups.map((x, i) => <Fragment key={`group:${x}`}>
|
<CardHeader className="flex-none">
|
||||||
<StyledLink to={`/search?allow_tag=group:${x}`}>{x}</StyledLink>
|
<CardTitle className="group-hover:text-primary transition-colors duration-200">
|
||||||
{i + 1 < groups.length && <span className="opacity-50">, </span>}
|
<StyledLink className="line-clamp-2 font-bold" to={`/doc/${x.id}`}>
|
||||||
</Fragment>
|
{x.title}
|
||||||
)}
|
</StyledLink>
|
||||||
</CardDescription>
|
</CardTitle>
|
||||||
</CardHeader>
|
<CardDescription className="flex flex-wrap items-center gap-x-3">
|
||||||
<CardContent className="flex-1 overflow-hidden">
|
{artists.length > 0 && (
|
||||||
<ul ref={ref} className="flex flex-wrap gap-2 items-baseline content-start">
|
<div className="flex items-center gap-1.5">
|
||||||
{clippedTags.map(tag => <TagBadge key={tag} tagname={tag} className="" />)}
|
<Palette className="h-3.5 w-3.5 text-primary/70" />
|
||||||
{clippedTags.length < originalTags.length && <TagBadge key={"..."} tagname="..." className="inline-block" disabled />}
|
<span className="flex flex-wrap items-center">
|
||||||
</ul>
|
{artists.map((x, i) => (
|
||||||
</CardContent>
|
<Fragment key={`artist:${x}`}>
|
||||||
</div>
|
<StyledLink
|
||||||
</Card>;
|
to={`/search?allow_tag=artist:${x}`}
|
||||||
|
className="hover:text-primary hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
{x}
|
||||||
|
</StyledLink>
|
||||||
|
{i + 1 < artists.length && <span className="opacity-50 mx-1">,</span>}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{groups.length > 0 && (
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Users className="h-3.5 w-3.5 text-primary/70" />
|
||||||
|
<span className="flex flex-wrap items-center">
|
||||||
|
{groups.map((x, i) => (
|
||||||
|
<Fragment key={`group:${x}`}>
|
||||||
|
<StyledLink
|
||||||
|
to={`/search?allow_tag=group:${x}`}
|
||||||
|
className="hover:text-primary hover:underline transition-colors"
|
||||||
|
>
|
||||||
|
{x}
|
||||||
|
</StyledLink>
|
||||||
|
{i + 1 < groups.length && <span className="opacity-50 mx-1">,</span>}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||||
|
<Clock className="h-3.5 w-3.5" />
|
||||||
|
<span className="text-xs">{new Date(x.created_at).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex-1 overflow-hidden">
|
||||||
|
<ul ref={ref} className="flex flex-wrap gap-1.5 items-baseline content-start">
|
||||||
|
{clippedTags.map(tag => (
|
||||||
|
<TagBadge
|
||||||
|
key={tag}
|
||||||
|
tagname={tag}
|
||||||
|
className="transition-all duration-200 hover:shadow-sm hover:scale-105"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{clippedTags.length < originalTags.length && (
|
||||||
|
<TagBadge
|
||||||
|
key={"..."}
|
||||||
|
tagname="..."
|
||||||
|
className="inline-block opacity-70"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GalleryCardSkeleton({
|
export function GalleryCardSkeleton({
|
||||||
|
@ -104,7 +191,7 @@ export function GalleryCardSkeleton({
|
||||||
<ul className="flex flex-wrap gap-2 items-baseline content-start">
|
<ul className="flex flex-wrap gap-2 items-baseline content-start">
|
||||||
{Array.from({ length: tagCount }).map((_, i) => <Skeleton key={i}
|
{Array.from({ length: tagCount }).map((_, i) => <Skeleton key={i}
|
||||||
style={{ width: `${Math.random() * 100 + 50}px` }}
|
style={{ width: `${Math.random() * 100 + 50}px` }}
|
||||||
className="h-4" />)}
|
className="h-4" />)}
|
||||||
</ul>
|
</ul>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,13 +9,26 @@ export function LazyImage({ src, alt, className }: { src: string; alt: string; c
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
if (entries.some(x => x.isIntersecting)) {
|
if (entries.some(x => x.isIntersecting)) {
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
ref.current?.animate([
|
if (ref.current?.complete) {
|
||||||
{ opacity: 0 },
|
ref.current?.animate([
|
||||||
{ opacity: 1 }
|
{ opacity: 0 },
|
||||||
], {
|
{ opacity: 1 }
|
||||||
duration: 300,
|
], {
|
||||||
easing: "ease-in-out"
|
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();
|
observer.disconnect();
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
|
@ -34,5 +47,7 @@ export function LazyImage({ src, alt, className }: { src: string; alt: string; c
|
||||||
src={loaded ? src : undefined}
|
src={loaded ? src : undefined}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
className={className}
|
className={className}
|
||||||
loading="lazy" />;
|
loading="lazy"
|
||||||
|
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { useSearchGalleryInfinite } from "../hook/useSearchGallery.ts";
|
||||||
import { Spinner } from "../components/Spinner.tsx";
|
import { Spinner } from "../components/Spinner.tsx";
|
||||||
import TagInput from "@/components/gallery/TagInput.tsx";
|
import TagInput from "@/components/gallery/TagInput.tsx";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Separator } from "@/components/ui/separator.tsx";
|
|
||||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||||
|
import { SearchIcon, TagIcon, AlertCircle, ImageIcon } from "lucide-react";
|
||||||
|
|
||||||
export default function Gallery() {
|
export default function Gallery() {
|
||||||
const search = useSearch();
|
const search = useSearch();
|
||||||
|
@ -24,13 +24,13 @@ export default function Gallery() {
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count: size,
|
count: size,
|
||||||
// biome-ignore lint/style/noNonNullAssertion: <explanation>
|
// biome-ignore lint/style/noNonNullAssertion: could not be null
|
||||||
getScrollElement: () => parentRef.current!,
|
getScrollElement: () => parentRef.current!,
|
||||||
estimateSize: (index) => {
|
estimateSize: (index) => {
|
||||||
if (!data) return 8;
|
if (!data) return 8;
|
||||||
const docs = data?.[index];
|
const docs = data?.[index];
|
||||||
if (!docs) return 8;
|
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,
|
overscan: 1,
|
||||||
});
|
});
|
||||||
|
@ -60,7 +60,11 @@ export default function Gallery() {
|
||||||
const NoResult = data.reduce((acc, x) => acc + x.data.length, 0) === 0;
|
const NoResult = data.reduce((acc, x) => acc + x.data.length, 0) === 0;
|
||||||
|
|
||||||
if (NoResult) {
|
if (NoResult) {
|
||||||
return <div className="p-4 text-3xl">No results</div>
|
return <div className="flex flex-col items-center justify-center p-12 text-center">
|
||||||
|
<ImageIcon className="w-16 h-16 text-muted-foreground mb-4 opacity-50" />
|
||||||
|
<h3 className="text-3xl font-semibold text-muted-foreground mb-2">검색 결과가 없습니다</h3>
|
||||||
|
<p className="text-muted-foreground">다른 검색어나 태그로 시도해보세요</p>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return <div className="w-full relative overflow-auto h-full"
|
return <div className="w-full relative overflow-auto h-full"
|
||||||
|
@ -69,30 +73,32 @@ export default function Gallery() {
|
||||||
virtualItems.map((item) => {
|
virtualItems.map((item) => {
|
||||||
const isLoaderRow = item.index === size - 1 && isLoadingMore;
|
const isLoaderRow = item.index === size - 1 && isLoadingMore;
|
||||||
if (isLoaderRow) {
|
if (isLoaderRow) {
|
||||||
return <div key={item.index} className="w-full flex justify-center top-0 left-0 absolute"
|
return <div key={item.index}
|
||||||
|
className="w-full flex justify-center top-0 left-0 absolute p-8"
|
||||||
style={{
|
style={{
|
||||||
height: `${item.size}px`,
|
height: `${item.size}px`,
|
||||||
transform: `translateY(${item.start}px)`
|
transform: `translateY(${item.start}px)`
|
||||||
}}>
|
}}>
|
||||||
<Spinner />
|
<span className="text-muted-foreground"><Spinner />컨텐츠를 불러오는 중...</span>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
const docs = data[item.index];
|
const docs = data[item.index];
|
||||||
if (!docs) return null;
|
if (!docs) return null;
|
||||||
return <div className="w-full grid gap-2 content-start top-0 left-0 absolute mb-2" key={item.index}
|
return <div className="w-full grid gap-2 content-start top-0 left-0 absolute mb-4" key={item.index}
|
||||||
style={{
|
style={{
|
||||||
height: `${item.size}px`,
|
height: `${item.size}px`,
|
||||||
transform: `translateY(${item.start}px)`
|
transform: `translateY(${item.start}px)`
|
||||||
}}>
|
}}>
|
||||||
{docs.startCursor && <div>
|
{docs.startCursor && (
|
||||||
<h3 className="text-3xl">Start with {docs.startCursor}</h3>
|
<div className="bg-muted/50 rounded-lg p-2">
|
||||||
<Separator />
|
<h3 className="text-2xl font-medium flex items-center gap-2">
|
||||||
</div>}
|
ID <span className="text-primary">{docs.startCursor}</span>이하
|
||||||
{docs?.data?.map((x) => {
|
</h3>
|
||||||
return (
|
</div>
|
||||||
<GalleryCard doc={x} key={x.id} />
|
)}
|
||||||
);
|
{docs?.data?.map((x) => (
|
||||||
})}
|
<GalleryCard doc={x} key={x.id} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -102,20 +108,41 @@ export default function Gallery() {
|
||||||
|
|
||||||
return (<div className="p-4 grid gap-2 h-dvh overflow-auto items-start content-start" ref={parentRef}>
|
return (<div className="p-4 grid gap-2 h-dvh overflow-auto items-start content-start" ref={parentRef}>
|
||||||
<Search />
|
<Search />
|
||||||
{(word || tags) &&
|
|
||||||
<div className="bg-primary rounded-full p-1 sticky top-0 shadow-md z-10">
|
{((word ?? "").length > 0 || tags.length > 0) &&
|
||||||
{word && <span className="text-primary-foreground ml-4">Search: {word}</span>}
|
<div className="bg-primary rounded-full p-2 mt-3 shadow-lg flex flex-wrap items-center gap-2">
|
||||||
{tags && <span className="text-primary-foreground ml-4">Tags: <ul className="inline-flex gap-1">{
|
{word && (
|
||||||
tags.map(x => <TagBadge tagname={x} key={x} />)}
|
<div className="flex items-center bg-primary-foreground/20 rounded-full px-3 py-1 text-primary-foreground">
|
||||||
</ul></span>}
|
<SearchIcon className="w-4 h-4 mr-2" />
|
||||||
|
<span className="font-medium">{word}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{tags && tags.length > 0 && (
|
||||||
|
<div className="flex items-center flex-wrap gap-1">
|
||||||
|
<TagIcon className="w-4 h-4 text-primary-foreground ml-2" />
|
||||||
|
<ul className="inline-flex flex-wrap gap-1 ml-1">
|
||||||
|
{tags.map(x => <TagBadge tagname={x} key={x} />)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{error && <div className="p-4">Error: {String(error)}</div>}
|
|
||||||
{isLoading && <>
|
{error && (
|
||||||
<GalleryCardSkeleton />
|
<div className="p-6 bg-destructive/10 rounded-lg flex items-center">
|
||||||
<GalleryCardSkeleton />
|
<AlertCircle className="w-6 h-6 text-destructive mr-2" />
|
||||||
<GalleryCardSkeleton />
|
<div className="text-destructive font-medium">{String(error)}</div>
|
||||||
</>}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<GalleryCardSkeleton />
|
||||||
|
<GalleryCardSkeleton />
|
||||||
|
<GalleryCardSkeleton />
|
||||||
|
<GalleryCardSkeleton />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -127,22 +154,32 @@ function Search() {
|
||||||
const searchParams = new URLSearchParams(search);
|
const searchParams = new URLSearchParams(search);
|
||||||
const [tags, setTags] = useState(searchParams.get("allow_tag")?.split(",") ?? []);
|
const [tags, setTags] = useState(searchParams.get("allow_tag")?.split(",") ?? []);
|
||||||
const [word, setWord] = useState(searchParams.get("word") ?? "");
|
const [word, setWord] = useState(searchParams.get("word") ?? "");
|
||||||
return <div className="flex space-x-2">
|
return <div className="flex flex-col sm:flex-row gap-3">
|
||||||
<TagInput className="flex-1" input={word} onInputChange={setWord}
|
<TagInput
|
||||||
tags={tags} onTagsChange={setTags}
|
className="flex-1 shadow-sm"
|
||||||
|
input={word}
|
||||||
|
onInputChange={setWord}
|
||||||
|
tags={tags}
|
||||||
|
onTagsChange={setTags}
|
||||||
/>
|
/>
|
||||||
<Button className="flex-none" onClick={() => {
|
<Button
|
||||||
const params = new URLSearchParams();
|
className="flex-none gap-2 px-4 shadow-md hover:shadow-lg transition-all"
|
||||||
if (tags.length > 0) {
|
onClick={() => {
|
||||||
for (const tag of tags) {
|
const params = new URLSearchParams();
|
||||||
params.append("allow_tag", tag);
|
if (tags.length > 0) {
|
||||||
|
for (const tag of tags) {
|
||||||
|
params.append("allow_tag", tag);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (word) {
|
||||||
if (word) {
|
params.set("word", word);
|
||||||
params.set("word", word);
|
}
|
||||||
}
|
navigate(`/search?${params.toString()}`);
|
||||||
navigate(`/search?${params.toString()}`);
|
}}
|
||||||
}}>Search</Button>
|
>
|
||||||
</div>;
|
<SearchIcon className="w-4 h-4" />
|
||||||
|
검색
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue