Khoi tao du an tap the duc

This commit is contained in:
phuongtc
2026-04-04 10:52:18 +07:00
commit aac648d98d
22 changed files with 3950 additions and 0 deletions

143
src/App.tsx Normal file
View File

@@ -0,0 +1,143 @@
import { useState, useEffect, useMemo } from 'react';
// Định nghĩa kiểu dữ liệu cho Video bài tập
interface ExerciseVideo {
id: string;
title: string;
category: string;
level: string;
youtube_id: string;
}
function App() {
const [videos, setVideos] = useState<ExerciseVideo[]>([]);
const [selectedVideo, setSelectedVideo] = useState<ExerciseVideo | null>(null);
const [activeCategory, setActiveCategory] = useState<string>('All');
const [loading, setLoading] = useState<boolean>(true);
// Lấy dữ liệu từ file data.json
useEffect(() => {
fetch('/data.json')
.then((res) => res.json())
.then((data: ExerciseVideo[]) => {
setVideos(data);
if (data.length > 0) {
setSelectedVideo(data[0]); // Mặc định chọn video đầu tiên
}
setLoading(false);
})
.catch((err) => {
console.error('Lỗi khi tải dữ liệu bài tập:', err);
setLoading(false);
});
}, []);
// Tự động trích xuất các danh mục (category) không trùng lặp
const categories = useMemo(() => {
const uniqueCategories = new Set(videos.map((v) => v.category));
return ['All', ...Array.from(uniqueCategories)];
}, [videos]);
// Lọc danh sách video theo category đang chọn
const filteredVideos = useMemo(() => {
if (activeCategory === 'All') return videos;
return videos.filter((v) => v.category === activeCategory);
}, [videos, activeCategory]);
if (loading) {
return <div className="min-h-screen flex items-center justify-center font-semibold text-xl">Đang tải dữ liệu...</div>;
}
return (
<div className="max-w-4xl mx-auto p-4 sm:p-6 lg:p-8 flex flex-col gap-6">
<h1 className="text-2xl sm:text-3xl font-bold text-center text-blue-600 mb-2">
Hệ Thống Bài Tập Thể Dục
</h1>
{/* PHẦN 1: TRÌNH PHÁT VIDEO */}
{selectedVideo && (
<div className="w-full bg-black rounded-xl overflow-hidden shadow-lg">
<div className="aspect-video w-full relative">
<iframe
className="absolute top-0 left-0 w-full h-full"
src={`https://www.youtube.com/embed/${selectedVideo.youtube_id}?autoplay=1`}
title={selectedVideo.title}
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
<div className="p-4 bg-white border-b border-x border-gray-200 rounded-b-xl">
<h2 className="text-xl font-bold text-gray-800">{selectedVideo.title}</h2>
<div className="flex gap-2 mt-2">
<span className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm font-medium">
{selectedVideo.category}
</span>
<span className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm font-medium">
{selectedVideo.level}
</span>
</div>
</div>
</div>
)}
{/* PHẦN 2: THANH LỌC (FILTER) */}
<div className="flex flex-wrap gap-2 justify-center my-2">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`px-5 py-2.5 rounded-full font-semibold transition-all duration-200 shadow-sm
${
activeCategory === cat
? 'bg-blue-600 text-white scale-105'
: 'bg-white text-gray-600 hover:bg-gray-100 border border-gray-200'
}
`}
>
{cat}
</button>
))}
</div>
{/* PHẦN 3: DANH SÁCH BÀI TẬP (CARDS) */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredVideos.map((video) => (
<div
key={video.id}
onClick={() => {
setSelectedVideo(video);
window.scrollTo({ top: 0, behavior: 'smooth' }); // Tự động cuộn lên trình phát
}}
className={`cursor-pointer rounded-xl p-4 border-2 transition-all duration-200 flex flex-col justify-between gap-3 bg-white hover:shadow-md
${selectedVideo?.id === video.id ? 'border-blue-500 shadow-md' : 'border-transparent shadow-sm'}
`}
>
<div className="aspect-video bg-gray-200 rounded-lg overflow-hidden relative">
<img
src={`https://img.youtube.com/vi/${video.youtube_id}/mqdefault.jpg`}
alt="Thumbnail"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black bg-opacity-20 flex items-center justify-center hover:bg-opacity-10 transition-all">
<div className="w-12 h-12 bg-red-600 rounded-full flex items-center justify-center">
<svg className="w-6 h-6 text-white ml-1" fill="currentColor" viewBox="0 0 20 20"><path d="M4 4l12 6-12 6z"/></svg>
</div>
</div>
</div>
<div>
<h3 className="font-bold text-gray-800 line-clamp-2">{video.title}</h3>
<div className="flex items-center gap-2 mt-2 text-xs font-semibold">
<span className="text-blue-600 bg-blue-50 px-2 py-1 rounded">{video.category}</span>
<span className="text-green-600 bg-green-50 px-2 py-1 rounded">{video.level}</span>
</div>
</div>
</div>
))}
</div>
</div>
);
}
export default App;