first commit
This commit is contained in:
208
frontend/src/pages/MoviesPage.tsx
Normal file
208
frontend/src/pages/MoviesPage.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Card,
|
||||
Container,
|
||||
Grid,
|
||||
Group,
|
||||
Image,
|
||||
Loader,
|
||||
Paper,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core'
|
||||
import { IconAlertCircle, IconDeviceTv, IconMovie } from '@tabler/icons-react'
|
||||
|
||||
type ContentType = 'movie' | 'tvshow'
|
||||
|
||||
interface ContentListItem {
|
||||
title: string
|
||||
year: number | null
|
||||
plot: string | null
|
||||
ageRating: string | null
|
||||
type: ContentType
|
||||
genres: string[]
|
||||
backdrop: string | null
|
||||
currentSeason: number | null
|
||||
}
|
||||
|
||||
interface ContentListResponse {
|
||||
success: boolean
|
||||
data?: ContentListItem[]
|
||||
error?: {
|
||||
message: string
|
||||
}
|
||||
}
|
||||
|
||||
export function MoviesPage() {
|
||||
const [items, setItems] = useState<ContentListItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [typeFilter, setTypeFilter] = useState<'all' | ContentType>('all')
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
|
||||
const loadContent = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
params.append('limit', '100')
|
||||
if (typeFilter !== 'all') {
|
||||
params.append('type', typeFilter)
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/content?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-API-Key': 'web-dev-key-change-me',
|
||||
},
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
const data: ContentListResponse = await response.json()
|
||||
if (data.success && data.data) {
|
||||
setItems(data.data)
|
||||
return
|
||||
}
|
||||
|
||||
setError(data.error?.message || 'Liste alınamadı')
|
||||
} catch {
|
||||
if (!controller.signal.aborted) {
|
||||
setError('Bağlantı hatası')
|
||||
}
|
||||
} finally {
|
||||
if (!controller.signal.aborted) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void loadContent()
|
||||
|
||||
return () => controller.abort()
|
||||
}, [typeFilter])
|
||||
|
||||
const pageTitle = useMemo(() => {
|
||||
if (typeFilter === 'movie') return 'Film Listesi'
|
||||
if (typeFilter === 'tvshow') return 'Dizi Listesi'
|
||||
return 'Film ve Dizi Listesi'
|
||||
}, [typeFilter])
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl">
|
||||
<Stack gap="lg">
|
||||
<Paper radius="md" p="lg" withBorder>
|
||||
<Group justify="space-between" align="end">
|
||||
<div>
|
||||
<Title order={1}>{pageTitle}</Title>
|
||||
<Text c="dimmed" size="sm">
|
||||
Veriler doğrudan veritabanından okunur.
|
||||
</Text>
|
||||
</div>
|
||||
<SegmentedControl
|
||||
value={typeFilter}
|
||||
onChange={(value) => setTypeFilter(value as 'all' | ContentType)}
|
||||
data={[
|
||||
{ label: 'Tümü', value: 'all' },
|
||||
{ label: 'Filmler', value: 'movie' },
|
||||
{ label: 'Diziler', value: 'tvshow' },
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
{error && (
|
||||
<Alert icon={<IconAlertCircle size={16} />} color="red" title="Hata">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<Group justify="center" py="xl">
|
||||
<Loader size="lg" />
|
||||
</Group>
|
||||
) : (
|
||||
<Grid>
|
||||
{items.map((item) => (
|
||||
<Grid.Col key={`${item.type}-${item.title}-${item.year ?? 'na'}`} span={{ base: 12, sm: 6, md: 4 }}>
|
||||
<Card shadow="sm" radius="md" padding="md" withBorder style={{ position: 'relative' }}>
|
||||
<Card.Section>
|
||||
{item.backdrop ? (
|
||||
<Image src={item.backdrop} alt={item.title} h={180} />
|
||||
) : (
|
||||
<Group h={180} justify="center" style={{ backgroundColor: '#262626' }}>
|
||||
{item.type === 'movie' ? (
|
||||
<IconMovie size={48} color="#8a8a8a" />
|
||||
) : (
|
||||
<IconDeviceTv size={48} color="#8a8a8a" />
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
</Card.Section>
|
||||
|
||||
<Stack gap="xs" mt="md" pb="lg">
|
||||
<Group justify="space-between" align="start">
|
||||
<Text fw={700} lineClamp={2}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<Badge color={item.type === 'movie' ? 'red' : 'blue'}>
|
||||
{item.type === 'movie' ? 'Film' : 'Dizi'}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
<Group gap="xs">
|
||||
{item.year && <Badge variant="light">{item.year}</Badge>}
|
||||
{item.ageRating && <Badge variant="outline">{item.ageRating}</Badge>}
|
||||
{item.currentSeason && item.type === 'tvshow' && (
|
||||
<Badge variant="light" color="grape">
|
||||
S{item.currentSeason}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Text size="sm" c="dimmed" lineClamp={3}>
|
||||
{item.plot || 'Açıklama bulunamadı.'}
|
||||
</Text>
|
||||
|
||||
<Group gap={6}>
|
||||
{item.genres.slice(0, 3).map((genre) => (
|
||||
<Badge key={genre} size="sm" variant="dot">
|
||||
{genre}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
<img
|
||||
src="/netflix.png"
|
||||
alt="Netflix"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 12,
|
||||
bottom: 12,
|
||||
width: 30,
|
||||
height: 30,
|
||||
objectFit: 'contain',
|
||||
opacity: 0.95,
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{!loading && !error && items.length === 0 && (
|
||||
<Text c="dimmed" ta="center">
|
||||
Kayıt bulunamadı.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user