Initial commit: Animexe Laravel platform

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 00:01:48 +03:00
co-authored by Claude Opus 4.8
commit a63515cfc6
366 changed files with 74773 additions and 0 deletions
+471
View File
@@ -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>&nbsp;</span>
<span>Pzt</span>
<span>&nbsp;</span>
<span>Çar</span>
<span>&nbsp;</span>
<span>Cum</span>
<span>&nbsp;</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