All checks were successful
Deploy on Master Change / deploy (push) Successful in 40s
654 lines
30 KiB
TypeScript
654 lines
30 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { api, type AppDownloadItem, type AppGuideItem } from '../api';
|
|
import { useAuth } from '../auth/AuthContext';
|
|
|
|
// Icons
|
|
const IconDownload = () => (
|
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
<polyline points="7 10 12 15 17 10" />
|
|
<line x1="12" y1="15" x2="12" y2="3" />
|
|
</svg>
|
|
);
|
|
|
|
const PlatformIcon = ({ platform }: { platform: string }) => {
|
|
const p = platform.toLowerCase();
|
|
if (p.includes('win')) {
|
|
return (
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" style={{ color: '#0078d7' }}>
|
|
<path d="M0 3.449L9.75 2.1v9.45H0V3.449zM0 12.45h9.75v9.45L0 20.551v-8.1zM10.8 1.95L24 0v11.55H10.8V1.95zM10.8 12.45H24v11.55l-13.2-1.95v-9.6z" />
|
|
</svg>
|
|
);
|
|
}
|
|
if (p.includes('mac') || p.includes('apple') || p.includes('ios')) {
|
|
return (
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" style={{ color: '#555555' }}>
|
|
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M15.97 4.17c.66-.81 1.11-1.93.99-3.05-1 .04-2.22.67-2.94 1.51-.62.73-1.16 1.87-1.01 2.97 1.12.09 2.27-.58 2.96-1.43z" />
|
|
</svg>
|
|
);
|
|
}
|
|
if (p.includes('linux')) {
|
|
return (
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" style={{ color: '#FCC624' }}>
|
|
<path d="M12 2c-3.3 0-6 2.7-6 6 0 2.2.8 4.2 2.2 5.6C6.5 15.6 5 18.5 5 22h14c0-3.5-1.5-6.4-3.2-8.4 1.4-1.4 2.2-3.4 2.2-5.6 0-3.3-2.7-6-6-6zm0 2c2.2 0 4 1.8 4 4 0 .9-.3 1.7-.8 2.3-.3.4-.7.7-1.2.9-.6.2-1.3.3-2 .3s-1.4-.1-2-.3c-.5-.2-.9-.5-1.2-.9-.5-.6-.8-1.4-.8-2.3 0-2.2 1.8-4 4-4z" />
|
|
</svg>
|
|
);
|
|
}
|
|
return (
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ color: 'var(--accent)' }}>
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="2" y1="12" x2="22" y2="12" />
|
|
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
|
|
</svg>
|
|
);
|
|
};
|
|
|
|
export const ApplicationsTab: React.FC = () => {
|
|
const { staff } = useAuth();
|
|
const isSuperAdmin = staff?.email === 'phuocntb@rikkeiacademy.com';
|
|
|
|
const [activeSubTab, setActiveSubTab] = useState<'apps' | 'guides'>('apps');
|
|
|
|
// Apps state
|
|
const [apps, setApps] = useState<AppDownloadItem[]>([]);
|
|
const [appsLoading, setAppsLoading] = useState(true);
|
|
|
|
// Guides state
|
|
const [guides, setGuides] = useState<AppGuideItem[]>([]);
|
|
const [guidesLoading, setGuidesLoading] = useState(true);
|
|
|
|
// App Modal states
|
|
const [showAppModal, setShowAppModal] = useState(false);
|
|
const [editingApp, setEditingApp] = useState<AppDownloadItem | null>(null);
|
|
const [appName, setAppName] = useState('');
|
|
const [appDescription, setAppDescription] = useState('');
|
|
const [appDownloadUrl, setAppDownloadUrl] = useState('');
|
|
const [appPlatform, setAppPlatform] = useState('Windows');
|
|
const [appSaving, setAppSaving] = useState(false);
|
|
|
|
// Guide Modal states
|
|
const [showGuideModal, setShowGuideModal] = useState(false);
|
|
const [editingGuide, setEditingGuide] = useState<AppGuideItem | null>(null);
|
|
const [guideTitle, setGuideTitle] = useState('');
|
|
const [guideUrl, setGuideUrl] = useState('');
|
|
const [guideSaving, setGuideSaving] = useState(false);
|
|
|
|
const fetchApps = async () => {
|
|
try {
|
|
setAppsLoading(true);
|
|
const res = await api.listAppDownloads();
|
|
setApps(res.data || []);
|
|
} catch (err) {
|
|
console.error('Không thể tải danh sách ứng dụng:', err);
|
|
} finally {
|
|
setAppsLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchGuides = async () => {
|
|
try {
|
|
setGuidesLoading(true);
|
|
const res = await api.listAppGuides();
|
|
setGuides(res.data || []);
|
|
} catch (err) {
|
|
console.error('Không thể tải danh sách hướng dẫn sinh viên:', err);
|
|
} finally {
|
|
setGuidesLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchApps();
|
|
fetchGuides();
|
|
}, []);
|
|
|
|
// App Modal Handlers
|
|
const handleOpenAddApp = () => {
|
|
setEditingApp(null);
|
|
setAppName('');
|
|
setAppDescription('');
|
|
setAppDownloadUrl('');
|
|
setAppPlatform('Windows');
|
|
setShowAppModal(true);
|
|
};
|
|
|
|
const handleOpenEditApp = (app: AppDownloadItem) => {
|
|
setEditingApp(app);
|
|
setAppName(app.name);
|
|
setAppDescription(app.description);
|
|
setAppDownloadUrl(app.downloadUrl);
|
|
setAppPlatform(app.platform === 'macOS' ? 'MacOS' : app.platform);
|
|
setShowAppModal(true);
|
|
};
|
|
|
|
const handleSaveApp = async () => {
|
|
if (!appName.trim() || !appDownloadUrl.trim()) {
|
|
alert('Vui lòng nhập tên ứng dụng và đường dẫn tải.');
|
|
return;
|
|
}
|
|
let finalPlatform = appPlatform;
|
|
if (finalPlatform.toLowerCase() === 'macos') {
|
|
finalPlatform = 'MacOS';
|
|
}
|
|
|
|
try {
|
|
setAppSaving(true);
|
|
if (editingApp) {
|
|
await api.updateAppDownload(editingApp.id, appName.trim(), appDescription.trim(), appDownloadUrl.trim(), finalPlatform);
|
|
} else {
|
|
await api.createAppDownload(appName.trim(), appDescription.trim(), appDownloadUrl.trim(), finalPlatform);
|
|
}
|
|
setShowAppModal(false);
|
|
await fetchApps();
|
|
} catch (err: any) {
|
|
alert(err.message || 'Lưu ứng dụng thất bại');
|
|
} finally {
|
|
setAppSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteApp = async (id: number, name: string) => {
|
|
if (window.confirm(`Bạn có chắc chắn muốn xóa liên kết tải cho ứng dụng "${name}" không?`)) {
|
|
try {
|
|
await api.deleteAppDownload(id);
|
|
await fetchApps();
|
|
} catch (err: any) {
|
|
alert(err.message || 'Xóa ứng dụng thất bại');
|
|
}
|
|
}
|
|
};
|
|
|
|
// Guide Modal Handlers
|
|
const handleOpenAddGuide = () => {
|
|
setEditingGuide(null);
|
|
setGuideTitle('');
|
|
setGuideUrl('');
|
|
setShowGuideModal(true);
|
|
};
|
|
|
|
const handleOpenEditGuide = (guide: AppGuideItem) => {
|
|
setEditingGuide(guide);
|
|
setGuideTitle(guide.title);
|
|
setGuideUrl(guide.url);
|
|
setShowGuideModal(true);
|
|
};
|
|
|
|
const handleSaveGuide = async () => {
|
|
if (!guideTitle.trim() || !guideUrl.trim()) {
|
|
alert('Vui lòng điền đầy đủ tiêu đề và đường dẫn hướng dẫn');
|
|
return;
|
|
}
|
|
try {
|
|
setGuideSaving(true);
|
|
if (editingGuide) {
|
|
await api.updateAppGuide(editingGuide.id, guideTitle.trim(), guideUrl.trim());
|
|
} else {
|
|
await api.createAppGuide(guideTitle.trim(), guideUrl.trim());
|
|
}
|
|
setShowGuideModal(false);
|
|
await fetchGuides();
|
|
} catch (err: any) {
|
|
alert(err.message || 'Lưu hướng dẫn thất bại');
|
|
} finally {
|
|
setGuideSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteGuide = async (id: number, title: string) => {
|
|
if (window.confirm(`Bạn có chắc chắn muốn xóa tài liệu hướng dẫn sinh viên "${title}"?`)) {
|
|
try {
|
|
await api.deleteAppGuide(id);
|
|
await fetchGuides();
|
|
} catch (err: any) {
|
|
alert(err.message || 'Xóa hướng dẫn thất bại');
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="tab-page">
|
|
<header className="page-header page-header--row">
|
|
<div>
|
|
<h1 className="page-title">Ứng Dụng & Hướng Dẫn</h1>
|
|
<p className="page-desc">Quản lý các công cụ, link tải ứng dụng và các tài liệu hướng dẫn cho sinh viên.</p>
|
|
</div>
|
|
{isSuperAdmin && (
|
|
<div>
|
|
{activeSubTab === 'apps' ? (
|
|
<button className="btn btn-primary" onClick={handleOpenAddApp}>
|
|
+ Thêm ứng dụng
|
|
</button>
|
|
) : (
|
|
<button className="btn btn-primary" onClick={handleOpenAddGuide}>
|
|
+ Thêm tài liệu hướng dẫn
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</header>
|
|
|
|
<div style={{ padding: '0 2rem' }}>
|
|
<div style={{
|
|
display: 'flex',
|
|
gap: '12px',
|
|
borderBottom: '1px solid var(--border-color)',
|
|
paddingBottom: '12px',
|
|
marginBottom: '1.5rem'
|
|
}}>
|
|
<button
|
|
type="button"
|
|
className={`btn ${activeSubTab === 'apps' ? 'btn-primary' : 'btn-ghost'}`}
|
|
onClick={() => setActiveSubTab('apps')}
|
|
style={{
|
|
padding: '0.6rem 1.5rem',
|
|
borderRadius: 'var(--radius-sm)',
|
|
fontWeight: 600,
|
|
fontSize: '0.9rem',
|
|
transition: 'var(--transition)'
|
|
}}
|
|
>
|
|
Ứng dụng tải về
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className={`btn ${activeSubTab === 'guides' ? 'btn-primary' : 'btn-ghost'}`}
|
|
onClick={() => setActiveSubTab('guides')}
|
|
style={{
|
|
padding: '0.6rem 1.5rem',
|
|
borderRadius: 'var(--radius-sm)',
|
|
fontWeight: 600,
|
|
fontSize: '0.9rem',
|
|
transition: 'var(--transition)'
|
|
}}
|
|
>
|
|
Tài liệu & Video Hướng dẫn
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="tab-page-body tab-page-scroll" style={{ padding: '0 2rem 2rem 2rem' }}>
|
|
{activeSubTab === 'apps' && (
|
|
<div className="content-card" style={{ padding: '1.5rem', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-sm)' }}>
|
|
{appsLoading && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', padding: '4rem 1.5rem' }}>
|
|
<div className="sync-spinner" style={{ width: '36px', height: '36px', borderWidth: '3px' }}></div>
|
|
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Đang tải danh sách ứng dụng...</p>
|
|
</div>
|
|
)}
|
|
|
|
{!appsLoading && apps.length === 0 && (
|
|
<div style={{ textAlign: 'center', padding: '4rem 1.5rem', color: 'var(--text-muted)' }}>
|
|
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginBottom: '0.75rem' }}>
|
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
<path d="M21 12H3M12 3v18" />
|
|
</svg>
|
|
<p style={{ fontSize: '0.95rem' }}>Chưa có ứng dụng nào được cấu hình tải.</p>
|
|
{isSuperAdmin && (
|
|
<button className="btn btn-secondary" onClick={handleOpenAddApp} style={{ marginTop: '1rem' }}>
|
|
Thêm ứng dụng đầu tiên
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{!appsLoading && apps.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table" style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
<thead>
|
|
<tr>
|
|
<th style={{ padding: '1.15rem 1.5rem', width: '15%', fontWeight: 700, color: 'var(--text-secondary)', textAlign: 'left' }}>Nền tảng</th>
|
|
<th style={{ padding: '1.15rem 1.5rem', width: '25%', fontWeight: 700, color: 'var(--text-secondary)', textAlign: 'left' }}>Ứng dụng</th>
|
|
<th style={{ padding: '1.15rem 1.5rem', width: '35%', fontWeight: 700, color: 'var(--text-secondary)', textAlign: 'left' }}>Mô tả</th>
|
|
<th style={{ padding: '1.15rem 1.5rem', width: '15%', fontWeight: 700, color: 'var(--text-secondary)', textAlign: 'left' }}>Đường dẫn tải</th>
|
|
{isSuperAdmin && <th style={{ padding: '1.15rem 1.5rem', width: '10%', textAlign: 'right', fontWeight: 700, color: 'var(--text-secondary)' }}>Thao tác</th>}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{apps.map((app) => (
|
|
<tr key={app.id} style={{ borderBottom: '1px solid var(--border-color)' }}>
|
|
<td style={{ padding: '1.15rem 1.5rem' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
<PlatformIcon platform={app.platform} />
|
|
<span style={{ fontWeight: 600, fontSize: '0.9rem', color: 'var(--text-primary)' }}>
|
|
{app.platform === 'macOS' ? 'MacOS' : app.platform}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td style={{ padding: '1.15rem 1.5rem', fontWeight: 700, color: 'var(--text-primary)', fontSize: '0.95rem' }}>
|
|
{app.name}
|
|
</td>
|
|
<td style={{ padding: '1.15rem 1.5rem', color: 'var(--text-secondary)', fontSize: '0.9rem', lineHeight: '1.5' }}>
|
|
{app.description || '—'}
|
|
</td>
|
|
<td style={{ padding: '1.15rem 1.5rem' }}>
|
|
<a
|
|
href={app.downloadUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '6px',
|
|
color: 'var(--accent)',
|
|
fontSize: '0.9rem',
|
|
textDecoration: 'none',
|
|
fontWeight: 600
|
|
}}
|
|
onMouseEnter={(e) => e.currentTarget.style.textDecoration = 'underline'}
|
|
onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}
|
|
>
|
|
<IconDownload />
|
|
Tải ứng dụng
|
|
</a>
|
|
</td>
|
|
{isSuperAdmin && (
|
|
<td style={{ padding: '1.15rem 1.5rem', textAlign: 'right' }}>
|
|
<div style={{ display: 'inline-flex', gap: '6px' }}>
|
|
<button
|
|
className="btn btn-secondary btn-sm"
|
|
onClick={() => handleOpenEditApp(app)}
|
|
style={{ padding: '0.35rem 0.75rem', fontSize: '0.85rem' }}
|
|
>
|
|
Sửa
|
|
</button>
|
|
<button
|
|
className="btn btn-secondary btn-sm"
|
|
onClick={() => handleDeleteApp(app.id, app.name)}
|
|
style={{ padding: '0.35rem 0.75rem', fontSize: '0.85rem', color: 'var(--danger)', borderColor: 'rgba(214, 48, 49, 0.2)' }}
|
|
>
|
|
Xóa
|
|
</button>
|
|
</div>
|
|
</td>
|
|
)}
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{activeSubTab === 'guides' && (
|
|
<div className="content-card" style={{ padding: '1.5rem', borderRadius: 'var(--radius-md)', boxShadow: 'var(--shadow-sm)' }}>
|
|
{guidesLoading && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '0.75rem', padding: '4rem 1.5rem' }}>
|
|
<div className="sync-spinner" style={{ width: '36px', height: '36px', borderWidth: '3px' }}></div>
|
|
<p style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>Đang tải danh sách tài liệu hướng dẫn...</p>
|
|
</div>
|
|
)}
|
|
|
|
{!guidesLoading && guides.length === 0 && (
|
|
<div style={{ textAlign: 'center', padding: '4rem 1.5rem', color: 'var(--text-muted)' }}>
|
|
<svg viewBox="0 0 24 24" width="48" height="48" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginBottom: '0.75rem' }}>
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
<polyline points="14 2 14 8 20 8" />
|
|
</svg>
|
|
<p style={{ fontSize: '0.95rem' }}>Chưa có tài liệu hướng dẫn sinh viên nào.</p>
|
|
{isSuperAdmin && (
|
|
<button className="btn btn-secondary" onClick={handleOpenAddGuide} style={{ marginTop: '1rem' }}>
|
|
Thêm hướng dẫn sinh viên đầu tiên
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{!guidesLoading && guides.length > 0 && (
|
|
<div className="table-responsive">
|
|
<table className="table" style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
<thead>
|
|
<tr>
|
|
<th style={{ padding: '1.15rem 1.5rem', width: '15%', fontWeight: 700, color: 'var(--text-secondary)', textAlign: 'left' }}>Loại tài liệu</th>
|
|
<th style={{ padding: '1.15rem 1.5rem', width: '50%', fontWeight: 700, color: 'var(--text-secondary)', textAlign: 'left' }}>Tiêu đề hướng dẫn sinh viên</th>
|
|
<th style={{ padding: '1.15rem 1.5rem', width: '25%', fontWeight: 700, color: 'var(--text-secondary)', textAlign: 'left' }}>Đường dẫn liên kết</th>
|
|
{isSuperAdmin && <th style={{ padding: '1.15rem 1.5rem', width: '10%', textAlign: 'right', fontWeight: 700, color: 'var(--text-secondary)' }}>Thao tác</th>}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{guides.map((guide) => {
|
|
const isVideo = guide.url.toLowerCase().includes('youtube.com') || guide.url.toLowerCase().includes('youtu.be') || guide.url.toLowerCase().includes('drive.google.com/file');
|
|
return (
|
|
<tr key={guide.id} style={{ borderBottom: '1px solid var(--border-color)' }}>
|
|
<td style={{ padding: '1.15rem 1.5rem' }}>
|
|
<span className="platform-badge" style={{ backgroundColor: isVideo ? '#ffebee' : '#e3f2fd', color: isVideo ? '#d32f2f' : '#1976d2', padding: '0.3rem 0.6rem', fontSize: '0.75rem', borderRadius: '4px', fontWeight: 700 }}>
|
|
{isVideo ? 'VIDEO' : 'TÀI LIỆU'}
|
|
</span>
|
|
</td>
|
|
<td style={{ padding: '1.15rem 1.5rem', fontWeight: 700, color: 'var(--text-primary)', fontSize: '0.95rem' }}>
|
|
{guide.title}
|
|
</td>
|
|
<td style={{ padding: '1.15rem 1.5rem' }}>
|
|
<a
|
|
href={guide.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '6px',
|
|
color: 'var(--accent)',
|
|
fontSize: '0.9rem',
|
|
textDecoration: 'none',
|
|
fontWeight: 600
|
|
}}
|
|
onMouseEnter={(e) => e.currentTarget.style.textDecoration = 'underline'}
|
|
onMouseLeave={(e) => e.currentTarget.style.textDecoration = 'none'}
|
|
>
|
|
Xem liên kết →
|
|
</a>
|
|
</td>
|
|
{isSuperAdmin && (
|
|
<td style={{ padding: '1.15rem 1.5rem', textAlign: 'right' }}>
|
|
<div style={{ display: 'inline-flex', gap: '6px' }}>
|
|
<button
|
|
className="btn btn-secondary btn-sm"
|
|
onClick={() => handleOpenEditGuide(guide)}
|
|
style={{ padding: '0.35rem 0.75rem', fontSize: '0.85rem' }}
|
|
>
|
|
Sửa
|
|
</button>
|
|
<button
|
|
className="btn btn-secondary btn-sm"
|
|
onClick={() => handleDeleteGuide(guide.id, guide.title)}
|
|
style={{ padding: '0.35rem 0.75rem', fontSize: '0.85rem', color: 'var(--danger)', borderColor: 'rgba(214, 48, 49, 0.2)' }}
|
|
>
|
|
Xóa
|
|
</button>
|
|
</div>
|
|
</td>
|
|
)}
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* App Modal */}
|
|
{showAppModal && (
|
|
<div className="modal-overlay" onClick={() => setShowAppModal(false)}>
|
|
<div className="modal-container" style={{ maxWidth: '480px' }} onClick={e => e.stopPropagation()}>
|
|
<div className="modal-header">
|
|
<h2 className="modal-title">{editingApp ? 'Cập nhật ứng dụng' : 'Thêm ứng dụng tải'}</h2>
|
|
<button className="modal-close-btn" onClick={() => setShowAppModal(false)} aria-label="Đóng">×</button>
|
|
</div>
|
|
<div className="modal-body" style={{ padding: '1.5rem' }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
|
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 600, color: 'var(--text-secondary)' }}>Tên ứng dụng</span>
|
|
<input
|
|
type="text"
|
|
value={appName}
|
|
onChange={e => setAppName(e.target.value)}
|
|
placeholder="VD: Simple Care Client v1.2"
|
|
style={{
|
|
width: '100%',
|
|
padding: '0.6rem 0.85rem',
|
|
borderRadius: 'var(--radius-sm)',
|
|
border: '1px solid var(--border-color)',
|
|
fontSize: '0.9rem',
|
|
outline: 'none',
|
|
transition: 'var(--transition)'
|
|
}}
|
|
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
|
|
onBlur={e => e.target.style.borderColor = 'var(--border-color)'}
|
|
/>
|
|
</label>
|
|
|
|
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 600, color: 'var(--text-secondary)' }}>Nền tảng</span>
|
|
<select
|
|
value={appPlatform}
|
|
onChange={e => setAppPlatform(e.target.value)}
|
|
style={{
|
|
width: '100%',
|
|
padding: '0.6rem 0.85rem',
|
|
borderRadius: 'var(--radius-sm)',
|
|
border: '1px solid var(--border-color)',
|
|
fontSize: '0.9rem',
|
|
outline: 'none',
|
|
backgroundColor: '#fff',
|
|
transition: 'var(--transition)'
|
|
}}
|
|
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
|
|
onBlur={e => e.target.style.borderColor = 'var(--border-color)'}
|
|
>
|
|
<option value="Windows">Windows</option>
|
|
<option value="MacOS">MacOS</option>
|
|
<option value="Linux">Linux</option>
|
|
<option value="Khác">Khác</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 600, color: 'var(--text-secondary)' }}>Mô tả ngắn</span>
|
|
<input
|
|
type="text"
|
|
value={appDescription}
|
|
onChange={e => setAppDescription(e.target.value)}
|
|
placeholder="VD: Dành cho máy Windows 64-bit"
|
|
style={{
|
|
width: '100%',
|
|
padding: '0.6rem 0.85rem',
|
|
borderRadius: 'var(--radius-sm)',
|
|
border: '1px solid var(--border-color)',
|
|
fontSize: '0.9rem',
|
|
outline: 'none',
|
|
transition: 'var(--transition)'
|
|
}}
|
|
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
|
|
onBlur={e => e.target.style.borderColor = 'var(--border-color)'}
|
|
/>
|
|
</label>
|
|
|
|
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 600, color: 'var(--text-secondary)' }}>Đường dẫn tải</span>
|
|
<input
|
|
type="url"
|
|
value={appDownloadUrl}
|
|
onChange={e => setAppDownloadUrl(e.target.value)}
|
|
placeholder="https://..."
|
|
style={{
|
|
width: '100%',
|
|
padding: '0.6rem 0.85rem',
|
|
borderRadius: 'var(--radius-sm)',
|
|
border: '1px solid var(--border-color)',
|
|
fontSize: '0.9rem',
|
|
outline: 'none',
|
|
transition: 'var(--transition)'
|
|
}}
|
|
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
|
|
onBlur={e => e.target.style.borderColor = 'var(--border-color)'}
|
|
/>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div className="modal-footer" style={{ padding: '1rem 1.5rem' }}>
|
|
<button className="btn btn-secondary" onClick={() => setShowAppModal(false)}>Hủy</button>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleSaveApp}
|
|
disabled={appSaving}
|
|
>
|
|
{appSaving ? 'Đang lưu...' : 'Lưu lại'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Guide Modal */}
|
|
{showGuideModal && (
|
|
<div className="modal-overlay" onClick={() => setShowGuideModal(false)}>
|
|
<div className="modal-container" style={{ maxWidth: '480px' }} onClick={e => e.stopPropagation()}>
|
|
<div className="modal-header">
|
|
<h2 className="modal-title">{editingGuide ? 'Cập nhật hướng dẫn sinh viên' : 'Thêm tài liệu hướng dẫn sinh viên'}</h2>
|
|
<button className="modal-close-btn" onClick={() => setShowGuideModal(false)} aria-label="Đóng">×</button>
|
|
</div>
|
|
<div className="modal-body" style={{ padding: '1.5rem' }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
|
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 600, color: 'var(--text-secondary)' }}>Tiêu đề hướng dẫn</span>
|
|
<input
|
|
type="text"
|
|
value={guideTitle}
|
|
onChange={e => setGuideTitle(e.target.value)}
|
|
placeholder="VD: Hướng dẫn cài đặt và chạy ứng dụng cho sinh viên"
|
|
style={{
|
|
width: '100%',
|
|
padding: '0.6rem 0.85rem',
|
|
borderRadius: 'var(--radius-sm)',
|
|
border: '1px solid var(--border-color)',
|
|
fontSize: '0.9rem',
|
|
outline: 'none',
|
|
transition: 'var(--transition)'
|
|
}}
|
|
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
|
|
onBlur={e => e.target.style.borderColor = 'var(--border-color)'}
|
|
/>
|
|
</label>
|
|
|
|
<label className="login-field" style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
|
<span style={{ fontSize: '0.85rem', fontWeight: 600, color: 'var(--text-secondary)' }}>Đường dẫn hướng dẫn (Google Docs / Video Youtube)</span>
|
|
<input
|
|
type="url"
|
|
value={guideUrl}
|
|
onChange={e => setGuideUrl(e.target.value)}
|
|
placeholder="https://docs.google.com/document/d/... hoặc https://youtube.com/..."
|
|
style={{
|
|
width: '100%',
|
|
padding: '0.6rem 0.85rem',
|
|
borderRadius: 'var(--radius-sm)',
|
|
border: '1px solid var(--border-color)',
|
|
fontSize: '0.9rem',
|
|
outline: 'none',
|
|
transition: 'var(--transition)'
|
|
}}
|
|
onFocus={e => e.target.style.borderColor = 'var(--accent)'}
|
|
onBlur={e => e.target.style.borderColor = 'var(--border-color)'}
|
|
/>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div className="modal-footer" style={{ padding: '1rem 1.5rem' }}>
|
|
<button className="btn btn-secondary" onClick={() => setShowGuideModal(false)}>Hủy</button>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleSaveGuide}
|
|
disabled={guideSaving}
|
|
>
|
|
{guideSaving ? 'Đang lưu...' : 'Lưu lại'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|