-
-
개설자
- {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/dashboard/open/ApplicantsDialog.tsx b/apps/web/src/components/dashboard/open/ApplicantsDialog.tsx
index 4b58f356..226d22ab 100644
--- a/apps/web/src/components/dashboard/open/ApplicantsDialog.tsx
+++ b/apps/web/src/components/dashboard/open/ApplicantsDialog.tsx
@@ -2,6 +2,7 @@ import { DashboardApplicantsResponse } from '@repo/types';
import Avatar from '@/components/common/Avatar';
import { Dialog } from '@/components/common/Dialog';
+import Empty from '@/components/common/Empty';
interface ApplicantsDialogProps {
isOpen: boolean;
@@ -15,13 +16,17 @@ function ApplicantsDialog({ isOpen, onClose, applicants }: ApplicantsDialogProps
신청자 목록
-
- {applicants.map((applicant) => (
- -
-
- {applicant.user.nickname}
-
- ))}
+
+ {applicants.length === 0 ? (
+
+ ) : (
+ applicants.map((applicant) => (
+ -
+
+ {applicant.user.nickname}
+
+ ))
+ )}
diff --git a/apps/web/src/components/dashboard/open/TicleInfoCard.tsx b/apps/web/src/components/dashboard/open/TicleInfoCard.tsx
index adcb1b36..702a941e 100644
--- a/apps/web/src/components/dashboard/open/TicleInfoCard.tsx
+++ b/apps/web/src/components/dashboard/open/TicleInfoCard.tsx
@@ -1,4 +1,5 @@
-import { Link } from '@tanstack/react-router';
+import { Link, useNavigate } from '@tanstack/react-router';
+import { MouseEvent } from 'react';
import PersonFilledIc from '@/assets/icons/person-filled.svg?react';
import Button from '@/components/common/Button';
@@ -19,40 +20,56 @@ interface TicleInfoCardProps {
function TicleInfoCard({ ticleId, ticleTitle, startTime, endTime, status }: TicleInfoCardProps) {
const { isOpen, onOpen, onClose } = useModal();
- const { data: applicantsData, isLoading } = useApplicantsTicle(ticleId.toString());
+ const { data: applicantsData } = useApplicantsTicle(ticleId.toString());
const { dateStr, timeRangeStr } = formatDateTimeRange(startTime, endTime);
+ const navigate = useNavigate();
+
if (!applicantsData) return;
+ const handleTicleParticipate = (e: MouseEvent
) => {
+ e.preventDefault();
+ navigate({ to: `/live/${ticleId}` });
+ };
+
+ const handleApplicantsDialogOpen = (e: MouseEvent) => {
+ e.preventDefault();
+ onOpen();
+ };
+
return (
-
-
-
-
티클명
-
{ticleTitle}
+
+
+
+
+
티클명
+ {ticleTitle}
+
+
+
진행 일시
+ {`${dateStr} ${timeRangeStr}`}
+
-
-
진행 일시
-
{`${dateStr} ${timeRangeStr}`}
+
+
+
+ {isOpen && (
+
+ )}
-
-
-
-
-
-
- {isOpen &&
}
-
+
);
}
diff --git a/apps/web/src/components/dashboard/open/index.tsx b/apps/web/src/components/dashboard/open/index.tsx
index 724836b0..2c6642dc 100644
--- a/apps/web/src/components/dashboard/open/index.tsx
+++ b/apps/web/src/components/dashboard/open/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,53 @@ function Open() {
setSelectedOption(option);
};
- const { data: { ticles, meta } = { ticles: [], meta: {} }, isLoading } = useDashboardTicleList({
- isSpeaker: true,
- page: 1,
- pageSize: 10,
- ...(selectedOption.value && { status: selectedOption.value as 'open' | 'closed' }),
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useDashboardTicleList(
+ {
+ isSpeaker: true,
+ 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