Skip to content

Commit ea6c09e

Browse files
authored
Merge pull request #35 from pythonkr/feature/session-component
feat: 세션 목록 컴포넌트
2 parents 2585a90 + 9c73d58 commit ea6c09e

File tree

3 files changed

+403
-0
lines changed

3 files changed

+403
-0
lines changed
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
import styled from "@emotion/styled";
2+
import { Button, CircularProgress, Divider } from "@mui/material";
3+
import { styled as muiStyled } from "@mui/material/styles";
4+
import { Suspense } from "@suspensive/react";
5+
import React, { useEffect, useState } from "react";
6+
import { useNavigate } from "react-router-dom";
7+
import * as R from "remeda";
8+
9+
import BackendSessionAPISchemas from "../../../../../packages/common/src/schemas/backendSessionAPI";
10+
11+
const SessionItem: React.FC<{ session: BackendSessionAPISchemas.SessionSchema }> = Suspense.with(
12+
{ fallback: <CircularProgress /> },
13+
({ session }) => {
14+
const navigate = useNavigate();
15+
16+
const speakerImageSrc =
17+
session.presentationSpeaker[0].image ||
18+
(R.isArray(session.presentationSpeaker[0].image) && !R.isEmpty(session.presentationSpeaker[0].image)) ||
19+
"";
20+
const urlSafeTitle = session.name
21+
.replace(/ /g, "-")
22+
.replace(/([.])/g, "_")
23+
.replace(/(?![0-9A-Za-z---_])./g, "");
24+
25+
return (
26+
<>
27+
<SessionItemEl>
28+
<SessionItemImgContainer>
29+
{speakerImageSrc && (
30+
<img src={session.presentationSpeaker[0].image} alt={session.presentationSpeaker[0].name} />
31+
)}
32+
</SessionItemImgContainer>
33+
<SessionItemInfoContainer>
34+
<TagContainer>
35+
{session.presentationCategories.map((tag) => (
36+
<Tag key={tag.id}>{tag.name}</Tag>
37+
))}
38+
{session.doNotRecord && <Tag>{"녹화 불가"}</Tag>}
39+
</TagContainer>
40+
<SessionTitleContainer>
41+
<kbd onClick={() => navigate(`/session/${session.id}#${urlSafeTitle}`)}>{session.name}</kbd>
42+
</SessionTitleContainer>
43+
<SessionSpeakerContainer>
44+
{session.presentationSpeaker.map((speaker) => (
45+
<kbd key={speaker.id}>{speaker.name}</kbd>
46+
))}
47+
</SessionSpeakerContainer>
48+
</SessionItemInfoContainer>
49+
</SessionItemEl>
50+
<CategoryButtonDivider />
51+
</>
52+
);
53+
}
54+
);
55+
56+
export const SessionListPage: React.FC = () => {
57+
const [selectedCategory, setSelectedCategory] = useState<string>("전체");
58+
59+
const [sessions, setSessions] = useState<BackendSessionAPISchemas.SessionSchema[]>(sessionDummyData);
60+
const [filteredSessions, setFilteredSessions] = useState<BackendSessionAPISchemas.SessionSchema[]>([]);
61+
62+
useEffect(() => {
63+
setSessions(sessionDummyData);
64+
});
65+
66+
useEffect(() => {
67+
const newFilteredSessions = sessions.filter((session) => {
68+
const sessionCategoryNames: string[] = session.presentationCategories.map((category) => category.name);
69+
return sessionCategoryNames.includes(selectedCategory);
70+
});
71+
setFilteredSessions(newFilteredSessions);
72+
}, [selectedCategory]);
73+
74+
const CategoryButton: React.FC<{ category: string; isSelected: boolean }> = ({ category, isSelected }) => {
75+
return isSelected ? (
76+
<CategoryButtonSelectedStyle>{category}</CategoryButtonSelectedStyle>
77+
) : (
78+
<CategoryButtonStyle
79+
onClick={() => {
80+
setSelectedCategory(category);
81+
}}
82+
>
83+
{category}
84+
</CategoryButtonStyle>
85+
);
86+
};
87+
88+
const CategoryButtons: React.FC = () => {
89+
return (
90+
<>
91+
{categoriesDummyData.map((category) => {
92+
return <CategoryButton category={category} isSelected={category === selectedCategory} />;
93+
})}
94+
</>
95+
);
96+
};
97+
98+
return (
99+
<SessionContainer>
100+
<SessionCategoryButtonContainer>
101+
<InfoTextContainer>{"* 발표 목록은 발표자 사정에 따라 변동될 수 있습니다."}</InfoTextContainer>
102+
<CategoryButtonDivider />
103+
<ButtonGroupContainer>
104+
<CategoryButtons />
105+
</ButtonGroupContainer>
106+
<CategoryButtonDivider />
107+
</SessionCategoryButtonContainer>
108+
{(selectedCategory === "전체" ? sessions : filteredSessions).map((session) => (
109+
<SessionItem key={session.presentationType.id} session={session} />
110+
))}
111+
</SessionContainer>
112+
);
113+
};
114+
115+
const sessionDummyData: BackendSessionAPISchemas.SessionSchema[] = [
116+
{
117+
id: "presentationId",
118+
name: "django ORM 관리하기",
119+
doNotRecord: false,
120+
presentationType: {
121+
id: "presentationTypeId",
122+
event: "eventId",
123+
name: "poetry 의존성 관리",
124+
},
125+
presentationCategories: [
126+
{
127+
id: "presentationCategories1",
128+
presentationType: "presentationTypeId",
129+
name: "블록체인",
130+
},
131+
{
132+
id: "presentationCategories2",
133+
presentationType: "presentationTypeId",
134+
name: "라이브러리 / 코어",
135+
},
136+
],
137+
presentationSpeaker: [
138+
{
139+
id: "presentationSpeakerId1",
140+
presentation: "presentationId",
141+
user: "ksy0526",
142+
name: "강소영",
143+
biography: "다양한 언어를 공부합니다.",
144+
image: "https://fastly.picsum.photos/id/866/200/300.jpg?hmac=rcadCENKh4rD6MAp6V_ma-AyWv641M4iiOpe1RyFHeI",
145+
},
146+
],
147+
},
148+
{
149+
id: "presentationId",
150+
name: "uv로 python 프로젝트 관리하기",
151+
doNotRecord: true,
152+
presentationType: {
153+
id: "presentationTypeId",
154+
event: "eventId",
155+
name: "uv 의존성 관리",
156+
},
157+
presentationCategories: [
158+
{
159+
id: "presentationCategories1",
160+
presentationType: "presentationTypeId",
161+
name: "보안",
162+
},
163+
{
164+
id: "presentationCategories2",
165+
presentationType: "presentationTypeId",
166+
name: "실무",
167+
},
168+
],
169+
presentationSpeaker: [
170+
{
171+
id: "presentationSpeakerId1",
172+
presentation: "presentationId",
173+
user: "ksy0526",
174+
name: "강소영",
175+
biography: "다양한 언어를 공부합니다.",
176+
image: "https://fastly.picsum.photos/id/737/200/200.jpg?hmac=YPktyFzukhcmeW3VgULbam5iZTWOMXfwf6WIBPpJD50",
177+
},
178+
],
179+
},
180+
];
181+
182+
const categoriesDummyData: string[] = [
183+
"전체",
184+
"교육",
185+
"데이터 과학",
186+
"라이브러리 / 코어",
187+
"보안",
188+
"블록체인",
189+
"실무",
190+
"오픈소스 / 커뮤니티",
191+
"웹 서비스",
192+
"인공지능",
193+
"일상 / 사회",
194+
"자동화",
195+
"컴퓨터 비전",
196+
];
197+
198+
const CategoryButtonStyle = muiStyled(Button)(({ theme }) => ({
199+
color: theme.palette.primary.light,
200+
"&:hover": {
201+
color: theme.palette.primary.dark,
202+
},
203+
}));
204+
205+
const CategoryButtonSelectedStyle = muiStyled(Button)(({ theme }) => ({
206+
color: theme.palette.primary.dark,
207+
}));
208+
209+
const CategoryButtonDivider = muiStyled(Divider)(({ theme }) => ({
210+
bgcolor: theme.palette.primary.dark,
211+
borderColor: theme.palette.primary.dark,
212+
}));
213+
214+
const SessionContainer = styled.div`
215+
margin-top: 1rem;
216+
margin-bottom: 1rem;
217+
`;
218+
219+
const InfoTextContainer = styled.div`
220+
padding-bottom: 0.5rem;
221+
text-align: right;
222+
font-size: 0.75rem;
223+
p {
224+
text-align: right;
225+
font-size: 0.75rem;
226+
}
227+
`;
228+
229+
const SessionCategoryButtonContainer = styled.div``;
230+
231+
const ButtonGroupContainer = styled.div`
232+
display: flex;
233+
`;
234+
235+
const SessionItemEl = styled.div`
236+
display: flex;
237+
align-items: center;
238+
justify-content: flex-start;
239+
padding: 0.75rem;
240+
width: 100%;
241+
gap: 1rem;
242+
243+
color: var(--pico-h6-color);
244+
245+
border-top: 1px solid var(--pico-muted-border-color);
246+
border-bottom: 1px solid var(--pico-muted-border-color);
247+
248+
@media only screen and (max-width: 809px) {
249+
padding: 0rem;
250+
gap: 0.5rem;
251+
}
252+
`;
253+
254+
const SessionItemImgContainer = styled.div`
255+
width: 6rem;
256+
height: 6rem;
257+
margin: 0.6rem 0.6rem 0.6rem 1.5rem;
258+
flex-shrink: 0;
259+
flex-grow: 0;
260+
261+
border-radius: 50%;
262+
border: 1px solid var(--pico-muted-border-color);
263+
264+
* {
265+
width: 100%;
266+
height: 100%;
267+
min-width: 100%;
268+
min-height: 100%;
269+
max-width: 100%;
270+
max-height: 100%;
271+
border-radius: 50%;
272+
}
273+
274+
@media only screen and (max-width: 809px) {
275+
width: 5rem;
276+
height: 5rem;
277+
margin: 0.25rem;
278+
}
279+
`;
280+
281+
const SessionItemInfoContainer = styled.div`
282+
padding-top: 0.5rem;
283+
padding-bottom: 0.5rem;
284+
margin-left: 1rem;
285+
286+
flex-grow: 1;
287+
288+
h4 {
289+
color: #febd99;
290+
margin-bottom: 0.2rem;
291+
cursor: pointer;
292+
}
293+
294+
p {
295+
margin-bottom: 0.3rem;
296+
color: var(--pico-h3-color);
297+
font-size: 0.8rem;
298+
font-weight: bold;
299+
}
300+
301+
@media only screen and (max-width: 809px) {
302+
h3 {
303+
font-size: 1.5rem;
304+
}
305+
306+
p {
307+
font-size: 0.8rem;
308+
font-weight: bold;
309+
}
310+
}
311+
`;
312+
313+
const SessionTitleContainer = styled.div`
314+
display: flex;
315+
align-items: center;
316+
justify-content: flex-start;
317+
kbd {
318+
color: ${({ theme }) => theme.palette.text.primary};
319+
gap: 0.5rem;
320+
font-family: ${({ theme }) => theme.typography.fontFamily};
321+
font-weight: bold;
322+
font-size: 1.25rem;
323+
}
324+
`;
325+
326+
const SessionSpeakerContainer = styled.div`
327+
display: flex;
328+
align-items: center;
329+
justify-content: flex-start;
330+
kbd {
331+
padding-top: 0.25rem;
332+
font-size: 0.8rem;
333+
color: #4e869d;
334+
}
335+
`;
336+
337+
const TagContainer = styled.div`
338+
display: flex;
339+
align-items: center;
340+
justify-content: flex-start;
341+
padding: 0.25rem 0;
342+
gap: 0.35rem;
343+
`;
344+
345+
const Tag = styled.kbd`
346+
background-color: white;
347+
padding: 0.2rem 0.4rem;
348+
font-size: 0.75rem;
349+
font-family: ${({ theme }) => theme.typography.fontFamily};
350+
color: ${({ theme }) => theme.palette.primary.main};
351+
bordercolor: ${({ theme }) => theme.palette.primary.main};
352+
border: 1px solid;
353+
border-radius: 15px;
354+
`;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { BackendAPIClient } from "./client";
2+
import BackendSessionAPISchemas from "../schemas/backendSessionAPI";
3+
4+
namespace SessionAPIs {
5+
export const sessionList = (client: BackendAPIClient) => async () => {
6+
return await client.get<BackendSessionAPISchemas.SessionSchema[]>("v1/event/presentation/");
7+
};
8+
9+
export const sessionFilteredList = (client: BackendAPIClient, categoryName: string) => async () => {
10+
return await client.get<BackendSessionAPISchemas.SessionSchema[]>(
11+
`v1/event/presentation/?category=${categoryName}`
12+
);
13+
};
14+
}
15+
16+
export default SessionAPIs;

0 commit comments

Comments
 (0)