Spaces:
Sleeping
Sleeping
| --- | |
| import Layout from '../layouts/Layout.astro'; | |
| import { formatImageUrl } from '../lib/api.js'; | |
| import { txasupabase } from '../lib/txasupabase.js'; | |
| // Lấy tất cả phim có lịch chiếu tập tiếp theo | |
| let scheduledMovies = []; | |
| try { | |
| const localMovies = await txasupabase.getLocalMovies().catch(() => ({})); | |
| scheduledMovies = Object.entries(localMovies) | |
| .map(([slug, m]) => { | |
| if (!m || !m.movie) return null; | |
| return { | |
| ...m.movie, | |
| slug | |
| }; | |
| }) | |
| .filter(Boolean) | |
| .filter(movie => movie.next_episode_time) | |
| .filter(movie => { | |
| const nextTime = new Date(movie.next_episode_time); | |
| const minTime = new Date(Date.now() - 7 * 86400000); | |
| return nextTime >= minTime; | |
| }) | |
| .sort((a, b) => new Date(a.next_episode_time).getTime() - new Date(b.next_episode_time).getTime()); | |
| } catch (e) { | |
| console.error('[lich-chieu] Error fetching schedule:', e); | |
| } | |
| // Nhóm phim theo ngày | |
| const getVietnamDateString = (date) => { | |
| return new Date(date).toLocaleDateString('en-CA', { timeZone: 'Asia/Ho_Chi_Minh' }); | |
| }; | |
| const today = new Date(new Date().toLocaleDateString('en-US', { timeZone: 'Asia/Ho_Chi_Minh' })); | |
| today.setHours(0, 0, 0, 0); | |
| // Tạo danh sách 7 ngày từ hôm nay | |
| const days = []; | |
| for (let i = 0; i < 7; i++) { | |
| const d = new Date(today); | |
| d.setDate(d.getDate() + i); | |
| days.push(d); | |
| } | |
| const dayNames = ['Chủ Nhật', 'Thứ Hai', 'Thứ Ba', 'Thứ Tư', 'Thứ Năm', 'Thứ Sáu', 'Thứ Bảy']; | |
| function getMoviesForDate(date: Date) { | |
| const targetStr = getVietnamDateString(date); | |
| return scheduledMovies.filter(m => { | |
| return getVietnamDateString(m.next_episode_time) === targetStr; | |
| }); | |
| } | |
| --- | |
| <Layout title="Lịch chiếu phim - Lịch phát sóng tập mới" description="Xem lịch chiếu phim, lịch phát sóng tập mới nhất hàng ngày tại DPTXA." slug="/lich-chieu"> | |
| <div class="min-h-screen pb-24"> | |
| <div class="mx-auto max-w-7xl px-4"> | |
| <!-- Header --> | |
| <div class="mb-8"> | |
| <div class="flex items-center gap-3 text-[10px] font-black uppercase tracking-widest text-gray-500 mb-4"> | |
| <a href="/" class="hover:text-primary transition-colors">Trang chủ</a> | |
| <i class="fas fa-chevron-right text-[8px] opacity-30"></i> | |
| <span class="text-primary">Lịch chiếu</span> | |
| </div> | |
| <h1 class="text-3xl font-black text-white tracking-tight flex items-center gap-3"> | |
| <i class="fas fa-calendar-alt text-primary"></i> Lịch chiếu | |
| </h1> | |
| </div> | |
| <!-- Date Tabs --> | |
| <div class="relative mb-8"> | |
| <div id="date-tabs" class="flex items-center gap-2 overflow-x-auto pb-2 scrollbar-thin"> | |
| {days.map((d, i) => { | |
| const dd = String(d.getDate()).padStart(2, '0'); | |
| const mm = String(d.getMonth() + 1).padStart(2, '0'); | |
| const dayName = dayNames[d.getDay()]; | |
| const isToday = i === 0; | |
| const count = getMoviesForDate(d).length; | |
| return ( | |
| <button | |
| data-tab-index={i} | |
| class={`schedule-tab flex flex-col items-center shrink-0 px-6 py-3 rounded-2xl border transition-all cursor-pointer ${ | |
| isToday | |
| ? 'bg-primary/15 border-primary/30 text-primary' | |
| : 'bg-white/[0.03] border-white/[0.06] text-gray-400 hover:bg-white/5 hover:text-white' | |
| }`} | |
| > | |
| <span class="text-[10px] font-bold text-gray-500">{dd}/{mm}</span> | |
| <span class={`text-sm font-black ${isToday ? 'text-primary' : ''}`}>{dayName}</span> | |
| {count > 0 && ( | |
| <span class="mt-1 text-[9px] font-bold bg-primary/20 text-primary px-2 py-0.5 rounded-full">{count} phim</span> | |
| )} | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| <!-- Movie Lists per Day --> | |
| {days.map((d, i) => { | |
| const moviesForDay = getMoviesForDate(d); | |
| return ( | |
| <div data-panel-index={i} class={`schedule-panel ${i === 0 ? '' : 'hidden'}`}> | |
| {moviesForDay.length === 0 ? ( | |
| <div class="text-center py-24 glass rounded-[32px] border border-white/5 p-8 relative overflow-hidden"> | |
| <div class="absolute -right-20 -top-20 h-40 w-40 rounded-full bg-white/[0.01] blur-3xl"></div> | |
| <div class="h-16 w-16 rounded-2xl bg-white/[0.03] border border-white/5 flex items-center justify-center mx-auto mb-4 text-gray-600 shadow-inner"> | |
| <i class="far fa-calendar-times text-2xl"></i> | |
| </div> | |
| <p class="text-gray-400 text-sm font-bold tracking-tight">Không có phim nào dự kiến phát sóng vào ngày này.</p> | |
| <p class="text-xs text-gray-600 mt-1">Đón xem các cập nhật mới nhất từ ban quản trị hệ thống.</p> | |
| </div> | |
| ) : ( | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| {moviesForDay.map(m => { | |
| const poster = formatImageUrl(m.poster_url || m.thumb_url); | |
| const epLabel = m.next_episode_name || m.episode_current || 'Tập mới'; | |
| const timeStr = new Date(m.next_episode_time).toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'Asia/Ho_Chi_Minh' }); | |
| return ( | |
| <a href={`/phim/${m.slug}`} class="relative flex items-center gap-4 rounded-3xl bg-gradient-to-br from-white/[0.03] to-white/[0.01] border border-white/5 p-4 hover:bg-white/[0.06] hover:border-primary/30 transition-all duration-300 group shadow-lg overflow-hidden"> | |
| <!-- Ambient Glow --> | |
| <div class="absolute -right-8 -bottom-8 h-24 w-24 rounded-full bg-primary/5 blur-2xl group-hover:bg-primary/10 transition-colors duration-300"></div> | |
| <!-- Movie Thumbnail --> | |
| <div class="h-20 w-16 rounded-2xl overflow-hidden shrink-0 border border-white/10 shadow-md relative"> | |
| <img src={poster} alt={m.name} class="h-full w-full object-cover group-hover:scale-110 transition-transform duration-500" loading="lazy" /> | |
| <div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div> | |
| </div> | |
| <!-- Movie Meta --> | |
| <div class="min-w-0 flex-1 space-y-1"> | |
| <span class="inline-flex items-center gap-1.5 text-[9px] font-black uppercase tracking-wider text-gray-500"> | |
| <span class="h-1.5 w-1.5 rounded-full bg-primary animate-pulse"></span> | |
| Sắp phát sóng | |
| </span> | |
| <div class="text-sm font-black text-white truncate group-hover:text-primary transition-colors duration-300">{m.name}</div> | |
| <div class="flex items-center gap-2 mt-1.5"> | |
| <span class="text-[10px] font-black bg-primary/10 border border-primary/20 text-primary px-2.5 py-1 rounded-xl flex items-center gap-1"> | |
| <i class="far fa-clock"></i> | |
| {timeStr} | |
| </span> | |
| <span class="text-[10px] font-bold text-gray-400"> | |
| {epLabel} | |
| </span> | |
| </div> | |
| </div> | |
| <!-- Action Icon --> | |
| <div class="h-8 w-8 rounded-full bg-white/[0.03] border border-white/5 flex items-center justify-center shrink-0 group-hover:bg-primary/20 group-hover:border-primary/30 transition-all duration-300 text-gray-500 group-hover:text-primary shadow-inner"> | |
| <i class="fas fa-chevron-right text-xs group-hover:translate-x-0.5 transition-transform"></i> | |
| </div> | |
| </a> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| </Layout> | |
| <script> | |
| document.addEventListener('astro:page-load', () => { | |
| const tabs = document.querySelectorAll('.schedule-tab'); | |
| const panels = document.querySelectorAll('.schedule-panel'); | |
| tabs.forEach(tab => { | |
| tab.addEventListener('click', () => { | |
| const idx = tab.getAttribute('data-tab-index'); | |
| // Reset all tabs | |
| tabs.forEach(t => { | |
| t.classList.remove('bg-primary/15', 'border-primary/30', 'text-primary'); | |
| t.classList.add('bg-white/[0.03]', 'border-white/[0.06]', 'text-gray-400'); | |
| const nameSpan = t.querySelector('.text-sm'); | |
| if (nameSpan) nameSpan.classList.remove('text-primary'); | |
| }); | |
| // Activate clicked tab | |
| tab.classList.add('bg-primary/15', 'border-primary/30', 'text-primary'); | |
| tab.classList.remove('bg-white/[0.03]', 'border-white/[0.06]', 'text-gray-400'); | |
| const nameSpan = tab.querySelector('.text-sm'); | |
| if (nameSpan) nameSpan.classList.add('text-primary'); | |
| // Show/hide panels | |
| panels.forEach(p => { | |
| if (p.getAttribute('data-panel-index') === idx) { | |
| p.classList.remove('hidden'); | |
| } else { | |
| p.classList.add('hidden'); | |
| } | |
| }); | |
| }); | |
| }); | |
| }); | |
| </script> | |