Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
@@ -0,0 +1,319 @@
@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