diff --git a/docker-compose.yml b/docker-compose.yml index d2ec7dc..d327350 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,5 +7,5 @@ services: ports: - "3005:80" volumes: - # Đường dẫn đã được chuẩn hóa theo tên dự án mới - - /opt/exercise-app/data.json:/usr/share/nginx/html/data.json \ No newline at end of file + # Nối toàn bộ thư mục data vào trong nginx + - /opt/exercise-app/data:/usr/share/nginx/html/data \ No newline at end of file diff --git a/public/data.json b/public/data/data.json similarity index 75% rename from public/data.json rename to public/data/data.json index ce0c235..9d556c9 100644 --- a/public/data.json +++ b/public/data/data.json @@ -19,5 +19,14 @@ "category": "Core", "level": "Nâng cao", "youtube_id": "3p8EBPvz2ZI" + }, + + { + "id": "4", + "youtube_id": "ovUtlbbKb_4", + "title": "30 Minute Beginner Mobility Workout", + "category": "Mobility", + "level": "Vừa" } + ] diff --git a/src/Admin.tsx b/src/Admin.tsx new file mode 100644 index 0000000..06ffc42 --- /dev/null +++ b/src/Admin.tsx @@ -0,0 +1,194 @@ +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([ + { uid: Date.now(), link: '', id: '', title: '', category: 'Cardio', customCategory: '', level: 'Vừa' } + ]); + const [jsonOutput, setJsonOutput] = useState(''); + 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 ( +
+
+

🛠️ Trạm Tạo Dữ Liệu

+ +
+ +
+ {entries.map((entry, index) => ( +
+ + +
+ {/* Cột Link */} +
+ + 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 &&

ID: {entry.id}

} +
+ + {/* Cột Tiêu đề */} +
+ + 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" + /> +
+ + {/* Cột Phân loại */} +
+ + + {entry.category === 'Khác' && ( + 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" + /> + )} +
+ + {/* Cột Mức độ */} +
+ + +
+
+
+ ))} +
+ +
+ + +
+ + {jsonOutput && ( +
+ +
+            {jsonOutput}
+          
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 82748b4..4fc5873 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useMemo } from 'react'; +import Admin from './Admin'; // <-- THÊM DÒNG IMPORT NÀY // Định nghĩa kiểu dữ liệu cho Video bài tập interface ExerciseVideo { @@ -9,15 +10,16 @@ interface ExerciseVideo { youtube_id: string; } -function App() { +// BƯỚC 1: Đổi tên App cũ của bạn thành MainApp +function MainApp() { const [videos, setVideos] = useState([]); const [selectedVideo, setSelectedVideo] = useState(null); const [activeCategory, setActiveCategory] = useState('All'); const [loading, setLoading] = useState(true); - // Lấy dữ liệu từ file data.json + // Lấy dữ liệu từ thư mục con /data/ và chống Cache useEffect(() => { - fetch('/data.json') + fetch('/data/data.json?t=' + new Date().getTime()) .then((res) => res.json()) .then((data: ExerciseVideo[]) => { setVideos(data); @@ -140,4 +142,15 @@ function App() { ); } -export default App; +// BƯỚC 2: Tạo App mới làm người gác cổng kiểm tra URL +function App() { + const isAdmin = window.location.search.includes('admin=true'); + + if (isAdmin) { + return ; + } + + return ; +} + +export default App; \ No newline at end of file