tt (#1)
All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m40s

Reviewed-on: #1
Co-authored-by: Pham Hung <22.phamviethung.12a12@gmail.com>
Co-committed-by: Pham Hung <22.phamviethung.12a12@gmail.com>
This commit is contained in:
2026-07-23 10:16:52 +07:00
committed by hungpham
parent 2bdec7d7e7
commit 3b80550c76
17 changed files with 7378 additions and 2839 deletions

View File

@@ -11,13 +11,13 @@ function fmtTime(iso: string) {
}
}
function statusLabel(st: string) {
if (st === 'draft') return { text: 'Tạm thời', cls: 'badge-muted' };
if (st === 'ready') return { text: 'Sẵn sàng', cls: 'badge-info' };
if (st === 'active') return { text: 'Đang thi', cls: 'badge-success' };
if (st === 'cancelled') return { text: 'Đã hủy', cls: 'badge-warning' };
if (st === 'ended') return { text: 'Đã kết thúc', cls: 'badge-muted' };
return { text: st, cls: 'badge-muted' };
function statusMeta(st: string) {
if (st === 'draft') return { text: 'Tạm thời', tone: 'muted' };
if (st === 'ready') return { text: 'Sẵn sàng', tone: 'info' };
if (st === 'active') return { text: 'Đang thi', tone: 'active' };
if (st === 'cancelled') return { text: 'Đã hủy', tone: 'warn' };
if (st === 'ended') return { text: 'Đã kết thúc', tone: 'muted' };
return { text: st, tone: 'muted' };
}
function toLocalInput(iso?: string) {
@@ -33,6 +33,64 @@ function localInputToISO(v: string) {
}
type RoomScope = 'mine' | 'all';
type IconProps = { size?: number; filled?: boolean };
const IconClipboard = ({ size = 22 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2" />
<rect x="9" y="3" width="6" height="4" rx="1" />
<path d="M9 12h6M9 16h4" />
</svg>
);
const IconPlus = ({ size = 16 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="2.25" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M12 5v14M5 12h14" />
</svg>
);
const IconStar = ({ size = 14, filled }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill={filled ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="m12 3.5 2.6 5.3 5.8.8-4.2 4.1 1 5.8L12 16.8l-5.2 2.7 1-5.8-4.2-4.1 5.8-.8L12 3.5z" />
</svg>
);
const IconSearch = ({ size = 16 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<circle cx="11" cy="11" r="7" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
);
const IconCalendar = ({ size = 13 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<rect x="3" y="5" width="18" height="16" rx="2" />
<path d="M16 3v4M8 3v4M3 11h18" />
</svg>
);
const IconUsers = ({ size = 12 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="3.5" />
<path d="M22 21v-2a3.5 3.5 0 0 0-2.5-3.35" />
<path d="M16 3.5a3.5 3.5 0 0 1 0 7" />
</svg>
);
const IconOpen = ({ size = 14 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="M5 12h14" />
<path d="m13 6 6 6-6 6" />
</svg>
);
const IconInbox = ({ size = 36 }: IconProps) => (
<svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
</svg>
);
export const ExamsTab: React.FC = () => {
const { staff } = useAuth();
@@ -52,7 +110,6 @@ export const ExamsTab: React.FC = () => {
const load = async () => {
setLoading(true);
try {
// Luôn lấy đủ danh sách để toggle mine/all mượt; lọc phía client theo createdByStaffId
const res = await apiExam.list();
setRooms(res.data);
} catch (e) {
@@ -112,7 +169,11 @@ export const ExamsTab: React.FC = () => {
};
const handleDeleteRoom = async (roomId: number, roomName: string) => {
if (window.confirm(`Bạn có chắc chắn muốn xóa hoàn toàn phòng thi "${roomName}" không? Hành động này sẽ xóa sạch dữ liệu phòng thi, các bài nộp, và không thể khôi phục.`)) {
if (
window.confirm(
`Bạn có chắc chắn muốn xóa hoàn toàn phòng thi "${roomName}" không? Hành động này sẽ xóa sạch dữ liệu phòng thi, các bài nộp, và không thể khôi phục.`
)
) {
try {
await apiExam.remove(roomId);
alert('Đã xóa phòng thi thành công!');
@@ -125,97 +186,216 @@ export const ExamsTab: React.FC = () => {
return (
<div className="tab-page exams-page">
<header className="page-header page-header--row">
<div>
<h1 className="page-title">Danh sách phòng thi</h1>
<p className="page-desc">
{roomScope === 'mine'
? 'Mặc định chỉ hiện phòng thi do bạn tạo — chuyển sang xem tất cả khi cần'
: 'Đang xem toàn bộ phòng thi trong hệ thống'}
</p>
</div>
<button type="button" className="btn btn-primary" onClick={() => setShowCreate(true)}>
+ Tạo phòng thi
</button>
</header>
<div className="exams-stats-row">
<div className="learning-stat">
<span className="learning-stat-value">{stats.total}</span>
<span className="learning-stat-label">{roomScope === 'mine' ? 'Phòng của tôi' : 'Tổng phòng'}</span>
</div>
<div className="learning-stat learning-stat--accent">
<span className="learning-stat-value">{stats.active}</span>
<span className="learning-stat-label">Đang thi</span>
</div>
<div className="learning-stat">
<span className="learning-stat-value">{stats.upcoming}</span>
<span className="learning-stat-label">Sắp / tạm</span>
<div className="tab-page-toolbar">
<div className="page-header">
<div className="page-title">
<h1 className="page-title-heading">
<span className="page-title-icon" aria-hidden>
<IconClipboard />
</span>
Danh sách phòng thi
</h1>
<p>
{roomScope === 'mine'
? 'Mặc định chỉ hiện phòng thi do bạn tạo — chuyển sang xem tất cả khi cần'
: 'Đang xem toàn bộ phòng thi trong hệ thống'}
</p>
</div>
<div className="exams-header-actions">
<button type="button" className="btn btn-primary" onClick={() => setShowCreate(true)}>
<IconPlus />
Tạo phòng thi
</button>
</div>
</div>
</div>
<div className="exams-toolbar" style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', alignItems: 'center' }}>
<div className="learning-view-toggle">
<button
type="button"
className={`learning-view-btn${roomScope === 'mine' ? ' learning-view-btn--active' : ''}`}
onClick={() => setRoomScope('mine')}
>
Phòng của tôi
</button>
<button
type="button"
className={`learning-view-btn${roomScope === 'all' ? ' learning-view-btn--active' : ''}`}
onClick={() => setRoomScope('all')}
>
Xem tất cả
</button>
<div className="tab-page-body" style={{ gap: '0.85rem' }}>
<div className="exams-stats-row">
<div className="learning-stat">
<span className="learning-stat-value">{stats.total}</span>
<span className="learning-stat-label">
{roomScope === 'mine' ? 'Phòng của tôi' : 'Tổng phòng'}
</span>
</div>
<div className="learning-stat learning-stat--accent">
<span className="learning-stat-value">{stats.active}</span>
<span className="learning-stat-label">Đang thi</span>
</div>
<div className="learning-stat">
<span className="learning-stat-value">{stats.upcoming}</span>
<span className="learning-stat-label">Sắp / tạm</span>
</div>
</div>
<div className="content-card learning-toolbar-card exams-toolbar-card">
<div className="learning-toolbar exams-toolbar-inner">
<div className="learning-seg" role="group" aria-label="Phạm vi phòng thi">
<button
type="button"
className={`learning-seg-btn${roomScope === 'mine' ? ' learning-seg-btn--active' : ''}`}
onClick={() => setRoomScope('mine')}
>
<IconStar size={13} filled={roomScope === 'mine'} />
<span>Phòng của tôi</span>
</button>
<button
type="button"
className={`learning-seg-btn${roomScope === 'all' ? ' learning-seg-btn--active' : ''}`}
onClick={() => setRoomScope('all')}
>
<span>Xem tất cả</span>
</button>
</div>
<input
type="search"
className="app-pool-search learning-search exams-search"
placeholder="Tìm theo tên phòng thi..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
<div className="tab-page-scroll">
<div className="content-card" style={{ padding: 0 }}>
{loading ? (
<div className="exams-empty-panel">
<div className="sync-spinner" style={{ width: 36, height: 36, borderWidth: 3 }} />
<p>Đang tải danh sách phòng thi...</p>
</div>
) : filtered.length === 0 ? (
<div className="exams-empty-panel">
<div className="exams-empty-icon">
{search.trim() ? <IconSearch size={32} /> : <IconInbox />}
</div>
<p>
{search.trim()
? 'Không tìm thấy phòng thi phù hợp.'
: roomScope === 'mine'
? 'Bạn chưa tạo phòng thi nào.'
: 'Chưa có phòng thi — bấm Tạo phòng thi để bắt đầu.'}
</p>
{!search.trim() && (
<div className="exams-empty-actions">
<button type="button" className="btn btn-primary" onClick={() => setShowCreate(true)}>
<IconPlus />
Tạo phòng thi
</button>
{roomScope === 'mine' && (
<button type="button" className="btn btn-secondary" onClick={() => setRoomScope('all')}>
Xem tất cả
</button>
)}
</div>
)}
</div>
) : (
<div className="exams-card-grid">
{filtered.map((r) => {
const st = statusMeta(r.displayStatus || r.status);
const mine = myStaffId > 0 && Number(r.createdByStaffId) === myStaffId;
const isActive = (r.displayStatus || r.status) === 'active';
return (
<article
key={r.id}
className={`exam-list-card${isActive ? ' exam-list-card--active' : ''}`}
>
<header className="exam-list-card-head">
<div className="exam-list-card-info">
<h3 className="exam-list-card-title" title={r.name}>{r.name}</h3>
<div className="exam-list-card-meta-block">
<div className="exam-meta-row">
<IconCalendar />
<span>{fmtTime(r.startTime)}</span>
<span className="exam-list-card-sep"></span>
<span>{fmtTime(r.endTime)}</span>
</div>
<div className="exam-meta-row exam-meta-row--secondary">
<IconUsers />
<span>
{r.studentCount} SV · {r.paperCount} đ
</span>
</div>
</div>
</div>
<div className="exam-list-card-badges">
{mine && <span className="exam-tag exam-tag--mine">Của tôi</span>}
<span className={`exam-tag exam-tag--${st.tone}`}>{st.text}</span>
</div>
</header>
<footer className="exam-list-card-actions">
<button
type="button"
className="btn btn-primary btn-sm"
onClick={() => openExam(r.id, r.name)}
>
Mở phòng thi
<IconOpen />
</button>
{canDeleteRoom(r) && (
<button
type="button"
className="btn btn-secondary btn-sm exam-btn-danger"
onClick={(e) => {
e.stopPropagation();
handleDeleteRoom(r.id, r.name);
}}
>
Xóa
</button>
)}
</footer>
</article>
);
})}
</div>
)}
</div>
</div>
<input
type="search"
className="app-pool-search exams-search"
placeholder="Tìm theo tên phòng thi..."
value={search}
onChange={(e) => setSearch(e.target.value)}
style={{ flex: 1, minWidth: 200 }}
/>
</div>
{showCreate && (
<div className="modal-overlay" onClick={() => setShowCreate(false)}>
<div className="modal-container" style={{ maxWidth: '500px' }} onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h2 className="modal-title">Tạo phòng thi mới</h2>
<button className="modal-close-btn" onClick={() => setShowCreate(false)} aria-label="Đóng">&times;</button>
<div className="modal-container class-picker-modal exam-create-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header class-picker-header">
<div>
<h2 className="modal-title">Tạo phòng thi mới</h2>
<p className="class-picker-subtitle">
Đt tên khung giờ thi. Sau khi tạo sẽ mở workspace đ cấu hình đ sinh viên.
</p>
</div>
<button className="modal-close-btn" onClick={() => setShowCreate(false)} aria-label="Đóng">
&times;
</button>
</div>
<div className="modal-body" style={{ padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
<span style={{ fontWeight: 600, fontSize: '0.85rem', color: 'var(--text-secondary)' }}>Tên phòng thi</span>
<div className="modal-body class-picker-body exam-create-body">
<label className="exam-field">
<span className="exam-field-label">Tên phòng thi</span>
<input
className="search-input"
style={{ width: '100%', boxSizing: 'border-box' }}
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="VD: Thi cuối kỳ Java"
autoFocus
/>
</label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
<span style={{ fontWeight: 600, fontSize: '0.85rem', color: 'var(--text-secondary)' }}>Bắt đu</span>
<div className="exam-field-grid">
<label className="exam-field">
<span className="exam-field-label">Bắt đu</span>
<input
type="datetime-local"
className="search-input"
style={{ width: '100%', boxSizing: 'border-box', padding: '0.5rem 0.75rem' }}
className="search-input exam-datetime"
value={start}
onChange={(e) => setStart(e.target.value)}
/>
</label>
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
<span style={{ fontWeight: 600, fontSize: '0.85rem', color: 'var(--text-secondary)' }}>Kết thúc</span>
<label className="exam-field">
<span className="exam-field-label">Kết thúc</span>
<input
type="datetime-local"
className="search-input"
style={{ width: '100%', boxSizing: 'border-box', padding: '0.5rem 0.75rem' }}
className="search-input exam-datetime"
value={end}
onChange={(e) => setEnd(e.target.value)}
/>
@@ -226,7 +406,12 @@ export const ExamsTab: React.FC = () => {
<button type="button" className="btn btn-secondary" onClick={() => setShowCreate(false)}>
Hủy
</button>
<button type="button" className="btn btn-primary" disabled={busy} onClick={create}>
<button
type="button"
className="btn btn-primary"
disabled={busy || !name.trim() || !start || !end}
onClick={create}
>
{busy ? 'Đang tạo...' : 'Tạo & mở phòng'}
</button>
</div>
@@ -234,85 +419,327 @@ export const ExamsTab: React.FC = () => {
</div>
)}
{loading ? (
<div className="system-empty">Đang tải...</div>
) : filtered.length === 0 ? (
<div className="system-empty" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem' }}>
<p style={{ margin: 0 }}>
{search.trim()
? 'Không tìm thấy phòng thi phù hợp.'
: roomScope === 'mine'
? 'Bạn chưa tạo phòng thi nào.'
: 'Chưa có phòng thi — bấm Tạo phòng thi để bắt đầu.'}
</p>
{!search.trim() && roomScope === 'mine' && (
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button type="button" className="btn btn-primary" onClick={() => setShowCreate(true)}>
+ Tạo phòng thi
</button>
<button type="button" className="btn btn-secondary" onClick={() => setRoomScope('all')}>
Xem tất cả
</button>
</div>
)}
</div>
) : (
<div className="exams-card-grid">
{filtered.map((r) => {
const st = statusLabel(r.displayStatus || r.status);
const mine = myStaffId > 0 && Number(r.createdByStaffId) === myStaffId;
return (
<article key={r.id} className="exam-list-card">
<div className="exam-list-card-head">
<h3>{r.name}</h3>
<div style={{ display: 'flex', gap: '0.35rem', alignItems: 'center', flexWrap: 'wrap' }}>
{mine && <span className="badge badge-warning" style={{ fontSize: '0.7rem' }}>Của tôi</span>}
<span className={`badge ${st.cls}`}>{st.text}</span>
</div>
</div>
<div className="exam-list-card-meta">
<div>
<span className="exam-list-card-label">Thời gian</span>
<span>{fmtTime(r.startTime)}</span>
<span className="exam-list-card-sep"></span>
<span>{fmtTime(r.endTime)}</span>
</div>
<div>
<span className="exam-list-card-label">Quy </span>
<span>{r.studentCount} sinh viên · {r.paperCount} đ</span>
</div>
</div>
<div style={{ display: 'flex', gap: '8px', marginTop: '1.25rem' }}>
<button type="button" className="btn btn-primary btn-sm exam-list-card-open" onClick={() => openExam(r.id, r.name)} style={{ flex: 1, margin: 0 }}>
Mở phòng thi
</button>
{canDeleteRoom(r) && (
<button
type="button"
className="btn btn-secondary btn-sm"
onClick={(e) => {
e.stopPropagation();
handleDeleteRoom(r.id, r.name);
}}
style={{
borderColor: 'var(--danger)',
color: 'var(--danger)',
backgroundColor: 'transparent',
padding: '0.5rem 0.75rem',
fontWeight: 600,
transition: 'var(--transition)',
margin: 0
}}
>
Xóa
</button>
)}
</div>
</article>
);
})}
</div>
)}
<style>{`
.page-title-heading {
display: flex;
align-items: center;
gap: 0.55rem;
}
.page-title-icon {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--accent);
flex-shrink: 0;
}
.exams-header-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: flex-end;
}
.exams-header-actions .btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.exams-toolbar-card {
margin-bottom: 0 !important;
}
.exams-toolbar-inner {
margin: 0;
}
.exams-search {
max-width: none !important;
}
.exams-empty-panel {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.65rem;
padding: 2.75rem 1.25rem;
text-align: center;
}
.exams-empty-panel p {
margin: 0;
color: var(--text-secondary);
font-size: 0.9rem;
font-weight: 500;
max-width: 36ch;
line-height: 1.45;
}
.exams-empty-icon {
color: var(--text-muted);
display: flex;
}
.exams-empty-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: center;
margin-top: 0.25rem;
}
.exams-empty-actions .btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.exams-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 0.75rem;
padding: 0.85rem;
padding-bottom: 4.75rem;
}
.exam-list-card {
background: #fff;
border: 1px solid var(--border-color);
border-radius: 10px;
padding: 0.85rem;
display: flex;
flex-direction: column;
gap: 0.55rem;
box-shadow: var(--shadow-sm);
transition: border-color 0.15s, box-shadow 0.15s;
min-height: 0;
}
.exam-list-card:hover {
border-color: #c5cdd8;
box-shadow: 0 3px 12px rgba(26, 35, 50, 0.08);
}
.exam-list-card--active {
border-left: 3px solid var(--accent);
background: rgba(187, 33, 38, 0.03);
}
.exam-list-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.5rem;
flex: 1;
}
.exam-list-card-info {
min-width: 0;
flex: 1;
}
.exam-list-card-title {
margin: 0 0 0.4rem;
font-size: 0.88rem;
font-weight: 700;
color: var(--text-primary);
line-height: 1.35;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.exam-list-card-meta-block {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.exam-meta-row {
display: flex;
align-items: flex-start;
gap: 0.35rem;
font-size: 0.7rem;
color: var(--text-secondary);
line-height: 1.4;
flex-wrap: wrap;
}
.exam-meta-row svg {
flex-shrink: 0;
color: var(--text-muted);
margin-top: 0.1rem;
}
.exam-meta-row--secondary {
color: var(--text-muted);
font-size: 0.68rem;
}
.exam-list-card-sep {
color: var(--text-muted);
opacity: 0.7;
}
.exam-list-card-badges {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
flex-shrink: 0;
}
.exam-tag {
display: inline-flex;
align-items: center;
padding: 0.15rem 0.45rem;
border-radius: 999px;
font-size: 0.62rem;
font-weight: 700;
line-height: 1.2;
white-space: nowrap;
border: 1px solid transparent;
}
.exam-tag--mine {
background: rgba(234, 179, 8, 0.14);
color: #a16207;
border-color: rgba(234, 179, 8, 0.35);
}
.exam-tag--active {
background: rgba(13, 159, 110, 0.12);
color: #0b7a55;
border-color: rgba(13, 159, 110, 0.28);
}
.exam-tag--info {
background: rgba(37, 99, 235, 0.1);
color: #1d4ed8;
border-color: rgba(37, 99, 235, 0.22);
}
.exam-tag--warn {
background: rgba(245, 158, 11, 0.12);
color: #b45309;
border-color: rgba(245, 158, 11, 0.3);
}
.exam-tag--muted {
background: var(--bg-subtle);
color: var(--text-muted);
border-color: var(--border-color);
}
.exam-list-card-actions {
display: flex;
gap: 0.4rem;
margin-top: auto;
padding-top: 0.55rem;
border-top: 1px solid var(--border-light);
}
.exam-list-card-actions .btn {
flex: 1;
justify-content: center;
align-items: center;
gap: 0.25rem;
height: 30px;
padding: 0.35rem 0.5rem;
font-size: 0.72rem;
border-radius: 7px;
line-height: 1;
}
.exam-btn-danger {
color: #b91c1c !important;
border-color: #fecaca !important;
background: #fef2f2 !important;
flex: 0 0 auto !important;
min-width: 52px;
}
.exam-btn-danger:hover {
background: #fee2e2 !important;
border-color: #fca5a5 !important;
}
.exam-create-modal {
max-width: 520px !important;
}
.exam-create-modal .class-picker-header {
align-items: flex-start !important;
padding: 1.1rem 1.35rem !important;
}
.exam-create-modal .class-picker-subtitle {
margin: 0.3rem 0 0;
color: var(--text-muted);
font-size: 0.78rem;
line-height: 1.4;
max-width: 42ch;
}
.exam-create-modal .class-picker-body {
padding: 1rem 1.35rem 1.15rem !important;
display: flex;
flex-direction: column;
min-height: 0;
}
.exam-create-body {
gap: 0.9rem !important;
}
.exam-field {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.exam-field-label {
font-weight: 600;
font-size: 0.75rem;
color: var(--text-secondary);
}
.exam-field .search-input {
width: 100%;
box-sizing: border-box;
}
.exam-field-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
.exam-datetime {
padding: 0.5rem 0.75rem !important;
}
@media (max-width: 900px) {
.exams-header-actions {
width: 100%;
}
.exams-header-actions .btn {
flex: 1;
justify-content: center;
}
.page-header {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.learning-seg {
width: 100%;
}
.learning-seg-btn {
flex: 1;
}
.exams-stats-row {
grid-template-columns: repeat(3, 1fr) !important;
gap: 0.45rem;
}
}
@media (max-width: 768px) {
.exams-card-grid {
grid-template-columns: 1fr;
padding: 0.75rem;
gap: 0.65rem;
}
.exam-list-card-actions .btn {
height: 32px;
font-size: 0.75rem;
}
}
@media (max-width: 560px) {
.exams-stats-row {
grid-template-columns: 1fr !important;
}
.exam-field-grid {
grid-template-columns: 1fr;
}
.exam-list-card-actions {
flex-direction: column;
}
.exam-btn-danger {
width: 100%;
}
}
`}</style>
</div>
);
};