Skip to content

Commit

Permalink
feat: [FE] add event list page (#40)
Browse files Browse the repository at this point in the history
  • Loading branch information
yuchem2 authored Dec 9, 2024
1 parent e48724b commit 2120cf5
Show file tree
Hide file tree
Showing 13 changed files with 416 additions and 1 deletion.
1 change: 1 addition & 0 deletions apps/web/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const SERVER_URL = process.env.NEXT_PUBLIC_API_SERVER_URL
4 changes: 3 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"@tanstack/react-query": "^5.62.2",
"next": "15.0.2",
"react": "19.0.0-rc-02c0e824-20241028",
"react-dom": "19.0.0-rc-02c0e824-20241028"
"react-dom": "19.0.0-rc-02c0e824-20241028",
"react-intersection-observer": "^9.13.1",
"zustand": "^5.0.2"
},
"devDependencies": {
"@next/eslint-plugin-next": "^15.0.2",
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/api/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IGetEventsByCategoryResponse } from '@/types/event'
import { SERVER_URL } from '../../config'

export async function getEventsByCategory(category: string, page: number, limit: number): Promise<IGetEventsByCategoryResponse> {
const res = await fetch(`${SERVER_URL}/v1/events/category/${category}?page=${page}&limit=${limit}`, {
method: 'GET',
// TODO: add authorization Header
})

if (!res.ok) {
throw new Error(`code: ${res.status}\ndescription: ${res.statusText}`)
}
return res.json()
}
38 changes: 38 additions & 0 deletions apps/web/src/app/events/category/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client'

import { useRouter } from 'next/navigation'

import { EventList } from '@/components/events/eventList'
import { BackButtonIcon } from '@/components/icon'
import { categoryStore } from '@/store'

export default function Page() {
const router = useRouter()
const { category, setCategory } = categoryStore()

const onClick = () => {
router.back()
setCategory('')
}
if (!category) {
return (
<div className="h-screen flex items-center justify-center px-4">
<div className="text-lg text-center">잘못된 접근입니다.</div>
</div>
)
}
return (
<div className="h-min-screen items-center justify-items-center min-h-screen bg-inherit overflow-hidden">
{/*TODO: Remove temporary header*/}
<div className="flex flex-row items-center w-full pt-10 p-2 fixed top-0 left-0 right-0 z-10 bg-inherit">
<button className="absolute z-1 ps-4" onClick={onClick}>
<BackButtonIcon width={30} height={30} />
</button>
<div className="text-3xl text-center font-semibold w-full">{category}</div>
</div>
<div className="w-full h-full bg-inherit mt-24 overflow-y-auto">
<EventList category={category} />
</div>
</div>
)
}
27 changes: 27 additions & 0 deletions apps/web/src/app/events/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use client'

import { CategoryItem } from '@/components/events/categoryItem'

export default function Page() {
// TODO: add get category names
return (
<div className="grid justify-items-center px-8 mt-6">
<div className="grid border-b gap-4 w-full py-2 border-gray-300">
<CategoryItem category={'내가 등록한 행사'} isFavorites={false} />
<CategoryItem category={'인기 행사'} isFavorites={false} />
</div>
{/*User Favorites Categories*/}
<div className="grid border-b gap-4 w-full py-2 border-gray-300">
<CategoryItem category={'축제'} isFavorites={true} />
<CategoryItem category={'축제'} isFavorites={true} />
<CategoryItem category={'축제'} isFavorites={true} />
</div>
{/*Other Categories*/}
<div className="grid border-b gap-4 w-full py-2 border-gray-300">
<CategoryItem category={'축제'} isFavorites={false} />
<CategoryItem category={'축제'} isFavorites={false} />
<CategoryItem category={'축제'} isFavorites={false} />
</div>
</div>
)
}
40 changes: 40 additions & 0 deletions apps/web/src/components/events/categoryItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Link from 'next/link'

import { BaseCategoryIcon, HottestCategoryIcon, WrittenCategoryIcon } from '@/components/icon'
import { categoryStore } from '@/store'

interface Props {
category: string
isFavorites: boolean
}

export function CategoryItem({ category, isFavorites }: Props) {
const { setCategory } = categoryStore()
const onClick = () => {
setCategory(category)
}
// TODO: change category names
if (category === '내가 등록한 행사') {
return (
<Link className="flex flex-row w-full items-center justify-start p-4 gap-4" href={`/events/category`} onClick={onClick}>
<WrittenCategoryIcon width={40} height={40} />
<div className="text-3xl text-center">내가 등록한 행사</div>
</Link>
)
} else if (category === '인기 행사') {
return (
<Link className="flex flex-row w-full items-center justify-start p-4 gap-4" href={`/events/category`} onClick={onClick}>
<HottestCategoryIcon width={40} height={40} />
<div className="text-3xl text-center">인기 행사</div>
</Link>
)
} else {
// TODO: add logic making to Favorites
return (
<Link className="flex flex-row w-full items-center justify-start p-4 gap-4" href={`/events/category`} onClick={onClick}>
<BaseCategoryIcon width={40} height={40} isFavorites={isFavorites} />
<div className="text-3xl text-center pt-1">{category}</div>
</Link>
)
}
}
43 changes: 43 additions & 0 deletions apps/web/src/components/events/event.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { IEvent } from '@/types/event'

