Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0fa1c3c
feat: CLIENT_PATH 추가하여 클라이언트 라우팅 경로 정의
leeleeleeleejun Aug 17, 2025
d8f9d98
feat: @repo/components/Icon에서 IconType 타입도 내보내도록 수정
leeleeleeleejun Aug 17, 2025
cea3ddc
feat: BottomNavigation 및 TabItem 컴포넌트 추가
leeleeleeleejun Aug 17, 2025
8e99bd1
feat: SearchBar 컴포넌트에 className prop 추가
leeleeleeleejun Aug 17, 2025
45bd18d
chore: zod 패키지 추가
leeleeleeleejun Aug 17, 2025
359ed03
feat: HydrationBoundaryPage 컴포넌트의 쿼리 구성 타입 수정 및 주석 개선
leeleeleeleejun Aug 17, 2025
33bd5ea
feat: 카테고리 api 관련 쿼리 및 스키마 추가
leeleeleeleejun Aug 17, 2025
ecb34f1
feat: 메인화면 카테고리 목록 컴포넌트 추가
leeleeleeleejun Aug 17, 2025
3751ed0
feat: Categories 컴포넌트를 화살표 함수로 변경하고 index 파일 추가
leeleeleeleejun Aug 17, 2025
5550d75
chore: keen-slider 패키지 추가
leeleeleeleejun Aug 17, 2025
479abb2
feat: Banner 컴포넌트 추가 및 슬라이더 기능 구현
leeleeleeleejun Aug 17, 2025
c70d5f9
feat: OnlyLeftHeader 컴포넌트에서 Text 요소를 h1 태그로 변경
leeleeleeleejun Aug 17, 2025
1ee4782
refactor: Chip 컴포넌트에서 chipType을 icon과 label로 변경
leeleeleeleejun Aug 17, 2025
33ff257
feat: Divider 컴포넌트 추가 및 index 파일에서 내보내기 설정
leeleeleeleejun Aug 17, 2025
991540b
feat: Banner 컴포넌트에 최소 높이 속성 추가 및 jsdoc 추가
leeleeleeleejun Aug 17, 2025
c00e778
feat: HydrationBoundaryPage 컴포넌트에서 prefetch 파라미터 추가 및 쿼리 옵션 변경
leeleeleeleejun Aug 17, 2025
03a8dda
feat: RankingPlace 관련 핸들러 및 쿼리 추가, API 경로 설정
leeleeleeleejun Aug 17, 2025
e72bfca
feat: Chip 컴포넌트에서 아이콘 크기를 14에서 16으로 변경
leeleeleeleejun Aug 17, 2025
746f1b4
feat: SubTitle 컴포넌트 추가
leeleeleeleejun Aug 17, 2025
511b92f
feat: PlaceListItem 컴포넌트 추가
leeleeleeleejun Aug 17, 2025
fd8f740
feat: MostLikedPlaces 및 MostViewsPlaces 컴포넌트 추가 및 RankingPlaceList 통합
leeleeleeleejun Aug 17, 2025
0321ce6
feat: 메인 페이지 구성 요소 추가 및 데이터 프리패칭 설정
leeleeleeleejun Aug 17, 2025
3a899cc
feat: Layout 배경색을 회색에서 #FEFCF9으로 변경
leeleeleeleejun Aug 17, 2025
56ee87e
feat: likeCount 스키마를 음수 또는 소수 방지
leeleeleeleejun Aug 17, 2025
f965101
feat: prefetchQuery에 조회수 랭킹 리스트 포함
leeleeleeleejun Aug 17, 2025
2fa5401
feat: Chip 컴포넌트에서 onClick 이벤트 핸들러 위치 수정
leeleeleeleejun Aug 17, 2025
381ec61
feat: Chip 컴포넌트의 기본 HTML 태그 타입을 'button'에서 'div'로 변경
leeleeleeleejun Aug 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 14 additions & 28 deletions apps/web/app/HydrationBoundaryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,35 @@ import {
import { ReactNode } from 'react'

/**
* React Query 쿼리 구성 타입
* 서버 컴포넌트에서 React Query 쿼리를 미리 요청(prefetch)하고,
* 클라이언트로 전달하여 초기 데이터를 사용할 수 있도록 해주는 컴포넌트입니다.
*
* @property queryKey - React Query에서 사용할 쿼리 키
* @property queryFn - 데이터를 가져오는 비동기 함수
*/
type QueryConfig = {
queryKey: string[]
queryFn: () => Promise<unknown>
}

/**
* 서버 컴포넌트에서 React Query 쿼리를 미리 요청(prefetch)한 뒤,
* dehydrate 상태를 클라이언트에 전달하기 위한 컴포넌트.
* @param children - HydrationBoundary로 감쌀 React 노드
* @param prefetch - 서버에서 실행할 prefetch 함수. QueryClient를 받아서 필요한 쿼리를 모두 prefetch하도록 구현합니다.
*
* @returns HydrationBoundary로 감싼 children
*
* @example
* ```tsx
* <HydrationBoundaryPage
* queries={[
* { queryKey: ['user'], queryFn: fetchUser },
* { queryKey: ['posts'], queryFn: fetchPosts },
* ]}
* prefetch={async (queryClient) => {
* await queryClient.prefetchQuery(useCategoryQueries.list())
* await queryClient.prefetchQuery(usePlaceQueries.rankingList('likes'))
* }}
* >
* <MyPage />
* <Categories />
* </HydrationBoundaryPage>
* ```
*
* @param queries - 사전 요청할 쿼리들의 배열
* @param children - HydrationBoundary로 감쌀 React 노드
*/
export const HydrationBoundaryPage = async ({
queries,
children,
prefetch,
}: {
queries: QueryConfig[]
children: ReactNode
prefetch: (queryClient: QueryClient) => Promise<void>
}) => {
const queryClient = new QueryClient()

await Promise.all(
queries.map(({ queryKey, queryFn }) =>
queryClient.prefetchQuery({ queryKey, queryFn }),
),
)
await prefetch(queryClient)

return (
<HydrationBoundary state={dehydrate(queryClient)}>
Expand Down
17 changes: 17 additions & 0 deletions apps/web/app/_apis/queries/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { queryOptions } from '@tanstack/react-query'
import { getCategories } from '@/_apis/services/category'

export const CategoryQueryKeys = {
all: () => ['category'] as const,
list: () => [...CategoryQueryKeys.all(), 'list'] as const,
items: (categoryId: string) =>
[...CategoryQueryKeys.all(), 'items', categoryId] as const,
}

export const useCategoryQueries = {
list: () =>
queryOptions({
queryKey: CategoryQueryKeys.list(),
queryFn: getCategories,
}),
}
17 changes: 17 additions & 0 deletions apps/web/app/_apis/queries/place.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { queryOptions } from '@tanstack/react-query'
import { RankingPlaceSort } from '@/_apis/schemas/place'
import { getRankingPlaces } from '@/_apis/services/place'

export const PlaceQueryKeys = {
all: () => ['place'] as const,
rankingList: (sort: RankingPlaceSort) =>
[...PlaceQueryKeys.all(), 'ranking', sort] as const,
}

export const usePlaceQueries = {
rankingList: (sort: RankingPlaceSort) =>
queryOptions({
queryKey: PlaceQueryKeys.rankingList(sort),
queryFn: () => getRankingPlaces(sort),
}),
}
10 changes: 10 additions & 0 deletions apps/web/app/_apis/schemas/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from 'zod'
import { IconList } from '@repo/ui/components/Icon/IconMap'

export const CategorySchema = z.object({
id: z.number().transform(String),
name: z.string(),
iconKey: z.enum(IconList),
})
Comment on lines +5 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

z.enum에 동적 배열(IconList) 전달은 TS 타입 오류 발생 가능성이 큽니다

IconListstring[]으로 추론되어 z.enum의 튜플 요구사항을 만족하지 못합니다. 현재 구현은 빌드 타임에 TS 에러가 날 가능성이 높습니다.

아래처럼 z.custom<IconType>로 검증을 수행하면 런타임 검증과 타입 추론(IconType)을 동시에 만족합니다.

-import { z } from 'zod'
-import { IconList } from '@repo/ui/components/Icon/IconMap'
+import { z } from 'zod'
+import { IconList } from '@repo/ui/components/Icon/IconMap'
+import type { IconType } from '@repo/ui/components/Icon'
 
 export const CategorySchema = z.object({
   id: z.number().transform(String),
   name: z.string(),
-  iconKey: z.enum(IconList),
+  iconKey: z.custom<IconType>(
+    (v) => typeof v === 'string' && (IconList as readonly string[]).includes(v),
+    { message: '유효하지 않은 iconKey 입니다.' },
+  ),
 })

참고 대안:

  • UI 쪽에서 IconListreadonly [IconType, ...IconType[]] 튜플로 보장하도록 내보내면 z.enum(IconList) 사용이 가능합니다. 다만 현재 Object.keys 기반 구현에서는 튜플 보장이 어려워 위 방식이 안전합니다.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
id: z.number().transform(String),
name: z.string(),
iconKey: z.enum(IconList),
})
// apps/web/app/_apis/schemas/category.ts
import { z } from 'zod'
import { IconList } from '@repo/ui/components/Icon/IconMap'
import type { IconType } from '@repo/ui/components/Icon'
export const CategorySchema = z.object({
id: z.number().transform(String),
name: z.string(),
iconKey: z.custom<IconType>(
(v) => typeof v === 'string' && (IconList as readonly string[]).includes(v),
{ message: '유효하지 않은 iconKey 입니다.' },
),
})
🤖 Prompt for AI Agents
In apps/web/app/_apis/schemas/category.ts around lines 5 to 8, z.enum(IconList)
is unsafe because IconList is inferred as string[] not a readonly tuple and will
cause TypeScript errors; replace the z.enum usage with z.custom<IconType> that
performs a runtime check asserting the value is a string and is included in
IconList (use IconList.includes(value)) and ensure IconType is imported/defined
so z.custom is correctly typed; alternatively export IconList as a readonly
tuple on the UI side if you prefer to keep z.enum.


export type Category = z.infer<typeof CategorySchema>
20 changes: 20 additions & 0 deletions apps/web/app/_apis/schemas/place.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { z } from 'zod'
import { CategorySchema } from '@/_apis/schemas/category'

export const BasePlaceSchema = z.object({
placeId: z.number().transform(String),
placeName: z.string(),
address: z.string(),
categories: z.array(CategorySchema),
tags: z.array(CategorySchema),
})

export type RankingPlaceSort = 'views' | 'likes'

export const RankingPlaceSchema = BasePlaceSchema.extend({
isLiked: z.boolean(),
likeCount: z.number().int().nonnegative(),
})

export type BasePlace = z.infer<typeof BasePlaceSchema>
export type RankingPlace = z.infer<typeof RankingPlaceSchema>
8 changes: 8 additions & 0 deletions apps/web/app/_apis/services/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import axiosInstance from '@/_lib/axiosInstance'
import { API_PATH } from '@/_constants/path'
import { CategorySchema, Category } from '../schemas/category'

export const getCategories = async (): Promise<Category[]> => {
const { data } = await axiosInstance.get(API_PATH.CATEGORY)
return CategorySchema.array().parse(data)
}
14 changes: 14 additions & 0 deletions apps/web/app/_apis/services/place.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import axiosInstance from '@/_lib/axiosInstance'
import { API_PATH } from '@/_constants/path'
import {
type RankingPlace,
type RankingPlaceSort,
RankingPlaceSchema,
} from '../schemas/place'

export const getRankingPlaces = async (
sort: RankingPlaceSort,
): Promise<RankingPlace[]> => {
const { data } = await axiosInstance.get(API_PATH.RANKING(sort))
return RankingPlaceSchema.array().parse(data)
}
79 changes: 79 additions & 0 deletions apps/web/app/_components/Banner/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use client'

import 'keen-slider/keen-slider.min.css'
import { useKeenSlider } from 'keen-slider/react'

type Props = {
contents: React.ReactNode[]
minHeight?: number
}

/**
* Banner 컴포넌트
*
* - 여러 콘텐츠를 순차적으로 보여주는 슬라이더 배너입니다.
* - `keen-slider`를 기반으로 자동 재생(loop) 기능을 제공합니다.
* - 마우스를 올리면 자동 재생이 일시 정지되고, 마우스를 치우면 다시 재생됩니다.
*
* @param contents 렌더링할 React 노드 배열 (각각의 배너 콘텐츠)
* @param minHeight 배너의 최소 높이(px). 기본값은 150입니다.
*
* @example
* ```tsx
* <Banner
* contents={[
* <div>배너 1</div>,
* <div>배너 2</div>,
* <div>배너 3</div>,
* ]}
* minHeight={200}
* />
* ```
*/
export const Banner = ({ contents, minHeight = 150 }: Props) => {
const [sliderRef] = useKeenSlider<HTMLDivElement>(
{
loop: true,
},
[
(slider) => {
let timeout: ReturnType<typeof setTimeout>
let mouseOver = false
function clearNextTimeout() {
clearTimeout(timeout)
}
function nextTimeout() {
clearTimeout(timeout)
if (mouseOver) return
timeout = setTimeout(() => {
slider.next()
}, 2000)
}
slider.on('created', () => {
slider.container.addEventListener('mouseover', () => {
mouseOver = true
clearNextTimeout()
})
slider.container.addEventListener('mouseout', () => {
mouseOver = false
nextTimeout()
})
nextTimeout()
Comment on lines +40 to +61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

타이머 초기화 안전성 및 이벤트 리스너 정리 누락 — 메모리 누수/경합 방지 리팩터 권장

  • timeout이 초기화 전 clearTimeout 대상이 될 수 있습니다. null 가드가 안전합니다.
  • addEventListener로 등록한 핸들러는 destroyed 시점에 removeEventListener로 해제해야 합니다.
  • mouseout 대신 mouseleave(버블링 없음)를 사용하면 자식 엘리먼트로 이동 시 불필요한 pause/resume 이벤트가 줄어듭니다.

아래와 같이 보완을 제안합니다.

-        let timeout: ReturnType<typeof setTimeout>
+        let timeout: ReturnType<typeof setTimeout> | null = null
         let mouseOver = false
         function clearNextTimeout() {
-          clearTimeout(timeout)
+          if (timeout !== null) {
+            clearTimeout(timeout)
+            timeout = null
+          }
         }
         function nextTimeout() {
-          clearTimeout(timeout)
+          clearNextTimeout()
           if (mouseOver) return
-          timeout = setTimeout(() => {
+          timeout = setTimeout(() => {
             slider.next()
           }, 2000)
         }
-        slider.on('created', () => {
-          slider.container.addEventListener('mouseover', () => {
-            mouseOver = true
-            clearNextTimeout()
-          })
-          slider.container.addEventListener('mouseout', () => {
-            mouseOver = false
-            nextTimeout()
-          })
-          nextTimeout()
-        })
+        const onMouseEnter = () => {
+          mouseOver = true
+          clearNextTimeout()
+        }
+        const onMouseLeave = () => {
+          mouseOver = false
+          nextTimeout()
+        }
+        slider.on('created', () => {
+          slider.container.addEventListener('mouseenter', onMouseEnter)
+          slider.container.addEventListener('mouseleave', onMouseLeave)
+          nextTimeout()
+        })
         slider.on('dragStarted', clearNextTimeout)
         slider.on('animationEnded', nextTimeout)
         slider.on('updated', nextTimeout)
+        slider.on('destroyed', () => {
+          slider.container.removeEventListener('mouseenter', onMouseEnter)
+          slider.container.removeEventListener('mouseleave', onMouseLeave)
+          clearNextTimeout()
+        })

Also applies to: 63-66

🤖 Prompt for AI Agents
In apps/web/app/_components/Banner/Banner.tsx around lines 40 to 61 (also
applies to 63-66): the current timer and mouse event handling can cause unsafe
clearTimeout calls, missing listener cleanup and noisy mouseout events. Guard
clearTimeout by checking timeout !== undefined/null before clearing, replace
'mouseout' with 'mouseleave' to avoid bubbling issues, store the event handler
functions in variables so you can call removeEventListener with the exact
references, and in the slider destroyed/detached handler clear the timeout (with
a null guard) and remove the registered mouseenter/mouseleave listeners to
prevent leaks and race conditions.

})
slider.on('dragStarted', clearNextTimeout)
slider.on('animationEnded', nextTimeout)
slider.on('updated', nextTimeout)
},
],
)

return (
<div ref={sliderRef} className={'keen-slider'} style={{ minHeight }}>
{contents.map((content, index) => (
<div key={index} className='keen-slider__slide bg-gray-100'>
{content}
</div>
))}
</div>
)
}
1 change: 1 addition & 0 deletions apps/web/app/_components/Banner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Banner } from './Banner'
30 changes: 30 additions & 0 deletions apps/web/app/_components/BottomNavigation/BottomNavigation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { TabItem, type TabItemProps } from './TabItem'
import { cn } from '@repo/ui/utils/cn'
import { JustifyBetween } from '@repo/ui/components/Layout'

const tabs: TabItemProps[] = [
{ path: 'MAIN', label: '메인', icon: 'home' },
{ path: 'MAP', label: '주변 맛집', icon: 'map' },
{ path: 'PLACE_NEW', label: '', icon: 'circlePlus', iconSize: 50 },
{ path: 'LIKES', label: '찜', icon: 'navHeart' },
{ path: 'PROFILE', label: '내 정보', icon: 'navUser' },
]

export const BottomNavigation = () => {
return (
<JustifyBetween
as={'nav'}
className={cn(
'border-t-1',
'border-gray-50',
'h-15',
'mt-auto',
'px-5 py-2.5',
)}
>
{tabs.map((tab: TabItemProps) => (
<TabItem key={tab.path} {...tab} />
))}
</JustifyBetween>
)
}
42 changes: 42 additions & 0 deletions apps/web/app/_components/BottomNavigation/TabItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client'

import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { CLIENT_PATH } from '@/_constants/path'
import { cn } from '@repo/ui/utils/cn'
import { Text } from '@repo/ui/components/Text'
import { Icon, IconType } from '@repo/ui/components/Icon'

export type TabItemProps = {
path: keyof Pick<
typeof CLIENT_PATH,
'MAIN' | 'MAP' | 'LIKES' | 'PROFILE' | 'PLACE_NEW'
>
icon: IconType
iconSize?: number
label?: string
}

export const TabItem = ({ path, label, icon, iconSize = 26 }: TabItemProps) => {
const pathname = usePathname()
const href = CLIENT_PATH[path]
const active = pathname === href

return (
<Link href={href} className={'w-12.5 flex flex-col items-center'}>
<Icon
type={icon}
size={iconSize}
color={active ? '--color-main' : '--color-gray-200'}
/>
Comment on lines +27 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Icon color에 CSS 변수를 직접 전달할 때의 처리 로직 확인 필요

현재 color={ '--color-main' } 형태로 전달하고 있는데, 아이콘 컴포넌트가 이를 var(--color-main)로 래핑하지 않는다면 브라우저가 유효한 색상으로 인식하지 못합니다. 위 제안처럼 var(--color-*) 형태로 넘기거나, Icon 내부에서 -- 프리픽스를 감지해 var(...)로 변환하는 로직이 있는지 확인 부탁드립니다.

레포에서 Icon 컴포넌트의 color 처리 로직을 확인하는 스크립트:


🏁 Script executed:

#!/bin/bash
set -e

echo "Icon 컴포넌트에서 color 처리 로직 탐색"
rg -n -C2 "export.*Icon(?!List)" packages/ui/src/components/Icon || true
rg -n -C2 "color\??:\s*string" packages/ui/src/components/Icon || true
rg -n -C2 "var\\(--" packages/ui/src/components/Icon || true

echo "아이콘 에셋에서 color prop 사용 패턴 탐색(fill={color})"
rg -n -C2 "fill=\{color" packages/ui/src/components/Icon/assets/icons || true

Length of output: 12403


Icon 컴포넌트가 CSS 변수 문자열을 처리하지 않으므로 --color-* 대신 반드시 var(--color-*) 형태로 전달하거나, Icon 내부에서 -- 접두사를 감지해 var(...)로 감싸주는 로직을 추가해야 합니다.

  • TabItem.tsx (apps/web/app/_components/BottomNavigation/TabItem.tsx:27–31)
    color={active ? '--color-main' : '--color-gray-200'}
    • 즉시 color={active ? 'var(--color-main)' : 'var(--color-gray-200)'}로 변경

  • 또는 Icon 컴포넌트 수정 (packages/ui/src/components/Icon/**/*.tsx)
    color prop 값이 --로 시작하면 내부에서 var(${color})로 자동 변환하는 유틸 로직 추가

위 두 가지 중 하나를 선택해 적용해주세요.

🤖 Prompt for AI Agents
apps/web/app/_components/BottomNavigation/TabItem.tsx lines 27–31: the Icon
component is receiving raw CSS variable names like "--color-main" which it
doesn't handle; update the color prop to pass CSS variables using var(...) by
changing color={active ? '--color-main' : '--color-gray-200'} to color={active ?
'var(--color-main)' : 'var(--color-gray-200)'}; alternatively (if you prefer
central handling) modify the Icon component in packages/ui to detect values
starting with "--" and wrap them as var(<value>) before applying, but for a
minimal fix update TabItem to use var(...) as shown.

{label && (
<Text
fontWeight={'normal'}
className={cn('text-[10px]', active ? 'text-main' : 'text-gray-200')}
>
{label}
</Text>
)}
</Link>
)
}
1 change: 1 addition & 0 deletions apps/web/app/_components/BottomNavigation/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BottomNavigation } from './BottomNavigation'
20 changes: 20 additions & 0 deletions apps/web/app/_components/Categories/Categories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
'use client'

import { useSuspenseQuery } from '@tanstack/react-query'
import { useCategoryQueries } from '@/_apis/queries/category'
import { cn } from '@repo/ui/utils/cn'
import { CategoryItem } from './CategoryItem'

export const Categories = () => {
const { data: categories } = useSuspenseQuery(useCategoryQueries.list())

return (
<div
className={cn('grid grid-flow-row grid-cols-5 gap-y-6', 'px-7.5 py-2.5')}
>
{categories.map((category) => (
<CategoryItem key={category.id} {...category} />
))}
</div>
)
}
23 changes: 23 additions & 0 deletions apps/web/app/_components/Categories/CategoryItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CLIENT_PATH } from '@/_constants/path'
import { Category } from '@/_apis/schemas/category'
import { Icon } from '@repo/ui/components/Icon'
import { Text } from '@repo/ui/components/Text'
import { Column } from '@repo/ui/components/Layout'

export const CategoryItem = ({ id, name, iconKey }: Category) => (
<Column
as={'a'}
href={CLIENT_PATH.CATEGORY_DETAIL(id)}
className={'items-center gap-1'}
>
<Icon type={iconKey} size={40} />
<Text
as={'span'}
fontSize={'sm'}
fontWeight={'semibold'}
className={'text-main text-nowrap'}
>
{name}
</Text>
</Column>
)
1 change: 1 addition & 0 deletions apps/web/app/_components/Categories/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Categories } from './Categories'
Loading