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

156 lines
6.0 KiB
TypeScript

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 {
id: string;
title: string;
category: string;
level: string;
youtube_id: string;
}
// BƯỚC 1: Đổi tên App cũ của bạn thành MainApp
function MainApp() {
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ừ thư mục con /data/ và chống Cache
useEffect(() => {
fetch('/data/data.json?t=' + new Date().getTime())
.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>
);
}
// 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 <Admin />;
}
return <MainApp />;
}
export default App;