interface Props {
event: IEvent | undefined
}

const formatDate = (date: Date) => {
const formattedDate = new Date(date).toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
return formattedDate.replace(/\s/g, '').replace(/\.$/, '')
}

export default function Event({ event }: Props) {
if (!event) {
return (
<div className="flex justify-between gap-4 px-4 my-6 animate-pulse">
<div className="flex flex-col w-2/3 ps-2 gap-2">
<div className="w-4/5 h-6 bg-[#D9D9D9] my-2" />
<div className="w-3/4 h-3 bg-[#D9D9D9] my-2" />
<div className="w-2/3 h-3 bg-[#D9D9D9] my-2" />
</div>
<div className="flex justify-center items-center w-28 h-28 bg-[#D9D9D9] overflow-hidden me-2" />
</div>
)
}

return (
<div className="flex justify-between items-start gap-4 px-4 mb-12">
<div className="flex flex-col w-2/3 ps-2 pt-2 gap-2">
<div className="text-xl font-semibold">{event.name}</div>
<div className="text-lg text-gray-600">{`${formatDate(event.startDate)}~${formatDate(event.endDate)}`}</div>
<div className="text-lg text-gray-600">{event.address}</div>
</div>
<div className="flex justify-center items-center w-28 h-28 bg-[#D9D9D9] overflow-hidden me-2">
{/*TODO: change image tag*/}
<img src={event.photo[0]} alt="대표 사진" className="w-full h-full object-cover" />
</div>
</div>
)
}
83 changes: 83 additions & 0 deletions apps/web/src/components/events/eventList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use client'

import { useEffect } from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useInView } from 'react-intersection-observer'

import { getEventsByCategory } from '@/api/event'
import { IGetEventsByCategoryResponse } from '@/types/event'
import Event from '@/components/events/event'

interface useEventsByCategoryQueryProps {
category: string
startPage: number
}

export function EventList({ category }: { category: string }) {
const { data, isLoading, isError, fetchNextPage, isFetchingNextPage } = useEventsByCategoryQuery({
category: category,
startPage: 1,
})
const { ref, inView } = useInView()
useEffect(() => {
if (inView) {
console.log('무한 스크롤 요청중')
fetchNextPage()
}
}, [inView])

if (isLoading) {
return (
<div className="h-full w-full">
<div className="h-full w-full px-2 mt-4">
{Array.from({ length: 6 }, (_, index) => (
<Event event={undefined} key={index} />
))}
</div>
{isFetchingNextPage ? <Event event={undefined} /> : <div ref={ref} />}
</div>
)
}

if (isError) {
return (
<div className="h-screen flex items-center justify-center px-4">
<div className="text-lg text-center">
행사를 불러오는 데 실패하였습니다.
<br />
다시 시도해 주시길 바랍니다.
</div>
</div>
)
}

return (
<div className="h-full w-full">
<div className="h-full w-full px-2 mt-4">
{/*TODO: add navigation to event detail page*/}
{data && data.pages.map(events => events.events.map(event => <Event event={event} key={event._id} />))}
</div>
{isFetchingNextPage ? <Event event={undefined} /> : <div ref={ref} />}
</div>
)
}

const useEventsByCategoryQuery = ({ category, startPage }: useEventsByCategoryQueryProps) => {
const { data, isLoading, isError, fetchNextPage, isFetchingNextPage } = useInfiniteQuery<IGetEventsByCategoryResponse>({
queryKey: ['events', category],
queryFn: ({ pageParam }) => getEventsByCategory(category, pageParam as number, 6),
initialPageParam: startPage,
getNextPageParam: lastPage => {
if (lastPage.page.hasNextPage) {
return lastPage.page.page + 1
}
},
getPreviousPageParam: lastPage => {
if (lastPage.page.hasPrevPage) {
return lastPage.page.page - 1
}
},
})

return { data, isLoading, isError, fetchNextPage, isFetchingNextPage }
}
72 changes: 72 additions & 0 deletions apps/web/src/components/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
interface IconProps {
width: number
height: number
}

interface BaseCategoryProps extends IconProps {
isFavorites: boolean
}

