Initial commit: Animexe Laravel platform
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user