Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Aktivasyon Kodları')
|
||||
@section('page-title', 'Aktivasyon Kodları')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- İstatistik kartları --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-4">
|
||||
<div class="card h-100 border-0" style="background:rgba(0,245,255,.06);border:1px solid rgba(0,245,255,.18)!important;border-radius:14px">
|
||||
<div class="card-body text-center py-3">
|
||||
<div style="font-size:2rem;font-weight:900;color:#00f5ff">{{ number_format($stats['total']) }}</div>
|
||||
<div style="font-size:.75rem;color:rgba(255,255,255,.4);text-transform:uppercase;letter-spacing:.08em">Toplam Kod</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card h-100 border-0" style="background:rgba(50,220,100,.06);border:1px solid rgba(50,220,100,.18)!important;border-radius:14px">
|
||||
<div class="card-body text-center py-3">
|
||||
<div style="font-size:2rem;font-weight:900;color:#32dc64">{{ number_format($stats['used']) }}</div>
|
||||
<div style="font-size:.75rem;color:rgba(255,255,255,.4);text-transform:uppercase;letter-spacing:.08em">Kullanılmış</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card h-100 border-0" style="background:rgba(255,214,10,.06);border:1px solid rgba(255,214,10,.18)!important;border-radius:14px">
|
||||
<div class="card-body text-center py-3">
|
||||
<div style="font-size:2rem;font-weight:900;color:#ffd60a">{{ number_format($stats['unused']) }}</div>
|
||||
<div style="font-size:.75rem;color:rgba(255,255,255,.4);text-transform:uppercase;letter-spacing:.08em">Kullanılmamış</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Kod Oluşturma Formu --}}
|
||||
<div class="card mb-4" style="border-radius:16px;border:1px solid rgba(0,245,255,.15)">
|
||||
<div class="card-body p-4">
|
||||
<h6 style="font-weight:800;font-size:.95rem;margin-bottom:20px;display:flex;align-items:center;gap:8px">
|
||||
<i class="bi bi-plus-circle-fill" style="color:#00f5ff"></i> Yeni Aktivasyon Kodu Oluştur
|
||||
</h6>
|
||||
<form method="POST" action="{{ route('admin.activation-codes.generate') }}" id="gen-form">
|
||||
@csrf
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-3">
|
||||
<label class="form-label" style="font-size:.78rem;font-weight:700">Plan <span class="text-danger">*</span></label>
|
||||
<select name="plan_id" class="form-select" required>
|
||||
<option value="">— Plan Seç —</option>
|
||||
@foreach($plans as $plan)
|
||||
<option value="{{ $plan->id }}" {{ old('plan_id') == $plan->id ? 'selected' : '' }}>
|
||||
{{ $plan->name }} ({{ $plan->duration_days }} gün)
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<label class="form-label" style="font-size:.78rem;font-weight:700">Adet <span class="text-danger">*</span></label>
|
||||
<input type="number" name="quantity" class="form-control" min="1" max="500" value="{{ old('quantity', 1) }}" required>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<label class="form-label" style="font-size:.78rem;font-weight:700">Son Kullanma</label>
|
||||
<input type="date" name="expires_at" class="form-control" value="{{ old('expires_at') }}">
|
||||
<div class="form-text">Boş = süresiz</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label class="form-label" style="font-size:.78rem;font-weight:700">Batch Adı</label>
|
||||
<input type="text" name="batch" class="form-control" placeholder="örn: haziran-2026" value="{{ old('batch') }}" maxlength="64">
|
||||
<div class="form-text">Grubu tanımlamak için</div>
|
||||
</div>
|
||||
<div class="col-sm-2 d-flex align-items-end">
|
||||
<button type="submit" class="btn w-100" style="background:linear-gradient(135deg,#00f5ff,#b84dff);color:#06060d;font-weight:800;border:none;border-radius:10px;padding:10px">
|
||||
<i class="bi bi-lightning-fill me-1"></i> Oluştur
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" style="font-size:.78rem;font-weight:700">Not (isteğe bağlı)</label>
|
||||
<input type="text" name="notes" class="form-control" placeholder="İç not..." value="{{ old('notes') }}" maxlength="500">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Oluşturulan kodlar (anlık göster) --}}
|
||||
@if(session('generated_codes'))
|
||||
<div class="card mb-4" style="border-radius:16px;border:1px solid rgba(50,220,100,.25);background:rgba(50,220,100,.04)">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h6 style="font-weight:800;font-size:.95rem;margin:0;color:#32dc64">
|
||||
<i class="bi bi-check-circle-fill me-2"></i>{{ count(session('generated_codes')) }} Kod Oluşturuldu
|
||||
</h6>
|
||||
<div class="d-flex gap-2">
|
||||
<button onclick="copyAllCodes(this)" class="btn btn-sm" style="border:1px solid rgba(0,245,255,.3);color:#00f5ff;background:rgba(0,245,255,.06);border-radius:8px;font-size:.78rem;font-weight:700">
|
||||
<i class="bi bi-clipboard me-1"></i> Tümünü Kopyala
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="generated-codes-list" style="display:flex;flex-wrap:wrap;gap:8px">
|
||||
@foreach(session('generated_codes') as $gc)
|
||||
<span class="generated-code" style="font-family:monospace;font-size:.88rem;font-weight:700;color:#e0f7ff;padding:6px 14px;background:rgba(0,245,255,.08);border:1px solid rgba(0,245,255,.2);border-radius:8px;cursor:pointer;letter-spacing:.08em;transition:all .15s"
|
||||
onclick="navigator.clipboard.writeText('{{ $gc }}').then(()=>{ this.style.borderColor='#32dc64';this.style.color='#32dc64'; setTimeout(()=>{ this.style.borderColor='rgba(0,245,255,.2)';this.style.color='#e0f7ff'; },1500); })"
|
||||
title="Kopyalamak için tıkla">{{ $gc }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success d-flex align-items-center gap-2" style="border-radius:12px;font-size:.85rem">
|
||||
<i class="bi bi-check-circle-fill"></i> {{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Filtreler --}}
|
||||
<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:160px;max-width:220px">
|
||||
<label class="form-label mb-1" style="font-size:.75rem;font-weight:700">Plan</label>
|
||||
<select name="plan_id" class="form-select form-select-sm">
|
||||
<option value="">Tüm Planlar</option>
|
||||
@foreach($plans as $plan)
|
||||
<option value="{{ $plan->id }}" {{ request('plan_id') == $plan->id ? 'selected' : '' }}>{{ $plan->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div style="min-width:140px">
|
||||
<label class="form-label mb-1" style="font-size:.75rem;font-weight:700">Durum</label>
|
||||
<select name="status" class="form-select form-select-sm">
|
||||
<option value="">Tümü</option>
|
||||
<option value="unused" {{ request('status') === 'unused' ? 'selected' : '' }}>Kullanılmamış</option>
|
||||
<option value="used" {{ request('status') === 'used' ? 'selected' : '' }}>Kullanılmış</option>
|
||||
</select>
|
||||
</div>
|
||||
@if($batches->isNotEmpty())
|
||||
<div style="min-width:160px">
|
||||
<label class="form-label mb-1" style="font-size:.75rem;font-weight:700">Batch</label>
|
||||
<select name="batch" class="form-select form-select-sm">
|
||||
<option value="">Tüm Batch'ler</option>
|
||||
@foreach($batches as $b)
|
||||
<option value="{{ $b }}" {{ request('batch') === $b ? 'selected' : '' }}>{{ $b }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-sm btn-primary" style="border-radius:8px;font-weight:700">Filtrele</button>
|
||||
<a href="{{ route('admin.activation-codes.index') }}" class="btn btn-sm btn-outline-secondary" style="border-radius:8px">Temizle</a>
|
||||
<a href="{{ route('admin.activation-codes.export') }}?{{ http_build_query(request()->only('plan_id','batch')) }}"
|
||||
class="btn btn-sm" style="border:1px solid rgba(0,245,255,.3);color:#00f5ff;background:rgba(0,245,255,.06);border-radius:8px;font-weight:700">
|
||||
<i class="bi bi-download me-1"></i>CSV
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Batch toplu silme --}}
|
||||
@if(request('batch'))
|
||||
<div class="d-flex justify-content-end mb-2">
|
||||
<form method="POST" action="{{ route('admin.activation-codes.destroy-batch') }}" onsubmit="return confirm('Bu batch\'teki kullanılmamış kodların tümü silinecek. Emin misiniz?')">
|
||||
@csrf
|
||||
<input type="hidden" name="batch" value="{{ request('batch') }}">
|
||||
<button type="submit" class="btn btn-sm btn-danger" style="border-radius:8px;font-size:.78rem;font-weight:700">
|
||||
<i class="bi bi-trash me-1"></i> "{{ request('batch') }}" Batch — Kullanılmamışları Sil
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Toplu seçim aksiyonu --}}
|
||||
<div id="bulk-bar" style="display:none;align-items:center;gap:12px;padding:10px 16px;margin-bottom:12px;background:rgba(255,80,80,.07);border:1px solid rgba(255,80,80,.25);border-radius:12px">
|
||||
<span style="font-size:.85rem;font-weight:700;color:#ff8080">
|
||||
<i class="bi bi-check2-square me-1"></i>
|
||||
<span id="sel-count">0</span> kod seçildi
|
||||
</span>
|
||||
<button onclick="bulkDelete()" class="btn btn-sm btn-danger" style="border-radius:8px;font-size:.78rem;font-weight:700;padding:5px 14px">
|
||||
<i class="bi bi-trash3 me-1"></i> Seçilenleri Sil
|
||||
</button>
|
||||
<button onclick="clearSelection()" class="btn btn-sm btn-outline-secondary" style="border-radius:8px;font-size:.78rem">
|
||||
İptal
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Gizli bulk delete formu --}}
|
||||
<form id="bulk-del-form" method="POST" action="{{ route('admin.activation-codes.destroy-selected') }}" style="display:none">
|
||||
@csrf
|
||||
</form>
|
||||
|
||||
{{-- Tablo --}}
|
||||
<div class="card" style="border-radius:16px;overflow:hidden">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" style="font-size:.84rem">
|
||||
<thead style="border-bottom:1px solid rgba(255,255,255,.08)">
|
||||
<tr>
|
||||
<th style="padding:12px 16px;width:44px">
|
||||
<input type="checkbox" id="select-all" class="form-check-input" style="width:16px;height:16px;cursor:pointer;background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.3)">
|
||||
</th>
|
||||
<th style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;padding:12px 16px">Kod</th>
|
||||
<th style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em">Plan</th>
|
||||
<th style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em">Batch</th>
|
||||
<th style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em">Durum</th>
|
||||
<th style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em">Kullanan</th>
|
||||
<th style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em">Son Kullanma</th>
|
||||
<th style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em">Oluşturulma</th>
|
||||
<th style="width:60px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($codes as $code)
|
||||
<tr>
|
||||
<td style="padding:11px 16px">
|
||||
@if(!$code->isUsed())
|
||||
<input type="checkbox" class="row-check form-check-input" value="{{ $code->id }}"
|
||||
style="width:16px;height:16px;cursor:pointer;background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.3)">
|
||||
@endif
|
||||
</td>
|
||||
<td style="padding:11px 16px">
|
||||
<code style="font-size:.88rem;font-weight:700;letter-spacing:.1em;color:#00f5ff;cursor:pointer"
|
||||
onclick="navigator.clipboard.writeText('{{ $code->code }}').then(()=>showToast?.('Kopyalandı'))"
|
||||
title="Kopyalamak için tıkla">{{ $code->code }}</code>
|
||||
</td>
|
||||
<td>
|
||||
<span style="font-size:.78rem;font-weight:700;padding:3px 10px;border-radius:8px;background:rgba(0,245,255,.08);color:#00f5ff">
|
||||
{{ $code->plan->name ?? '—' }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="color:rgba(255,255,255,.4);font-size:.78rem">{{ $code->batch ?? '—' }}</td>
|
||||
<td>
|
||||
@if($code->isUsed())
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;font-size:.75rem;font-weight:700;padding:3px 10px;border-radius:8px;background:rgba(50,220,100,.1);color:#32dc64;border:1px solid rgba(50,220,100,.2)">
|
||||
<i class="bi bi-check-circle-fill"></i> Kullanıldı
|
||||
</span>
|
||||
@elseif($code->isExpired())
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;font-size:.75rem;font-weight:700;padding:3px 10px;border-radius:8px;background:rgba(255,80,80,.08);color:#ff5050;border:1px solid rgba(255,80,80,.2)">
|
||||
<i class="bi bi-clock-history"></i> Süresi Doldu
|
||||
</span>
|
||||
@else
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;font-size:.75rem;font-weight:700;padding:3px 10px;border-radius:8px;background:rgba(255,214,10,.08);color:#ffd60a;border:1px solid rgba(255,214,10,.2)">
|
||||
<i class="bi bi-hourglass-split"></i> Bekliyor
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($code->usedBy)
|
||||
<span style="font-size:.8rem;color:rgba(255,255,255,.65)">{{ $code->usedBy->name }}</span>
|
||||
<div style="font-size:.7rem;color:rgba(255,255,255,.3)">{{ $code->used_at?->format('d.m.Y H:i') }}</div>
|
||||
@else
|
||||
<span style="color:rgba(255,255,255,.2);font-size:.78rem">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="font-size:.78rem;color:rgba(255,255,255,.4)">
|
||||
{{ $code->expires_at?->format('d.m.Y') ?? '∞ Süresiz' }}
|
||||
</td>
|
||||
<td style="font-size:.78rem;color:rgba(255,255,255,.35)">
|
||||
{{ $code->created_at->format('d.m.Y') }}
|
||||
</td>
|
||||
<td>
|
||||
@if(!$code->isUsed())
|
||||
<form method="POST" action="{{ route('admin.activation-codes.destroy', $code) }}"
|
||||
onsubmit="return confirm('Bu kodu silmek istediğine emin misin?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-sm" style="color:rgba(255,80,80,.6);background:none;border:none;padding:6px 8px;border-radius:7px;transition:all .15s"
|
||||
onmouseover="this.style.background='rgba(255,80,80,.1)';this.style.color='#ff5050'"
|
||||
onmouseout="this.style.background='none';this.style.color='rgba(255,80,80,.6)'"
|
||||
title="Sil">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-5" style="color:rgba(255,255,255,.3)">
|
||||
<i class="bi bi-key" style="font-size:2rem;display:block;margin-bottom:10px;opacity:.3"></i>
|
||||
Henüz aktivasyon kodu yok. Yukarıdan oluşturabilirsin.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Pagination --}}
|
||||
@if($codes->hasPages())
|
||||
<div class="mt-3">{{ $codes->links() }}</div>
|
||||
@endif
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const selectAll = document.getElementById('select-all');
|
||||
const bulkBar = document.getElementById('bulk-bar');
|
||||
const selCount = document.getElementById('sel-count');
|
||||
|
||||
function getChecked() {
|
||||
return [...document.querySelectorAll('.row-check:checked')];
|
||||
}
|
||||
|
||||
function updateBulkBar() {
|
||||
const n = getChecked().length;
|
||||
bulkBar.style.display = n > 0 ? 'flex' : 'none';
|
||||
selCount.textContent = n;
|
||||
const all = document.querySelectorAll('.row-check');
|
||||
selectAll.indeterminate = n > 0 && n < all.length;
|
||||
selectAll.checked = all.length > 0 && n === all.length;
|
||||
}
|
||||
|
||||
selectAll.addEventListener('change', function () {
|
||||
document.querySelectorAll('.row-check').forEach(cb => cb.checked = this.checked);
|
||||
updateBulkBar();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.row-check').forEach(cb => {
|
||||
cb.addEventListener('change', updateBulkBar);
|
||||
});
|
||||
|
||||
window.clearSelection = function () {
|
||||
document.querySelectorAll('.row-check').forEach(cb => cb.checked = false);
|
||||
selectAll.checked = false;
|
||||
updateBulkBar();
|
||||
};
|
||||
|
||||
window.bulkDelete = function () {
|
||||
const checked = getChecked();
|
||||
if (!checked.length) return;
|
||||
if (!confirm(checked.length + ' adet kodu silmek istediğine emin misin? Bu işlem geri alınamaz.')) return;
|
||||
|
||||
const form = document.getElementById('bulk-del-form');
|
||||
form.querySelectorAll('input[name="ids[]"]').forEach(el => el.remove());
|
||||
checked.forEach(cb => {
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'hidden';
|
||||
inp.name = 'ids[]';
|
||||
inp.value = cb.value;
|
||||
form.appendChild(inp);
|
||||
});
|
||||
form.submit();
|
||||
};
|
||||
})();
|
||||
|
||||
function copyAllCodes(btn) {
|
||||
const codes = [...document.querySelectorAll('.generated-code')].map(el => el.textContent.trim());
|
||||
if (!codes.length) return;
|
||||
navigator.clipboard.writeText(codes.join('\n')).then(() => {
|
||||
btn.innerHTML = '<i class="bi bi-check-lg me-1"></i> Kopyalandı!';
|
||||
btn.style.color = '#32dc64';
|
||||
btn.style.borderColor = 'rgba(50,220,100,.4)';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = '<i class="bi bi-clipboard me-1"></i> Tümünü Kopyala';
|
||||
btn.style.color = '#00f5ff';
|
||||
btn.style.borderColor = 'rgba(0,245,255,.3)';
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,117 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Reklam Düzenle')
|
||||
@section('page-title', 'Reklam Düzenle')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex align-items-center gap-3 mb-4">
|
||||
<a href="{{ route('admin.ads.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Reklamlara Dön
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-6 col-lg-8">
|
||||
|
||||
<form method="POST" action="{{ route('admin.ads.update', $ad) }}" enctype="multipart/form-data">
|
||||
@csrf @method('PUT')
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-badge-ad-fill" style="color:#ffd60a"></i>
|
||||
<h6>{{ $ad->name }}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger py-2" style="font-size:13px">
|
||||
@foreach($errors->all() as $err)<div>{{ $err }}</div>@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Önizleme --}}
|
||||
@if($ad->media_url)
|
||||
<div class="mb-3 p-3" style="background:var(--bg);border-radius:8px;text-align:center">
|
||||
@if($ad->type === 'video')
|
||||
<video src="{{ $ad->media_url }}" controls style="max-width:100%;max-height:220px;border-radius:6px"></video>
|
||||
@else
|
||||
<img src="{{ $ad->media_url }}" alt="" style="max-width:100%;max-height:160px;border-radius:6px">
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="row g-2 mb-1" style="font-size:13px;color:var(--text2)">
|
||||
<div class="col-4">Gösterim: <b style="color:var(--text)">{{ number_format($ad->impressions) }}</b></div>
|
||||
<div class="col-4">Tıklama: <b style="color:var(--text)">{{ number_format($ad->clicks) }}</b></div>
|
||||
<div class="col-4">CTR: <b style="color:var(--text)">%{{ $ad->ctr }}</b></div>
|
||||
</div>
|
||||
<hr style="border-color:var(--border)">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Reklam Adı</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ old('name', $ad->name) }}" required>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Tür</label>
|
||||
<select name="type" class="form-select">
|
||||
<option value="video" {{ $ad->type === 'video' ? 'selected' : '' }}>Video (pre-roll)</option>
|
||||
<option value="banner" {{ $ad->type === 'banner' ? 'selected' : '' }}>Banner (görsel)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Konum</label>
|
||||
<select name="placement" class="form-select">
|
||||
<option value="preroll" {{ $ad->placement === 'preroll' ? 'selected' : '' }}>Video başı</option>
|
||||
<option value="home_mid" {{ $ad->placement === 'home_mid' ? 'selected' : '' }}>Ana sayfa — orta</option>
|
||||
<option value="home_bottom" {{ $ad->placement === 'home_bottom' ? 'selected' : '' }}>Ana sayfa — alt</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Yeni Dosya Yükle <span style="color:var(--text2);font-weight:400">(mevcut dosyanın yerine geçer)</span></label>
|
||||
<input type="file" name="media_file" class="form-control" accept="{{ $ad->type === 'video' ? 'video/mp4' : 'image/*' }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Dış URL</label>
|
||||
<input type="url" name="external_url" class="form-control font-monospace" value="{{ old('external_url', $ad->external_url) }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tıklama URL'si</label>
|
||||
<input type="url" name="click_url" class="form-control" value="{{ old('click_url', $ad->click_url) }}">
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Geçme süresi (sn)</label>
|
||||
<input type="number" name="skip_after" class="form-control" min="0" max="60" value="{{ old('skip_after', $ad->skip_after) }}">
|
||||
<div class="form-text">0 = geçilemez</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Ağırlık</label>
|
||||
<input type="number" name="weight" class="form-control" min="1" max="100" value="{{ old('weight', $ad->weight) }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Başlangıç</label>
|
||||
<input type="datetime-local" name="starts_at" class="form-control"
|
||||
value="{{ old('starts_at', $ad->starts_at?->format('Y-m-d\TH:i')) }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Bitiş</label>
|
||||
<input type="datetime-local" name="ends_at" class="form-control"
|
||||
value="{{ old('ends_at', $ad->ends_at?->format('Y-m-d\TH:i')) }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" value="1" id="edit_ad_active" role="switch"
|
||||
{{ $ad->is_active ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="edit_ad_active">Aktif</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-floppy"></i> Değişiklikleri Kaydet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,291 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Reklamlar')
|
||||
@section('page-title', 'Reklam Yönetimi')
|
||||
|
||||
@section('content')
|
||||
|
||||
@if(session('success'))
|
||||
<div class="alert alert-success py-2 mb-3" style="font-size:14px">
|
||||
<i class="bi bi-check-circle-fill"></i> {{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger mb-3" style="font-size:14px">
|
||||
<div style="font-weight:700;margin-bottom:6px"><i class="bi bi-exclamation-triangle-fill"></i> Reklam eklenemedi:</div>
|
||||
@foreach($errors->all() as $err)<div>• {{ $err }}</div>@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Özet kartlar --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<div style="font-size:12px;color:var(--text2)">Toplam Gösterim</div>
|
||||
<div style="font-size:24px;font-weight:800;color:var(--text)">{{ number_format($stats['total_impressions']) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<div style="font-size:12px;color:var(--text2)">Toplam Tıklama</div>
|
||||
<div style="font-size:24px;font-weight:800;color:var(--text)">{{ number_format($stats['total_clicks']) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<div style="font-size:12px;color:var(--text2)">Ortalama CTR</div>
|
||||
<div style="font-size:24px;font-weight:800;color:var(--text)">%{{ $stats['avg_ctr'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-lg-3">
|
||||
<div class="card p-3">
|
||||
<div style="font-size:12px;color:var(--text2)">Aktif Reklam</div>
|
||||
<div style="font-size:24px;font-weight:800;color:var(--success)">{{ $stats['active_count'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
{{-- Sol: Ayarlar --}}
|
||||
<div class="col-lg-4">
|
||||
<form method="POST" action="{{ route('admin.ads.settings') }}">
|
||||
@csrf
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-sliders" style="color:var(--accent-lt)"></i>
|
||||
<h6>Video Reklam Ayarları</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="vad_enabled" value="1" id="vad_enabled" role="switch"
|
||||
{{ $settings['vad_enabled'] == '1' ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="vad_enabled">Video Reklamları Etkinleştir</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Kaç bölümde bir göster?</label>
|
||||
<div class="input-group">
|
||||
<input type="number" name="vad_freq_episodes" class="form-control" min="1" max="50"
|
||||
value="{{ $settings['vad_freq_episodes'] }}">
|
||||
<span class="input-group-text">bölüm</span>
|
||||
</div>
|
||||
<div class="form-text">2 = her 2 bölümde 1 reklam</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Minimum reklam aralığı</label>
|
||||
<div class="input-group">
|
||||
<input type="number" name="vad_freq_minutes" class="form-control" min="1" max="120"
|
||||
value="{{ $settings['vad_freq_minutes'] }}">
|
||||
<span class="input-group-text">dakika</span>
|
||||
</div>
|
||||
<div class="form-text">5 = 5 dakika içinde en fazla 1 reklam</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Premium teklif oranı</label>
|
||||
<div class="input-group">
|
||||
<input type="number" name="vad_upsell_percent" class="form-control" min="0" max="100"
|
||||
value="{{ $settings['vad_upsell_percent'] }}">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
<div class="form-text">Reklam yerine "Premium'a geç" ekranı gösterme olasılığı. 20 = her 5 reklam slotundan 1'i premium teklifi.</div>
|
||||
</div>
|
||||
<hr style="border-color:var(--border)">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" name="banner_ads_enabled" value="1" id="banner_ads_enabled" role="switch"
|
||||
{{ $settings['banner_ads_enabled'] == '1' ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="banner_ads_enabled">Ana Sayfa Banner Reklamları</label>
|
||||
</div>
|
||||
<div class="form-text mb-3">Premium üyeler hiçbir reklamı görmez.</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-floppy"></i> Ayarları Kaydet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{-- Yeni Reklam --}}
|
||||
<form method="POST" action="{{ route('admin.ads.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-plus-circle" style="color:var(--success)"></i>
|
||||
<h6>Yeni Reklam Ekle</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger py-2" style="font-size:13px">
|
||||
@foreach($errors->all() as $err)<div>{{ $err }}</div>@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Reklam Adı</label>
|
||||
<input type="text" name="name" class="form-control" value="{{ old('name') }}"
|
||||
placeholder="Örn: Sponsor X — Temmuz kampanya" required>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Tür</label>
|
||||
<select name="type" class="form-select" id="ad-type-select" onchange="adTypeChanged()">
|
||||
<option value="video">Video (pre-roll)</option>
|
||||
<option value="banner">Banner (görsel)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Konum</label>
|
||||
<select name="placement" class="form-select" id="ad-placement-select">
|
||||
<option value="preroll">Video başı</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Dosya Yükle <span id="ad-file-hint" style="color:var(--text2);font-weight:400">(MP4, maks 50MB)</span></label>
|
||||
<input type="file" name="media_file" class="form-control" id="ad-file-input" accept="video/mp4">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">veya Dış URL</label>
|
||||
<input type="url" name="external_url" class="form-control font-monospace" value="{{ old('external_url') }}"
|
||||
placeholder="https://cdn.example.com/reklam.mp4">
|
||||
<div class="form-text">Dosya yüklemek yerine hazır bir URL de verebilirsin.</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tıklama URL'si</label>
|
||||
<input type="url" name="click_url" class="form-control" value="{{ old('click_url') }}"
|
||||
placeholder="https://sponsor-sitesi.com">
|
||||
<div class="form-text">Reklama tıklanınca açılacak sayfa (boş bırakılabilir).</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6" id="ad-skip-wrap">
|
||||
<label class="form-label">Geçme süresi (sn)</label>
|
||||
<input type="number" name="skip_after" class="form-control" min="0" max="60" value="{{ old('skip_after', 5) }}">
|
||||
<div class="form-text">0 = geçilemez</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Ağırlık</label>
|
||||
<input type="number" name="weight" class="form-control" min="1" max="100" value="{{ old('weight', 10) }}">
|
||||
<div class="form-text">Yüksek = daha sık</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Başlangıç</label>
|
||||
<input type="datetime-local" name="starts_at" class="form-control" value="{{ old('starts_at') }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Bitiş</label>
|
||||
<input type="datetime-local" name="ends_at" class="form-control" value="{{ old('ends_at') }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" value="1" id="new_ad_active" role="switch" checked>
|
||||
<label class="form-check-label" for="new_ad_active">Hemen yayınla</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100">
|
||||
<i class="bi bi-plus-lg"></i> Reklamı Ekle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Sağ: Reklam listesi --}}
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-badge-ad-fill" style="color:#ffd60a"></i>
|
||||
<h6>Reklamlar ({{ $ads->count() }})</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($ads->isEmpty())
|
||||
<div class="text-center py-5" style="color:var(--text2)">
|
||||
<i class="bi bi-badge-ad" style="font-size:32px"></i>
|
||||
<div class="mt-2">Henüz reklam eklenmedi.</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark-custom mb-0" style="font-size:13px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Reklam</th>
|
||||
<th>Tür / Konum</th>
|
||||
<th class="text-center">Gösterim</th>
|
||||
<th class="text-center">Tıklama</th>
|
||||
<th class="text-center">CTR</th>
|
||||
<th class="text-center">Durum</th>
|
||||
<th class="text-end">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($ads as $ad)
|
||||
<tr>
|
||||
<td>
|
||||
<div style="font-weight:600;color:var(--text)">{{ $ad->name }}</div>
|
||||
<div style="font-size:11px;color:var(--text2)">
|
||||
Ağırlık: {{ $ad->weight }}
|
||||
@if($ad->type === 'video') · Geç: {{ $ad->skip_after > 0 ? $ad->skip_after.'sn' : 'kapalı' }} @endif
|
||||
@if($ad->starts_at || $ad->ends_at)
|
||||
· 📅 {{ $ad->starts_at?->format('d.m') ?? '∞' }}—{{ $ad->ends_at?->format('d.m') ?? '∞' }}
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {{ $ad->type === 'video' ? 'bg-primary' : 'bg-info' }}">{{ $ad->type === 'video' ? 'Video' : 'Banner' }}</span>
|
||||
<div style="font-size:11px;color:var(--text2);margin-top:2px">
|
||||
{{ ['preroll' => 'Video başı', 'home_mid' => 'Ana sayfa orta', 'home_bottom' => 'Ana sayfa alt'][$ad->placement] ?? $ad->placement }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">{{ number_format($ad->impressions) }}</td>
|
||||
<td class="text-center">{{ number_format($ad->clicks) }}</td>
|
||||
<td class="text-center">%{{ $ad->ctr }}</td>
|
||||
<td class="text-center">
|
||||
<form method="POST" action="{{ route('admin.ads.toggle', $ad) }}" style="display:inline">
|
||||
@csrf
|
||||
<button type="submit" class="badge border-0 {{ $ad->is_active ? 'bg-success' : 'bg-secondary' }}" style="cursor:pointer">
|
||||
{{ $ad->is_active ? 'Aktif' : 'Pasif' }}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="{{ route('admin.ads.edit', $ad) }}" class="btn btn-sm btn-outline-secondary py-0 px-2">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ route('admin.ads.destroy', $ad) }}" style="display:inline"
|
||||
onsubmit="return confirm('Bu reklamı silmek istediğine emin misin?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger py-0 px-2">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function adTypeChanged() {
|
||||
const type = document.getElementById('ad-type-select').value;
|
||||
const placement = document.getElementById('ad-placement-select');
|
||||
const fileInput = document.getElementById('ad-file-input');
|
||||
const fileHint = document.getElementById('ad-file-hint');
|
||||
const skipWrap = document.getElementById('ad-skip-wrap');
|
||||
|
||||
if (type === 'video') {
|
||||
placement.innerHTML = '<option value="preroll">Video başı</option>';
|
||||
fileInput.accept = 'video/mp4';
|
||||
fileHint.textContent = '(MP4, maks 50MB)';
|
||||
skipWrap.style.display = '';
|
||||
} else {
|
||||
placement.innerHTML = '<option value="home_mid">Ana sayfa — orta</option><option value="home_bottom">Ana sayfa — alt</option>';
|
||||
fileInput.accept = 'image/*';
|
||||
fileHint.textContent = '(JPG/PNG/WebP, maks 4MB — önerilen 970×120 veya 728×90)';
|
||||
skipWrap.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,207 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'AI — Toplu Anime Meta Doldurma')
|
||||
@section('page-title', 'AI — Toplu Anime Meta Doldurma')
|
||||
|
||||
@section('content')
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-9">
|
||||
|
||||
{{-- İstatistikler --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-sm-4">
|
||||
<div class="card text-center">
|
||||
<div class="fs-3 fw-800 text-info">{{ $total }}</div>
|
||||
<div class="text-muted small">Toplam anime</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card text-center">
|
||||
<div class="fs-3 fw-800 text-warning">{{ $missing }}</div>
|
||||
<div class="text-muted small">Eksik meta alanı olan</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
<div class="card text-center">
|
||||
<div class="fs-3 fw-800 text-danger">{{ $noGenres }}</div>
|
||||
<div class="text-muted small">Kategori ataması yok</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<h6 class="mb-1">Toplu Anime Meta Doldurma</h6>
|
||||
<p class="text-muted small mb-3">
|
||||
DeepSeek AI, eksik alanı olan animeleri tek tek işler. Açıklama, yıl, stüdyo, tür, durum, puan ve kategorileri doldurur.
|
||||
Dolu alanların üstüne yazmaz. Anime başına ~3-5 sn sürer.
|
||||
</p>
|
||||
|
||||
<div class="d-flex align-items-center gap-3 flex-wrap mb-3">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="force-mode">
|
||||
<label class="form-check-label small" for="force-mode">Dolu alanları da yenile (force)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mb-4">
|
||||
<button id="start-btn" class="btn btn-info px-4" onclick="startFill()">
|
||||
<i class="bi bi-stars"></i> Başlat
|
||||
</button>
|
||||
<button id="stop-btn" class="btn btn-outline-danger px-4" onclick="stopFill()" style="display:none">
|
||||
<i class="bi bi-stop-fill"></i> Durdur
|
||||
</button>
|
||||
<button id="reset-btn" class="btn btn-outline-secondary px-4" onclick="resetFill()" style="display:none">
|
||||
<i class="bi bi-arrow-counterclockwise"></i> Sıfırla
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- İlerleme --}}
|
||||
<div id="progress-wrap" style="display:none">
|
||||
<div class="d-flex justify-content-between small text-muted mb-1">
|
||||
<span id="prog-label">Hazırlanıyor…</span>
|
||||
<span id="prog-count">0 / 0</span>
|
||||
</div>
|
||||
<div class="progress mb-3" style="height:10px;border-radius:6px">
|
||||
<div id="prog-bar" class="progress-bar bg-info progress-bar-striped progress-bar-animated" style="width:0%"></div>
|
||||
</div>
|
||||
<div class="d-flex gap-3 small text-muted mb-3">
|
||||
<span>✅ <span id="cnt-ok">0</span> tamamlandı</span>
|
||||
<span>⏭ <span id="cnt-skip">0</span> atlandı</span>
|
||||
<span>❌ <span id="cnt-err">0</span> hata</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Log --}}
|
||||
<div id="log-box" style="display:none;background:#0d0d0d;border:1px solid #222;border-radius:8px;padding:12px;max-height:420px;overflow-y:auto;font-family:monospace;font-size:.78rem;line-height:1.6"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
let _animes = [], _idx = 0, _stop = false;
|
||||
let _ok = 0, _skip = 0, _err = 0;
|
||||
|
||||
const logBox = document.getElementById('log-box');
|
||||
const progWrap = document.getElementById('progress-wrap');
|
||||
const progBar = document.getElementById('prog-bar');
|
||||
const progLbl = document.getElementById('prog-label');
|
||||
const progCnt = document.getElementById('prog-count');
|
||||
|
||||
function log(html) {
|
||||
logBox.insertAdjacentHTML('beforeend', `<div>${html}</div>`);
|
||||
logBox.scrollTop = logBox.scrollHeight;
|
||||
}
|
||||
|
||||
async function startFill() {
|
||||
document.getElementById('start-btn').style.display = 'none';
|
||||
document.getElementById('stop-btn').style.display = '';
|
||||
document.getElementById('reset-btn').style.display = 'none';
|
||||
progWrap.style.display = '';
|
||||
logBox.style.display = '';
|
||||
logBox.innerHTML = '';
|
||||
_stop = false; _ok = 0; _skip = 0; _err = 0;
|
||||
updateCounts();
|
||||
|
||||
const force = document.getElementById('force-mode').checked;
|
||||
|
||||
log('<span style="color:#00c8d4">⏳ Anime listesi alınıyor…</span>');
|
||||
|
||||
const res = await fetch('{{ route("ai.anime-meta-ids") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
body: JSON.stringify({ force }),
|
||||
});
|
||||
const data = await res.json();
|
||||
_animes = data.animes || [];
|
||||
_idx = 0;
|
||||
|
||||
if (!_animes.length) {
|
||||
log('<span style="color:#4caf50">✅ Tüm animeler zaten dolu, yapılacak bir şey yok.</span>');
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
log(`<span style="color:#ccc">📋 ${_animes.length} anime işlenecek.</span>`);
|
||||
progBar.style.width = '0%';
|
||||
|
||||
await processNext(force);
|
||||
}
|
||||
|
||||
async function processNext(force) {
|
||||
if (_stop || _idx >= _animes.length) { finish(); return; }
|
||||
|
||||
const anime = _animes[_idx];
|
||||
_idx++;
|
||||
|
||||
const pct = Math.round((_idx / _animes.length) * 100);
|
||||
progBar.style.width = pct + '%';
|
||||
progCnt.textContent = `${_idx} / ${_animes.length}`;
|
||||
progLbl.textContent = anime.title;
|
||||
|
||||
const missingStr = anime.missing.length ? anime.missing.join(', ') : '—';
|
||||
log(`<span style="color:#888">[${_idx}/${_animes.length}]</span> <span style="color:#e8e8e8">${anime.title}</span> <span style="color:#555">— eksik: ${missingStr}</span>`);
|
||||
|
||||
try {
|
||||
const r = await fetch('{{ route("ai.fill-anime-meta") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
||||
body: JSON.stringify({ anime_id: anime.id, force }),
|
||||
});
|
||||
const d = await r.json();
|
||||
|
||||
if (d.ok) {
|
||||
const fields = (d.filled || []).join(', ') || 'değişiklik yok';
|
||||
log(` <span style="color:#4caf50">✅ OK</span> → ${fields}`);
|
||||
_ok++;
|
||||
} else {
|
||||
log(` <span style="color:#ff5722">❌ HATA</span> → ${d.error || 'Bilinmeyen hata'}`);
|
||||
_err++;
|
||||
}
|
||||
} catch (e) {
|
||||
log(` <span style="color:#ff5722">❌ HATA</span> → ${e.message}`);
|
||||
_err++;
|
||||
}
|
||||
|
||||
updateCounts();
|
||||
|
||||
// 1.5sn bekle, sonraki anime'ye geç
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
await processNext(force);
|
||||
}
|
||||
|
||||
function finish() {
|
||||
progBar.classList.remove('progress-bar-animated');
|
||||
progBar.style.width = '100%';
|
||||
progLbl.textContent = 'Tamamlandı';
|
||||
document.getElementById('stop-btn').style.display = 'none';
|
||||
document.getElementById('reset-btn').style.display = '';
|
||||
document.getElementById('start-btn').style.display = '';
|
||||
log(`<br><span style="color:#00c8d4;font-weight:700">🏁 Bitti → ${_ok} tamamlandı, ${_skip} atlandı, ${_err} hata</span>`);
|
||||
}
|
||||
|
||||
function stopFill() {
|
||||
_stop = true;
|
||||
log('<span style="color:#ff9800">⏸ Kullanıcı tarafından durduruldu.</span>');
|
||||
}
|
||||
|
||||
function resetFill() {
|
||||
_animes = []; _idx = 0; _ok = 0; _skip = 0; _err = 0;
|
||||
logBox.innerHTML = '';
|
||||
logBox.style.display = 'none';
|
||||
progWrap.style.display = 'none';
|
||||
progBar.style.width = '0%';
|
||||
progBar.classList.add('progress-bar-animated');
|
||||
document.getElementById('reset-btn').style.display = 'none';
|
||||
document.getElementById('start-btn').style.display = '';
|
||||
updateCounts();
|
||||
}
|
||||
|
||||
function updateCounts() {
|
||||
document.getElementById('cnt-ok').textContent = _ok;
|
||||
document.getElementById('cnt-skip').textContent = _skip;
|
||||
document.getElementById('cnt-err').textContent = _err;
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,141 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'AI Açıklama Yazma')
|
||||
@section('page-title', 'AI — Toplu Bölüm Açıklaması')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-7 col-lg-8">
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-stars" style="color:var(--info)"></i>
|
||||
<h6>Toplu Açıklama Yaz</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="font-size:13px;color:var(--text2);margin-bottom:20px">
|
||||
DeepSeek, seçilen animenin açıklaması boş olan tüm bölümlerine sırayla Türkçe açıklama yazar.
|
||||
Bölüm başına ~2-3 saniye sürer.
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Anime Seç</label>
|
||||
<select id="anime-select" class="form-select" style="max-width:480px">
|
||||
<option value="0">— Tüm animeler ({{ $totalMissing }} boş bölüm) —</option>
|
||||
@foreach($animes as $anime)
|
||||
<option value="{{ $anime->id }}">{{ $anime->title }} ({{ $anime->total_eps }} boş bölüm)</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button id="start-btn" class="btn btn-info px-4" onclick="startFill()">
|
||||
<i class="bi bi-stars"></i> Başlat
|
||||
</button>
|
||||
<button id="stop-btn" class="btn btn-outline-danger px-4" onclick="stopFill()" style="display:none">
|
||||
<i class="bi bi-stop-fill"></i> Durdur
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Progress --}}
|
||||
<div id="progress-card" class="card mb-4" style="display:none">
|
||||
<div class="card-header">
|
||||
<h6>İlerleme</h6>
|
||||
<span id="prog-count" style="font-size:12.5px;color:var(--text2);margin-left:auto">0 / 0</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="background:var(--surface);border-radius:99px;height:8px;overflow:hidden;margin-bottom:14px">
|
||||
<div id="prog-bar" style="height:100%;width:0%;background:linear-gradient(90deg,var(--info),var(--accent-lt));border-radius:99px;transition:width .3s"></div>
|
||||
</div>
|
||||
<div id="prog-log" style="max-height:360px;overflow-y:auto;font-size:12px;font-family:'JetBrains Mono',monospace;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;line-height:1.7"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Result --}}
|
||||
<div id="result-card" class="card" style="display:none">
|
||||
<div class="card-body">
|
||||
<div style="font-size:14px;font-weight:700;color:var(--text);margin-bottom:6px">Tamamlandı</div>
|
||||
<div id="result-text" style="font-size:13.5px;color:var(--text2)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const CSRF = '{{ csrf_token() }}';
|
||||
const URL_IDS = '{{ route("admin.ai.episode-ids") }}';
|
||||
const URL_FILL = '{{ route("admin.ai.fill-one") }}';
|
||||
|
||||
let running = false, shouldStop = false;
|
||||
|
||||
async function startFill() {
|
||||
const animeId = document.getElementById('anime-select').value;
|
||||
running = true; shouldStop = false;
|
||||
document.getElementById('start-btn').disabled = true;
|
||||
document.getElementById('stop-btn').style.display = 'inline-flex';
|
||||
document.getElementById('progress-card').style.display = 'block';
|
||||
document.getElementById('result-card').style.display = 'none';
|
||||
document.getElementById('prog-log').innerHTML = '';
|
||||
document.getElementById('prog-bar').style.width = '0%';
|
||||
|
||||
log('📋 Bölüm listesi alınıyor...', 'info');
|
||||
let episodes;
|
||||
try {
|
||||
const res = await post(URL_IDS, { anime_id: animeId });
|
||||
episodes = res.episodes || [];
|
||||
} catch(e) { log('❌ ' + e.message, 'danger'); finish(0,0); return; }
|
||||
|
||||
if (!episodes.length) { log('✅ Tüm açıklamalar zaten dolu!', 'success'); finish(0,0); return; }
|
||||
log(`🎯 ${episodes.length} boş açıklama. Başlıyor...`, 'info');
|
||||
document.getElementById('prog-count').textContent = '0 / ' + episodes.length;
|
||||
|
||||
let done = 0, failed = 0;
|
||||
for (let i = 0; i < episodes.length; i++) {
|
||||
if (shouldStop) { log('⏹ Durduruldu.', 'warning'); break; }
|
||||
const ep = episodes[i];
|
||||
log(`⏳ [${i+1}/${episodes.length}] ${ep.label}`, 'muted');
|
||||
try {
|
||||
const res = await post(URL_FILL, { episode_id: ep.id });
|
||||
if (res.ok) { done++; log(`✓ [${i+1}/${episodes.length}] ${ep.label}`, 'success'); }
|
||||
else { failed++; log(`✗ [${i+1}/${episodes.length}] ${ep.label} — ${res.error||'Hata'}`, 'danger'); }
|
||||
} catch(e) { failed++; log(`✗ ${ep.label} — ${e.message}`, 'danger'); }
|
||||
const pct = Math.round(((i+1)/episodes.length)*100);
|
||||
document.getElementById('prog-bar').style.width = pct+'%';
|
||||
document.getElementById('prog-count').textContent = (i+1)+' / '+episodes.length;
|
||||
document.getElementById('prog-log').scrollTop = document.getElementById('prog-log').scrollHeight;
|
||||
}
|
||||
finish(done, failed);
|
||||
}
|
||||
|
||||
function stopFill() { shouldStop = true; }
|
||||
|
||||
function finish(done, failed) {
|
||||
running = false;
|
||||
document.getElementById('start-btn').disabled = false;
|
||||
document.getElementById('stop-btn').style.display = 'none';
|
||||
const res = document.getElementById('result-card');
|
||||
res.style.display = 'block';
|
||||
document.getElementById('result-text').innerHTML =
|
||||
`<span style="color:var(--success);font-weight:700">${done} bölüm</span> yazıldı` +
|
||||
(failed ? `, <span style="color:var(--danger);font-weight:700">${failed} hata</span>` : '') + '.';
|
||||
}
|
||||
|
||||
function log(msg, type='muted') {
|
||||
const c = { success:'var(--success)', danger:'var(--danger)', info:'var(--info)', warning:'var(--warning)', muted:'var(--text3)' };
|
||||
const el = document.getElementById('prog-log');
|
||||
el.innerHTML += `<div style="color:${c[type]||c.muted}">${msg}</div>`;
|
||||
}
|
||||
|
||||
async function post(url, body) {
|
||||
const r = await fetch(url, { method:'POST', headers:{'Content-Type':'application/json','X-CSRF-TOKEN':CSRF}, body:JSON.stringify(body) });
|
||||
const data = await r.json();
|
||||
if (!r.ok && !data.ok) throw new Error(data.error||r.status);
|
||||
return data;
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,802 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Analitik')
|
||||
@section('page-title', 'Kullanıcı Analitikleri')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.period-btn { padding: 6px 18px; border-radius: 99px; border: 1px solid var(--border2); background: transparent; color: var(--text2); font-size: 0.82rem; cursor: pointer; transition: all .15s; text-decoration: none; display: inline-block; font-weight:600; }
|
||||
.period-btn.active, .period-btn:hover { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||
|
||||
.an-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px; }
|
||||
.an-card .val { font-size: 2rem; font-weight: 800; line-height: 1.1; color: var(--text); letter-spacing:-0.5px; }
|
||||
.an-card .lbl { font-size: 0.78rem; color: var(--text2); margin-top: 4px; font-weight:600; }
|
||||
.an-card .ico { font-size: 1.8rem; opacity: 0.5; }
|
||||
.an-card .delta { font-size: 0.72rem; font-weight: 600; margin-top: 6px; }
|
||||
.delta-up { color: var(--success); }
|
||||
.delta-down { color: var(--danger); }
|
||||
|
||||
.chart-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; }
|
||||
.chart-card .chart-title { font-size: 0.88rem; font-weight: 700; color: var(--text); margin-bottom: 14px; }
|
||||
.chart-wrap { position: relative; }
|
||||
|
||||
.geo-flag { font-size: 1.1rem; }
|
||||
.pbar-wrap { height: 6px; background: var(--surface); border-radius: 3px; flex: 1; }
|
||||
.pbar-fill { height: 100%; background: linear-gradient(90deg,var(--accent),var(--accent-lt)); border-radius: 3px; transition: width .4s; }
|
||||
|
||||
.live-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success); display: inline-block; animation: blink 1.2s infinite; }
|
||||
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.3} }
|
||||
|
||||
.tbl-sm th { font-size: 0.73rem; color: var(--text2); text-transform: uppercase; letter-spacing: .5px; padding: 8px 10px; }
|
||||
.tbl-sm td { font-size: 0.84rem; padding: 8px 10px; vertical-align: middle; border-top: 1px solid var(--border); color: var(--text); }
|
||||
.tbl-sm tr:first-child td { border-top: none; }
|
||||
|
||||
.badge-type { font-size: 0.7rem; padding: 2px 8px; border-radius: 20px; }
|
||||
.badge-home { background: rgba(6,182,212,0.12); color: var(--info); }
|
||||
.badge-anime { background: rgba(16,185,129,0.12); color: var(--success); }
|
||||
.badge-player { background: rgba(124,58,237,0.12); color: var(--accent-lt); }
|
||||
.badge-search { background: rgba(245,158,11,0.12); color: var(--warning); }
|
||||
.badge-ai { background: rgba(124,58,237,0.12); color: var(--accent-lt); }
|
||||
.badge-genre { background: rgba(6,182,212,0.12); color: var(--info); }
|
||||
.badge-other { background: rgba(255,255,255,0.06); color: var(--text2); }
|
||||
|
||||
.pct-pill { font-size: 0.7rem; padding: 1px 7px; border-radius: 10px; }
|
||||
.pct-high { background: var(--success); color: #fff; }
|
||||
.pct-mid { background: var(--warning); color: #000; }
|
||||
.pct-low { background: var(--surface); color: var(--text2); }
|
||||
|
||||
.ai-q-text { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.82rem; color: var(--text2); }
|
||||
|
||||
.section-head { font-size: 0.78rem; text-transform: uppercase; letter-spacing: .7px; color: var(--text2); font-weight: 700; margin-bottom: 14px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="content-wrap">
|
||||
|
||||
{{-- Period seçici --}}
|
||||
<div class="d-flex align-items-center gap-2 mb-4 flex-wrap">
|
||||
<span class="text-muted me-1" style="font-size:.82rem">Dönem:</span>
|
||||
@foreach(['today'=>'Bugün','7d'=>'7 Gün','30d'=>'30 Gün','90d'=>'90 Gün'] as $key=>$lbl)
|
||||
<a href="{{ route('admin.analytics.index', ['period'=>$key]) }}"
|
||||
class="period-btn {{ $period===$key ? 'active' : '' }}">{{ $lbl }}</a>
|
||||
@endforeach
|
||||
<span class="ms-auto text-muted" style="font-size:.75rem">
|
||||
<span class="live-dot me-1"></span> Canlı — Son güncelleme: {{ now()->format('H:i') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- Özet kartlar --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-sm-4 col-lg">
|
||||
<div class="an-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="val">{{ number_format($totalViews) }}</div>
|
||||
<div class="lbl">Sayfa Görüntüleme</div>
|
||||
@if($period === '7d' || $period === 'today')
|
||||
<div class="delta {{ $viewsDelta >= 0 ? 'delta-up' : 'delta-down' }}">
|
||||
<i class="bi bi-arrow-{{ $viewsDelta >= 0 ? 'up' : 'down' }}"></i>
|
||||
{{ abs($viewsDelta) }}% dünden
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<i class="bi bi-eye ico text-info"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-sm-4 col-lg">
|
||||
<div class="an-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="val">{{ number_format($uniqueVisitors) }}</div>
|
||||
<div class="lbl">Tekil Ziyaretçi</div>
|
||||
</div>
|
||||
<i class="bi bi-people ico text-success"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-sm-4 col-lg">
|
||||
<div class="an-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
@php
|
||||
$wh = floor($watchSeconds / 3600);
|
||||
$wm = floor(($watchSeconds % 3600) / 60);
|
||||
@endphp
|
||||
<div class="val">{{ $wh > 0 ? $wh.'s' : $wm.'d' }}</div>
|
||||
<div class="lbl">Toplam İzleme Süresi</div>
|
||||
<div class="delta" style="color:var(--text2)">{{ number_format($watchSeconds/3600, 1) }} saat</div>
|
||||
</div>
|
||||
<i class="bi bi-play-circle ico" style="color:#a371f7"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-sm-4 col-lg">
|
||||
<div class="an-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="val">{{ number_format($aiTotal) }}</div>
|
||||
<div class="lbl">AI Sorgusu</div>
|
||||
</div>
|
||||
<i class="bi bi-stars ico" style="color:#e8393c"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-sm-4 col-lg">
|
||||
<div class="an-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="val">{{ number_format($newUsers) }}</div>
|
||||
<div class="lbl">Yeni Kayıt</div>
|
||||
</div>
|
||||
<i class="bi bi-person-plus ico" style="color:#f0883e"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Görüntüleme trendi + Cihaz dağılımı --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="chart-card h-100">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div class="chart-title mb-0">Görüntüleme ve İzleme Trendi</div>
|
||||
<div class="d-flex gap-3" style="font-size:.72rem;color:var(--text2)">
|
||||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:var(--accent);margin-right:4px"></span>Görüntüleme</span>
|
||||
<span><span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:#a371f7;margin-right:4px"></span>İzleme (saat)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-wrap" style="height:220px">
|
||||
<canvas id="trendChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="chart-card h-100">
|
||||
<div class="chart-title">Cihaz Dağılımı</div>
|
||||
<div class="chart-wrap" style="height:160px;display:flex;align-items:center;justify-content:center">
|
||||
<canvas id="deviceChart"></canvas>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
@php $deviceTotal = $deviceStats->sum(); @endphp
|
||||
@foreach(['mobile'=>['bi-phone','var(--accent)'],'desktop'=>['bi-laptop','var(--success)'],'tablet'=>['bi-tablet','#79c0ff']] as $dv=>[$ico,$col])
|
||||
@if(($cnt=$deviceStats[$dv]??0) > 0)
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<i class="bi {{ $ico }}" style="color:{{ $col }};width:16px"></i>
|
||||
<span style="font-size:.82rem;width:60px">{{ ucfirst($dv) }}</span>
|
||||
<div class="pbar-wrap"><div class="pbar-fill" style="width:{{ $deviceTotal>0?round($cnt/$deviceTotal*100):0 }}%;background:{{ $col }}"></div></div>
|
||||
<span style="font-size:.78rem;color:var(--text2);width:40px;text-align:right">{{ number_format($cnt) }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Top anime + AI sorgular --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-7">
|
||||
<div class="chart-card h-100">
|
||||
<div class="chart-title">En Çok İzlenen 10 Anime</div>
|
||||
@if($topAnimes->count())
|
||||
@php $maxV = $topAnimes->max('views') ?: 1; @endphp
|
||||
@foreach($topAnimes as $i => $a)
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span style="font-size:.72rem;color:var(--text2);width:16px;text-align:right">{{ $i+1 }}</span>
|
||||
<span style="font-size:.84rem;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ $a['title'] }}</span>
|
||||
<div class="pbar-wrap" style="width:120px"><div class="pbar-fill" style="width:{{ round($a['views']/$maxV*100) }}%"></div></div>
|
||||
<span style="font-size:.78rem;color:var(--text2);width:42px;text-align:right">{{ number_format($a['views']) }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
<div class="text-muted text-center py-4" style="font-size:.85rem">Bu dönemde veri yok</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="chart-card h-100">
|
||||
<div class="chart-title">AI Sorgu Türleri</div>
|
||||
<div class="chart-wrap" style="height:160px;display:flex;align-items:center;justify-content:center">
|
||||
<canvas id="aiChart"></canvas>
|
||||
</div>
|
||||
@php
|
||||
$aiLabels = ['chat'=>'Sohbet','recommend'=>'Öneri','search'=>'Arama','episode_info'=>'Bölüm Analiz','similar'=>'Benzer'];
|
||||
$aiColors = ['chat'=>'var(--accent)','recommend'=>'#a371f7','search'=>'var(--success)','episode_info'=>'#f0883e','similar'=>'#79c0ff'];
|
||||
$aiTotalQ = $aiByType->sum();
|
||||
@endphp
|
||||
<div class="mt-3">
|
||||
@foreach($aiByType->sortDesc() as $type => $cnt)
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:{{ $aiColors[$type]??'var(--text2)' }}"></span>
|
||||
<span style="font-size:.82rem;flex:1">{{ $aiLabels[$type]??$type }}</span>
|
||||
<span style="font-size:.78rem;color:var(--text2)">{{ number_format($cnt) }}</span>
|
||||
<span style="font-size:.72rem;color:#6e7681">{{ $aiTotalQ>0?round($cnt/$aiTotalQ*100):0 }}%</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@if($aiByType->isEmpty())
|
||||
<div class="text-muted text-center py-2" style="font-size:.82rem">Bu dönemde AI sorgusu yok</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Saatlik dağılım --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-12">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">Bugünkü Saatlik Görüntüleme Dağılımı</div>
|
||||
<div class="chart-wrap" style="height:130px">
|
||||
<canvas id="hourlyChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Top bölümler + Coğrafi --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-7">
|
||||
<div class="chart-card h-100">
|
||||
<div class="section-head">En Çok İzlenen Bölümler (İzleme Süresi)</div>
|
||||
@if($topEpisodes->count())
|
||||
<table class="table mb-0 tbl-sm" style="color:#c9d1d9">
|
||||
<thead><tr>
|
||||
<th>Anime</th><th>Bölüm</th><th>Oynatma</th><th>Süre</th><th>Ort. %</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@foreach($topEpisodes as $ep)
|
||||
<tr>
|
||||
<td style="max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ $ep['anime'] }}</td>
|
||||
<td><span class="badge" style="background:var(--border);color:#c9d1d9">{{ $ep['label'] }}</span></td>
|
||||
<td>{{ number_format($ep['plays']) }}</td>
|
||||
<td>{{ $ep['hours'] }} s</td>
|
||||
<td>
|
||||
@php $pct=$ep['avg_pct']??0; @endphp
|
||||
<span class="pct-pill {{ $pct>=70?'pct-high':($pct>=40?'pct-mid':'pct-low') }}">%{{ $pct }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<div class="text-muted text-center py-4" style="font-size:.85rem">Bu dönemde izleme verisi yok</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="chart-card h-100">
|
||||
<div class="section-head">Coğrafi Dağılım (Şehir)</div>
|
||||
@if($geoStats->count())
|
||||
@php $maxGeo = $geoStats->max('cnt') ?: 1; @endphp
|
||||
@foreach($geoStats as $g)
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span style="font-size:.82rem;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
{{ $g->city }}
|
||||
<span style="color:var(--text2);font-size:.75rem"> · {{ $g->country }}</span>
|
||||
</span>
|
||||
<div class="pbar-wrap" style="width:80px"><div class="pbar-fill" style="width:{{ round($g->cnt/$maxGeo*100) }}%"></div></div>
|
||||
<span style="font-size:.75rem;color:var(--text2);width:32px;text-align:right">{{ $g->cnt }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
<div class="text-muted text-center py-4" style="font-size:.85rem">Coğrafi veri yok (IP geolocation bekliyor)</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- En aktif kullanıcılar + AI top sorular --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="chart-card h-100">
|
||||
<div class="section-head">En Aktif Kullanıcılar</div>
|
||||
@if($activeUsers->count())
|
||||
<table class="table mb-0 tbl-sm" style="color:#c9d1d9">
|
||||
<thead><tr>
|
||||
<th>#</th><th>Kullanıcı</th><th>Görüntüleme</th><th>Aktif Gün</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@foreach($activeUsers as $i => $au)
|
||||
<tr>
|
||||
<td style="color:var(--text2)">{{ $i+1 }}</td>
|
||||
<td>
|
||||
<div style="font-size:.83rem">{{ $au['user']->name }}</div>
|
||||
<div style="font-size:.72rem;color:var(--text2)">{{ $au['user']->email }}</div>
|
||||
</td>
|
||||
<td>{{ number_format($au['views']) }}</td>
|
||||
<td>{{ $au['days'] }} gün</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<div class="text-muted text-center py-4" style="font-size:.85rem">Bu dönemde giriş yapan kullanıcı yok</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="chart-card h-100">
|
||||
<div class="section-head">Popüler AI Soruları (Sohbet)</div>
|
||||
@if($aiTopQuestions->count())
|
||||
@foreach($aiTopQuestions as $q)
|
||||
<div class="d-flex align-items-start gap-2 mb-3">
|
||||
<i class="bi bi-chat-right-text" style="color:var(--accent);margin-top:2px;flex-shrink:0"></i>
|
||||
<div class="flex-1" style="flex:1">
|
||||
<div class="ai-q-text" title="{{ $q->query_text }}">{{ $q->query_text }}</div>
|
||||
<div style="font-size:.7rem;color:var(--text2);margin-top:2px">{{ $q->cnt }} kez soruldu</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
<div class="text-muted text-center py-4" style="font-size:.85rem">Bu dönemde chat sorusu yok</div>
|
||||
@endif
|
||||
|
||||
@if($aiTopUsers->count())
|
||||
<div class="section-head mt-3">AI'ı En Çok Kullananlar</div>
|
||||
@foreach($aiTopUsers as $au)
|
||||
<div class="d-flex align-items-center justify-content-between mb-2" style="font-size:.83rem">
|
||||
<span>{{ $au['name'] }}</span>
|
||||
<span class="badge" style="background:var(--border);color:#a371f7">{{ $au['cnt'] }} sorgu</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Sayfa türleri + Tarayıcılar --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="chart-card">
|
||||
<div class="section-head">Sayfa Türü Dağılımı</div>
|
||||
@php
|
||||
$ptLabels = ['home'=>'Ana Sayfa','anime'=>'Anime Detay','player'=>'Video Player','search'=>'Arama','ai'=>'AI Hub','genre'=>'Tür','other'=>'Diğer'];
|
||||
$ptColors = ['home'=>'#79c0ff','anime'=>'var(--success)','player'=>'#a371f7','search'=>'#f0883e','ai'=>'var(--accent)','genre'=>'#ffa657','other'=>'#6e7681'];
|
||||
$ptTotal = $pageTypeStats->sum();
|
||||
@endphp
|
||||
<div class="row">
|
||||
@foreach($pageTypeStats->sortDesc() as $pt => $cnt)
|
||||
<div class="col-6 mb-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span style="display:inline-block;width:10px;height:10px;border-radius:2px;background:{{ $ptColors[$pt]??'var(--text2)' }};flex-shrink:0"></span>
|
||||
<span style="font-size:.82rem;flex:1">{{ $ptLabels[$pt]??$pt }}</span>
|
||||
<span style="font-size:.78rem;color:var(--text2)">{{ number_format($cnt) }}</span>
|
||||
</div>
|
||||
<div class="pbar-wrap mt-1"><div class="pbar-fill" style="width:{{ $ptTotal>0?round($cnt/$ptTotal*100):0 }}%;background:{{ $ptColors[$pt]??'var(--text2)' }}"></div></div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="chart-card">
|
||||
<div class="section-head">Tarayıcı Dağılımı</div>
|
||||
@php $brTotal = $browserStats->sum(); @endphp
|
||||
@foreach($browserStats->sortDesc()->take(6) as $br => $cnt)
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span style="font-size:.82rem;width:80px">{{ $br ?: 'Diğer' }}</span>
|
||||
<div class="pbar-wrap"><div class="pbar-fill" style="width:{{ $brTotal>0?round($cnt/$brTotal*100):0 }}%;background:var(--accent)"></div></div>
|
||||
<span style="font-size:.78rem;color:var(--text2);width:40px;text-align:right">{{ number_format($cnt) }}</span>
|
||||
<span style="font-size:.72rem;color:#6e7681;width:32px;text-align:right">{{ $brTotal>0?round($cnt/$brTotal*100):0 }}%</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Trafik Kaynakları --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-7">
|
||||
<div class="chart-card h-100">
|
||||
<div class="section-head">Trafik Kaynakları (Referrer)</div>
|
||||
@php
|
||||
$refTotal = $referrerStats->sum() + $directTraffic;
|
||||
$allSources = collect(['Direkt / Boş' => $directTraffic])->merge($referrerStats)->sortDesc();
|
||||
$srcColors = ['Direkt / Boş'=>'var(--text2)'];
|
||||
$palette = ['var(--accent)','var(--success)','#79c0ff','#f0883e','#a371f7','#ffa657','#58a6ff','#7ee787','#ff7bc6','#d2a8ff','#ffa198','#56d364'];
|
||||
$pi = 0;
|
||||
foreach($referrerStats->keys() as $k) { $srcColors[$k] = $palette[$pi % count($palette)]; $pi++; }
|
||||
@endphp
|
||||
@if($allSources->sum() > 0)
|
||||
@foreach($allSources->take(12) as $src => $cnt)
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:{{ $srcColors[$src]??'var(--text2)' }};flex-shrink:0"></span>
|
||||
<span style="font-size:.82rem;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="{{ $src }}">
|
||||
@if($src === 'Direkt / Boş')
|
||||
<i class="bi bi-link-45deg me-1" style="color:var(--text2)"></i>{{ $src }}
|
||||
@else
|
||||
<i class="bi bi-globe2 me-1" style="color:{{ $srcColors[$src]??'var(--text2)' }}"></i>{{ $src }}
|
||||
@endif
|
||||
</span>
|
||||
<div class="pbar-wrap" style="width:100px"><div class="pbar-fill" style="width:{{ $refTotal>0?round($cnt/$refTotal*100):0 }}%;background:{{ $srcColors[$src]??'var(--text2)' }}"></div></div>
|
||||
<span style="font-size:.78rem;color:var(--text2);width:38px;text-align:right">{{ number_format($cnt) }}</span>
|
||||
<span style="font-size:.72rem;color:#6e7681;width:30px;text-align:right">{{ $refTotal>0?round($cnt/$refTotal*100):0 }}%</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@else
|
||||
<div class="text-muted text-center py-4" style="font-size:.85rem">Bu dönemde referrer verisi yok</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="chart-card h-100">
|
||||
<div class="section-head">Kaynak Dağılımı</div>
|
||||
<div class="chart-wrap" style="height:180px;display:flex;align-items:center;justify-content:center">
|
||||
<canvas id="referrerChart"></canvas>
|
||||
</div>
|
||||
<div class="mt-3 text-center">
|
||||
<div style="font-size:.78rem;color:var(--text2)">
|
||||
Toplam kayıtlı trafik: <strong style="color:var(--text)">{{ number_format($refTotal) }}</strong>
|
||||
</div>
|
||||
<div style="font-size:.75rem;color:#6e7681;margin-top:4px">
|
||||
Direkt: {{ $refTotal>0?round($directTraffic/$refTotal*100):0 }}% · Referral: {{ $refTotal>0?round($referrerStats->sum()/$refTotal*100):0 }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Son aktiviteler (canlı akış) --}}
|
||||
<div class="chart-card mb-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div class="section-head mb-0">Son Sayfa Görüntülemeleri</div>
|
||||
<span class="live-dot"></span>
|
||||
</div>
|
||||
<table class="table mb-0 tbl-sm" style="color:#c9d1d9">
|
||||
<thead><tr>
|
||||
<th>Zaman</th><th>Kullanıcı</th><th>Sayfa Türü</th><th>URL</th><th>Şehir</th><th>Cihaz</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@forelse($recentViews as $rv)
|
||||
<tr>
|
||||
<td style="white-space:nowrap;color:var(--text2);font-size:.75rem">{{ $rv->created_at->diffForHumans() }}</td>
|
||||
<td>
|
||||
@if($rv->user)
|
||||
<span style="font-size:.82rem">{{ $rv->user->name }}</span>
|
||||
@else
|
||||
<span style="color:#6e7681;font-size:.78rem">Misafir</span>
|
||||
@endif
|
||||
</td>
|
||||
<td><span class="badge-type badge-{{ $rv->page_type }}">{{ $rv->page_type }}</span></td>
|
||||
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.75rem;color:var(--text2)" title="{{ $rv->url }}">{{ $rv->url }}</td>
|
||||
<td style="font-size:.78rem">{{ $rv->city ?: '—' }}</td>
|
||||
<td><i class="bi bi-{{ match($rv->device){'mobile'=>'phone','tablet'=>'tablet',default=>'laptop'} }}" style="color:var(--text2)"></i></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="text-center text-muted py-4" style="font-size:.85rem">Bu dönemde görüntüleme yok</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- ── BOT TRAFİK PANELİ ─────────────────────────────────────────────────── --}}
|
||||
<div class="an-card mt-4">
|
||||
<div class="section-head" style="display:flex;align-items:center;justify-content:space-between">
|
||||
<span>🤖 Bot & Güvenlik</span>
|
||||
<span style="font-size:.78rem;color:var(--text2)">Son dönem</span>
|
||||
</div>
|
||||
|
||||
{{-- Bot özet kartları --}}
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:20px">
|
||||
<div class="an-card" style="text-align:center">
|
||||
<div style="font-size:1.6rem;font-weight:800;color:var(--danger)">{{ number_format($botViews) }}</div>
|
||||
<div style="font-size:.78rem;color:var(--text2)">Bot İsteği</div>
|
||||
</div>
|
||||
<div class="an-card" style="text-align:center">
|
||||
<div style="font-size:1.6rem;font-weight:800;color:var(--success)">{{ number_format($humanViews) }}</div>
|
||||
<div style="font-size:.78rem;color:var(--text2)">İnsan İsteği</div>
|
||||
</div>
|
||||
<div class="an-card" style="text-align:center">
|
||||
<div style="font-size:1.6rem;font-weight:800;color:{{ ($botRatio ?? 0) > 50 ? 'var(--danger)' : '#d29922' }}">{{ $botRatio ?? 0 }}%</div>
|
||||
<div style="font-size:.78rem;color:var(--text2)">Bot Oranı</div>
|
||||
</div>
|
||||
<div class="an-card" style="text-align:center">
|
||||
<div style="font-size:1.6rem;font-weight:800;color:var(--info)">{{ $blockedIps->count() }}</div>
|
||||
<div style="font-size:.78rem;color:var(--text2)">Engelli IP</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||
|
||||
{{-- En çok vuran botlar --}}
|
||||
<div>
|
||||
<div style="font-size:.82rem;font-weight:700;color:var(--text);margin-bottom:10px">Top Bot IP'leri</div>
|
||||
<table class="table table-sm" style="font-size:.78rem">
|
||||
<thead><tr><th>IP</th><th>İstek</th><th>Durum</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($botTopIps as $b)
|
||||
<tr>
|
||||
<td><code style="color:var(--info);font-size:.75rem">{{ $b->ip }}</code></td>
|
||||
<td>{{ number_format($b->cnt) }}</td>
|
||||
<td>
|
||||
<span style="font-size:.7rem;padding:2px 7px;border-radius:4px;background:{{ $b->action === 'blocked' ? 'rgba(248,81,73,.15)' : 'rgba(63,185,80,.15)' }};color:{{ $b->action === 'blocked' ? 'var(--danger)' : 'var(--success)' }}">
|
||||
{{ $b->action }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ route('admin.analytics.block-ip') }}" style="display:inline">
|
||||
@csrf
|
||||
<input type="hidden" name="ip" value="{{ $b->ip }}">
|
||||
<input type="hidden" name="reason" value="Admin: bot trafiği">
|
||||
<button type="submit" class="btn btn-sm" style="padding:1px 8px;font-size:.7rem;background:rgba(248,81,73,.15);border:1px solid rgba(248,81,73,.3);color:var(--danger);border-radius:4px">Engelle</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="text-muted text-center py-3">Bot logu yok</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Bot türleri --}}
|
||||
<div>
|
||||
<div style="font-size:.82rem;font-weight:700;color:var(--text);margin-bottom:10px">Bot Türleri</div>
|
||||
<table class="table table-sm" style="font-size:.78rem">
|
||||
<thead><tr><th>Bot</th><th>İstek</th><th>Durum</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($botByName as $b)
|
||||
<tr>
|
||||
<td style="max-width:150px;overflow:hidden;text-overflow:ellipsis">{{ $b->bot_name ?: '—' }}</td>
|
||||
<td>{{ number_format($b->cnt) }}</td>
|
||||
<td>
|
||||
<span style="font-size:.7rem;padding:2px 7px;border-radius:4px;background:{{ $b->action === 'blocked' ? 'rgba(248,81,73,.15)' : 'rgba(210,153,34,.15)' }};color:{{ $b->action === 'blocked' ? 'var(--danger)' : '#d29922' }}">
|
||||
{{ $b->action }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="3" class="text-muted text-center py-3">Kayıt yok</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Manuel IP Engelleme --}}
|
||||
<div style="margin-top:20px;border-top:1px solid var(--border);padding-top:16px">
|
||||
<div style="font-size:.82rem;font-weight:700;color:var(--text);margin-bottom:10px">Manuel IP Engelle</div>
|
||||
<form method="POST" action="{{ route('admin.analytics.block-ip') }}" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
|
||||
@csrf
|
||||
<div>
|
||||
<label style="font-size:.75rem;color:var(--text2);display:block;margin-bottom:4px">IP Adresi</label>
|
||||
<input type="text" name="ip" class="form-control form-control-sm" placeholder="1.2.3.4" style="width:160px">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:.75rem;color:var(--text2);display:block;margin-bottom:4px">Sebep</label>
|
||||
<input type="text" name="reason" class="form-control form-control-sm" placeholder="Manuel engel" style="width:200px">
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:.75rem;color:var(--text2);display:block;margin-bottom:4px">Bitiş (boşsa kalıcı)</label>
|
||||
<input type="date" name="expires_at" class="form-control form-control-sm" style="width:150px">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-sm" style="background:rgba(248,81,73,.2);border:1px solid rgba(248,81,73,.4);color:var(--danger);border-radius:6px">Engelle</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Engelli IP listesi --}}
|
||||
@if($blockedIps->count())
|
||||
<div style="margin-top:16px">
|
||||
<div style="font-size:.82rem;font-weight:700;color:var(--text);margin-bottom:8px">Aktif Engeller</div>
|
||||
<table class="table table-sm" style="font-size:.78rem">
|
||||
<thead><tr><th>IP</th><th>Sebep</th><th>Tür</th><th>Bitiş</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
@foreach($blockedIps as $bi)
|
||||
<tr>
|
||||
<td><code style="color:var(--danger);font-size:.75rem">{{ $bi->ip }}</code></td>
|
||||
<td>{{ $bi->reason ?: '—' }}</td>
|
||||
<td><span style="font-size:.7rem;color:{{ $bi->auto_blocked ? '#d29922' : 'var(--text2)' }}">{{ $bi->auto_blocked ? 'Otomatik' : 'Manuel' }}</span></td>
|
||||
<td style="font-size:.75rem;color:var(--text2)">{{ $bi->expires_at ? \Carbon\Carbon::parse($bi->expires_at)->diffForHumans() : 'Kalıcı' }}</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ route('admin.analytics.unblock-ip') }}" style="display:inline">
|
||||
@csrf
|
||||
<input type="hidden" name="ip" value="{{ $bi->ip }}">
|
||||
<button type="submit" class="btn btn-sm" style="padding:1px 8px;font-size:.7rem;background:rgba(63,185,80,.1);border:1px solid rgba(63,185,80,.3);color:var(--success);border-radius:4px">Kaldır</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- ── OTURUM DETAYLARI ────────────────────────────────────────────────────── --}}
|
||||
<div class="an-card mt-4">
|
||||
<div class="section-head" style="display:flex;align-items:center;justify-content:space-between">
|
||||
<span>📊 Oturum Analizi</span>
|
||||
<div style="display:flex;gap:16px;font-size:.82rem">
|
||||
<span>Ort. Süre: <strong style="color:var(--info)">{{ gmdate('i:s', $avgSessionTime) }}</strong></span>
|
||||
<span>Ort. Sayfa: <strong style="color:var(--info)">{{ $avgPages }}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-sm" style="font-size:.78rem">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP</th><th>Ülke</th><th>Cihaz</th><th>Tarayıcı</th>
|
||||
<th>Sayfalar</th><th>Süre</th><th>Kaynak</th><th>Bot?</th><th>Başlangıç</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($sessions as $s)
|
||||
<tr style="{{ $s->is_bot ? 'opacity:.5' : '' }}">
|
||||
<td><code style="font-size:.72rem;color:var(--info)">{{ $s->ip }}</code></td>
|
||||
<td>{{ $s->country ?: '—' }}</td>
|
||||
<td><i class="bi bi-{{ match($s->device ?? ''){ 'mobile'=>'phone','tablet'=>'tablet',default=>'laptop'} }}"></i></td>
|
||||
<td>{{ $s->browser ?: '—' }}</td>
|
||||
<td>{{ $s->pages_visited }}</td>
|
||||
<td>{{ gmdate('i:s', $s->total_seconds) }}</td>
|
||||
<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="{{ $s->referrer }}">{{ $s->referrer ? parse_url($s->referrer, PHP_URL_HOST) : 'Direkt' }}</td>
|
||||
<td>
|
||||
@if($s->is_bot)
|
||||
<span style="font-size:.7rem;color:var(--danger)">{{ $s->bot_type }}</span>
|
||||
@else
|
||||
<span style="font-size:.7rem;color:var(--success)">İnsan</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="color:var(--text2)">{{ \Carbon\Carbon::parse($s->started_at)->diffForHumans() }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="9" class="text-center text-muted py-4">Oturum kaydı yok</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
Chart.defaults.color = 'var(--text2)';
|
||||
Chart.defaults.borderColor = 'var(--border)';
|
||||
|
||||
// ── Trend grafiği ────────────────────────────────────────────────────────────
|
||||
new Chart(document.getElementById('trendChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: @json($trendLabels),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Görüntüleme',
|
||||
data: @json($trendData),
|
||||
borderColor: 'var(--accent)',
|
||||
backgroundColor: 'rgba(232,67,147,.08)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 3,
|
||||
pointBackgroundColor: 'var(--accent)',
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'İzleme (saat)',
|
||||
data: @json($watchTrendData),
|
||||
borderColor: '#a371f7',
|
||||
backgroundColor: 'rgba(163,113,247,.08)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointRadius: 3,
|
||||
pointBackgroundColor: '#a371f7',
|
||||
yAxisID: 'y1',
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { color: 'var(--border)' }, ticks: { maxTicksLimit: 12, font: { size: 10 } } },
|
||||
y: { grid: { color: 'var(--border)' }, ticks: { font: { size: 10 } }, beginAtZero: true },
|
||||
y1: { position: 'right', grid: { display: false }, ticks: { font: { size: 10 } }, beginAtZero: true },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Cihaz pasta grafiği ──────────────────────────────────────────────────────
|
||||
@php
|
||||
$dvKeys = ['mobile','desktop','tablet','unknown'];
|
||||
$dvData = array_map(fn($k) => $deviceStats[$k] ?? 0, $dvKeys);
|
||||
@endphp
|
||||
new Chart(document.getElementById('deviceChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Mobil', 'Masaüstü', 'Tablet', 'Diğer'],
|
||||
datasets: [{
|
||||
data: @json($dvData),
|
||||
backgroundColor: ['var(--accent)','var(--success)','#79c0ff','#6e7681'],
|
||||
borderWidth: 0,
|
||||
hoverOffset: 4,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false }, tooltip: { callbacks: {
|
||||
label: ctx => ` ${ctx.label}: ${ctx.parsed.toLocaleString()}`
|
||||
}}}
|
||||
}
|
||||
});
|
||||
|
||||
// ── AI sorgu türleri grafiği ─────────────────────────────────────────────────
|
||||
@php
|
||||
$aiK = $aiByType->keys()->toArray();
|
||||
$aiV = $aiByType->values()->toArray();
|
||||
$aiColorMap = ['chat'=>'var(--accent)','recommend'=>'#a371f7','search'=>'var(--success)','episode_info'=>'#f0883e','similar'=>'#79c0ff'];
|
||||
$aiC = array_map(fn($k) => $aiColorMap[$k] ?? 'var(--text2)', $aiK);
|
||||
$aiLabelMap = ['chat'=>'Sohbet','recommend'=>'Öneri','search'=>'Arama','episode_info'=>'Bölüm','similar'=>'Benzer'];
|
||||
$aiL = array_map(fn($k) => $aiLabelMap[$k] ?? $k, $aiK);
|
||||
@endphp
|
||||
new Chart(document.getElementById('aiChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: @json($aiL),
|
||||
datasets: [{
|
||||
data: @json($aiV),
|
||||
backgroundColor: @json($aiC),
|
||||
borderWidth: 0,
|
||||
hoverOffset: 4,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } }
|
||||
}
|
||||
});
|
||||
|
||||
// ── Saatlik dağılım ──────────────────────────────────────────────────────────
|
||||
new Chart(document.getElementById('hourlyChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: Array.from({length:24}, (_,i) => `${String(i).padStart(2,'0')}:00`),
|
||||
datasets: [{
|
||||
label: 'Görüntüleme',
|
||||
data: @json($hourlyData),
|
||||
backgroundColor: 'rgba(232,67,147,.5)',
|
||||
borderColor: 'var(--accent)',
|
||||
borderWidth: 1,
|
||||
borderRadius: 3,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { display: false }, ticks: { font: { size: 9 } } },
|
||||
y: { grid: { color: 'var(--border)' }, ticks: { font: { size: 10 } }, beginAtZero: true },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Referrer pasta grafiği ────────────────────────────────────────────────────
|
||||
@php
|
||||
$refChartLabels = $allSources->take(8)->keys()->map(fn($k) => strlen($k)>22 ? substr($k,0,20).'…' : $k)->values()->toArray();
|
||||
$refChartData = $allSources->take(8)->values()->toArray();
|
||||
$refChartColors = $allSources->take(8)->keys()->map(fn($k) => $srcColors[$k] ?? 'var(--text2)')->values()->toArray();
|
||||
@endphp
|
||||
new Chart(document.getElementById('referrerChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: @json($refChartLabels),
|
||||
datasets: [{
|
||||
data: @json($refChartData),
|
||||
backgroundColor: @json($refChartColors),
|
||||
borderWidth: 0,
|
||||
hoverOffset: 4,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false }, tooltip: { callbacks: {
|
||||
label: ctx => ` ${ctx.label}: ${ctx.parsed.toLocaleString()}`
|
||||
}}}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Otomatik yenileme (60 saniye) ────────────────────────────────────────────
|
||||
setTimeout(() => location.reload(), 60000);
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Kullanıcı Aktivitesi — ' . $user->name)
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex align-items-center gap-3 mb-4">
|
||||
<a href="{{ route('admin.analytics.users', ['tab'=>'activity']) }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i></a>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
@if($user->avatar)
|
||||
<img src="{{ $user->avatar }}" class="rounded-circle" width="48" height="48" style="object-fit:cover">
|
||||
@else
|
||||
<div class="rounded-circle bg-info d-flex align-items-center justify-content-center text-dark fw-bold" style="width:48px;height:48px;font-size:18px">{{ strtoupper(substr($user->name,0,1)) }}</div>
|
||||
@endif
|
||||
<div>
|
||||
<h4 class="mb-0">{{ $user->name }} <span class="badge {{ $user->role==='admin'?'bg-danger':($user->role==='moderator'?'bg-warning text-dark':'bg-secondary') }}">{{ $user->role }}</span></h4>
|
||||
<small class="text-muted">{{ $user->email }} · Üyelik: {{ $user->created_at->format('d.m.Y') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-auto d-flex gap-2">
|
||||
<form class="d-flex gap-2">
|
||||
<input type="hidden" name="period" value="{{ $period }}">
|
||||
<select name="period" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
@foreach([7=>'Son 7 Gün',14=>'Son 14 Gün',30=>'Son 30 Gün',90=>'Son 90 Gün'] as $d=>$l)
|
||||
<option value="{{ $d }}" {{ $period==$d?'selected':'' }}>{{ $l }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
<a href="{{ route('admin.users.show', $user) }}" class="btn btn-sm btn-outline-info">
|
||||
<i class="bi bi-person me-1"></i>Profili Gör
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
{{-- Aksiyon Özeti --}}
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Aksiyon Özeti</strong></div>
|
||||
<div class="card-body">
|
||||
@forelse($actBreakdown as $ab)
|
||||
@php $labels = \App\Models\UserActivityLog::$actionLabels; @endphp
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span style="font-size:12px">{{ $labels[$ab->action] ?? $ab->action }}</span>
|
||||
<span class="badge bg-info text-dark">{{ $ab->cnt }}</span>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-muted text-center small">Bu dönemde aktivite yok</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Son izleme --}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><strong><i class="bi bi-play-circle me-1"></i>Son İzlemeler</strong></div>
|
||||
<div class="list-group list-group-flush">
|
||||
@forelse($watchEvents as $w)
|
||||
<div class="list-group-item py-2 px-3">
|
||||
<div class="fw-semibold" style="font-size:12px">{{ $w->anime_title }}</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<small class="text-muted">Bölüm {{ $w->episode_number }}</small>
|
||||
<small class="text-{{ $w->percent_complete >= 80 ? 'success' : 'warning' }}">%{{ $w->percent_complete }}</small>
|
||||
</div>
|
||||
<div style="height:3px;background:#333;border-radius:2px;margin-top:4px;overflow:hidden">
|
||||
<div style="width:{{ $w->percent_complete }}%;height:100%;background:var(--bs-info)"></div>
|
||||
</div>
|
||||
<small class="text-muted" style="font-size:10px">{{ \Carbon\Carbon::parse($w->created_at)->diffForHumans() }}</small>
|
||||
</div>
|
||||
@empty
|
||||
<div class="list-group-item text-muted small text-center py-3">İzleme verisi yok</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Aktivite Log Tablosu --}}
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Detaylı Aktivite Günlüğü</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr><th>Aksiyon</th><th>Konu</th><th>IP</th><th>Ülke</th><th>Cihaz</th><th>Zaman</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($logs as $log)
|
||||
@php $labels = \App\Models\UserActivityLog::$actionLabels; @endphp
|
||||
<tr>
|
||||
<td><span class="badge bg-info text-dark" style="font-size:10px">{{ $labels[$log->action] ?? $log->action }}</span></td>
|
||||
<td><small class="text-muted">{{ $log->subject_type ? class_basename($log->subject_type).'#'.$log->subject_id : '—' }}</small></td>
|
||||
<td><code style="font-size:10px">{{ $log->ip }}</code></td>
|
||||
<td><small class="text-muted">{{ $log->country ?: '—' }}</small></td>
|
||||
<td><small class="text-muted text-capitalize">{{ $log->device ?: '—' }}</small></td>
|
||||
<td><small class="text-muted">{{ \Carbon\Carbon::parse($log->created_at)->format('d.m.Y H:i') }}</small></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="text-center text-muted py-4">Aktivite bulunamadı</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">{{ $logs->links() }}</div>
|
||||
</div>
|
||||
|
||||
{{-- Son Sayfa Görüntülemeleri --}}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header"><strong><i class="bi bi-eye me-1"></i>Son Sayfa Görüntülemeleri</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-dark"><tr><th>URL</th><th>Sayfa Tipi</th><th>Süre</th><th>Zaman</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($pageviews as $pv)
|
||||
<tr>
|
||||
<td><small class="text-muted" style="max-width:200px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="{{ $pv->url }}">{{ $pv->url }}</small></td>
|
||||
<td><span class="badge bg-secondary" style="font-size:9px">{{ $pv->page_type }}</span></td>
|
||||
<td><small class="text-muted">{{ isset($pv->time_on_page) && $pv->time_on_page > 0 ? $pv->time_on_page.'s' : '—' }}</small></td>
|
||||
<td><small class="text-muted">{{ \Carbon\Carbon::parse($pv->created_at)->format('d.m H:i') }}</small></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="text-center text-muted py-3">Sayfa görüntüleme yok</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,375 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Kullanıcı Analitiği')
|
||||
|
||||
@section('content')
|
||||
<div class="d-flex align-items-center justify-content-between mb-4">
|
||||
<div>
|
||||
<h4 class="mb-1"><i class="bi bi-people me-2 text-info"></i>Kullanıcı Analitiği</h4>
|
||||
<p class="text-muted mb-0 small">Gerçek kullanıcılar, bot trafiği, ülke dağılımı ve aktivite takibi</p>
|
||||
</div>
|
||||
<form class="d-flex gap-2 align-items-center" method="GET">
|
||||
<input type="hidden" name="tab" value="{{ $tab }}">
|
||||
<select name="period" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
@foreach([7=>'Son 7 Gün',14=>'Son 14 Gün',30=>'Son 30 Gün',90=>'Son 90 Gün'] as $d=>$l)
|
||||
<option value="{{ $d }}" {{ $period==$d?'selected':'' }}>{{ $l }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if($country)
|
||||
<a href="{{ request()->fullUrlWithoutQuery(['country']) }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-x"></i> {{ $country }}</a>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Overview Cards --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3 border-info">
|
||||
<div class="h3 mb-0 text-info">{{ number_format($totalReal) }}</div>
|
||||
<small class="text-muted">Toplam Kullanıcı</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3 border-success">
|
||||
<div class="h3 mb-0 text-success">+{{ number_format($newReal) }}</div>
|
||||
<small class="text-muted">Yeni Üye</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3 border-primary">
|
||||
<div class="h3 mb-0 text-primary">{{ number_format($active30) }}</div>
|
||||
<small class="text-muted">Aktif Kullanıcı</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3 border-success">
|
||||
<div class="h3 mb-0 text-success">{{ number_format($realViews) }}</div>
|
||||
<small class="text-muted">Gerçek Görüntüleme</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3 border-danger">
|
||||
<div class="h3 mb-0 text-danger">{{ number_format($botViews) }}</div>
|
||||
<small class="text-muted">Bot Görüntüleme</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="card text-center py-3">
|
||||
@php $ratio = ($realViews+$botViews) > 0 ? round($botViews/($realViews+$botViews)*100,1) : 0; @endphp
|
||||
<div class="h3 mb-0 {{ $ratio > 30 ? 'text-danger' : 'text-warning' }}">{{ $ratio }}%</div>
|
||||
<small class="text-muted">Bot Oranı</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tabs --}}
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
@foreach(['overview'=>'Genel Bakış','activity'=>'Aktivite Günlüğü','bots'=>'Bot Analizi','country'=>'Ülke Dağılımı'] as $t=>$l)
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{ $tab==$t?'active':'' }}" href="{{ request()->fullUrlWithQuery(['tab'=>$t]) }}">{{ $l }}</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
{{-- ── Overview Tab ──────────────────────────────────────────────────────────── --}}
|
||||
@if($tab === 'overview')
|
||||
<div class="row g-4">
|
||||
{{-- Daily new users chart --}}
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Günlük Yeni Üye</strong></div>
|
||||
<div class="card-body"><canvas id="dailyChart" height="80"></canvas></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Device breakdown --}}
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Cihaz Dağılımı (gerçek kullanıcı)</strong></div>
|
||||
<div class="card-body">
|
||||
@foreach($deviceBreakdown as $dev)
|
||||
@php $pct = $realViews > 0 ? round($dev->cnt/$realViews*100,1) : 0; @endphp
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="text-capitalize">{{ $dev->device ?: 'bilinmiyor' }}</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div style="width:80px;height:6px;background:#333;border-radius:3px;overflow:hidden">
|
||||
<div style="width:{{ $pct }}%;height:100%;background:var(--bs-info)"></div>
|
||||
</div>
|
||||
<small class="text-muted">{{ number_format($dev->cnt) }} ({{ $pct }}%)</small>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Top active users --}}
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>En Aktif Kullanıcılar</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-dark"><tr><th>#</th><th>Kullanıcı</th><th>Aksiyon</th></tr></thead>
|
||||
<tbody>
|
||||
@foreach($topUsers as $i => $tu)
|
||||
@php $u = $topUserMap[$tu->user_id] ?? null; @endphp
|
||||
<tr>
|
||||
<td class="text-muted">{{ $i+1 }}</td>
|
||||
<td>
|
||||
@if($u)
|
||||
<a href="{{ route('admin.analytics.user-detail', $u) }}" class="text-decoration-none">{{ $u->name }}</a>
|
||||
<br><small class="text-muted">{{ $u->email }}</small>
|
||||
@else <span class="text-muted">#{{ $tu->user_id }}</span> @endif
|
||||
</td>
|
||||
<td><span class="badge bg-primary">{{ number_format($tu->actions) }}</span></td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Action breakdown --}}
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong>Aksiyon Dağılımı</strong></div>
|
||||
<div class="card-body">
|
||||
@foreach($actionBreakdown as $ab)
|
||||
@php $labels = \App\Models\UserActivityLog::$actionLabels; @endphp
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span style="font-size:12px">{{ $labels[$ab->action] ?? $ab->action }}</span>
|
||||
<span class="badge bg-secondary">{{ number_format($ab->cnt) }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@if($actionBreakdown->isEmpty()) <p class="text-muted text-center small py-3">Henüz aktivite yok</p> @endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Activity Tab ──────────────────────────────────────────────────────────── --}}
|
||||
@elseif($tab === 'activity')
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<strong>Aktivite Günlüğü</strong>
|
||||
<form class="d-flex gap-2" method="GET">
|
||||
<input type="hidden" name="tab" value="activity">
|
||||
<input type="hidden" name="period" value="{{ $period }}">
|
||||
<select name="action" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
<option value="">Tüm Aksiyonlar</option>
|
||||
@foreach(\App\Models\UserActivityLog::$actionLabels as $k=>$v)
|
||||
<option value="{{ $k }}" {{ request('action')==$k?'selected':'' }}>{{ $v }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<input type="text" name="user_id" class="form-control form-control-sm" placeholder="User ID" value="{{ request('user_id') }}" style="width:100px">
|
||||
<input type="text" name="country" class="form-control form-control-sm" placeholder="Ülke" value="{{ $country }}" style="width:80px">
|
||||
<button type="submit" class="btn btn-sm btn-outline-info"><i class="bi bi-search"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr><th>Kullanıcı</th><th>Aksiyon</th><th>Konu</th><th>IP</th><th>Ülke</th><th>Cihaz</th><th>Zaman</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($actLogs as $log)
|
||||
@php $labels = \App\Models\UserActivityLog::$actionLabels; @endphp
|
||||
<tr>
|
||||
<td>
|
||||
@if($log->user)
|
||||
<a href="{{ route('admin.analytics.user-detail', $log->user) }}" class="text-decoration-none small">{{ $log->user->name }}</a>
|
||||
@else <span class="text-muted small">Misafir</span> @endif
|
||||
</td>
|
||||
<td><span class="badge bg-info text-dark" style="font-size:10px">{{ $labels[$log->action] ?? $log->action }}</span></td>
|
||||
<td><small class="text-muted">{{ $log->subject_type ? class_basename($log->subject_type).'#'.$log->subject_id : '—' }}</small></td>
|
||||
<td><code style="font-size:10px">{{ $log->ip }}</code></td>
|
||||
<td>
|
||||
@if($log->country)
|
||||
<a href="{{ request()->fullUrlWithQuery(['tab'=>'activity','country'=>$log->country]) }}" class="badge bg-secondary text-decoration-none" style="font-size:10px">{{ $log->country }}</a>
|
||||
@else — @endif
|
||||
</td>
|
||||
<td><small class="text-muted text-capitalize">{{ $log->device }}</small></td>
|
||||
<td><small class="text-muted">{{ \Carbon\Carbon::parse($log->created_at)->diffForHumans() }}</small></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="7" class="text-center text-muted py-4">Kayıt bulunamadı</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card-footer">{{ $actLogs->links() }}</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Bots Tab ──────────────────────────────────────────────────────────────── --}}
|
||||
@elseif($tab === 'bots')
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong><i class="bi bi-robot me-1 text-danger"></i>Bot Trafik Analizi</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-dark"><tr><th>Bot / Araç</th><th>Aksiyon</th><th>İstek</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($botStats as $b)
|
||||
<tr>
|
||||
<td><code style="font-size:11px">{{ $b->bot_name }}</code></td>
|
||||
<td>
|
||||
<span class="badge {{ $b->action==='blocked'?'bg-danger':($b->action==='allowed'?'bg-success':'bg-warning text-dark') }}" style="font-size:10px">
|
||||
{{ $b->action }}
|
||||
</span>
|
||||
</td>
|
||||
<td><span class="badge bg-secondary">{{ number_format($b->cnt) }}</span></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="3" class="text-center text-muted py-3">Bot logu bulunamadı</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong><i class="bi bi-geo-alt me-1 text-danger"></i>En Fazla İstek Atan IP'ler (Botlar)</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-dark"><tr><th>IP</th><th>İstek</th><th>Durum</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($topBotIps as $bip)
|
||||
@php $blocked = $blockedIps->firstWhere('ip', $bip->ip); @endphp
|
||||
<tr>
|
||||
<td><code style="font-size:11px">{{ $bip->ip }}</code></td>
|
||||
<td><span class="badge bg-danger">{{ number_format($bip->cnt) }}</span></td>
|
||||
<td>
|
||||
@if($blocked)
|
||||
<span class="badge bg-warning text-dark" style="font-size:10px">Engelli</span>
|
||||
@else
|
||||
<span class="badge bg-secondary" style="font-size:10px">Serbest</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if(!$blocked)
|
||||
<form method="POST" action="{{ route('admin.analytics.block-ip') }}" class="d-inline">
|
||||
@csrf <input type="hidden" name="ip" value="{{ $bip->ip }}">
|
||||
<input type="hidden" name="reason" value="Bot trafiği — otomatik">
|
||||
<button class="btn btn-xs btn-danger py-0 px-1" style="font-size:10px">Engelle</button>
|
||||
</form>
|
||||
@else
|
||||
<form method="POST" action="{{ route('admin.analytics.unblock-ip') }}" class="d-inline">
|
||||
@csrf <input type="hidden" name="ip" value="{{ $bip->ip }}">
|
||||
<button class="btn btn-xs btn-success py-0 px-1" style="font-size:10px">Serbest Bırak</button>
|
||||
</form>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="4" class="text-center text-muted py-3">Bot IP kaydı yok</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header"><strong><i class="bi bi-shield-x me-1 text-warning"></i>Engelli IP'ler ({{ $blockedIps->count() }})</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm mb-0">
|
||||
<thead class="table-dark"><tr><th>IP</th><th>Sebep</th><th>Otomatik</th><th>Engellenme</th><th>Bitiş</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
@forelse($blockedIps as $bi)
|
||||
<tr>
|
||||
<td><code style="font-size:11px">{{ $bi->ip }}</code></td>
|
||||
<td><small class="text-muted">{{ $bi->reason }}</small></td>
|
||||
<td>{{ $bi->auto_blocked ? '<span class="badge bg-warning text-dark" style="font-size:10px">Otomatik</span>' : '<span class="badge bg-secondary" style="font-size:10px">Manuel</span>' }}</td>
|
||||
<td><small class="text-muted">{{ \Carbon\Carbon::parse($bi->blocked_at)->format('d.m.Y H:i') }}</small></td>
|
||||
<td><small class="text-muted">{{ $bi->expires_at ? \Carbon\Carbon::parse($bi->expires_at)->diffForHumans() : 'Kalıcı' }}</small></td>
|
||||
<td>
|
||||
<form method="POST" action="{{ route('admin.analytics.unblock-ip') }}" class="d-inline">
|
||||
@csrf <input type="hidden" name="ip" value="{{ $bi->ip }}">
|
||||
<button class="btn btn-xs btn-outline-success py-0 px-1" style="font-size:10px">Serbest</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="text-center text-muted py-3">Engelli IP yok</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Country Tab ───────────────────────────────────────────────────────────── --}}
|
||||
@elseif($tab === 'country')
|
||||
<div class="card">
|
||||
<div class="card-header"><strong><i class="bi bi-globe me-1"></i>Ülke Bazlı Trafik (Gerçek Kullanıcı)</strong></div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-dark"><tr><th>#</th><th>Ülke</th><th>Görüntüleme</th><th>Benzersiz Kullanıcı</th><th>Oran</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
@php $totalViews = $countries->sum('views'); @endphp
|
||||
@forelse($countries as $i => $c)
|
||||
@php $pct = $totalViews > 0 ? round($c->views/$totalViews*100,1) : 0; @endphp
|
||||
<tr>
|
||||
<td class="text-muted">{{ $i+1 }}</td>
|
||||
<td class="fw-semibold">{{ $c->country ?: 'Bilinmiyor' }}</td>
|
||||
<td>{{ number_format($c->views) }}</td>
|
||||
<td>{{ number_format($c->users) }}</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div style="width:100px;height:6px;background:#333;border-radius:3px;overflow:hidden">
|
||||
<div style="width:{{ $pct }}%;height:100%;background:var(--bs-info)"></div>
|
||||
</div>
|
||||
<small class="text-muted">{{ $pct }}%</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ request()->fullUrlWithQuery(['tab'=>'activity','country'=>$c->country]) }}" class="btn btn-xs btn-outline-info py-0 px-1" style="font-size:10px">Aktivite</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="text-center text-muted py-4">Ülke verisi bulunamadı</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
@if($tab === 'overview')
|
||||
const labels = @json($dailyNew->keys());
|
||||
const data = @json($dailyNew->values());
|
||||
new Chart(document.getElementById('dailyChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Yeni Üye',
|
||||
data,
|
||||
backgroundColor: 'rgba(13,202,240,0.5)',
|
||||
borderColor: 'rgba(13,202,240,1)',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { color: '#333' }, ticks: { color: '#aaa', maxRotation: 45 } },
|
||||
y: { grid: { color: '#333' }, ticks: { color: '#aaa' }, beginAtZero: true },
|
||||
}
|
||||
}
|
||||
});
|
||||
@endif
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,117 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title','Anime İstekleri')
|
||||
@section('page-title','Anime İstekleri')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">Anime İstekleri</h5>
|
||||
</div>
|
||||
{{-- Durum filtreleri --}}
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||
@foreach(['all'=>'Tümü','pending'=>'Bekliyor','approved'=>'Onaylandı','added'=>'Eklendi','rejected'=>'Reddedildi'] as $key=>$lbl)
|
||||
<a href="{{ route('admin.anime-requests.index',['status'=>$key]) }}"
|
||||
class="btn btn-sm {{ $status===$key ? 'btn-primary' : 'btn-secondary' }}">
|
||||
{{ $lbl }}
|
||||
@if(isset($counts[$key]))
|
||||
<span class="badge {{ $status===$key ? 'bg-light text-dark' : 'bg-secondary' }} ms-1">{{ $counts[$key] }}</span>
|
||||
@endif
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px">Anime</th>
|
||||
<th>İsteyen</th>
|
||||
<th style="text-align:center">Oy</th>
|
||||
<th style="text-align:center">Durum</th>
|
||||
<th>Tarih</th>
|
||||
<th style="text-align:right;padding-right:20px">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($requests as $req)
|
||||
@php $s = \App\Models\AnimeRequest::STATUSES[$req->status] ?? ['label'=>$req->status,'color'=>'#8b87a8']; @endphp
|
||||
<tr>
|
||||
<td style="padding-left:20px">
|
||||
<div style="font-size:13.5px;font-weight:700;color:var(--text)">{{ $req->title }}</div>
|
||||
@if($req->original_title)
|
||||
<div style="font-size:12px;color:var(--text2)">{{ $req->original_title }}</div>
|
||||
@endif
|
||||
@if($req->note)
|
||||
<div style="font-size:12px;color:var(--text3);margin-top:3px;font-style:italic">
|
||||
<i class="bi bi-chat-right-text" style="font-size:10px;margin-right:3px"></i>{{ Str::limit($req->note,70) }}
|
||||
</div>
|
||||
@endif
|
||||
@if($req->admin_note)
|
||||
<div style="font-size:12px;color:var(--warning);margin-top:2px">
|
||||
<i class="bi bi-shield" style="font-size:10px;margin-right:3px"></i>{{ $req->admin_note }}
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($req->user)
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text)">{{ $req->user->name }}</div>
|
||||
<div style="font-size:11.5px;color:var(--text2)">{{ $req->user->email }}</div>
|
||||
@else
|
||||
<span style="color:var(--text3)">Silinmiş</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<span class="badge" style="background:var(--accent-dim);color:var(--accent-lt);font-size:12px;padding:4px 10px">
|
||||
<i class="bi bi-heart-fill" style="font-size:10px;margin-right:3px"></i>{{ $req->vote_count }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<span class="badge" style="background:{{ $s['color'] }}22;color:{{ $s['color'] }};border:1px solid {{ $s['color'] }}44">
|
||||
{{ $s['label'] }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="font-size:12.5px;color:var(--text2)">{{ $req->created_at->format('d.m.Y') }}</td>
|
||||
<td style="text-align:right;padding-right:20px">
|
||||
<div class="d-flex gap-1 justify-content-end flex-wrap">
|
||||
@foreach(['approved'=>['Onayla','success'],'added'=>['Eklendi','info'],'rejected'=>['Reddet','danger']] as $st=>[$lbl,$clr])
|
||||
@if($req->status !== $st)
|
||||
<form method="POST" action="{{ route('admin.anime-requests.update',$req) }}">
|
||||
@csrf @method('PATCH')
|
||||
<input type="hidden" name="status" value="{{ $st }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-{{ $clr }}">{{ $lbl }}</button>
|
||||
</form>
|
||||
@endif
|
||||
@endforeach
|
||||
<form method="POST" action="{{ route('admin.anime-requests.destroy',$req) }}"
|
||||
onsubmit="return confirm('Bu istek silinsin mi?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-plus-square"></i>
|
||||
<h6>Bu kategoride istek yok</h6>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
{{ $requests->withQueryString()->links('admin.partials.pagination') }}
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,334 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Anime Ekle')
|
||||
@section('page-title', 'Anime Ekle')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.cover-preview {
|
||||
width:100%; aspect-ratio:2/3; object-fit:cover;
|
||||
border-radius:var(--radius-sm); display:block;
|
||||
}
|
||||
.cover-placeholder {
|
||||
width:100%; aspect-ratio:2/3;
|
||||
background:var(--surface);
|
||||
border:2px dashed var(--border2);
|
||||
border-radius:var(--radius-sm);
|
||||
display:flex; flex-direction:column;
|
||||
align-items:center; justify-content:center;
|
||||
gap:8px; color:var(--text3); font-size:13px;
|
||||
cursor:pointer; transition:border-color .15s, background .15s;
|
||||
}
|
||||
.cover-placeholder:hover { border-color:var(--accent); background:var(--accent-dim); color:var(--accent-lt); }
|
||||
.banner-placeholder {
|
||||
width:100%; aspect-ratio:16/5;
|
||||
background:var(--surface);
|
||||
border:2px dashed var(--border2);
|
||||
border-radius:var(--radius-sm);
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
gap:8px; color:var(--text3); font-size:13px;
|
||||
cursor:pointer; transition:border-color .15s, background .15s;
|
||||
}
|
||||
.banner-placeholder:hover { border-color:var(--accent); background:var(--accent-dim); color:var(--accent-lt); }
|
||||
.section-card {
|
||||
background:var(--bg2);
|
||||
border:1px solid var(--border);
|
||||
border-radius:var(--radius);
|
||||
margin-bottom:16px;
|
||||
overflow:hidden;
|
||||
}
|
||||
.section-header {
|
||||
display:flex; align-items:center; gap:10px;
|
||||
padding:14px 20px;
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
.section-header h6 {
|
||||
font-size:13.5px; font-weight:700;
|
||||
color:var(--text); margin:0;
|
||||
}
|
||||
.section-body { padding:20px; }
|
||||
.genre-chip {
|
||||
display:inline-flex; align-items:center; gap:6px;
|
||||
padding:5px 12px; border-radius:99px;
|
||||
border:1px solid var(--border2);
|
||||
background:transparent;
|
||||
color:var(--text2); font-size:12.5px; font-weight:600;
|
||||
cursor:pointer; transition:all .15s; user-select:none;
|
||||
}
|
||||
.genre-chip input { display:none; }
|
||||
.genre-chip:has(input:checked) {
|
||||
background:var(--accent-dim);
|
||||
border-color:var(--accent);
|
||||
color:var(--accent-lt);
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Breadcrumb --}}
|
||||
<nav class="mb-4">
|
||||
<a href="{{ route('admin.animes.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Animelere Dön
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<form method="POST" action="{{ route('admin.animes.store') }}" enctype="multipart/form-data" id="anime-form">
|
||||
@csrf
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{{-- ── Sol: Form Alanları ── --}}
|
||||
<div class="col-xl-8 col-lg-7">
|
||||
|
||||
{{-- Temel Bilgiler --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-info-circle" style="color:var(--accent-lt)"></i>
|
||||
<h6>Temel Bilgiler</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Türkçe Başlık <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" name="title" class="form-control form-control-lg" value="{{ old('title') }}" placeholder="Anime adını girin..." required style="font-size:15px;font-weight:600">
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">İngilizce Başlık</label>
|
||||
<input type="text" name="title_en" class="form-control" value="{{ old('title_en') }}" placeholder="English title...">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Japonca Başlık</label>
|
||||
<input type="text" name="title_jp" class="form-control" value="{{ old('title_jp') }}" placeholder="日本語タイトル...">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Açıklama</label>
|
||||
<textarea name="description" class="form-control" rows="5" placeholder="Anime hakkında kısa açıklama...">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Detaylar --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-sliders" style="color:var(--info)"></i>
|
||||
<h6>Detaylar</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Tip <span style="color:var(--danger)">*</span></label>
|
||||
<select name="type" class="form-select" required>
|
||||
<option value="series" {{ old('type')=='series' ?'selected':'' }}>Dizi (Series)</option>
|
||||
<option value="movie" {{ old('type')=='movie' ?'selected':'' }}>Film (Movie)</option>
|
||||
<option value="ova" {{ old('type')=='ova' ?'selected':'' }}>OVA</option>
|
||||
<option value="ona" {{ old('type')=='ona' ?'selected':'' }}>ONA</option>
|
||||
<option value="special" {{ old('type')=='special' ?'selected':'' }}>Special</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Durum <span style="color:var(--danger)">*</span></label>
|
||||
<select name="status" class="form-select" required>
|
||||
<option value="ongoing" {{ old('status','ongoing')=='ongoing' ?'selected':'' }}>Devam Ediyor</option>
|
||||
<option value="completed" {{ old('status')=='completed' ?'selected':'' }}>Tamamlandı</option>
|
||||
<option value="upcoming" {{ old('status')=='upcoming' ?'selected':'' }}>Yakında</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Yayın Yılı</label>
|
||||
<input type="number" name="release_year" class="form-control" value="{{ old('release_year', date('Y')) }}" min="1900" max="2099">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Stüdyo</label>
|
||||
<input type="text" name="studio" class="form-control" value="{{ old('studio') }}" placeholder="Örn: MAPPA, WIT Studio...">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">MyAnimeList ID</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-link-45deg"></i></span>
|
||||
<input type="number" name="mal_id" class="form-control" value="{{ old('mal_id') }}" placeholder="Örn: 16498">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Fragman URL</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-youtube" style="color:#ff0000"></i></span>
|
||||
<input type="url" name="trailer_url" class="form-control" value="{{ old('trailer_url') }}" placeholder="https://youtube.com/watch?v=...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Türler --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-tags" style="color:var(--warning)"></i>
|
||||
<h6>Türler</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
@foreach($genres as $genre)
|
||||
<label class="genre-chip">
|
||||
<input type="checkbox" name="genres[]" value="{{ $genre->id }}"
|
||||
{{ in_array($genre->id, old('genres', [])) ? 'checked' : '' }}>
|
||||
{{ $genre->name }}
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- İzin Ayarları --}}
|
||||
@if(isset($permissions) && count($permissions))
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-shield-lock" style="color:var(--success)"></i>
|
||||
<h6>İzin Ayarları</h6>
|
||||
<span style="font-size:12px;color:var(--text2);margin-left:4px">— Bu anime için özel izinler</span>
|
||||
</div>
|
||||
<div class="section-body p-0">
|
||||
@foreach($permissions as $perm)
|
||||
<div class="perm-row">
|
||||
<div>
|
||||
<div class="perm-name">{{ $perm->label }}</div>
|
||||
@if($perm->description)
|
||||
<div class="perm-desc">{{ $perm->description }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="perm-toggle">
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]"
|
||||
id="perm_{{ $perm->key }}_free" value="free" checked>
|
||||
<label for="perm_{{ $perm->key }}_free" class="free">Ücretsiz</label>
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]"
|
||||
id="perm_{{ $perm->key }}_prem" value="premium">
|
||||
<label for="perm_{{ $perm->key }}_prem" class="premium">Premium</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
{{-- ── Sağ: Görsel & Ayarlar ── --}}
|
||||
<div class="col-xl-4 col-lg-5">
|
||||
|
||||
{{-- Kapak Görseli --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-image" style="color:var(--info)"></i>
|
||||
<h6>Kapak Görseli</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<input type="file" name="cover_image" id="cover-input" accept="image/*"
|
||||
style="display:none" onchange="previewCover(this)">
|
||||
<div onclick="document.getElementById('cover-input').click()" style="cursor:pointer">
|
||||
<img id="cover-preview" class="cover-preview" src="" alt="" style="display:none">
|
||||
<div class="cover-placeholder" id="cover-ph">
|
||||
<i class="bi bi-cloud-upload" style="font-size:26px"></i>
|
||||
<span>Kapak yükle</span>
|
||||
<span style="font-size:11px;color:var(--text3)">Önerilen: 300×450px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Banner Görseli --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-panorama" style="color:var(--info)"></i>
|
||||
<h6>Banner Görseli</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<input type="file" name="banner_image" id="banner-input" accept="image/*"
|
||||
style="display:none" onchange="previewBanner(this)">
|
||||
<div onclick="document.getElementById('banner-input').click()" style="cursor:pointer">
|
||||
<img id="banner-preview" style="width:100%;aspect-ratio:16/5;object-fit:cover;border-radius:var(--radius-sm);display:none" alt="">
|
||||
<div class="banner-placeholder" id="banner-ph">
|
||||
<i class="bi bi-cloud-upload" style="font-size:22px"></i>
|
||||
<span>Banner yükle</span>
|
||||
<span style="font-size:11px;color:var(--text3)">Önerilen: 1280×720px</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Yayın Ayarları --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-toggle-on" style="color:var(--success)"></i>
|
||||
<h6>Yayın Ayarları</h6>
|
||||
</div>
|
||||
<div class="section-body d-flex flex-column gap-3">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Yayınla</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Kullanıcılara görünür yap</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_published" value="1"
|
||||
id="is_published" role="switch">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Öne Çıkar</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Ana sayfada öne çıkar</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_featured" value="1"
|
||||
id="is_featured" role="switch">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Türkçe Dublaj</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Ana sayfada Dublaj bölümünde göster</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_dubbed" value="1"
|
||||
id="is_dubbed" role="switch">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Kaydet --}}
|
||||
<button type="submit" class="btn btn-primary w-100 btn-lg">
|
||||
<i class="bi bi-plus-circle"></i> Anime Ekle
|
||||
</button>
|
||||
<a href="{{ route('admin.animes.index') }}" class="btn btn-secondary w-100 mt-2">
|
||||
İptal
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function previewCover(input) {
|
||||
if (!input.files[0]) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
document.getElementById('cover-preview').src = e.target.result;
|
||||
document.getElementById('cover-preview').style.display = 'block';
|
||||
document.getElementById('cover-ph').style.display = 'none';
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
function previewBanner(input) {
|
||||
if (!input.files[0]) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
document.getElementById('banner-preview').src = e.target.result;
|
||||
document.getElementById('banner-preview').style.display = 'block';
|
||||
document.getElementById('banner-ph').style.display = 'none';
|
||||
};
|
||||
reader.readAsDataURL(input.files[0]);
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,369 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Düzenle: ' . $anime->title)
|
||||
@section('page-title', $anime->title)
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.section-card { background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius); margin-bottom:16px; overflow:hidden; }
|
||||
.section-header { display:flex; align-items:center; gap:10px; padding:14px 20px; border-bottom:1px solid var(--border); }
|
||||
.section-header h6 { font-size:13.5px; font-weight:700; color:var(--text); margin:0; }
|
||||
.section-body { padding:20px; }
|
||||
.ai-banner {
|
||||
display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px;
|
||||
padding:14px 18px;
|
||||
background:linear-gradient(135deg,rgba(6,182,212,0.06),rgba(124,58,237,0.06));
|
||||
border:1px solid rgba(6,182,212,0.2);
|
||||
border-radius:var(--radius-sm);
|
||||
margin-bottom:16px;
|
||||
}
|
||||
.genre-chip { display:inline-flex; align-items:center; gap:6px; padding:5px 12px; border-radius:99px; border:1px solid var(--border2); background:transparent; color:var(--text2); font-size:12.5px; font-weight:600; cursor:pointer; transition:all .15s; user-select:none; }
|
||||
.genre-chip input { display:none; }
|
||||
.genre-chip:has(input:checked) { background:var(--accent-dim); border-color:var(--accent); color:var(--accent-lt); }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-4 flex-wrap">
|
||||
<a href="{{ route('admin.animes.show', $anime) }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Geri Dön
|
||||
</a>
|
||||
<div style="font-size:13px;color:var(--text2)">
|
||||
Son güncelleme: {{ $anime->updated_at->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- AI Banner --}}
|
||||
<div class="ai-banner">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:700;color:var(--info);display:flex;align-items:center;gap:7px">
|
||||
<i class="bi bi-stars"></i> AI ile Tüm Boşları Doldur
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text2);margin-top:2px">
|
||||
Açıklama, yıl, stüdyo, tür, durum ve puan bilgilerini DeepSeek'ten çeker. Yalnızca boş alanları doldurur.
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-info btn-sm" id="ai-meta-btn" onclick="aiAutoFill()">
|
||||
<i class="bi bi-magic"></i> Tümünü Doldur
|
||||
</button>
|
||||
</div>
|
||||
<div id="ai-meta-status" style="display:none;font-size:13px;padding:8px 12px;border-radius:8px;margin-bottom:12px"></div>
|
||||
|
||||
<form method="POST" action="{{ route('admin.animes.update', $anime) }}" enctype="multipart/form-data">
|
||||
@csrf @method('PUT')
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-8 col-lg-7">
|
||||
|
||||
{{-- Temel Bilgiler --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-info-circle" style="color:var(--accent-lt)"></i>
|
||||
<h6>Temel Bilgiler</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Türkçe Başlık <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" name="title" class="form-control" value="{{ old('title', $anime->title) }}" required style="font-size:15px;font-weight:600">
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">İngilizce Başlık</label>
|
||||
<input type="text" name="title_en" class="form-control" value="{{ old('title_en', $anime->title_en) }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Japonca Başlık</label>
|
||||
<input type="text" name="title_jp" class="form-control" value="{{ old('title_jp', $anime->title_jp) }}">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0">Açıklama</label>
|
||||
<button type="button" class="btn btn-sm btn-secondary" id="ai-anime-btn" onclick="aiGenerateAnime()">
|
||||
<i class="bi bi-stars" style="color:var(--info)"></i> AI ile Yaz
|
||||
</button>
|
||||
</div>
|
||||
<textarea name="description" id="anime-description" class="form-control" rows="5">{{ old('description', $anime->description) }}</textarea>
|
||||
<div id="ai-anime-status" class="form-text" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Detaylar --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-sliders" style="color:var(--info)"></i>
|
||||
<h6>Detaylar</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Tip</label>
|
||||
<select name="type" class="form-select">
|
||||
@foreach(['series'=>'Dizi','movie'=>'Film','ova'=>'OVA','ona'=>'ONA','special'=>'Special'] as $v => $l)
|
||||
<option value="{{ $v }}" {{ old('type',$anime->type)===$v?'selected':'' }}>{{ $l }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Durum</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="ongoing" {{ old('status',$anime->status)==='ongoing' ?'selected':'' }}>Devam Ediyor</option>
|
||||
<option value="completed" {{ old('status',$anime->status)==='completed' ?'selected':'' }}>Tamamlandı</option>
|
||||
<option value="upcoming" {{ old('status',$anime->status)==='upcoming' ?'selected':'' }}>Yakında</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Yayın Yılı</label>
|
||||
<input type="number" name="release_year" class="form-control" value="{{ old('release_year',$anime->release_year) }}" min="1900" max="2099">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Stüdyo</label>
|
||||
<input type="text" name="studio" class="form-control" value="{{ old('studio',$anime->studio) }}" placeholder="MAPPA, WIT Studio...">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Puan (0–10)</label>
|
||||
<input type="number" name="rating" class="form-control" step="0.1" min="0" max="10" value="{{ old('rating',$anime->rating) }}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">
|
||||
MAL ID
|
||||
@foreach($anime->seasons as $s)
|
||||
@if($s->mal_id)<span class="badge bg-success ms-1" style="font-size:10px">S{{ $s->season_number }}:{{ $s->mal_id }}</span>@endif
|
||||
@endforeach
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="text" name="mal_id" id="mal_id_input" class="form-control"
|
||||
value="{{ old('mal_id',$anime->mal_id) }}" placeholder="örn: 16498">
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="btn-fetch-mal"
|
||||
onclick="fetchMalSeasons()" title="MAL ID kaydet + sezon ID'lerini Jikan'dan çek">
|
||||
<i class="bi bi-magic"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="mal-fetch-result" class="form-text">
|
||||
@if(!$anime->mal_id)
|
||||
<span style="color:var(--warning)">⚠ MAL ID girilmemiş — İntro Atla çalışmaz</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Fragman URL</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="bi bi-youtube" style="color:#ff0000"></i></span>
|
||||
<input type="url" name="trailer_url" class="form-control" value="{{ old('trailer_url',$anime->trailer_url) }}" placeholder="https://youtube.com/...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Türler --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-tags" style="color:var(--warning)"></i>
|
||||
<h6>Türler</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
@foreach($genres as $genre)
|
||||
<label class="genre-chip">
|
||||
<input type="checkbox" name="genres[]" value="{{ $genre->id }}"
|
||||
{{ $anime->genres->contains($genre->id) ? 'checked' : '' }}>
|
||||
{{ $genre->name }}
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- İzinler --}}
|
||||
@if(isset($permissions) && count($permissions))
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-shield-lock" style="color:var(--success)"></i>
|
||||
<h6>İzin Ayarları</h6>
|
||||
<span style="font-size:12px;color:var(--text2)">— Global ayarları override eder</span>
|
||||
</div>
|
||||
<div class="section-body p-0">
|
||||
@foreach($permissions as $perm)
|
||||
@php $current = $contentPerms[$perm->key] ?? 'free'; @endphp
|
||||
<div class="perm-row">
|
||||
<div>
|
||||
<div class="perm-name">{{ $perm->label }}</div>
|
||||
@if($perm->description)<div class="perm-desc">{{ $perm->description }}</div>@endif
|
||||
</div>
|
||||
<div class="perm-toggle">
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]" id="ep_{{ $perm->key }}_free" value="free" {{ $current==='free'?'checked':'' }}>
|
||||
<label for="ep_{{ $perm->key }}_free" class="free">Ücretsiz</label>
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]" id="ep_{{ $perm->key }}_prem" value="premium" {{ $current==='premium'?'checked':'' }}>
|
||||
<label for="ep_{{ $perm->key }}_prem" class="premium">Premium</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
{{-- ── Sağ Kolon ── --}}
|
||||
<div class="col-xl-4 col-lg-5">
|
||||
|
||||
{{-- Kapak --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-image" style="color:var(--info)"></i>
|
||||
<h6>Kapak Görseli</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
@if($anime->coverUrl)
|
||||
<img src="{{ $anime->coverUrl }}" alt=""
|
||||
style="width:100%;aspect-ratio:2/3;object-fit:cover;border-radius:var(--radius-sm);margin-bottom:12px">
|
||||
@endif
|
||||
<label style="font-size:12px;color:var(--text2);margin-bottom:6px;display:block">
|
||||
{{ $anime->coverUrl ? 'Yeni görsel yükle (mevcut üzerine yazar)' : 'Kapak görseli yükle' }}
|
||||
</label>
|
||||
<input type="file" name="cover_image" class="form-control" accept="image/*">
|
||||
<div class="form-text">Önerilen: 300×450px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Banner --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-panorama" style="color:var(--info)"></i>
|
||||
<h6>Banner Görseli</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<input type="file" name="banner_image" class="form-control" accept="image/*">
|
||||
<div class="form-text">Önerilen: 1280×720px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Yayın --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-toggle-on" style="color:var(--success)"></i>
|
||||
<h6>Yayın Ayarları</h6>
|
||||
</div>
|
||||
<div class="section-body d-flex flex-column gap-3">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Yayınla</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Kullanıcılara görünür</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_published" value="1"
|
||||
id="is_published" role="switch" {{ $anime->is_published ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Öne Çıkar</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Ana sayfada öne çıkar</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_featured" value="1"
|
||||
id="is_featured" role="switch" {{ $anime->is_featured ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Türkçe Dublaj</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Ana sayfada Dublaj bölümünde göster</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_dubbed" value="1"
|
||||
id="is_dubbed" role="switch" {{ $anime->is_dubbed ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100 btn-lg">
|
||||
<i class="bi bi-floppy"></i> Değişiklikleri Kaydet
|
||||
</button>
|
||||
<a href="{{ route('admin.animes.show', $anime) }}" class="btn btn-secondary w-100 mt-2">İptal</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const AI_META_URL = '{{ route("admin.ai.anime-meta") }}';
|
||||
const AI_GEN_URL = '{{ route("admin.ai.generate") }}';
|
||||
const CSRF_TOKEN = document.querySelector('meta[name=csrf-token]')?.content || '';
|
||||
|
||||
async function aiAutoFill() {
|
||||
const btn = document.getElementById('ai-meta-btn');
|
||||
const status = document.getElementById('ai-meta-status');
|
||||
const title = document.querySelector('[name="title"]')?.value?.trim();
|
||||
if (!title) { axToast('Önce anime başlığını girin.', 'warning'); return; }
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" style="width:12px;height:12px"></span> Sorgulanıyor...';
|
||||
status.style.display = 'block';
|
||||
status.style.background = 'var(--info-dim)';
|
||||
status.style.color = 'var(--info)';
|
||||
status.textContent = 'DeepSeek sorgulanıyor, lütfen bekleyin (~5-10 sn)...';
|
||||
|
||||
try {
|
||||
const res = await fetch(AI_META_URL, { method:'POST', headers:{'Content-Type':'application/json','X-CSRF-TOKEN':CSRF_TOKEN}, body:JSON.stringify({ title, title_jp: document.querySelector('[name="title_jp"]')?.value?.trim()||'' }) });
|
||||
const data = await res.json();
|
||||
if (!res.ok || data.error) { status.style.background='var(--danger-dim)'; status.style.color='var(--danger)'; status.textContent='❌ '+(data.error||'Hata'); return; }
|
||||
|
||||
const m = data.meta; let filled = [];
|
||||
const fillF = (sel, val) => { if (!val) return; const el = document.querySelector(sel); if (el && !el.value?.trim()) { el.value = val; filled.push(el.name||sel); } };
|
||||
fillF('[name="description"]',m.description); fillF('[name="title_en"]',m.title_en); fillF('[name="title_jp"]',m.title_jp);
|
||||
fillF('[name="studio"]',m.studio); fillF('[name="release_year"]',m.release_year); fillF('[name="rating"]',m.rating); fillF('[name="trailer_url"]',m.trailer_url);
|
||||
const fillS = (sel, val) => { if (!val) return; const el = document.querySelector(sel); if (!el) return; const opt = [...el.options].find(o => o.value===val); if (opt) { el.value=val; filled.push(el.name); } };
|
||||
fillS('[name="type"]',m.type); fillS('[name="status"]',m.status);
|
||||
if (Array.isArray(m.genres) && m.genres.length) {
|
||||
document.querySelectorAll('[name="genres[]"]').forEach(cb => {
|
||||
const lbl = cb.closest('label')?.textContent?.trim()?.toLowerCase();
|
||||
if (!lbl) return;
|
||||
if (m.genres.some(g => lbl.includes(g.toLowerCase()) || g.toLowerCase().includes(lbl)) && !cb.checked) { cb.checked=true; filled.push('genre'); }
|
||||
});
|
||||
}
|
||||
status.style.background='var(--success-dim)'; status.style.color='var(--success)';
|
||||
status.textContent = `✓ ${filled.length} alan dolduruldu. Kontrol edip kaydedin.`;
|
||||
} catch(e) { status.style.background='var(--danger-dim)'; status.style.color='var(--danger)'; status.textContent='❌ Bağlantı hatası: '+e.message; }
|
||||
finally { btn.disabled=false; btn.innerHTML='<i class="bi bi-magic"></i> Tümünü Doldur'; }
|
||||
}
|
||||
|
||||
async function aiGenerateAnime() {
|
||||
const btn = document.getElementById('ai-anime-btn');
|
||||
const status = document.getElementById('ai-anime-status');
|
||||
const ta = document.getElementById('anime-description');
|
||||
const title = document.querySelector('[name="title"]')?.value?.trim();
|
||||
if (!title) { axToast('Önce başlık girin.', 'warning'); return; }
|
||||
|
||||
btn.disabled=true; btn.innerHTML='<span class="spinner-border spinner-border-sm" style="width:12px;height:12px"></span>';
|
||||
status.style.display='block'; status.className='form-text'; status.style.color='var(--info)'; status.textContent='AI yazıyor...';
|
||||
try {
|
||||
const res = await fetch(AI_GEN_URL, { method:'POST', headers:{'Content-Type':'application/json','X-CSRF-TOKEN':CSRF_TOKEN}, body:JSON.stringify({ type:'anime', title, title_jp: document.querySelector('[name="title_jp"]')?.value?.trim(), genres:[...document.querySelectorAll('[name="genres[]"]:checked')].map(el=>el.closest('label')?.textContent?.trim()||el.value).join(', ') }) });
|
||||
const data = await res.json();
|
||||
if (data.error) { status.style.color='var(--danger)'; status.textContent='❌ '+data.error; }
|
||||
else { ta.value=data.description; status.style.color='var(--success)'; status.textContent='✓ Açıklama oluşturuldu.'; }
|
||||
} catch(e) { status.style.color='var(--danger)'; status.textContent='❌ Hata.'; }
|
||||
finally { btn.disabled=false; btn.innerHTML='<i class="bi bi-stars" style="color:var(--info)"></i> AI ile Yaz'; }
|
||||
}
|
||||
|
||||
async function fetchMalSeasons() {
|
||||
const btn=document.getElementById('btn-fetch-mal'); const result=document.getElementById('mal-fetch-result');
|
||||
const malId=document.getElementById('mal_id_input').value.trim();
|
||||
if (!malId) { result.innerHTML='<span style="color:var(--warning)">⚠ MAL ID girin.</span>'; return; }
|
||||
btn.disabled=true; btn.innerHTML='<span class="spinner-border spinner-border-sm" style="width:12px;height:12px"></span>';
|
||||
result.innerHTML='<span style="color:var(--text2)">Jikan API\'ye bağlanılıyor...</span>';
|
||||
try {
|
||||
await fetch('{{ route("admin.animes.update", $anime) }}', { method:'POST', headers:{'X-CSRF-TOKEN':CSRF_TOKEN,'Accept':'application/json','Content-Type':'application/x-www-form-urlencoded'}, body:new URLSearchParams({_method:'PUT',mal_id:malId}) });
|
||||
const res = await fetch('{{ route("admin.animes.fetch-mal-seasons", $anime) }}', { method:'POST', headers:{'X-CSRF-TOKEN':CSRF_TOKEN,'Accept':'application/json'} });
|
||||
const data = await res.json();
|
||||
if (data.success) { const rows=data.updated.map(u=>`S${u.season}→${u.mal_id}`).join(', '); result.innerHTML=`<span style="color:var(--success)">✓ ${data.message}${rows?' ('+rows+')':''}</span>`; }
|
||||
else { result.innerHTML=`<span style="color:var(--danger)">❌ ${data.error}</span>`; }
|
||||
} catch(e) { result.innerHTML='<span style="color:var(--danger)">❌ Hata.</span>'; }
|
||||
finally { btn.disabled=false; btn.innerHTML='<i class="bi bi-magic"></i>'; }
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,488 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Animeler')
|
||||
@section('page-title', 'Animeler')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.anime-cover {
|
||||
width: 40px; height: 58px;
|
||||
object-fit: cover;
|
||||
border-radius: 6px;
|
||||
background: var(--surface);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.anime-cover-placeholder {
|
||||
width: 40px; height: 58px;
|
||||
border-radius: 6px;
|
||||
background: var(--surface);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--text3);
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.anime-title { font-size: 13.5px; font-weight: 600; color: var(--text); }
|
||||
.anime-genres { font-size: 12px; color: var(--text2); margin-top: 2px; }
|
||||
|
||||
.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; }
|
||||
.bulk-bar-count { font-size: 13px; font-weight: 700; color: var(--accent-lt); }
|
||||
|
||||
.mal-progress-card {
|
||||
display: none;
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border2);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.mal-progress-card.on { display: block; }
|
||||
.mal-log {
|
||||
font-size: 12px;
|
||||
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
||||
color: var(--text2);
|
||||
max-height: 130px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
margin-top: 10px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* Action dropdown */
|
||||
.act-drop { position: relative; }
|
||||
.act-drop-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0; top: calc(100% + 4px);
|
||||
min-width: 160px;
|
||||
background: var(--bg2);
|
||||
border: 1px solid var(--border2);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 5px;
|
||||
z-index: 200;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
.act-drop-menu.on { display: block; }
|
||||
.act-drop-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 11px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
transition: all 0.12s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.act-drop-item:hover { background: rgba(255,255,255,0.06); color: var(--text); }
|
||||
.act-drop-item.danger:hover { background: var(--danger-dim); color: var(--danger); }
|
||||
.act-drop-sep { height: 1px; background: var(--border); margin: 4px 0; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- ── Page Header ── --}}
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">
|
||||
Animeler
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text2);margin-left:6px">{{ number_format($animes->total()) }} toplam</span>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
@if($zeroEpisodeCount > 0)
|
||||
<button class="btn btn-outline-warning btn-sm" onclick="destroyZeroEpisodes()" id="btn-zero-ep">
|
||||
<i class="bi bi-trash3"></i>
|
||||
<span class="d-none d-md-inline">0 Bölümlü Sil</span>
|
||||
<span class="badge bg-warning" style="color:#000">{{ $zeroEpisodeCount }}</span>
|
||||
</button>
|
||||
@endif
|
||||
<button class="btn btn-secondary btn-sm" onclick="bulkFindMal()" id="btn-bulk-mal">
|
||||
<i class="bi bi-magic"></i>
|
||||
<span class="d-none d-md-inline">MAL ID Otomatik Bul</span>
|
||||
</button>
|
||||
<a href="{{ route('admin.animes.create') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg"></i> Anime Ekle
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── MAL Progress Card ── --}}
|
||||
<div class="mal-progress-card" id="bulk-mal-box">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div style="font-size:14px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:8px">
|
||||
<i class="bi bi-magic" style="color:var(--info)"></i> MAL ID Otomatik Doldurma
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="stopBulkMal()"><i class="bi bi-stop-fill"></i> Durdur</button>
|
||||
</div>
|
||||
<div style="background:var(--bg);border-radius:99px;height:6px;overflow:hidden">
|
||||
<div id="bulk-mal-bar" style="height:100%;width:0%;background:linear-gradient(90deg,var(--accent),var(--accent-lt));border-radius:99px;transition:width .3s"></div>
|
||||
</div>
|
||||
<div class="mal-log" id="bulk-mal-log"></div>
|
||||
</div>
|
||||
|
||||
{{-- ── Filters ── --}}
|
||||
<div class="filter-card mb-3">
|
||||
<form method="GET" class="d-flex flex-wrap gap-2 align-items-end">
|
||||
<div class="search-input-wrap flex-grow-1" style="min-width:180px;max-width:320px">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" name="search" class="form-control" placeholder="Anime 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="ongoing" {{ request('status')=='ongoing' ?'selected':'' }}>Devam Ediyor</option>
|
||||
<option value="completed" {{ request('status')=='completed' ?'selected':'' }}>Tamamlandı</option>
|
||||
<option value="upcoming" {{ request('status')=='upcoming' ?'selected':'' }}>Yakında</option>
|
||||
</select>
|
||||
<select name="type" class="form-select" style="width:auto;min-width:120px">
|
||||
<option value="">Tüm Tipler</option>
|
||||
<option value="series" {{ request('type')=='series'?'selected':'' }}>Dizi</option>
|
||||
<option value="movie" {{ request('type')=='movie' ?'selected':'' }}>Film</option>
|
||||
<option value="ova" {{ request('type')=='ova' ?'selected':'' }}>OVA</option>
|
||||
</select>
|
||||
<select name="no_episodes" class="form-select" style="width:auto;min-width:150px">
|
||||
<option value="">Tüm Animeler</option>
|
||||
<option value="1" {{ request('no_episodes')=='1'?'selected':'' }}>Sadece 0 Bölümlüler</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.animes.index') }}" class="btn btn-secondary btn-sm"><i class="bi bi-x-lg"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- ── Bulk Selection Bar ── --}}
|
||||
<div class="bulk-bar" id="bulk-bar">
|
||||
<span class="bulk-bar-count" id="bulk-count">0 anime seçildi</span>
|
||||
<span class="badge bg-info d-none" id="bulk-all-note" style="color:#000">Tüm sayfalardaki animeler</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ç ({{ $animes->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</th>
|
||||
<th>Tip / Durum</th>
|
||||
<th>MAL ID</th>
|
||||
<th style="text-align:center">Bölüm</th>
|
||||
<th style="text-align:center">Yayın</th>
|
||||
<th style="text-align:right;padding-right:20px">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($animes as $anime)
|
||||
<tr id="anime-row-{{ $anime->id }}">
|
||||
<td style="padding-left:20px">
|
||||
<input type="checkbox" class="form-check-input chk-item" value="{{ $anime->id }}" onchange="onCheck()">
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
@if($anime->coverUrl)
|
||||
<img src="{{ $anime->coverUrl }}" class="anime-cover" alt="">
|
||||
@else
|
||||
<div class="anime-cover-placeholder"><i class="bi bi-image"></i></div>
|
||||
@endif
|
||||
<div class="overflow-hidden">
|
||||
<div class="anime-title text-truncate" style="max-width:260px">{{ $anime->title }}</div>
|
||||
<div class="anime-genres">{{ $anime->genres->pluck('name')->join(', ') ?: '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-column gap-1">
|
||||
<span class="badge bg-secondary" style="width:fit-content">{{ strtoupper($anime->type) }}</span>
|
||||
@if($anime->status === 'ongoing')
|
||||
<span class="badge bg-success" style="width:fit-content">Devam Ediyor</span>
|
||||
@elseif($anime->status === 'completed')
|
||||
<span class="badge bg-info" style="width:fit-content;color:#000">Tamamlandı</span>
|
||||
@else
|
||||
<span class="badge bg-warning" style="width:fit-content;color:#000">Yakında</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="mal-id-cell" data-id="{{ $anime->id }}" data-fetch-url="{{ route('admin.animes.fetch-mal', $anime) }}">
|
||||
@if($anime->mal_id)
|
||||
<a href="https://myanimelist.net/anime/{{ $anime->mal_id }}" target="_blank"
|
||||
class="badge bg-success text-decoration-none mal-badge-{{ $anime->id }}" style="color:#fff">
|
||||
<i class="bi bi-box-arrow-up-right" style="font-size:10px;margin-right:3px"></i>{{ $anime->mal_id }}
|
||||
</a>
|
||||
@else
|
||||
<span class="mal-badge-{{ $anime->id }}" style="display:inline-flex;align-items:center;gap:5px">
|
||||
<span class="badge bg-danger">Eksik</span>
|
||||
<button class="btn btn-link btn-sm p-0" style="font-size:12px;color:var(--info)" title="Otomatik Bul"
|
||||
onclick="fetchSingleMal({{ $anime->id }}, this)">
|
||||
<i class="bi bi-magic"></i>
|
||||
</button>
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<span style="font-size:14px;font-weight:700;color:{{ $anime->episode_count > 0 ? 'var(--text)' : 'var(--danger)' }}">
|
||||
{{ $anime->episode_count }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
@if($anime->is_published)
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:var(--success)">
|
||||
<i class="bi bi-circle-fill" style="font-size:7px"></i> Yayında
|
||||
</span>
|
||||
@else
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:var(--text3)">
|
||||
<i class="bi bi-circle" style="font-size:7px"></i> Taslak
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align:right;padding-right:20px">
|
||||
<div class="act-drop">
|
||||
<button class="btn btn-secondary btn-sm" onclick="toggleDrop(this)">
|
||||
<i class="bi bi-three-dots"></i>
|
||||
</button>
|
||||
<div class="act-drop-menu">
|
||||
<a href="{{ route('admin.animes.show', $anime) }}" class="act-drop-item">
|
||||
<i class="bi bi-eye"></i> Görüntüle
|
||||
</a>
|
||||
<a href="{{ route('admin.animes.edit', $anime) }}" class="act-drop-item">
|
||||
<i class="bi bi-pencil"></i> Düzenle
|
||||
</a>
|
||||
<a href="{{ route('admin.episodes.index', ['anime_id' => $anime->id]) }}" class="act-drop-item">
|
||||
<i class="bi bi-collection-play"></i> Bölümler
|
||||
</a>
|
||||
<div class="act-drop-sep"></div>
|
||||
<form method="POST" action="{{ route('admin.animes.destroy', $anime) }}"
|
||||
onsubmit="return confirm('{{ addslashes($anime->title) }} silinsin mi?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="act-drop-item danger">
|
||||
<i class="bi bi-trash3"></i> Sil
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-play-circle"></i>
|
||||
<h6>Anime bulunamadı</h6>
|
||||
<p>Arama kriterlerinizi değiştirin veya yeni anime ekleyin.</p>
|
||||
<a href="{{ route('admin.animes.create') }}" class="btn btn-primary btn-sm mt-2">
|
||||
<i class="bi bi-plus-lg"></i> Anime Ekle
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Pagination ── --}}
|
||||
<div class="mt-3">
|
||||
{{ $animes->links('admin.partials.pagination') }}
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const BULK_MAL_URL = '{{ route("admin.animes.bulk-find-mal") }}';
|
||||
const BULK_DESTROY_URL = '{{ route("admin.animes.bulk-destroy") }}';
|
||||
const ZERO_DESTROY_URL = '{{ route("admin.animes.destroy-zero-episodes") }}';
|
||||
const TOTAL_ANIMES = {{ $animes->total() }};
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content || '';
|
||||
|
||||
/* ── Action dropdown ── */
|
||||
function toggleDrop(btn) {
|
||||
const menu = btn.nextElementSibling;
|
||||
const wasOn = menu.classList.contains('on');
|
||||
document.querySelectorAll('.act-drop-menu.on').forEach(m => m.classList.remove('on'));
|
||||
if (!wasOn) menu.classList.add('on');
|
||||
}
|
||||
document.addEventListener('click', e => {
|
||||
if (!e.target.closest('.act-drop')) {
|
||||
document.querySelectorAll('.act-drop-menu.on').forEach(m => m.classList.remove('on'));
|
||||
}
|
||||
});
|
||||
|
||||
/* ── Zero episode delete ── */
|
||||
async function destroyZeroEpisodes() {
|
||||
if (!confirm('0 bölümlü tüm animeler silinsin mi? Bu işlem geri alınamaz.')) return;
|
||||
const btn = document.getElementById('btn-zero-ep');
|
||||
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span>'; }
|
||||
try {
|
||||
const res = await fetch(ZERO_DESTROY_URL, { method:'POST', headers:{'X-CSRF-TOKEN':CSRF,'Accept':'application/json'} });
|
||||
const d = await res.json();
|
||||
if (d.success) { axToast(`${d.count} anime silindi.`, 'success'); setTimeout(() => location.reload(), 1200); }
|
||||
else { axToast('Hata oluştu.', 'error'); if (btn) btn.disabled = false; }
|
||||
} catch(e) { axToast('Hata: ' + e.message, 'error'); if (btn) btn.disabled = false; }
|
||||
}
|
||||
|
||||
/* ── Bulk selection ── */
|
||||
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_ANIMES} anime seçildi`;
|
||||
bar.classList.add('on'); _selBtns(false);
|
||||
} else if (chk.length > 0) {
|
||||
cnt.textContent = `${chk.length} anime 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_ANIMES} animeyi` : `${chk.length} animeyi`;
|
||||
if (!confirm(`${label} silmek istediğinize emin misiniz?\nBu işlem CDN dosyalarını da siler!`)) return;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const body = _selAll
|
||||
? { all: true, filters: { search: params.get('search')||'', status: params.get('status')||'', type: params.get('type')||'' } }
|
||||
: { 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(`anime-row-${id}`)?.remove());
|
||||
clearSel();
|
||||
axToast(`${chk.length} anime silindi.`, 'success');
|
||||
} else { axToast('Hata: ' + (d.message || '?'), 'error'); }
|
||||
} catch(e) { axToast('Hata: ' + e.message, 'error'); }
|
||||
}
|
||||
|
||||
/* ── Bulk MAL find ── */
|
||||
let bulkRunning = false, skipIds = [], totalDone = 0, totalInitial = 0;
|
||||
|
||||
async function bulkFindMal() {
|
||||
if (bulkRunning) return;
|
||||
bulkRunning = true; skipIds = []; totalDone = 0; totalInitial = 0;
|
||||
document.getElementById('bulk-mal-box').classList.add('on');
|
||||
document.getElementById('btn-bulk-mal').disabled = true;
|
||||
document.getElementById('bulk-mal-log').innerHTML = '';
|
||||
document.getElementById('bulk-mal-bar').style.width = '0%';
|
||||
log('⏳ Başlatılıyor...');
|
||||
await runNext();
|
||||
}
|
||||
|
||||
async function runNext() {
|
||||
if (!bulkRunning) return;
|
||||
try {
|
||||
const res = await fetch(BULK_MAL_URL, { method:'POST', headers:{'X-CSRF-TOKEN':CSRF,'Content-Type':'application/json','Accept':'application/json'}, body:JSON.stringify({skip_ids:skipIds}) });
|
||||
const d = await res.json();
|
||||
if (d.done) { log('✅ ' + d.message); finish(); return; }
|
||||
totalDone++;
|
||||
if (!totalInitial) totalInitial = totalDone + (d.remaining ?? 0);
|
||||
const pct = totalInitial > 0 ? Math.round((totalDone / totalInitial) * 100) : 0;
|
||||
document.getElementById('bulk-mal-bar').style.width = pct + '%';
|
||||
if (d.found) log(`✓ <b>${esc(d.anime)}</b> → ${d.mal_id}`);
|
||||
else { log(`<span style="opacity:.5">— ${esc(d.anime)}: bulunamadı</span>`); if (d.skipped_id) skipIds.push(d.skipped_id); }
|
||||
setTimeout(runNext, 450);
|
||||
} catch(e) { log(`<span style="color:var(--danger)">❌ ${e.message}</span>`); setTimeout(runNext, 2000); }
|
||||
}
|
||||
|
||||
function stopBulkMal() { bulkRunning = false; log('⏹ Durduruldu.'); finish(); }
|
||||
function finish() { bulkRunning = false; document.getElementById('btn-bulk-mal').disabled = false; document.getElementById('bulk-mal-bar').style.width = '100%'; }
|
||||
function log(html) { const el = document.getElementById('bulk-mal-log'); el.innerHTML += html + '<br>'; el.scrollTop = el.scrollHeight; }
|
||||
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
async function fetchSingleMal(animeId, btn) {
|
||||
const cell = btn.closest('.mal-id-cell');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:12px;height:12px;border-width:1.5px"></span>';
|
||||
try {
|
||||
const res = await fetch(cell.dataset.fetchUrl, { method:'POST', headers:{'X-CSRF-TOKEN':CSRF,'Accept':'application/json'} });
|
||||
const d = await res.json();
|
||||
if (d.found) {
|
||||
cell.innerHTML = `<a href="https://myanimelist.net/anime/${d.mal_id}" target="_blank" class="badge bg-success text-decoration-none" style="color:#fff"><i class="bi bi-box-arrow-up-right" style="font-size:10px;margin-right:3px"></i>${d.mal_id}</a>`;
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-magic"></i>';
|
||||
btn.title = 'Bulunamadı — tekrar dene';
|
||||
}
|
||||
} catch(e) { btn.disabled = false; btn.innerHTML = '<i class="bi bi-exclamation-triangle" style="color:var(--danger)"></i>'; }
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,247 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', $anime->title)
|
||||
@section('page-title', $anime->title)
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-4 flex-wrap">
|
||||
<a href="{{ route('admin.animes.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Animelere Dön
|
||||
</a>
|
||||
<div style="margin-left:auto;display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addSeasonModal">
|
||||
<i class="bi bi-plus-circle"></i> Sezon Ekle
|
||||
</button>
|
||||
<a href="{{ route('admin.animes.edit', $anime) }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-pencil"></i> Düzenle
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{{-- ── Sol: Sezonlar & Bölümler ── --}}
|
||||
<div class="col-lg-8">
|
||||
@forelse($anime->seasons as $season)
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<span style="font-size:14px;font-weight:700;color:var(--text)">{{ $season->season_number }}. Sezon</span>
|
||||
@if($season->title)
|
||||
<span style="font-size:13px;color:var(--text2);margin-left:6px">— {{ $season->title }}</span>
|
||||
@endif
|
||||
@if($season->mal_id)
|
||||
<span class="badge bg-success ms-2" style="font-size:10px">MAL: {{ $season->mal_id }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div style="margin-left:auto;display:flex;gap:7px">
|
||||
<a href="{{ route('admin.episodes.create', ['anime_id'=>$anime->id,'season_id'=>$season->id]) }}"
|
||||
class="btn btn-sm btn-success">
|
||||
<i class="bi bi-plus-lg"></i> Bölüm Ekle
|
||||
</a>
|
||||
<form method="POST" action="{{ route('admin.seasons.destroy', $season) }}"
|
||||
onsubmit="return confirm('{{ $season->season_number }}. sezon ve tüm bölümleri silinecek. Devam edilsin mi?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@if($season->episodes->count() > 0)
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px">Bölüm</th>
|
||||
<th>Başlık</th>
|
||||
<th style="text-align:center">Durum</th>
|
||||
<th style="text-align:center">İzlenme</th>
|
||||
<th style="text-align:right;padding-right:20px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($season->episodes as $ep)
|
||||
<tr>
|
||||
<td style="padding-left:20px">
|
||||
<span style="font-family:monospace;font-size:12.5px;background:var(--accent-dim);color:var(--accent-lt);padding:2px 8px;border-radius:5px">
|
||||
{{ str_pad($ep->episode_number,2,'0',STR_PAD_LEFT) }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="font-size:13px;color:var(--text2)">{{ $ep->title ?: '—' }}</td>
|
||||
<td style="text-align:center">
|
||||
@if($ep->is_published)
|
||||
<span class="badge bg-success">Yayında</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Taslak</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align:center;font-size:13px;color:var(--text2)">{{ number_format($ep->view_count) }}</td>
|
||||
<td style="text-align:right;padding-right:20px">
|
||||
<a href="{{ route('admin.episodes.edit', $ep) }}" class="btn btn-sm btn-secondary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-state py-4">
|
||||
<i class="bi bi-collection-play"></i>
|
||||
<p>Henüz bölüm yok.</p>
|
||||
<a href="{{ route('admin.episodes.create', ['anime_id'=>$anime->id,'season_id'=>$season->id]) }}"
|
||||
class="btn btn-sm btn-success mt-1"><i class="bi bi-plus-lg"></i> Bölüm Ekle</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-layers"></i>
|
||||
<h6>Henüz sezon eklenmemiş</h6>
|
||||
<button class="btn btn-primary btn-sm mt-2" data-bs-toggle="modal" data-bs-target="#addSeasonModal">
|
||||
<i class="bi bi-plus-circle"></i> İlk Sezonu Ekle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
{{-- İzin Ayarları --}}
|
||||
@if(isset($permissions) && count($permissions))
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-shield-lock" style="color:var(--success)"></i>
|
||||
<h6>Bu Anime İçin İzin Ayarları</h6>
|
||||
<span style="font-size:12px;color:var(--text2)">— Global ayarları override eder</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<form method="POST" action="{{ route('admin.animes.permissions', $anime) }}">
|
||||
@csrf
|
||||
@foreach($permissions as $perm)
|
||||
@php $current = $contentPerms[$perm->key] ?? 'free'; @endphp
|
||||
<div class="perm-row">
|
||||
<div>
|
||||
<div class="perm-name">{{ $perm->label }}</div>
|
||||
@if($perm->description)<div class="perm-desc">{{ $perm->description }}</div>@endif
|
||||
</div>
|
||||
<div class="perm-toggle">
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]" id="ap_{{ $perm->key }}_free" value="free" {{ $current==='free'?'checked':'' }}>
|
||||
<label for="ap_{{ $perm->key }}_free" class="free">Ücretsiz</label>
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]" id="ap_{{ $perm->key }}_prem" value="premium" {{ $current==='premium'?'checked':'' }}>
|
||||
<label for="ap_{{ $perm->key }}_prem" class="premium">Premium</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
<div class="p-3">
|
||||
<button type="submit" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-floppy"></i> İzinleri Kaydet
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- ── Sağ: Anime Bilgileri ── --}}
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-body text-center" style="padding:24px">
|
||||
@if($anime->coverUrl)
|
||||
<img src="{{ $anime->coverUrl }}" alt="{{ $anime->title }}"
|
||||
style="width:100%;max-width:180px;aspect-ratio:2/3;object-fit:cover;border-radius:var(--radius-sm);margin-bottom:16px">
|
||||
@endif
|
||||
<div style="font-size:16px;font-weight:800;color:var(--text);margin-bottom:4px">{{ $anime->title }}</div>
|
||||
@if($anime->title_en)
|
||||
<div style="font-size:12.5px;color:var(--text2);margin-bottom:12px">{{ $anime->title_en }}</div>
|
||||
@endif
|
||||
<div class="d-flex justify-content-center gap-2 flex-wrap">
|
||||
<span class="badge bg-secondary">{{ strtoupper($anime->type) }}</span>
|
||||
<span class="badge {{ $anime->is_published?'bg-success':'bg-secondary' }}">
|
||||
{{ $anime->is_published?'Yayında':'Taslak' }}
|
||||
</span>
|
||||
@if($anime->is_featured)
|
||||
<span class="badge bg-warning" style="color:#000">Öne Çıkan</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-info-circle" style="color:var(--accent-lt)"></i>
|
||||
<h6>Detaylar</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@foreach([
|
||||
['Durum', $anime->status, ''],
|
||||
['Yayın Yılı', $anime->release_year ?: '—', ''],
|
||||
['Stüdyo', $anime->studio ?: '—', ''],
|
||||
['Puan', $anime->rating ?: '—', ''],
|
||||
['MAL ID', $anime->mal_id ?: '—', ''],
|
||||
['Türler', $anime->genres->pluck('name')->join(', ') ?: '—', ''],
|
||||
] as [$lbl, $val])
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 16px;border-bottom:1px solid var(--border)">
|
||||
<span style="font-size:12.5px;color:var(--text2)">{{ $lbl }}</span>
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text);text-align:right;max-width:60%">{{ $val }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<a href="{{ route('admin.animes.edit', $anime) }}" class="btn btn-primary">
|
||||
<i class="bi bi-pencil"></i> Düzenle
|
||||
</a>
|
||||
<a href="{{ route('admin.episodes.index', ['anime_id'=>$anime->id]) }}" class="btn btn-secondary">
|
||||
<i class="bi bi-collection-play"></i> Tüm Bölümleri Gör
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Sezon Ekle Modal --}}
|
||||
<div class="modal fade" id="addSeasonModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title"><i class="bi bi-plus-circle me-2" style="color:var(--accent-lt)"></i>Sezon Ekle</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('admin.animes.seasons.store', $anime) }}">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Sezon Numarası <span style="color:var(--danger)">*</span></label>
|
||||
<input type="number" name="season_number" class="form-control"
|
||||
value="{{ $anime->seasons->count() + 1 }}" min="1" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Başlık <span style="color:var(--text2);font-size:11px">(opsiyonel)</span></label>
|
||||
<input type="text" name="title" class="form-control" placeholder="1. Sezon">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Yayın Yılı</label>
|
||||
<input type="number" name="release_year" class="form-control" value="{{ date('Y') }}">
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded" style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Yayınla</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_published" value="1" id="sp" role="switch" checked>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">İptal</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-plus-circle"></i> Sezon Ekle</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,291 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Giriş — Animexe Admin</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#07071c;
|
||||
--surface:#0c0c26;
|
||||
--border:rgba(255,255,255,0.08);
|
||||
--accent:#7c3aed;
|
||||
--accent-lt:#a855f7;
|
||||
--accent-glow:rgba(124,58,237,0.3);
|
||||
--text:#e2e0f0;
|
||||
--text2:#8b87a8;
|
||||
--text3:#55527a;
|
||||
--danger:#ef4444;
|
||||
--danger-dim:rgba(239,68,68,0.1);
|
||||
}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
background:var(--bg);
|
||||
color:var(--text);
|
||||
font-family:'Inter','Segoe UI',system-ui,sans-serif;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
min-height:100vh;
|
||||
padding:20px;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
|
||||
/* Animated background */
|
||||
.bg-glow{
|
||||
position:fixed;inset:0;z-index:0;overflow:hidden;pointer-events:none;
|
||||
}
|
||||
.bg-glow::before{
|
||||
content:'';
|
||||
position:absolute;
|
||||
top:-30%;left:-20%;
|
||||
width:60%;height:60%;
|
||||
background:radial-gradient(ellipse,rgba(124,58,237,0.12) 0%,transparent 70%);
|
||||
animation:gbg 8s ease-in-out infinite alternate;
|
||||
}
|
||||
.bg-glow::after{
|
||||
content:'';
|
||||
position:absolute;
|
||||
bottom:-20%;right:-15%;
|
||||
width:50%;height:50%;
|
||||
background:radial-gradient(ellipse,rgba(168,85,247,0.08) 0%,transparent 70%);
|
||||
animation:gbg 10s ease-in-out infinite alternate-reverse;
|
||||
}
|
||||
@keyframes gbg{0%{transform:translate(0,0) scale(1)}100%{transform:translate(3%,4%) scale(1.05)}}
|
||||
|
||||
/* Login box */
|
||||
.login-wrap{
|
||||
position:relative;z-index:1;
|
||||
width:100%;max-width:420px;
|
||||
}
|
||||
.login-box{
|
||||
background:var(--surface);
|
||||
border:1px solid var(--border);
|
||||
border-radius:20px;
|
||||
padding:44px 40px;
|
||||
box-shadow:0 24px 80px rgba(0,0,0,0.45);
|
||||
}
|
||||
|
||||
/* Brand */
|
||||
.login-brand{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
gap:12px;
|
||||
margin-bottom:8px;
|
||||
}
|
||||
.login-brand-icon{
|
||||
width:46px;height:46px;
|
||||
background:linear-gradient(135deg,var(--accent),var(--accent-lt));
|
||||
border-radius:14px;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:22px;color:#fff;
|
||||
box-shadow:0 0 30px var(--accent-glow);
|
||||
}
|
||||
.login-brand-name{
|
||||
font-size:22px;font-weight:800;
|
||||
color:var(--text);letter-spacing:0.5px;
|
||||
}
|
||||
.login-brand-name span{color:var(--accent-lt);}
|
||||
.login-sub{
|
||||
text-align:center;
|
||||
font-size:13px;
|
||||
color:var(--text3);
|
||||
margin-bottom:32px;
|
||||
font-weight:500;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.f-group{margin-bottom:18px;}
|
||||
.f-label{
|
||||
display:block;
|
||||
font-size:13px;font-weight:600;
|
||||
color:var(--text2);
|
||||
margin-bottom:7px;
|
||||
}
|
||||
.f-input{
|
||||
width:100%;
|
||||
background:var(--bg);
|
||||
border:1px solid rgba(255,255,255,0.1);
|
||||
border-radius:10px;
|
||||
padding:11px 14px;
|
||||
color:var(--text);
|
||||
font-size:14px;
|
||||
font-family:inherit;
|
||||
outline:none;
|
||||
transition:border-color 0.15s,box-shadow 0.15s;
|
||||
}
|
||||
.f-input:focus{
|
||||
border-color:var(--accent);
|
||||
box-shadow:0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
.f-input::placeholder{color:var(--text3);}
|
||||
|
||||
/* Password toggle */
|
||||
.f-pass-wrap{position:relative;}
|
||||
.f-pass-toggle{
|
||||
position:absolute;right:12px;top:50%;transform:translateY(-50%);
|
||||
background:none;border:none;color:var(--text3);
|
||||
cursor:pointer;font-size:15px;padding:2px;line-height:1;
|
||||
transition:color 0.12s;
|
||||
}
|
||||
.f-pass-toggle:hover{color:var(--text2);}
|
||||
|
||||
/* Remember */
|
||||
.f-remember{
|
||||
display:flex;align-items:center;gap:8px;
|
||||
margin-bottom:22px;
|
||||
}
|
||||
.f-remember input[type=checkbox]{
|
||||
width:16px;height:16px;
|
||||
appearance:none;-webkit-appearance:none;
|
||||
background:var(--bg);
|
||||
border:1px solid rgba(255,255,255,0.15);
|
||||
border-radius:4px;cursor:pointer;
|
||||
position:relative;flex-shrink:0;
|
||||
transition:all 0.12s;
|
||||
}
|
||||
.f-remember input:checked{background:var(--accent);border-color:var(--accent);}
|
||||
.f-remember input:checked::after{
|
||||
content:'';position:absolute;
|
||||
top:2px;left:5px;width:4px;height:8px;
|
||||
border:2px solid #fff;border-top:none;border-left:none;
|
||||
transform:rotate(45deg);
|
||||
}
|
||||
.f-remember label{font-size:13px;color:var(--text2);cursor:pointer;}
|
||||
|
||||
/* Submit */
|
||||
.f-submit{
|
||||
width:100%;
|
||||
padding:12px;
|
||||
background:linear-gradient(135deg,var(--accent),var(--accent-lt));
|
||||
border:none;border-radius:10px;
|
||||
color:#fff;
|
||||
font-size:14px;font-weight:700;font-family:inherit;
|
||||
cursor:pointer;
|
||||
box-shadow:0 4px 20px var(--accent-glow);
|
||||
transition:opacity 0.15s,transform 0.12s;
|
||||
}
|
||||
.f-submit:hover{opacity:0.9;}
|
||||
.f-submit:active{transform:scale(0.99);}
|
||||
|
||||
/* Error */
|
||||
.f-error{
|
||||
background:var(--danger-dim);
|
||||
border:1px solid rgba(239,68,68,0.25);
|
||||
border-radius:10px;
|
||||
padding:11px 14px;
|
||||
font-size:13px;
|
||||
color:#fca5a5;
|
||||
display:flex;align-items:center;gap:8px;
|
||||
margin-bottom:20px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.login-footer{
|
||||
text-align:center;
|
||||
margin-top:24px;
|
||||
font-size:12px;
|
||||
color:var(--text3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="bg-glow"></div>
|
||||
|
||||
<div class="login-wrap">
|
||||
<div class="login-box">
|
||||
<div class="login-brand">
|
||||
<div class="login-brand-icon"><i class="bi bi-play-fill"></i></div>
|
||||
<div class="login-brand-name">ANIME<span>XE</span></div>
|
||||
</div>
|
||||
<div class="login-sub">Yönetim Paneli</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="f-error">
|
||||
<i class="bi bi-exclamation-circle-fill"></i>
|
||||
<span>{{ $errors->first() }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(session('error'))
|
||||
<div class="f-error">
|
||||
<i class="bi bi-exclamation-circle-fill"></i>
|
||||
<span>{{ session('error') }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('admin.login.post') }}">
|
||||
@csrf
|
||||
|
||||
<div class="f-group">
|
||||
<label class="f-label" for="email">E-posta adresi</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
class="f-input"
|
||||
placeholder="ornek@animexe.com"
|
||||
value="{{ old('email') }}"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="email"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="f-group">
|
||||
<label class="f-label" for="password">Şifre</label>
|
||||
<div class="f-pass-wrap">
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
class="f-input"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
style="padding-right:42px"
|
||||
>
|
||||
<button type="button" class="f-pass-toggle" onclick="togglePass()" tabindex="-1">
|
||||
<i class="bi bi-eye" id="pass-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="f-remember">
|
||||
<input type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
|
||||
<label for="remember">Beni hatırla</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="f-submit">
|
||||
<i class="bi bi-box-arrow-in-right" style="margin-right:6px"></i> Giriş Yap
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="login-footer">
|
||||
Animexe Admin — Yetkisiz erişim yasaktır.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function togglePass() {
|
||||
const inp = document.getElementById('password');
|
||||
const eye = document.getElementById('pass-eye');
|
||||
if (inp.type === 'password') {
|
||||
inp.type = 'text';
|
||||
eye.className = 'bi bi-eye-slash';
|
||||
} else {
|
||||
inp.type = 'password';
|
||||
eye.className = 'bi bi-eye';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,123 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Bannerlar')
|
||||
@section('page-title', 'Bannerlar')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">Bannerlar</h5>
|
||||
<div style="font-size:13px;color:var(--text2);margin-top:2px">Ana sayfa slider bannerleri</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{{-- Ekle --}}
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-plus-circle" style="color:var(--accent-lt)"></i>
|
||||
<h6>Yeni Banner Ekle</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.banners.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Başlık <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" name="title" class="form-control" placeholder="Banner adı" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Görsel <span style="color:var(--danger)">*</span></label>
|
||||
<input type="file" name="image" class="form-control" accept="image/*" required>
|
||||
<div class="form-text">Önerilen: 1920×900px</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Link</label>
|
||||
<input type="url" name="link" class="form-control" placeholder="https://...">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Sıra</label>
|
||||
<input type="number" name="sort_order" class="form-control" value="0" min="0">
|
||||
<div class="form-text">Küçük sayı önce gösterilir</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded mb-4"
|
||||
style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Aktif</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Sitede göster</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" value="1"
|
||||
id="ba" role="switch" checked>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-plus-lg"></i> Banner Ekle
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Liste --}}
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-images" style="color:var(--info)"></i>
|
||||
<h6>Mevcut Bannerlar</h6>
|
||||
<span style="font-size:12px;color:var(--text2);margin-left:4px">{{ $banners->count() }} adet</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@forelse($banners as $banner)
|
||||
<div style="display:flex;align-items:center;gap:16px;padding:14px 20px;border-bottom:1px solid var(--border)">
|
||||
{{-- Görsel --}}
|
||||
<div style="flex-shrink:0;width:96px;height:54px;border-radius:8px;overflow:hidden;background:var(--surface)">
|
||||
@if($banner->imageUrl)
|
||||
<img src="{{ $banner->imageUrl }}" alt="{{ $banner->title }}"
|
||||
style="width:100%;height:100%;object-fit:cover">
|
||||
@else
|
||||
<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:var(--text3)">
|
||||
<i class="bi bi-image"></i>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Bilgi --}}
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:13.5px;font-weight:700;color:var(--text)" class="text-truncate">{{ $banner->title }}</div>
|
||||
@if($banner->link)
|
||||
<div style="font-size:11.5px;color:var(--text2)" class="text-truncate">{{ $banner->link }}</div>
|
||||
@endif
|
||||
<div style="font-size:11.5px;color:var(--text3);margin-top:2px">Sıra: {{ $banner->sort_order }}</div>
|
||||
</div>
|
||||
|
||||
{{-- Durum + Sil --}}
|
||||
<div style="flex-shrink:0;display:flex;align-items:center;gap:10px">
|
||||
@if($banner->is_active)
|
||||
<span class="badge bg-success">Aktif</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Pasif</span>
|
||||
@endif
|
||||
<form method="POST" action="{{ route('admin.banners.destroy', $banner) }}"
|
||||
onsubmit="return confirm('Bu banner silinsin mi?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-images"></i>
|
||||
<h6>Henüz banner eklenmemiş</h6>
|
||||
<p>Sol panelden ilk bannerınızı ekleyebilirsiniz.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,248 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', isset($blog->id) ? 'Blog Düzenle' : 'Yeni Blog Yazısı')
|
||||
@section('page-title', isset($blog->id) ? 'Blog Yazısı Düzenle' : 'Yeni Blog Yazısı')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.char-counter{font-size:.72rem;color:var(--text2);text-align:right}
|
||||
.char-counter.warn{color:#d97706}
|
||||
.char-counter.danger{color:#ef4444}
|
||||
.seo-preview{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:16px;margin-top:12px}
|
||||
.seo-preview .sp-url{color:#4ade80;font-size:.78rem;margin-bottom:4px}
|
||||
.seo-preview .sp-title{color:#60a5fa;font-size:1rem;font-weight:600;margin-bottom:4px;cursor:pointer}
|
||||
.seo-preview .sp-title:hover{text-decoration:underline}
|
||||
.seo-preview .sp-desc{color:#9ca3af;font-size:.82rem;line-height:1.5}
|
||||
.ai-gen-box{background:linear-gradient(135deg,rgba(124,58,237,.1),rgba(232,67,147,.08));
|
||||
border:1px solid rgba(124,58,237,.3);border-radius:12px;padding:20px;margin-bottom:24px}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="row g-4">
|
||||
{{-- LEFT: AI Üretici --}}
|
||||
<div class="col-12">
|
||||
<div class="ai-gen-box">
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<i class="bi bi-stars" style="color:#7c3aed;font-size:1.2rem"></i>
|
||||
<strong style="color:var(--text)">AI Blog Yazısı Üretici</strong>
|
||||
<span style="color:var(--text2);font-size:.8rem">— DeepSeek ile otomatik SEO içeriği üret</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-end flex-wrap">
|
||||
<div style="flex:1;min-width:200px">
|
||||
<label class="form-label">Anime Seç</label>
|
||||
<select id="ai-anime-select" class="form-select form-select-sm">
|
||||
<option value="">— Anime seç —</option>
|
||||
@foreach($animes as $a)
|
||||
<option value="{{ $a->id }}" {{ (isset($blog->anime_id) && $blog->anime_id == $a->id) ? 'selected' : '' }}>
|
||||
{{ $a->title }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" onclick="generateAiBlog()" id="ai-gen-btn"
|
||||
class="btn btn-sm" style="background:linear-gradient(135deg,#7c3aed,var(--accent));color:#fff;border:none;white-space:nowrap">
|
||||
<i class="bi bi-magic me-1"></i>AI ile Üret
|
||||
</button>
|
||||
<div id="ai-gen-status" style="font-size:.8rem;color:var(--text2)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FORM --}}
|
||||
<div class="col-12">
|
||||
<form action="{{ isset($blog->id) ? route('admin.blog.update', $blog) : route('admin.blog.store') }}"
|
||||
method="POST">
|
||||
@csrf
|
||||
@if(isset($blog->id)) @method('PUT') @endif
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
{{-- Başlık --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Başlık *</label>
|
||||
<input type="text" name="title" id="f-title" class="form-control"
|
||||
value="{{ old('title', $blog->title ?? '') }}" required maxlength="255"
|
||||
oninput="updateCharCount(this,'cc-title',60)">
|
||||
<div class="char-counter" id="cc-title">{{ mb_strlen(old('title', $blog->title ?? '')) }}/60</div>
|
||||
</div>
|
||||
|
||||
{{-- Excerpt --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Özet (Excerpt)</label>
|
||||
<textarea name="excerpt" id="f-excerpt" class="form-control" rows="3"
|
||||
maxlength="300" oninput="updateCharCount(this,'cc-excerpt',160)">{{ old('excerpt', $blog->excerpt ?? '') }}</textarea>
|
||||
<div class="char-counter" id="cc-excerpt">{{ mb_strlen(old('excerpt', $blog->excerpt ?? '')) }}/160</div>
|
||||
</div>
|
||||
|
||||
{{-- Content --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">İçerik (HTML)</label>
|
||||
<textarea name="content" id="f-content" class="form-control"
|
||||
rows="22" style="font-family:monospace;font-size:.82rem">{{ old('content', $blog->content ?? '') }}</textarea>
|
||||
<div class="form-text">HTML etiketleri desteklenir. AI içeriği doğrudan buraya eklenir.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
{{-- SEO Önizleme --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Google Önizleme</label>
|
||||
<div class="seo-preview">
|
||||
<div class="sp-url" id="sp-url">animexe.com/blog/...</div>
|
||||
<div class="sp-title" id="sp-title">{{ old('title', $blog->title ?? 'Başlık giriniz') }}</div>
|
||||
<div class="sp-desc" id="sp-desc">{{ old('meta_description', $blog->meta_description ?? 'Açıklama giriniz') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Yayın Durumu --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Durum</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="draft" {{ (old('status', $blog->status ?? '') === 'draft') ? 'selected' : '' }}>Taslak</option>
|
||||
<option value="published" {{ (old('status', $blog->status ?? '') === 'published') ? 'selected' : '' }}>Yayında</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Anime --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">İlgili Anime</label>
|
||||
<select name="anime_id" id="f-anime" class="form-select">
|
||||
<option value="">— Seçiniz —</option>
|
||||
@foreach($animes as $a)
|
||||
<option value="{{ $a->id }}" {{ (old('anime_id', $blog->anime_id ?? '') == $a->id) ? 'selected' : '' }}>
|
||||
{{ $a->title }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Kapak Görseli --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Kapak Görseli URL</label>
|
||||
<input type="url" name="cover_image" class="form-control"
|
||||
value="{{ old('cover_image', $blog->cover_image ?? '') }}" placeholder="https://...">
|
||||
</div>
|
||||
|
||||
{{-- Odak Kelime --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Odak Anahtar Kelime</label>
|
||||
<input type="text" name="focus_keyword" id="f-focus" class="form-control"
|
||||
value="{{ old('focus_keyword', $blog->focus_keyword ?? '') }}" maxlength="255">
|
||||
</div>
|
||||
|
||||
{{-- Meta Description --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meta Description</label>
|
||||
<textarea name="meta_description" id="f-metadesc" class="form-control" rows="3"
|
||||
maxlength="200" oninput="updateCharCount(this,'cc-metadesc',160);updateSeoPreview()">{{ old('meta_description', $blog->meta_description ?? '') }}</textarea>
|
||||
<div class="char-counter" id="cc-metadesc">{{ mb_strlen(old('meta_description', $blog->meta_description ?? '')) }}/160</div>
|
||||
</div>
|
||||
|
||||
{{-- Meta Keywords --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meta Keywords</label>
|
||||
<input type="text" name="meta_keywords" class="form-control"
|
||||
value="{{ old('meta_keywords', $blog->meta_keywords ?? '') }}" placeholder="kelime1, kelime2, ...">
|
||||
</div>
|
||||
|
||||
{{-- Okuma Süresi --}}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Okuma Süresi (dakika)</label>
|
||||
<input type="number" name="reading_time" class="form-control" min="1" max="60"
|
||||
value="{{ old('reading_time', $blog->reading_time ?? 5) }}">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-check-lg me-1"></i>Kaydet
|
||||
</button>
|
||||
<a href="{{ route('admin.blog.index') }}" class="btn btn-outline-secondary w-100 mt-2">İptal</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function updateCharCount(el, counterId, warn) {
|
||||
const len = el.value.length;
|
||||
const el2 = document.getElementById(counterId);
|
||||
if (!el2) return;
|
||||
el2.textContent = len + '/' + warn;
|
||||
el2.className = 'char-counter' + (len > warn * 1.1 ? ' danger' : len > warn * .9 ? ' warn' : '');
|
||||
}
|
||||
|
||||
function updateSeoPreview() {
|
||||
const title = document.getElementById('f-title')?.value || '';
|
||||
const desc = document.getElementById('f-metadesc')?.value || document.getElementById('f-excerpt')?.value || '';
|
||||
document.getElementById('sp-title').textContent = title || 'Başlık giriniz';
|
||||
document.getElementById('sp-desc').textContent = desc || 'Açıklama giriniz';
|
||||
}
|
||||
|
||||
document.getElementById('f-title')?.addEventListener('input', updateSeoPreview);
|
||||
document.getElementById('f-excerpt')?.addEventListener('input', updateSeoPreview);
|
||||
updateSeoPreview();
|
||||
|
||||
async function generateAiBlog() {
|
||||
const animeId = document.getElementById('ai-anime-select')?.value;
|
||||
if (!animeId) { alert('Lütfen bir anime seçin.'); return; }
|
||||
|
||||
const btn = document.getElementById('ai-gen-btn');
|
||||
const status = document.getElementById('ai-gen-status');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Üretiliyor...';
|
||||
status.textContent = 'DeepSeek AI çalışıyor, lütfen bekleyin (30-60 sn)...';
|
||||
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name=csrf-token]')?.content
|
||||
|| document.querySelector('input[name=_token]')?.value || '';
|
||||
const res = await fetch('{{ route("admin.blog.generate-ai") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ anime_id: animeId })
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok || data.error) {
|
||||
status.textContent = 'Hata: ' + (data.error || 'Bilinmeyen hata');
|
||||
status.style.color = '#ef4444';
|
||||
return;
|
||||
}
|
||||
|
||||
// Form alanlarını doldur
|
||||
document.getElementById('f-title').value = data.title || '';
|
||||
document.getElementById('f-excerpt').value = data.excerpt || '';
|
||||
document.getElementById('f-content').value = data.content || '';
|
||||
document.getElementById('f-focus').value = data.focus_keyword || '';
|
||||
document.getElementById('f-metadesc').value = data.meta_description || '';
|
||||
|
||||
// Anime select'i de güncelle
|
||||
const animeSelect = document.getElementById('f-anime');
|
||||
if (animeSelect) animeSelect.value = animeId;
|
||||
|
||||
// Char count'ları güncelle
|
||||
['f-title','f-excerpt','f-metadesc'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.dispatchEvent(new Event('input'));
|
||||
});
|
||||
updateSeoPreview();
|
||||
|
||||
status.textContent = '✓ İçerik başarıyla üretildi! Kontrol edip kaydedin.';
|
||||
status.style.color = '#4ade80';
|
||||
|
||||
} catch(e) {
|
||||
status.textContent = 'Bağlantı hatası: ' + e.message;
|
||||
status.style.color = '#ef4444';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-magic me-1"></i>AI ile Üret';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,165 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Blog Yönetimi')
|
||||
@section('page-title', 'Blog Yazıları')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">Blog Yazıları</h5>
|
||||
<div style="font-size:13px;color:var(--text2);margin-top:2px">{{ $stats['total'] }} yazı</div>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<form action="{{ route('admin.blog.index') }}" method="GET" class="d-flex gap-2">
|
||||
<div class="search-input-wrap">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" name="q" value="{{ $q }}" placeholder="Başlık ara..." class="form-control" style="width:200px">
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-sm"><i class="bi bi-search"></i></button>
|
||||
</form>
|
||||
<form action="{{ route('admin.blog.bulk-generate') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="count" value="3">
|
||||
<button type="submit" class="btn btn-sm"
|
||||
style="background:linear-gradient(135deg,var(--accent),var(--accent-lt));color:#fff;border:none"
|
||||
onclick="return confirm('3 yeni AI blog yazısı üretilsin mi? (2-3 dakika sürebilir)')">
|
||||
<i class="bi bi-stars"></i> AI ile 3 Yazı Üret
|
||||
</button>
|
||||
</form>
|
||||
<a href="{{ route('admin.blog.create') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg"></i> Yeni Yazı
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Stats --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div><div class="stat-value">{{ $stats['total'] }}</div><div class="stat-label mt-1">Toplam</div></div>
|
||||
<div class="stat-icon violet"><i class="bi bi-journal-richtext"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div><div class="stat-value" style="color:var(--success)">{{ $stats['published'] }}</div><div class="stat-label mt-1">Yayında</div></div>
|
||||
<div class="stat-icon green"><i class="bi bi-check-circle-fill"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div><div class="stat-value" style="color:var(--text2)">{{ $stats['draft'] }}</div><div class="stat-label mt-1">Taslak</div></div>
|
||||
<div class="stat-icon cyan"><i class="bi bi-file-earmark-text"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div><div class="stat-value" style="color:var(--accent-lt)">{{ $stats['ai'] }}</div><div class="stat-label mt-1">AI Üretimi</div></div>
|
||||
<div class="stat-icon violet"><i class="bi bi-stars"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Tablo --}}
|
||||
<div class="table-wrap">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px">Başlık</th>
|
||||
<th>Anime</th>
|
||||
<th style="text-align:center">Durum</th>
|
||||
<th style="text-align:center">Kaynak</th>
|
||||
<th style="text-align:center">Görüntülenme</th>
|
||||
<th>Tarih</th>
|
||||
<th style="text-align:right;padding-right:20px">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($posts as $post)
|
||||
<tr>
|
||||
<td style="padding-left:20px;max-width:280px">
|
||||
<div style="font-size:13.5px;font-weight:700;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ $post->title }}</div>
|
||||
@if($post->focus_keyword)
|
||||
<div style="font-size:12px;color:var(--text3)">{{ $post->focus_keyword }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($post->anime)
|
||||
<span style="font-size:13px;color:var(--info)">{{ Str::limit($post->anime->title,25) }}</span>
|
||||
@else
|
||||
<span style="color:var(--text3)">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
@if($post->status === 'published')
|
||||
<span class="badge bg-success">Yayında</span>
|
||||
@elseif($post->status === 'generating')
|
||||
<span class="badge bg-warning" style="color:#000">Üretiliyor</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Taslak</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
@if($post->ai_generated)
|
||||
<span style="font-size:12.5px;color:var(--accent-lt);font-weight:700">
|
||||
<i class="bi bi-stars"></i> AI
|
||||
</span>
|
||||
@else
|
||||
<span style="font-size:12.5px;color:var(--text2)">
|
||||
<i class="bi bi-pencil"></i> Manuel
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align:center;font-size:13px;color:var(--text)">{{ number_format($post->views) }}</td>
|
||||
<td style="font-size:12.5px;color:var(--text2)">{{ $post->created_at->format('d.m.Y') }}</td>
|
||||
<td style="text-align:right;padding-right:20px">
|
||||
<div class="d-flex gap-2 justify-content-end">
|
||||
@if($post->status === 'published')
|
||||
<a href="{{ route('blog.show', $post->slug) }}" target="_blank" class="btn btn-sm btn-secondary" title="Görüntüle">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
@endif
|
||||
<a href="{{ route('admin.blog.edit', $post) }}" class="btn btn-sm btn-secondary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form action="{{ route('admin.blog.destroy', $post) }}" method="POST"
|
||||
onsubmit="return confirm('Bu yazı silinsin mi?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash3"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-journal-richtext"></i>
|
||||
<h6>Henüz blog yazısı yok</h6>
|
||||
<p>Yeni yazı ekleyin veya AI ile otomatik üretin.</p>
|
||||
<a href="{{ route('admin.blog.create') }}" class="btn btn-primary btn-sm mt-2">
|
||||
<i class="bi bi-plus-lg"></i> Yeni Yazı
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($posts->hasPages())
|
||||
<div class="mt-3">{{ $posts->appends(['q'=>$q])->links('admin.partials.pagination') }}</div>
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,249 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Yorumlar')
|
||||
@section('page-title', 'Yorumlar')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.comment-row td { vertical-align: top; padding-top: 14px; padding-bottom: 14px; }
|
||||
.cmt-user { font-size:13px; font-weight:700; color:var(--text); }
|
||||
.cmt-meta-badge { font-size:10.5px; font-weight:700; padding:2px 7px; border-radius:99px; }
|
||||
.cmt-where { font-size:12px; font-weight:700; color:var(--info); }
|
||||
.cmt-ep-code { font-size:11px; color:var(--text2); font-family:monospace; }
|
||||
.cmt-text { font-size:13px; color:var(--text2); line-height:1.5; word-break:break-word; }
|
||||
.cmt-gif { max-width:120px; max-height:80px; border-radius:6px; cursor:pointer; transition:max-width .2s; }
|
||||
.cmt-gif:hover { max-width:240px; max-height:160px; }
|
||||
.reply-form { display:none; margin-top:8px; }
|
||||
.reply-form.on { display:block; }
|
||||
.action-icon-btn {
|
||||
width:30px; height:30px; border-radius:7px;
|
||||
display:inline-flex; align-items:center; justify-content:center;
|
||||
font-size:13px; cursor:pointer; border:1px solid;
|
||||
transition:all .12s; flex-shrink:0;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">
|
||||
Yorumlar
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text2);margin-left:6px">{{ number_format($comments->total()) }} toplam</span>
|
||||
</h5>
|
||||
</div>
|
||||
{{-- Quick filter tabs --}}
|
||||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||||
<a href="{{ route('admin.comments.index') }}"
|
||||
class="btn btn-sm {{ !request('status') ? 'btn-primary' : 'btn-secondary' }}">Tümü</a>
|
||||
<a href="{{ route('admin.comments.index', ['status'=>'pending']) }}"
|
||||
class="btn btn-sm {{ request('status')=='pending' ? 'btn-primary' : 'btn-secondary' }}">
|
||||
Bekleyen
|
||||
@php try { $pc = \App\Models\Comment::where('status','pending')->count(); } catch(\Exception $e) { $pc = 0; } @endphp
|
||||
@if($pc > 0)<span class="badge bg-danger ms-1">{{ $pc }}</span>@endif
|
||||
</a>
|
||||
<a href="{{ route('admin.comments.index', ['status'=>'approved']) }}"
|
||||
class="btn btn-sm {{ request('status')=='approved' ? 'btn-primary' : 'btn-secondary' }}">Onaylı</a>
|
||||
<a href="{{ route('admin.comments.index', ['status'=>'rejected']) }}"
|
||||
class="btn btn-sm {{ request('status')=='rejected' ? 'btn-primary' : 'btn-secondary' }}">Reddedilmiş</a>
|
||||
<a href="{{ route('admin.comments.index', ['status'=>'spam']) }}"
|
||||
class="btn btn-sm {{ request('status')=='spam' ? 'btn-primary' : 'btn-secondary' }}">Spam</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Filter --}}
|
||||
<div class="filter-card mb-3">
|
||||
<form method="GET" class="d-flex gap-2 flex-wrap align-items-end">
|
||||
@if(request('status'))
|
||||
<input type="hidden" name="status" value="{{ request('status') }}">
|
||||
@endif
|
||||
<div class="search-input-wrap flex-grow-1" style="min-width:200px">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" name="search" class="form-control" placeholder="Yorum içeriği..." value="{{ request('search') }}">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-funnel"></i> Ara</button>
|
||||
<a href="{{ route('admin.comments.index') }}" class="btn btn-secondary btn-sm"><i class="bi bi-x-lg"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Table --}}
|
||||
<div class="table-wrap">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px;min-width:130px">Kullanıcı</th>
|
||||
<th style="min-width:150px">Nereye</th>
|
||||
<th style="min-width:200px">İçerik</th>
|
||||
<th style="text-align:center">Durum</th>
|
||||
<th style="min-width:100px">Tarih</th>
|
||||
<th style="text-align:right;padding-right:20px;min-width:180px">İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($comments as $comment)
|
||||
@php
|
||||
$commentable = $comment->commentable;
|
||||
$isEpisode = $commentable instanceof \App\Models\Episode;
|
||||
$isAnime = $commentable instanceof \App\Models\Anime;
|
||||
@endphp
|
||||
<tr class="comment-row">
|
||||
{{-- Kullanıcı --}}
|
||||
<td style="padding-left:20px">
|
||||
<div class="d-flex align-items-center gap-2 mb-1">
|
||||
<div class="user-avatar sm">{{ strtoupper(substr($comment->user?->name ?? '?', 0, 1)) }}</div>
|
||||
<span class="cmt-user">{{ $comment->user?->name ?? 'Silinmiş' }}</span>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
@if($comment->parent_id)
|
||||
<span class="badge bg-secondary" style="font-size:10px">↩ Yanıt</span>
|
||||
@endif
|
||||
@if($comment->is_pinned)
|
||||
<span class="badge bg-info" style="font-size:10px;color:#000"><i class="bi bi-pin-fill" style="font-size:9px"></i> Sabit</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{{-- Nereye --}}
|
||||
<td>
|
||||
@if($isEpisode && $commentable->anime)
|
||||
<div class="cmt-where">{{ Str::limit($commentable->anime->title, 20) }}</div>
|
||||
<a href="{{ url('/watch/'.$commentable->anime->slug.'/'.($commentable->season->season_number ?? 1).'/'.$commentable->episode_number) }}"
|
||||
target="_blank" class="cmt-ep-code text-decoration-none">
|
||||
S{{ str_pad($commentable->season->season_number ?? 1,2,'0',STR_PAD_LEFT) }}E{{ str_pad($commentable->episode_number,2,'0',STR_PAD_LEFT) }}
|
||||
<i class="bi bi-box-arrow-up-right" style="font-size:9px"></i>
|
||||
</a>
|
||||
@elseif($isAnime)
|
||||
<a href="{{ route('admin.animes.show', $commentable->slug) }}"
|
||||
class="cmt-where text-decoration-none">
|
||||
{{ Str::limit($commentable->title, 22) }}
|
||||
</a>
|
||||
@else
|
||||
<span style="color:var(--text3);font-size:12px">{{ class_basename($comment->commentable_type) }}</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- İçerik --}}
|
||||
<td>
|
||||
@if($comment->content)
|
||||
<div class="cmt-text">{{ Str::limit($comment->content, 100) }}</div>
|
||||
@endif
|
||||
@if($comment->gif_url)
|
||||
<img src="{{ $comment->gif_url }}" class="cmt-gif mt-1" alt="gif">
|
||||
@endif
|
||||
|
||||
{{-- Yanıt formu --}}
|
||||
<div class="reply-form" id="rf-{{ $comment->id }}">
|
||||
<form method="POST" action="{{ route('admin.comments.reply', $comment) }}" class="d-flex gap-2 mt-2">
|
||||
@csrf
|
||||
<input type="text" name="content" class="form-control form-control-sm"
|
||||
placeholder="Yanıtınızı yazın..." required>
|
||||
<button type="submit" class="btn btn-primary btn-sm text-nowrap">
|
||||
<i class="bi bi-send"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{{-- Durum --}}
|
||||
<td style="text-align:center">
|
||||
@php
|
||||
$sb = match($comment->status) {
|
||||
'approved' => ['bg-success','Onaylı'],
|
||||
'pending' => ['bg-warning','Bekliyor'],
|
||||
'spam' => ['bg-secondary','Spam'],
|
||||
default => ['bg-danger','Reddedilmiş'],
|
||||
};
|
||||
@endphp
|
||||
<span class="badge {{ $sb[0] }}" style="{{ $comment->status==='pending'?'color:#000':'' }}">{{ $sb[1] }}</span>
|
||||
</td>
|
||||
|
||||
{{-- Tarih --}}
|
||||
<td style="font-size:12px;color:var(--text2)">
|
||||
{{ $comment->created_at->format('d.m.Y') }}<br>
|
||||
<span style="color:var(--text3)">{{ $comment->created_at->format('H:i') }}</span>
|
||||
</td>
|
||||
|
||||
{{-- İşlemler --}}
|
||||
<td style="text-align:right;padding-right:20px">
|
||||
<div class="d-flex gap-1 justify-content-end flex-wrap">
|
||||
@if($comment->status !== 'approved')
|
||||
<form method="POST" action="{{ route('admin.comments.approve', $comment) }}">
|
||||
@csrf
|
||||
<button type="submit" class="action-icon-btn"
|
||||
style="border-color:var(--success);color:var(--success);background:var(--success-dim)"
|
||||
title="Onayla">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@if($comment->status !== 'rejected')
|
||||
<form method="POST" action="{{ route('admin.comments.reject', $comment) }}">
|
||||
@csrf
|
||||
<button type="submit" class="action-icon-btn"
|
||||
style="border-color:var(--warning);color:var(--warning);background:var(--warning-dim)"
|
||||
title="Reddet">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
<form method="POST" action="{{ route('admin.comments.pin', $comment) }}">
|
||||
@csrf
|
||||
<button type="submit" class="action-icon-btn"
|
||||
style="border-color:var(--info);color:var(--info);background:var(--info-dim)"
|
||||
title="{{ $comment->is_pinned ? 'Sabiti Kaldır' : 'Sabitle' }}">
|
||||
<i class="bi bi-pin{{ $comment->is_pinned ? '-angle-fill' : '' }}"></i>
|
||||
</button>
|
||||
</form>
|
||||
<button onclick="toggleReply({{ $comment->id }})"
|
||||
class="action-icon-btn"
|
||||
style="border-color:var(--accent);color:var(--accent-lt);background:var(--accent-dim)"
|
||||
title="Yanıtla">
|
||||
<i class="bi bi-reply-fill"></i>
|
||||
</button>
|
||||
<form method="POST" action="{{ route('admin.comments.destroy', $comment) }}"
|
||||
onsubmit="return confirm('Yorumu silmek istediğinize emin misiniz?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="action-icon-btn"
|
||||
style="border-color:var(--danger);color:var(--danger);background:var(--danger-dim)"
|
||||
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-chat-dots"></i>
|
||||
<h6>Yorum bulunamadı</h6>
|
||||
<p>Seçili filtreler için yorum bulunmuyor.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
{{ $comments->links('admin.partials.pagination') }}
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function toggleReply(id) {
|
||||
const el = document.getElementById('rf-' + id);
|
||||
if (!el) return;
|
||||
el.classList.toggle('on');
|
||||
if (el.classList.contains('on')) el.querySelector('input')?.focus();
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,192 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Dashboard')
|
||||
@section('page-title', 'Dashboard')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- ── Stat Cards ── --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-xl-3 col-sm-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value">{{ number_format($stats['total_users']) }}</div>
|
||||
<div class="stat-label mt-1">Toplam Kullanıcı</div>
|
||||
</div>
|
||||
<div class="stat-icon violet"><i class="bi bi-people-fill"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-sm-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value">{{ number_format($stats['premium_users']) }}</div>
|
||||
<div class="stat-label mt-1">Premium Üye</div>
|
||||
</div>
|
||||
<div class="stat-icon orange"><i class="bi bi-star-fill"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-sm-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value">{{ number_format($stats['total_animes']) }}</div>
|
||||
<div class="stat-label mt-1">Toplam Anime</div>
|
||||
</div>
|
||||
<div class="stat-icon cyan"><i class="bi bi-play-circle-fill"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-sm-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value">{{ number_format($stats['total_episodes']) }}</div>
|
||||
<div class="stat-label mt-1">Toplam Bölüm</div>
|
||||
</div>
|
||||
<div class="stat-icon green"><i class="bi bi-collection-play-fill"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-sm-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value">{{ number_format($stats['total_comments']) }}</div>
|
||||
<div class="stat-label mt-1">Toplam Yorum</div>
|
||||
</div>
|
||||
<div class="stat-icon violet"><i class="bi bi-chat-dots-fill"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-sm-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value">{{ number_format($stats['pending_comments']) }}</div>
|
||||
<div class="stat-label mt-1">Bekleyen Yorum</div>
|
||||
</div>
|
||||
<div class="stat-icon red"><i class="bi bi-clock-fill"></i></div>
|
||||
</div>
|
||||
@if($stats['pending_comments'] > 0)
|
||||
<a href="{{ route('admin.comments.index', ['status'=>'pending']) }}" class="btn btn-sm btn-outline-danger mt-2 w-100">
|
||||
<i class="bi bi-arrow-right"></i> İncele
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-sm-6">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value">{{ number_format($stats['active_subs']) }}</div>
|
||||
<div class="stat-label mt-1">Aktif Abonelik</div>
|
||||
</div>
|
||||
<div class="stat-icon orange"><i class="bi bi-credit-card-fill"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Alt Paneller ── --}}
|
||||
<div class="row g-3">
|
||||
|
||||
{{-- Son Kullanıcılar --}}
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-people" style="color:var(--accent-lt)"></i>
|
||||
<h6>Son Kayıtlar</h6>
|
||||
<a href="{{ route('admin.users.index') }}" class="btn btn-sm btn-outline-secondary ms-auto">Tümü</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@forelse($recent_users as $user)
|
||||
<div class="d-flex align-items-center gap-3 px-4 py-3 border-bottom border-0" style="border-bottom:1px solid var(--border) !important">
|
||||
<div class="user-avatar sm">{{ strtoupper(substr($user->name, 0, 1)) }}</div>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text)" class="text-truncate">{{ $user->name }}</div>
|
||||
<div style="font-size:11.5px;color:var(--text2)">{{ $user->created_at->diffForHumans() }}</div>
|
||||
</div>
|
||||
@if($user->membership === 'premium')
|
||||
<span class="badge badge-premium">PRO</span>
|
||||
@else
|
||||
<span class="badge badge-free">Ücretsiz</span>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-state py-5">
|
||||
<i class="bi bi-people"></i>
|
||||
<p>Henüz kullanıcı yok.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Son Animeler --}}
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-play-circle" style="color:var(--info)"></i>
|
||||
<h6>Son Animeler</h6>
|
||||
<a href="{{ route('admin.animes.index') }}" class="btn btn-sm btn-outline-secondary ms-auto">Tümü</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@forelse($recent_animes as $anime)
|
||||
<div class="d-flex align-items-center gap-3 px-4 py-3" style="border-bottom:1px solid var(--border)">
|
||||
@if($anime->cover_image)
|
||||
<img src="{{ $anime->coverUrl }}" alt="" style="width:38px;height:54px;object-fit:cover;border-radius:6px;flex-shrink:0;background:var(--surface)">
|
||||
@else
|
||||
<div style="width:38px;height:54px;border-radius:6px;background:var(--surface);display:flex;align-items:center;justify-content:center;flex-shrink:0;color:var(--text3)"><i class="bi bi-image"></i></div>
|
||||
@endif
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text)" class="text-truncate">{{ $anime->title }}</div>
|
||||
<div style="font-size:11.5px;color:var(--text2)">{{ $anime->created_at->diffForHumans() }}</div>
|
||||
</div>
|
||||
<span class="badge {{ $anime->is_published ? 'bg-success' : 'bg-secondary' }}">
|
||||
{{ $anime->is_published ? 'Yayında' : 'Taslak' }}
|
||||
</span>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-state py-5">
|
||||
<i class="bi bi-play-circle"></i>
|
||||
<p>Henüz anime eklenmemiş.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Son Yorumlar --}}
|
||||
<div class="col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-chat-dots" style="color:var(--warning)"></i>
|
||||
<h6>Son Yorumlar</h6>
|
||||
<a href="{{ route('admin.comments.index') }}" class="btn btn-sm btn-outline-secondary ms-auto">Tümü</a>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@forelse($recent_comments as $comment)
|
||||
<div class="px-4 py-3" style="border-bottom:1px solid var(--border)">
|
||||
<div class="d-flex align-items-center justify-content-between gap-2 mb-1">
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text)">{{ $comment->user?->name ?? 'Silinmiş Kullanıcı' }}</span>
|
||||
<span class="badge {{ $comment->status === 'approved' ? 'bg-success' : ($comment->status === 'pending' ? 'bg-warning' : 'bg-danger') }}">
|
||||
{{ match($comment->status) { 'approved'=>'Onaylı', 'pending'=>'Bekliyor', default=>'Reddedildi' } }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size:12.5px;color:var(--text2);line-height:1.4">{{ Str::limit($comment->content, 65) }}</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-state py-5">
|
||||
<i class="bi bi-chat-dots"></i>
|
||||
<p>Henüz yorum yok.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,230 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Bölüm Ekle')
|
||||
@section('page-title', 'Bölüm Ekle')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.section-card { background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius); margin-bottom:16px; overflow:hidden; }
|
||||
.section-header { display:flex; align-items:center; gap:10px; padding:14px 20px; border-bottom:1px solid var(--border); }
|
||||
.section-header h6 { font-size:13.5px; font-weight:700; color:var(--text); margin:0; }
|
||||
.section-body { padding:20px; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="mb-4">
|
||||
<a href="{{ route('admin.episodes.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Bölümlere Dön
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('admin.episodes.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-8 col-lg-7">
|
||||
|
||||
{{-- Bölüm Bilgileri --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-collection-play" style="color:var(--accent-lt)"></i>
|
||||
<h6>Bölüm Bilgileri</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Anime <span style="color:var(--danger)">*</span></label>
|
||||
<select name="anime_id" class="form-select" id="animeSelect" required onchange="loadSeasons(this.value)">
|
||||
<option value="">— Anime Seç —</option>
|
||||
@foreach($animes as $anime)
|
||||
<option value="{{ $anime->id }}"
|
||||
{{ (request('anime_id')==$anime->id || old('anime_id')==$anime->id) ? 'selected' : '' }}>
|
||||
{{ $anime->title }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Sezon <span style="color:var(--danger)">*</span></label>
|
||||
<select name="season_id" class="form-select" id="seasonSelect" required>
|
||||
<option value="">— Sezon Seç —</option>
|
||||
@foreach($seasons as $season)
|
||||
<option value="{{ $season->id }}"
|
||||
{{ (request('season_id')==$season->id || old('season_id')==$season->id) ? 'selected' : '' }}>
|
||||
{{ $season->season_number }}. Sezon
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Bölüm No <span style="color:var(--danger)">*</span></label>
|
||||
<input type="number" name="episode_number" class="form-control"
|
||||
value="{{ old('episode_number', 1) }}" min="1" required>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Başlık <span style="color:var(--text2);font-size:11px">(opsiyonel)</span></label>
|
||||
<input type="text" name="title" class="form-control"
|
||||
value="{{ old('title') }}" placeholder="Bölüm adı...">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Süre (saniye)</label>
|
||||
<input type="number" name="duration" class="form-control"
|
||||
value="{{ old('duration') }}" placeholder="1440">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Açıklama</label>
|
||||
<textarea name="description" class="form-control" rows="3"
|
||||
placeholder="Bölüm özeti...">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Video Kaynağı --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-play-btn" style="color:var(--info)"></i>
|
||||
<h6>Video Kaynağı</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Kaynak Tipi</label>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
@foreach(['bunnycdn'=>'BunnyCDN','external'=>'External URL','direct'=>'Direct M3U8'] as $val => $lbl)
|
||||
<label style="display:flex;align-items:center;gap:7px;padding:8px 14px;border-radius:8px;border:1px solid var(--border2);cursor:pointer;transition:all .12s;font-size:13px;font-weight:600;color:var(--text2)" class="source-opt">
|
||||
<input type="radio" name="source" value="{{ $val }}"
|
||||
{{ old('source','bunnycdn')===$val?'checked':'' }}
|
||||
style="accent-color:var(--accent)" onchange="toggleSource('{{ $val }}')">
|
||||
{{ $lbl }}
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div id="sourceUrlField">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">M3U8 / Video URL</label>
|
||||
<input type="text" name="source_url" class="form-control"
|
||||
value="{{ old('source_url') }}"
|
||||
placeholder="https://f.aniziumserver.sbs/.../master.m3u8">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Video URL (Pull CDN)</label>
|
||||
<input type="text" name="video_url" class="form-control" value="{{ old('video_url') }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">M3U8 URL</label>
|
||||
<input type="text" name="m3u8_url" class="form-control" value="{{ old('m3u8_url') }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- İzinler --}}
|
||||
@if(isset($permissions) && count($permissions))
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-shield-lock" style="color:var(--success)"></i>
|
||||
<h6>İzin Ayarları</h6>
|
||||
<span style="font-size:12px;color:var(--text2)">— Boş bırakınca anime/global ayarları geçerli olur</span>
|
||||
</div>
|
||||
<div class="section-body p-0">
|
||||
@foreach($permissions as $perm)
|
||||
<div class="perm-row">
|
||||
<div>
|
||||
<div class="perm-name">{{ $perm->label }}</div>
|
||||
</div>
|
||||
<div class="perm-toggle">
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]"
|
||||
id="ep_{{ $perm->key }}_free" value="free" checked>
|
||||
<label for="ep_{{ $perm->key }}_free" class="free">Ücretsiz</label>
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]"
|
||||
id="ep_{{ $perm->key }}_prem" value="premium">
|
||||
<label for="ep_{{ $perm->key }}_prem" class="premium">Premium</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Sağ --}}
|
||||
<div class="col-xl-4 col-lg-5">
|
||||
|
||||
{{-- Thumbnail --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-image" style="color:var(--info)"></i>
|
||||
<h6>Thumbnail</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<input type="file" name="thumbnail" class="form-control" accept="image/*">
|
||||
<div class="form-text">Önerilen: 1280×720px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Yayın --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-toggle-on" style="color:var(--success)"></i>
|
||||
<h6>Yayın Ayarı</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Yayınla</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Kullanıcılara görünür yap</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_published"
|
||||
value="1" id="is_published" role="switch">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100 btn-lg">
|
||||
<i class="bi bi-plus-circle"></i> Bölüm Ekle
|
||||
</button>
|
||||
<a href="{{ route('admin.episodes.index') }}" class="btn btn-secondary w-100 mt-2">İptal</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function loadSeasons(animeId) {
|
||||
if (!animeId) return;
|
||||
fetch('/admin/animes/' + animeId + '/seasons-json')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
const sel = document.getElementById('seasonSelect');
|
||||
sel.innerHTML = '<option value="">— Sezon Seç —</option>';
|
||||
data.forEach(s => {
|
||||
sel.innerHTML += `<option value="${s.id}">${s.season_number}. Sezon</option>`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSource(val) {
|
||||
// Seçili source opt'u vurgula
|
||||
document.querySelectorAll('.source-opt').forEach(el => {
|
||||
const inp = el.querySelector('input');
|
||||
el.style.borderColor = inp.value === val ? 'var(--accent)' : 'var(--border2)';
|
||||
el.style.background = inp.value === val ? 'var(--accent-dim)' : '';
|
||||
el.style.color = inp.value === val ? 'var(--accent-lt)' : 'var(--text2)';
|
||||
});
|
||||
}
|
||||
|
||||
// Init on load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const checked = document.querySelector('[name="source"]:checked');
|
||||
if (checked) toggleSource(checked.value);
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,371 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Bölüm Düzenle')
|
||||
@section('page-title', 'Bölüm Düzenle')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.section-card { background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius); margin-bottom:16px; overflow:hidden; }
|
||||
.section-header { display:flex; align-items:center; gap:10px; padding:14px 20px; border-bottom:1px solid var(--border); }
|
||||
.section-header h6 { font-size:13.5px; font-weight:700; color:var(--text); margin:0; }
|
||||
.section-body { padding:20px; }
|
||||
.stat-mini { background:var(--bg2); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden; }
|
||||
.stat-mini-row { display:flex; justify-content:space-between; align-items:center; padding:10px 16px; border-bottom:1px solid var(--border); font-size:13px; }
|
||||
.stat-mini-row:last-child { border-bottom:none; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-4 flex-wrap">
|
||||
<a href="{{ route('admin.episodes.index', ['anime_id'=>$episode->anime_id]) }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Bölümlere Dön
|
||||
</a>
|
||||
<div style="font-size:13px;color:var(--text2)">
|
||||
Son güncelleme: {{ $episode->updated_at->format('d.m.Y H:i') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('admin.episodes.update', $episode) }}" enctype="multipart/form-data">
|
||||
@csrf @method('PUT')
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-xl-8 col-lg-7">
|
||||
|
||||
{{-- Bölüm Bilgileri --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-collection-play" style="color:var(--accent-lt)"></i>
|
||||
<h6>Bölüm Bilgileri</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Anime</label>
|
||||
<select name="anime_id" class="form-select" required>
|
||||
@foreach($animes as $anime)
|
||||
<option value="{{ $anime->id }}" {{ $episode->anime_id==$anime->id?'selected':'' }}>
|
||||
{{ $anime->title }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Sezon</label>
|
||||
<select name="season_id" class="form-select" required>
|
||||
@foreach($seasons as $season)
|
||||
<option value="{{ $season->id }}" {{ $episode->season_id==$season->id?'selected':'' }}>
|
||||
{{ $season->season_number }}. Sezon
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Bölüm No</label>
|
||||
<input type="number" name="episode_number" class="form-control"
|
||||
value="{{ old('episode_number',$episode->episode_number) }}" required>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Başlık</label>
|
||||
<input type="text" name="title" class="form-control"
|
||||
value="{{ old('title',$episode->title) }}" placeholder="Opsiyonel">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Süre (saniye)</label>
|
||||
<input type="number" name="duration" class="form-control"
|
||||
value="{{ old('duration',$episode->duration) }}">
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0">Açıklama</label>
|
||||
<button type="button" class="btn btn-sm btn-secondary" id="ai-ep-btn" onclick="aiGenerateEpisode()">
|
||||
<i class="bi bi-stars" style="color:var(--info)"></i> AI ile Yaz
|
||||
</button>
|
||||
</div>
|
||||
<textarea name="description" id="ep-description" class="form-control" rows="3">{{ old('description',$episode->description) }}</textarea>
|
||||
<div id="ai-ep-status" class="form-text" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- İntro Ayarları --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-skip-end-fill" style="color:var(--warning)"></i>
|
||||
<h6>İntro / Outro Zaman Ayarı</h6>
|
||||
<span style="font-size:12px;color:var(--text2)">— "İntroyu Atla" butonu için</span>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">İntro Başlangıç (sn)</label>
|
||||
<input type="number" name="intro_start" class="form-control" min="0"
|
||||
value="{{ old('intro_start',$episode->intro_start) }}" placeholder="örn: 5">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">İntro Bitiş (sn)</label>
|
||||
<input type="number" name="intro_end" class="form-control" min="0"
|
||||
value="{{ old('intro_end',$episode->intro_end) }}" placeholder="örn: 95">
|
||||
</div>
|
||||
</div>
|
||||
@if($episode->intro_start && $episode->intro_end)
|
||||
<div class="mt-2 p-2 rounded" style="background:var(--success-dim);border:1px solid rgba(16,185,129,.2);font-size:12.5px;color:var(--success)">
|
||||
<i class="bi bi-check-circle-fill me-1"></i>
|
||||
Mevcut: {{ $episode->intro_start }}sn — {{ $episode->intro_end }}sn
|
||||
({{ $episode->intro_end - $episode->intro_start }}sn süre)
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Video Kaynağı --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-play-btn" style="color:var(--info)"></i>
|
||||
<h6>Video Kaynağı</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Kaynak Tipi</label>
|
||||
<select name="source" class="form-select">
|
||||
@foreach(['bunnycdn'=>'BunnyCDN','external'=>'External URL','direct'=>'Direct M3U8'] as $v => $l)
|
||||
<option value="{{ $v }}" {{ old('source',$episode->source)===$v?'selected':'' }}>{{ $l }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Source URL</label>
|
||||
<input type="text" name="source_url" class="form-control"
|
||||
value="{{ old('source_url',$episode->source_url) }}"
|
||||
placeholder="https://...master.m3u8">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Video URL (CDN Pull)</label>
|
||||
<input type="text" name="video_url" class="form-control"
|
||||
value="{{ old('video_url',$episode->video_url) }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">M3U8 URL</label>
|
||||
<input type="text" name="m3u8_url" class="form-control"
|
||||
value="{{ old('m3u8_url',$episode->m3u8_url) }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- İzinler --}}
|
||||
@if(isset($permissions) && count($permissions))
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-shield-lock" style="color:var(--success)"></i>
|
||||
<h6>İzin Ayarları</h6>
|
||||
</div>
|
||||
<div class="section-body p-0">
|
||||
@foreach($permissions as $perm)
|
||||
@php $current = $contentPerms[$perm->key] ?? 'free'; @endphp
|
||||
<div class="perm-row">
|
||||
<div><div class="perm-name">{{ $perm->label }}</div></div>
|
||||
<div class="perm-toggle">
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]" id="ep2_{{ $perm->key }}_free" value="free" {{ $current==='free'?'checked':'' }}>
|
||||
<label for="ep2_{{ $perm->key }}_free" class="free">Ücretsiz</label>
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]" id="ep2_{{ $perm->key }}_prem" value="premium" {{ $current==='premium'?'checked':'' }}>
|
||||
<label for="ep2_{{ $perm->key }}_prem" class="premium">Premium</label>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Sağ --}}
|
||||
<div class="col-xl-4 col-lg-5">
|
||||
|
||||
{{-- İstatistikler --}}
|
||||
<div class="section-card mb-3">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-bar-chart" style="color:var(--info)"></i>
|
||||
<h6>Bölüm İstatistikleri</h6>
|
||||
</div>
|
||||
<div class="section-body p-0">
|
||||
<div class="stat-mini-row">
|
||||
<span style="color:var(--text2)">İzlenme</span>
|
||||
<span style="font-weight:700;color:var(--text)">{{ number_format($episode->view_count) }}</span>
|
||||
</div>
|
||||
<div class="stat-mini-row">
|
||||
<span style="color:var(--text2)">Süre</span>
|
||||
<span style="font-weight:600;color:var(--text)">{{ $episode->duration_formatted ?? ($episode->duration ? floor($episode->duration/60).'dk' : '—') }}</span>
|
||||
</div>
|
||||
<div class="stat-mini-row">
|
||||
<span style="color:var(--text2)">Kaynak</span>
|
||||
<span class="badge bg-secondary">{{ strtoupper($episode->source) }}</span>
|
||||
</div>
|
||||
<div class="stat-mini-row">
|
||||
<span style="color:var(--text2)">Eklenme</span>
|
||||
<span style="font-size:12px;color:var(--text)">{{ $episode->created_at->format('d.m.Y H:i') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Thumbnail --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-image" style="color:var(--info)"></i>
|
||||
<h6>Thumbnail</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
@if($episode->thumbnailUrl)
|
||||
<img src="{{ $episode->thumbnailUrl }}" alt=""
|
||||
style="width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:var(--radius-sm);margin-bottom:12px">
|
||||
@endif
|
||||
<input type="file" name="thumbnail" class="form-control" accept="image/*">
|
||||
<div class="form-text">Önerilen: 1280×720px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Yayın --}}
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<i class="bi bi-toggle-on" style="color:var(--success)"></i>
|
||||
<h6>Yayın Ayarı</h6>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Yayınla</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Kullanıcılara görünür yap</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_published" value="1"
|
||||
id="is_published" role="switch" {{ $episode->is_published?'checked':'' }}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100 btn-lg">
|
||||
<i class="bi bi-floppy"></i> Değişiklikleri Kaydet
|
||||
</button>
|
||||
<a href="{{ route('admin.episodes.index', ['anime_id'=>$episode->anime_id]) }}"
|
||||
class="btn btn-secondary w-100 mt-2">İptal</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{-- Video Kaynakları + HEVC Tarama --}}
|
||||
@php
|
||||
$videoSources = \App\Models\VideoSource::where('episode_id', $episode->id)->orderBy('sort_order')->get();
|
||||
@endphp
|
||||
@if($videoSources->count())
|
||||
<div class="section-card mt-4">
|
||||
<div class="section-header d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-collection-play" style="color:var(--warning)"></i>
|
||||
<h6 class="mb-0">Video Kaynakları ({{ $videoSources->count() }})</h6>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-warning" id="hevc-scan-btn"
|
||||
onclick="scanHevc({{ $episode->id }})">
|
||||
<i class="bi bi-search"></i> HEVC Tara
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-body p-0">
|
||||
<div id="hevc-sources-list">
|
||||
@foreach($videoSources as $src)
|
||||
<div class="stat-mini-row" id="src-row-{{ $src->id }}">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge bg-secondary" style="font-size:10px">{{ strtoupper($src->type) }}</span>
|
||||
<span style="font-size:13px;color:var(--text)">{{ $src->label ?: '—' }}</span>
|
||||
@if($src->quality)
|
||||
<span style="font-size:11px;color:var(--text2)">{{ $src->quality }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
@if($src->is_hevc)
|
||||
<span class="badge bg-danger" title="H.265 (HEVC) — Chrome'da oynatılamaz">H.265</span>
|
||||
@elseif($src->hevc_checked_at)
|
||||
<span class="badge bg-success" title="Tarandı: H.264 (uyumlu)">H.264 ✓</span>
|
||||
@else
|
||||
<span class="badge bg-secondary" id="hevc-badge-{{ $src->id }}">Taranmadı</span>
|
||||
@endif
|
||||
<a href="{{ $src->url }}" target="_blank" class="btn btn-xs btn-outline-secondary" style="font-size:11px;padding:2px 6px">
|
||||
<i class="bi bi-link-45deg"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div id="hevc-scan-status" class="px-3 py-2" style="display:none;font-size:12px;color:var(--text2)"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
async function scanHevc(episodeId) {
|
||||
const btn = document.getElementById('hevc-scan-btn');
|
||||
const status = document.getElementById('hevc-scan-status');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:11px;height:11px"></span> Taranıyor...';
|
||||
status.style.display = 'block';
|
||||
status.textContent = 'Kaynaklar sunucu tarafında taranıyor, lütfen bekleyin...';
|
||||
try {
|
||||
const res = await fetch(`/admin/episodes/${episodeId}/scan-hevc`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content || '' },
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.ok) throw new Error('Sunucu hatası');
|
||||
data.results.forEach(r => {
|
||||
const badge = document.getElementById(`hevc-badge-${r.id}`);
|
||||
if (badge) {
|
||||
badge.className = r.is_hevc ? 'badge bg-danger' : 'badge bg-success';
|
||||
badge.textContent = r.is_hevc ? 'H.265 ⚠' : 'H.264 ✓';
|
||||
badge.title = r.is_hevc ? 'HEVC — Chrome desteklemiyor' : 'H.264 uyumlu';
|
||||
badge.id = ''; // artık dinamik badge değil, stabil
|
||||
}
|
||||
});
|
||||
const hevcCount = data.results.filter(r => r.is_hevc).length;
|
||||
status.style.color = hevcCount > 0 ? 'var(--danger)' : 'var(--success)';
|
||||
status.textContent = hevcCount > 0
|
||||
? `⚠ ${hevcCount} kaynak H.265 (HEVC) — Chrome'da açılmaz!`
|
||||
: '✓ Tüm kaynaklar H.264 uyumlu.';
|
||||
} catch(e) {
|
||||
status.style.color = 'var(--danger)';
|
||||
status.textContent = '❌ Tarama başarısız: ' + e.message;
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-search"></i> HEVC Tara';
|
||||
}
|
||||
}
|
||||
|
||||
async function aiGenerateEpisode() {
|
||||
const btn = document.getElementById('ai-ep-btn');
|
||||
const status = document.getElementById('ai-ep-status');
|
||||
const ta = document.getElementById('ep-description');
|
||||
const animeSelect = document.querySelector('[name="anime_id"]');
|
||||
const animeTitle = animeSelect?.options[animeSelect.selectedIndex]?.text?.trim() || '';
|
||||
const episodeNumber = document.querySelector('[name="episode_number"]')?.value?.trim();
|
||||
const episodeTitle = document.querySelector('[name="title"]')?.value?.trim();
|
||||
if (!animeTitle) { axToast('Anime seçili değil.', 'warning'); return; }
|
||||
|
||||
btn.disabled=true; btn.innerHTML='<span class="spinner-border spinner-border-sm" style="width:12px;height:12px"></span>';
|
||||
status.style.display='block'; status.style.color='var(--info)'; status.textContent='AI açıklama oluşturuyor...';
|
||||
try {
|
||||
const res = await fetch('{{ route("admin.ai.generate") }}', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json','X-CSRF-TOKEN':document.querySelector('meta[name=csrf-token]')?.content||''},
|
||||
body:JSON.stringify({ type:'episode', title:animeTitle, anime_title:animeTitle, episode_number:episodeNumber, episode_title:episodeTitle })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) { status.style.color='var(--danger)'; status.textContent='❌ '+data.error; }
|
||||
else { ta.value=data.description; status.style.color='var(--success)'; status.textContent='✓ Açıklama oluşturuldu.'; }
|
||||
} catch(e) { status.style.color='var(--danger)'; status.textContent='❌ Hata.'; }
|
||||
finally { btn.disabled=false; btn.innerHTML='<i class="bi bi-stars" style="color:var(--info)"></i> AI ile Yaz'; }
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -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
|
||||
@@ -0,0 +1,173 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Türler')
|
||||
@section('page-title', 'Türler')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">Türler</h5>
|
||||
<div style="font-size:13px;color:var(--text2);margin-top:2px">{{ $genres->count() }} tür kayıtlı</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{{-- ── Sol: Ekle ── --}}
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-plus-circle" style="color:var(--accent-lt)"></i>
|
||||
<h6>Yeni Tür Ekle</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.genres.store') }}">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tür Adı <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" name="name" class="form-control" placeholder="Örn: Aksiyon" required autofocus>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Renk</label>
|
||||
<div class="d-flex gap-3 align-items-center">
|
||||
<input type="color" name="color" class="form-control form-control-color" value="#7c3aed" style="width:50px;height:38px;padding:2px">
|
||||
<span style="font-size:12px;color:var(--text2)">Kategori kartlarında görünecek renk</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-plus-lg"></i> Tür Ekle
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Sağ: Liste ── --}}
|
||||
<div class="col-lg-8">
|
||||
<div class="table-wrap">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px">Tür</th>
|
||||
<th style="text-align:center">Renk</th>
|
||||
<th style="text-align:center">Anime Sayısı</th>
|
||||
<th style="text-align:center">Durum</th>
|
||||
<th style="text-align:right;padding-right:20px">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($genres as $genre)
|
||||
<tr>
|
||||
<td style="padding-left:20px">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<div style="width:10px;height:10px;border-radius:50%;background:{{ $genre->color ?: 'var(--text3)' }};flex-shrink:0"></div>
|
||||
<span style="font-size:13.5px;font-weight:600;color:var(--text)">{{ $genre->name }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
@if($genre->color)
|
||||
<span style="font-size:12px;font-family:monospace;color:var(--text2)">{{ $genre->color }}</span>
|
||||
@else
|
||||
<span style="color:var(--text3)">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align:center;font-size:13px;font-weight:700;color:var(--text)">
|
||||
{{ $genre->animes_count }}
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
@if($genre->is_active)
|
||||
<span class="badge bg-success">Aktif</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Pasif</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align:right;padding-right:20px">
|
||||
<div class="d-flex gap-2 justify-content-end">
|
||||
<button class="btn btn-sm btn-secondary"
|
||||
onclick="openEdit({{ $genre->id }}, '{{ addslashes($genre->name) }}', '{{ $genre->color }}', {{ $genre->is_active ? 1 : 0 }})"
|
||||
data-bs-toggle="modal" data-bs-target="#editModal">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<form method="POST" action="{{ route('admin.genres.destroy', $genre) }}"
|
||||
onsubmit="return confirm('{{ addslashes($genre->name) }} türünü silmek istediğinize emin misiniz?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-tags"></i>
|
||||
<h6>Henüz tür eklenmemiş</h6>
|
||||
<p>Sol panelden ilk türü ekleyebilirsiniz.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Edit Modal --}}
|
||||
<div class="modal fade" id="editModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title"><i class="bi bi-pencil-square me-2" style="color:var(--accent-lt)"></i>Tür Düzenle</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST" id="editForm">
|
||||
@csrf @method('PUT')
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Tür Adı <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" name="name" id="editName" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Renk</label>
|
||||
<div class="d-flex gap-3 align-items-center">
|
||||
<input type="color" name="color" id="editColor" class="form-control form-control-color" style="width:50px;height:38px;padding:2px">
|
||||
<span style="font-size:12px;color:var(--text2)">Kategori rengi</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded" style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Aktif</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Pasif türler kullanıcılara gösterilmez</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" id="editActive" value="1" role="switch">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">İptal</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Kaydet</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function openEdit(id, name, color, active) {
|
||||
document.getElementById('editForm').action = '/admin/genres/' + id;
|
||||
document.getElementById('editName').value = name;
|
||||
document.getElementById('editColor').value = color || '#7c3aed';
|
||||
document.getElementById('editActive').checked = active === 1;
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,373 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Sağlık Kontrolü')
|
||||
@section('page-title', 'Sağlık Kontrolü')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.health-card {
|
||||
background:var(--bg2);
|
||||
border-radius:var(--radius);
|
||||
margin-bottom:16px;
|
||||
overflow:hidden;
|
||||
}
|
||||
.health-card-header {
|
||||
display:flex; align-items:center; gap:10px;
|
||||
padding:14px 20px;
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
.health-card-header .badge { margin-left:auto; }
|
||||
.health-card-body { padding:0; }
|
||||
.health-row {
|
||||
padding:14px 20px;
|
||||
border-bottom:1px solid var(--border);
|
||||
display:flex; align-items:center; flex-wrap:wrap; gap:12px;
|
||||
}
|
||||
.health-row:last-child { border-bottom:none; }
|
||||
.health-ok { padding:14px 20px; color:var(--success); font-size:13.5px; display:flex; align-items:center; gap:7px; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">Sağlık Kontrolü</h5>
|
||||
<div style="font-size:13px;color:var(--text2);margin-top:2px">{{ now()->format('d.m.Y H:i') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Özet Kartlar --}}
|
||||
<div class="row g-3 mb-4">
|
||||
@php
|
||||
$checks = [
|
||||
['icon'=>'bi-copy','label'=>'Duplike Anime','value'=>$malDuplicates->count(),'danger'=>$malDuplicates->count()>0],
|
||||
['icon'=>'bi-shuffle','label'=>'Karışık Kaynaklı','value'=>$mixedSources->count(),'warn'=>$mixedSources->count()>0],
|
||||
['icon'=>'bi-collection-play','label'=>'Eksik Bölümlü','value'=>$missingEpisodes->count(),'warn'=>$missingEpisodes->count()>0],
|
||||
['icon'=>'bi-cloud','label'=>'Harici CDN Bölüm','value'=>number_format($externalCount),'info'=>$externalCount>0],
|
||||
['icon'=>'bi-trash3','label'=>'0 Bölümlü Anime','value'=>$zeroEpisodeAnimes->count(),'danger'=>$zeroEpisodeAnimes->count()>0],
|
||||
];
|
||||
@endphp
|
||||
@foreach($checks as $c)
|
||||
<div class="col-6 col-md-4 col-lg">
|
||||
<div class="stat-card" style="border-color:{{ ($c['danger']??false)?'rgba(239,68,68,.4)':(($c['warn']??false)?'rgba(245,158,11,.4)':(($c['info']??false)?'rgba(6,182,212,.4)':'rgba(16,185,129,.3)')) }}">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value" style="color:{{ ($c['danger']??false)?'var(--danger)':(($c['warn']??false)?'var(--warning)':(($c['info']??false)?'var(--info)':'var(--success)')) }}">{{ $c['value'] }}</div>
|
||||
<div class="stat-label mt-1">{{ $c['label'] }}</div>
|
||||
</div>
|
||||
<div class="stat-icon {{ ($c['danger']??false)?'red':(($c['warn']??false)?'orange':(($c['info']??false)?'cyan':'green')) }}">
|
||||
<i class="bi {{ $c['icon'] }}"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- 1. Duplike --}}
|
||||
<div class="health-card" style="border:1px solid {{ $malDuplicates->count()>0 ? 'rgba(239,68,68,.3)' : 'var(--border)' }}">
|
||||
<div class="health-card-header">
|
||||
<i class="bi bi-copy" style="color:var(--danger)"></i>
|
||||
<span style="font-size:14px;font-weight:700;color:var(--text)">Duplike Animeler (MAL ID)</span>
|
||||
<span class="badge {{ $malDuplicates->count()>0 ? 'bg-danger' : 'bg-success' }}">{{ $malDuplicates->count() }}</span>
|
||||
</div>
|
||||
<div class="health-card-body">
|
||||
@forelse($malDuplicates as $dup)
|
||||
<div class="health-row flex-column align-items-start">
|
||||
<div style="font-size:12.5px;color:var(--text2)">MAL ID: <strong style="color:var(--warning)">{{ $dup['mal_id'] }}</strong></div>
|
||||
<div class="table-wrap w-100">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead><tr><th>ID</th><th>Başlık</th><th style="text-align:center">Bölüm</th><th>Eklenme</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
@foreach($dup['animes'] as $anime)
|
||||
<tr>
|
||||
<td style="color:var(--text2);font-size:12px">#{{ $anime->id }}</td>
|
||||
<td>
|
||||
<a href="{{ route('admin.animes.show',$anime->slug) }}" target="_blank"
|
||||
style="color:var(--info);text-decoration:none;font-size:13px;font-weight:600">
|
||||
{{ $anime->title }} <i class="bi bi-box-arrow-up-right" style="font-size:10px"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td style="text-align:center"><span class="badge bg-secondary">{{ $anime->episodes_count }}</span></td>
|
||||
<td style="font-size:12px;color:var(--text2)">{{ $anime->created_at->format('d.m.Y') }}</td>
|
||||
<td>
|
||||
<form method="POST" action="{{ route('admin.health.delete-anime',$anime) }}"
|
||||
onsubmit="return confirm('{{ addslashes($anime->title) }} silinsin mi?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash3"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="health-ok"><i class="bi bi-check-circle-fill"></i> Duplike anime yok.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 2. Karışık Kaynak --}}
|
||||
<div class="health-card" style="border:1px solid {{ $mixedSources->count()>0 ? 'rgba(245,158,11,.3)' : 'var(--border)' }}">
|
||||
<div class="health-card-header">
|
||||
<i class="bi bi-shuffle" style="color:var(--warning)"></i>
|
||||
<span style="font-size:14px;font-weight:700;color:var(--text)">Karışık Kaynaklı Animeler</span>
|
||||
<span style="font-size:12px;color:var(--text2)">(hem animecix hem anizium)</span>
|
||||
<span class="badge {{ $mixedSources->count()>0 ? 'bg-warning' : 'bg-success' }}" style="{{ $mixedSources->count()>0 ? 'color:#000' : '' }}">{{ $mixedSources->count() }}</span>
|
||||
</div>
|
||||
<div class="health-card-body">
|
||||
@forelse($mixedSources as $item)
|
||||
<div class="health-row">
|
||||
<div style="flex:1;min-width:0">
|
||||
<a href="{{ route('admin.animes.show',$item['anime']->slug) }}" target="_blank"
|
||||
style="color:var(--info);font-size:13.5px;font-weight:700;text-decoration:none">
|
||||
{{ $item['anime']->title }}
|
||||
</a>
|
||||
<div class="d-flex gap-2 flex-wrap mt-1">
|
||||
@foreach($item['sources'] as $src => $cnt)
|
||||
<span class="badge bg-secondary" style="font-size:11px">{{ $src }}: {{ $cnt }} bölüm</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
@foreach($item['sources'] as $src => $cnt)
|
||||
<form method="POST" action="{{ route('admin.health.delete-source-episodes',$item['anime']) }}"
|
||||
onsubmit="return confirm('{{ addslashes($item['anime']->title) }} — {{ $src }} bölümleri ({{ $cnt }} adet) silinsin mi?')">
|
||||
@csrf
|
||||
<input type="hidden" name="source" value="{{ $src }}">
|
||||
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash3"></i> {{ $src }}</button>
|
||||
</form>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="health-ok"><i class="bi bi-check-circle-fill"></i> Karışık kaynaklı anime yok.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 3. Eksik Bölümler --}}
|
||||
<div class="health-card" style="border:1px solid {{ $missingEpisodes->count()>0 ? 'rgba(245,158,11,.3)' : 'var(--border)' }}">
|
||||
<div class="health-card-header">
|
||||
<i class="bi bi-collection-play" style="color:var(--warning)"></i>
|
||||
<span style="font-size:14px;font-weight:700;color:var(--text)">Eksik Bölümlü Animeler</span>
|
||||
<span class="badge {{ $missingEpisodes->count()>0 ? 'bg-warning' : 'bg-success' }}" style="{{ $missingEpisodes->count()>0 ? 'color:#000' : '' }}">{{ $missingEpisodes->count() }}</span>
|
||||
</div>
|
||||
<div class="health-card-body">
|
||||
@if($missingEpisodes->count())
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead><tr><th style="padding-left:20px">Anime</th><th style="text-align:center">Beklenen</th><th style="text-align:center">Mevcut</th><th style="text-align:center">Eksik</th><th style="padding-right:20px"></th></tr></thead>
|
||||
<tbody>
|
||||
@foreach($missingEpisodes as $item)
|
||||
<tr>
|
||||
<td style="padding-left:20px;font-size:13px;font-weight:700;color:var(--text)">{{ $item['title'] }}</td>
|
||||
<td style="text-align:center;font-size:13px">{{ $item['expected'] }}</td>
|
||||
<td style="text-align:center;font-size:13px;color:var(--text2)">{{ $item['actual'] }}</td>
|
||||
<td style="text-align:center"><span class="badge {{ $item['missing']>5?'bg-danger':'bg-warning' }}" style="{{ $item['missing']>5?'':'color:#000' }}">-{{ $item['missing'] }}</span></td>
|
||||
<td style="padding-right:20px;text-align:right">
|
||||
<a href="{{ route('admin.episodes.index',['anime_id'=>$item['id']]) }}" class="btn btn-sm btn-secondary" target="_blank">
|
||||
<i class="bi bi-list"></i> Bölümler
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="health-ok"><i class="bi bi-check-circle-fill"></i> Eksik bölümlü anime yok.</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 4. Sıfır Bölümlü --}}
|
||||
<div class="health-card" style="border:1px solid {{ $zeroEpisodeAnimes->count()>0 ? 'rgba(239,68,68,.3)' : 'var(--border)' }}">
|
||||
<div class="health-card-header">
|
||||
<i class="bi bi-trash3" style="color:var(--danger)"></i>
|
||||
<span style="font-size:14px;font-weight:700;color:var(--text)">0 Bölümlü Animeler</span>
|
||||
<span class="badge {{ $zeroEpisodeAnimes->count()>0 ? 'bg-danger' : 'bg-success' }}">{{ $zeroEpisodeAnimes->count() }}</span>
|
||||
@if($zeroEpisodeAnimes->count() > 0)
|
||||
<button class="btn btn-sm btn-danger" onclick="destroyZero()" id="btn-zero-health" style="margin-left:auto">
|
||||
<i class="bi bi-trash3"></i> Tümünü Sil
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="health-card-body">
|
||||
@if($zeroEpisodeAnimes->count())
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead><tr><th style="padding-left:20px">Anime</th><th>Eklenme</th><th style="padding-right:20px;text-align:right">İşlem</th></tr></thead>
|
||||
<tbody>
|
||||
@foreach($zeroEpisodeAnimes as $anime)
|
||||
<tr>
|
||||
<td style="padding-left:20px;font-size:13px;font-weight:700;color:var(--text)">{{ $anime->title }}</td>
|
||||
<td style="font-size:12.5px;color:var(--text2)">{{ $anime->created_at->format('d.m.Y') }}</td>
|
||||
<td style="padding-right:20px;text-align:right">
|
||||
<form method="POST" action="{{ route('admin.animes.destroy',$anime) }}"
|
||||
onsubmit="return confirm('{{ addslashes($anime->title) }} silinsin mi?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash3"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="health-ok"><i class="bi bi-check-circle-fill"></i> 0 bölümlü anime yok.</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Depolama & İnode Yönetimi ── --}}
|
||||
<div class="card mt-4">
|
||||
<div class="card-header d-flex align-items-center gap-2">
|
||||
<i class="bi bi-hdd-fill" style="color:var(--warning)"></i>
|
||||
<h6 class="mb-0">Depolama & İnode Temizliği</h6>
|
||||
<button class="btn btn-sm btn-outline-secondary ms-auto" onclick="loadStorageStats()">
|
||||
<i class="bi bi-arrow-clockwise"></i> Yenile
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<div class="alert alert-warning mb-3" style="font-size:13px">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
||||
<strong>İnode uyarısı:</strong> Sunucuda 188.000+ inode kullanılıyor. En büyük tüketiciler:
|
||||
HLS segment cache, file session'lar, Laravel cache dosyaları.
|
||||
</div>
|
||||
|
||||
<div id="storage-stats-grid" style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:16px">
|
||||
<div class="text-center p-2" style="background:var(--bg);border:1px solid var(--border);border-radius:8px">
|
||||
<div style="font-size:20px;font-weight:700" id="ss-seg">—</div>
|
||||
<div style="font-size:11px;color:var(--text2)">HLS Segment Cache</div>
|
||||
<div style="font-size:10px;color:var(--text3)" id="ss-seg-size">—</div>
|
||||
</div>
|
||||
<div class="text-center p-2" style="background:var(--bg);border:1px solid var(--border);border-radius:8px">
|
||||
<div style="font-size:20px;font-weight:700" id="ss-sessions">—</div>
|
||||
<div style="font-size:11px;color:var(--text2)">Session Dosyaları</div>
|
||||
<div style="font-size:10px;color:var(--text3)" id="ss-driver">—</div>
|
||||
</div>
|
||||
<div class="text-center p-2" style="background:var(--bg);border:1px solid var(--border);border-radius:8px">
|
||||
<div style="font-size:20px;font-weight:700" id="ss-cache">—</div>
|
||||
<div style="font-size:11px;color:var(--text2)">Laravel Cache</div>
|
||||
<div style="font-size:10px;color:var(--text3)" id="ss-views">— views</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:8px">
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="cleanStorage('seg_cache')">
|
||||
<i class="bi bi-trash3 me-1"></i>HLS Cache Temizle
|
||||
<small class="d-block" style="font-size:10px;opacity:.7">storage/app/seg_cache/*</small>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-warning" onclick="cleanStorage('sessions')">
|
||||
<i class="bi bi-person-x me-1"></i>Session'ları Temizle
|
||||
<small class="d-block" style="font-size:10px;opacity:.7">storage/framework/sessions/*</small>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cleanStorage('cache_data')">
|
||||
<i class="bi bi-database-x me-1"></i>Laravel Cache Temizle
|
||||
<small class="d-block" style="font-size:10px;opacity:.7">storage/framework/cache/data/*</small>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="cleanStorage('views')">
|
||||
<i class="bi bi-file-code me-1"></i>Derlenmiş View Temizle
|
||||
<small class="d-block" style="font-size:10px;opacity:.7">storage/framework/views/*</small>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="cleanup-result" style="display:none;margin-top:10px;font-size:12px;padding:8px 12px;border-radius:6px;background:var(--bg);border:1px solid var(--border)"></div>
|
||||
|
||||
<div class="mt-3 p-3" style="background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:12px">
|
||||
<strong>Session sürücüsünü değiştir → .env</strong>
|
||||
<div style="color:var(--text2);margin-top:4px">
|
||||
File session'lar her ziyaretçi için 1 inode kullanır. Değiştirmek için <code>.env</code> dosyasında:
|
||||
</div>
|
||||
<code style="display:block;margin-top:6px;padding:6px;background:var(--bg2);border-radius:4px">SESSION_DRIVER=cookie</code>
|
||||
<div style="color:var(--text3);margin-top:4px">Cookie driver: 0 sunucu dosyası, anında inode tasarrufu. Sonra cache temizle.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content || '';
|
||||
|
||||
async function loadStorageStats() {
|
||||
try {
|
||||
const d = await (await fetch('{{ route("admin.health.storage-stats") }}')).json();
|
||||
const s = d.stats;
|
||||
const fmt = (n) => n >= 1000 ? (n/1000).toFixed(1) + 'K' : n;
|
||||
const fmtSize = (b) => b > 1048576 ? (b/1048576).toFixed(1) + ' MB' : b > 1024 ? (b/1024).toFixed(1) + ' KB' : b + ' B';
|
||||
|
||||
document.getElementById('ss-seg').textContent = fmt(s.seg_cache?.count || 0) + ' dosya';
|
||||
document.getElementById('ss-seg-size').textContent = fmtSize(s.seg_cache?.size || 0);
|
||||
|
||||
const sessions = s.sessions?.count || 0;
|
||||
document.getElementById('ss-sessions').textContent = fmt(sessions) + ' dosya';
|
||||
document.getElementById('ss-sessions').style.color = sessions > 1000 ? 'var(--danger)' : sessions > 100 ? 'var(--warning)' : 'inherit';
|
||||
|
||||
const cache = s.cache_data?.count || 0;
|
||||
document.getElementById('ss-cache').textContent = fmt(cache) + ' dosya';
|
||||
|
||||
const views = s.views?.count || 0;
|
||||
document.getElementById('ss-views').textContent = views + ' view dosyası';
|
||||
|
||||
// Session driver
|
||||
fetch('{{ route("admin.health.session-info") }}').then(r=>r.json()).then(sd => {
|
||||
document.getElementById('ss-driver').textContent = 'Driver: ' + sd.driver;
|
||||
if (sd.driver === 'file') document.getElementById('ss-driver').style.color = 'var(--danger)';
|
||||
}).catch(()=>{});
|
||||
} catch(e) {}
|
||||
}
|
||||
loadStorageStats();
|
||||
|
||||
async function cleanStorage(target) {
|
||||
const labels = {
|
||||
seg_cache: 'HLS segment cache',
|
||||
sessions: 'session dosyaları',
|
||||
cache_data: 'Laravel cache',
|
||||
views: 'derlenmiş view dosyaları',
|
||||
};
|
||||
if (!confirm(`${labels[target] || target} temizlensin mi?`)) return;
|
||||
|
||||
const res = document.getElementById('cleanup-result');
|
||||
res.style.display = 'block';
|
||||
res.textContent = 'Temizleniyor...';
|
||||
|
||||
try {
|
||||
const r = await fetch('{{ route("admin.health.cleanup-storage") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF },
|
||||
body: JSON.stringify({ target }),
|
||||
});
|
||||
const d = await r.json();
|
||||
res.innerHTML = `<span style="color:var(--success)">✓ ${d.message}</span>`;
|
||||
loadStorageStats();
|
||||
} catch(e) {
|
||||
res.innerHTML = `<span style="color:var(--danger)">Hata: ${e.message}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function destroyZero() {
|
||||
if (!confirm('0 bölümlü tüm animeler silinsin mi? Bu işlem geri alınamaz!')) return;
|
||||
const btn = document.getElementById('btn-zero-health');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm" style="width:12px;height:12px"></span>';
|
||||
try {
|
||||
const res = await fetch('{{ route("admin.animes.destroy-zero-episodes") }}', {
|
||||
method:'POST', headers:{'X-CSRF-TOKEN':CSRF,'Accept':'application/json'}
|
||||
});
|
||||
const d = await res.json();
|
||||
if (d.success) { axToast(`${d.count} anime silindi.`, 'success'); setTimeout(()=>location.reload(),1200); }
|
||||
else { axToast('Hata oluştu.', 'error'); btn.disabled=false; }
|
||||
} catch(e) { axToast('Hata: '+e.message, 'error'); btn.disabled=false; }
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,528 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Anime İçe Aktar')
|
||||
@section('page-title', 'Anime İçe Aktar')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.job-card {
|
||||
background:var(--bg2); border:1px solid var(--border);
|
||||
border-radius:var(--radius); padding:16px 20px; margin-bottom:12px;
|
||||
transition:border-color .15s;
|
||||
}
|
||||
.job-card:hover { border-color:var(--border2); }
|
||||
.test-output {
|
||||
display:none;
|
||||
background:var(--bg);
|
||||
border:1px solid var(--border);
|
||||
border-radius:var(--radius-sm);
|
||||
padding:14px;
|
||||
font-size:12px;
|
||||
font-family:'JetBrains Mono',monospace;
|
||||
white-space:pre-wrap;
|
||||
max-height:200px;
|
||||
overflow:auto;
|
||||
margin-top:12px;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">Anime İçe Aktar</h5>
|
||||
<div style="font-size:13px;color:var(--text2);margin-top:2px">CDN bağlantısından otomatik anime indirme</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{{-- ── Sol: Form ── --}}
|
||||
<div class="col-lg-5">
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-cloud-download" style="color:var(--accent-lt)"></i>
|
||||
<h6>Yeni Import Job Oluştur</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.import.store') }}">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Anime URL <span style="color:var(--danger)">*</span></label>
|
||||
<input type="url" name="source_url" class="form-control"
|
||||
placeholder="https://anizium.co/watch/..." value="{{ old('source_url') }}" required>
|
||||
<div class="form-text">Anizium watch veya anime sayfası URL'si</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Anime Adı <span style="color:var(--text2);font-size:11px">(opsiyonel)</span></label>
|
||||
<input type="text" name="anime_title" class="form-control"
|
||||
placeholder="Demon Slayer" value="{{ old('anime_title') }}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">CDN ID <span style="color:var(--text2);font-size:11px">(opsiyonel — otomatik bulunur)</span></label>
|
||||
<input type="text" name="cdn_id" class="form-control"
|
||||
style="font-family:monospace" placeholder="85937" value="{{ old('cdn_id') }}">
|
||||
<div class="form-text">
|
||||
URL'den al: <code>f.aniziumserver.sbs/<strong>85937</strong>/1/1/...</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">
|
||||
Sezon / Bölüm Aralıkları
|
||||
<span style="color:var(--text2);font-size:11px">(opsiyonel — otomatik taranır)</span>
|
||||
</label>
|
||||
<div id="rangeRows">
|
||||
@php $oldRanges = old('season_ranges',[['season'=>1,'from'=>1,'to'=>'']]) @endphp
|
||||
@foreach($oldRanges as $i => $range)
|
||||
<div class="range-row d-flex gap-2 mb-2 align-items-center">
|
||||
<div style="flex:0 0 70px">
|
||||
<input type="number" name="season_ranges[{{ $i }}][season]"
|
||||
class="form-control form-control-sm text-center"
|
||||
placeholder="S" min="1" value="{{ $range['season']??1 }}" required>
|
||||
</div>
|
||||
<input type="number" name="season_ranges[{{ $i }}][from]"
|
||||
class="form-control form-control-sm text-center"
|
||||
placeholder="Baş" min="1" value="{{ $range['from']??'' }}" style="flex:1">
|
||||
<span style="color:var(--text3);font-size:12px">—</span>
|
||||
<input type="number" name="season_ranges[{{ $i }}][to]"
|
||||
class="form-control form-control-sm text-center"
|
||||
placeholder="Bit" min="1" value="{{ $range['to']??'' }}" style="flex:1">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeRange(this)">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-secondary mt-1" onclick="addRange()">
|
||||
<i class="bi bi-plus-circle"></i> Sezon Ekle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-download"></i> Import Job Oluştur
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Test --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-plug" style="color:var(--info)"></i>
|
||||
<h6>Bağlantı Testi</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="testApi()">
|
||||
<i class="bi bi-plug"></i> API Test
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="testCdn()">
|
||||
<i class="bi bi-play-circle"></i> CDN Test
|
||||
</button>
|
||||
</div>
|
||||
<div class="test-output" id="testResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Araçlar: Çift Kaynak + Altyazı Fix ── --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-tools" style="color:var(--warning)"></i>
|
||||
<h6>Çift Kaynak Araçları</h6>
|
||||
</div>
|
||||
<div class="card-body" style="font-size:13px">
|
||||
|
||||
{{-- İstatistikler --}}
|
||||
<div id="srcStats" style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:14px">
|
||||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:8px 10px;text-align:center">
|
||||
<div style="font-size:11px;color:var(--text2)">İki kaynağı olan bölüm</div>
|
||||
<div style="font-size:18px;font-weight:700;color:var(--success)" id="st-both">—</div>
|
||||
</div>
|
||||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:8px 10px;text-align:center">
|
||||
<div style="font-size:11px;color:var(--text2)">Altyazı hatası</div>
|
||||
<div style="font-size:18px;font-weight:700;color:var(--danger)" id="st-sub">{{ $stats['subtitle_mismatch'] }}</div>
|
||||
</div>
|
||||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:8px 10px;text-align:center">
|
||||
<div style="font-size:11px;color:var(--text2)">Sadece Anizium</div>
|
||||
<div style="font-size:18px;font-weight:700;color:var(--info)" id="st-aniz">—</div>
|
||||
</div>
|
||||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:8px 10px;text-align:center">
|
||||
<div style="font-size:11px;color:var(--text2)">Sadece AnimeCix</div>
|
||||
<div style="font-size:18px;font-weight:700;color:var(--accent)" id="st-acix">—</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-secondary mb-3 w-100" onclick="loadStats()">
|
||||
<i class="bi bi-arrow-clockwise"></i> İstatistikleri Yenile
|
||||
</button>
|
||||
|
||||
<hr style="border-color:var(--border);margin:10px 0">
|
||||
|
||||
{{-- Altyazı Fix --}}
|
||||
<div class="mb-3">
|
||||
<div style="font-weight:600;color:var(--text);margin-bottom:6px">
|
||||
<i class="bi bi-closed-caption" style="color:var(--danger)"></i>
|
||||
Altyazı Uyuşmazlık Düzelt
|
||||
</div>
|
||||
<div style="color:var(--text2);font-size:12px;margin-bottom:8px">
|
||||
Anizium'dan yanlış bölümün altyazısının kaydedildiği satırları siler.
|
||||
Bot sonraki çalışmada doğru altyazıları yeniden indirir.
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-warning flex-fill" onclick="fixSubs(true)">
|
||||
<i class="bi bi-eye"></i> Önce Gör
|
||||
</button>
|
||||
<button class="btn btn-sm btn-danger flex-fill" onclick="fixSubs(false)">
|
||||
<i class="bi bi-trash3"></i> Hataları Sil
|
||||
</button>
|
||||
</div>
|
||||
<div id="fixSubResult" style="display:none;margin-top:8px;font-size:12px;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:6px;font-family:monospace;white-space:pre-wrap;max-height:150px;overflow:auto"></div>
|
||||
</div>
|
||||
|
||||
<hr style="border-color:var(--border);margin:10px 0">
|
||||
|
||||
{{-- Re-queue --}}
|
||||
<div class="mb-2">
|
||||
<div style="font-weight:600;color:var(--text);margin-bottom:6px">
|
||||
<i class="bi bi-arrow-repeat" style="color:var(--info)"></i>
|
||||
Yeniden Kuyruğa Al
|
||||
</div>
|
||||
<div style="color:var(--text2);font-size:12px;margin-bottom:8px">
|
||||
Done job'ları tekrar pending yaparak botların her iki kaynaktan
|
||||
<code>video_sources</code> eklemesini sağlar.
|
||||
</div>
|
||||
<div class="d-flex gap-2 mb-2">
|
||||
<div style="flex:1">
|
||||
<label style="font-size:11px;color:var(--text2)">Adet</label>
|
||||
<input type="number" id="requeueLimit" class="form-control form-control-sm" value="50" min="1" max="500">
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-info flex-fill" onclick="requeueAnizium()">
|
||||
<i class="bi bi-cloud-arrow-down"></i> Anizium'u Yenile
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary flex-fill" onclick="requeueAnimecix()">
|
||||
<i class="bi bi-film"></i> AnimeCix'i Yenile
|
||||
</button>
|
||||
</div>
|
||||
<div id="requeueResult" style="display:none;margin-top:8px;font-size:12px;padding:8px;background:var(--bg);border:1px solid var(--border);border-radius:6px"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Yardım --}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-question-circle" style="color:var(--text2)"></i>
|
||||
<h6>Nasıl Kullanılır?</h6>
|
||||
</div>
|
||||
<div class="card-body" style="font-size:13px">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--text);margin-bottom:6px">Çift Kaynak Akışı</div>
|
||||
<ol style="color:var(--text2);padding-left:18px;line-height:2;margin-bottom:10px">
|
||||
<li>Local: <code>python reimport_all.py</code> çalıştır</li>
|
||||
<li>Local: <code>python anizium_scraper/bot2_upload.py --daemon</code></li>
|
||||
<li>Local: <code>python animecix_scraper/daemon.py</code></li>
|
||||
<li>Her bölüm otomatik <strong>"4K"</strong> + <strong>çevirmen</strong> seçeneğiyle gelir</li>
|
||||
</ol>
|
||||
<div style="font-size:12px;font-weight:700;color:var(--text);margin-bottom:6px">CDN ID Nasıl Bulunur?</div>
|
||||
<ol style="color:var(--text2);padding-left:18px;line-height:2;margin-bottom:0">
|
||||
<li>Anizium'da herhangi bir bölümü oynat</li>
|
||||
<li>F12 → Network → <code>master.m3u8</code> ara</li>
|
||||
<li>URL'den sayıyı al: <code style="color:var(--warning)">85937</code></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- ── Sağ: Job Listesi ── --}}
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-list-task" style="color:var(--info)"></i>
|
||||
<h6>Import Geçmişi</h6>
|
||||
<span style="font-size:12px;color:var(--text2)">{{ $jobs->total() }} job</span>
|
||||
<button class="btn btn-sm btn-outline-danger ms-auto" onclick="openBulkDelete()" title="Toplu Sil">
|
||||
<i class="bi bi-trash3"></i> Toplu Sil
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Toplu silme paneli --}}
|
||||
<div id="bulkDeletePanel" style="display:none;padding:12px 16px;background:rgba(220,53,69,.06);border-bottom:1px solid rgba(220,53,69,.2)">
|
||||
<div style="font-size:12px;font-weight:700;color:var(--danger);margin-bottom:10px">
|
||||
<i class="bi bi-exclamation-triangle"></i> Hangi joblar silinsin?
|
||||
</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer">
|
||||
<input type="checkbox" id="del-failed" checked> <span style="color:var(--danger)">Hatalı</span> <span id="cnt-failed" class="badge bg-danger ms-1">—</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer">
|
||||
<input type="checkbox" id="del-pending"> <span style="color:var(--text2)">Bekleyen</span> <span id="cnt-pending" class="badge bg-secondary ms-1">—</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer">
|
||||
<input type="checkbox" id="del-stuck"> <span style="color:var(--warning)">Takılı Kalan</span> <span id="cnt-stuck" class="badge bg-warning text-dark ms-1">—</span>
|
||||
<span style="font-size:11px;color:var(--text2)">(2+ saat işlemde)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<button class="btn btn-sm btn-danger" onclick="runBulkDelete()">
|
||||
<i class="bi bi-trash3"></i> Seçilenleri Sil
|
||||
</button>
|
||||
<button class="btn btn-sm btn-secondary" onclick="document.getElementById('bulkDeletePanel').style.display='none'">
|
||||
İptal
|
||||
</button>
|
||||
<span id="bulkDeleteMsg" style="font-size:12px;margin-left:4px"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@forelse($jobs as $job)
|
||||
<div class="job-card">
|
||||
<div class="d-flex align-items-start justify-content-between gap-3 mb-2">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:13.5px;font-weight:700;color:var(--text)" class="text-truncate">
|
||||
{{ $job->anime_title ?: 'Anime CDN #'.$job->cdn_id }}
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text2);margin-top:2px">
|
||||
CDN: <code>{{ $job->cdn_id }}</code>
|
||||
@if($job->season_ranges)
|
||||
·
|
||||
@foreach($job->season_ranges as $r)
|
||||
S{{ $r['season'] }} E{{ $r['from'] }}-{{ $r['to'] }}{{ !$loop->last ? ', ' : '' }}
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 align-items-center flex-shrink-0">
|
||||
<span class="badge bg-{{ $job->status_color }}">{{ $job->status_label }}</span>
|
||||
<a href="{{ route('admin.import.show',$job) }}" class="btn btn-sm btn-secondary">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<form method="POST" action="{{ route('admin.import.destroy',$job) }}"
|
||||
onsubmit="return confirm('Job silinsin mi?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash3"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($job->total_episodes > 0)
|
||||
<div class="mb-2">
|
||||
<div class="d-flex justify-content-between mb-1" style="font-size:12px;color:var(--text2)">
|
||||
<span>{{ $job->done_episodes }}/{{ $job->total_episodes }} bölüm</span>
|
||||
<span>%{{ $job->progress_percent }}</span>
|
||||
</div>
|
||||
<div class="progress" style="height:5px">
|
||||
<div class="progress-bar" style="background:{{ $job->status_color==='success'?'var(--success)':($job->status_color==='danger'?'var(--danger)':'var(--accent)') }};width:{{ $job->progress_percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($job->current_step)
|
||||
<div style="font-size:12px;color:var(--text2)">{{ $job->current_step }}</div>
|
||||
@endif
|
||||
|
||||
<div style="font-size:11.5px;color:var(--text3);margin-top:4px">{{ $job->created_at->format('d.m.Y H:i') }}</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-cloud-download"></i>
|
||||
<h6>Henüz import job yok</h6>
|
||||
<p>Sol panelden yeni bir import başlatın.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
<div class="mt-3">
|
||||
{{ $jobs->links('admin.partials.pagination') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function showTestResult(text, ok=true) {
|
||||
const el = document.getElementById('testResult');
|
||||
el.style.display='block';
|
||||
el.style.color = ok ? 'var(--success)' : 'var(--danger)';
|
||||
el.textContent = text;
|
||||
}
|
||||
async function testApi() {
|
||||
showTestResult('Test ediliyor...', true);
|
||||
try {
|
||||
const r = await fetch('{{ url("/api/import/test") }}', {
|
||||
headers:{'X-Import-Key':'{{ config("app.import_api_key") }}','Accept':'application/json'}
|
||||
});
|
||||
const d = await r.json();
|
||||
if (d.ok) showTestResult(`✓ API bağlantısı başarılı\n DB: ${d.db}\n Toplam job: ${d.total}\n Bekleyen: ${d.pending}`, true);
|
||||
else showTestResult('✗ API hatası: '+JSON.stringify(d), false);
|
||||
} catch(e) { showTestResult('✗ Bağlantı hatası: '+e.message, false); }
|
||||
}
|
||||
async function testCdn() {
|
||||
const cdnId = document.querySelector('[name="cdn_id"]').value.trim()||'85937';
|
||||
const url = `https://f.aniziumserver.sbs/${cdnId}/1/1/1080p-original/master.m3u8`;
|
||||
showTestResult(`CDN test ediliyor...\nURL: ${url}`, true);
|
||||
try {
|
||||
await fetch(url, {method:'HEAD',mode:'no-cors'});
|
||||
showTestResult(`✓ CDN erişilebilir (CDN ID: ${cdnId})\n${url}`, true);
|
||||
} catch(e) { showTestResult(`✗ CDN erişilemedi: ${e.message}`, false); }
|
||||
}
|
||||
|
||||
let rangeIndex = {{ count(old('season_ranges',[[]])) }};
|
||||
function addRange() {
|
||||
const row = document.createElement('div');
|
||||
row.className='range-row d-flex gap-2 mb-2 align-items-center';
|
||||
row.innerHTML=`
|
||||
<div style="flex:0 0 70px"><input type="number" name="season_ranges[${rangeIndex}][season]" class="form-control form-control-sm text-center" placeholder="S" min="1" value="1" required></div>
|
||||
<input type="number" name="season_ranges[${rangeIndex}][from]" class="form-control form-control-sm text-center" placeholder="Baş" min="1" style="flex:1">
|
||||
<span style="color:var(--text3);font-size:12px">—</span>
|
||||
<input type="number" name="season_ranges[${rangeIndex}][to]" class="form-control form-control-sm text-center" placeholder="Bit" min="1" style="flex:1">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeRange(this)"><i class="bi bi-trash3"></i></button>`;
|
||||
document.getElementById('rangeRows').appendChild(row);
|
||||
rangeIndex++;
|
||||
}
|
||||
function removeRange(btn) {
|
||||
if (document.querySelectorAll('.range-row').length<=1) return;
|
||||
btn.closest('.range-row').remove();
|
||||
}
|
||||
|
||||
// ── Araçlar ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const CSRF = document.querySelector('meta[name="csrf-token"]')?.content || '';
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const r = await fetch('{{ route("admin.import.sourceStats") }}', {
|
||||
headers:{'Accept':'application/json','X-Requested-With':'XMLHttpRequest'}
|
||||
});
|
||||
const d = await r.json();
|
||||
document.getElementById('st-both').textContent = d.episodes_with_both ?? '—';
|
||||
document.getElementById('st-aniz').textContent = d.episodes_only_anizium ?? '—';
|
||||
document.getElementById('st-acix').textContent = d.episodes_only_animecix ?? '—';
|
||||
document.getElementById('st-sub').textContent = d.subtitle_mismatch ?? '—';
|
||||
} catch(e) { console.warn('stats error', e); }
|
||||
}
|
||||
loadStats(); // sayfa açılışında bir kez çek
|
||||
|
||||
async function fixSubs(dryRun) {
|
||||
const el = document.getElementById('fixSubResult');
|
||||
el.style.display = 'block';
|
||||
el.textContent = 'İşleniyor...';
|
||||
try {
|
||||
const r = await fetch('{{ route("admin.import.fixSubtitles") }}', {
|
||||
method:'POST',
|
||||
headers:{'Accept':'application/json','Content-Type':'application/json','X-CSRF-TOKEN':CSRF},
|
||||
body: JSON.stringify({dry_run: dryRun}),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (dryRun) {
|
||||
el.textContent = `Kontrol edildi: ${d.checked}\nUyuşmazlık: ${d.mismatch}\n\n`
|
||||
+ (d.details.map(x => `S${x.season}E${x.episode} ${x.lang}: name=${x.name} → beklenen=${x.expected}`).join('\n') || 'Hata bulunamadı.');
|
||||
} else {
|
||||
el.textContent = `Kontrol: ${d.checked} • Uyuşmazlık: ${d.mismatch} • Silindi: ${d.deleted}`;
|
||||
loadStats();
|
||||
}
|
||||
} catch(e) { el.textContent = 'Hata: ' + e.message; }
|
||||
}
|
||||
|
||||
async function requeueAnizium() {
|
||||
const limit = document.getElementById('requeueLimit').value || 50;
|
||||
const el = document.getElementById('requeueResult');
|
||||
el.style.display = 'block';
|
||||
el.textContent = 'İşleniyor...';
|
||||
try {
|
||||
const r = await fetch('{{ route("admin.import.requeueAnizium") }}', {
|
||||
method:'POST',
|
||||
headers:{'Accept':'application/json','Content-Type':'application/json','X-CSRF-TOKEN':CSRF},
|
||||
body: JSON.stringify({limit: parseInt(limit)}),
|
||||
});
|
||||
const d = await r.json();
|
||||
el.innerHTML = `<span style="color:var(--success)">✓ Anizium: ${d.requeued} job yeniden kuyruğa alındı (${d.checked} kontrol edildi)</span>`;
|
||||
} catch(e) { el.textContent = 'Hata: ' + e.message; }
|
||||
}
|
||||
|
||||
async function requeueAnimecix() {
|
||||
const limit = document.getElementById('requeueLimit').value || 50;
|
||||
const el = document.getElementById('requeueResult');
|
||||
el.style.display = 'block';
|
||||
el.textContent = 'İşleniyor...';
|
||||
try {
|
||||
const r = await fetch('{{ route("admin.import.requeueAnimecix") }}', {
|
||||
method:'POST',
|
||||
headers:{'Accept':'application/json','Content-Type':'application/json','X-CSRF-TOKEN':CSRF},
|
||||
body: JSON.stringify({limit: parseInt(limit)}),
|
||||
});
|
||||
const d = await r.json();
|
||||
el.innerHTML = `<span style="color:var(--success)">✓ AnimeCix: ${d.requeued} job yeniden kuyruğa alındı (${d.checked} kontrol edildi)</span>`;
|
||||
} catch(e) { el.textContent = 'Hata: ' + e.message; }
|
||||
}
|
||||
|
||||
// ── Toplu Silme ──────────────────────────────────────────────────────────────
|
||||
|
||||
const BULK_COUNTS_URL = '{{ route("admin.import.bulkCounts") }}';
|
||||
const BULK_DELETE_URL = '{{ route("admin.import.destroyByStatus") }}';
|
||||
|
||||
async function openBulkDelete() {
|
||||
const panel = document.getElementById('bulkDeletePanel');
|
||||
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
||||
if (panel.style.display === 'block') loadBulkCounts();
|
||||
}
|
||||
|
||||
async function loadBulkCounts() {
|
||||
try {
|
||||
const r = await fetch(BULK_COUNTS_URL, {headers:{'Accept':'application/json'}});
|
||||
const d = await r.json();
|
||||
document.getElementById('cnt-failed').textContent = d.failed ?? 0;
|
||||
document.getElementById('cnt-pending').textContent = d.pending ?? 0;
|
||||
document.getElementById('cnt-stuck').textContent = d.stuck ?? 0;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
async function runBulkDelete() {
|
||||
const statuses = [];
|
||||
if (document.getElementById('del-failed').checked) statuses.push('failed');
|
||||
if (document.getElementById('del-pending').checked) statuses.push('pending');
|
||||
if (document.getElementById('del-stuck').checked) statuses.push('fetching','downloading','uploading');
|
||||
|
||||
if (!statuses.length) { alert('En az bir kategori seç.'); return; }
|
||||
|
||||
const names = [];
|
||||
if (document.getElementById('del-failed').checked) names.push('hatalı');
|
||||
if (document.getElementById('del-pending').checked) names.push('bekleyen');
|
||||
if (document.getElementById('del-stuck').checked) names.push('takılı kalan');
|
||||
|
||||
if (!confirm(`${names.join(', ')} joblar silinsin mi? Bu işlem geri alınamaz.`)) return;
|
||||
|
||||
const msgEl = document.getElementById('bulkDeleteMsg');
|
||||
msgEl.style.color = 'var(--text2)';
|
||||
msgEl.textContent = 'Siliniyor...';
|
||||
|
||||
try {
|
||||
const r = await fetch(BULK_DELETE_URL, {
|
||||
method: 'DELETE',
|
||||
headers: {'Accept':'application/json','Content-Type':'application/json','X-CSRF-TOKEN':CSRF},
|
||||
body: JSON.stringify({ statuses, stuck_hours: 2 }),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (d.ok) {
|
||||
msgEl.style.color = 'var(--success)';
|
||||
msgEl.textContent = `✓ ${d.deleted} job silindi.`;
|
||||
setTimeout(() => location.reload(), 1200);
|
||||
} else {
|
||||
msgEl.style.color = 'var(--danger)';
|
||||
msgEl.textContent = d.message || 'Hata oluştu.';
|
||||
}
|
||||
} catch(e) {
|
||||
msgEl.style.color = 'var(--danger)';
|
||||
msgEl.textContent = 'Sunucu hatası: ' + e.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,104 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Import #' . $import->id)
|
||||
@section('page-title', 'Import #' . $import->id)
|
||||
|
||||
@section('content')
|
||||
<div class="mb-3">
|
||||
<a href="{{ route('admin.import.index') }}" class="text-muted small"><i class="bi bi-arrow-left me-1"></i>Import Listesi</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card mb-4">
|
||||
<h6 class="mb-3">Import Detayı</h6>
|
||||
<table class="table table-dark table-sm mb-0">
|
||||
<tr><td class="text-muted small">Anime</td><td class="small">{{ $import->anime_title ?: 'Anime #'.$import->watch_id }}</td></tr>
|
||||
<tr><td class="text-muted small">Watch ID</td><td class="small">{{ $import->watch_id }}</td></tr>
|
||||
<tr><td class="text-muted small">URL</td><td class="small text-truncate" style="max-width:200px">{{ $import->source_url }}</td></tr>
|
||||
<tr>
|
||||
<td class="text-muted small">Durum</td>
|
||||
<td><span class="badge bg-{{ $import->status_color }}">{{ $import->status_label }}</span></td>
|
||||
</tr>
|
||||
<tr><td class="text-muted small">Bölümler</td><td class="small">{{ $import->done_episodes }} / {{ $import->total_episodes }}</td></tr>
|
||||
<tr><td class="text-muted small">Hatalı</td><td class="small {{ $import->failed_episodes > 0 ? 'text-danger' : '' }}">{{ $import->failed_episodes }}</td></tr>
|
||||
<tr><td class="text-muted small">Oluşturulma</td><td class="small">{{ $import->created_at->format('d.m.Y H:i') }}</td></tr>
|
||||
@if($import->anime)
|
||||
<tr>
|
||||
<td class="text-muted small">Anime Kaydı</td>
|
||||
<td><a href="{{ route('admin.animes.show', $import->anime) }}" class="small text-info">{{ $import->anime->title }}</a></td>
|
||||
</tr>
|
||||
@endif
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($import->total_episodes > 0)
|
||||
<div class="card mb-4">
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
<span class="small">İlerleme</span>
|
||||
<span class="small">%{{ $import->progress_percent }}</span>
|
||||
</div>
|
||||
<div class="progress mb-2" style="height:8px">
|
||||
<div class="progress-bar bg-{{ $import->status_color }}" style="width:{{ $import->progress_percent }}%"></div>
|
||||
</div>
|
||||
<div class="text-muted small">{{ $import->done_episodes }}/{{ $import->total_episodes }} bölüm tamamlandı</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($import->current_step)
|
||||
<div class="card mb-4">
|
||||
<h6 class="mb-2">Şu An</h6>
|
||||
<p class="small mb-0 text-info">{{ $import->current_step }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($import->error_log)
|
||||
<div class="card mb-4">
|
||||
<h6 class="mb-2 text-danger">Hata Logu</h6>
|
||||
<pre style="background:var(--surface);padding:10px;border-radius:6px;font-size:.75rem;color:var(--danger);max-height:200px;overflow-y:auto">{{ $import->error_log }}</pre>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('admin.import.destroy', $import) }}" onsubmit="return confirm('Bu import job silinsin mi?')">
|
||||
@csrf @method('DELETE')
|
||||
<button class="btn btn-outline-danger btn-sm"><i class="bi bi-trash me-1"></i>Job'u Sil</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6">
|
||||
<div class="card">
|
||||
<h6 class="mb-3">Bu Job İçin Python Komutları</h6>
|
||||
<p class="text-muted small">Sadece bu job'u çalıştırmak için:</p>
|
||||
<pre style="background:var(--surface);border:1px solid var(--border2);border-radius:8px;padding:12px;font-size:.78rem;color:var(--info)">cd C:\Users\yusuf\Desktop\anizium_scraper
|
||||
python bot.py --job-id {{ $import->id }}</pre>
|
||||
|
||||
<p class="text-muted small mt-3">Tüm bekleyen job'ları çalıştırmak için:</p>
|
||||
<pre style="background:var(--surface);border:1px solid var(--border2);border-radius:8px;padding:12px;font-size:.78rem;color:var(--info)">python bot.py</pre>
|
||||
|
||||
@if($import->status === 'pending')
|
||||
<div class="alert alert-warning small mt-3 mb-0">
|
||||
<i class="bi bi-clock me-1"></i>Bu job bekliyor. Python script'i başlatın.
|
||||
</div>
|
||||
@elseif($import->status === 'done')
|
||||
<div class="alert alert-success small mt-3 mb-0">
|
||||
<i class="bi bi-check-circle me-1"></i>Tamamlandı!
|
||||
@if($import->anime)
|
||||
<a href="{{ route('admin.animes.show', $import->anime) }}" class="alert-link">Anime'yi görüntüle →</a>
|
||||
@endif
|
||||
</div>
|
||||
@elseif(in_array($import->status, ['downloading', 'uploading', 'fetching']))
|
||||
<div class="alert alert-info small mt-3 mb-0">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i>İşlem devam ediyor... Sayfayı yenile.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@push('scripts')
|
||||
@if(in_array($import->status, ['fetching', 'downloading', 'uploading']))
|
||||
<script>
|
||||
// Aktif job ise 5 saniyede bir yenile
|
||||
setTimeout(() => location.reload(), 5000);
|
||||
</script>
|
||||
@endif
|
||||
@endpush
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,178 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Mobil Uygulama')
|
||||
@section('page-title', 'Mobil Uygulama Yönetimi')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Bakım modu uyarısı --}}
|
||||
@if($settings['mobile_maintenance_mode'] == '1')
|
||||
<div class="alert alert-danger d-flex align-items-center gap-10 mb-4" style="gap:10px">
|
||||
<i class="bi bi-tools" style="font-size:18px"></i>
|
||||
<div style="font-size:13.5px;font-weight:700">Bakım Modu AKTİF — Kullanıcılar uygulamaya giremez!</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Stats --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="font-size:22px">{{ number_format($stats['total_users']) }}</div>
|
||||
<div class="stat-label mt-1">Kayıtlı Kullanıcı</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="font-size:22px;color:var(--success)">{{ number_format($stats['active_30d']) }}</div>
|
||||
<div class="stat-label mt-1">Aktif (30 gün)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="font-size:22px;color:var(--info)">{{ number_format($stats['fcm_tokens']) }}</div>
|
||||
<div class="stat-label mt-1">Push Token</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="font-size:22px;color:var(--warning)">{{ number_format($stats['notifs_unread']) }}</div>
|
||||
<div class="stat-label mt-1">Okunmamış</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="font-size:22px">{{ number_format($stats['notifications_sent']) }}</div>
|
||||
<div class="stat-label mt-1">Toplam Bildirim</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="font-size:22px;color:var(--accent-lt)">{{ number_format($stats['notifs_today']) }}</div>
|
||||
<div class="stat-label mt-1">Bugün Gönderilen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{{-- ── Sol: Ayarlar ── --}}
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-sliders" style="color:var(--accent-lt)"></i>
|
||||
<h6>Uygulama Ayarları</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.mobile.update') }}">
|
||||
@csrf
|
||||
|
||||
{{-- Bakım Modu --}}
|
||||
<div class="p-4 rounded mb-4" style="background:var(--danger-dim);border:1px solid rgba(239,68,68,.3)">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<div style="font-size:14px;font-weight:700;color:var(--danger);display:flex;align-items:center;gap:7px">
|
||||
<i class="bi bi-tools"></i> Bakım Modu
|
||||
</div>
|
||||
<div style="font-size:12.5px;color:var(--text);margin-top:3px">
|
||||
Açıldığında tüm kullanıcılar uygulamaya giremez.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input type="hidden" name="mobile_maintenance_mode" value="0">
|
||||
<input class="form-check-input" type="checkbox" name="mobile_maintenance_mode" value="1"
|
||||
id="maintenanceToggle" role="switch"
|
||||
{{ $settings['mobile_maintenance_mode']=='1'?'checked':'' }}
|
||||
style="width:2.5rem;height:1.3rem;cursor:pointer">
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-label">Bakım Mesajı</label>
|
||||
<textarea class="form-control" name="mobile_maintenance_message" rows="2">{{ $settings['mobile_maintenance_message'] }}</textarea>
|
||||
</div>
|
||||
|
||||
{{-- Zorla Güncelleme --}}
|
||||
<div class="p-4 rounded mb-4" style="background:var(--warning-dim);border:1px solid rgba(245,158,11,.3)">
|
||||
<div style="font-size:14px;font-weight:700;color:var(--warning);display:flex;align-items:center;gap:7px;margin-bottom:4px">
|
||||
<i class="bi bi-arrow-repeat"></i> Zorla Güncelleme
|
||||
</div>
|
||||
<div style="font-size:12.5px;color:var(--text);margin-bottom:16px">
|
||||
Minimum sürümü geçemeyen kullanıcılar zorunlu güncelleme ekranı görür.
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Minimum Sürüm <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" class="form-control" name="mobile_min_version"
|
||||
value="{{ $settings['mobile_min_version'] }}" placeholder="1.0.2">
|
||||
<div class="form-text">Bu sürümden eskiler güncellemeye zorlanır</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Güncel Sürüm <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" class="form-control" name="mobile_current_version"
|
||||
value="{{ $settings['mobile_current_version'] }}" placeholder="1.0.5">
|
||||
<div class="form-text">Kullanıcılara gösterilen güncel sürüm</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">APK İndirme Linki</label>
|
||||
<input type="url" class="form-control" name="mobile_apk_url"
|
||||
value="{{ $settings['mobile_apk_url'] }}"
|
||||
placeholder="https://animexe.com/app/animexe.apk">
|
||||
<div class="form-text">Boş bırakılırsa Play Store'a yönlendirir</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Güncelleme Mesajı</label>
|
||||
<textarea class="form-control" name="mobile_force_update_msg" rows="2">{{ $settings['mobile_force_update_msg'] }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-floppy"></i> Ayarları Kaydet
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Sağ: Durum & API ── --}}
|
||||
<div class="col-md-4">
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-activity" style="color:var(--success)"></i>
|
||||
<h6>Anlık Durum</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@foreach([
|
||||
['Uygulama', $settings['mobile_maintenance_mode']=='1' ? '<span class="badge bg-danger">Bakımda</span>' : '<span class="badge bg-success">Çalışıyor</span>'],
|
||||
['Minimum Sürüm', '<code>'.$settings['mobile_min_version'].'</code>'],
|
||||
['Güncel Sürüm', '<code>'.$settings['mobile_current_version'].'</code>'],
|
||||
['Push Token\'lı Cihaz', '<strong>'.number_format($stats['fcm_tokens']).'</strong>'],
|
||||
] as [$lbl, $val])
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px 16px;border-bottom:1px solid var(--border)">
|
||||
<span style="font-size:12.5px;color:var(--text2)">{{ $lbl }}</span>
|
||||
<span style="font-size:13px;color:var(--text)">{!! $val !!}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-code-slash" style="color:var(--info)"></i>
|
||||
<h6>API Endpoint</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="font-size:12.5px;color:var(--text2);margin-bottom:8px">Uygulama başlangıcında kontrol:</div>
|
||||
<code style="display:block;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:12px;color:var(--info);word-break:break-all">GET /api/app-status</code>
|
||||
<div style="font-size:12.5px;color:var(--text2);margin:12px 0 6px">Örnek yanıt:</div>
|
||||
<pre style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;font-size:11.5px;color:var(--text);overflow-x:auto;margin:0">{
|
||||
"maintenance": false,
|
||||
"min_version": "1.0.0",
|
||||
"current_version": "1.0.0",
|
||||
"force_update": false
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,133 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'İzin Düzenle — ' . $moderator->name)
|
||||
@section('page-title', $moderator->name . ' — İzin Yönetimi')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.perm-switch-row {
|
||||
display:flex; align-items:center; justify-content:space-between;
|
||||
padding:11px 0; border-bottom:1px solid var(--border);
|
||||
}
|
||||
.perm-switch-row:last-child { border-bottom:none; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="d-flex align-items-center gap-3 mb-4">
|
||||
<a href="{{ route('admin.moderators.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Geri Dön
|
||||
</a>
|
||||
<div>
|
||||
<div style="font-size:13px;color:var(--text2)">{{ $moderator->email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('admin.moderators.permissions', $moderator) }}">
|
||||
@csrf
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap gap-2">
|
||||
<div style="display:flex;align-items:center;gap:10px">
|
||||
<div style="width:40px;height:40px;border-radius:12px;background:linear-gradient(135deg,#f59e0b,#d97706);display:flex;align-items:center;justify-content:center;font-weight:700;font-size:15px;color:#000">
|
||||
{{ strtoupper(substr($moderator->name,0,1)) }}
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:15px;font-weight:700;color:var(--text)">{{ $moderator->name }}</div>
|
||||
<div style="font-size:12px;color:var(--text2)">
|
||||
<span style="color:var(--warning);font-weight:700" id="permCount">{{ count($permissions) }}</span> aktif izin
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-sm btn-secondary" onclick="selectAll(true)">
|
||||
<i class="bi bi-check-all"></i> Tümünü Ver
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="selectAll(false)">
|
||||
<i class="bi bi-x-circle"></i> Tümünü Kaldır
|
||||
</button>
|
||||
<button type="submit" class="btn btn-warning btn-sm">
|
||||
<i class="bi bi-floppy"></i> Kaydet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
@foreach($groups as $groupName => $keys)
|
||||
@php $groupGranted = collect(array_keys($keys))->filter(fn($k) => isset($permissions[$k]))->count(); @endphp
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-folder-fill" style="color:var(--warning)"></i>
|
||||
<h6>{{ $groupName }}</h6>
|
||||
<span class="badge {{ $groupGranted>0?'bg-success':'bg-secondary' }} ms-auto">
|
||||
{{ $groupGranted }}/{{ count($keys) }}
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-secondary" style="padding:3px 9px;font-size:11px"
|
||||
onclick="toggleGroup(this, {{ collect(array_keys($keys))->toJson() }})">
|
||||
Tümünü Seç
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@foreach($keys as $key => $label)
|
||||
<div class="perm-switch-row" data-perm="{{ $key }}">
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text)">{{ $label }}</div>
|
||||
<code style="font-size:11px;color:var(--text3)">{{ $key }}</code>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input perm-toggle" type="checkbox"
|
||||
name="permissions[]" value="{{ $key }}"
|
||||
{{ isset($permissions[$key]) ? 'checked' : '' }}
|
||||
data-perm="{{ $key }}"
|
||||
id="perm_{{ str_replace('.','_',$key) }}"
|
||||
role="switch"
|
||||
style="cursor:pointer">
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-4 d-flex gap-2">
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-floppy"></i> İzinleri Kaydet
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="selectAll(true)">
|
||||
<i class="bi bi-check-all"></i> Tüm İzinleri Ver
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="selectAll(false)">
|
||||
<i class="bi bi-x-circle"></i> Tüm İzinleri Kaldır
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function updateCount() {
|
||||
const n = document.querySelectorAll('.perm-toggle:checked').length;
|
||||
document.getElementById('permCount').textContent = n;
|
||||
}
|
||||
document.querySelectorAll('.perm-toggle').forEach(cb => cb.addEventListener('change', updateCount));
|
||||
|
||||
function selectAll(state) {
|
||||
document.querySelectorAll('.perm-toggle').forEach(cb => cb.checked = state);
|
||||
updateCount();
|
||||
}
|
||||
|
||||
function toggleGroup(btn, keys) {
|
||||
const anyUnchecked = keys.some(k => !document.querySelector(`[data-perm="${k}"].perm-toggle`)?.checked);
|
||||
keys.forEach(k => {
|
||||
const el = document.querySelector(`[data-perm="${k}"].perm-toggle`);
|
||||
if (el) el.checked = anyUnchecked;
|
||||
});
|
||||
btn.textContent = anyUnchecked ? 'Tümünü Kaldır' : 'Tümünü Seç';
|
||||
updateCount();
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,189 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Moderatör Yönetimi')
|
||||
@section('page-title', 'Moderatör Yönetimi')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">Moderatör Yönetimi</h5>
|
||||
<div style="font-size:13px;color:var(--text2);margin-top:2px">
|
||||
{{ $moderators->total() }} aktif moderatör · {{ count($groups) }} izin grubu
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-warning btn-sm" data-bs-toggle="modal" data-bs-target="#promoteModal">
|
||||
<i class="bi bi-person-plus"></i> Moderatör Ekle
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Tablo --}}
|
||||
<div class="table-wrap mb-4">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px">Kullanıcı</th>
|
||||
<th style="text-align:center">İzin Sayısı</th>
|
||||
<th>İzin Grupları</th>
|
||||
<th>Katılım Tarihi</th>
|
||||
<th style="text-align:right;padding-right:20px">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($moderators as $mod)
|
||||
@php
|
||||
$perms = $mod->moderatorPermissions->pluck('permission');
|
||||
$grantedGroups = collect($groups)->filter(fn($keys) => collect(array_keys($keys))->intersect($perms)->isNotEmpty());
|
||||
@endphp
|
||||
<tr>
|
||||
<td style="padding-left:20px">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="user-avatar" style="border-radius:12px;background:linear-gradient(135deg,#f59e0b,#d97706)">
|
||||
{{ strtoupper(substr($mod->name,0,1)) }}
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:700;color:var(--text)">{{ $mod->name }}</div>
|
||||
<div style="font-size:12px;color:var(--text2)">{{ $mod->email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
<span class="badge {{ $mod->moderator_permissions_count > 0 ? 'bg-success' : 'bg-secondary' }}">
|
||||
{{ $mod->moderator_permissions_count }} izin
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
@foreach($grantedGroups->keys() as $gName)
|
||||
<span class="badge bg-warning" style="color:#000;font-size:10.5px">{{ $gName }}</span>
|
||||
@endforeach
|
||||
@if($grantedGroups->isEmpty())
|
||||
<span style="font-size:12px;color:var(--text3)">— izin yok —</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-size:12.5px;color:var(--text2)">{{ $mod->created_at->format('d.m.Y') }}</td>
|
||||
<td style="text-align:right;padding-right:20px">
|
||||
<div class="d-flex gap-2 justify-content-end">
|
||||
<a href="{{ route('admin.moderators.edit',$mod) }}" class="btn btn-sm btn-outline-warning">
|
||||
<i class="bi bi-sliders"></i> İzinler
|
||||
</a>
|
||||
<form method="POST" action="{{ route('admin.moderators.demote',$mod) }}"
|
||||
onsubmit="return confirm('{{ addslashes($mod->name) }} moderatörlükten çıkarılsın mı?')">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-person-dash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-shield-check"></i>
|
||||
<h6>Henüz moderatör yok</h6>
|
||||
<p>Kullanıcıları moderatör olarak atayın.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- İzin Grupları --}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-key" style="color:var(--warning)"></i>
|
||||
<h6>Mevcut İzin Grupları</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
@foreach($groups as $groupName => $keys)
|
||||
<div class="col-md-4">
|
||||
<div style="padding:14px;border:1px solid rgba(245,158,11,.2);border-radius:var(--radius-sm);background:rgba(245,158,11,.04)">
|
||||
<div style="font-size:12px;font-weight:800;color:var(--warning);text-transform:uppercase;letter-spacing:.8px;margin-bottom:10px">
|
||||
<i class="bi bi-folder-fill me-1"></i>{{ $groupName }}
|
||||
</div>
|
||||
@foreach($keys as $key => $label)
|
||||
<div style="display:flex;align-items:center;gap:7px;padding:3px 0">
|
||||
<code style="font-size:11px;color:var(--info);background:var(--info-dim);padding:1px 5px;border-radius:4px">{{ $key }}</code>
|
||||
<span style="font-size:12px;color:var(--text2)">{{ $label }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Promote Modal --}}
|
||||
<div class="modal fade" id="promoteModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title" style="color:var(--warning)">
|
||||
<i class="bi bi-person-plus me-2"></i>Moderatör Ekle
|
||||
</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('admin.moderators.promote') }}">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<div class="p-3 rounded mb-3" style="background:var(--warning-dim);border:1px solid rgba(245,158,11,.25);font-size:13px;color:var(--text)">
|
||||
Normal bir kullanıcıyı moderatör olarak atayın. Sonrasında İzinler butonuyla yetkileri belirleyebilirsiniz.
|
||||
</div>
|
||||
<label class="form-label">Kullanıcı Ara</label>
|
||||
<input type="text" name="user_id" id="userSearch" class="form-control"
|
||||
placeholder="İsim veya e-posta ile ara..." autocomplete="off">
|
||||
<div id="userSearchResults" class="mt-1" style="position:relative;z-index:100"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">İptal</button>
|
||||
<button type="submit" class="btn btn-warning">
|
||||
<i class="bi bi-shield-plus"></i> Moderatör Yap
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
let searchTimer;
|
||||
const inp = document.getElementById('userSearch');
|
||||
const res = document.getElementById('userSearchResults');
|
||||
if (inp) {
|
||||
inp.addEventListener('input', function() {
|
||||
clearTimeout(searchTimer);
|
||||
if (this.value.length < 2) { res.innerHTML=''; return; }
|
||||
searchTimer = setTimeout(() => {
|
||||
fetch(`/admin/users?q=${encodeURIComponent(this.value)}&json=1`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.length) { res.innerHTML = '<div style="font-size:12.5px;color:var(--text2);padding:8px">Kullanıcı bulunamadı.</div>'; return; }
|
||||
res.innerHTML = data.map(u => `
|
||||
<div class="d-flex align-items-center gap-2 p-2 mb-1 rounded" style="background:var(--surface2);border:1px solid var(--border);cursor:pointer" onclick="selectUser(${u.id},'${u.name} — ${u.email}')">
|
||||
<div style="width:28px;height:28px;border-radius:8px;background:var(--accent-dim);color:var(--accent-lt);display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0">${u.name.charAt(0).toUpperCase()}</div>
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text)">${u.name}</div>
|
||||
<div style="font-size:11.5px;color:var(--text2)">${u.email}</div>
|
||||
</div>
|
||||
</div>`).join('');
|
||||
});
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
function selectUser(id, label) {
|
||||
inp.value = id;
|
||||
res.innerHTML = `<div style="font-size:12.5px;color:var(--success);padding:6px 0;display:flex;align-items:center;gap:6px"><i class="bi bi-check-circle-fill"></i>${label} seçildi</div>`;
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,243 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Bildirimler')
|
||||
@section('page-title', 'Bildirim Yönetimi')
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Stats --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value">{{ number_format($stats['total']) }}</div>
|
||||
<div class="stat-label mt-1">Toplam Bildirim</div>
|
||||
</div>
|
||||
<div class="stat-icon violet"><i class="bi bi-bell-fill"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value">{{ number_format($stats['unread']) }}</div>
|
||||
<div class="stat-label mt-1">Okunmamış</div>
|
||||
</div>
|
||||
<div class="stat-icon orange"><i class="bi bi-eye-slash-fill"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value">{{ number_format($stats['users']) }}</div>
|
||||
<div class="stat-label mt-1">Kullanıcı</div>
|
||||
</div>
|
||||
<div class="stat-icon green"><i class="bi bi-people-fill"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-top">
|
||||
<div>
|
||||
<div class="stat-value">{{ number_format($stats['today']) }}</div>
|
||||
<div class="stat-label mt-1">Bugün Gönderildi</div>
|
||||
</div>
|
||||
<div class="stat-icon cyan"><i class="bi bi-send-fill"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{{-- ── Sol: Gönder ── --}}
|
||||
<div class="col-lg-5">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-send" style="color:var(--accent-lt)"></i>
|
||||
<h6>Bildirim Gönder</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.notifications.send') }}">
|
||||
@csrf
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Başlık <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" name="title" class="form-control"
|
||||
value="{{ old('title') }}" placeholder="Yeni Bölüm Yüklendi!" maxlength="100" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Mesaj <span style="color:var(--danger)">*</span></label>
|
||||
<textarea name="body" id="notif-body" class="form-control" rows="3"
|
||||
placeholder="Yeni bölümler yayında, hemen izle!" maxlength="500" required>{{ old('body') }}</textarea>
|
||||
<div class="form-text">
|
||||
<span id="bodyCount">{{ strlen(old('body','')) }}</span>/500 karakter
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
Link <span style="color:var(--text2);font-size:11px">(opsiyonel)</span>
|
||||
</label>
|
||||
<input type="url" name="url" class="form-control"
|
||||
value="{{ old('url') }}" placeholder="https://...">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">İkon</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text" style="min-width:42px;justify-content:center">
|
||||
<i id="iconPreview" class="bi bi-megaphone-fill" style="color:var(--accent-lt)"></i>
|
||||
</span>
|
||||
<input type="text" name="icon" id="iconInput" class="form-control"
|
||||
value="{{ old('icon','bi-megaphone-fill') }}" placeholder="bi-megaphone-fill">
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap mt-2">
|
||||
@foreach(['bi-megaphone-fill','bi-star-fill','bi-bell-fill','bi-gift-fill','bi-play-circle-fill','bi-trophy-fill','bi-info-circle-fill','bi-exclamation-triangle-fill'] as $ic)
|
||||
<button type="button" class="icon-preset"
|
||||
style="width:32px;height:32px;border-radius:8px;border:1px solid var(--border2);background:var(--surface2);color:var(--text2);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;transition:all .12s"
|
||||
data-icon="{{ $ic }}" title="{{ $ic }}">
|
||||
<i class="bi {{ $ic }}"></i>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Hedef Kitle <span style="color:var(--danger)">*</span></label>
|
||||
<select name="target" class="form-select" required>
|
||||
<option value="all" {{ old('target','all')==='all' ?'selected':'' }}>
|
||||
Tüm Kullanıcılar ({{ number_format($stats['users']) }})
|
||||
</option>
|
||||
<option value="premium" {{ old('target')==='premium' ?'selected':'' }}>Sadece Premium Üyeler</option>
|
||||
<option value="free" {{ old('target')==='free' ?'selected':'' }}>Sadece Ücretsiz Üyeler</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100"
|
||||
onclick="return confirm('Bildirim gönderilsin mi?')">
|
||||
<i class="bi bi-send"></i> Bildirimi Gönder
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Sağ: Son Bildirimler ── --}}
|
||||
<div class="col-lg-7">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-clock-history" style="color:var(--info)"></i>
|
||||
<h6>Son Bildirimler</h6>
|
||||
<span style="font-size:12px;color:var(--text2);margin-left:4px">Son 50</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px">Kullanıcı</th>
|
||||
<th>Tür</th>
|
||||
<th>İçerik</th>
|
||||
<th style="text-align:center">Durum</th>
|
||||
<th>Tarih</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($recent as $n)
|
||||
@php $d = $n->data; @endphp
|
||||
<tr>
|
||||
<td style="padding-left:20px;font-size:13px;font-weight:600;color:var(--text)">
|
||||
{{ $n->user?->name ?? '—' }}
|
||||
@if($n->user?->username)
|
||||
<div style="font-size:11.5px;font-weight:400;color:var(--text2)">@{{ $n->user->username }}</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($n->type === 'admin')
|
||||
<span class="badge" style="background:var(--accent-dim);color:var(--accent-lt)">Admin</span>
|
||||
@elseif($n->type === 'episode')
|
||||
<span class="badge bg-success">Bölüm</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">{{ $n->type }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="max-width:180px">
|
||||
@if($n->type === 'admin')
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text)">{{ $d['title'] ?? '' }}</div>
|
||||
@if(!empty($d['body']))
|
||||
<div style="font-size:12px;color:var(--text2)">{{ Str::limit($d['body'],55) }}</div>
|
||||
@endif
|
||||
@elseif($n->type === 'episode')
|
||||
<div style="font-size:12.5px;color:var(--info);font-weight:600">{{ $d['anime_title'] ?? '' }}</div>
|
||||
<div style="font-size:12px;color:var(--text2)">{{ $d['episode_number'] ?? '' }}. Bölüm</div>
|
||||
@endif
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
@if($n->read_at)
|
||||
<span style="font-size:12px;color:var(--text3);display:inline-flex;align-items:center;gap:3px">
|
||||
<i class="bi bi-check2-all" style="font-size:11px"></i> Okundu
|
||||
</span>
|
||||
@else
|
||||
<span style="font-size:12px;color:var(--warning);display:inline-flex;align-items:center;gap:4px">
|
||||
<i class="bi bi-circle-fill" style="font-size:6px"></i> Yeni
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="font-size:12px;color:var(--text2)">{{ $n->created_at->format('d.m H:i') }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state py-4">
|
||||
<i class="bi bi-bell"></i>
|
||||
<p>Henüz bildirim gönderilmemiş.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Karakter sayacı
|
||||
const bodyTa = document.getElementById('notif-body');
|
||||
const bodyCount = document.getElementById('bodyCount');
|
||||
if (bodyTa && bodyCount) {
|
||||
bodyTa.addEventListener('input', () => {
|
||||
bodyCount.textContent = bodyTa.value.length;
|
||||
bodyCount.style.color = bodyTa.value.length > 450 ? 'var(--danger)' : '';
|
||||
});
|
||||
}
|
||||
// İkon preview
|
||||
const iconInput = document.getElementById('iconInput');
|
||||
const iconPreview = document.getElementById('iconPreview');
|
||||
if (iconInput) {
|
||||
iconInput.addEventListener('input', () => {
|
||||
iconPreview.className = 'bi ' + iconInput.value.trim();
|
||||
});
|
||||
}
|
||||
document.querySelectorAll('.icon-preset').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const ic = btn.dataset.icon;
|
||||
if (iconInput) { iconInput.value = ic; }
|
||||
if (iconPreview) { iconPreview.className = 'bi ' + ic; }
|
||||
document.querySelectorAll('.icon-preset').forEach(b => b.style.borderColor = '');
|
||||
btn.style.borderColor = 'var(--accent)';
|
||||
btn.style.background = 'var(--accent-dim)';
|
||||
btn.style.color = 'var(--accent-lt)';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,53 @@
|
||||
@if ($paginator->hasPages())
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px">
|
||||
<div style="font-size:12.5px;color:var(--text2)">
|
||||
<strong style="color:var(--text)">{{ $paginator->firstItem() }}–{{ $paginator->lastItem() }}</strong>
|
||||
/ {{ number_format($paginator->total()) }} sonuç
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm mb-0 gap-1">
|
||||
{{-- Previous --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-left"></i></span>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ $paginator->previousPageUrl() }}" rel="prev">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
{{-- Pages --}}
|
||||
@foreach ($elements as $element)
|
||||
@if (is_string($element))
|
||||
<li class="page-item disabled"><span class="page-link">…</span></li>
|
||||
@endif
|
||||
@if (is_array($element))
|
||||
@foreach ($element as $page => $url)
|
||||
@if ($page == $paginator->currentPage())
|
||||
<li class="page-item active"><span class="page-link">{{ $page }}</span></li>
|
||||
@else
|
||||
<li class="page-item"><a class="page-link" href="{{ $url }}">{{ $page }}</a></li>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- Next --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="{{ $paginator->nextPageUrl() }}" rel="next">
|
||||
<i class="bi bi-chevron-right"></i>
|
||||
</a>
|
||||
</li>
|
||||
@else
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link"><i class="bi bi-chevron-right"></i></span>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@endif
|
||||
@@ -0,0 +1,74 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Global İzin Ayarları')
|
||||
@section('page-title', 'Global İzin Ayarları')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-7 col-lg-8">
|
||||
|
||||
<div class="card mb-3" style="border-color:rgba(245,158,11,.2);background:rgba(245,158,11,.04)">
|
||||
<div class="card-body" style="padding:14px 18px">
|
||||
<div style="font-size:13.5px;font-weight:700;color:var(--warning);display:flex;align-items:center;gap:7px;margin-bottom:4px">
|
||||
<i class="bi bi-info-circle-fill"></i> Genel Bilgi
|
||||
</div>
|
||||
<div style="font-size:13px;color:var(--text)">
|
||||
Bu ayarlar tüm site için geçerlidir. Belirli anime veya bölümler için
|
||||
anime/bölüm sayfasından override yapabilirsiniz.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-shield-lock" style="color:var(--accent-lt)"></i>
|
||||
<h6>Global İzin Ayarları</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<form method="POST" action="{{ route('admin.permissions.update') }}">
|
||||
@csrf
|
||||
@forelse($permissions as $perm)
|
||||
<div class="perm-row">
|
||||
<div>
|
||||
<div class="perm-name">{{ $perm->label }}</div>
|
||||
@if($perm->description)
|
||||
<div class="perm-desc">{{ $perm->description }}</div>
|
||||
@endif
|
||||
<div style="margin-top:3px">
|
||||
<code style="font-size:11px;color:var(--text3);background:var(--surface);padding:1px 6px;border-radius:4px">{{ $perm->key }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="perm-toggle">
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]"
|
||||
id="g_{{ $perm->key }}_free" value="free"
|
||||
{{ $perm->required_membership === 'free' ? 'checked' : '' }}>
|
||||
<label for="g_{{ $perm->key }}_free" class="free">Ücretsiz</label>
|
||||
|
||||
<input type="radio" name="permissions[{{ $perm->key }}]"
|
||||
id="g_{{ $perm->key }}_prem" value="premium"
|
||||
{{ $perm->required_membership === 'premium' ? 'checked' : '' }}>
|
||||
<label for="g_{{ $perm->key }}_prem" class="premium">Premium</label>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-shield-lock"></i>
|
||||
<h6>İzin ayarı bulunamadı</h6>
|
||||
<p>Seeder'ı çalıştırdın mı?</p>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
@if($permissions->count() > 0)
|
||||
<div class="p-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-floppy"></i> Kaydet
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,202 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Plan Ekle')
|
||||
@section('page-title', 'Plan Ekle')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.perk-label {
|
||||
display:flex; align-items:flex-start; gap:10px;
|
||||
padding:10px 12px; border-radius:var(--radius-sm);
|
||||
border:1px solid var(--border);
|
||||
background:var(--surface2);
|
||||
cursor:pointer; transition:border-color .12s, background .12s;
|
||||
}
|
||||
.perk-label:hover { border-color:var(--border2); background:rgba(255,255,255,.04); }
|
||||
.perk-label:has(input:checked) { border-color:var(--accent); background:var(--accent-dim); }
|
||||
.perk-category { font-size:11px; font-weight:800; text-transform:uppercase; letter-spacing:1px; color:var(--accent-lt); margin-bottom:10px; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="mb-4">
|
||||
<a href="{{ route('admin.plans.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Planlara Dön
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-8 col-lg-9">
|
||||
<form method="POST" action="{{ route('admin.plans.store') }}">
|
||||
@csrf
|
||||
|
||||
{{-- Temel --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-star" style="color:var(--warning)"></i>
|
||||
<h6>Plan Bilgileri</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plan Adı <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" name="name" class="form-control" value="{{ old('name') }}" placeholder="Aylık Premium" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Açıklama</label>
|
||||
<textarea name="description" class="form-control" rows="2" placeholder="Plan hakkında kısa açıklama...">{{ old('description') }}</textarea>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Fiyat (₺) <span style="color:var(--danger)">*</span></label>
|
||||
<input type="number" name="price" class="form-control" step="0.01" value="{{ old('price','0') }}" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Süre (gün) <span style="color:var(--danger)">*</span></label>
|
||||
<input type="number" name="duration_days" class="form-control" value="{{ old('duration_days',30) }}" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Deneme Süresi (gün)</label>
|
||||
<input type="number" name="trial_days" class="form-control" value="{{ old('trial_days',0) }}" min="0">
|
||||
<div class="form-text">0 = deneme yok</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Sıralama</label>
|
||||
<input type="number" name="sort_order" class="form-control" value="{{ old('sort_order',0) }}">
|
||||
</div>
|
||||
</div>
|
||||
<hr style="border-color:rgba(255,255,255,.07);margin:18px 0">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Satın Alma Linki</label>
|
||||
<input type="url" name="purchase_link" class="form-control" placeholder="https://..." value="{{ old('purchase_link') }}">
|
||||
<div class="form-text">Kullanıcı "Satın Al" butonuna tıklayınca buraya yönlendirilir.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Rozet Etiketi</label>
|
||||
<input type="text" name="badge_label" class="form-control" placeholder="EN POPÜLER" value="{{ old('badge_label') }}" maxlength="32">
|
||||
<div class="form-text">Plan kartının üstüne çıkar. Boş = rozet yok.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Vurgu Rengi</label>
|
||||
<input type="text" name="accent_color" class="form-control" placeholder="cyan / pink / gold" value="{{ old('accent_color') }}" maxlength="16">
|
||||
<div class="form-text">cyan, pink veya gold</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Özellikler --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-list-check" style="color:var(--success)"></i>
|
||||
<h6>Görünür Özellikler</h6>
|
||||
<span style="font-size:12px;color:var(--text2)">— Premium sayfasında listeler</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="features-wrap" class="d-flex flex-column gap-2">
|
||||
<div class="input-group">
|
||||
<input type="text" name="features[]" class="form-control" placeholder="Reklamsız izleme">
|
||||
<button type="button" class="btn btn-success btn-sm" onclick="addFeature()"><i class="bi bi-plus-lg"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Perkler --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-key" style="color:var(--accent-lt)"></i>
|
||||
<h6>Premium Özellikler (Perk'ler)</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="font-size:13px;color:var(--text2);margin-bottom:16px">
|
||||
Bu planda hangi premium özelliklerin aktif olacağını seçin.
|
||||
</div>
|
||||
@foreach($allPerks as $category => $perks)
|
||||
<div class="mb-4">
|
||||
<div class="perk-category">{{ $category }}</div>
|
||||
<div class="row g-2">
|
||||
@foreach($perks as $key => $meta)
|
||||
<div class="col-md-6">
|
||||
<label class="perk-label">
|
||||
<input type="checkbox" name="perks[]" value="{{ $key }}" class="form-check-input mt-0 flex-shrink-0"
|
||||
{{ in_array($key, old('perks',[])) ? 'checked' : '' }}>
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:6px">
|
||||
<i class="bi {{ $meta['icon'] }}" style="color:var(--accent-lt);font-size:13px"></i>
|
||||
{{ $meta['name'] }}
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text2);margin-top:2px">{{ $meta['description'] }}</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Görünürlük --}}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-eye" style="color:var(--info)"></i>
|
||||
<h6>Görünürlük Ayarları</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded" style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Aktif</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Plan kullanılabilir durumda</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" value="1" id="is_active" role="switch" checked>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded" style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Herkese Görünür</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Premium sayfasında listele</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_public" value="1" id="is_public" role="switch" checked>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Görünürlük Bitiş Tarihi <span style="color:var(--text2);font-size:11px">(opsiyonel)</span></label>
|
||||
<input type="datetime-local" name="visible_until" class="form-control" value="{{ old('visible_until') }}">
|
||||
<div class="form-text">Bu tarihten sonra plan otomatik kaldırılır. Boş = süresiz.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary flex-grow-1 btn-lg">
|
||||
<i class="bi bi-plus-circle"></i> Plan Ekle
|
||||
</button>
|
||||
<a href="{{ route('admin.plans.index') }}" class="btn btn-secondary">İptal</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function addFeature() {
|
||||
const wrap = document.getElementById('features-wrap');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'input-group';
|
||||
div.innerHTML = `<input type="text" name="features[]" class="form-control" placeholder="Özellik...">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="this.parentElement.remove()"><i class="bi bi-dash-lg"></i></button>`;
|
||||
wrap.appendChild(div);
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,198 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Plan Düzenle')
|
||||
@section('page-title', 'Plan Düzenle: ' . $plan->name)
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.perk-label { display:flex; align-items:flex-start; gap:10px; padding:10px 12px; border-radius:var(--radius-sm); border:1px solid var(--border); background:var(--surface2); cursor:pointer; transition:border-color .12s, background .12s; }
|
||||
.perk-label:hover { border-color:var(--border2); background:rgba(255,255,255,.04); }
|
||||
.perk-label:has(input:checked) { border-color:var(--accent); background:var(--accent-dim); }
|
||||
.perk-category { font-size:11px; font-weight:800; text-transform:uppercase; letter-spacing:1px; color:var(--accent-lt); margin-bottom:10px; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="mb-4">
|
||||
<a href="{{ route('admin.plans.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Planlara Dön
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-8 col-lg-9">
|
||||
<form method="POST" action="{{ route('admin.plans.update', $plan) }}">
|
||||
@csrf @method('PUT')
|
||||
|
||||
{{-- Temel --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-star" style="color:var(--warning)"></i>
|
||||
<h6>Plan Bilgileri</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Plan Adı <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" name="name" class="form-control" value="{{ old('name',$plan->name) }}" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Açıklama</label>
|
||||
<textarea name="description" class="form-control" rows="2">{{ old('description',$plan->description) }}</textarea>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Fiyat (₺)</label>
|
||||
<input type="number" name="price" class="form-control" step="0.01" value="{{ old('price',$plan->price) }}" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Süre (gün)</label>
|
||||
<input type="number" name="duration_days" class="form-control" value="{{ old('duration_days',$plan->duration_days) }}" required>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Deneme (gün)</label>
|
||||
<input type="number" name="trial_days" class="form-control" value="{{ old('trial_days',$plan->trial_days??0) }}" min="0">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Sıralama</label>
|
||||
<input type="number" name="sort_order" class="form-control" value="{{ old('sort_order',$plan->sort_order) }}">
|
||||
</div>
|
||||
</div>
|
||||
<hr style="border-color:rgba(255,255,255,.07);margin:18px 0">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Satın Alma Linki</label>
|
||||
<input type="url" name="purchase_link" class="form-control" placeholder="https://..." value="{{ old('purchase_link',$plan->purchase_link) }}">
|
||||
<div class="form-text">Kullanıcı "Satın Al" butonuna tıklayınca buraya yönlendirilir.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Rozet Etiketi</label>
|
||||
<input type="text" name="badge_label" class="form-control" placeholder="EN POPÜLER" value="{{ old('badge_label',$plan->badge_label) }}" maxlength="32">
|
||||
<div class="form-text">Plan kartının üstüne çıkar. Boş = rozet yok.</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Vurgu Rengi</label>
|
||||
<input type="text" name="accent_color" class="form-control" placeholder="cyan / pink / gold" value="{{ old('accent_color',$plan->accent_color) }}" maxlength="16">
|
||||
<div class="form-text">cyan, pink veya gold</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Özellikler --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-list-check" style="color:var(--success)"></i>
|
||||
<h6>Görünür Özellikler</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="features-wrap" class="d-flex flex-column gap-2">
|
||||
@foreach(old('features',$plan->features??[]) as $f)
|
||||
<div class="input-group">
|
||||
<input type="text" name="features[]" class="form-control" value="{{ $f }}">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="this.parentElement.remove()"><i class="bi bi-dash-lg"></i></button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary btn-sm mt-2" onclick="addFeature()">
|
||||
<i class="bi bi-plus-circle"></i> Özellik Ekle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Perkler --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-key" style="color:var(--accent-lt)"></i>
|
||||
<h6>Premium Özellikler (Perk'ler)</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@php $currentPerks = old('perks', array_keys(array_filter($plan->perks ?? []))); @endphp
|
||||
@foreach($allPerks as $category => $perks)
|
||||
<div class="mb-4">
|
||||
<div class="perk-category">{{ $category }}</div>
|
||||
<div class="row g-2">
|
||||
@foreach($perks as $key => $meta)
|
||||
<div class="col-md-6">
|
||||
<label class="perk-label">
|
||||
<input type="checkbox" name="perks[]" value="{{ $key }}" class="form-check-input mt-0 flex-shrink-0"
|
||||
{{ in_array($key,$currentPerks) ? 'checked' : '' }}>
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:700;color:var(--text);display:flex;align-items:center;gap:6px">
|
||||
<i class="bi {{ $meta['icon'] }}" style="color:var(--accent-lt);font-size:13px"></i>
|
||||
{{ $meta['name'] }}
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--text2);margin-top:2px">{{ $meta['description'] }}</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Görünürlük --}}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-eye" style="color:var(--info)"></i>
|
||||
<h6>Görünürlük Ayarları</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded" style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Aktif</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Plan kullanılabilir</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_active" value="1" id="is_active" role="switch" {{ $plan->is_active?'checked':'' }}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded" style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Herkese Görünür</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Premium sayfasında listele</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="is_public" value="1" id="is_public" role="switch" {{ ($plan->is_public??true)?'checked':'' }}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Görünürlük Bitiş Tarihi <span style="color:var(--text2);font-size:11px">(opsiyonel)</span></label>
|
||||
<input type="datetime-local" name="visible_until" class="form-control"
|
||||
value="{{ old('visible_until',$plan->visible_until?$plan->visible_until->format('Y-m-d\TH:i'):'') }}">
|
||||
<div class="form-text">Bu tarihten sonra plan otomatik kaldırılır. Boş = süresiz.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary flex-grow-1 btn-lg">
|
||||
<i class="bi bi-floppy"></i> Değişiklikleri Kaydet
|
||||
</button>
|
||||
<a href="{{ route('admin.plans.index') }}" class="btn btn-secondary">İptal</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function addFeature() {
|
||||
const wrap = document.getElementById('features-wrap');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'input-group';
|
||||
div.innerHTML = `<input type="text" name="features[]" class="form-control" placeholder="Özellik...">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="this.parentElement.remove()"><i class="bi bi-dash-lg"></i></button>`;
|
||||
wrap.appendChild(div);
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,93 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Üyelik Planları')
|
||||
@section('page-title', 'Üyelik Planları')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">Üyelik Planları</h5>
|
||||
<div style="font-size:13px;color:var(--text2);margin-top:2px">{{ $plans->count() }} plan tanımlı</div>
|
||||
</div>
|
||||
<a href="{{ route('admin.plans.create') }}" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg"></i> Plan Ekle
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if($plans->isEmpty())
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-star"></i>
|
||||
<h6>Henüz plan eklenmemiş</h6>
|
||||
<p>İlk üyelik planınızı oluşturun.</p>
|
||||
<a href="{{ route('admin.plans.create') }}" class="btn btn-primary btn-sm mt-2">
|
||||
<i class="bi bi-plus-lg"></i> Plan Ekle
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="row g-3">
|
||||
@foreach($plans as $plan)
|
||||
<div class="col-xl-3 col-md-4 col-sm-6">
|
||||
<div class="card h-100" style="transition:border-color .15s">
|
||||
<div class="card-body" style="padding:22px">
|
||||
{{-- Header --}}
|
||||
<div class="d-flex align-items-start justify-content-between gap-2 mb-3">
|
||||
<div>
|
||||
<div style="font-size:16px;font-weight:800;color:var(--text)">{{ $plan->name }}</div>
|
||||
<div style="font-size:12px;color:var(--text2);margin-top:2px">
|
||||
<i class="bi bi-calendar3 me-1"></i>{{ $plan->duration_days }} gün
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge {{ $plan->is_active ? 'bg-success' : 'bg-secondary' }}">
|
||||
{{ $plan->is_active ? 'Aktif' : 'Pasif' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- Fiyat --}}
|
||||
<div style="font-size:28px;font-weight:800;color:var(--warning);letter-spacing:-0.5px;margin-bottom:14px">
|
||||
{{ number_format($plan->price, 2) }}
|
||||
<span style="font-size:14px;font-weight:600;color:var(--text2)">₺</span>
|
||||
</div>
|
||||
|
||||
@if($plan->description)
|
||||
<div style="font-size:12.5px;color:var(--text2);margin-bottom:12px;line-height:1.5">
|
||||
{{ $plan->description }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Özellikler --}}
|
||||
@if($plan->features)
|
||||
<div style="margin-bottom:16px">
|
||||
@foreach($plan->features as $feature)
|
||||
<div style="display:flex;align-items:center;gap:7px;font-size:12.5px;color:var(--text);padding:4px 0;border-bottom:1px solid var(--border)">
|
||||
<i class="bi bi-check-circle-fill" style="color:var(--success);font-size:12px;flex-shrink:0"></i>
|
||||
{{ $feature }}
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Aksiyonlar --}}
|
||||
<div class="d-flex gap-2 mt-auto">
|
||||
<a href="{{ route('admin.plans.edit', $plan) }}" class="btn btn-secondary btn-sm flex-grow-1">
|
||||
<i class="bi bi-pencil"></i> Düzenle
|
||||
</a>
|
||||
<form method="POST" action="{{ route('admin.plans.destroy', $plan) }}"
|
||||
onsubmit="return confirm('{{ addslashes($plan->name) }} planını silmek istediğinize emin misiniz?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash3"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,417 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Ayarlar')
|
||||
@section('page-title', 'Site Ayarları')
|
||||
|
||||
@section('content')
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-6 col-lg-7">
|
||||
|
||||
<form method="POST" action="{{ route('admin.settings.update') }}">
|
||||
@csrf
|
||||
|
||||
{{-- Genel --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-globe" style="color:var(--accent-lt)"></i>
|
||||
<h6>Genel Ayarlar</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Site Adı</label>
|
||||
<input type="text" name="site_name" class="form-control" value="{{ $settings['site_name']->value ?? 'Animexe' }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Site Açıklaması</label>
|
||||
<textarea name="site_description" class="form-control" rows="2">{{ $settings['site_description']->value ?? '' }}</textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">İletişim E-postası</label>
|
||||
<input type="email" name="contact_email" class="form-control" value="{{ $settings['contact_email']->value ?? '' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- BunnyCDN --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-cloud" style="color:var(--info)"></i>
|
||||
<h6>BunnyCDN</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Storage Zone Adı</label>
|
||||
<input type="text" name="bunnycdn_zone" class="form-control" value="{{ $settings['bunnycdn_zone']->value ?? '' }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">API Key</label>
|
||||
<input type="password" name="bunnycdn_api_key" class="form-control" value="{{ $settings['bunnycdn_api_key']->value ?? '' }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Pull Zone URL</label>
|
||||
<input type="text" name="bunnycdn_pull_url" class="form-control" value="{{ $settings['bunnycdn_pull_url']->value ?? '' }}" placeholder="https://animexe.b-cdn.net">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
Token Auth Security Key
|
||||
<span style="color:var(--text2);font-size:11px">— BunnyCDN → Pull Zone → Security</span>
|
||||
</label>
|
||||
<input type="password" name="bunnycdn_token_key" class="form-control" value="{{ $settings['bunnycdn_token_key']->value ?? '' }}" placeholder="Boş = token imzalama kapalı">
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label">Token Geçerlilik Süresi (dakika)</label>
|
||||
<input type="number" name="bunnycdn_token_ttl" class="form-control" min="10" max="1440"
|
||||
value="{{ $settings['bunnycdn_token_ttl']->value ?? 120 }}" style="max-width:140px">
|
||||
<div class="form-text">URL bu kadar dakika sonra geçersiz olur. IDM / direkt link çalışmaz.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Intro Video --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-play-circle" style="color:var(--warning)"></i>
|
||||
<h6>Intro Video</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="font-size:13px;color:var(--text2);margin-bottom:16px">
|
||||
Her bölüm başında oynatılacak tanıtım videosu. Intro bittikten sonra asıl video
|
||||
<strong style="color:var(--text)">belirtilen saniyeden</strong> başlar.
|
||||
</div>
|
||||
|
||||
@if($settings['intro_video_url']->value ?? null)
|
||||
<div class="mb-3 p-3 rounded" style="background:var(--info-dim);border:1px solid rgba(6,182,212,.2)">
|
||||
<div style="font-size:12px;color:var(--text2);margin-bottom:6px">Mevcut intro:</div>
|
||||
<code style="font-size:11.5px;color:var(--info);word-break:break-all;display:block;margin-bottom:8px">{{ $settings['intro_video_url']->value }}</code>
|
||||
<video src="{{ $settings['intro_video_url']->value }}" controls style="max-width:100%;max-height:110px;border-radius:6px"></video>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Intro Video URL</label>
|
||||
<input type="url" name="intro_video_url" class="form-control"
|
||||
value="{{ $settings['intro_video_url']->value ?? '' }}"
|
||||
placeholder="https://cdn.animexe.com/intro/site-intro.mp4">
|
||||
<div class="form-text">Aşağıdan BunnyCDN'e yükledikten sonra burası otomatik dolar.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded mb-3" style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Intro Aktif</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Her bölüm öncesi intro oynat</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="intro_enabled" value="1" id="intro_enabled" role="switch"
|
||||
{{ ($settings['intro_enabled']->value ?? '0') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Skip butonu (saniye sonra)</label>
|
||||
<input type="number" name="intro_skip_after" class="form-control" min="0" max="30"
|
||||
value="{{ $settings['intro_skip_after']->value ?? 5 }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Ana videodan atla (saniye)</label>
|
||||
<input type="number" name="main_video_skip_seconds" class="form-control" min="0" max="120"
|
||||
value="{{ $settings['main_video_skip_seconds']->value ?? 10 }}">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Rakip watermark kapat (saniye)</label>
|
||||
<input type="number" name="watermark_cover_seconds" class="form-control" min="0" max="60"
|
||||
value="{{ $settings['watermark_cover_seconds']->value ?? 11 }}">
|
||||
<div class="form-text">0 = kapalı</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- DeepSeek AI --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-stars" style="color:var(--accent-lt)"></i>
|
||||
<h6>DeepSeek AI</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-4">
|
||||
<label class="form-label">API Key</label>
|
||||
<input type="password" name="deepseek_api_key" class="form-control" value="{{ $settings['deepseek_api_key']->value ?? '' }}">
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded" style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Otomatik Açıklama</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Bot bölüm eklerken boş açıklamayı AI ile doldurur</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="ai_auto_description" value="1" id="ai_auto_desc" role="switch"
|
||||
{{ ($settings['ai_auto_description']->value ?? '0') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded" style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">Otomatik SEO</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Bot yeni anime eklerken SEO alanlarını AI ile doldurur</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="ai_auto_seo" value="1" id="ai_auto_seo" role="switch"
|
||||
{{ ($settings['ai_auto_seo']->value ?? '0') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Premium Ücretsiz Mod --}}
|
||||
<div class="card mb-3" style="border-color:rgba(255,215,0,.3)">
|
||||
<div class="card-header" style="background:rgba(255,215,0,.05);border-color:rgba(255,215,0,.2)">
|
||||
<i class="bi bi-stars" style="color:#ffd700"></i>
|
||||
<h6 style="color:#ffd700">Premium — Ücretsiz Mod</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="font-size:13px;color:var(--text2);margin-bottom:16px">
|
||||
Açıkken <strong style="color:var(--text)">tüm kullanıcılar</strong> premium özelliklere ücretsiz erişir.
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded" style="background:rgba(255,215,0,.06);border:1px solid rgba(255,215,0,.2)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:700;color:#ffd700">Herkes Premium Kullansın</div>
|
||||
<div style="font-size:12px;color:var(--text2)">Kapatınca sadece aboneler erişebilir</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="premium_free_mode" value="1" id="pfm" role="switch"
|
||||
{{ ($settings['premium_free_mode']->value ?? '0') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- GIF --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-image-alt" style="color:var(--info)"></i>
|
||||
<h6>GIF Arama</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
Giphy API Key
|
||||
<span class="badge bg-success ms-1" style="font-size:10px">Önerilen</span>
|
||||
</label>
|
||||
<input type="text" name="giphy_api_key" class="form-control" value="{{ $settings['giphy_api_key']->value ?? '' }}" placeholder="Giphy API key...">
|
||||
<div class="form-text">
|
||||
Ücretsiz: <a href="https://developers.giphy.com/" target="_blank" style="color:var(--info)">developers.giphy.com</a> → Create App → API
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" style="color:var(--text2)">Tenor API Key <span style="font-size:11px">(Giphy yoksa kullanılır)</span></label>
|
||||
<input type="text" name="tenor_api_key" class="form-control" value="{{ $settings['tenor_api_key']->value ?? '' }}" placeholder="Google Cloud Tenor API key...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Yorumlar --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-chat-dots" style="color:var(--warning)"></i>
|
||||
<h6>Yorum Ayarları</h6>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column gap-3">
|
||||
@foreach([
|
||||
['comments_enabled','Yorumlar Açık','Kullanıcılar yorum yapabilir','ce'],
|
||||
['comments_require_approval','Onay Beklesin','Yorumlar yayınlanmadan önce onaylanır','cra'],
|
||||
['nav_show_messages','Navbarda Mesajlar Linki','Kapatınca AI linki gösterilir','nsm'],
|
||||
] as [$key,$label,$desc,$id])
|
||||
<div class="d-flex align-items-center justify-content-between p-3 rounded" style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">{{ $label }}</div>
|
||||
<div style="font-size:12px;color:var(--text2)">{{ $desc }}</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="{{ $key }}" value="1" id="{{ $id }}" role="switch"
|
||||
{{ ($settings[$key]->value ?? '1') == '1' ? 'checked' : '' }}>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Reklam (VAST) --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex align-items-center gap-2">
|
||||
<i class="bi bi-badge-ad-fill" style="color:#ffd60a"></i>
|
||||
<h6 class="mb-0">Reklam (VAST)</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3 d-flex align-items-center gap-3">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="ads_enabled" value="1" id="ads_enabled" role="switch"
|
||||
{{ ($settings['ads_enabled']->value ?? '0') == '1' ? 'checked' : '' }}>
|
||||
<label class="form-check-label" for="ads_enabled">Reklamları Etkinleştir</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">VAST Tag URL</label>
|
||||
<input type="url" name="ads_vast_url" class="form-control font-monospace"
|
||||
value="{{ $settings['ads_vast_url']->value ?? '' }}"
|
||||
placeholder="https://...">
|
||||
<div class="form-text">Reklam ağından aldığın VAST URL'si buraya yapıştır.</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label">Kaç bölümde bir göster?</label>
|
||||
<div class="input-group">
|
||||
<input type="number" name="ads_freq_episodes" class="form-control" min="1" max="50"
|
||||
value="{{ $settings['ads_freq_episodes']->value ?? 4 }}">
|
||||
<span class="input-group-text">bölüm</span>
|
||||
</div>
|
||||
<div class="form-text">Her kullanıcı en fazla bu kadar bölümde bir reklam görür.</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label">Minimum süre (dakika)</label>
|
||||
<div class="input-group">
|
||||
<input type="number" name="ads_freq_minutes" class="form-control" min="1" max="1440"
|
||||
value="{{ $settings['ads_freq_minutes']->value ?? 10 }}">
|
||||
<span class="input-group-text">dakika</span>
|
||||
</div>
|
||||
<div class="form-text">Son reklamdan bu kadar dakika geçmeden tekrar göstermez.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100 mb-4">
|
||||
<i class="bi bi-floppy"></i> Ayarları Kaydet
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{{-- Favicon --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-image" style="color:var(--info)"></i>
|
||||
<h6>Site Favicon</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="font-size:13px;color:var(--text2);margin-bottom:14px">PNG, ICO veya SVG. Tarayıcı sekmesinde gösterilir. Maksimum 2 MB.</div>
|
||||
|
||||
@php $curFav = $settings['site_favicon']->value ?? '' @endphp
|
||||
@if($curFav)
|
||||
<div class="d-flex align-items-center gap-3 mb-3 p-3 rounded" style="background:var(--surface2);border:1px solid var(--border)">
|
||||
<img src="{{ $curFav }}?v={{ time() }}" alt="favicon" style="width:32px;height:32px;object-fit:contain;border-radius:6px;background:var(--surface);padding:2px">
|
||||
<code style="font-size:11.5px;color:var(--info);word-break:break-all">{{ $curFav }}</code>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('admin.settings.favicon.upload') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
<input type="file" name="favicon_file" class="form-control" accept=".png,.ico,.svg,.jpg,.jpeg" required>
|
||||
<button type="submit" class="btn btn-info flex-shrink-0"><i class="bi bi-upload"></i> Yükle</button>
|
||||
</div>
|
||||
@error('favicon_file')<div style="font-size:12px;color:var(--danger);margin-top:6px">{{ $message }}</div>@enderror
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Intro Upload --}}
|
||||
<div class="card mb-5">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-cloud-upload" style="color:var(--warning)"></i>
|
||||
<h6>Intro Video Yükle (BunnyCDN)</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div style="font-size:13px;color:var(--text2);margin-bottom:14px">
|
||||
MP4 veya WebM, maksimum 200 MB. Yükleme tamamlanınca URL yukarıdaki alana otomatik kaydedilir.
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('admin.settings.intro.upload') }}" enctype="multipart/form-data" id="intro-upload-form">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Video Dosyası</label>
|
||||
<input type="file" name="intro_video_file" class="form-control" accept="video/mp4,video/webm" required
|
||||
onchange="document.getElementById('intro-upload-btn').textContent = 'Yükle: ' + this.files[0]?.name">
|
||||
</div>
|
||||
<button type="submit" id="intro-upload-btn" class="btn btn-warning w-100"
|
||||
onclick="this.textContent='Yükleniyor...';this.disabled=true;this.form.submit()">
|
||||
<i class="bi bi-cloud-upload"></i> BunnyCDN'e Yükle
|
||||
</button>
|
||||
<div class="form-text mt-2">BunnyCDN'de <code>intro/site-intro.mp4</code> yoluna kaydedilir.</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- SMTP / E-posta --}}
|
||||
<div class="card mb-5">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-envelope-fill" style="color:var(--accent2)"></i>
|
||||
<h6>SMTP / E-posta Ayarları</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
@if(session('mail_success'))
|
||||
<div class="alert alert-success mb-3"><i class="bi bi-check-circle-fill"></i> {{ session('mail_success') }}</div>
|
||||
@endif
|
||||
@if(session('mail_error'))
|
||||
<div class="alert alert-danger mb-3"><i class="bi bi-x-circle-fill"></i> {{ session('mail_error') }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('admin.settings.update') }}">
|
||||
@csrf @method('POST')
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">SMTP Host</label>
|
||||
<input type="text" name="mail_host" class="form-control" placeholder="smtp.gmail.com" value="{{ $settings['mail_host']->value ?? '' }}">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Port</label>
|
||||
<input type="number" name="mail_port" class="form-control" placeholder="587" value="{{ $settings['mail_port']->value ?? '587' }}">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Şifreleme</label>
|
||||
<select name="mail_encryption" class="form-select">
|
||||
<option value="tls" {{ ($settings['mail_encryption']->value ?? 'tls') === 'tls' ? 'selected' : '' }}>TLS</option>
|
||||
<option value="ssl" {{ ($settings['mail_encryption']->value ?? '') === 'ssl' ? 'selected' : '' }}>SSL</option>
|
||||
<option value="" {{ ($settings['mail_encryption']->value ?? '') === '' ? 'selected' : '' }}>Yok</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Kullanıcı Adı</label>
|
||||
<input type="text" name="mail_username" class="form-control" placeholder="ornek@gmail.com" value="{{ $settings['mail_username']->value ?? '' }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Şifre</label>
|
||||
<input type="password" name="mail_password" class="form-control" placeholder="••••••••" value="{{ $settings['mail_password']->value ?? '' }}" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Gönderen Adı</label>
|
||||
<input type="text" name="mail_from_name" class="form-control" placeholder="Animexe" value="{{ $settings['mail_from_name']->value ?? '' }}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Gönderen E-posta</label>
|
||||
<input type="email" name="mail_from_address" class="form-control" placeholder="noreply@animexe.com" value="{{ $settings['mail_from_address']->value ?? '' }}">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mt-3">
|
||||
<i class="bi bi-floppy-fill"></i> SMTP Ayarlarını Kaydet
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr style="border-color:rgba(255,255,255,.08);margin:24px 0">
|
||||
|
||||
<form method="POST" action="{{ route('admin.settings.mail.test') }}">
|
||||
@csrf
|
||||
<label class="form-label fw-bold">Test E-postası Gönder</label>
|
||||
<div class="input-group">
|
||||
<input type="email" name="test_mail_to" class="form-control" placeholder="test@ornek.com" required>
|
||||
<button type="submit" class="btn btn-outline-info">
|
||||
<i class="bi bi-send-fill"></i> Gönder
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">Ayarları kaydettikten sonra test göndererek SMTP bağlantısını doğrulayın.</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,471 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'İçerik İstatistikleri')
|
||||
@section('page-title', 'İçerik İstatistikleri')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.st-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px 22px; }
|
||||
.st-card .val { font-size: 2rem; font-weight: 800; line-height: 1.1; color: var(--text); letter-spacing:-0.5px; }
|
||||
.st-card .lbl { font-size: 0.78rem; color: var(--text2); margin-top: 4px; font-weight:600; }
|
||||
.st-card .sub { font-size: 0.72rem; color: var(--text3); margin-top: 3px; }
|
||||
.st-card .ico { font-size: 1.8rem; opacity: 0.5; }
|
||||
|
||||
.chart-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 18px 20px; }
|
||||
.chart-title { font-size: 0.88rem; font-weight: 700; color: var(--text); margin-bottom: 14px; }
|
||||
.section-head { font-size: 0.75rem; text-transform: uppercase; letter-spacing: .8px; color: var(--text2); font-weight: 700; margin-bottom: 14px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
|
||||
|
||||
.tbl-sm th { font-size: 0.73rem; color: var(--text2); text-transform: uppercase; letter-spacing: .5px; padding: 8px 10px; }
|
||||
.tbl-sm td { font-size: 0.84rem; padding: 8px 10px; vertical-align: middle; border-top: 1px solid var(--border); color: var(--text); }
|
||||
.tbl-sm tr:first-child td { border-top: none; }
|
||||
|
||||
.pbar-wrap { height: 6px; background: var(--surface); border-radius: 3px; flex: 1; }
|
||||
.pbar-fill { height: 100%; background: linear-gradient(90deg,var(--accent),var(--accent-lt)); border-radius: 3px; }
|
||||
|
||||
/* GitHub-style heatmap */
|
||||
.heatmap-wrap { overflow-x: auto; padding-bottom: 4px; }
|
||||
.heatmap { display: flex; gap: 3px; }
|
||||
.heatmap-col { display: flex; flex-direction: column; gap: 3px; }
|
||||
.heat-cell {
|
||||
width: 13px; height: 13px; border-radius: 2px;
|
||||
background: var(--surface);
|
||||
cursor: default;
|
||||
transition: opacity .15s;
|
||||
position: relative;
|
||||
}
|
||||
.heat-cell:hover::after {
|
||||
content: attr(data-tip);
|
||||
position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%);
|
||||
background: var(--bg2); border: 1px solid var(--border2); color: var(--text);
|
||||
font-size: 11px; padding: 3px 8px; border-radius: 5px; white-space: nowrap;
|
||||
pointer-events: none; z-index: 100;
|
||||
}
|
||||
.heat-0 { background: var(--surface); }
|
||||
.heat-1 { background: rgba(124,58,237,0.25); }
|
||||
.heat-2 { background: rgba(124,58,237,0.45); }
|
||||
.heat-3 { background: rgba(124,58,237,0.7); }
|
||||
.heat-4 { background: var(--accent); }
|
||||
|
||||
.heatmap-legend { display: flex; align-items: center; gap: 4px; margin-top: 8px; font-size: .72rem; color: var(--text2); justify-content: flex-end; }
|
||||
.heatmap-legend span { width: 13px; height: 13px; border-radius: 2px; display: inline-block; }
|
||||
|
||||
.month-labels { display: flex; gap: 3px; margin-bottom: 4px; font-size: .72rem; color: var(--text3); padding-left: 22px; }
|
||||
.day-labels { display: flex; flex-direction: column; gap: 3px; margin-right: 4px; }
|
||||
.day-labels span { height: 13px; font-size: .68rem; color: var(--text3); line-height: 13px; }
|
||||
|
||||
.ep-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--border); }
|
||||
.ep-row:last-child { border-bottom: none; }
|
||||
.ep-badge { font-size: .7rem; padding: 2px 7px; border-radius: 10px; background: var(--surface); color: var(--text2); font-weight: 600; white-space: nowrap; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
{{-- Özet kartlar --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-sm-4 col-lg">
|
||||
<div class="st-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="val">{{ number_format($totalAnimes) }}</div>
|
||||
<div class="lbl">Toplam Anime</div>
|
||||
<div class="sub">{{ number_format($publishedAnimes) }} yayında</div>
|
||||
</div>
|
||||
<i class="bi bi-play-circle ico text-info"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-sm-4 col-lg">
|
||||
<div class="st-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="val">{{ number_format($totalEpisodes) }}</div>
|
||||
<div class="lbl">Toplam Bölüm</div>
|
||||
<div class="sub">{{ number_format($publishedEps) }} yayında</div>
|
||||
</div>
|
||||
<i class="bi bi-collection-play ico" style="color:#a371f7"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-sm-4 col-lg">
|
||||
<div class="st-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<div class="val">{{ number_format($totalSeasons) }}</div>
|
||||
<div class="lbl">Toplam Sezon</div>
|
||||
</div>
|
||||
<i class="bi bi-layers ico" style="color:#f0883e"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-sm-4 col-lg">
|
||||
<div class="st-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
@php $avgEpPerAnime = $totalAnimes > 0 ? round($totalEpisodes / $totalAnimes, 1) : 0; @endphp
|
||||
<div class="val">{{ $avgEpPerAnime }}</div>
|
||||
<div class="lbl">Ort. Bölüm/Anime</div>
|
||||
</div>
|
||||
<i class="bi bi-bar-chart ico" style="color:var(--success)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-sm-4 col-lg">
|
||||
<div class="st-card h-100">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
@php $unpublished = $totalEpisodes - $publishedEps; @endphp
|
||||
<div class="val">{{ number_format($unpublished) }}</div>
|
||||
<div class="lbl">Taslak Bölüm</div>
|
||||
<div class="sub">Yayında değil</div>
|
||||
</div>
|
||||
<i class="bi bi-eye-slash ico" style="color:var(--text2)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Yükleme Isı Haritası --}}
|
||||
<div class="chart-card mb-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div class="chart-title mb-0">Bölüm Yükleme Yoğunluk Haritası <span style="color:#6e7681;font-size:.78rem">(son 52 hafta)</span></div>
|
||||
</div>
|
||||
|
||||
@php
|
||||
// Ay etiketleri için hesaplama
|
||||
$heatMonths = [];
|
||||
foreach ($heatData as $wi => $week) {
|
||||
$firstDay = $week[0]['date'];
|
||||
$month = \Carbon\Carbon::parse($firstDay)->format('M');
|
||||
$dayOfMonth = \Carbon\Carbon::parse($firstDay)->day;
|
||||
if ($dayOfMonth <= 7 && !isset($lastMonth) || (isset($lastMonth) && $lastMonth !== $month)) {
|
||||
$heatMonths[$wi] = $month;
|
||||
$lastMonth = $month;
|
||||
} else {
|
||||
$heatMonths[$wi] = '';
|
||||
}
|
||||
}
|
||||
$heatMax = collect($heatData)->flatten(1)->max('cnt') ?: 1;
|
||||
@endphp
|
||||
|
||||
<div class="d-flex">
|
||||
{{-- Gün etiketleri --}}
|
||||
<div class="day-labels mt-4">
|
||||
<span> </span>
|
||||
<span>Pzt</span>
|
||||
<span> </span>
|
||||
<span>Çar</span>
|
||||
<span> </span>
|
||||
<span>Cum</span>
|
||||
<span> </span>
|
||||
</div>
|
||||
<div style="flex:1;min-width:0">
|
||||
{{-- Ay etiketleri --}}
|
||||
<div class="month-labels">
|
||||
@foreach($heatMonths as $wi => $mon)
|
||||
<span style="width:16px;text-align:left;flex-shrink:0">{{ $mon }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
{{-- Hücreler --}}
|
||||
<div class="heatmap-wrap">
|
||||
<div class="heatmap">
|
||||
@foreach($heatData as $week)
|
||||
<div class="heatmap-col">
|
||||
@foreach($week as $cell)
|
||||
@php
|
||||
$cnt = $cell['cnt'];
|
||||
if ($cnt === 0) $level = 0;
|
||||
elseif ($cnt <= 2) $level = 1;
|
||||
elseif ($cnt <= 5) $level = 2;
|
||||
elseif ($cnt <= 10) $level = 3;
|
||||
else $level = 4;
|
||||
$tip = $cell['date'] . ': ' . $cnt . ' bölüm';
|
||||
@endphp
|
||||
<div class="heat-cell heat-{{ $level }}" data-tip="{{ $tip }}" title="{{ $tip }}"></div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="heatmap-legend">
|
||||
<span>Az</span>
|
||||
<span class="heat-0"></span>
|
||||
<span class="heat-1"></span>
|
||||
<span class="heat-2"></span>
|
||||
<span class="heat-3"></span>
|
||||
<span class="heat-4"></span>
|
||||
<span>Çok</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 90 gün trend + Aylık dağılım --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="chart-card h-100">
|
||||
<div class="chart-title">Son 90 Gün — Günlük Yükleme Trendi</div>
|
||||
<div style="position:relative;height:200px">
|
||||
<canvas id="trendChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="chart-card h-100">
|
||||
<div class="chart-title">Aylık Bölüm Dağılımı (24 Ay)</div>
|
||||
<div style="position:relative;height:200px">
|
||||
<canvas id="monthChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Saatlik + Haftalık dağılım --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-7">
|
||||
<div class="chart-card h-100">
|
||||
<div class="chart-title">Saatlik Yükleme Dağılımı <span style="color:#6e7681;font-size:.78rem">(tüm zamanlar)</span></div>
|
||||
<div style="position:relative;height:160px">
|
||||
<canvas id="hourlyChart"></canvas>
|
||||
</div>
|
||||
@php
|
||||
$peakHour = array_search(max($hourlyEpsData), $hourlyEpsData);
|
||||
$peakCount = max($hourlyEpsData);
|
||||
@endphp
|
||||
@if($peakCount > 0)
|
||||
<div class="mt-2 text-center" style="font-size:.78rem;color:var(--text2)">
|
||||
En yoğun saat: <strong style="color:var(--accent)">{{ str_pad($peakHour,2,'0',STR_PAD_LEFT) }}:00</strong>
|
||||
({{ number_format($peakCount) }} bölüm)
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="chart-card h-100">
|
||||
<div class="chart-title">Haftanın Günlerine Göre <span style="color:#6e7681;font-size:.78rem">(tüm zamanlar)</span></div>
|
||||
<div style="position:relative;height:160px">
|
||||
<canvas id="weekdayChart"></canvas>
|
||||
</div>
|
||||
@php
|
||||
$peakDow = array_search(max($weekdayData), $weekdayData);
|
||||
$peakDowCount = max($weekdayData);
|
||||
@endphp
|
||||
@if($peakDowCount > 0)
|
||||
<div class="mt-2 text-center" style="font-size:.78rem;color:var(--text2)">
|
||||
En yoğun gün: <strong style="color:#a371f7">{{ $weekdayLabels[$peakDow] }}</strong>
|
||||
({{ number_format($peakDowCount) }} bölüm)
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Top animeler + Tür dağılımı --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="chart-card h-100">
|
||||
<div class="section-head">En Fazla Bölüm İçeren 10 Anime</div>
|
||||
@php $maxEpCount = $topByEpisodes->max('episodes_count') ?: 1; @endphp
|
||||
@foreach($topByEpisodes as $i => $a)
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span style="font-size:.72rem;color:var(--text2);width:16px;text-align:right">{{ $i+1 }}</span>
|
||||
<span style="font-size:.82rem;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
<a href="{{ route('admin.animes.show', $a) }}" style="color:var(--text);text-decoration:none" class="hover-accent">{{ $a->title }}</a>
|
||||
</span>
|
||||
@php
|
||||
$statusColor = match($a->status) { 'ongoing'=>'var(--success)','completed'=>'#79c0ff','upcoming'=>'#f0883e',default=>'var(--text2)' };
|
||||
@endphp
|
||||
<span style="font-size:.7rem;color:{{ $statusColor }}">
|
||||
{{ match($a->status){'ongoing'=>'Devam','completed'=>'Bitti','upcoming'=>'Yakında',default=>$a->status} }}
|
||||
</span>
|
||||
<div class="pbar-wrap" style="width:90px"><div class="pbar-fill" style="width:{{ round($a->episodes_count/$maxEpCount*100) }}%"></div></div>
|
||||
<span style="font-size:.78rem;color:var(--text2);width:35px;text-align:right">{{ $a->episodes_count }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="chart-card h-100">
|
||||
<div class="section-head">Türe Göre Bölüm Sayısı</div>
|
||||
@php $maxGenreEp = $genreEpStats->max('ep_count') ?: 1; @endphp
|
||||
@foreach($genreEpStats as $g)
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span style="font-size:.82rem;flex:1">{{ $g->name }}</span>
|
||||
<div class="pbar-wrap"><div class="pbar-fill" style="width:{{ round($g->ep_count/$maxGenreEp*100) }}%;background:#a371f7"></div></div>
|
||||
<span style="font-size:.78rem;color:var(--text2);width:38px;text-align:right">{{ number_format($g->ep_count) }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Son eklenen bölümler --}}
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-lg-7">
|
||||
<div class="chart-card h-100">
|
||||
<div class="section-head">Son Eklenen Bölümler</div>
|
||||
@foreach($recentEpisodes as $ep)
|
||||
<div class="ep-row">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:.83rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
{{ $ep->anime?->title ?? '—' }}
|
||||
</div>
|
||||
<div style="font-size:.72rem;color:var(--text2)">
|
||||
S{{ $ep->season?->season_number ?? '?' }}E{{ $ep->episode_number }}
|
||||
@if($ep->title) · {{ $ep->title }} @endif
|
||||
</div>
|
||||
</div>
|
||||
<span class="ep-badge {{ $ep->is_published ? '' : '' }}" style="{{ $ep->is_published ? 'background:#0e4429;color:var(--success)' : 'background:#3d1a1a;color:var(--danger)' }}">
|
||||
{{ $ep->is_published ? 'Yayında' : 'Taslak' }}
|
||||
</span>
|
||||
<span style="font-size:.72rem;color:#6e7681;white-space:nowrap">{{ $ep->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="chart-card h-100">
|
||||
<div class="section-head">Son Eklenen Animeler</div>
|
||||
@foreach($recentAnimes as $a)
|
||||
<div class="ep-row">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:.83rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
||||
<a href="{{ route('admin.animes.show', $a) }}" style="color:var(--text);text-decoration:none">{{ $a->title }}</a>
|
||||
</div>
|
||||
<div style="font-size:.72rem;color:var(--text2)">
|
||||
{{ strtoupper($a->type) }}
|
||||
</div>
|
||||
</div>
|
||||
<span style="font-size:.7rem;padding:2px 7px;border-radius:10px;{{ $a->is_published ? 'background:#0e4429;color:var(--success)' : 'background:var(--surface);color:var(--text2)' }}">
|
||||
{{ $a->is_published ? 'Yayında' : 'Taslak' }}
|
||||
</span>
|
||||
<span style="font-size:.72rem;color:#6e7681;white-space:nowrap">{{ $a->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>
|
||||
<script>
|
||||
Chart.defaults.color = 'var(--text2)';
|
||||
Chart.defaults.borderColor = 'var(--border)';
|
||||
|
||||
// ── 90 gün trend ─────────────────────────────────────────────────────────────
|
||||
new Chart(document.getElementById('trendChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: @json($trendLabels),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Bölüm',
|
||||
data: @json($epTrendData),
|
||||
borderColor: 'var(--accent)',
|
||||
backgroundColor: 'rgba(232,67,147,.1)',
|
||||
fill: true, tension: 0.4, pointRadius: 2,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'Anime',
|
||||
data: @json($animeTrendData),
|
||||
borderColor: 'var(--success)',
|
||||
backgroundColor: 'rgba(63,185,80,.08)',
|
||||
fill: true, tension: 0.4, pointRadius: 2,
|
||||
yAxisID: 'y',
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
interaction: { mode: 'index', intersect: false },
|
||||
plugins: {
|
||||
legend: { display: true, labels: { font: { size: 11 }, boxWidth: 10, padding: 12 } }
|
||||
},
|
||||
scales: {
|
||||
x: { grid: { color: 'var(--border)' }, ticks: { maxTicksLimit: 10, font: { size: 10 } } },
|
||||
y: { grid: { color: 'var(--border)' }, ticks: { font: { size: 10 } }, beginAtZero: true },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Aylık bölüm ───────────────────────────────────────────────────────────────
|
||||
new Chart(document.getElementById('monthChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($monthLabels),
|
||||
datasets: [{
|
||||
label: 'Bölüm',
|
||||
data: @json($monthData),
|
||||
backgroundColor: 'rgba(163,113,247,.6)',
|
||||
borderColor: '#a371f7',
|
||||
borderWidth: 1,
|
||||
borderRadius: 3,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { display: false }, ticks: { font: { size: 9 }, maxTicksLimit: 8 } },
|
||||
y: { grid: { color: 'var(--border)' }, ticks: { font: { size: 10 } }, beginAtZero: true },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Saatlik dağılım ───────────────────────────────────────────────────────────
|
||||
new Chart(document.getElementById('hourlyChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: Array.from({length:24}, (_,i) => `${String(i).padStart(2,'0')}:00`),
|
||||
datasets: [{
|
||||
label: 'Bölüm',
|
||||
data: @json($hourlyEpsData),
|
||||
backgroundColor: ctx => {
|
||||
const max = Math.max(...@json($hourlyEpsData));
|
||||
return ctx.parsed.y === max ? 'var(--accent)' : 'rgba(232,67,147,.35)';
|
||||
},
|
||||
borderColor: 'var(--accent)',
|
||||
borderWidth: 1,
|
||||
borderRadius: 3,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { display: false }, ticks: { font: { size: 9 } } },
|
||||
y: { grid: { color: 'var(--border)' }, ticks: { font: { size: 10 } }, beginAtZero: true },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Haftanın günleri ──────────────────────────────────────────────────────────
|
||||
new Chart(document.getElementById('weekdayChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: @json($weekdayLabels),
|
||||
datasets: [{
|
||||
label: 'Bölüm',
|
||||
data: @json($weekdayData),
|
||||
backgroundColor: ctx => {
|
||||
const max = Math.max(...@json($weekdayData));
|
||||
return ctx.parsed.y === max ? '#a371f7' : 'rgba(163,113,247,.35)';
|
||||
},
|
||||
borderColor: '#a371f7',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false } },
|
||||
scales: {
|
||||
x: { grid: { display: false }, ticks: { font: { size: 11 } } },
|
||||
y: { grid: { color: 'var(--border)' }, ticks: { font: { size: 10 } }, beginAtZero: true },
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,109 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Abonelikler')
|
||||
@section('page-title', 'Abonelikler')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">
|
||||
Abonelikler
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text2);margin-left:6px">{{ number_format($subscriptions->total()) }} toplam</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Filtreler --}}
|
||||
<div class="filter-card mb-3">
|
||||
<form method="GET" class="d-flex flex-wrap gap-2 align-items-end">
|
||||
<div class="search-input-wrap flex-grow-1" style="min-width:200px;max-width:320px">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" name="search" class="form-control" placeholder="Kullanıcı adı veya e-posta..." value="{{ request('search') }}">
|
||||
</div>
|
||||
<select name="status" class="form-select" style="width:auto;min-width:150px">
|
||||
<option value="">Tüm Durumlar</option>
|
||||
<option value="active" {{ request('status')=='active' ?'selected':'' }}>Aktif</option>
|
||||
<option value="expired" {{ request('status')=='expired' ?'selected':'' }}>Süresi Dolmuş</option>
|
||||
<option value="cancelled" {{ request('status')=='cancelled' ?'selected':'' }}>İptal Edilmiş</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.subscriptions.index') }}" class="btn btn-secondary btn-sm"><i class="bi bi-x-lg"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="table-wrap">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px">Kullanıcı</th>
|
||||
<th>Plan</th>
|
||||
<th>Başlangıç</th>
|
||||
<th>Bitiş</th>
|
||||
<th style="text-align:center">Durum</th>
|
||||
<th>Ödeme</th>
|
||||
<th style="text-align:right;padding-right:20px">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($subscriptions as $sub)
|
||||
<tr>
|
||||
<td style="padding-left:20px">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="user-avatar sm">{{ strtoupper(substr($sub->user?->name ?? '?', 0, 1)) }}</div>
|
||||
<div>
|
||||
<div style="font-size:13px;font-weight:600;color:var(--text)">{{ $sub->user?->name ?? '—' }}</div>
|
||||
<div style="font-size:11.5px;color:var(--text2)">{{ $sub->user?->email ?? '' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-size:13px;font-weight:600;color:var(--text)">{{ $sub->plan?->name ?? '—' }}</td>
|
||||
<td style="font-size:13px;color:var(--text2)">{{ $sub->starts_at?->format('d.m.Y') ?? '—' }}</td>
|
||||
<td style="font-size:13px;color:var(--text2)">{{ $sub->expires_at?->format('d.m.Y') ?? '—' }}</td>
|
||||
<td style="text-align:center">
|
||||
@if($sub->status === 'active')
|
||||
<span class="badge bg-success">Aktif</span>
|
||||
@elseif($sub->status === 'expired')
|
||||
<span class="badge bg-secondary">Sona Erdi</span>
|
||||
@else
|
||||
<span class="badge bg-danger">İptal</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="font-size:12.5px;color:var(--text2)">{{ $sub->payment_method ?? '—' }}</td>
|
||||
<td style="text-align:right;padding-right:20px">
|
||||
@if($sub->status === 'active')
|
||||
<form method="POST" action="{{ route('admin.subscriptions.destroy', $sub) }}"
|
||||
onsubmit="return confirm('Bu aboneliği iptal etmek istediğinize emin misiniz?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-x-circle"></i> İptal Et
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<span style="font-size:12px;color:var(--text3)">—</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-credit-card"></i>
|
||||
<h6>Abonelik bulunamadı</h6>
|
||||
<p>Filtrelerinizi değiştirin.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
{{ $subscriptions->links('admin.partials.pagination') }}
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,274 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title','Trend Yönetimi')
|
||||
@section('page-title','Trend Yönetimi')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.trend-item {
|
||||
display:flex; align-items:center; gap:12px;
|
||||
padding:12px 16px;
|
||||
border-bottom:1px solid var(--border);
|
||||
transition:background .12s;
|
||||
}
|
||||
.trend-item:last-child { border-bottom:none; }
|
||||
.trend-item:hover { background:rgba(255,255,255,0.02); }
|
||||
.trend-rank {
|
||||
width:28px; height:28px;
|
||||
border-radius:8px;
|
||||
background:var(--surface);
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
font-size:12px; font-weight:800; color:var(--text2);
|
||||
flex-shrink:0;
|
||||
}
|
||||
.trend-cover {
|
||||
width:36px; height:52px;
|
||||
object-fit:cover; border-radius:6px;
|
||||
flex-shrink:0;
|
||||
}
|
||||
.trend-cover-ph {
|
||||
width:36px; height:52px;
|
||||
background:var(--surface);
|
||||
border-radius:6px; flex-shrink:0;
|
||||
display:flex; align-items:center; justify-content:center;
|
||||
color:var(--text3); font-size:14px;
|
||||
}
|
||||
.search-result-item {
|
||||
display:flex; align-items:center; gap:10px;
|
||||
padding:10px 12px; border-radius:8px;
|
||||
margin-bottom:4px;
|
||||
background:rgba(255,255,255,0.03);
|
||||
border:1px solid var(--border);
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">Trend Yönetimi</h5>
|
||||
<div style="font-size:13px;color:var(--text2);margin-top:2px">
|
||||
Ana sayfada gösterilecek trend animeleri yönet
|
||||
@if($lastComputed)
|
||||
<span class="badge bg-secondary ms-2">Son hesap: {{ $lastComputed }}</span>
|
||||
@else
|
||||
<span class="badge bg-danger ms-2">Skor henüz hesaplanmadı</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-warning" id="btn-compute-scores" onclick="computeScores()">
|
||||
<i class="bi bi-lightning-fill me-1"></i>
|
||||
Trend Skorlarını Hesapla
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="compute-result" style="display:none" class="alert alert-success mb-3"></div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{{-- ── Sol: Manuel Trend ── --}}
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-fire" style="color:var(--warning)"></i>
|
||||
<h6>Manuel Trend Listesi</h6>
|
||||
<span style="font-size:12px;color:var(--text2)">{{ $manual->count() }} anime</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@if($manual->isEmpty())
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-fire"></i>
|
||||
<h6>Manuel trend seçilmedi</h6>
|
||||
<p>Sağdan anime ekleyin veya otomatik trend kullanılır.</p>
|
||||
</div>
|
||||
@else
|
||||
@foreach($manual as $anime)
|
||||
<div class="trend-item">
|
||||
<div class="trend-rank">{{ $anime->trending_order }}</div>
|
||||
@if($anime->coverUrl)
|
||||
<img src="{{ $anime->coverUrl }}" class="trend-cover" alt="">
|
||||
@else
|
||||
<div class="trend-cover-ph"><i class="bi bi-image"></i></div>
|
||||
@endif
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:13.5px;font-weight:700;color:var(--text)" class="text-truncate">{{ $anime->title }}</div>
|
||||
<div style="font-size:12px;color:var(--text2)">{{ $anime->release_year }} · {{ strtoupper($anime->type) }}</div>
|
||||
</div>
|
||||
<div class="d-flex gap-1">
|
||||
@if(!$loop->first)
|
||||
<form method="POST" action="{{ route('admin.trending.move',$anime) }}">
|
||||
@csrf
|
||||
<input type="hidden" name="direction" value="up">
|
||||
<button type="submit" class="btn btn-sm btn-secondary" title="Yukarı">
|
||||
<i class="bi bi-chevron-up"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@if(!$loop->last)
|
||||
<form method="POST" action="{{ route('admin.trending.move',$anime) }}">
|
||||
@csrf
|
||||
<input type="hidden" name="direction" value="down">
|
||||
<button type="submit" class="btn btn-sm btn-secondary" title="Aşağı">
|
||||
<i class="bi bi-chevron-down"></i>
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
<form method="POST" action="{{ route('admin.trending.toggle',$anime) }}">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Çıkar">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Sağ: Otomatik + Ekle ── --}}
|
||||
<div class="col-lg-6">
|
||||
|
||||
{{-- Arama ve Ekle --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-search" style="color:var(--info)"></i>
|
||||
<h6>Anime Ekle</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="search-input-wrap mb-2">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" id="trending-search" class="form-control"
|
||||
placeholder="Anime adı yazın..." autocomplete="off">
|
||||
</div>
|
||||
<div id="search-results" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Otomatik Trend --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-graph-up-arrow" style="color:var(--info)"></i>
|
||||
<h6>Otomatik Trend</h6>
|
||||
<span style="font-size:12px;color:var(--text2)">YouTube-benzeri trending skoru</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@foreach($autoTrending as $anime)
|
||||
<div class="trend-item">
|
||||
@if($anime->coverUrl)
|
||||
<img src="{{ $anime->coverUrl }}" class="trend-cover" alt="">
|
||||
@else
|
||||
<div class="trend-cover-ph"><i class="bi bi-image"></i></div>
|
||||
@endif
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:13px;font-weight:700;color:var(--text)" class="text-truncate">{{ $anime->title }}</div>
|
||||
<div style="font-size:12px;color:var(--text2)">
|
||||
@if($anime->trending_score > 0)
|
||||
<i class="bi bi-lightning-fill" style="color:var(--warning);font-size:10px"></i>
|
||||
{{ number_format($anime->trending_score, 1) }} puan
|
||||
@else
|
||||
{{ number_format($anime->recent_views ?? 0) }} izlenme
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if($anime->is_trending)
|
||||
<span class="badge" style="background:var(--warning-dim);color:var(--warning);border:1px solid rgba(245,158,11,.3)">
|
||||
<i class="bi bi-fire"></i> Trend
|
||||
</span>
|
||||
@else
|
||||
<form method="POST" action="{{ route('admin.trending.toggle',$anime) }}">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning">
|
||||
<i class="bi bi-plus-lg"></i> Ekle
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Açıklama --}}
|
||||
<div class="card">
|
||||
<div class="card-body" style="padding:16px">
|
||||
<div style="font-size:13px;font-weight:700;color:var(--text);margin-bottom:8px;display:flex;align-items:center;gap:7px">
|
||||
<i class="bi bi-info-circle" style="color:var(--info)"></i> Nasıl Çalışır?
|
||||
</div>
|
||||
<ul style="font-size:12.5px;color:var(--text2);padding-left:16px;line-height:2;margin:0">
|
||||
<li>Manuel liste boşsa → otomatik trend (izlenme) gösterilir</li>
|
||||
<li>Manuel liste doluysa → seçtiğiniz animeler sırayla görünür</li>
|
||||
<li>Sıralama yukarı/aşağı butonlarıyla değiştirilir</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
let searchTimer = null;
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content || '';
|
||||
|
||||
document.getElementById('trending-search').addEventListener('input', function() {
|
||||
clearTimeout(searchTimer);
|
||||
const q = this.value.trim();
|
||||
const box = document.getElementById('search-results');
|
||||
if (q.length < 2) { box.style.display='none'; return; }
|
||||
|
||||
searchTimer = setTimeout(async () => {
|
||||
const r = await fetch(`/admin/trending/search?q=${encodeURIComponent(q)}`);
|
||||
const d = await r.json();
|
||||
box.style.display = 'block';
|
||||
if (!d.results.length) {
|
||||
box.innerHTML = '<div style="font-size:12.5px;color:var(--text2);padding:8px">Sonuç bulunamadı.</div>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = d.results.map(a => `
|
||||
<div class="search-result-item">
|
||||
${a.cover ? `<img src="${a.cover}" style="width:30px;height:42px;object-fit:cover;border-radius:5px;flex-shrink:0">` : '<div style="width:30px;height:42px;background:var(--surface);border-radius:5px;flex-shrink:0"></div>'}
|
||||
<div style="flex:1;min-width:0;font-size:13px;font-weight:600;color:var(--text)" class="text-truncate">${a.title}</div>
|
||||
${a.is_trending
|
||||
? '<span class="badge" style="background:var(--warning-dim);color:var(--warning)"><i class="bi bi-fire"></i> Trend</span>'
|
||||
: `<form method="POST" action="/admin/trending/${a.id}/toggle" style="flex-shrink:0"><input type="hidden" name="_token" value="${CSRF}"><button class="btn btn-sm btn-outline-warning"><i class="bi bi-plus-lg"></i> Ekle</button></form>`
|
||||
}
|
||||
</div>
|
||||
`).join('');
|
||||
}, 350);
|
||||
});
|
||||
|
||||
// ── Trend Skoru Hesapla ──────────────────────────────────────────────────────
|
||||
async function computeScores() {
|
||||
const btn = document.getElementById('btn-compute-scores');
|
||||
const res = document.getElementById('compute-result');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Hesaplanıyor...';
|
||||
res.style.display = 'none';
|
||||
|
||||
try {
|
||||
const r = await fetch('{{ route("admin.trending.compute-scores") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json' },
|
||||
});
|
||||
const d = await r.json();
|
||||
res.style.display = 'block';
|
||||
res.className = 'alert alert-success mb-3';
|
||||
res.innerHTML = `<i class="bi bi-check-circle-fill me-2"></i><strong>${d.message}</strong>
|
||||
<br><small>Anasayfa cache temizlendi. Sayfa yenileniyor...</small>`;
|
||||
btn.innerHTML = '<i class="bi bi-check me-1"></i>Tamamlandı!';
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
} catch(e) {
|
||||
res.style.display = 'block';
|
||||
res.className = 'alert alert-danger mb-3';
|
||||
res.textContent = 'Hata: ' + e.message;
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-lightning-fill me-1"></i>Trend Skorlarını Hesapla';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,77 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Kullanıcı Düzenle')
|
||||
@section('page-title', 'Kullanıcı Düzenle')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="mb-4">
|
||||
<a href="{{ route('admin.users.show', $user) }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Geri Dön
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-5 col-lg-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="user-avatar sm">{{ strtoupper(substr($user->name,0,1)) }}</div>
|
||||
<h6>{{ $user->name }}</h6>
|
||||
<span style="font-size:12px;color:var(--text2)">— Düzenle</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('admin.users.update', $user) }}">
|
||||
@csrf @method('PUT')
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Ad Soyad <span style="color:var(--danger)">*</span></label>
|
||||
<input type="text" name="name" class="form-control"
|
||||
value="{{ old('name',$user->name) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">E-posta <span style="color:var(--danger)">*</span></label>
|
||||
<input type="email" name="email" class="form-control"
|
||||
value="{{ old('email',$user->email) }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Rol</label>
|
||||
<select name="role" class="form-select">
|
||||
<option value="user" {{ $user->role==='user' ?'selected':'' }}>Kullanıcı</option>
|
||||
<option value="moderator" {{ $user->role==='moderator' ?'selected':'' }}>Moderatör</option>
|
||||
<option value="admin" {{ $user->role==='admin' ?'selected':'' }}>Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">
|
||||
Yeni Şifre
|
||||
<span style="color:var(--text2);font-size:11px">(boş bırakırsan değişmez)</span>
|
||||
</label>
|
||||
<input type="password" name="password" class="form-control" autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label">
|
||||
Admin Rozeti
|
||||
<span style="color:var(--text2);font-size:11px">(yorumlarda altın etiket)</span>
|
||||
</label>
|
||||
<input type="text" name="admin_badge" class="form-control" maxlength="32"
|
||||
value="{{ old('admin_badge',$user->admin_badge) }}"
|
||||
placeholder="Örn: Beta Tester, VIP, Staff...">
|
||||
<div class="form-text">Boş bırakırsan rozet gösterilmez.</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary flex-grow-1">
|
||||
<i class="bi bi-floppy"></i> Kaydet
|
||||
</button>
|
||||
<a href="{{ route('admin.users.show',$user) }}" class="btn btn-secondary">İptal</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,134 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', 'Kullanıcılar')
|
||||
@section('page-title', 'Kullanıcılar')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 style="font-size:18px;font-weight:700;color:var(--text);margin:0">
|
||||
Kullanıcılar
|
||||
<span style="font-size:13px;font-weight:600;color:var(--text2);margin-left:6px">{{ number_format($users->total()) }} toplam</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Filters ── --}}
|
||||
<div class="filter-card mb-3">
|
||||
<form method="GET" class="d-flex flex-wrap gap-2 align-items-end">
|
||||
<div class="search-input-wrap flex-grow-1" style="min-width:180px;max-width:300px">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" name="search" class="form-control" placeholder="İsim veya e-posta..." value="{{ request('search') }}">
|
||||
</div>
|
||||
<select name="membership" class="form-select" style="width:auto;min-width:130px">
|
||||
<option value="">Tüm Üyelik</option>
|
||||
<option value="free" {{ request('membership')=='free' ?'selected':'' }}>Ücretsiz</option>
|
||||
<option value="premium" {{ request('membership')=='premium' ?'selected':'' }}>Premium</option>
|
||||
</select>
|
||||
<select name="role" class="form-select" style="width:auto;min-width:130px">
|
||||
<option value="">Tüm Roller</option>
|
||||
<option value="user" {{ request('role')=='user' ?'selected':'' }}>Kullanıcı</option>
|
||||
<option value="moderator" {{ request('role')=='moderator' ?'selected':'' }}>Moderatör</option>
|
||||
<option value="admin" {{ request('role')=='admin' ?'selected':'' }}>Admin</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.users.index') }}" class="btn btn-secondary btn-sm"><i class="bi bi-x-lg"></i></a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- ── Table ── --}}
|
||||
<div class="table-wrap">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="padding-left:20px">Kullanıcı</th>
|
||||
<th>Rol</th>
|
||||
<th>Üyelik</th>
|
||||
<th>Premium Bitiş</th>
|
||||
<th style="text-align:center">Durum</th>
|
||||
<th>Kayıt Tarihi</th>
|
||||
<th style="text-align:right;padding-right:20px">İşlem</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse($users as $user)
|
||||
<tr>
|
||||
<td style="padding-left:20px">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="user-avatar sm">
|
||||
@if($user->avatar)
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($user->avatar) }}" alt="">
|
||||
@else
|
||||
{{ strtoupper(substr($user->name, 0, 1)) }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<div style="font-size:13.5px;font-weight:600;color:var(--text)">{{ $user->name }}</div>
|
||||
<div style="font-size:12px;color:var(--text2)">{{ $user->email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if($user->role === 'admin')
|
||||
<span class="badge bg-danger">Admin</span>
|
||||
@elseif($user->role === 'moderator')
|
||||
<span class="badge bg-info" style="color:#000">Moderatör</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Kullanıcı</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($user->membership === 'premium')
|
||||
<span class="badge badge-premium"><i class="bi bi-star-fill" style="font-size:9px;margin-right:3px"></i>Premium</span>
|
||||
@else
|
||||
<span class="badge badge-free">Ücretsiz</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="font-size:13px;color:var(--text2)">
|
||||
{{ $user->premium_expires_at ? $user->premium_expires_at->format('d.m.Y') : '—' }}
|
||||
</td>
|
||||
<td style="text-align:center">
|
||||
@if($user->is_banned)
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:700;color:var(--danger)">
|
||||
<i class="bi bi-slash-circle-fill" style="font-size:11px"></i> Banlı
|
||||
</span>
|
||||
@else
|
||||
<span style="display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:var(--success)">
|
||||
<i class="bi bi-circle-fill" style="font-size:7px"></i> Aktif
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td style="font-size:13px;color:var(--text2)">
|
||||
{{ $user->created_at->format('d.m.Y') }}
|
||||
</td>
|
||||
<td style="text-align:right;padding-right:20px">
|
||||
<a href="{{ route('admin.users.show', $user) }}" class="btn btn-sm btn-secondary">
|
||||
<i class="bi bi-eye"></i>
|
||||
<span class="d-none d-md-inline btn-text"> Görüntüle</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7">
|
||||
<div class="empty-state">
|
||||
<i class="bi bi-people"></i>
|
||||
<h6>Kullanıcı bulunamadı</h6>
|
||||
<p>Arama kriterlerinizi değiştirin.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
{{ $users->links('admin.partials.pagination') }}
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@@ -0,0 +1,267 @@
|
||||
@extends('admin.layouts.app')
|
||||
@section('title', $user->name)
|
||||
@section('page-title', $user->name)
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.info-row {
|
||||
display:flex; justify-content:space-between; align-items:center;
|
||||
padding:11px 0;
|
||||
border-bottom:1px solid var(--border);
|
||||
font-size:13.5px;
|
||||
}
|
||||
.info-row:last-child { border-bottom:none; }
|
||||
.info-label { color:var(--text2); font-weight:500; }
|
||||
.info-value { color:var(--text); font-weight:600; text-align:right; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="mb-4">
|
||||
<a href="{{ route('admin.users.index') }}" class="btn btn-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Kullanıcılara Dön
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
|
||||
{{-- ── Sol Kolon ── --}}
|
||||
<div class="col-lg-4">
|
||||
|
||||
{{-- Profil Kartı --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body text-center" style="padding:28px 20px">
|
||||
<div class="user-avatar lg mx-auto mb-3">
|
||||
@if($user->avatar)
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($user->avatar) }}" alt="">
|
||||
@else
|
||||
{{ strtoupper(substr($user->name, 0, 1)) }}
|
||||
@endif
|
||||
</div>
|
||||
<div style="font-size:17px;font-weight:800;color:var(--text);margin-bottom:4px">{{ $user->name }}</div>
|
||||
<div style="font-size:13px;color:var(--text2);margin-bottom:14px">{{ $user->email }}</div>
|
||||
<div class="d-flex justify-content-center gap-2 flex-wrap">
|
||||
@if($user->role === 'admin')
|
||||
<span class="badge bg-danger">Admin</span>
|
||||
@elseif($user->role === 'moderator')
|
||||
<span class="badge bg-info" style="color:#000">Moderatör</span>
|
||||
@else
|
||||
<span class="badge bg-secondary">Kullanıcı</span>
|
||||
@endif
|
||||
@if($user->membership === 'premium')
|
||||
<span class="badge badge-premium"><i class="bi bi-star-fill" style="font-size:9px;margin-right:3px"></i>Premium</span>
|
||||
@else
|
||||
<span class="badge badge-free">Ücretsiz</span>
|
||||
@endif
|
||||
@if($user->is_banned)
|
||||
<span class="badge bg-danger"><i class="bi bi-slash-circle-fill" style="font-size:9px;margin-right:3px"></i>Banlı</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Bilgiler --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-info-circle" style="color:var(--accent-lt)"></i>
|
||||
<h6>Hesap Bilgileri</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Kayıt Tarihi</span>
|
||||
<span class="info-value">{{ $user->created_at->format('d.m.Y') }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Son Giriş</span>
|
||||
<span class="info-value">{{ $user->last_login_at ? $user->last_login_at->format('d.m.Y H:i') : '—' }}</span>
|
||||
</div>
|
||||
@if($user->membership === 'premium')
|
||||
<div class="info-row">
|
||||
<span class="info-label">Premium Bitiş</span>
|
||||
<span class="info-value" style="color:var(--warning)">
|
||||
{{ $user->premium_expires_at ? $user->premium_expires_at->format('d.m.Y') : 'Süresiz' }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($user->is_banned)
|
||||
<div class="info-row">
|
||||
<span class="info-label">Ban Tarihi</span>
|
||||
<span class="info-value" style="color:var(--danger)">{{ $user->banned_at?->format('d.m.Y') ?? '—' }}</span>
|
||||
</div>
|
||||
@if($user->ban_reason)
|
||||
<div class="info-row" style="flex-direction:column;align-items:flex-start;gap:4px">
|
||||
<span class="info-label">Ban Sebebi</span>
|
||||
<span style="font-size:13px;color:var(--text)">{{ $user->ban_reason }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- İşlemler --}}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-sliders" style="color:var(--info)"></i>
|
||||
<h6>İşlemler</h6>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column gap-2">
|
||||
<a href="{{ route('admin.users.edit', $user) }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-pencil"></i> Düzenle
|
||||
</a>
|
||||
|
||||
@if($user->is_banned)
|
||||
<form method="POST" action="{{ route('admin.users.unban', $user) }}">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-success btn-sm w-100">
|
||||
<i class="bi bi-check-circle"></i> Banı Kaldır
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<button class="btn btn-outline-warning btn-sm" data-bs-toggle="modal" data-bs-target="#banModal">
|
||||
<i class="bi bi-slash-circle"></i> Banla
|
||||
</button>
|
||||
@endif
|
||||
|
||||
@if($user->id !== auth()->id())
|
||||
<form method="POST" action="{{ route('admin.users.destroy', $user) }}"
|
||||
onsubmit="return confirm('{{ addslashes($user->name) }} kullanıcısını kalıcı olarak silmek istediğinize emin misiniz?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm w-100">
|
||||
<i class="bi bi-trash3"></i> Hesabı Sil
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Premium Yönetimi --}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-star" style="color:var(--warning)"></i>
|
||||
<h6>Premium Yönetimi</h6>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column gap-3">
|
||||
@if($user->membership === 'premium')
|
||||
<form method="POST" action="{{ route('admin.users.remove-premium', $user) }}">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-outline-warning btn-sm w-100">
|
||||
<i class="bi bi-x-circle"></i> Premium'u Kaldır
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
<form method="POST" action="{{ route('admin.users.give-premium', $user) }}">
|
||||
@csrf
|
||||
<div class="mb-2">
|
||||
<select name="plan_id" class="form-select" required>
|
||||
<option value="">— Plan Seç —</option>
|
||||
@foreach($plans as $plan)
|
||||
<option value="{{ $plan->id }}">{{ $plan->name }} ({{ $plan->duration_days }} gün)</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm w-100">
|
||||
<i class="bi bi-star-fill"></i> Premium Ver
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- ── Sağ Kolon ── --}}
|
||||
<div class="col-lg-8">
|
||||
|
||||
{{-- Abonelik Geçmişi --}}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-credit-card" style="color:var(--success)"></i>
|
||||
<h6>Abonelik Geçmişi</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@forelse($user->subscriptions as $sub)
|
||||
<div class="d-flex align-items-center justify-content-between px-4 py-3" style="border-bottom:1px solid var(--border)">
|
||||
<div>
|
||||
<div style="font-size:13.5px;font-weight:700;color:var(--text)">{{ $sub->plan->name ?? '—' }}</div>
|
||||
<div style="font-size:12px;color:var(--text2)">
|
||||
{{ $sub->starts_at?->format('d.m.Y') }} — {{ $sub->expires_at?->format('d.m.Y') }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge {{ $sub->status === 'active' ? 'bg-success' : ($sub->status === 'expired' ? 'bg-secondary' : 'bg-danger') }}">
|
||||
{{ match($sub->status) { 'active'=>'Aktif', 'expired'=>'Sona Erdi', default=>'İptal' } }}
|
||||
</span>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-state py-5">
|
||||
<i class="bi bi-credit-card"></i>
|
||||
<p>Abonelik geçmişi yok.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Son Yorumlar --}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<i class="bi bi-chat-dots" style="color:var(--info)"></i>
|
||||
<h6>Son Yorumlar</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
@forelse($user->comments->take(10) as $comment)
|
||||
<div class="px-4 py-3" style="border-bottom:1px solid var(--border)">
|
||||
<div class="d-flex justify-content-between align-items-center gap-2 mb-1">
|
||||
<span style="font-size:12px;color:var(--text2)">{{ $comment->created_at->format('d.m.Y H:i') }}</span>
|
||||
<span class="badge {{ $comment->status === 'approved' ? 'bg-success' : ($comment->status === 'pending' ? 'bg-warning' : 'bg-secondary') }}"
|
||||
style="{{ $comment->status==='pending'?'color:#000':'' }}">
|
||||
{{ match($comment->status) { 'approved'=>'Onaylı', 'pending'=>'Bekliyor', default=>'Reddedildi' } }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size:13px;color:var(--text);line-height:1.5">{{ Str::limit($comment->content, 120) }}</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-state py-5">
|
||||
<i class="bi bi-chat-dots"></i>
|
||||
<p>Henüz yorum yapılmamış.</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Ban Modal --}}
|
||||
<div class="modal fade" id="banModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title" style="color:var(--warning)">
|
||||
<i class="bi bi-slash-circle me-2"></i>Kullanıcıyı Banla
|
||||
</h6>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST" action="{{ route('admin.users.ban', $user) }}">
|
||||
@csrf
|
||||
<div class="modal-body">
|
||||
<div class="p-3 rounded mb-3" style="background:var(--warning-dim);border:1px solid rgba(245,158,11,.3)">
|
||||
<div style="font-size:13px;font-weight:700;color:var(--warning)">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>Dikkat
|
||||
</div>
|
||||
<div style="font-size:12.5px;color:var(--text);margin-top:4px">
|
||||
<strong>{{ $user->name }}</strong> adlı kullanıcının siteye erişimi engellenecek.
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-label">Ban Sebebi <span style="color:var(--text2)">(opsiyonel)</span></label>
|
||||
<textarea name="ban_reason" class="form-control" rows="3"
|
||||
placeholder="Neden banlıyorsunuz? Kullanıcıya gösterilmez."></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">İptal</button>
|
||||
<button type="submit" class="btn btn-warning"><i class="bi bi-slash-circle"></i> Banla</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
Reference in New Issue
Block a user