feat(ui): add provider branding and admin purge confirmation modal
This commit is contained in:
BIN
frontend/public/prime.png
Normal file
BIN
frontend/public/prime.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -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<ContentListItem, 'type' | 'title' | 'year'>):
|
||||
return `${item.type}::${item.title}::${item.year ?? 'na'}`
|
||||
}
|
||||
|
||||
function getProviderBrand(item: Pick<ContentListItem, 'provider'>): { 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<SocketClient | null> {
|
||||
if (typeof window === 'undefined') return null
|
||||
if (window.io) {
|
||||
@@ -260,6 +269,7 @@ export function MoviesPage() {
|
||||
const [adminError, setAdminError] = useState<string | null>(null)
|
||||
const [adminActionMessage, setAdminActionMessage] = useState<string | null>(null)
|
||||
const [adminActionPending, setAdminActionPending] = useState<string | null>(null)
|
||||
const [purgeConfirmOpened, setPurgeConfirmOpened] = useState(false)
|
||||
const [typeFilter, setTypeFilter] = useState<'all' | ContentType>('all')
|
||||
const [search, setSearch] = useState('')
|
||||
const [sortBy, setSortBy] = useState<SortBy>('title')
|
||||
@@ -401,7 +411,7 @@ export function MoviesPage() {
|
||||
actionKey: string,
|
||||
endpoint: string,
|
||||
body?: Record<string, unknown>
|
||||
) => {
|
||||
): Promise<boolean> => {
|
||||
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
|
||||
</Button>
|
||||
<Button
|
||||
size="compact-sm"
|
||||
color="red"
|
||||
variant="light"
|
||||
onClick={() => setPurgeConfirmOpened(true)}
|
||||
>
|
||||
DB tum veriyi sil
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@@ -904,7 +937,7 @@ export function MoviesPage() {
|
||||
<Group gap="xs" mt="sm">
|
||||
<Badge variant="light">Kaynak Cache: {adminOverview.requestMetrics.sourceCounts.cache}</Badge>
|
||||
<Badge variant="light">Kaynak DB: {adminOverview.requestMetrics.sourceCounts.database}</Badge>
|
||||
<Badge variant="light">Kaynak Netflix: {adminOverview.requestMetrics.sourceCounts.netflix}</Badge>
|
||||
<Badge variant="light">Kaynak Scraper: {adminOverview.requestMetrics.sourceCounts.scraper}</Badge>
|
||||
</Group>
|
||||
<Stack gap={6} mt="md">
|
||||
<Text size="sm" fw={600}>
|
||||
@@ -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 (
|
||||
<Grid.Col key={itemKey} span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<Card
|
||||
@@ -1099,7 +1133,7 @@ export function MoviesPage() {
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs">
|
||||
<Group gap="xs" className="card-meta-row">
|
||||
{item.year && <Badge variant="light">{item.year}</Badge>}
|
||||
{item.ageRating && <Badge variant="outline">{item.ageRating}</Badge>}
|
||||
{item.currentSeason && item.type === 'tvshow' && (
|
||||
@@ -1109,11 +1143,11 @@ export function MoviesPage() {
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Text size="sm" c="dimmed" lineClamp={3}>
|
||||
<Text size="sm" c="dimmed" lineClamp={3} className="card-plot">
|
||||
{item.plot || 'Açıklama bulunamadı.'}
|
||||
</Text>
|
||||
|
||||
<Group gap={6}>
|
||||
<Group gap={6} className="card-genres">
|
||||
{item.genres.slice(0, 3).map((genre) => (
|
||||
<Badge key={genre} size="sm" variant="dot">
|
||||
{genre}
|
||||
@@ -1122,7 +1156,7 @@ export function MoviesPage() {
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<img src="/netflix.png" alt="Netflix" className="brand-stamp" />
|
||||
<img src={brand.src} alt={brand.alt} className="brand-stamp" />
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
)
|
||||
@@ -1142,6 +1176,39 @@ export function MoviesPage() {
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
<Modal
|
||||
opened={purgeConfirmOpened}
|
||||
onClose={() => {
|
||||
if (adminActionPending === 'purge-content') return
|
||||
setPurgeConfirmOpened(false)
|
||||
}}
|
||||
centered
|
||||
lockScroll={false}
|
||||
radius="md"
|
||||
title="Veritabani verilerini sil"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
Bu islem geri alinmaz. Icerik tablosundaki tum film/dizi kayitlari silinecek.
|
||||
</Text>
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setPurgeConfirmOpened(false)}
|
||||
disabled={adminActionPending === 'purge-content'}
|
||||
>
|
||||
Vazgec
|
||||
</Button>
|
||||
<Button
|
||||
color="red"
|
||||
loading={adminActionPending === 'purge-content'}
|
||||
onClick={() => void handlePurgeAllContent()}
|
||||
>
|
||||
Evet, tumunu sil
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
<Modal
|
||||
opened={Boolean(selectedContent)}
|
||||
onClose={closeContentModal}
|
||||
@@ -1159,7 +1226,7 @@ export function MoviesPage() {
|
||||
title={null}
|
||||
>
|
||||
{selectedContent && (
|
||||
<Stack gap="md">
|
||||
<Stack gap="md" className="detail-content-stack">
|
||||
<div className="detail-media-wrap">
|
||||
{selectedContent.backdrop ? (
|
||||
<Image src={selectedContent.backdrop} alt={selectedContent.title} h={230} />
|
||||
@@ -1220,7 +1287,7 @@ export function MoviesPage() {
|
||||
</Text>
|
||||
<Group gap={6}>
|
||||
{selectedContent.cast && selectedContent.cast.length > 0 ? (
|
||||
selectedContent.cast.slice(0, 14).map((castName) => (
|
||||
selectedContent.cast.slice(0, 5).map((castName) => (
|
||||
<Badge key={castName} size="sm" variant="light" color="gray">
|
||||
{castName}
|
||||
</Badge>
|
||||
@@ -1232,7 +1299,13 @@ export function MoviesPage() {
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
<img src="/netflix.png" alt="Netflix" className="detail-brand-stamp" />
|
||||
{selectedContentBrand && (
|
||||
<img
|
||||
src={selectedContentBrand.src}
|
||||
alt={selectedContentBrand.alt}
|
||||
className="detail-brand-stamp"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user