156 lines
6.0 KiB
TypeScript
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; |