Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/time table #113

Merged
merged 12 commits into from
Sep 23, 2024
Merged
49 changes: 28 additions & 21 deletions src/pages/Timetable/.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,30 @@ export const section = style({
});

const baseFilterButton = style({
position: "relative",
padding: "10px 15px",
margin: "0 5px 10px 0",
border: "none",
borderRadius: "20px",
cursor: "pointer",
transition: "background-color 0.3s ease",
backgroundColor: "rgba(79, 205, 197, 0.5)",
transition: "all 0.3s ease",
overflow: "hidden",

"::before": {
content: '""',
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(255, 255, 255, 0.5)",
transition: "background-color 0.3s ease",
zIndex: -1,
},

"@media": {
"screen and (max-width: 768px)": {
padding: "16px 26px",
padding: "8px 12px",
margin: "0 3px 8px 0",
},
},
Expand All @@ -37,34 +51,26 @@ export const filterButton = styleVariants({
baseFilterButton,
{
color: vars.color.white,
":hover": {

":hover::before": {
backgroundColor: "rgba(255, 255, 255, 0.2)",
},
},
],
active: [
baseFilterButton,
{
backgroundImage: "linear-gradient(to top left, #04D1C3, #009efd, #FFFFFF)",
fontWeight: "bold",
backgroundColor: "rgba(255, 255, 255, 0.3)", // 강조 네모 배경
color: "transparent", // 텍스트 색상 투명
backgroundImage: "linear-gradient(to top left, #04D1C3,#009efd, #FFFFFF)", // 텍스트 그라데이션
WebkitBackgroundClip: "text", // 텍스트에만 그라데이션 적용
WebkitTextFillColor: "transparent", // 텍스트 색상 채우기 제거
position: "relative", // 텍스트와 배경 분리
WebkitBackgroundClip: "text",
backgroundClip: "text",
color: "transparent",

"::before": {
// 배경 유지
content: '""',
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(255, 255, 255, 0.3)", // 강조 네모 배경
borderRadius: "20px", // 배경의 모서리 둥글게
zIndex: -1, // 배경이 텍스트 뒤로 가게
backgroundColor: "rgba(255, 255, 255, 0.5)",
},
":hover": {

":hover::before": {
backgroundColor: "rgba(255, 255, 255, 0.4)",
},
},
Expand Down Expand Up @@ -180,6 +186,7 @@ export const artistImage = style({
export const artistName = style({
fontSize: "1.7rem",
fontFamily: vars.font.pyeongChangLight,
wordBreak: "keep-all",
"@media": {
"screen and (max-width: 768px)": {
fontSize: "1.7rem",
Expand Down
157 changes: 43 additions & 114 deletions src/pages/Timetable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,118 +1,20 @@
import React, { useEffect, useState, useMemo, useCallback, useRef } from "react";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { timeTableInfo } from "./timeTableInfo.ts";
import { timeTableFilterProps, timeTableInfoProps } from "../../shared/types/timeTable.ts";
import * as styles from "./.css.ts";
import { Link } from "react-router-dom";
import { artistInfoListProps } from "../../shared/types/mainPage.ts";
import { FilterButton, TimeTableItem } from "./subComponents.tsx";
import { clearTime, END_DATE, START_DATE, TIME_TABLE_FILTER } from "./utils.tsx";

// Constants
const TIME_TABLE_FILTER: timeTableFilterProps[] = [
{ name: "1일차", date: new Date("2024-09-23T00:00:00+09:00") },
{ name: "2일차", date: new Date("2024-09-24T00:00:00+09:00") },
{ name: "3일차", date: new Date("2024-09-25T00:00:00+09:00") },
{ name: "4일차", date: new Date("2024-09-26T00:00:00+09:00") },
];

const START_DATE = new Date("2024-09-23T00:00:00+09:00");
const END_DATE = new Date("2024-09-26T00:00:00+09:00");

const clearTime = (date: Date): Date => {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
};

const formatTime = (date: Date): string => {
return date.toLocaleTimeString("ko-KR", { hour: "2-digit", minute: "2-digit", hour12: false });
};

const getEventStatus = (
event: timeTableInfoProps,
currentTime: Date,
): "past" | "current" | "future" => {
let adjustedStartTime = new Date(event.startTime);

// KNU-ARTIST 항목에 대한 시작 시간 조정
if (event.description.includes("KNU-ARTIST")) {
if (event.description.includes("댄스")) {
adjustedStartTime = new Date(adjustedStartTime.getTime() - 5 * 60000);
} else if (event.description.includes("밴드")) {
adjustedStartTime = new Date(adjustedStartTime.getTime() - 4 * 60000);
}
}

if (currentTime < adjustedStartTime) return "future";
if (currentTime > event.endTime) return "past";
return "current";
};

// Sub-components
const FilterButton: React.FC<{
timeTable: timeTableFilterProps;
onClick: (date: Date) => void;
isActive: boolean;
}> = React.memo(({ timeTable, onClick, isActive }) => (
<button
className={isActive ? styles.filterButton.active : styles.filterButton.default}
onClick={() => onClick(clearTime(timeTable.date))}
>
{timeTable.name}
</button>
));

const ArtistInfo: React.FC<{
artist: artistInfoListProps[];
}> = React.memo(({ artist }) => (
<div className={styles.artistInfoContainer}>
<h3 className={styles.artistInfoTitle}>상세정보</h3>
{artist.map((a: artistInfoListProps, index) => (
<Link to={a.url} key={index} className={styles.artistItem}>
<img src={a.image} alt={a.name} className={styles.artistImage} />
<p className={styles.artistName}>{a.name}</p>
</Link>
))}
</div>
));

const TimeTableItem: React.FC<{
timeTable: timeTableInfoProps;
currentTime: Date;
refCallback: (node: HTMLDivElement | null) => void;
}> = React.memo(({ timeTable, currentTime, refCallback }) => {
const status = getEventStatus(timeTable, currentTime);

return (
<div className={`${styles.timeTableItem}`} ref={status === "current" ? refCallback : null}>
<h2 className={styles.timeTableTitle}>{timeTable.title}</h2>
{timeTable.descriptionShow && (
<p className={styles.timeTableDescription}>
{timeTable.description}
{timeTable.link && (
<a
className={styles.timeTableLink}
href={timeTable.link.url}
target="_blank"
rel="noreferrer"
>
{timeTable.link.text}
</a>
)}
</p>
)}
<p className={styles.timeTableTime}>
{timeTable.startTime.getMonth() + 1}/{timeTable.startTime.getDate()} |{" "}
{formatTime(timeTable.startTime)} ~ {formatTime(timeTable.endTime)}
</p>
{timeTable.artist && <ArtistInfo artist={timeTable.artist} />}
{status === "current" && <div className={styles.currentIndicator}>진행 중</div>}
</div>
);
});

// Main component
/**
* 타임테이블 페이지
*/
export default function Timetable() {
const [viewTime, setViewTime] = useState<Date>(START_DATE);
const [currentTime, setCurrentTime] = useState<Date>(new Date());
const lastCurrentEventRef = useRef<HTMLDivElement | null>(null);

/**
* 현재 날짜에 맞게 기준시간 설정
*/
useEffect(() => {
const today = clearTime(new Date());
const start = clearTime(START_DATE);
Expand All @@ -132,6 +34,11 @@
return () => clearInterval(timer);
}, []);

/**
* 필터링된 타임테이블 정보
* @see timeTableInfo
* @see useMemo
*/
const filteredTimeTableInfo = useMemo(
() =>
timeTableInfo
Expand All @@ -140,23 +47,43 @@
[viewTime],
);

/**
* 현재 진행중인 이벤트로 스크롤
*/
useEffect(() => {
// 진행 중인 마지막 요소로 스크롤
if (lastCurrentEventRef.current) {
if ("scrollIntoView" in lastCurrentEventRef.current) {
lastCurrentEventRef.current.scrollIntoView({ behavior: "smooth" });
return;
}
if (
lastCurrentEventRef.current &&
clearTime(currentTime).getTime() === clearTime(viewTime).getTime()
) {
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
const offset = rootFontSize * 3.75;

const elementPosition =
lastCurrentEventRef.current!.getBoundingClientRect().top + window.scrollY;
const offsetPosition = elementPosition - offset;

window.scrollTo({
top: offsetPosition,
behavior: "smooth",
});
} else {
window.scrollTo(0, 0);
}
window.scrollTo(0, 0);
}, [filteredTimeTableInfo]);

Check warning on line 72 in src/pages/Timetable/index.tsx

View workflow job for this annotation

GitHub Actions / CI

React Hook useEffect has missing dependencies: 'currentTime' and 'viewTime'. Either include them or remove the dependency array

/**
* 필터 클릭 이벤트
*/
const handleFilterClick = useCallback((date: Date) => {
setViewTime(clearTime(date));
}, []);

/**
* 렌더링
*/
return (
<section className={styles.section}>
{/*상단 필터링 버튼*/}
<div>
{TIME_TABLE_FILTER.map((timeTable, index) => (
<FilterButton
Expand All @@ -167,6 +94,7 @@
/>
))}
</div>
{/*필터링된 타임테이블 정보*/}
<div>
{filteredTimeTableInfo.map((timeTable, index) => (
<React.Fragment key={index}>
Expand All @@ -181,6 +109,7 @@
</React.Fragment>
))}
</div>
{/*하단 필터링 버튼*/}
<div>
{TIME_TABLE_FILTER.map((timeTable, index) => (
<FilterButton
Expand Down
82 changes: 82 additions & 0 deletions src/pages/Timetable/subComponents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from "react";
import { artistInfoListProps } from "../../shared/types/mainPage.ts";
import * as styles from "./.css.ts";
import { Link } from "react-router-dom";
import { timeTableFilterProps, timeTableInfoProps } from "../../shared/types/timeTable.ts";
import { clearTime, formatTime, getEventStatus } from "./utils.tsx";

/**
* 일별 필터 버튼
*/
export const FilterButton: React.FC<{
timeTable: timeTableFilterProps;
onClick: (date: Date) => void;
isActive: boolean;
}> = React.memo(({ timeTable, onClick, isActive }) => (
<button
className={isActive ? styles.filterButton.active : styles.filterButton.default}
onClick={() => onClick(clearTime(timeTable.date))}
>
{timeTable.name}
</button>
));

/**
* 아티스트 정보
* @param artist 아티스트 정보
* @see artistInfoListProps
*/
const ArtistInfo: React.FC<{
artist: artistInfoListProps[];
}> = React.memo(({ artist }) => (
<div className={styles.artistInfoContainer}>
<h3 className={styles.artistInfoTitle}>상세정보</h3>
{artist.map((a: artistInfoListProps, index) => (
<Link to={a.url} key={index} className={styles.artistItem}>
<img src={a.image} alt={a.name} className={styles.artistImage} />
<p className={styles.artistName}>{a.name}</p>
</Link>
))}
</div>
));

/**
* 타임테이블 아이템
* @param timeTable 타임테이블 정보
* @param currentTime 현재 시간
* @param refCallback 현재 진행중인 이벤트를 가리키기 위한 콜백
*/
export const TimeTableItem: React.FC<{
timeTable: timeTableInfoProps;
currentTime: Date;
refCallback: (node: HTMLDivElement | null) => void;
}> = React.memo(({ timeTable, currentTime, refCallback }) => {
const status = getEventStatus(timeTable, currentTime);

return (
<div className={`${styles.timeTableItem}`} ref={status === "current" ? refCallback : null}>
<h2 className={styles.timeTableTitle}>{timeTable.title}</h2>
{timeTable.descriptionShow && (
<p className={styles.timeTableDescription}>
{timeTable.description}
{timeTable.link && (
<a
className={styles.timeTableLink}
href={timeTable.link.url}
target="_blank"
rel="noreferrer"
>
{timeTable.link.text}
</a>
)}
</p>
)}
<p className={styles.timeTableTime}>
{timeTable.startTime.getMonth() + 1}/{timeTable.startTime.getDate()} |{" "}
{formatTime(timeTable.startTime)} ~ {formatTime(timeTable.endTime)}
</p>
{timeTable.artist && <ArtistInfo artist={timeTable.artist} />}
{status === "current" && <div className={styles.currentIndicator}>진행 중</div>}
</div>
);
});
Loading