244 lines
9.0 KiB
TypeScript
244 lines
9.0 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { getAllVideos, isLoggedIn } from '../../utils/api';
|
|
import { VideoListItem } from '../../types/api';
|
|
import LoginPromptModal from '../../components/LoginPromptModal';
|
|
import VideoDetailModal from '../../components/VideoDetailModal';
|
|
import CitySelectModal from '../../components/CitySelectModal';
|
|
|
|
interface ADO2ContentsPageProps {
|
|
onBack?: () => void;
|
|
}
|
|
|
|
const ADO2ContentsPage: React.FC<ADO2ContentsPageProps> = () => {
|
|
const { t } = useTranslation();
|
|
const authed = isLoggedIn();
|
|
const [selectedVideoId, setSelectedVideoId] = useState<number | null>(null);
|
|
const [videos, setVideos] = useState<VideoListItem[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(authed);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [page, setPage] = useState(1);
|
|
const [hasNext, setHasNext] = useState(false);
|
|
const [hasPrev, setHasPrev] = useState(false);
|
|
const [totalPages, setTotalPages] = useState(1);
|
|
|
|
const pageSize = 20;
|
|
const [sortBy, setSortBy] = useState<'created_at' | 'like_count' | 'comment_count'>('created_at');
|
|
const [order, setOrder] = useState<'desc' | 'asc'>('desc');
|
|
const [searchInput, setSearchInput] = useState('');
|
|
const [storeName, setStoreName] = useState('');
|
|
const [region, setRegion] = useState('');
|
|
const [showCityModal, setShowCityModal] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!authed) return;
|
|
fetchVideos();
|
|
}, [page, sortBy, order, storeName, region]);
|
|
|
|
const fetchVideos = async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await getAllVideos(page, pageSize, sortBy, storeName, order, region);
|
|
setVideos(response.items);
|
|
setTotal(response.total);
|
|
setTotalPages(response.total_pages);
|
|
setHasNext(response.has_next);
|
|
setHasPrev(response.has_prev);
|
|
} catch (err) {
|
|
console.error('Failed to fetch all videos:', err);
|
|
setError(t('ado2Contents.loadFailed'));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCardClick = (videoId: number) => {
|
|
setSelectedVideoId(videoId);
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}.${month}.${day}`;
|
|
};
|
|
|
|
return (
|
|
<div className="ado2-contents-page">
|
|
<div className="ado2-contents-header">
|
|
<h1 className="ado2-contents-title">{t('sidebar.ado2Contents')}</h1>
|
|
<span className="ado2-contents-count">{t('ado2Contents.totalCount', { count: total })}</span>
|
|
</div>
|
|
|
|
<div className="ado2-contents-filters">
|
|
<select
|
|
className="ado2-filter-select"
|
|
value={`${sortBy}__${order}`}
|
|
onChange={(e) => {
|
|
const [sb, ord] = e.target.value.split('__') as [typeof sortBy, typeof order];
|
|
setSortBy(sb);
|
|
setOrder(ord);
|
|
setPage(1);
|
|
}}
|
|
>
|
|
<option value="created_at__desc">{t('ado2Contents.sortLatest')}</option>
|
|
<option value="created_at__asc">{t('ado2Contents.sortOldest')}</option>
|
|
<option value="like_count__desc">{t('ado2Contents.sortLikes')}</option>
|
|
<option value="comment_count__desc">{t('ado2Contents.sortComments')}</option>
|
|
</select>
|
|
|
|
<button
|
|
type="button"
|
|
className={`ado2-region-pill ${region ? 'active' : ''}`}
|
|
onClick={() => setShowCityModal(true)}
|
|
>
|
|
{region || t('ado2Contents.regionPlaceholder')}
|
|
{region && (
|
|
<span className="ado2-region-clear" onClick={(e) => { e.stopPropagation(); setRegion(''); setPage(1); }}>✕</span>
|
|
)}
|
|
</button>
|
|
|
|
<form
|
|
className="ado2-filter-search"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
setStoreName(searchInput);
|
|
setPage(1);
|
|
}}
|
|
>
|
|
<input
|
|
type="text"
|
|
className="ado2-filter-input"
|
|
value={searchInput}
|
|
onChange={(e) => setSearchInput(e.target.value)}
|
|
placeholder={t('ado2Contents.searchPlaceholder')}
|
|
/>
|
|
<button type="submit" className="ado2-filter-btn">
|
|
{t('ado2Contents.searchBtn')}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="ado2-contents-loading">
|
|
<div className="loading-spinner"></div>
|
|
<p>{t('ado2Contents.loading')}</p>
|
|
</div>
|
|
) : error ? (
|
|
<div className="ado2-contents-error">
|
|
<p>{error}</p>
|
|
<button onClick={fetchVideos} className="retry-btn">{t('ado2Contents.retry')}</button>
|
|
</div>
|
|
) : videos.length === 0 ? (
|
|
<div className="ado2-contents-empty">
|
|
<p>{t('ado2Contents.noContent')}</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="ado2-contents-grid">
|
|
{videos.map((video) => (
|
|
<div
|
|
key={video.video_id}
|
|
className="ado2-content-card"
|
|
style={{ cursor: 'pointer' }}
|
|
onClick={() => handleCardClick(video.video_id)}
|
|
role="button"
|
|
tabIndex={0}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleCardClick(video.video_id)}
|
|
>
|
|
<div className="content-card-thumbnail ado2-gallery-thumbnail-wrap">
|
|
{video.thumbnail_url ? (
|
|
<img
|
|
src={video.thumbnail_url}
|
|
alt={video.store_name}
|
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
|
/>
|
|
) : video.result_movie_url ? (
|
|
<video
|
|
src={video.result_movie_url}
|
|
className="content-video-preview"
|
|
preload="metadata"
|
|
muted
|
|
playsInline
|
|
/>
|
|
) : (
|
|
<div className="content-no-video" />
|
|
)}
|
|
{/* 호버 오버레이 */}
|
|
<div className="ado2-gallery-overlay">
|
|
<div className="ado2-gallery-play-btn">
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor">
|
|
<polygon points="5,3 19,12 5,21"/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="content-card-info">
|
|
<div className="content-card-text">
|
|
<h3 className="content-card-title">{video.store_name}</h3>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<p className="content-card-date">{formatDate(video.created_at)}</p>
|
|
<span className="content-card-like">
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="2">
|
|
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
|
</svg>
|
|
{video.like_count ?? 0}
|
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ marginLeft: '6px' }}>
|
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
|
</svg>
|
|
{video.comment_count ?? 0}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="ado2-contents-pagination">
|
|
<button
|
|
className="pagination-btn"
|
|
onClick={() => setPage(p => p - 1)}
|
|
disabled={!hasPrev}
|
|
>
|
|
{t('ado2Contents.previous')}
|
|
</button>
|
|
<span className="pagination-info">{page} / {totalPages}</span>
|
|
<button
|
|
className="pagination-btn"
|
|
onClick={() => setPage(p => p + 1)}
|
|
disabled={!hasNext}
|
|
>
|
|
{t('ado2Contents.next')}
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{!authed && (
|
|
<LoginPromptModal onClose={() => { window.location.href = '/'; }} />
|
|
)}
|
|
|
|
{selectedVideoId !== null && (
|
|
<VideoDetailModal
|
|
videoId={String(selectedVideoId)}
|
|
onClose={() => setSelectedVideoId(null)}
|
|
/>
|
|
)}
|
|
|
|
{showCityModal && (
|
|
<CitySelectModal
|
|
selected={region}
|
|
onSelect={(city) => { setRegion(city); setPage(1); }}
|
|
onClose={() => setShowCityModal(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ADO2ContentsPage;
|