320 lines
16 KiB
PHP
320 lines
16 KiB
PHP
@extends('admin.layouts.app')
|
||
@section('title', 'Bölümler')
|
||
@section('page-title', 'Bölümler')
|
||
|
||
@push('styles')
|
||
<style>
|
||
.ep-anime { font-size:12.5px; font-weight:700; color:var(--text); }
|
||
.ep-code { font-size:12px; font-weight:700; color:var(--accent-lt); font-family:monospace; background:var(--accent-dim); padding:2px 7px; border-radius:5px; }
|
||
.ep-title { font-size:12.5px; color:var(--text2); }
|
||
|
||
.bulk-bar { display:none; align-items:center; gap:10px; padding:10px 16px; background:var(--accent-dim); border:1px solid rgba(124,58,237,0.3); border-radius:var(--radius-sm); margin-bottom:12px; flex-wrap:wrap; }
|
||
.bulk-bar.on { display:flex; }
|
||
|
||
.intro-card { display:none; background:var(--bg2); border:1px solid var(--border2); border-radius:var(--radius); padding:20px; margin-bottom:16px; }
|
||
.intro-card.on { display:block; }
|
||
</style>
|
||
@endpush
|
||
|
||
@section('content')
|
||
|
||
{{-- ── Page Header ── --}}
|
||
<div class="page-header">
|
||
<div>
|
||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">
|
||
Bölümler
|
||
<span style="font-size:13px;font-weight:600;color:var(--text2);margin-left:6px">{{ number_format($episodes->total()) }} toplam</span>
|
||
</h5>
|
||
</div>
|
||
<div class="page-header-actions">
|
||
<button class="btn btn-secondary btn-sm" onclick="document.getElementById('intro-card').classList.toggle('on')">
|
||
<i class="bi bi-skip-end-fill"></i>
|
||
<span class="d-none d-md-inline">Toplu İntro Ayarı</span>
|
||
</button>
|
||
<a href="{{ route('admin.episodes.create') }}" class="btn btn-primary btn-sm">
|
||
<i class="bi bi-plus-lg"></i> Bölüm Ekle
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── Toplu İntro Card ── --}}
|
||
<div class="intro-card" id="intro-card">
|
||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||
<div style="font-size:14px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px">
|
||
<i class="bi bi-skip-end-fill" style="color:var(--info)"></i> Toplu İntro Zaman Ayarı
|
||
</div>
|
||
<button class="btn btn-sm btn-secondary" onclick="document.getElementById('intro-card').classList.remove('on')">
|
||
<i class="bi bi-x-lg"></i>
|
||
</button>
|
||
</div>
|
||
<div class="row g-3 align-items-end">
|
||
<div class="col-md-3">
|
||
<label class="form-label">Anime</label>
|
||
<select id="bi-anime" class="form-select">
|
||
<option value="">— Anime seç —</option>
|
||
@foreach($animes as $a)
|
||
<option value="{{ $a->id }}">{{ $a->title }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div class="col-md-2">
|
||
<label class="form-label">Sezon <span style="color:var(--text3);font-size:11px">(0 = hepsi)</span></label>
|
||
<input type="number" id="bi-season" class="form-control" value="0" min="0">
|
||
</div>
|
||
<div class="col-md-2">
|
||
<label class="form-label">İntro Başlangıç (sn)</label>
|
||
<input type="number" id="bi-start" class="form-control" placeholder="örn: 5" min="0">
|
||
</div>
|
||
<div class="col-md-2">
|
||
<label class="form-label">İntro Bitiş (sn)</label>
|
||
<input type="number" id="bi-end" class="form-control" placeholder="örn: 95" min="0">
|
||
</div>
|
||
<div class="col-md-3 d-flex gap-2">
|
||
<button class="btn btn-info btn-sm flex-grow-1" onclick="doBulkIntro()">
|
||
<i class="bi bi-check-lg"></i> Uygula
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div id="bi-result" class="mt-3" style="font-size:13px"></div>
|
||
</div>
|
||
|
||
{{-- ── Filters ── --}}
|
||
<div class="filter-card mb-3">
|
||
<form method="GET" class="d-flex flex-wrap gap-2 align-items-end">
|
||
<div class="flex-grow-1" style="min-width:180px;max-width:280px">
|
||
<select name="anime_id" class="form-select">
|
||
<option value="">Tüm Animeler</option>
|
||
@foreach($animes as $anime)
|
||
<option value="{{ $anime->id }}" {{ request('anime_id') == $anime->id ? 'selected' : '' }}>{{ $anime->title }}</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
<div class="search-input-wrap flex-grow-1" style="min-width:150px;max-width:240px">
|
||
<i class="bi bi-search"></i>
|
||
<input type="text" name="search" class="form-control" placeholder="Başlık ara..." value="{{ request('search') }}">
|
||
</div>
|
||
<select name="status" class="form-select" style="width:auto;min-width:140px">
|
||
<option value="">Tüm Durumlar</option>
|
||
<option value="published" {{ request('status')=='published' ?'selected':'' }}>Yayında</option>
|
||
<option value="pending" {{ request('status')=='pending' ?'selected':'' }}>Bekliyor</option>
|
||
<option value="processing" {{ request('status')=='processing' ?'selected':'' }}>İşleniyor</option>
|
||
<option value="failed" {{ request('status')=='failed' ?'selected':'' }}>Hatalı</option>
|
||
</select>
|
||
<div class="d-flex gap-2">
|
||
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-funnel"></i> Filtrele</button>
|
||
<a href="{{ route('admin.episodes.index') }}" class="btn btn-secondary btn-sm"><i class="bi bi-x-lg"></i></a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{{-- ── Bulk Bar ── --}}
|
||
<div class="bulk-bar" id="bulk-bar">
|
||
<span style="font-size:13px;font-weight:700;color:var(--accent-lt)" id="bulk-count">0 bölüm seçildi</span>
|
||
<span class="badge bg-info d-none" id="bulk-all-note" style="color:#000">Tüm sayfalardaki bölümler</span>
|
||
<div style="margin-left:auto;display:flex;gap:8px;flex-wrap:wrap">
|
||
<button class="btn btn-sm btn-secondary d-none" id="btn-sel-page" onclick="selPage()"><i class="bi bi-check2-square"></i> Sayfayı Seç</button>
|
||
<button class="btn btn-sm btn-secondary d-none" id="btn-sel-all" onclick="selAllPages()"><i class="bi bi-check2-all"></i> Tümünü Seç ({{ $episodes->total() }})</button>
|
||
<button class="btn btn-sm btn-outline-danger" onclick="bulkDelete()"><i class="bi bi-trash3"></i> Seçilenleri Sil</button>
|
||
<button class="btn btn-sm btn-secondary" onclick="clearSel()"><i class="bi bi-x-lg"></i> İptal</button>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── Table ── --}}
|
||
<div class="table-wrap">
|
||
<div class="table-responsive">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:44px;padding-left:20px">
|
||
<input type="checkbox" class="form-check-input" id="chk-page" onchange="togglePage(this.checked)">
|
||
</th>
|
||
<th>Anime & Bölüm</th>
|
||
<th>Kaynak</th>
|
||
<th style="text-align:center">Durum</th>
|
||
<th style="text-align:center">İzlenme</th>
|
||
<th style="text-align:right;padding-right:20px">İşlem</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse($episodes as $ep)
|
||
<tr id="ep-row-{{ $ep->id }}">
|
||
<td style="padding-left:20px">
|
||
<input type="checkbox" class="form-check-input chk-item" value="{{ $ep->id }}" onchange="onCheck()">
|
||
</td>
|
||
<td>
|
||
<div class="ep-anime mb-1">{{ $ep->anime->title ?? '—' }}</div>
|
||
<div class="d-flex align-items-center gap-2">
|
||
<span class="ep-code">S{{ str_pad($ep->season->season_number ?? 0, 2, '0', STR_PAD_LEFT) }}E{{ str_pad($ep->episode_number, 2, '0', STR_PAD_LEFT) }}</span>
|
||
@if($ep->title)
|
||
<span class="ep-title">{{ Str::limit($ep->title, 45) }}</span>
|
||
@endif
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<span class="badge bg-secondary">{{ strtoupper($ep->source) }}</span>
|
||
</td>
|
||
<td style="text-align:center">
|
||
@php
|
||
$sBadge = match($ep->status) {
|
||
'published' => ['bg-success', 'Yayında', ''],
|
||
'processing' => ['bg-info', 'İşleniyor', 'color:#000'],
|
||
'failed' => ['bg-danger', 'Hatalı', ''],
|
||
default => ['bg-secondary','Bekliyor', ''],
|
||
};
|
||
@endphp
|
||
<span class="badge {{ $sBadge[0] }}" style="{{ $sBadge[2] }}">{{ $sBadge[1] }}</span>
|
||
</td>
|
||
<td style="text-align:center;font-size:13px;font-weight:600;color:var(--text2)">
|
||
{{ number_format($ep->view_count) }}
|
||
</td>
|
||
<td style="text-align:right;padding-right:20px">
|
||
<div class="d-flex gap-2 justify-content-end">
|
||
<a href="{{ route('admin.episodes.edit', $ep) }}" class="btn btn-sm btn-secondary" title="Düzenle">
|
||
<i class="bi bi-pencil"></i>
|
||
</a>
|
||
<form method="POST" action="{{ route('admin.episodes.destroy', $ep) }}" onsubmit="return confirm('Bu bölümü silmek istediğinize emin misiniz?')">
|
||
@csrf @method('DELETE')
|
||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Sil">
|
||
<i class="bi bi-trash3"></i>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@empty
|
||
<tr>
|
||
<td colspan="6">
|
||
<div class="empty-state">
|
||
<i class="bi bi-collection-play"></i>
|
||
<h6>Bölüm bulunamadı</h6>
|
||
<p>Filtrelerinizi değiştirin veya yeni bölüm ekleyin.</p>
|
||
<a href="{{ route('admin.episodes.create') }}" class="btn btn-primary btn-sm mt-2">
|
||
<i class="bi bi-plus-lg"></i> Bölüm Ekle
|
||
</a>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-3">
|
||
{{ $episodes->links('admin.partials.pagination') }}
|
||
</div>
|
||
|
||
@endsection
|
||
|
||
@push('scripts')
|
||
<script>
|
||
const BULK_DESTROY_URL = '{{ route("admin.episodes.bulk-destroy") }}';
|
||
const TOTAL_EPS = {{ $episodes->total() }};
|
||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content || '';
|
||
|
||
let _selAll = false;
|
||
|
||
function _selBtns(show) {
|
||
document.getElementById('btn-sel-page').classList.toggle('d-none', !show);
|
||
document.getElementById('btn-sel-all').classList.toggle('d-none', !show);
|
||
}
|
||
function selPage() {
|
||
document.querySelectorAll('.chk-item').forEach(c => c.checked = true);
|
||
document.getElementById('chk-page').checked = true;
|
||
_selAll = false;
|
||
document.getElementById('bulk-all-note').classList.add('d-none');
|
||
onCheck();
|
||
}
|
||
function selAllPages() {
|
||
selPage(); _selAll = true;
|
||
document.getElementById('bulk-all-note').classList.remove('d-none');
|
||
updateBar();
|
||
}
|
||
function clearSel() {
|
||
document.querySelectorAll('.chk-item').forEach(c => c.checked = false);
|
||
document.getElementById('chk-page').checked = false;
|
||
_selAll = false;
|
||
document.getElementById('bulk-all-note').classList.add('d-none');
|
||
updateBar();
|
||
}
|
||
function togglePage(checked) {
|
||
document.querySelectorAll('.chk-item').forEach(c => c.checked = checked);
|
||
_selAll = false;
|
||
document.getElementById('bulk-all-note').classList.add('d-none');
|
||
onCheck();
|
||
}
|
||
function onCheck() {
|
||
const all = document.querySelectorAll('.chk-item');
|
||
const chk = document.querySelectorAll('.chk-item:checked');
|
||
document.getElementById('chk-page').checked = all.length > 0 && all.length === chk.length;
|
||
_selAll = false;
|
||
document.getElementById('bulk-all-note').classList.add('d-none');
|
||
updateBar();
|
||
}
|
||
function updateBar() {
|
||
const chk = document.querySelectorAll('.chk-item:checked');
|
||
const bar = document.getElementById('bulk-bar');
|
||
const cnt = document.getElementById('bulk-count');
|
||
if (_selAll) {
|
||
cnt.textContent = `Tüm ${TOTAL_EPS} bölüm seçildi`;
|
||
bar.classList.add('on'); _selBtns(false);
|
||
} else if (chk.length > 0) {
|
||
cnt.textContent = `${chk.length} bölüm seçildi`;
|
||
bar.classList.add('on'); _selBtns(true);
|
||
} else {
|
||
bar.classList.remove('on'); _selBtns(false);
|
||
}
|
||
}
|
||
|
||
async function bulkDelete() {
|
||
const chk = [...document.querySelectorAll('.chk-item:checked')].map(c => c.value);
|
||
const label = _selAll ? `tüm ${TOTAL_EPS} bölümü` : `${chk.length} bölümü`;
|
||
if (!confirm(`${label} silmek istediğinize emin misiniz? CDN dosyaları da silinecek!`)) return;
|
||
const params = new URLSearchParams(location.search);
|
||
const body = _selAll
|
||
? { all:true, filters:{ anime_id:params.get('anime_id')||'', status:params.get('status')||'', search:params.get('search')||'' } }
|
||
: { ids:chk };
|
||
try {
|
||
const res = await fetch(BULK_DESTROY_URL, { method:'POST', headers:{'X-CSRF-TOKEN':CSRF,'Content-Type':'application/json','Accept':'application/json'}, body:JSON.stringify(body) });
|
||
const d = await res.json();
|
||
if (d.success) {
|
||
if (_selAll) { location.reload(); return; }
|
||
chk.forEach(id => document.getElementById(`ep-row-${id}`)?.remove());
|
||
clearSel();
|
||
axToast(`${chk.length} bölüm silindi.`, 'success');
|
||
} else { axToast('Hata: ' + (d.message || '?'), 'error'); }
|
||
} catch(e) { axToast('Hata: ' + e.message, 'error'); }
|
||
}
|
||
|
||
async function doBulkIntro() {
|
||
const animeId = document.getElementById('bi-anime').value;
|
||
const season = document.getElementById('bi-season').value;
|
||
const start = document.getElementById('bi-start').value;
|
||
const end = document.getElementById('bi-end').value;
|
||
const result = document.getElementById('bi-result');
|
||
|
||
if (!animeId) { result.innerHTML = '<span style="color:var(--warning)">Anime seçiniz.</span>'; return; }
|
||
if (!start || !end) { result.innerHTML = '<span style="color:var(--warning)">Başlangıç ve bitiş saniyelerini girin.</span>'; return; }
|
||
if (parseInt(end) <= parseInt(start)) { result.innerHTML = '<span style="color:var(--warning)">Bitiş saniyesi başlangıçtan büyük olmalı.</span>'; return; }
|
||
|
||
result.innerHTML = '<span style="color:var(--text2)"><span class="spinner-border spinner-border-sm me-1" style="width:12px;height:12px"></span> Uygulanıyor...</span>';
|
||
try {
|
||
const res = await fetch('{{ route("admin.episodes.bulk-intro") }}', {
|
||
method: 'POST',
|
||
headers: { 'X-CSRF-TOKEN': CSRF, 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
||
body: JSON.stringify({ anime_id: animeId, season: season, intro_start: start, intro_end: end }),
|
||
});
|
||
const d = await res.json();
|
||
if (d.ok) {
|
||
result.innerHTML = `<span style="color:var(--success)"><i class="bi bi-check-circle-fill me-1"></i>${d.updated} bölüm güncellendi.</span>`;
|
||
axToast(`${d.updated} bölüm intro zamanları güncellendi.`, 'success');
|
||
} else {
|
||
result.innerHTML = `<span style="color:var(--danger)"><i class="bi bi-x-circle me-1"></i>${d.error}</span>`;
|
||
}
|
||
} catch(e) {
|
||
result.innerHTML = '<span style="color:var(--danger)">Bağlantı hatası.</span>';
|
||
}
|
||
}
|
||
</script>
|
||
@endpush
|