Skip to content

Commit 3092f4b

Browse files
authored
Merge pull request #306 from KW-ClassLog/Feat/#302/presentation-page-markup
✨ Feat/#302 강의중 화면 마크업
2 parents cf42ac7 + 4c61eeb commit 3092f4b

File tree

49 files changed

+3244
-11
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+3244
-11
lines changed

frontend/app/teacher/lecture-detail/[lectureId]/_components/LectureHeader/LectureHeader.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import MakeQuizModal from "@/components/Modal/MakeQuizModal/MakeQuizModal";
99
import { FetchLectureDetailResult } from "@/types/lectures/fetchLectureDetailTypes";
1010
import { fetchLectureDetail } from "@/api/lectures/fetchLectureDetail";
1111
import { useLectureDetail } from "../LectureDetailContext";
12+
import { useRouter } from "next/navigation";
13+
import { ROUTES } from "@/constants/routes";
1214

1315
export default function LectureHeader() {
1416
const { lectureId, setClassId, refreshKey } = useLectureDetail();
@@ -17,6 +19,8 @@ export default function LectureHeader() {
1719
const [showQuizModal, setShowQuizModal] = useState(false);
1820
const [loading, setLoading] = useState(true);
1921

22+
const router = useRouter();
23+
2024
const fetchData = useCallback(async () => {
2125
try {
2226
setLoading(true);
@@ -43,8 +47,7 @@ export default function LectureHeader() {
4347
}, [lectureId, setClassId, fetchData, refreshKey]);
4448

4549
const handleStartLecture = () => {
46-
// TODO: 강의 시작 로직 구현
47-
console.log("강의 시작");
50+
router.push(ROUTES.teacherLectureLive(lectureId));
4851
};
4952

5053
const handleQuizModalClose = () => {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@import "@/styles/variables";
2+
3+
.wrap {
4+
position: relative;
5+
display: inline-flex;
6+
}
7+
8+
.badge {
9+
position: absolute;
10+
top: 6px;
11+
right: 6px;
12+
width: 6px;
13+
height: 6px;
14+
border-radius: 50%;
15+
background: #ef4444;
16+
box-shadow: 0 0 0 2px #fff;
17+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import IconButton from "@/components/Button/IconButton/IconButton";
5+
import { MessageCircleMore } from "lucide-react";
6+
import { useLive } from "../../LectureLiveProvider";
7+
import styles from "./ChatingButton.module.scss";
8+
9+
export default function ChatingButton({
10+
className,
11+
onPress,
12+
}: {
13+
className?: string;
14+
onPress?: () => void;
15+
}) {
16+
const { panels, togglePanel } = useLive();
17+
const isOpen = panels.chat;
18+
const [unread, setUnread] = useState(false);
19+
20+
useEffect(() => {
21+
if (isOpen) setUnread(false);
22+
}, [isOpen]);
23+
24+
useEffect(() => {
25+
const onNew = () => {
26+
if (!isOpen) setUnread(true);
27+
};
28+
window.addEventListener("live:chat:new", onNew as EventListener);
29+
return () => window.removeEventListener("live:chat:new", onNew as EventListener);
30+
}, [isOpen]);
31+
32+
const onClick = () => {
33+
onPress?.();
34+
togglePanel("chat");
35+
};
36+
37+
return (
38+
<span className={`${styles.wrap} ${className ?? ""}`}>
39+
<IconButton
40+
ariaLabel="채팅"
41+
onClick={onClick}
42+
icon={<MessageCircleMore data-active={isOpen} />}
43+
/>
44+
{unread && !isOpen && <i className={styles.badge} aria-hidden />}
45+
</span>
46+
);
47+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
@import "@/styles/variables";
2+
3+
.wrap {
4+
height: 100%;
5+
display: grid;
6+
grid-template-rows: auto 1fr auto;
7+
}
8+
9+
.header {
10+
padding: $spacing-sm $spacing-sm 0 $spacing-sm;
11+
display: flex;
12+
align-items: center;
13+
justify-content: space-between;
14+
}
15+
16+
.headerTitle {
17+
font-size: $font-size-lg;
18+
font-weight: $font-weight-medium;
19+
color: $color-neutral-1;
20+
}
21+
22+
.closeBtn :global(button) {
23+
width: 28px;
24+
height: 28px;
25+
padding: 0;
26+
background: transparent;
27+
border: none;
28+
border-radius: 8px;
29+
30+
&:hover { background: $color-neutral-7; }
31+
}
32+
33+
.body {
34+
display: flex;
35+
flex-direction: column;
36+
overflow: auto;
37+
padding: $spacing-sm;
38+
gap: $spacing-xs;
39+
}
40+
41+
.row {
42+
display: flex;
43+
}
44+
45+
.teacher { justify-content: flex-end; }
46+
.student { justify-content: flex-start; }
47+
48+
.inputRow {
49+
padding: $spacing-sm;
50+
border-top: 1px solid $color-neutral-7;
51+
52+
:global(.inputBox) {
53+
padding-right: 6px;
54+
}
55+
}
56+
57+
.inputRow :global(.iconRight) {
58+
display: inline-flex;
59+
align-items: center;
60+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"use client";
2+
3+
import { useEffect, useRef, useState } from "react";
4+
import styles from "./ChatingPanel.module.scss";
5+
import IconButton from "@/components/Button/IconButton/IconButton";
6+
import { X, SendHorizontal } from "lucide-react";
7+
import { useLive } from "../../LectureLiveProvider";
8+
import ChatBox from "@/components/ChatBox/ChatBox";
9+
import BasicInput from "@/components/Input/BasicInput/BasicInput";
10+
11+
type Msg = {
12+
id: string;
13+
text: string;
14+
role: "teacher" | "student";
15+
ts?: number;
16+
};
17+
18+
export default function ChatPanel() {
19+
const { togglePanel } = useLive();
20+
21+
const [msgs, setMsgs] = useState<Msg[]>([
22+
{ id: "seed1", text: "질문이요~", role: "student", ts: Date.now()},
23+
]);
24+
const [text, setText] = useState("");
25+
26+
const bodyRef = useRef<HTMLDivElement>(null);
27+
28+
const closeChat = () => togglePanel("chat");
29+
30+
const send = () => {
31+
const t = text.trim();
32+
if (!t) return;
33+
setMsgs((m) => [
34+
...m,
35+
{ id: String(Date.now()), text: t, role: "teacher", ts: Date.now() },
36+
]);
37+
setText("");
38+
};
39+
40+
const onSubmit: React.FormEventHandler = (e) => {
41+
e.preventDefault();
42+
send();
43+
};
44+
45+
useEffect(() => {
46+
const el = bodyRef.current;
47+
if (!el) return;
48+
el.scrollTop = el.scrollHeight;
49+
}, [msgs]);
50+
51+
const pad = (n: number) => n.toString().padStart(2, "0");
52+
const fmt = (ts: number) => {
53+
const d = new Date(ts);
54+
const yy = pad(d.getFullYear() % 100);
55+
const MM = pad(d.getMonth() + 1);
56+
const dd = pad(d.getDate());
57+
const hh = pad(d.getHours());
58+
const mm = pad(d.getMinutes());
59+
const ss = pad(d.getSeconds());
60+
return `${yy}.${MM}.${dd} ${hh}:${mm}:${ss}`;
61+
};
62+
63+
return (
64+
<div className={styles.wrap}>
65+
<div className={styles.header}>
66+
<span className={styles.headerTitle}>Question</span>
67+
<div className={styles.closeBtn}>
68+
<IconButton ariaLabel="채팅 닫기" onClick={closeChat} icon={<X />} />
69+
</div>
70+
</div>
71+
72+
<div ref={bodyRef} className={styles.body}>
73+
{msgs.map((m) => {
74+
const tsText = m.ts ? fmt(m.ts) : "";
75+
return (
76+
<div
77+
key={m.id}
78+
className={`${styles.row} ${
79+
m.role === "teacher" ? styles.teacher : styles.student
80+
}`}
81+
>
82+
<ChatBox
83+
isAnonymous={true}
84+
nickname=""
85+
profilePicture=""
86+
message={m.text}
87+
timestamp={tsText}
88+
variant={m.role === "teacher" ? "teacher" : "student"}
89+
/>
90+
</div>
91+
);
92+
})}
93+
</div>
94+
95+
<form className={styles.inputRow} onSubmit={onSubmit}>
96+
<BasicInput
97+
value={text}
98+
onChange={(e) => setText(e.target.value)}
99+
placeholder="답변 입력하기"
100+
iconRight={
101+
<IconButton
102+
ariaLabel="전송"
103+
onClick={send}
104+
icon={<SendHorizontal size={18} color="#9AA4B2" />}
105+
/>
106+
}
107+
/>
108+
</form>
109+
</div>
110+
);
111+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"use client";
2+
3+
import IconButton from "@/components/Button/IconButton/IconButton";
4+
import { PanelLeft, PanelLeftDashed } from "lucide-react";
5+
import { useLive } from "../LectureLiveProvider";
6+
7+
export type DocumentSideButtonProps = {
8+
open: boolean;
9+
onToggle: () => void;
10+
ariaLabelOpen?: string;
11+
ariaLabelClose?: string;
12+
disabled?: boolean;
13+
};
14+
15+
export default function DocumentSideButton({
16+
open,
17+
onToggle,
18+
ariaLabelOpen = "슬라이드 패널 열기",
19+
ariaLabelClose = "슬라이드 패널 닫기",
20+
disabled = false,
21+
}: DocumentSideButtonProps) {
22+
const label = open ? ariaLabelClose : ariaLabelOpen;
23+
24+
return (
25+
<IconButton
26+
ariaLabel={label}
27+
onClick={onToggle}
28+
disabled={disabled}
29+
icon={open ? <PanelLeft /> : <PanelLeftDashed />}
30+
/>
31+
);
32+
}
33+
34+
export function DocumentSideButtonConnected() {
35+
const { panels, togglePanel } = useLive();
36+
return (
37+
<DocumentSideButton
38+
open={panels.files}
39+
onToggle={() => togglePanel("files")}
40+
/>
41+
);
42+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
@import "@/styles/variables";
2+
3+
.container {
4+
width: 100%;
5+
background: $color-white;
6+
border-bottom: 1px solid $color-neutral-7;
7+
}
8+
9+
.inner {
10+
height: auto;
11+
display: flex;
12+
align-items: center;
13+
justify-content: space-between;
14+
padding: 3px $spacing-md;
15+
}
16+
17+
.left {
18+
display: flex;
19+
align-items: center;
20+
gap: $spacing-xs;
21+
}
22+
23+
.divider {
24+
width: 1px;
25+
height: 20px;
26+
background: $color-neutral-7;
27+
margin: 0 $spacing-xs;
28+
}
29+
30+
31+
.left svg[data-active="true"] {
32+
color: $color-blue;
33+
stroke: $color-blue;
34+
}
35+
36+
37+
.endBtn {
38+
align-self: center;
39+
height: auto !important;
40+
min-height: 0 !important;
41+
line-height: 2;
42+
padding-block: 3px !important;
43+
}
44+
45+
.docBtnZ {
46+
position: relative;
47+
z-index: 2000;
48+
}

0 commit comments

Comments
 (0)