diff --git a/apps/web/src/components/common/Empty/index.tsx b/apps/web/src/components/common/Empty/index.tsx new file mode 100644 index 00000000..010c4363 --- /dev/null +++ b/apps/web/src/components/common/Empty/index.tsx @@ -0,0 +1,30 @@ +import TicleCharacter from '@/assets/images/ticle-character.png'; +import cn from '@/utils/cn'; + +interface EmptyProps { + title?: string; + className?: string; + imageSize?: number; +} + +function Empty({ title = '항목이 비어있어요!', className, imageSize = 180 }: EmptyProps) { + return ( +
+ 흑백 티클 캐릭터 +

{title}

+
+ ); +} + +export default Empty; diff --git a/apps/web/src/components/Header/index.tsx b/apps/web/src/components/common/Header/index.tsx similarity index 100% rename from apps/web/src/components/Header/index.tsx rename to apps/web/src/components/common/Header/index.tsx diff --git a/apps/web/src/components/common/Loading/Loading.tsx b/apps/web/src/components/common/Loading/Loading.tsx deleted file mode 100644 index 9095f83f..00000000 --- a/apps/web/src/components/common/Loading/Loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const Loading = () => { - return ; -}; - -export default Loading; diff --git a/apps/web/src/components/common/Loading/index.tsx b/apps/web/src/components/common/Loading/index.tsx new file mode 100644 index 00000000..b1cb36d3 --- /dev/null +++ b/apps/web/src/components/common/Loading/index.tsx @@ -0,0 +1,32 @@ +import { cva } from 'class-variance-authority'; + +const dotVariants = cva('h-4 w-4 rounded-full', { + variants: { + variant: { + white: ['animate-[flashWhite_1.5s_ease-out_infinite_alternate] bg-altWeak'], + primary: ['animate-[flashPrimary_1.5s_ease-out_infinite_alternate] bg-secondary'], + }, + position: { + first: '', + second: '[animation-delay:0.5s]', + third: '[animation-delay:1s]', + }, + }, + defaultVariants: { + variant: 'white', + }, +}); + +type LoadingProps = { + color?: 'white' | 'primary'; +}; + +const Loading = ({ color = 'white' }: LoadingProps) => ( +
+
+
+
+
+); + +export default Loading; diff --git a/apps/web/src/components/common/Tab/index.tsx b/apps/web/src/components/common/Tab/index.tsx index 5ba63fb9..cdf7cbf0 100644 --- a/apps/web/src/components/common/Tab/index.tsx +++ b/apps/web/src/components/common/Tab/index.tsx @@ -1,16 +1,17 @@ import { KeyboardEvent } from 'react'; -export interface TabData { - name: string; +export interface TabData { + value: T; + label: string; onClick: () => void; } -interface TabProps { - tabItems: TabData[]; - selectedTab: string; +interface TabProps { + tabItems: TabData[]; + selectedTab: T; } -function Tab({ tabItems, selectedTab }: TabProps) { +function Tab({ tabItems, selectedTab }: TabProps) { const handleKeyDown = (e: KeyboardEvent, onClick: () => void) => { if (e.key !== 'Enter') return; onClick(); @@ -20,15 +21,15 @@ function Tab({ tabItems, selectedTab }: TabProps) {
{tabItems.map((tab) => ( ))}
diff --git a/apps/web/src/components/dashboard/DashboardTab.tsx b/apps/web/src/components/dashboard/DashboardTab.tsx index 224f4424..e4014ac4 100644 --- a/apps/web/src/components/dashboard/DashboardTab.tsx +++ b/apps/web/src/components/dashboard/DashboardTab.tsx @@ -1,5 +1,4 @@ -import { useNavigate } from '@tanstack/react-router'; -import { useState } from 'react'; +import { useMatch, useNavigate } from '@tanstack/react-router'; import Tab, { TabData } from '../common/Tab'; @@ -15,21 +14,22 @@ const DASHBOARD_ROUTES = { function DashboardTab() { const navigate = useNavigate(); - const [selectedTab, setSelectedTab] = useState(DASHBOARD_TAB.APPLIED); + const isOpenedMatch = useMatch({ from: '/dashboard/open', shouldThrow: false }); + const selectedTab = isOpenedMatch ? 'OPENED' : 'APPLIED'; - const DASHBOARD_TAB_DATA: TabData[] = [ + const DASHBOARD_TAB_DATA: TabData[] = [ { - name: DASHBOARD_TAB.APPLIED, + value: 'APPLIED', + label: DASHBOARD_TAB.APPLIED, onClick: () => { navigate({ to: DASHBOARD_ROUTES.APPLIED }); - setSelectedTab(DASHBOARD_TAB.APPLIED); }, }, { - name: DASHBOARD_TAB.OPENED, + value: 'OPENED', + label: DASHBOARD_TAB.OPENED, onClick: () => { navigate({ to: DASHBOARD_ROUTES.OPENED }); - setSelectedTab(DASHBOARD_TAB.OPENED); }, }, ]; diff --git a/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx b/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx index 3ef0a019..cd6e75a5 100644 --- a/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx +++ b/apps/web/src/components/dashboard/apply/TicleInfoCard.tsx @@ -1,4 +1,5 @@ -import { Link } from '@tanstack/react-router'; +import { Link, useNavigate } from '@tanstack/react-router'; +import { MouseEvent } from 'react'; import Button from '@/components/common/Button'; import { formatDateTimeRange } from '@/utils/date'; @@ -21,27 +22,36 @@ function TicleInfoCard({ status, }: TicleInfoCardProps) { const { dateStr, timeRangeStr } = formatDateTimeRange(startTime, endTime); + const navigate = useNavigate(); + + const handleTicleParticipate = (e: MouseEvent) => { + e.preventDefault(); + navigate({ to: `/live/${ticleId}` }); + }; return ( -
-
-
-

개설자

- {speakerName} -
-
-

티클명

- {ticleTitle} -
-
-

진행 일시

- {`${dateStr} ${timeRangeStr}`} + +
+
+
+

개설자

+ {speakerName} +
+
+

티클명

+ {ticleTitle} +
+
+

진행 일시

+ {`${dateStr} ${timeRangeStr}`} +
+ +
- - - -
+ ); } diff --git a/apps/web/src/components/dashboard/apply/index.tsx b/apps/web/src/components/dashboard/apply/index.tsx index de846359..9517e2a9 100644 --- a/apps/web/src/components/dashboard/apply/index.tsx +++ b/apps/web/src/components/dashboard/apply/index.tsx @@ -1,7 +1,10 @@ -import { useState } from 'react'; +import { Fragment, useState } from 'react'; +import Empty from '@/components/common/Empty'; +import Loading from '@/components/common/Loading'; import Select, { Option } from '@/components/common/Select'; import { useDashboardTicleList } from '@/hooks/api/dashboard'; +import useIntersectionObserver from '@/hooks/useIntersectionObserver'; import TicleInfoCard from './TicleInfoCard'; @@ -26,28 +29,55 @@ function Apply() { setSelectedOption(option); }; - const { data: { ticles, meta } = { ticles: [], meta: {} }, isLoading } = useDashboardTicleList({ - isSpeaker: false, - page: 1, - pageSize: 10, - ...(selectedOption.value && { status: selectedOption.value as 'open' | 'closed' }), + const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useDashboardTicleList( + { + isSpeaker: false, + page: 1, + pageSize: 10, + ...(selectedOption.value && { status: selectedOption.value as 'open' | 'closed' }), + } + ); + + const { ref: intersectionRef } = useIntersectionObserver({ + hasNextPage, + fetchNextPage, }); return (
-
- {ticles.map((ticle) => ( - - ))} -
+ {isLoading || !data ? ( +
+ +
+ ) : !data.pages[0]?.ticles?.length ? ( + + ) : ( +
+ {data?.pages.map((page) => ( + + {page.ticles.map((ticle) => ( + + ))} + + ))} +
+ {isFetchingNextPage && ( +
+ +
+ )} +
+ )}
); } diff --git a/apps/web/src/components/live/VideoPlayer.tsx b/apps/web/src/components/live/VideoPlayer.tsx index b781c3b6..a4884d47 100644 --- a/apps/web/src/components/live/VideoPlayer.tsx +++ b/apps/web/src/components/live/VideoPlayer.tsx @@ -6,7 +6,7 @@ import MicOnIc from '@/assets/icons/mic-on.svg?react'; import Avatar from '../common/Avatar'; import Badge from '../common/Badge'; -import Loading from '../common/Loading/Loading'; +import Loading from '../common/Loading'; const videoVariants = cva('h-full w-full rounded-lg object-cover transition-opacity duration-300', { variants: { diff --git a/apps/web/src/components/ticle/detail/index.tsx b/apps/web/src/components/ticle/detail/index.tsx index ccfcf98e..ba20a923 100644 --- a/apps/web/src/components/ticle/detail/index.tsx +++ b/apps/web/src/components/ticle/detail/index.tsx @@ -10,16 +10,13 @@ import { formatDateTimeRange } from '@/utils/date'; function Detail() { const { ticleId } = useParams({ from: '/ticle/$ticleId' }); - const navigate = useNavigate({ from: `/ticle/${ticleId}` }); - const { data, isLoading } = useTicle(ticleId); + const { data } = useTicle(ticleId); const { mutate } = useApplyTicle(); const handleApplyButtonClick = () => { mutate(ticleId); - navigate({ to: `/dashboard/apply` }); }; - // TODO: 티클 신청 완료시 alert띄우기 if (!data) return; const { dateStr, timeRangeStr } = formatDateTimeRange(data.startTime, data.endTime); diff --git a/apps/web/src/components/ticle/list/TicleCard.tsx b/apps/web/src/components/ticle/list/TicleCard.tsx index 4c38873c..6e4a13ad 100644 --- a/apps/web/src/components/ticle/list/TicleCard.tsx +++ b/apps/web/src/components/ticle/list/TicleCard.tsx @@ -21,10 +21,10 @@ const TicleCard = ({ speakerProfileImg, }: TicleCardProps) => { return ( -
+

{title}

-
+
{tags.map((tag) => ( {tag} ))} diff --git a/apps/web/src/components/ticle/list/index.tsx b/apps/web/src/components/ticle/list/index.tsx index 507d2924..52d602bc 100644 --- a/apps/web/src/components/ticle/list/index.tsx +++ b/apps/web/src/components/ticle/list/index.tsx @@ -1,13 +1,14 @@ import { Link } from '@tanstack/react-router'; -import { useState } from 'react'; +import { Fragment, useState } from 'react'; -import Loading from '@/components/common/Loading/Loading'; -import SearchInput from '@/components/common/SearchInput'; +import Empty from '@/components/common/Empty'; +import Loading from '@/components/common/Loading'; import Select, { Option } from '@/components/common/Select'; +import Tab, { TabData } from '@/components/common/Tab'; import { useTicleList } from '@/hooks/api/ticle'; +import useIntersectionObserver from '@/hooks/useIntersectionObserver'; import { formatDateTimeRange } from '@/utils/date'; -import Banner from './Banner'; import TicleCard from './TicleCard'; const getDateString = (startTime: string, endTime: string) => { @@ -30,43 +31,82 @@ const SORT_OPTIONS: Option[] = [ }, ]; +const TICLE_LIST_TAB = { + OPENED: '진행 예정 티클', + CLOSED: '종료된 티클', +} as const; + function TicleList() { const [sortOption, setSortOption] = useState