export function BackButtonIcon({ width, height }: IconProps) {
return (
<svg width={width} height={height} viewBox="0 0 14 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 25L1 13L13 1" stroke="black" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}

export function WrittenCategoryIcon({ width, height }: IconProps) {
return (
<svg width={width} height={height} viewBox="0 0 24 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M16 1.66675H8.00004C7.26366 1.66675 6.66671 2.2637 6.66671 3.00008V5.66675C6.66671 6.40313 7.26366 7.00008 8.00004 7.00008H16C16.7364 7.00008 17.3334 6.40313 17.3334 5.66675V3.00008C17.3334 2.2637 16.7364 1.66675 16 1.66675Z"
fill="#7DDEFF"
/>
<path
d="M17.3334 4.33341H20C20.7073 4.33341 21.3856 4.61437 21.8857 5.11446C22.3858 5.61456 22.6667 6.29284 22.6667 7.00008V25.6667C22.6667 26.374 22.3858 27.0523 21.8857 27.5524C21.3856 28.0525 20.7073 28.3334 20 28.3334H4.00004C3.2928 28.3334 2.61452 28.0525 2.11442 27.5524C1.61433 27.0523 1.33337 26.374 1.33337 25.6667V7.00008C1.33337 6.29284 1.61433 5.61456 2.11442 5.11446C2.61452 4.61437 3.2928 4.33341 4.00004 4.33341H6.66671"
fill="#7DDEFF"
/>
<path d="M12 13.6667H17.3334H12Z" fill="#7DDEFF" />
<path d="M12 20.3334H17.3334H12Z" fill="#7DDEFF" />
<path d="M6.66671 13.6667H6.68004H6.66671Z" fill="#7DDEFF" />
<path d="M6.66671 20.3334H6.68004H6.66671Z" fill="#7DDEFF" />
<path
d="M17.3334 4.33341H20C20.7073 4.33341 21.3856 4.61437 21.8857 5.11446C22.3858 5.61456 22.6667 6.29284 22.6667 7.00008V25.6667C22.6667 26.374 22.3858 27.0523 21.8857 27.5524C21.3856 28.0525 20.7073 28.3334 20 28.3334H4.00004C3.2928 28.3334 2.61452 28.0525 2.11442 27.5524C1.61433 27.0523 1.33337 26.374 1.33337 25.6667V7.00008C1.33337 6.29284 1.61433 5.61456 2.11442 5.11446C2.61452 4.61437 3.2928 4.33341 4.00004 4.33341H6.66671M12 13.6667H17.3334M12 20.3334H17.3334M6.66671 13.6667H6.68004M6.66671 20.3334H6.68004M8.00004 1.66675H16C16.7364 1.66675 17.3334 2.2637 17.3334 3.00008V5.66675C17.3334 6.40313 16.7364 7.00008 16 7.00008H8.00004C7.26366 7.00008 6.66671 6.40313 6.66671 5.66675V3.00008C6.66671 2.2637 7.26366 1.66675 8.00004 1.66675Z"
stroke="black"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}

export function HottestCategoryIcon({ width, height }: IconProps) {
return (
<svg width={width} height={height} viewBox="0 0 22 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M6.33329 16.3333C7.21735 16.3333 8.06519 15.9821 8.69031 15.357C9.31544 14.7319 9.66663 13.8841 9.66663 13C9.66663 11.16 8.99996 10.3333 8.33329 9C6.90396 6.14267 8.03463 3.59467 11 1C11.6666 4.33333 13.6666 7.53333 16.3333 9.66667C19 11.8 20.3333 14.3333 20.3333 17C20.3333 18.2257 20.0919 19.4393 19.6228 20.5717C19.1538 21.7041 18.4663 22.733 17.5996 23.5997C16.7329 24.4663 15.704 25.1538 14.5717 25.6229C13.4393 26.0919 12.2256 26.3333 11 26.3333C9.77429 26.3333 8.56062 26.0919 7.42825 25.6229C6.29587 25.1538 5.26698 24.4663 4.4003 23.5997C3.53362 22.733 2.84613 21.7041 2.37708 20.5717C1.90804 19.4393 1.66663 18.2257 1.66663 17C1.66663 15.4627 2.24396 13.9413 2.99996 13C2.99996 13.8841 3.35115 14.7319 3.97627 15.357C4.60139 15.9821 5.44924 16.3333 6.33329 16.3333Z"
fill="#FF9575"
stroke="#1B1B1B"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}

export function BaseCategoryIcon({ width, height, isFavorites }: BaseCategoryProps) {
return (
<svg width={width} height={height} viewBox="0 0 30 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M15 1.6665L19.12 10.0132L28.3333 11.3598L21.6666 17.8532L23.24 27.0265L15 22.6932L6.75996 27.0265L8.33329 17.8532L1.66663 11.3598L10.88 10.0132L15 1.6665Z"
fill={`${isFavorites ? '#FF9575' : '#FFFFFF'}`}
stroke="#1B1B1B"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
13 changes: 13 additions & 0 deletions apps/web/src/store/category.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { create } from 'zustand'

interface CategoryStore {
category: string
setCategory: (name: string) => void
}

export const categoryStore = create<CategoryStore>(set => ({
category: '',
setCategory: category => {
set({ category })
},
}))
1 change: 1 addition & 0 deletions apps/web/src/store/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './category'
31 changes: 31 additions & 0 deletions apps/web/src/types/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export interface IEvent {
_id: string
name: string
address: string
location: {
type: string
coordinates: number[]
}
startDate: Date
endDate: Date
description: string
photo: string[]
cost: number
likeCount: number
commentCount: number
category: string[]
targetAudience: string[]
createdAt: Date
}

export interface IGetEventsByCategoryResponse {
page: {
totalDocs: number
totalPages: number
hasNextPage: boolean
hasPrevPage: boolean
page: number
limit: number
}
events: IEvent[]
}
Loading

0 comments on commit 2120cf5

Please sign in to comment.