Khoi tao du an tap the duc
This commit is contained in:
143
src/App.tsx
Normal file
143
src/App.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user