-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add Search UI #267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
feat: add Search UI #267
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
7b096c0
feat: add Search UI
HeesooJun 373cadd
change page.tsx
HeesooJun d1bac81
Result and Screen change
HeesooJun ce245f5
loading change
HeesooJun 5a2e48a
Screen type change
HeesooJun aa26102
Merge remote-tracking branch 'origin/develop' into search
HeesooJun 4719ede
feat : 카테고리별 검색 연동 + 아이콘 매핑
HeesooJun 3e73a5e
fix: 검색 화면 데이터 매핑 및 레이아웃 정리
HeesooJun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| 'use client' | ||
|
|
||
| import { ClipLoader } from 'react-spinners' | ||
|
|
||
| export default function Loading() { | ||
| return ( | ||
| <div className="mx-auto w-full max-w-[420px] min-h-screen bg-white overflow-x-hidden"> | ||
| <div className="flex h-[60vh] items-center justify-center"> | ||
| <ClipLoader color="#FF6B6B" size={50} /> | ||
| </div> | ||
| <div className="h-16" /> | ||
| </div> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // apps/web/src/app/(backButton)/search/page.tsx | ||
| import SearchScreen from '@/components/search/SearchScreen' | ||
|
|
||
| type SearchParams = Promise<Record<string, string | string[] | undefined>> | ||
|
|
||
| export default async function Page({ searchParams }: { searchParams?: SearchParams }) { | ||
| const sp = (await searchParams) ?? {} | ||
|
|
||
| const first = (v: string | string[] | undefined) => (Array.isArray(v) ? v[0] : (v ?? '')) | ||
|
|
||
| const initialQuery = first(sp.query) | ||
| const initialCategory = first(sp.category) | ||
|
|
||
| return <SearchScreen initialQuery={initialQuery} initialCategory={initialCategory} /> | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import type { StaticImageData } from 'next/image' | ||
|
|
||
| import iconHealth from './건강.png' | ||
| import iconGame from './게임.png' | ||
| import iconScience from './과학.png' | ||
| import iconEducation from './교육.png' | ||
| import iconTechnology from './기술.png' | ||
| import iconOthers from './기타.png' | ||
| import iconMyEvent from './내가 등록한 행사.png' | ||
| import iconVehicles from './모빌리티.png' | ||
| import iconArts from './문화예술.png' | ||
| import iconBusiness from './비지니스, 창업.png' | ||
| import iconSports from './스포츠.png' | ||
| import iconAnime from './애니메이션.png' | ||
| import iconMovieTv from './영화, 드라마.png' | ||
| import iconFoodDrinks from './음식.png' | ||
| import iconMusic from './음악.png' | ||
| import iconHotEvent from './인기 행사.png' | ||
| import iconHome from './인테리어.png' | ||
| import iconNatureOutdoors from './자연, 야외활동.png' | ||
| import iconGarden from './정원.png' | ||
| import iconCareer from './취업.png' | ||
| import iconPopup from './팝업 행사.png' | ||
| import iconFashionBeauty from './패션, 뷰티.png' | ||
|
|
||
| export const CATEGORY_ICON: Record<string, StaticImageData> = { | ||
| 건강: iconHealth, | ||
| 게임: iconGame, | ||
| 과학: iconScience, | ||
| 교육: iconEducation, | ||
| 기술: iconTechnology, | ||
| 기타: iconOthers, | ||
| '내가 등록한 행사': iconMyEvent, | ||
| 모빌리티: iconVehicles, | ||
| 문화예술: iconArts, | ||
| '비지니스, 창업': iconBusiness, | ||
| 스포츠: iconSports, | ||
| 애니메이션: iconAnime, | ||
| '영화, 드라마': iconMovieTv, | ||
| 음식: iconFoodDrinks, | ||
| 음악: iconMusic, | ||
| '인기 행사': iconHotEvent, | ||
| 인테리어: iconHome, | ||
| '자연, 야외활동': iconNatureOutdoors, | ||
| 정원: iconGarden, | ||
| 취업: iconCareer, | ||
| '팝업 행사': iconPopup, | ||
| '패션, 뷰티': iconFashionBeauty, | ||
| } | ||
|
Comment on lines
+26
to
+49
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저희 행사 카테고리 중 해당 카테고리들을 선택한 이유가 있을까요?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 화면만 구성하려고 임시로 다 넣은거라 나중에 추가하려고 했습니다!! |
||
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| 'use client' | ||
|
|
||
| import { useCallback, useEffect, useMemo, useRef, useState } from 'react' | ||
| import Image from 'next/image' | ||
| import { CATEGORY_ICON } from '@/components/icon/Categoryicon' | ||
| import type { StaticImageData } from 'next/image' | ||
|
|
||
| // 서버 타이틀을 그대로 사용 | ||
| const ALIAS: Record<string, string> = {} | ||
|
|
||
| function normalizeList(list: string[]): string[] { | ||
| const mapped = list.map(n => ALIAS[n] ?? n) | ||
| return Array.from(new Set(mapped)) | ||
| } | ||
|
|
||
| function normalizeOne(name?: string): string | undefined { | ||
| if (!name) return undefined | ||
| return ALIAS[name] ?? name | ||
| } | ||
|
|
||
| export default function CategoryTabs({ items, current, onChange }: { items: string[]; current?: string; onChange: (value: string) => void }) { | ||
| const wrapRef = useRef<HTMLDivElement>(null) | ||
| const rowRef = useRef<HTMLDivElement>(null) | ||
| const btnRefs = useRef<(HTMLButtonElement | null)[]>([]) | ||
| const lblRefs = useRef<(HTMLSpanElement | null)[]>([]) | ||
|
|
||
| const normItems = useMemo(() => normalizeList(items), [items]) | ||
| const normCurrent = useMemo(() => normalizeOne(current), [current]) | ||
|
|
||
| const INDICATOR_W = 60 | ||
| const [left, setLeft] = useState(0) | ||
|
|
||
| const activeIndex = useMemo(() => { | ||
| const idx = normItems.findIndex(x => x === normCurrent) | ||
| return idx >= 0 ? idx : 0 | ||
| }, [normItems, normCurrent]) | ||
|
|
||
| const measure = useCallback(() => { | ||
| const row = rowRef.current | ||
| const btn = btnRefs.current[activeIndex] | ||
| const lbl = lblRefs.current[activeIndex] | ||
| if (!row || !btn || !lbl) return | ||
|
|
||
| const labelW = lbl.offsetWidth | ||
| const labelCenter = (btn.offsetWidth - labelW) / 2 + labelW / 2 | ||
| const x = btn.offsetLeft - row.scrollLeft + labelCenter - INDICATOR_W / 2 | ||
| setLeft(x) | ||
| }, [activeIndex]) | ||
|
|
||
| useEffect(() => { | ||
| const row = rowRef.current | ||
| const wrap = wrapRef.current | ||
| if (!row || !wrap) return | ||
|
|
||
| const onScroll = () => requestAnimationFrame(measure) | ||
| const ro = new ResizeObserver(() => requestAnimationFrame(measure)) | ||
|
|
||
| ro.observe(wrap) | ||
| ro.observe(row) | ||
| row.addEventListener('scroll', onScroll, { passive: true }) | ||
|
|
||
| const raf = requestAnimationFrame(measure) | ||
|
|
||
| return () => { | ||
| cancelAnimationFrame(raf) | ||
| ro.disconnect() | ||
| row.removeEventListener('scroll', onScroll) | ||
| } | ||
| }, [measure, activeIndex]) | ||
|
|
||
| return ( | ||
| <div className="select-none"> | ||
| <div ref={wrapRef} className="relative px-8"> | ||
| <div ref={rowRef} className="flex items-center gap-6 overflow-x-auto pb-3 no-scrollbar"> | ||
| {normItems.map((name, i) => { | ||
| const active = normCurrent === name | ||
| return ( | ||
| <button | ||
| key={name} | ||
| ref={el => { | ||
| btnRefs.current[i] = el | ||
| }} | ||
| onClick={() => onChange(active ? '' : name)} | ||
| className="flex flex-col items-center px-2 py-1 shrink-0" | ||
| > | ||
| {hasIcon(name) ? <div className="mb-2 leading-none">{iconByName(name, active)}</div> : null} | ||
| <span | ||
| ref={el => { | ||
| lblRefs.current[i] = el | ||
| }} | ||
| className={`text-[12px] ${active ? 'font-semibold text-black' : 'text-gray-500'}`} | ||
| > | ||
| {name} | ||
| </span> | ||
| </button> | ||
| ) | ||
| })} | ||
| </div> | ||
|
|
||
| <div className="absolute left-[-24px] right-[-24px] bottom-0 h-[2px] bg-gray-100 z-0" /> | ||
|
HeesooJun marked this conversation as resolved.
|
||
|
|
||
| <span | ||
| className="absolute bottom-0 h-[3px] rounded-full bg-[#FF9575] z-10 transition-[left] duration-200" | ||
| style={{ left, width: INDICATOR_W }} | ||
| /> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| function hasIcon(name: string) { | ||
| return Object.prototype.hasOwnProperty.call(CATEGORY_ICON, name) | ||
| } | ||
|
|
||
| function iconByName(name: string, active: boolean) { | ||
| const src: StaticImageData | undefined = CATEGORY_ICON[name as keyof typeof CATEGORY_ICON] | ||
| if (!src) return null | ||
| const size = 20 | ||
| return ( | ||
| <Image | ||
| src={src} | ||
| alt="" | ||
| width={size} | ||
| height={size} | ||
| draggable={false} | ||
| priority={false} | ||
| className={`block ${active ? '' : 'opacity-45'}`} | ||
| style={{ imageRendering: 'crisp-edges' }} | ||
| /> | ||
| ) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| 'use client' | ||
|
|
||
| import { useMemo } from 'react' | ||
| import { ClipLoader } from 'react-spinners' | ||
|
|
||
| import { useImageLoader } from '@/hooks/s3/useImageLoader' | ||
|
|
||
| const FALLBACK_IMAGE = 'https://picsum.photos/seed/fallback/600/400' | ||
|
|
||
| export default function SearchResultCard({ | ||
| title, | ||
| region, | ||
| period, | ||
| image, | ||
| imageHeight, | ||
| }: { | ||
| title: string | ||
| region: string | ||
| period: string | ||
| image: string | ||
| imageHeight?: number | ||
| }) { | ||
| const { imageUrl, isLoading } = useImageLoader(image) | ||
|
|
||
| const resolvedSrc = useMemo(() => { | ||
| if (imageUrl) return imageUrl | ||
| return FALLBACK_IMAGE | ||
| }, [imageUrl]) | ||
|
|
||
| return ( | ||
| <article className="w-full"> | ||
| <div className="rounded-2xl overflow-hidden" style={{ height: imageHeight }}> | ||
| {isLoading ? ( | ||
| <div className="flex h-full items-center justify-center bg-gray-100"> | ||
| <ClipLoader color="#FF6B6B" size={28} /> | ||
| </div> | ||
| ) : ( | ||
| // eslint-disable-next-line @next/next/no-img-element | ||
| <img src={resolvedSrc} alt={title} className="w-full h-full object-cover" loading="lazy" /> | ||
| )} | ||
| </div> | ||
|
|
||
| <h2 className="mt-3 text-[16px] font-semibold">{title}</h2> | ||
| <p className="mt-1 text-[13px] text-gray-400">{region}</p> | ||
| <p className="mt-1 text-[13px] text-gray-500">{period}</p> | ||
| </article> | ||
| ) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저희 icon을 svg로 관리하고 있는걸로 알고 있는데 png로 가져신 이유가 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
그... 제가 가져온게 무료 flaticon 소스라 SVG를 못 받았고, 자동 변환은 테두리/색이 깨져서 PNG로 유지했습니다!!