Skip to content

Commit

Permalink
Merge pull request #76 from hufs-sports-live/feat/match-panel
Browse files Browse the repository at this point in the history
[FEAT] match panel 컴포넌트 생성
  • Loading branch information
seongminn authored Nov 25, 2023
2 parents 6ee9953 + 0ef03bb commit 9386f23
Show file tree
Hide file tree
Showing 17 changed files with 318 additions and 9 deletions.
2 changes: 1 addition & 1 deletion src/api/league.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Sentry from '@sentry/nextjs';
import { AxiosError } from 'axios';

import instance from './instance';
import instance from '.';

export type LeagueType = {
name: string;
Expand Down
55 changes: 48 additions & 7 deletions src/app/match/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,71 @@
'use client';

// import Link from 'next/link';

import { Suspense } from 'react';

import MatchBanner from '@/components/match/Banner';
import Cheer from '@/components/match/Cheer';
// import CommentList from '@/components/detail/CommentList';
// import GameInfo from '@/components/detail/GameInfo';
// import GameTimeline from '@/components/detail/GameTimeline';
import Lineup from '@/components/match/LineupList';
import Panel from '@/components/match/Panel';
import RecordList from '@/components/match/RecordList';
import Video from '@/components/match/Video';
import MatchByIdFetcher from '@/queries/useMatchById/Fetcher';
import MatchCheerByIdFetcher from '@/queries/useMatchCheerById/Fetcher';
import MatchLineupFetcher from '@/queries/useMatchLineupById/Fetcher';
import MatchTimelineFetcher from '@/queries/useMatchTimelineById/Fetcher';

export default function Match({ params }: { params: { id: string } }) {
const options = [
{ label: '라인업' },
{ label: '응원댓글' },
{ label: '경기영상' },
{ label: '타임라인' },
];

return (
<section>
<Suspense fallback={<div>배너 로딩 중...</div>}>
<Suspense fallback={<div>배너 로딩중...</div>}>
<MatchByIdFetcher matchId={params.id}>
{data => <MatchBanner {...data} />}
</MatchByIdFetcher>
</Suspense>
<Suspense fallback={<div>응원 로딩 중...</div>}>
<Suspense fallback={<div>응원 로딩중...</div>}>
<MatchCheerByIdFetcher matchId={params.id}>
{data => <Cheer cheers={data} />}
</MatchCheerByIdFetcher>
</Suspense>

<Panel options={options} defaultValue="라인업">
{({ selected }) => (
<Suspense fallback={<div>로딩중...</div>}>
{selected === '라인업' && (
<MatchLineupFetcher matchId={params.id}>
{([firstTeam, secondTeam]) => (
<div className="grid grid-cols-2 py-5 [&>*:first-child>ul]:before:absolute [&>*:first-child>ul]:before:right-0 [&>*:first-child>ul]:before:h-full [&>*:first-child>ul]:before:border-l-2 [&>*:first-child>ul]:before:bg-gray-2">
<Lineup {...firstTeam} />
<Lineup {...secondTeam} />
</div>
)}
</MatchLineupFetcher>
)}
{selected === '타임라인' && (
<MatchTimelineFetcher matchId={params.id}>
{([firstHalf, secondHalf]) => (
<div className="overflow-y-auto p-5">
<RecordList {...firstHalf} />
<RecordList {...secondHalf} />
</div>
)}
</MatchTimelineFetcher>
)}
{selected === '경기영상' && (
<div className="overflow-y-auto p-5">
{/* // TODO VideoId API 업데이트 시 ID를 받아와서 주입하는 형태로 수정 */}
<Video />
</div>
)}
</Suspense>
)}
</Panel>
</section>
);
}
8 changes: 8 additions & 0 deletions src/components/common/Dropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Item from './units/Item';
import Menu from './units/Menu';
import DropdownWrapper from './units/Wrapper';

export const Dropdown = Object.assign(DropdownWrapper, {
Menu,
Item,
});
11 changes: 11 additions & 0 deletions src/components/common/Dropdown/units/Item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client';

import { ComponentProps, MouseEvent } from 'react';

interface DropdownItemProps extends ComponentProps<'button'> {
onClick: (e: MouseEvent<HTMLButtonElement>) => void;
}

export default function Item({ children, ...props }: DropdownItemProps) {
return <button {...props}>{children}</button>;
}
15 changes: 15 additions & 0 deletions src/components/common/Dropdown/units/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ComponentProps } from 'react';

import { $ } from '@/utils/core';

export default function Menu({
className,
children,
...props
}: ComponentProps<'div'>) {
return (
<div className={$('flex items-center', className)} {...props}>
{children}
</div>
);
}
23 changes: 23 additions & 0 deletions src/components/common/Dropdown/units/Wrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentProps, createContext } from 'react';

interface DropdownProps extends DropdownContextType, ComponentProps<'div'> {}

type DropdownContextType = {
label?: string;
};

export const DropdownContext = createContext<DropdownContextType>({
label: '',
});

export default function DropdownWrapper({
className,
children,
...props
}: DropdownProps) {
return (
<DropdownContext.Provider value={{ ...props }}>
<div className={className}>{children}</div>
</DropdownContext.Provider>
);
}
18 changes: 18 additions & 0 deletions src/components/match/LineupItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MatchPlayerType } from '@/types/match';

export default function LineupItem({
playerName,
description,
}: MatchPlayerType) {
return (
<li
className="mb-2 grid items-center gap-4"
style={{ gridTemplateColumns: 'minmax(0, 30px) 1fr' }}
>
<span className="flex aspect-square items-center justify-center rounded-full bg-secondary leading-relaxed text-primary">
{description}
</span>
<span className="text-gray-5">{playerName}</span>
</li>
);
}
16 changes: 16 additions & 0 deletions src/components/match/LineupList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MatchLineupType } from '@/types/match';

