diff --git a/frontend/public/prime.png b/frontend/public/prime.png new file mode 100644 index 0000000..093ccd7 Binary files /dev/null and b/frontend/public/prime.png differ diff --git a/frontend/src/pages/MoviesPage.tsx b/frontend/src/pages/MoviesPage.tsx index ec7758c..618121b 100644 --- a/frontend/src/pages/MoviesPage.tsx +++ b/frontend/src/pages/MoviesPage.tsx @@ -32,6 +32,7 @@ import { import './movies-page.css' type ContentType = 'movie' | 'tvshow' +type ContentProvider = 'netflix' | 'primevideo' type SortBy = 'title' | 'year' const WEB_API_KEY = import.meta.env.VITE_WEB_API_KEY ?? 'web-dev-key-change-me' const ADMIN_API_KEY = import.meta.env.VITE_ADMIN_API_KEY ?? 'admin-dev-key-change-me' @@ -51,6 +52,7 @@ declare global { } interface ContentListItem { + provider?: ContentProvider title: string year: number | null plot: string | null @@ -143,7 +145,7 @@ interface AdminOverview { sourceCounts: { cache: number database: number - netflix: number + scraper: number } } } @@ -195,6 +197,13 @@ function getContentKey(item: Pick): return `${item.type}::${item.title}::${item.year ?? 'na'}` } +function getProviderBrand(item: Pick): { src: string; alt: string } { + if (item.provider === 'primevideo') { + return { src: '/prime.png', alt: 'Prime Video' } + } + return { src: '/netflix.png', alt: 'Netflix' } +} + async function loadSocketClient(): Promise { if (typeof window === 'undefined') return null if (window.io) { @@ -260,6 +269,7 @@ export function MoviesPage() { const [adminError, setAdminError] = useState(null) const [adminActionMessage, setAdminActionMessage] = useState(null) const [adminActionPending, setAdminActionPending] = useState(null) + const [purgeConfirmOpened, setPurgeConfirmOpened] = useState(false) const [typeFilter, setTypeFilter] = useState<'all' | ContentType>('all') const [search, setSearch] = useState('') const [sortBy, setSortBy] = useState('title') @@ -401,7 +411,7 @@ export function MoviesPage() { actionKey: string, endpoint: string, body?: Record - ) => { + ): Promise => { const startedAt = Date.now() setAdminActionPending(actionKey) setAdminActionMessage(null) @@ -424,21 +434,32 @@ export function MoviesPage() { if (!result.success || !result.data) { setAdminError(result.error?.message || 'Admin aksiyonu basarisiz') - return + return false } setAdminActionMessage( `${result.data.details ?? 'Aksiyon tamamlandi'} | queued: ${result.data.queued}, skipped: ${result.data.skipped}` ) await loadAdminOverview(undefined, true) + return true } catch { setAdminError('Admin aksiyonu baglanti hatasi') + return false } finally { await waitForMinimumDuration(startedAt, MIN_BUTTON_LOADING_MS) setAdminActionPending(null) } } + const handlePurgeAllContent = async () => { + const success = await runAdminAction('purge-content', '/api/admin/content/purge') + if (!success) return + + setPurgeConfirmOpened(false) + setSelectedContentKey(null) + await loadContent(undefined, true, typeFilterRef.current) + } + const handleAdminRefresh = async () => { const startedAt = Date.now() await loadAdminOverview(undefined, false) @@ -582,6 +603,10 @@ export function MoviesPage() { () => items.find((item) => getContentKey(item) === selectedContentKey) ?? null, [items, selectedContentKey] ) + const selectedContentBrand = useMemo( + () => (selectedContent ? getProviderBrand(selectedContent) : null), + [selectedContent] + ) const movieCount = items.filter((item) => item.type === 'movie').length const tvCount = items.filter((item) => item.type === 'tvshow').length @@ -725,6 +750,14 @@ export function MoviesPage() { > Stale refresh + @@ -904,7 +937,7 @@ export function MoviesPage() { Kaynak Cache: {adminOverview.requestMetrics.sourceCounts.cache} Kaynak DB: {adminOverview.requestMetrics.sourceCounts.database} - Kaynak Netflix: {adminOverview.requestMetrics.sourceCounts.netflix} + Kaynak Scraper: {adminOverview.requestMetrics.sourceCounts.scraper} @@ -1057,6 +1090,7 @@ export function MoviesPage() { {visibleItems.map((item, index) => { const itemKey = getContentKey(item) const isLive = flashItemKeys.includes(itemKey) + const brand = getProviderBrand(item) return ( - + {item.year && {item.year}} {item.ageRating && {item.ageRating}} {item.currentSeason && item.type === 'tvshow' && ( @@ -1109,11 +1143,11 @@ export function MoviesPage() { )} - + {item.plot || 'Açıklama bulunamadı.'} - + {item.genres.slice(0, 3).map((genre) => ( {genre} @@ -1122,7 +1156,7 @@ export function MoviesPage() { - Netflix + {brand.alt} ) @@ -1142,6 +1176,39 @@ export function MoviesPage() { )} + { + if (adminActionPending === 'purge-content') return + setPurgeConfirmOpened(false) + }} + centered + lockScroll={false} + radius="md" + title="Veritabani verilerini sil" + > + + + Bu islem geri alinmaz. Icerik tablosundaki tum film/dizi kayitlari silinecek. + + + + + + + {selectedContent && ( - +
{selectedContent.backdrop ? ( {selectedContent.title} @@ -1220,7 +1287,7 @@ export function MoviesPage() { {selectedContent.cast && selectedContent.cast.length > 0 ? ( - selectedContent.cast.slice(0, 14).map((castName) => ( + selectedContent.cast.slice(0, 5).map((castName) => ( {castName} @@ -1232,7 +1299,13 @@ export function MoviesPage() { )} - Netflix + {selectedContentBrand && ( + {selectedContentBrand.alt} + )} )} diff --git a/frontend/src/pages/movies-page.css b/frontend/src/pages/movies-page.css index 3560216..78320f1 100644 --- a/frontend/src/pages/movies-page.css +++ b/frontend/src/pages/movies-page.css @@ -142,6 +142,10 @@ .catalog-card { position: relative; + display: flex; + flex-direction: column; + height: 100%; + min-height: 420px; overflow: hidden; cursor: pointer; background: linear-gradient(165deg, rgba(29, 33, 44, 0.95), rgba(15, 19, 30, 0.96)); @@ -212,6 +216,8 @@ .media-wrap { position: relative; + height: 190px; + flex: 0 0 190px; } .image-shell { @@ -305,6 +311,10 @@ line-height: 1.62; } +.detail-content-stack { + padding-bottom: 38px; +} + .detail-brand-stamp { position: absolute; right: 20px; @@ -319,9 +329,30 @@ .card-content { position: relative; + display: flex; + flex-direction: column; + flex: 1; + min-height: 168px; z-index: 2; } +.card-meta-row { + min-height: 26px; + align-items: center; +} + +.card-plot { + min-height: 4.8em; + line-height: 1.6; +} + +.card-genres { + margin-top: auto; + min-height: 24px; + padding-right: 36px; + overflow: hidden; +} + .card-title { font-family: 'Bricolage Grotesque', 'IBM Plex Sans', sans-serif; letter-spacing: 0.01em;