Files
exercise-app/src/Admin.tsx
2026-04-06 11:09:21 +07:00

194 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
interface VideoEntry {
uid: number; // ID nội bộ để quản lý danh sách
link: string;
id: string; // YouTube ID sau khi tách
title: string;
category: string;
customCategory: string;
level: string;
}
const CATEGORIES = ["Cardio", "Cơ Bụng", "Yoga", "HIIT", "Ngực", "Chân", "Toàn thân", "Khác"];
const LEVELS = ["Dễ", "Vừa", "Khó"];
export default function Admin() {
const [entries, setEntries] = useState<VideoEntry[]>([
{ uid: Date.now(), link: '', id: '', title: '', category: 'Cardio', customCategory: '', level: 'Vừa' }
]);
const [jsonOutput, setJsonOutput] = useState<string>('');
const [copied, setCopied] = useState(false);
// Hàm tự động tách ID Youtube từ Link
const extractYouTubeID = (url: string) => {
const regExp = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/i;
const match = url.match(regExp);
return match ? match[1] : '';
};
const handleUpdate = (index: number, field: keyof VideoEntry, value: string) => {
const newEntries = [...entries];
newEntries[index][field] = value as never;
// Nếu đang dán link, tự động tách ID luôn
if (field === 'link') {
newEntries[index].id = extractYouTubeID(value);
}
setEntries(newEntries);
};
const handleAddRow = () => {
setEntries([
...entries,
{ uid: Date.now(), link: '', id: '', title: '', category: 'Cardio', customCategory: '', level: 'Vừa' }
]);
};
const handleRemoveRow = (index: number) => {
if (entries.length === 1) return; // Giữ lại ít nhất 1 dòng
const newEntries = [...entries];
newEntries.splice(index, 1);
setEntries(newEntries);
};
const handleGenerate = () => {
// Lọc bỏ những dòng chưa có ID
const validEntries = entries.filter(e => e.id.trim() !== '');
const finalData = validEntries.map((e, index) => ({
id: `vid_${Date.now()}_${index}`, // Sinh ra ID ngẫu nhiên cho video
youtube_id: e.id, // Gán mã Youtube ID vào đúng trường youtube_id
title: e.title.trim() || "Chưa có tiêu đề",
category: e.category === 'Khác' ? e.customCategory.trim() : e.category,
level: e.level
}));
setJsonOutput(JSON.stringify(finalData, null, 2));
setCopied(false);
};
const handleCopy = () => {
if (!jsonOutput) return;
navigator.clipboard.writeText(jsonOutput);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="max-w-4xl mx-auto p-4 md:p-8 bg-gray-50 min-h-screen">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-800">🛠 Trạm Tạo Dữ Liệu</h1>
<button
onClick={() => window.location.href = '/'}
className="text-sm text-blue-600 hover:underline"
>
&larr; Về App
</button>
</div>
<div className="space-y-4 mb-6">
{entries.map((entry, index) => (
<div key={entry.uid} className="bg-white p-4 rounded-xl shadow-sm border border-gray-200 relative">
<button
onClick={() => handleRemoveRow(index)}
className="absolute top-2 right-2 text-red-500 hover:bg-red-50 p-1 rounded-md"
title="Xóa dòng này"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
<div className="grid grid-cols-1 md:grid-cols-12 gap-4">
{/* Cột Link */}
<div className="md:col-span-5">
<label className="block text-xs font-medium text-gray-500 mb-1">Link YouTube</label>
<input
type="text"
value={entry.link}
onChange={(e) => handleUpdate(index, 'link', e.target.value)}
placeholder="https://youtu.be/..."
className="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
/>
{entry.id && <p className="text-xs text-green-600 mt-1">ID: {entry.id}</p>}
</div>
{/* Cột Tiêu đề */}
<div className="md:col-span-3">
<label className="block text-xs font-medium text-gray-500 mb-1">Tiêu đ</label>
<input
type="text"
value={entry.title}
onChange={(e) => handleUpdate(index, 'title', e.target.value)}
placeholder="Vd: 10 Phút Đốt Mỡ"
className="w-full border border-gray-300 rounded-md p-2 text-sm focus:ring-2 focus:ring-blue-500 outline-none"
/>
</div>
{/* Cột Phân loại */}
<div className="md:col-span-2">
<label className="block text-xs font-medium text-gray-500 mb-1">Danh mục</label>
<select
value={entry.category}
onChange={(e) => handleUpdate(index, 'category', e.target.value)}
className="w-full border border-gray-300 rounded-md p-2 text-sm bg-white"
>
{CATEGORIES.map(cat => <option key={cat} value={cat}>{cat}</option>)}
</select>
{entry.category === 'Khác' && (
<input
type="text"
value={entry.customCategory}
onChange={(e) => handleUpdate(index, 'customCategory', e.target.value)}
placeholder="Nhập tag..."
className="w-full border border-gray-300 rounded-md p-1 mt-2 text-sm"
/>
)}
</div>
{/* Cột Mức độ */}
<div className="md:col-span-2">
<label className="block text-xs font-medium text-gray-500 mb-1">Mức đ</label>
<select
value={entry.level}
onChange={(e) => handleUpdate(index, 'level', e.target.value)}
className="w-full border border-gray-300 rounded-md p-2 text-sm bg-white"
>
{LEVELS.map(lvl => <option key={lvl} value={lvl}>{lvl}</option>)}
</select>
</div>
</div>
</div>
))}
</div>
<div className="flex gap-3 mb-8">
<button
onClick={handleAddRow}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg text-sm font-medium hover:bg-gray-300 transition"
>
+ Thêm Video
</button>
<button
onClick={handleGenerate}
className="px-6 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition"
>
Sinh JSON
</button>
</div>
{jsonOutput && (
<div className="bg-gray-800 rounded-xl p-4 relative">
<button
onClick={handleCopy}
className={`absolute top-4 right-4 px-3 py-1 rounded text-xs font-medium transition ${copied ? 'bg-green-500 text-white' : 'bg-gray-600 text-gray-200 hover:bg-gray-500'}`}
>
{copied ? 'Đã Copy!' : 'Copy JSON'}
</button>
<pre className="text-green-400 text-sm overflow-x-auto pt-8">
<code>{jsonOutput}</code>
</pre>
</div>
)}
</div>
);
}