import LineupItem from '../LineupItem';

export default function Lineup({ teamName, gameTeamPlayers }: MatchLineupType) {
return (
<div>
<div className="mb-3 px-4 text-primary">{teamName}</div>
<ul className="relative px-4">
{gameTeamPlayers.map((player, idx) => (
<LineupItem key={player.playerName + idx} {...player} />
))}
</ul>
</div>
);
}
45 changes: 45 additions & 0 deletions src/components/match/Panel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MouseEvent, ReactNode, useState } from 'react';

import { Dropdown } from '@/components/common/Dropdown';
import { $ } from '@/utils/core';

type PanelProps = {
options: Array<{ label: string }>;
children: ({ selected }: { selected: string }) => ReactNode;
defaultValue: string;
};

export default function Panel({ defaultValue, options, children }: PanelProps) {
const [selected, setSelected] = useState(defaultValue);

const handleClickItem = (e: MouseEvent<HTMLButtonElement>) => {
const selectedValue = (e.target as Element).textContent;

if (!selectedValue) return;
if (selected === selectedValue) return;

setSelected(selectedValue);
};

return (
<div>
<Dropdown className="relative rounded-xl border-2 border-gray-2">
<Dropdown.Menu className="grid w-full grid-cols-4 rounded-lg bg-gray-2 text-gray-4">
{options.map(option => (
<Dropdown.Item
onClick={handleClickItem}
key={option.label}
className={$(
'rounded-xl py-3',
selected === option.label ? 'bg-secondary text-primary' : '',
)}
>
{option.label}
</Dropdown.Item>
))}
</Dropdown.Menu>
{children({ selected })}
</Dropdown>
</div>
);
}
18 changes: 18 additions & 0 deletions src/components/match/RecordItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { MatchRecordsType } from '@/types/match';

export default function RecordItem({
playerName,
score,
scoredAt,
teamName,
}: MatchRecordsType) {
return (
<li className="relative mb-3 ms-4">
<time className="absolute -left-1/2">{scoredAt}</time>
<span>[ {teamName} ] </span>
<span>
{playerName} 선수 {score}점 득점 🎉
</span>
</li>
);
}
19 changes: 19 additions & 0 deletions src/components/match/RecordList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MatchTimelineType } from '@/types/match';

import RecordItem from '../RecordItem';

export default function RecordList({
gameQuarter,
records,
}: MatchTimelineType) {
return (
<>
<div className="mb-3 text-primary">{gameQuarter}</div>
<ol className="ms-5 border-s">
{records.map(record => (
<RecordItem key={record.scoredAt} {...record} />
))}
</ol>
</>
);
}
11 changes: 11 additions & 0 deletions src/components/match/Video/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function Video() {
return (
<iframe
className="aspect-video w-full"
src="https://www.youtube.com/embed/rLtgA5qQryM?si=w-py0FFuiHisx-k-"
title="Match Video"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
></iframe>
);
}
21 changes: 21 additions & 0 deletions src/queries/useMatchLineupById/Fetcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ReactNode } from 'react';

import { MatchLineupType } from '@/types/match';

import { useMatchLineupById } from './query';

type MatchLineupFetcherProps = {
matchId: string;
children: (data: MatchLineupType[]) => ReactNode;
};

export default function MatchLineupFetcher({
matchId,
children,
}: MatchLineupFetcherProps) {
const { lineup, error } = useMatchLineupById(matchId);

if (error) throw error;

return children(lineup);
}
15 changes: 15 additions & 0 deletions src/queries/useMatchLineupById/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { useSuspenseQuery } from '@tanstack/react-query';

import { getMatchLineupById } from '@/api/match';

export const useMatchLineupById = (matchId: string) => {
const { data, error } = useSuspenseQuery({
queryKey: ['match-lineup', matchId],
queryFn: () => getMatchLineupById(matchId),
});

return {
lineup: data,
error,
};
};
31 changes: 31 additions & 0 deletions src/queries/useMatchTimelineById/Fetcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ReactNode } from 'react';

import { useMatchTimelineById } from './query';

type MatchRecordsType = {
scoredAt: number;
playerName: string;
teamName: string;
score: number;
};

type MatchTimelineType = {
gameQuarter: string;
records: MatchRecordsType[];
};

type MatchTimelineFetcherProps = {
matchId: string;
children: (data: MatchTimelineType[]) => ReactNode;
};

export default function MatchTimelineFetcher({
matchId,
children,
}: MatchTimelineFetcherProps) {
const { timeline, error } = useMatchTimelineById(matchId);

if (error) throw error;

return children(timeline);
}
17 changes: 17 additions & 0 deletions src/queries/useMatchTimelineById/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useSuspenseQuery } from '@tanstack/react-query';

import { getMatchTimelineById } from '@/api/match';

export const useMatchTimelineById = (matchId: string) => {
const { data, error } = useSuspenseQuery({
queryKey: ['match-timeline', matchId],
queryFn: () => getMatchTimelineById(matchId),
});

if (error) throw error;

return {
timeline: data,
error,
};
};
2 changes: 1 addition & 1 deletion src/types/match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export type MatchRecordsType = {
};

export type MatchTimelineType = {
gameQuater: MatchQuarterType;
gameQuarter: MatchQuarterType;
records: MatchRecordsType[];
};

Expand Down

0 comments on commit 9386f23

Please sign in to comment.