feat(ui): saglayici logosu, kart duzeni ve admin silme onay modali ekle

This commit is contained in:
2026-03-01 01:13:59 +03:00
parent 84131576cf
commit ad65453fcf
3 changed files with 115 additions and 11 deletions

BIN
frontend/public/prime.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@@ -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>

View File

@@ -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;