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
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>@yield('title','Animexe')</title>
|
||||
<style>
|
||||
body{margin:0;padding:0;background:#06060d;font-family:'Segoe UI',Arial,sans-serif;color:#f0f4ff}
|
||||
.wrap{max-width:520px;margin:40px auto;background:linear-gradient(160deg,#0e0e1c,#080812);border:1px solid rgba(0,245,255,.18);border-radius:16px;overflow:hidden}
|
||||
.hdr{padding:28px 32px 20px;border-bottom:1px solid rgba(255,255,255,.06);text-align:center}
|
||||
.logo{font-size:1.3rem;font-weight:900;letter-spacing:.22em;background:linear-gradient(105deg,#fff,#00f5ff,#ff2d7d);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
|
||||
.body{padding:32px}
|
||||
.title{font-size:1.1rem;font-weight:800;margin-bottom:16px;color:#fff}
|
||||
.text{font-size:.9rem;line-height:1.7;color:#9ca8d4;margin-bottom:16px}
|
||||
.btn{display:inline-block;padding:13px 32px;background:linear-gradient(135deg,#ff2d7d,#b84dff);border-radius:10px;color:#fff!important;font-weight:800;font-size:.92rem;text-decoration:none;margin:8px 0 20px}
|
||||
.divider{height:1px;background:rgba(255,255,255,.07);margin:20px 0}
|
||||
.small{font-size:.78rem;color:#5c6789;line-height:1.6}
|
||||
.ftr{padding:20px 32px;border-top:1px solid rgba(255,255,255,.06);text-align:center;font-size:.75rem;color:#5c6789}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="hdr">
|
||||
<div class="logo">ANIMEXE</div>
|
||||
</div>
|
||||
<div class="body">
|
||||
@yield('content')
|
||||
</div>
|
||||
<div class="ftr">
|
||||
Bu e-postayı siz talep etmediyseniz dikkate almayınız.<br>
|
||||
© {{ date('Y') }} Animexe — Tüm hakları saklıdır.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
@extends('emails.layout')
|
||||
@section('content')
|
||||
<div class="title">🔐 Şifre Sıfırlama</div>
|
||||
<p class="text">Merhaba <strong style="color:#fff">{{ $userName }}</strong>,</p>
|
||||
<p class="text">Animexe hesabınız için şifre sıfırlama talebinde bulundunuz. Aşağıdaki butona tıklayarak yeni şifrenizi belirleyebilirsiniz.</p>
|
||||
<div style="text-align:center">
|
||||
<a href="{{ $resetUrl }}" class="btn">Şifremi Sıfırla</a>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<p class="small">Bu bağlantı <strong>60 dakika</strong> geçerlidir. Şifre sıfırlama talebinde bulunmadıysanız bu e-postayı dikkate almayınız — hesabınız güvende.</p>
|
||||
<p class="small" style="margin-top:10px;word-break:break-all">Buton çalışmıyorsa bu bağlantıyı tarayıcınıza yapıştırın:<br>{{ $resetUrl }}</p>
|
||||
@endsection
|
||||
@@ -0,0 +1,7 @@
|
||||
@extends('emails.layout')
|
||||
@section('content')
|
||||
<div class="title">✅ SMTP Bağlantısı Başarılı</div>
|
||||
<p class="text">Bu e-posta, Animexe admin panelinden yapılan SMTP test gönderimidir. E-postayı aldıysanız SMTP ayarlarınız doğru çalışıyor demektir.</p>
|
||||
<div class="divider"></div>
|
||||
<p class="small">Gönderim zamanı: {{ now()->format('d.m.Y H:i:s') }}</p>
|
||||
@endsection
|
||||
@@ -0,0 +1,12 @@
|
||||
@extends('emails.layout')
|
||||
@section('content')
|
||||
<div class="title">📧 E-posta Adresini Doğrula</div>
|
||||
<p class="text">Merhaba <strong style="color:#fff">{{ $userName }}</strong>, Animexe'ye hoş geldin!</p>
|
||||
<p class="text">Hesabını aktifleştirmek için e-posta adresini doğrulamanı istiyoruz. Aşağıdaki butona tıkla:</p>
|
||||
<div style="text-align:center">
|
||||
<a href="{{ $verifyUrl }}" class="btn">E-postamı Doğrula</a>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<p class="small">Bu bağlantı <strong>24 saat</strong> geçerlidir. Hesap açmadıysanız bu e-postayı dikkate almayınız.</p>
|
||||
<p class="small" style="margin-top:10px;word-break:break-all">Buton çalışmıyorsa bu bağlantıyı tarayıcınıza yapıştırın:<br>{{ $verifyUrl }}</p>
|
||||
@endsection
|
||||
@@ -0,0 +1,462 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title','AI Asistan — Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
.ai-hero{
|
||||
padding:52px 24px 28px;max-width:900px;margin:0 auto;text-align:center;
|
||||
}
|
||||
.ai-hero-icon{
|
||||
width:80px;height:80px;border-radius:50%;margin:0 auto 20px;
|
||||
background:linear-gradient(135deg,var(--accent3),var(--accent));
|
||||
display:flex;align-items:center;justify-content:center;font-size:2.2rem;color:#fff;
|
||||
box-shadow:0 0 48px rgba(184,77,255,.45),0 0 80px rgba(255,45,125,.2);
|
||||
animation:ai-glow 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes ai-glow{
|
||||
0%,100%{box-shadow:0 0 48px rgba(184,77,255,.45),0 0 80px rgba(255,45,125,.2)}
|
||||
50%{box-shadow:0 0 64px rgba(184,77,255,.65),0 0 100px rgba(255,45,125,.35)}
|
||||
}
|
||||
.ai-hero h1{
|
||||
font-family:var(--font-display);font-size:clamp(1.6rem,4vw,2.5rem);font-weight:800;
|
||||
background:linear-gradient(120deg,#fff 0%,var(--accent3) 40%,var(--accent) 100%);
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
.ai-hero p{color:var(--text2);font-size:1rem;max-width:520px;margin:0 auto 32px;line-height:1.7}
|
||||
|
||||
.ai-tabs{
|
||||
display:flex;gap:8px;justify-content:center;flex-wrap:wrap;margin-bottom:32px;
|
||||
}
|
||||
.ai-tab{
|
||||
padding:10px 22px;border-radius:999px;font-size:.88rem;font-weight:700;cursor:pointer;
|
||||
background:var(--surface);border:1px solid var(--border);color:var(--text2);
|
||||
transition:all .18s;
|
||||
}
|
||||
.ai-tab.on,.ai-tab:hover{
|
||||
background:linear-gradient(135deg,rgba(184,77,255,.18),rgba(255,45,125,.1));
|
||||
border-color:var(--accent3);color:var(--accent3);
|
||||
box-shadow:0 0 20px rgba(184,77,255,.2);
|
||||
}
|
||||
|
||||
.ai-section{max-width:900px;margin:0 auto;padding:0 24px 48px}
|
||||
|
||||
/* Öneri kartı */
|
||||
.ai-box{
|
||||
background:linear-gradient(145deg,rgba(17,17,31,.95),rgba(10,10,20,.98));
|
||||
border:1px solid rgba(184,77,255,.2);border-radius:18px;padding:28px;
|
||||
box-shadow:0 8px 40px rgba(0,0,0,.35),0 0 40px rgba(184,77,255,.05);
|
||||
}
|
||||
.ai-box-title{
|
||||
font-family:var(--font-display);font-size:.85rem;letter-spacing:.12em;
|
||||
text-transform:uppercase;color:var(--accent3);font-weight:700;
|
||||
display:flex;align-items:center;gap:10px;margin-bottom:20px;
|
||||
}
|
||||
.ai-box-title i{font-size:1.1rem}
|
||||
|
||||
/* Ruh hali chips */
|
||||
.mood-grid{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:20px}
|
||||
.mood-chip{
|
||||
padding:8px 16px;border-radius:999px;font-size:.82rem;font-weight:600;cursor:pointer;
|
||||
background:rgba(17,17,31,.8);border:1px solid var(--border);color:var(--text2);
|
||||
transition:all .16s;display:flex;align-items:center;gap:6px;
|
||||
}
|
||||
.mood-chip.on,.mood-chip:hover{
|
||||
background:rgba(184,77,255,.14);border-color:var(--accent3);color:var(--accent3);
|
||||
box-shadow:0 0 16px rgba(184,77,255,.2);
|
||||
}
|
||||
|
||||
/* Tür chips */
|
||||
.genre-wrap{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:20px}
|
||||
|
||||
/* Tip toggle */
|
||||
.type-row{display:flex;gap:8px;margin-bottom:24px}
|
||||
.type-btn{
|
||||
flex:1;padding:10px;border-radius:10px;border:1px solid var(--border);
|
||||
background:var(--surface);color:var(--text2);font-size:.84rem;font-weight:700;
|
||||
cursor:pointer;transition:all .16s;text-align:center;
|
||||
}
|
||||
.type-btn.on{
|
||||
background:rgba(0,245,255,.1);border-color:var(--accent2);color:var(--accent2);
|
||||
box-shadow:0 0 16px rgba(0,245,255,.12);
|
||||
}
|
||||
|
||||
.ai-btn{
|
||||
width:100%;padding:14px;border-radius:12px;border:none;cursor:pointer;
|
||||
background:linear-gradient(135deg,var(--accent3),var(--accent));color:#fff;
|
||||
font-size:.95rem;font-weight:800;letter-spacing:.04em;
|
||||
transition:transform .16s,box-shadow .18s;
|
||||
box-shadow:0 0 28px rgba(184,77,255,.3),0 4px 16px rgba(255,45,125,.2);
|
||||
display:flex;align-items:center;justify-content:center;gap:10px;
|
||||
}
|
||||
.ai-btn:hover{transform:translateY(-1px);box-shadow:0 0 40px rgba(184,77,255,.45)}
|
||||
.ai-btn:disabled{opacity:.55;cursor:not-allowed;transform:none}
|
||||
|
||||
/* Loader */
|
||||
.ai-loader{display:none;justify-content:center;align-items:center;gap:6px;padding:28px}
|
||||
.ai-loader.on{display:flex}
|
||||
.al-dot{width:10px;height:10px;border-radius:50%;background:var(--accent3);animation:al-b .9s ease-in-out infinite}
|
||||
.al-dot:nth-child(2){animation-delay:.18s;background:var(--accent)}
|
||||
.al-dot:nth-child(3){animation-delay:.36s;background:var(--accent2)}
|
||||
@keyframes al-b{0%,80%,100%{transform:scale(.5);opacity:.4}40%{transform:scale(1);opacity:1}}
|
||||
|
||||
/* Sonuç grid */
|
||||
.ai-results{margin-top:24px;display:none}
|
||||
.ai-results.on{display:block}
|
||||
.ai-res-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:16px;margin-top:16px}
|
||||
.ai-res-card{
|
||||
background:var(--surface);border:1px solid rgba(0,245,255,.1);border-radius:14px;
|
||||
overflow:hidden;cursor:pointer;transition:all .2s;
|
||||
}
|
||||
.ai-res-card:hover{transform:translateY(-5px);border-color:rgba(255,45,125,.4);box-shadow:0 12px 36px rgba(0,0,0,.4),0 0 28px rgba(255,45,125,.1)}
|
||||
.ai-res-poster{position:relative;aspect-ratio:2/3;background:var(--surface2);overflow:hidden}
|
||||
.ai-res-poster img{width:100%;height:100%;object-fit:cover;transition:transform .28s}
|
||||
.ai-res-card:hover .ai-res-poster img{transform:scale(1.06)}
|
||||
.ai-res-poster-ph{width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:2rem}
|
||||
.ai-res-reason{
|
||||
position:absolute;bottom:0;left:0;right:0;padding:8px;
|
||||
background:linear-gradient(to top,rgba(0,0,0,.88),transparent);
|
||||
font-size:.65rem;color:rgba(255,255,255,.8);line-height:1.35;
|
||||
}
|
||||
.ai-res-body{padding:9px 10px}
|
||||
.ai-res-title{font-size:.8rem;font-weight:700;color:var(--text);display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||||
.ai-res-meta{font-size:.7rem;color:var(--text3);margin-top:3px}
|
||||
|
||||
/* Natural search */
|
||||
.ai-search-wrap{position:relative}
|
||||
.ai-search-wrap input{
|
||||
width:100%;background:rgba(255,255,255,.05);border:1px solid rgba(184,77,255,.3);
|
||||
border-radius:12px;padding:14px 52px 14px 18px;color:var(--text);font-size:.9rem;
|
||||
font-family:inherit;outline:none;transition:all .18s;
|
||||
}
|
||||
.ai-search-wrap input:focus{
|
||||
border-color:var(--accent3);background:rgba(184,77,255,.06);
|
||||
box-shadow:0 0 0 3px rgba(184,77,255,.15),0 0 32px rgba(184,77,255,.1);
|
||||
}
|
||||
.ai-search-wrap input::placeholder{color:var(--text3)}
|
||||
.ai-search-btn{
|
||||
position:absolute;right:10px;top:50%;transform:translateY(-50%);
|
||||
width:36px;height:36px;border-radius:9px;border:none;cursor:pointer;
|
||||
background:linear-gradient(135deg,var(--accent3),var(--accent));color:#fff;font-size:.9rem;
|
||||
display:flex;align-items:center;justify-content:center;transition:transform .14s;
|
||||
}
|
||||
.ai-search-btn:hover{transform:translateY(-50%) scale(1.08)}
|
||||
.ai-search-hints{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}
|
||||
.ai-hint{
|
||||
padding:6px 13px;border-radius:999px;font-size:.75rem;font-weight:600;cursor:pointer;
|
||||
background:rgba(0,245,255,.06);border:1px solid rgba(0,245,255,.2);color:var(--accent2);
|
||||
transition:all .14s;
|
||||
}
|
||||
.ai-hint:hover{background:rgba(0,245,255,.12);box-shadow:0 0 12px rgba(0,245,255,.15)}
|
||||
|
||||
.ai-empty{text-align:center;padding:32px;color:var(--text3);font-size:.9rem}
|
||||
.ai-error{padding:14px 18px;border-radius:10px;background:rgba(255,60,90,.08);border:1px solid rgba(255,60,90,.25);color:#ff8a9a;font-size:.85rem;margin-top:16px;display:none}
|
||||
.ai-error.on{display:block}
|
||||
|
||||
@media(max-width:600px){
|
||||
.ai-hero{padding:36px 16px 20px}
|
||||
.ai-hero p{font-size:.9rem}
|
||||
.ai-section{padding:0 14px 40px}
|
||||
.ai-box{padding:18px 14px}
|
||||
.ai-res-grid{grid-template-columns:repeat(3,1fr);gap:10px}
|
||||
.ai-tabs{gap:5px}
|
||||
.ai-tab{padding:8px 14px;font-size:.82rem}
|
||||
.type-row{gap:6px}
|
||||
.mood-chip{font-size:.78rem;padding:6px 12px}
|
||||
.ai-search-wrap input{font-size:.85rem;padding:12px 46px 12px 14px}
|
||||
}
|
||||
@media(max-width:380px){
|
||||
.ai-res-grid{grid-template-columns:repeat(2,1fr)}
|
||||
.ai-hero h1{font-size:1.4rem}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="ai-hero">
|
||||
<div class="ai-hero-icon"><i class="bi bi-stars"></i></div>
|
||||
<h1>Animexe AI Asistan</h1>
|
||||
<p>Yapay zeka destekli anime keşfi. Ruh haline göre öneri al, doğal dille ara veya sağ alttaki balondan sohbet et.</p>
|
||||
|
||||
@guest
|
||||
<div style="background:rgba(255,45,125,.08);border:1px solid rgba(255,45,125,.25);border-radius:14px;padding:20px 24px;text-align:center;max-width:480px;margin:20px auto 0">
|
||||
<i class="bi bi-lock" style="font-size:1.8rem;color:var(--accent);display:block;margin-bottom:10px"></i>
|
||||
<p style="color:var(--text2);font-size:.9rem;margin-bottom:16px">AI özelliklerini kullanmak için ücretsiz hesap oluştur veya giriş yap.</p>
|
||||
<a href="{{ route('frontend.register') }}" style="display:inline-block;padding:10px 24px;background:var(--accent);border-radius:20px;color:#fff;font-weight:700;text-decoration:none;margin-right:8px">Ücretsiz Kayıt Ol</a>
|
||||
<a href="{{ route('frontend.login') }}" style="display:inline-block;padding:10px 24px;background:#21262d;border-radius:20px;color:var(--text2);font-weight:600;text-decoration:none">Giriş Yap</a>
|
||||
</div>
|
||||
@endguest
|
||||
|
||||
<div class="ai-tabs" @guest style="margin-top:20px;opacity:.5;pointer-events:none" @endguest>
|
||||
<button class="ai-tab on" onclick="switchAiTab('recommend',this)"><i class="bi bi-magic"></i> Kişisel Öneri</button>
|
||||
<button class="ai-tab" onclick="switchAiTab('search',this)"><i class="bi bi-search-heart"></i> Doğal Dil Ara</button>
|
||||
<button class="ai-tab" onclick="switchAiTab('platform',this)"><i class="bi bi-question-circle"></i> Site Yardımı</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ai-section">
|
||||
|
||||
{{-- ── TAB 1: Kişisel Öneri ─────────────────────────────────── --}}
|
||||
<div id="tab-recommend">
|
||||
<div class="ai-box">
|
||||
<div class="ai-box-title"><i class="bi bi-emoji-smile"></i> Nasıl bir ruh halindeydin?</div>
|
||||
<div class="mood-grid">
|
||||
@foreach([
|
||||
['😤','Aksiyona dolu','action'],['😢','Duygusal ağlatıcı','emotional'],
|
||||
['😂','Komik ve eğlenceli','comedy'],['😨','Gerilim/Korku','thriller'],
|
||||
['🌸','Romantik','romance'],['🤯','Akıl oyunları','mindgame'],
|
||||
['⚔️','Fantezi macera','fantasy'],['🤖','Sci-Fi / Mecha','scifi'],
|
||||
['🏫','Okul / Slice of life','school'],['💪','Motivasyonel','motivational'],
|
||||
] as $m)
|
||||
<button class="mood-chip" data-mood="{{ $m[2] }}" onclick="toggleMood(this,'{{ addslashes($m[0].' '.$m[1]) }}')">
|
||||
{{ $m[0] }} {{ $m[1] }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="ai-box-title" style="margin-top:4px"><i class="bi bi-tags"></i> Tür tercihleri (opsiyonel)</div>
|
||||
<div class="genre-wrap" id="genre-wrap">
|
||||
@foreach($genres as $g)
|
||||
<button class="genre-chip" data-genre="{{ $g->name }}" onclick="this.classList.toggle('on')">{{ $g->name }}</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="ai-box-title" style="margin-top:4px"><i class="bi bi-collection-play"></i> İçerik tipi</div>
|
||||
<div class="type-row" id="type-row">
|
||||
<button class="type-btn on" data-type="" onclick="selectType(this)">Hepsi</button>
|
||||
<button class="type-btn" data-type="series" onclick="selectType(this)">Dizi</button>
|
||||
<button class="type-btn" data-type="movie" onclick="selectType(this)">Film</button>
|
||||
</div>
|
||||
|
||||
<button class="ai-btn" id="rec-btn" onclick="getRecommendations()">
|
||||
<i class="bi bi-magic"></i> Bana Özel Öneri Al
|
||||
</button>
|
||||
<div class="ai-error" id="rec-error"></div>
|
||||
</div>
|
||||
|
||||
<div class="ai-loader" id="rec-loader">
|
||||
<div class="al-dot"></div><div class="al-dot"></div><div class="al-dot"></div>
|
||||
<span style="color:var(--text3);font-size:.85rem;margin-left:6px">AI düşünüyor...</span>
|
||||
</div>
|
||||
|
||||
<div class="ai-results" id="rec-results">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:4px">
|
||||
<span style="font-family:var(--font-display);font-size:.82rem;letter-spacing:.1em;text-transform:uppercase;color:var(--accent3)"><i class="bi bi-stars" style="margin-right:6px"></i>AI Önerileri</span>
|
||||
</div>
|
||||
<div class="ai-res-grid" id="rec-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── TAB 3: Platform Yardımı ─────────────────────────────── --}}
|
||||
<div id="tab-platform" style="display:none">
|
||||
<div class="ai-box">
|
||||
<div class="ai-box-title"><i class="bi bi-question-circle"></i> Animexe Hakkında Sık Sorulanlar</div>
|
||||
<div style="display:grid;gap:10px">
|
||||
@foreach([
|
||||
['Kayıt olmak ücretsiz mi?','Evet! Animexe\'ye kayıt olmak tamamen ücretsizdir. E-posta adresinle saniyeler içinde hesap oluşturabilirsin.'],
|
||||
['Premium üyelik ne sağlar?','Premium üyelik ile reklamsız izleme, 1080p HD kalite ve yeni bölümlere erken erişim imkanı kazanırsın.'],
|
||||
['Altyazı ve dublaj var mı?','Evet, çoğu animede Türkçe altyazı bulunur. Bir kısım animede Türkçe dublaj da mevcuttur.'],
|
||||
['Hangi cihazlarda çalışır?','Bilgisayar, tablet ve telefon tarayıcısında sorunsuz çalışır. Özel bir uygulama indirmene gerek yok.'],
|
||||
['Şifremi unuttum, ne yapmalıyım?','Giriş sayfasındaki "Şifremi Unuttum" linkine tıklayarak e-postanla şifre sıfırlama bağlantısı alabilirsin.'],
|
||||
] as [$q, $a])
|
||||
<details style="background:rgba(255,255,255,.03);border:1px solid rgba(184,77,255,.15);border-radius:10px;overflow:hidden">
|
||||
<summary style="padding:13px 16px;cursor:pointer;font-size:.86rem;font-weight:600;color:var(--text);list-style:none;display:flex;align-items:center;justify-content:space-between">
|
||||
{{ $q }}
|
||||
<i class="bi bi-chevron-down" style="font-size:.75rem;color:var(--accent3);flex-shrink:0;transition:transform .2s"></i>
|
||||
</summary>
|
||||
<p style="padding:0 16px 14px;margin:0;font-size:.84rem;color:var(--text2);line-height:1.65">{{ $a }}</p>
|
||||
</details>
|
||||
@endforeach
|
||||
</div>
|
||||
<div style="margin-top:20px;padding:14px;background:rgba(184,77,255,.07);border:1px solid rgba(184,77,255,.2);border-radius:12px;text-align:center">
|
||||
<p style="margin:0 0 12px;color:var(--text2);font-size:.85rem">Cevabını bulamadın mı? AI asistana sor!</p>
|
||||
<button class="ai-btn" style="width:auto;padding:10px 24px;display:inline-flex" onclick="openAiWithQuestion()">
|
||||
<i class="bi bi-chat-dots-fill"></i> AI\'a Sor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── TAB 2: Doğal Dil Arama ─────────────────────────────── --}}
|
||||
<div id="tab-search" style="display:none">
|
||||
<div class="ai-box">
|
||||
<div class="ai-box-title"><i class="bi bi-search-heart"></i> Ne aramak istiyorsun?</div>
|
||||
<div class="ai-search-wrap">
|
||||
<input type="text" id="ai-search-inp" placeholder="Ör: ağlatan romantik okul animesi, 2023'te çıkmış aksiyon dizisi..."
|
||||
onkeydown="if(event.key==='Enter') aiNaturalSearch()">
|
||||
<button class="ai-search-btn" onclick="aiNaturalSearch()"><i class="bi bi-send-fill"></i></button>
|
||||
</div>
|
||||
<div class="ai-search-hints">
|
||||
@foreach(['Ağlatan romantik anime','İsekai macera dizisi','Kısa 12 bölüm aksiyon','Dövüş sanatları anime','Distopik sci-fi film','Komedi okul dizisi'] as $h)
|
||||
<button class="ai-hint" onclick="document.getElementById('ai-search-inp').value=this.textContent;aiNaturalSearch()">{{ $h }}</button>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="ai-error" id="search-error"></div>
|
||||
</div>
|
||||
|
||||
<div class="ai-loader" id="search-loader">
|
||||
<div class="al-dot"></div><div class="al-dot"></div><div class="al-dot"></div>
|
||||
<span style="color:var(--text3);font-size:.85rem;margin-left:6px">AI arıyor...</span>
|
||||
</div>
|
||||
|
||||
<div class="ai-results" id="search-results">
|
||||
<div style="display:flex;align-items:center;gap:10px;margin-bottom:4px">
|
||||
<span style="font-family:var(--font-display);font-size:.82rem;letter-spacing:.1em;text-transform:uppercase;color:var(--accent2)"><i class="bi bi-lightning-fill" style="margin-right:6px"></i>AI Arama Sonuçları</span>
|
||||
</div>
|
||||
<div class="ai-res-grid" id="search-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]').content;
|
||||
|
||||
// ── Tab yönetimi ──────────────────────────────────────────────────────────────
|
||||
function switchAiTab(tab, btn){
|
||||
document.querySelectorAll('.ai-tab').forEach(b=>b.classList.remove('on'));
|
||||
btn.classList.add('on');
|
||||
document.getElementById('tab-recommend').style.display = tab==='recommend' ? '' : 'none';
|
||||
document.getElementById('tab-search').style.display = tab==='search' ? '' : 'none';
|
||||
document.getElementById('tab-platform').style.display = tab==='platform' ? '' : 'none';
|
||||
}
|
||||
|
||||
function openAiWithQuestion(){
|
||||
if(typeof window.toggleAiPanel === 'function'){
|
||||
window.toggleAiPanel();
|
||||
setTimeout(()=>{
|
||||
const inp = document.getElementById('ai-inp');
|
||||
if(inp){ inp.focus(); inp.placeholder='Site hakkında soru sor...'; }
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ruh hali seçimi ───────────────────────────────────────────────────────────
|
||||
let selectedMoods = [];
|
||||
function toggleMood(btn, label){
|
||||
btn.classList.toggle('on');
|
||||
if(btn.classList.contains('on')){
|
||||
selectedMoods.push(label);
|
||||
} else {
|
||||
selectedMoods = selectedMoods.filter(m=>m!==label);
|
||||
}
|
||||
}
|
||||
|
||||
function selectType(btn){
|
||||
document.querySelectorAll('#type-row .type-btn').forEach(b=>b.classList.remove('on'));
|
||||
btn.classList.add('on');
|
||||
}
|
||||
|
||||
// ── Öneri al ─────────────────────────────────────────────────────────────────
|
||||
async function getRecommendations(){
|
||||
const btn = document.getElementById('rec-btn');
|
||||
const loader = document.getElementById('rec-loader');
|
||||
const results = document.getElementById('rec-results');
|
||||
const errEl = document.getElementById('rec-error');
|
||||
|
||||
const mood = selectedMoods.join(', ');
|
||||
const genres = [...document.querySelectorAll('#genre-wrap .genre-chip.on')].map(c=>c.dataset.genre);
|
||||
const type = document.querySelector('#type-row .type-btn.on')?.dataset.type || '';
|
||||
|
||||
btn.disabled = true;
|
||||
loader.classList.add('on');
|
||||
results.classList.remove('on');
|
||||
errEl.classList.remove('on');
|
||||
|
||||
try{
|
||||
const res = await fetch('{{ route("ai.recommend") }}',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json','X-CSRF-TOKEN':CSRF},
|
||||
body: JSON.stringify({ mood, genres, type })
|
||||
});
|
||||
const data = await res.json();
|
||||
loader.classList.remove('on');
|
||||
|
||||
if(data.error){ errEl.textContent=data.error; errEl.classList.add('on'); }
|
||||
else { renderRecGrid(data.recommendations || [], 'rec-grid', true); results.classList.add('on'); }
|
||||
}catch(e){
|
||||
loader.classList.remove('on');
|
||||
errEl.textContent = 'Bağlantı hatası, tekrar dene.';
|
||||
errEl.classList.add('on');
|
||||
}
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
// ── Doğal dil arama ───────────────────────────────────────────────────────────
|
||||
async function aiNaturalSearch(){
|
||||
const query = document.getElementById('ai-search-inp').value.trim();
|
||||
if(!query) return;
|
||||
|
||||
const loader = document.getElementById('search-loader');
|
||||
const results = document.getElementById('search-results');
|
||||
const errEl = document.getElementById('search-error');
|
||||
|
||||
loader.classList.add('on');
|
||||
results.classList.remove('on');
|
||||
errEl.classList.remove('on');
|
||||
|
||||
try{
|
||||
const res = await fetch('{{ route("ai.search") }}',{
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json','X-CSRF-TOKEN':CSRF},
|
||||
body: JSON.stringify({ query })
|
||||
});
|
||||
const data = await res.json();
|
||||
loader.classList.remove('on');
|
||||
|
||||
if(data.error){ errEl.textContent=data.error; errEl.classList.add('on'); }
|
||||
else if(!data.results || !data.results.length){
|
||||
errEl.textContent = 'Sonuç bulunamadı. Farklı bir şey dene!';
|
||||
errEl.classList.add('on');
|
||||
} else {
|
||||
renderRecGrid(data.results, 'search-grid', false);
|
||||
results.classList.add('on');
|
||||
}
|
||||
}catch(e){
|
||||
loader.classList.remove('on');
|
||||
errEl.textContent = 'Bağlantı hatası, tekrar dene.';
|
||||
errEl.classList.add('on');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Kart render ───────────────────────────────────────────────────────────────
|
||||
function renderRecGrid(items, gridId, showReason){
|
||||
const grid = document.getElementById(gridId);
|
||||
grid.innerHTML = '';
|
||||
if(!items.length){
|
||||
grid.innerHTML = '<div class="ai-empty">Sonuç bulunamadı.</div>';
|
||||
return;
|
||||
}
|
||||
items.forEach(a=>{
|
||||
const detailUrl = '/anime/'+a.slug;
|
||||
const watchUrl = '/watch/'+a.slug+'/1/1';
|
||||
const typeLabel = a.type==='movie' ? '🎬 Film' : (a.type==='ova' ? 'OVA' : '📺 Dizi');
|
||||
const ratingHtml = a.rating ? `<span style="color:#f59e0b;font-weight:700">★${parseFloat(a.rating).toFixed(1)}</span>` : '';
|
||||
const epHtml = a.episode_count ? `<span>${a.episode_count} bölüm</span>` : '';
|
||||
grid.innerHTML += `
|
||||
<div class="ai-res-card">
|
||||
<div class="ai-res-poster">
|
||||
${a.cover
|
||||
? `<img src="${esc(a.cover)}" alt="${esc(a.title)}" loading="lazy">`
|
||||
: `<div class="ai-res-poster-ph"><i class="bi bi-image"></i></div>`}
|
||||
<div style="position:absolute;top:6px;left:6px;background:rgba(0,0,0,.72);backdrop-filter:blur(4px);border-radius:5px;padding:2px 7px;font-size:.65rem;font-weight:700;color:#fff">${typeLabel}</div>
|
||||
${showReason && a.reason ? `<div class="ai-res-reason">${esc(a.reason)}</div>` : ''}
|
||||
</div>
|
||||
<div class="ai-res-body">
|
||||
<div class="ai-res-title">${esc(a.title)}</div>
|
||||
<div class="ai-res-meta" style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:4px">
|
||||
${ratingHtml}${epHtml}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:5px;padding:0 9px 9px">
|
||||
<a href="${esc(watchUrl)}" style="flex:1;padding:6px 0;background:linear-gradient(135deg,var(--accent3),var(--accent));border-radius:8px;color:#fff;font-size:.72rem;font-weight:700;text-align:center;text-decoration:none;display:flex;align-items:center;justify-content:center;gap:3px"><i class="bi bi-play-fill"></i>İzle</a>
|
||||
<a href="${esc(detailUrl)}" style="flex:1;padding:6px 0;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:8px;color:var(--text2);font-size:.72rem;font-weight:700;text-align:center;text-decoration:none;display:flex;align-items:center;justify-content:center;gap:3px"><i class="bi bi-info-circle"></i>Detay</a>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function esc(s){ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,213 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Anime İsteği — Animexe')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.req-hero{padding:60px 24px 32px;text-align:center;max-width:700px;margin:0 auto}
|
||||
.req-hero h1{font-size:2rem;font-weight:800;color:var(--text);margin-bottom:8px}
|
||||
.req-hero p{color:var(--text2);font-size:.95rem}
|
||||
|
||||
.req-wrap{max-width:900px;margin:0 auto;padding:0 20px 60px}
|
||||
|
||||
.req-form-card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:24px;margin-bottom:32px}
|
||||
.req-form-card h2{font-size:1rem;font-weight:700;color:var(--text);margin-bottom:18px;display:flex;align-items:center;gap:8px}
|
||||
|
||||
.rf-field{margin-bottom:16px}
|
||||
.rf-label{display:block;font-size:.8rem;color:var(--text2);margin-bottom:6px;font-weight:500}
|
||||
.rf-input{width:100%;background:#0a0a14;border:1px solid var(--border);border-radius:8px;padding:10px 14px;color:var(--text);font-size:.9rem;transition:border-color .2s}
|
||||
.rf-input:focus{outline:none;border-color:var(--accent)}
|
||||
.rf-textarea{resize:vertical;min-height:80px}
|
||||
.rf-submit{display:flex;align-items:center;justify-content:center;gap:8px;padding:11px 24px;background:var(--accent);border:none;border-radius:20px;color:#fff;font-size:.9rem;font-weight:600;cursor:pointer;transition:opacity .2s}
|
||||
.rf-submit:hover{opacity:.85}
|
||||
|
||||
.req-list-card{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden}
|
||||
.req-list-head{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
|
||||
.req-list-head h2{font-size:.95rem;font-weight:700;color:var(--text);margin:0}
|
||||
|
||||
.req-item{padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.04);display:flex;align-items:flex-start;gap:16px;transition:background .15s}
|
||||
.req-item:last-child{border-bottom:none}
|
||||
.req-item:hover{background:rgba(255,255,255,.02)}
|
||||
|
||||
.req-vote{display:flex;flex-direction:column;align-items:center;gap:4px;min-width:52px}
|
||||
.req-vote-btn{width:44px;height:44px;border-radius:50%;border:1px solid var(--border);background:transparent;color:var(--text3);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;font-size:.95rem}
|
||||
.req-vote-btn:hover,.req-vote-btn.voted{border-color:var(--accent);background:rgba(255,45,125,.12);color:var(--accent)}
|
||||
.req-vote-cnt{font-size:.82rem;font-weight:700;color:var(--text2)}
|
||||
|
||||
.req-body{flex:1;min-width:0}
|
||||
.req-title{font-size:.95rem;font-weight:600;color:var(--text);margin-bottom:3px}
|
||||
.req-ori{font-size:.78rem;color:var(--text3);margin-bottom:6px}
|
||||
.req-note{font-size:.78rem;color:var(--text2);margin-bottom:8px;line-height:1.5}
|
||||
.req-meta{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||||
.req-status{font-size:.7rem;padding:2px 10px;border-radius:20px;font-weight:600}
|
||||
.req-by{font-size:.72rem;color:var(--text3)}
|
||||
.req-admin-note{font-size:.75rem;color:#f0883e;margin-top:6px;display:flex;align-items:center;gap:5px}
|
||||
|
||||
.toast-msg{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#161b22;border:1px solid #30363d;border-radius:10px;padding:12px 20px;color:var(--text);font-size:.85rem;z-index:9999;animation:fadeInUp .3s ease;box-shadow:0 8px 24px rgba(0,0,0,.5)}
|
||||
@keyframes fadeInUp{from{transform:translateX(-50%) translateY(10px);opacity:0}to{transform:translateX(-50%) translateY(0);opacity:1}}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div style="padding-top:var(--nav-h)">
|
||||
|
||||
<div class="req-hero">
|
||||
<h1><i class="bi bi-plus-circle" style="color:var(--accent)"></i> Anime İsteği</h1>
|
||||
<p>İzlemek istediğin ama sitede olmayan bir anime var mı? Buradan bildir, en çok istenen animeleri ekliyoruz.</p>
|
||||
</div>
|
||||
|
||||
<div class="req-wrap">
|
||||
|
||||
{{-- İstek formu (sadece üyeler) --}}
|
||||
@auth
|
||||
<div class="req-form-card" id="req-form-card">
|
||||
<h2><i class="bi bi-send" style="color:var(--accent)"></i> Yeni İstek Gönder</h2>
|
||||
<div id="req-success" style="display:none;background:rgba(63,185,80,.1);border:1px solid #3fb95040;border-radius:8px;padding:12px 16px;color:#3fb950;font-size:.88rem;margin-bottom:16px"></div>
|
||||
<div class="rf-field">
|
||||
<label class="rf-label">Anime Adı (Türkçe veya bilinen ismi) *</label>
|
||||
<input id="rf-title" type="text" class="rf-input" placeholder="örn: Demon Slayer" maxlength="200">
|
||||
</div>
|
||||
<div class="rf-field">
|
||||
<label class="rf-label">Orijinal / İngilizce Adı</label>
|
||||
<input id="rf-ori" type="text" class="rf-input" placeholder="örn: Kimetsu no Yaiba" maxlength="200">
|
||||
</div>
|
||||
<div class="rf-field">
|
||||
<label class="rf-label">Not / Açıklama</label>
|
||||
<textarea id="rf-note" class="rf-input rf-textarea" placeholder="Hangi sezon, neden istiyorsun? vs."></textarea>
|
||||
</div>
|
||||
<button class="rf-submit" id="rf-btn" onclick="submitRequest()">
|
||||
<i class="bi bi-send-fill"></i> İstek Gönder
|
||||
</button>
|
||||
</div>
|
||||
@else
|
||||
<div style="background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:28px;text-align:center;margin-bottom:32px">
|
||||
<i class="bi bi-lock" style="font-size:2rem;color:var(--text3);margin-bottom:12px;display:block"></i>
|
||||
<p style="color:var(--text2);margin-bottom:16px">Anime isteği göndermek için giriş yapman gerekiyor.</p>
|
||||
<a href="{{ route('frontend.login') }}" style="padding:10px 24px;background:var(--accent);border-radius:20px;color:#fff;font-weight:600;text-decoration:none;font-size:.88rem">Giriş Yap</a>
|
||||
<a href="{{ route('frontend.register') }}" style="padding:10px 24px;background:#21262d;border-radius:20px;color:var(--text2);font-weight:600;text-decoration:none;font-size:.88rem;margin-left:8px">Kayıt Ol</a>
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
{{-- İstekler listesi --}}
|
||||
<div class="req-list-card">
|
||||
<div class="req-list-head">
|
||||
<h2><i class="bi bi-list-ul" style="color:var(--accent);margin-right:6px"></i>Tüm İstekler</h2>
|
||||
<span style="font-size:.78rem;color:var(--text3)">{{ $requests->total() }} istek</span>
|
||||
</div>
|
||||
|
||||
@forelse($requests as $req)
|
||||
@php
|
||||
$st = \App\Models\AnimeRequest::STATUSES[$req->status] ?? ['label'=>$req->status,'color'=>'#8b949e'];
|
||||
$hasVoted = in_array($req->id, $votedIds);
|
||||
@endphp
|
||||
<div class="req-item">
|
||||
<div class="req-vote">
|
||||
@auth
|
||||
<button class="req-vote-btn {{ $hasVoted?'voted':'' }}" id="vote-btn-{{ $req->id }}" onclick="voteRequest({{ $req->id }})">
|
||||
<i class="bi bi-heart{{ $hasVoted?'-fill':'' }}" id="vote-icon-{{ $req->id }}"></i>
|
||||
</button>
|
||||
@else
|
||||
<a href="{{ route('frontend.login') }}" class="req-vote-btn"><i class="bi bi-heart"></i></a>
|
||||
@endauth
|
||||
<span class="req-vote-cnt" id="vote-cnt-{{ $req->id }}">{{ $req->vote_count }}</span>
|
||||
</div>
|
||||
<div class="req-body">
|
||||
<div class="req-title">{{ $req->title }}</div>
|
||||
@if($req->original_title)<div class="req-ori">{{ $req->original_title }}</div>@endif
|
||||
@if($req->note)<div class="req-note">{{ Str::limit($req->note, 150) }}</div>@endif
|
||||
<div class="req-meta">
|
||||
<span class="req-status" style="background:{{ $st['color'] }}22;color:{{ $st['color'] }};border:1px solid {{ $st['color'] }}44">{{ $st['label'] }}</span>
|
||||
@if($req->user)<span class="req-by">{{ $req->user->name }} tarafından istendi</span>@endif
|
||||
<span class="req-by">· {{ $req->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
@if($req->admin_note)
|
||||
<div class="req-admin-note"><i class="bi bi-shield-check"></i> {{ $req->admin_note }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div style="padding:40px;text-align:center;color:var(--text3)">Henüz istek yok. İlk isteği sen gönder!</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="mt-4">{{ $requests->links() }}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content||'';
|
||||
|
||||
async function submitRequest() {
|
||||
const title = document.getElementById('rf-title').value.trim();
|
||||
if (!title) { showToast('Anime adı zorunlu!'); return; }
|
||||
|
||||
const btn = document.getElementById('rf-btn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Gönderiliyor...';
|
||||
|
||||
try {
|
||||
const res = await fetch('{{ route("anime.request.store") }}', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json','X-CSRF-TOKEN':CSRF},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
original_title: document.getElementById('rf-ori').value.trim(),
|
||||
note: document.getElementById('rf-note').value.trim(),
|
||||
})
|
||||
});
|
||||
const d = await res.json();
|
||||
const msg = d.merged
|
||||
? `Bu anime zaten istekler listesinde! Oyun eklendi (toplam: ${d.vote_count} oy).`
|
||||
: `İsteğin gönderildi! Teşekkürler 🎉`;
|
||||
document.getElementById('req-success').style.display = 'block';
|
||||
document.getElementById('req-success').textContent = msg;
|
||||
document.getElementById('rf-title').value = '';
|
||||
document.getElementById('rf-ori').value = '';
|
||||
document.getElementById('rf-note').value = '';
|
||||
setTimeout(()=>location.reload(), 2000);
|
||||
} catch(e) {
|
||||
showToast('Bir hata oluştu, tekrar dene.');
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-send-fill"></i> İstek Gönder';
|
||||
}
|
||||
|
||||
async function voteRequest(id) {
|
||||
const btn = document.getElementById(`vote-btn-${id}`);
|
||||
const ico = document.getElementById(`vote-icon-${id}`);
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(`/anime-request/${id}/vote`, {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json','X-CSRF-TOKEN':CSRF},
|
||||
body:'{}'
|
||||
});
|
||||
if (res.status === 401) { window.location = '{{ route("frontend.login") }}'; return; }
|
||||
if (!res.ok) { showToast('Bir hata oluştu.'); return; }
|
||||
const d = await res.json();
|
||||
document.getElementById(`vote-cnt-${id}`).textContent = d.vote_count;
|
||||
if (d.voted) {
|
||||
btn.classList.add('voted');
|
||||
ico.className = 'bi bi-heart-fill';
|
||||
} else {
|
||||
btn.classList.remove('voted');
|
||||
ico.className = 'bi bi-heart';
|
||||
}
|
||||
} catch(e) {
|
||||
showToast('Bağlantı hatası, tekrar dene.');
|
||||
}
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
|
||||
function showToast(msg) {
|
||||
const t = document.createElement('div');
|
||||
t.className='toast-msg';
|
||||
t.textContent=msg;
|
||||
document.body.appendChild(t);
|
||||
setTimeout(()=>t.remove(), 3000);
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title','Şifremi Unuttum — Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
.auth-page{min-height:calc(100vh - var(--nav-h));display:flex;align-items:center;justify-content:center;padding:40px 16px;position:relative;overflow:hidden;background:radial-gradient(ellipse 80% 60% at 50% -20%,rgba(184,77,255,.2),transparent 55%),radial-gradient(ellipse 50% 40% at 100% 100%,rgba(0,245,255,.1),transparent)}
|
||||
.auth-page::before{content:'';position:absolute;inset:0;pointer-events:none;background:linear-gradient(rgba(0,245,255,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,245,255,.03) 1px,transparent 1px);background-size:40px 40px;mask-image:radial-gradient(ellipse 80% 80% at 50% 50%,black,transparent)}
|
||||
@keyframes auth-box-in{from{opacity:0;transform:translateY(24px) scale(.97)}to{opacity:1;transform:none}}
|
||||
.auth-box{background:linear-gradient(160deg,rgba(14,14,28,.97),rgba(8,8,18,.99));border:1px solid rgba(0,245,255,.22);border-radius:20px;padding:clamp(28px,5vw,44px);width:100%;max-width:440px;box-shadow:0 32px 80px rgba(0,0,0,.65),0 0 60px rgba(0,245,255,.08);position:relative;overflow:hidden;animation:auth-box-in .55s cubic-bezier(.22,1,.36,1) .1s both}
|
||||
.auth-box::before{content:'';position:absolute;top:0;left:10%;right:10%;height:1px;background:linear-gradient(90deg,transparent,var(--accent2),var(--accent),transparent);opacity:.7}
|
||||
.auth-logo{text-align:center;margin-bottom:28px}
|
||||
.auth-logo .name{font-family:var(--font-display);font-size:1.4rem;font-weight:900;letter-spacing:.22em;background:linear-gradient(105deg,#fff,var(--accent2),var(--accent));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;filter:drop-shadow(0 0 20px rgba(0,245,255,.3))}
|
||||
.auth-logo p{color:rgba(255,255,255,.35);font-size:.85rem;margin-top:5px}
|
||||
.f-group{margin-bottom:16px}
|
||||
.f-group label{display:block;font-size:.79rem;font-weight:700;color:rgba(255,255,255,.45);margin-bottom:7px;letter-spacing:.05em;text-transform:uppercase}
|
||||
.f-group input{width:100%;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:12px 16px;color:var(--text);font-size:.9rem;outline:none;transition:border .18s,box-shadow .18s}
|
||||
.f-group input:focus{border-color:var(--accent2);box-shadow:0 0 0 2px rgba(0,245,255,.12);background:rgba(0,245,255,.04)}
|
||||
.f-group input::placeholder{color:rgba(255,255,255,.2)}
|
||||
.auth-btn{width:100%;padding:13px;border-radius:12px;border:none;cursor:pointer;background:linear-gradient(135deg,var(--accent),var(--accent3));color:#fff;font-size:.93rem;font-weight:800;transition:transform .14s,box-shadow .18s;box-shadow:0 0 32px rgba(255,45,125,.35),0 4px 20px rgba(0,0,0,.35)}
|
||||
.auth-btn:hover{transform:translateY(-2px);box-shadow:0 0 44px rgba(255,45,125,.5)}
|
||||
.auth-switch{text-align:center;font-size:.85rem;color:rgba(255,255,255,.35);margin-top:20px}
|
||||
.auth-switch a{color:var(--accent2);font-weight:700}
|
||||
.status-box{background:rgba(0,245,255,.07);border:1px solid rgba(0,245,255,.2);border-radius:10px;padding:14px 16px;font-size:.87rem;color:var(--accent2);margin-bottom:20px}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div class="auth-page">
|
||||
<div class="auth-box">
|
||||
<div class="auth-logo">
|
||||
<div class="name">ANIMEXE</div>
|
||||
<p>Şifre sıfırlama</p>
|
||||
</div>
|
||||
|
||||
@if(session('status'))
|
||||
<div class="status-box"><i class="bi bi-check-circle-fill"></i> {{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-err">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<p style="font-size:.87rem;color:rgba(255,255,255,.4);margin-bottom:20px">Kayıtlı e-posta adresini gir, sana şifre sıfırlama bağlantısı gönderelim.</p>
|
||||
|
||||
<form action="{{ route('password.request') }}" method="POST">
|
||||
@csrf
|
||||
<div class="f-group">
|
||||
<label>E-posta Adresi</label>
|
||||
<input type="email" name="email" value="{{ old('email') }}" placeholder="ornek@email.com" required autofocus>
|
||||
</div>
|
||||
<button type="submit" class="auth-btn">Sıfırlama Bağlantısı Gönder</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-switch">
|
||||
<a href="{{ route('frontend.login') }}"><i class="bi bi-arrow-left"></i> Girişe dön</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,159 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title','Giriş Yap — Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
/* ═══════════════════════════════════════════════════
|
||||
AUTH PAGE PREMIUM
|
||||
═══════════════════════════════════════════════════ */
|
||||
.auth-page{
|
||||
min-height:calc(100vh - var(--nav-h));
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
padding:40px 16px;
|
||||
position:relative;overflow:hidden;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 50% -20%,rgba(184,77,255,.2),transparent 55%),
|
||||
radial-gradient(ellipse 50% 40% at 100% 100%,rgba(0,245,255,.1),transparent),
|
||||
radial-gradient(ellipse 40% 35% at 0% 100%,rgba(255,45,125,.08),transparent);
|
||||
}
|
||||
/* Animated grid lines in background */
|
||||
.auth-page::before{
|
||||
content:'';position:absolute;inset:0;pointer-events:none;
|
||||
background:
|
||||
linear-gradient(rgba(0,245,255,.03) 1px,transparent 1px),
|
||||
linear-gradient(90deg,rgba(0,245,255,.03) 1px,transparent 1px);
|
||||
background-size:40px 40px;
|
||||
mask-image:radial-gradient(ellipse 80% 80% at 50% 50%,black,transparent);
|
||||
}
|
||||
|
||||
@keyframes auth-box-in{
|
||||
from{opacity:0;transform:translateY(24px) scale(.97)}
|
||||
to{opacity:1;transform:none}
|
||||
}
|
||||
.auth-box{
|
||||
background:linear-gradient(160deg,rgba(14,14,28,.97),rgba(8,8,18,.99));
|
||||
border:1px solid rgba(0,245,255,.22);
|
||||
border-radius:20px;padding:clamp(28px,5vw,44px);
|
||||
width:100%;max-width:440px;
|
||||
box-shadow:0 32px 80px rgba(0,0,0,.65),0 0 60px rgba(0,245,255,.08),0 0 80px rgba(255,45,125,.05);
|
||||
position:relative;overflow:hidden;
|
||||
animation:auth-box-in .55s cubic-bezier(.22,1,.36,1) .1s both;
|
||||
}
|
||||
/* Top accent line */
|
||||
.auth-box::before{
|
||||
content:'';position:absolute;top:0;left:10%;right:10%;height:1px;
|
||||
background:linear-gradient(90deg,transparent,var(--accent2),var(--accent),transparent);
|
||||
opacity:.7;
|
||||
}
|
||||
|
||||
.auth-logo{text-align:center;margin-bottom:28px}
|
||||
.auth-logo .name{
|
||||
font-family:var(--font-display);font-size:1.4rem;font-weight:900;letter-spacing:.22em;
|
||||
background:linear-gradient(105deg,#fff 0%,var(--accent2) 40%,var(--accent) 100%);
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
|
||||
text-shadow:none;
|
||||
filter:drop-shadow(0 0 20px rgba(0,245,255,.3));
|
||||
}
|
||||
.auth-logo p{color:rgba(255,255,255,.35);font-size:.85rem;margin-top:5px}
|
||||
.auth-title{font-size:1.18rem;font-weight:800;margin-bottom:22px;color:#fff}
|
||||
.f-group{margin-bottom:16px;position:relative}
|
||||
.f-group label{display:block;font-size:.79rem;font-weight:700;color:rgba(255,255,255,.45);margin-bottom:7px;letter-spacing:.05em;text-transform:uppercase}
|
||||
.f-group input{
|
||||
width:100%;
|
||||
background:rgba(255,255,255,.04);
|
||||
border:1px solid rgba(255,255,255,.1);
|
||||
border-radius:10px;
|
||||
padding:12px 16px;
|
||||
color:var(--text);font-size:.9rem;outline:none;
|
||||
transition:border .18s,box-shadow .18s,background .18s;
|
||||
}
|
||||
.f-group input:focus{
|
||||
border-color:var(--accent2);
|
||||
box-shadow:0 0 0 2px rgba(0,245,255,.12),0 0 24px rgba(0,245,255,.06);
|
||||
background:rgba(0,245,255,.04);
|
||||
}
|
||||
.f-group input::placeholder{color:rgba(255,255,255,.2)}
|
||||
.f-err{font-size:.78rem;color:#fca5a5;margin-top:5px;display:flex;align-items:center;gap:4px}
|
||||
.f-check{display:flex;align-items:center;gap:9px;margin-bottom:18px}
|
||||
.f-check input{width:16px;height:16px;accent-color:var(--accent);border-radius:4px}
|
||||
.f-check label{font-size:.84rem;color:rgba(255,255,255,.45);cursor:pointer}
|
||||
.auth-btn{
|
||||
width:100%;padding:13px;border-radius:12px;border:none;cursor:pointer;
|
||||
background:linear-gradient(135deg,var(--accent) 0%,var(--accent3) 100%);
|
||||
color:#fff;font-size:.93rem;font-weight:800;letter-spacing:.03em;
|
||||
transition:transform .14s,box-shadow .18s;
|
||||
box-shadow:0 0 32px rgba(255,45,125,.35),0 4px 20px rgba(0,0,0,.35);
|
||||
position:relative;overflow:hidden;
|
||||
}
|
||||
.auth-btn:hover{transform:translateY(-2px);box-shadow:0 0 44px rgba(255,45,125,.5),0 6px 24px rgba(0,0,0,.35)}
|
||||
.auth-btn:active{transform:translateY(0)}
|
||||
.auth-sep{text-align:center;margin:18px 0;color:rgba(255,255,255,.2);font-size:.8rem;position:relative}
|
||||
.auth-sep::before,.auth-sep::after{content:'';position:absolute;top:50%;width:38%;height:1px;background:rgba(255,255,255,.08)}
|
||||
.auth-sep::before{left:0}
|
||||
.auth-sep::after{right:0}
|
||||
.auth-switch{text-align:center;font-size:.85rem;color:rgba(255,255,255,.35);margin-top:20px}
|
||||
.auth-switch a{color:var(--accent2);font-weight:700}
|
||||
.auth-switch a:hover{text-decoration:underline;text-shadow:0 0 12px rgba(0,245,255,.5)}
|
||||
.social-btns{display:flex;flex-direction:column;gap:10px;margin-top:4px}
|
||||
.social-btn{
|
||||
display:flex;align-items:center;justify-content:center;gap:10px;
|
||||
width:100%;padding:11px 16px;border-radius:10px;border:1px solid rgba(255,255,255,.1);
|
||||
background:rgba(255,255,255,.04);color:rgba(255,255,255,.82);
|
||||
font-size:.88rem;font-weight:600;cursor:pointer;
|
||||
transition:background .15s,border-color .15s,transform .12s;
|
||||
text-decoration:none;
|
||||
}
|
||||
.social-btn:hover{background:rgba(255,255,255,.09);border-color:rgba(255,255,255,.22);transform:translateY(-1px)}
|
||||
.social-btn svg{width:18px;height:18px;flex-shrink:0}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div class="auth-page">
|
||||
<div class="auth-box">
|
||||
<div class="auth-logo">
|
||||
<div class="name">ANIMEXE</div>
|
||||
<p>Hesabına giriş yap</p>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-err">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('frontend.login') }}" method="POST">
|
||||
@csrf
|
||||
<div class="f-group">
|
||||
<label>E-posta</label>
|
||||
<input type="email" name="email" value="{{ old('email') }}" placeholder="ornek@email.com" required autofocus>
|
||||
</div>
|
||||
<div class="f-group">
|
||||
<label>Şifre</label>
|
||||
<input type="password" name="password" placeholder="••••••••" required>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:18px">
|
||||
<div class="f-check" style="margin-bottom:0">
|
||||
<input type="checkbox" id="rem" name="remember">
|
||||
<label for="rem">Beni hatırla</label>
|
||||
</div>
|
||||
<a href="{{ route('password.request') }}" style="font-size:.82rem;color:rgba(255,255,255,.4);transition:color .15s" onmouseover="this.style.color='var(--accent2)'" onmouseout="this.style.color='rgba(255,255,255,.4)'">Şifremi unuttum</a>
|
||||
</div>
|
||||
<button type="submit" class="auth-btn">Giriş Yap</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-sep" style="margin-top:22px">veya şununla devam et</div>
|
||||
|
||||
<div class="social-btns">
|
||||
<a href="{{ route('social.redirect', 'google') }}" class="social-btn">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>
|
||||
Google ile devam et
|
||||
</a>
|
||||
<a href="{{ route('social.redirect', 'discord') }}" class="social-btn">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#5865F2"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
||||
Discord ile devam et
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="auth-switch">
|
||||
Hesabın yok mu? <a href="{{ route('frontend.register') }}">Üye ol</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,110 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title','Üye Ol — Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
/* ═══════════════════════════════════════════════���═══
|
||||
REGISTER PAGE PREMIUM (shared with login styles)
|
||||
═══════════════════════════════════════════════════ */
|
||||
.auth-page{
|
||||
min-height:calc(100vh - var(--nav-h));
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
padding:40px 16px;position:relative;overflow:hidden;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 50% -20%,rgba(184,77,255,.2),transparent 55%),
|
||||
radial-gradient(ellipse 50% 40% at 100% 100%,rgba(0,245,255,.1),transparent),
|
||||
radial-gradient(ellipse 40% 35% at 0% 100%,rgba(255,45,125,.08),transparent);
|
||||
}
|
||||
.auth-page::before{
|
||||
content:'';position:absolute;inset:0;pointer-events:none;
|
||||
background:linear-gradient(rgba(0,245,255,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,245,255,.03) 1px,transparent 1px);
|
||||
background-size:40px 40px;
|
||||
mask-image:radial-gradient(ellipse 80% 80% at 50% 50%,black,transparent);
|
||||
}
|
||||
@keyframes auth-box-in{from{opacity:0;transform:translateY(24px) scale(.97)}to{opacity:1;transform:none}}
|
||||
.auth-box{
|
||||
background:linear-gradient(160deg,rgba(14,14,28,.97),rgba(8,8,18,.99));
|
||||
border:1px solid rgba(0,245,255,.22);border-radius:20px;
|
||||
padding:clamp(24px,5vw,40px);width:100%;max-width:440px;
|
||||
box-shadow:0 32px 80px rgba(0,0,0,.65),0 0 60px rgba(0,245,255,.08),0 0 80px rgba(255,45,125,.05);
|
||||
position:relative;overflow:hidden;
|
||||
animation:auth-box-in .55s cubic-bezier(.22,1,.36,1) .1s both;
|
||||
}
|
||||
.auth-box::before{content:'';position:absolute;top:0;left:10%;right:10%;height:1px;background:linear-gradient(90deg,transparent,var(--accent2),var(--accent),transparent);opacity:.7}
|
||||
.auth-logo{text-align:center;margin-bottom:24px}
|
||||
.auth-logo .name{font-family:var(--font-display);font-size:1.4rem;font-weight:900;letter-spacing:.22em;background:linear-gradient(105deg,#fff 0%,var(--accent2) 40%,var(--accent) 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;filter:drop-shadow(0 0 20px rgba(0,245,255,.3))}
|
||||
.auth-logo p{color:rgba(255,255,255,.35);font-size:.85rem;margin-top:5px}
|
||||
.f-group{margin-bottom:14px;position:relative}
|
||||
.f-group label{display:block;font-size:.79rem;font-weight:700;color:rgba(255,255,255,.4);margin-bottom:6px;letter-spacing:.05em;text-transform:uppercase}
|
||||
.f-group input{width:100%;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:12px 16px;color:var(--text);font-size:.9rem;outline:none;transition:border .18s,box-shadow .18s,background .18s}
|
||||
.f-group input:focus{border-color:var(--accent2);box-shadow:0 0 0 2px rgba(0,245,255,.12),0 0 24px rgba(0,245,255,.06);background:rgba(0,245,255,.04)}
|
||||
.f-group input::placeholder{color:rgba(255,255,255,.2)}
|
||||
.f-err{font-size:.78rem;color:#fca5a5;margin-top:5px}
|
||||
.auth-btn{width:100%;padding:13px;border-radius:12px;border:none;cursor:pointer;background:linear-gradient(135deg,var(--accent),var(--accent3));color:#fff;font-size:.93rem;font-weight:800;letter-spacing:.03em;transition:transform .14s,box-shadow .18s;margin-top:4px;box-shadow:0 0 32px rgba(255,45,125,.35),0 4px 20px rgba(0,0,0,.35);position:relative;overflow:hidden}
|
||||
.auth-btn:hover{transform:translateY(-2px);box-shadow:0 0 44px rgba(255,45,125,.5),0 6px 24px rgba(0,0,0,.35)}
|
||||
.auth-terms{font-size:.78rem;color:rgba(255,255,255,.25);text-align:center;margin-top:12px}
|
||||
.auth-terms a{color:rgba(255,255,255,.45)}
|
||||
.auth-switch{text-align:center;font-size:.85rem;color:rgba(255,255,255,.35);margin-top:16px}
|
||||
.auth-switch a{color:var(--accent2);font-weight:700}
|
||||
.auth-switch a:hover{text-decoration:underline;text-shadow:0 0 12px rgba(0,245,255,.5)}
|
||||
.auth-sep{text-align:center;margin:18px 0;color:rgba(255,255,255,.2);font-size:.8rem;position:relative}
|
||||
.auth-sep::before,.auth-sep::after{content:'';position:absolute;top:50%;width:38%;height:1px;background:rgba(255,255,255,.08)}
|
||||
.auth-sep::before{left:0}.auth-sep::after{right:0}
|
||||
.social-btns{display:flex;flex-direction:column;gap:10px}
|
||||
.social-btn{display:flex;align-items:center;justify-content:center;gap:10px;width:100%;padding:11px 16px;border-radius:10px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.04);color:rgba(255,255,255,.82);font-size:.88rem;font-weight:600;cursor:pointer;transition:background .15s,border-color .15s,transform .12s;text-decoration:none}
|
||||
.social-btn:hover{background:rgba(255,255,255,.09);border-color:rgba(255,255,255,.22);transform:translateY(-1px)}
|
||||
.social-btn svg{width:18px;height:18px;flex-shrink:0}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div class="auth-page">
|
||||
<div class="auth-box">
|
||||
<div class="auth-logo">
|
||||
<div class="name">ANIMEXE</div>
|
||||
<p>Ücretsiz hesap oluştur</p>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-err">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('frontend.register') }}" method="POST">
|
||||
@csrf
|
||||
<div class="f-group">
|
||||
<label>Adın</label>
|
||||
<input type="text" name="name" value="{{ old('name') }}" placeholder="Adın Soyadın" required autofocus>
|
||||
</div>
|
||||
<div class="f-group">
|
||||
<label>E-posta</label>
|
||||
<input type="email" name="email" value="{{ old('email') }}" placeholder="ornek@email.com" required>
|
||||
</div>
|
||||
<div class="f-group">
|
||||
<label>Şifre</label>
|
||||
<input type="password" name="password" placeholder="En az 6 karakter" required>
|
||||
</div>
|
||||
<div class="f-group">
|
||||
<label>Şifre Tekrar</label>
|
||||
<input type="password" name="password_confirmation" placeholder="Şifreni tekrarla" required>
|
||||
</div>
|
||||
<button type="submit" class="auth-btn">Üye Ol — Ücretsiz</button>
|
||||
<p class="auth-terms">Üye olarak <a href="#" style="color:var(--accent)">Kullanım Koşulları</a>'nı kabul etmiş olursunuz.</p>
|
||||
</form>
|
||||
|
||||
<div class="auth-sep">veya şununla kayıt ol</div>
|
||||
|
||||
<div class="social-btns">
|
||||
<a href="{{ route('social.redirect', 'google') }}" class="social-btn">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>
|
||||
Google ile kayıt ol
|
||||
</a>
|
||||
<a href="{{ route('social.redirect', 'discord') }}" class="social-btn">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#5865F2"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
||||
Discord ile kayıt ol
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="auth-switch">
|
||||
Zaten üye misin? <a href="{{ route('frontend.login') }}">Giriş yap</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,53 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title','Yeni Şifre Belirle — Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
.auth-page{min-height:calc(100vh - var(--nav-h));display:flex;align-items:center;justify-content:center;padding:40px 16px;position:relative;overflow:hidden;background:radial-gradient(ellipse 80% 60% at 50% -20%,rgba(184,77,255,.2),transparent 55%),radial-gradient(ellipse 50% 40% at 100% 100%,rgba(0,245,255,.1),transparent)}
|
||||
.auth-page::before{content:'';position:absolute;inset:0;pointer-events:none;background:linear-gradient(rgba(0,245,255,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,245,255,.03) 1px,transparent 1px);background-size:40px 40px;mask-image:radial-gradient(ellipse 80% 80% at 50% 50%,black,transparent)}
|
||||
@keyframes auth-box-in{from{opacity:0;transform:translateY(24px) scale(.97)}to{opacity:1;transform:none}}
|
||||
.auth-box{background:linear-gradient(160deg,rgba(14,14,28,.97),rgba(8,8,18,.99));border:1px solid rgba(0,245,255,.22);border-radius:20px;padding:clamp(28px,5vw,44px);width:100%;max-width:440px;box-shadow:0 32px 80px rgba(0,0,0,.65),0 0 60px rgba(0,245,255,.08);position:relative;overflow:hidden;animation:auth-box-in .55s cubic-bezier(.22,1,.36,1) .1s both}
|
||||
.auth-box::before{content:'';position:absolute;top:0;left:10%;right:10%;height:1px;background:linear-gradient(90deg,transparent,var(--accent2),var(--accent),transparent);opacity:.7}
|
||||
.auth-logo{text-align:center;margin-bottom:28px}
|
||||
.auth-logo .name{font-family:var(--font-display);font-size:1.4rem;font-weight:900;letter-spacing:.22em;background:linear-gradient(105deg,#fff,var(--accent2),var(--accent));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;filter:drop-shadow(0 0 20px rgba(0,245,255,.3))}
|
||||
.auth-logo p{color:rgba(255,255,255,.35);font-size:.85rem;margin-top:5px}
|
||||
.f-group{margin-bottom:16px}
|
||||
.f-group label{display:block;font-size:.79rem;font-weight:700;color:rgba(255,255,255,.45);margin-bottom:7px;letter-spacing:.05em;text-transform:uppercase}
|
||||
.f-group input{width:100%;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:12px 16px;color:var(--text);font-size:.9rem;outline:none;transition:border .18s,box-shadow .18s}
|
||||
.f-group input:focus{border-color:var(--accent2);box-shadow:0 0 0 2px rgba(0,245,255,.12);background:rgba(0,245,255,.04)}
|
||||
.f-group input::placeholder{color:rgba(255,255,255,.2)}
|
||||
.auth-btn{width:100%;padding:13px;border-radius:12px;border:none;cursor:pointer;background:linear-gradient(135deg,var(--accent),var(--accent3));color:#fff;font-size:.93rem;font-weight:800;transition:transform .14s,box-shadow .18s;box-shadow:0 0 32px rgba(255,45,125,.35),0 4px 20px rgba(0,0,0,.35)}
|
||||
.auth-btn:hover{transform:translateY(-2px)}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div class="auth-page">
|
||||
<div class="auth-box">
|
||||
<div class="auth-logo">
|
||||
<div class="name">ANIMEXE</div>
|
||||
<p>Yeni şifre belirle</p>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-err">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('password.update') }}" method="POST">
|
||||
@csrf
|
||||
<input type="hidden" name="token" value="{{ $token }}">
|
||||
<div class="f-group">
|
||||
<label>E-posta Adresi</label>
|
||||
<input type="email" name="email" value="{{ old('email', $email) }}" required>
|
||||
</div>
|
||||
<div class="f-group">
|
||||
<label>Yeni Şifre</label>
|
||||
<input type="password" name="password" placeholder="En az 6 karakter" required>
|
||||
</div>
|
||||
<div class="f-group">
|
||||
<label>Şifre Tekrar</label>
|
||||
<input type="password" name="password_confirmation" placeholder="Şifreni tekrarla" required>
|
||||
</div>
|
||||
<button type="submit" class="auth-btn">Şifremi Güncelle</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,48 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title','E-posta Doğrulama — Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
.auth-page{min-height:calc(100vh - var(--nav-h));display:flex;align-items:center;justify-content:center;padding:40px 16px;background:radial-gradient(ellipse 80% 60% at 50% -20%,rgba(184,77,255,.2),transparent 55%)}
|
||||
@keyframes auth-box-in{from{opacity:0;transform:translateY(24px) scale(.97)}to{opacity:1;transform:none}}
|
||||
.auth-box{background:linear-gradient(160deg,rgba(14,14,28,.97),rgba(8,8,18,.99));border:1px solid rgba(0,245,255,.22);border-radius:20px;padding:clamp(28px,5vw,44px);width:100%;max-width:440px;box-shadow:0 32px 80px rgba(0,0,0,.65);position:relative;overflow:hidden;text-align:center;animation:auth-box-in .55s cubic-bezier(.22,1,.36,1) .1s both}
|
||||
.auth-box::before{content:'';position:absolute;top:0;left:10%;right:10%;height:1px;background:linear-gradient(90deg,transparent,var(--accent2),var(--accent),transparent);opacity:.7}
|
||||
.ve-icon{font-size:3rem;margin-bottom:16px;filter:drop-shadow(0 0 16px rgba(0,245,255,.4))}
|
||||
.ve-title{font-size:1.1rem;font-weight:800;color:#fff;margin-bottom:10px}
|
||||
.ve-text{font-size:.88rem;color:rgba(255,255,255,.4);line-height:1.7;margin-bottom:24px}
|
||||
.auth-btn{display:inline-block;padding:12px 28px;border-radius:12px;border:none;cursor:pointer;background:linear-gradient(135deg,var(--accent),var(--accent3));color:#fff;font-size:.9rem;font-weight:800;transition:transform .14s;box-shadow:0 0 28px rgba(255,45,125,.3);width:100%}
|
||||
.auth-btn:hover{transform:translateY(-2px)}
|
||||
.status-box{background:rgba(0,245,255,.07);border:1px solid rgba(0,245,255,.2);border-radius:10px;padding:12px 16px;font-size:.85rem;color:var(--accent2);margin-bottom:20px}
|
||||
.logout-link{display:block;margin-top:16px;font-size:.82rem;color:rgba(255,255,255,.3)}
|
||||
.logout-link a{color:rgba(255,255,255,.4)}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div class="auth-page">
|
||||
<div class="auth-box">
|
||||
<div class="ve-icon">📧</div>
|
||||
<div class="ve-title">E-postanı Doğrula</div>
|
||||
<p class="ve-text">
|
||||
<strong style="color:#fff">{{ auth()->user()->email }}</strong> adresine doğrulama bağlantısı gönderdik.<br>
|
||||
Gelen kutunu kontrol et. Spam klasörüne de bakmanı öneririz.
|
||||
</p>
|
||||
|
||||
@if(session('status'))
|
||||
<div class="status-box"><i class="bi bi-check-circle-fill"></i> {{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('verification.send') }}">
|
||||
@csrf
|
||||
<button type="submit" class="auth-btn">
|
||||
<i class="bi bi-envelope-fill"></i> Tekrar Gönder
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="logout-link">
|
||||
<form method="POST" action="{{ route('frontend.logout') }}" style="display:inline">
|
||||
@csrf
|
||||
<a href="#" onclick="this.closest('form').submit()">Çıkış yap</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,144 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Anime Blog — Rehberler, İncelemeler ve Öneriler | Animexe')
|
||||
@section('meta_description', 'Animexe anime blogunda izleme rehberleri, anime incelemeleri, tür önerileri ve AI destekli içerikler keşfedin.')
|
||||
@section('meta_keywords', 'anime blog, anime rehberi, anime inceleme, anime önerisi, animexe blog')
|
||||
@section('og_type', 'website')
|
||||
@push('styles')
|
||||
<style>
|
||||
.blog-hero{background:linear-gradient(180deg,rgba(12,12,26,.95) 0%,var(--bg) 100%);padding:calc(var(--nav-h) + 32px) 24px 36px;border-bottom:1px solid rgba(0,245,255,.12)}
|
||||
.blog-hero h1{font-family:var(--font-display);font-size:1.6rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;margin-bottom:8px}
|
||||
.blog-hero p{color:var(--text2);font-size:.95rem;max-width:520px}
|
||||
.blog-grid{display:grid;grid-template-columns:1fr 300px;gap:32px;max-width:1200px;margin:0 auto}
|
||||
.blog-card{
|
||||
background:linear-gradient(145deg,rgba(17,17,31,.95),rgba(10,10,20,.98));
|
||||
border:1px solid rgba(0,245,255,.1);border-radius:var(--r2);overflow:hidden;
|
||||
transition:transform .2s,box-shadow .22s,border-color .2s;
|
||||
display:flex;flex-direction:column;
|
||||
}
|
||||
.blog-card:hover{transform:translateY(-4px);border-color:rgba(255,45,125,.4);box-shadow:0 16px 48px rgba(0,0,0,.5),0 0 32px rgba(255,45,125,.1)}
|
||||
.blog-thumb{aspect-ratio:16/9;overflow:hidden;background:var(--surface2)}
|
||||
.blog-thumb img{width:100%;height:100%;object-fit:cover;transition:transform .3s}
|
||||
.blog-card:hover .blog-thumb img{transform:scale(1.05)}
|
||||
.blog-thumb-ph{width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:var(--text3);font-size:2.5rem}
|
||||
.blog-body{padding:18px;flex:1;display:flex;flex-direction:column}
|
||||
.blog-category{font-size:.68rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:var(--accent2);margin-bottom:8px}
|
||||
.blog-title{font-size:.95rem;font-weight:700;color:var(--text);line-height:1.45;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-bottom:8px}
|
||||
.blog-excerpt{font-size:.8rem;color:var(--text2);line-height:1.55;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;flex:1}
|
||||
.blog-footer{display:flex;align-items:center;justify-content:space-between;margin-top:14px;padding-top:12px;border-top:1px solid rgba(0,245,255,.08)}
|
||||
.blog-date{font-size:.72rem;color:var(--text3)}
|
||||
.blog-read{font-size:.72rem;color:var(--text3);display:flex;align-items:center;gap:4px}
|
||||
.blog-ai-badge{display:inline-flex;align-items:center;gap:4px;font-size:.65rem;font-weight:700;
|
||||
padding:2px 8px;border-radius:999px;background:rgba(124,58,237,.2);border:1px solid rgba(124,58,237,.3);
|
||||
color:#a78bfa;margin-bottom:8px}
|
||||
.sidebar-card{background:rgba(17,17,31,.8);border:1px solid rgba(0,245,255,.1);border-radius:var(--r2);padding:18px;margin-bottom:20px}
|
||||
.sidebar-title{font-family:var(--font-display);font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;color:var(--accent2);margin-bottom:14px;font-weight:700}
|
||||
.sidebar-item{display:flex;gap:10px;padding:8px 0;border-bottom:1px solid rgba(0,245,255,.05);align-items:flex-start}
|
||||
.sidebar-item:last-child{border-bottom:none}
|
||||
.sidebar-item-title{font-size:.8rem;font-weight:600;color:var(--text);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||||
.sidebar-item-date{font-size:.68rem;color:var(--text3);margin-top:3px}
|
||||
.sidebar-thumb{width:52px;height:52px;border-radius:6px;overflow:hidden;flex-shrink:0;background:var(--surface2)}
|
||||
.sidebar-thumb img{width:100%;height:100%;object-fit:cover}
|
||||
.posts-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px}
|
||||
@media(max-width:900px){
|
||||
.blog-grid{grid-template-columns:1fr}
|
||||
.blog-sidebar{display:none}
|
||||
}
|
||||
@media(max-width:480px){
|
||||
.posts-grid{grid-template-columns:1fr}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="blog-hero">
|
||||
<div style="max-width:1200px;margin:0 auto">
|
||||
<h1><i class="bi bi-journal-richtext" style="color:var(--accent2);font-size:1.2rem;margin-right:10px"></i>Anime Blog</h1>
|
||||
<p>AI destekli anime rehberleri, izleme önerileri ve derinlemesine incelemeler.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="blog-grid">
|
||||
<div>
|
||||
@if($posts->count())
|
||||
<div class="posts-grid">
|
||||
@foreach($posts as $post)
|
||||
<a href="{{ route('blog.show', $post->slug) }}" class="blog-card" style="text-decoration:none">
|
||||
<div class="blog-thumb">
|
||||
@if($post->coverUrl)
|
||||
<img src="{{ $post->coverUrl }}" alt="{{ $post->title }}" loading="lazy">
|
||||
@else
|
||||
<div class="blog-thumb-ph"><i class="bi bi-journal-richtext"></i></div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="blog-body">
|
||||
@if($post->ai_generated)
|
||||
<div class="blog-ai-badge"><i class="bi bi-stars" style="font-size:.6rem"></i> AI İçerik</div>
|
||||
@endif
|
||||
@if($post->anime)
|
||||
<div class="blog-category">{{ $post->anime->title }}</div>
|
||||
@endif
|
||||
<div class="blog-title">{{ $post->title }}</div>
|
||||
<div class="blog-excerpt">{{ $post->excerpt }}</div>
|
||||
<div class="blog-footer">
|
||||
<span class="blog-date">{{ $post->published_at?->format('d.m.Y') }}</span>
|
||||
<span class="blog-read"><i class="bi bi-clock" style="font-size:.65rem"></i> {{ $post->reading_time }} dk</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if($posts->hasPages())
|
||||
<div style="margin-top:32px">{{ $posts->links('frontend.pagination') }}</div>
|
||||
@endif
|
||||
@else
|
||||
<div style="text-align:center;padding:80px 20px;color:var(--text3)">
|
||||
<i class="bi bi-journal-text" style="font-size:3rem;opacity:.25;display:block;margin-bottom:14px"></i>
|
||||
<p>Henüz blog yazısı yok. Yakında yapay zeka destekli içerikler gelecek!</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<aside class="blog-sidebar">
|
||||
{{-- Son Yazılar --}}
|
||||
@if($recent->count())
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-title">Son Yazılar</div>
|
||||
@foreach($recent as $p)
|
||||
<a href="{{ route('blog.show', $p->slug) }}" class="sidebar-item" style="text-decoration:none">
|
||||
<div class="sidebar-thumb">
|
||||
@if($p->coverUrl)<img src="{{ $p->coverUrl }}" alt="" loading="lazy">
|
||||
@else<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:var(--text3)"><i class="bi bi-journal-text"></i></div>@endif
|
||||
</div>
|
||||
<div>
|
||||
<div class="sidebar-item-title">{{ $p->title }}</div>
|
||||
<div class="sidebar-item-date">{{ $p->published_at?->format('d.m.Y') }}</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- En Çok Okunanlar --}}
|
||||
@if($popular->count())
|
||||
<div class="sidebar-card">
|
||||
<div class="sidebar-title"><i class="bi bi-fire" style="color:var(--accent)"></i> En Çok Okunanlar</div>
|
||||
@foreach($popular as $p)
|
||||
<a href="{{ route('blog.show', $p->slug) }}" class="sidebar-item" style="text-decoration:none">
|
||||
<div class="sidebar-thumb">
|
||||
@if($p->coverUrl)<img src="{{ $p->coverUrl }}" alt="" loading="lazy">
|
||||
@else<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:var(--text3)"><i class="bi bi-journal-text"></i></div>@endif
|
||||
</div>
|
||||
<div>
|
||||
<div class="sidebar-item-title">{{ $p->title }}</div>
|
||||
<div class="sidebar-item-date">{{ number_format($p->views) }} görüntülenme</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,234 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@php
|
||||
$__canon = rtrim(\App\Models\Setting::get('seo_canonical_domain', config('app.url')), '/') . route('blog.show', $post->slug, false);
|
||||
$__ogImg = $post->coverUrl;
|
||||
$__pubDate = $post->published_at?->toIso8601String();
|
||||
$__modDate = $post->updated_at?->toIso8601String();
|
||||
$__faqItems = $post->faq ?? [];
|
||||
@endphp
|
||||
@section('title', ($post->meta_title ?: $post->title) . ' — Animexe Blog')
|
||||
@section('meta_description', $post->meta_description ?: $post->excerpt)
|
||||
@section('meta_keywords', $post->meta_keywords)
|
||||
@section('og_type', 'article')
|
||||
@section('og_title', $post->title)
|
||||
@section('og_description', $post->excerpt)
|
||||
@if($__ogImg)@section('og_image', $__ogImg)@endif
|
||||
@section('canonical', $__canon)
|
||||
@section('robots', 'index, follow')
|
||||
|
||||
@section('schema')
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context":"https://schema.org",
|
||||
"@@type":"BlogPosting",
|
||||
"headline":"{{ addslashes($post->title) }}",
|
||||
"description":"{{ addslashes($post->excerpt ?? '') }}",
|
||||
"url":"{{ $__canon }}",
|
||||
"datePublished":"{{ $__pubDate }}",
|
||||
"dateModified":"{{ $__modDate }}",
|
||||
"author":{"@@type":"Organization","name":"Animexe","url":"{{ rtrim(\App\Models\Setting::get('seo_canonical_domain',config('app.url')),'/') }}"},
|
||||
"publisher":{"@@type":"Organization","name":"Animexe","logo":{"@@type":"ImageObject","url":"{{ url('/logo.jpg') }}"}},
|
||||
"image":"{{ $__ogImg ?: url('/logo.jpg') }}",
|
||||
"inLanguage":"tr-TR",
|
||||
"keywords":"{{ addslashes($post->meta_keywords ?? '') }}"
|
||||
}
|
||||
</script>
|
||||
@if(count($__faqItems) > 0)
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context":"https://schema.org",
|
||||
"@@type":"FAQPage",
|
||||
"mainEntity":[
|
||||
@foreach($__faqItems as $i => $faq)
|
||||
{"@@type":"Question","name":"{{ addslashes($faq['q'] ?? '') }}","acceptedAnswer":{"@@type":"Answer","text":"{{ addslashes($faq['a'] ?? '') }}"}}{{ $i < count($__faqItems)-1 ? ',' : '' }}
|
||||
@endforeach
|
||||
]
|
||||
}
|
||||
</script>
|
||||
@endif
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context":"https://schema.org",
|
||||
"@@type":"BreadcrumbList",
|
||||
"itemListElement":[
|
||||
{"@@type":"ListItem","position":1,"name":"Ana Sayfa","item":"{{ rtrim(\App\Models\Setting::get('seo_canonical_domain',config('app.url')),'/') }}/"},
|
||||
{"@@type":"ListItem","position":2,"name":"Blog","item":"{{ rtrim(\App\Models\Setting::get('seo_canonical_domain',config('app.url')),'/') }}/blog"},
|
||||
{"@@type":"ListItem","position":3,"name":"{{ addslashes($post->title) }}","item":"{{ $__canon }}"}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.blog-layout{display:grid;grid-template-columns:1fr 300px;gap:40px;max-width:1160px;margin:0 auto}
|
||||
.blog-article{}
|
||||
.blog-hero-img{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:var(--r2);margin-bottom:28px;
|
||||
box-shadow:0 8px 32px rgba(0,0,0,.5)}
|
||||
.blog-meta-bar{display:flex;align-items:center;gap:16px;flex-wrap:wrap;margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid rgba(0,245,255,.1)}
|
||||
.blog-meta-item{display:flex;align-items:center;gap:6px;font-size:.78rem;color:var(--text3)}
|
||||
.blog-meta-item i{font-size:.75rem;color:var(--accent2)}
|
||||
.blog-ai-badge-lg{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:999px;
|
||||
background:linear-gradient(135deg,rgba(124,58,237,.2),rgba(232,67,147,.1));
|
||||
border:1px solid rgba(124,58,237,.3);color:#a78bfa;font-size:.78rem;font-weight:700;margin-bottom:16px}
|
||||
.blog-h1{font-family:var(--font-display);font-size:1.55rem;font-weight:800;letter-spacing:.04em;
|
||||
line-height:1.35;color:var(--text);margin-bottom:20px}
|
||||
.blog-content{color:var(--text2);line-height:1.85;font-size:.95rem}
|
||||
.blog-content h2{font-family:var(--font-display);font-size:1.1rem;font-weight:700;color:var(--text);
|
||||
margin:32px 0 14px;letter-spacing:.04em;text-transform:uppercase;
|
||||
border-left:3px solid var(--accent2);padding-left:12px}
|
||||
.blog-content h3{font-size:.95rem;font-weight:700;color:var(--text);margin:22px 0 10px}
|
||||
.blog-content p{margin-bottom:16px}
|
||||
.blog-content ul,.blog-content ol{padding-left:24px;margin-bottom:16px}
|
||||
.blog-content li{margin-bottom:8px}
|
||||
.blog-content strong{color:var(--text);font-weight:700}
|
||||
.blog-content a{color:var(--accent2);text-decoration:underline;text-decoration-color:rgba(0,245,255,.4)}
|
||||
.blog-content a:hover{color:var(--accent);text-decoration-color:rgba(255,45,125,.6)}
|
||||
.faq-section{margin-top:40px;padding-top:28px;border-top:1px solid rgba(0,245,255,.1)}
|
||||
.faq-section h2{font-family:var(--font-display);font-size:1rem;font-weight:800;text-transform:uppercase;
|
||||
letter-spacing:.06em;color:var(--accent2);margin-bottom:20px}
|
||||
.faq-item{background:rgba(17,17,31,.8);border:1px solid rgba(0,245,255,.1);border-radius:var(--r2);
|
||||
padding:18px;margin-bottom:12px}
|
||||
.faq-item h3{font-size:.9rem;font-weight:700;color:var(--text);margin-bottom:10px;line-height:1.4}
|
||||
.faq-item p{font-size:.85rem;color:var(--text2);line-height:1.65;margin:0}
|
||||
.cta-box{margin-top:36px;padding:28px;background:linear-gradient(135deg,rgba(255,45,125,.12),rgba(0,245,255,.08));
|
||||
border:1px solid rgba(255,45,125,.3);border-radius:var(--r2);text-align:center}
|
||||
.cta-box h3{font-family:var(--font-display);font-size:1rem;font-weight:800;text-transform:uppercase;
|
||||
letter-spacing:.06em;color:var(--text);margin-bottom:12px}
|
||||
.cta-btn{display:inline-flex;align-items:center;gap:8px;padding:12px 28px;border-radius:999px;
|
||||
background:linear-gradient(135deg,var(--accent),var(--accent3));color:#fff;
|
||||
font-weight:700;font-size:.9rem;text-decoration:none;transition:transform .16s,box-shadow .18s;
|
||||
box-shadow:0 0 24px rgba(255,45,125,.4)}
|
||||
.cta-btn:hover{transform:translateY(-2px);box-shadow:0 0 36px rgba(255,45,125,.55);color:#fff}
|
||||
.related-card{
|
||||
background:rgba(17,17,31,.8);border:1px solid rgba(0,245,255,.1);border-radius:var(--r);
|
||||
overflow:hidden;transition:border-color .2s;display:flex;gap:12px;align-items:flex-start;padding:12px;
|
||||
}
|
||||
.related-card:hover{border-color:rgba(255,45,125,.3)}
|
||||
.related-thumb{width:64px;height:64px;border-radius:6px;overflow:hidden;flex-shrink:0;background:var(--surface2)}
|
||||
.related-thumb img{width:100%;height:100%;object-fit:cover}
|
||||
.linked-animes{margin-top:28px;padding:20px;background:rgba(0,245,255,.04);border:1px solid rgba(0,245,255,.12);border-radius:var(--r2)}
|
||||
.linked-animes h3{font-size:.82rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--accent2);margin-bottom:14px}
|
||||
.la-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:10px}
|
||||
@media(max-width:900px){
|
||||
.blog-layout{grid-template-columns:1fr}
|
||||
.blog-sidebar-wrap{display:none}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="section" style="padding-top:calc(var(--nav-h) + 28px)">
|
||||
<div class="blog-layout">
|
||||
<article class="blog-article" itemscope itemtype="https://schema.org/BlogPosting">
|
||||
|
||||
@if($post->coverUrl)
|
||||
<img src="{{ $post->coverUrl }}" alt="{{ $post->title }}"
|
||||
class="blog-hero-img" itemprop="image" fetchpriority="high">
|
||||
@endif
|
||||
|
||||
@if($post->ai_generated)
|
||||
<div class="blog-ai-badge-lg"><i class="bi bi-stars"></i> AI Destekli İçerik</div>
|
||||
@endif
|
||||
|
||||
<h1 class="blog-h1" itemprop="headline">{{ $post->title }}</h1>
|
||||
|
||||
<div class="blog-meta-bar">
|
||||
@if($post->published_at)
|
||||
<span class="blog-meta-item">
|
||||
<i class="bi bi-calendar3"></i>
|
||||
<time itemprop="datePublished" datetime="{{ $post->published_at->toIso8601String() }}">
|
||||
{{ $post->published_at->format('d F Y') }}
|
||||
</time>
|
||||
</span>
|
||||
@endif
|
||||
<span class="blog-meta-item"><i class="bi bi-clock"></i> {{ $post->reading_time }} dk okuma</span>
|
||||
@if($post->anime)
|
||||
<span class="blog-meta-item">
|
||||
<i class="bi bi-play-circle"></i>
|
||||
<a href="{{ $post->anime?->detailUrl ?? '#' }}" style="color:var(--accent2)">{{ $post->anime->title }}</a>
|
||||
</span>
|
||||
@endif
|
||||
@if($post->focus_keyword)
|
||||
<span class="blog-meta-item"><i class="bi bi-tag"></i> {{ $post->focus_keyword }}</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="blog-content" itemprop="articleBody">
|
||||
{!! $post->content !!}
|
||||
</div>
|
||||
|
||||
{{-- FAQ Bölümü --}}
|
||||
@if(count($__faqItems) > 0)
|
||||
<div class="faq-section">
|
||||
<h2>Sıkça Sorulan Sorular</h2>
|
||||
@foreach($__faqItems as $faq)
|
||||
<div class="faq-item">
|
||||
<h3>{{ $faq['q'] ?? '' }}</h3>
|
||||
<p>{{ $faq['a'] ?? '' }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- İlgili Anime Linkleri --}}
|
||||
@if($linkedAnimes->count())
|
||||
<div class="linked-animes">
|
||||
<h3><i class="bi bi-collection-play me-1"></i>Bu Yazıda Bahsedilen Animeler</h3>
|
||||
<div class="la-grid">
|
||||
@foreach($linkedAnimes as $la)
|
||||
<a href="{{ $la->detailUrl }}" class="a-card">
|
||||
<div class="a-poster">
|
||||
@if($la->coverUrl)<img src="{{ $la->coverUrl }}" alt="{{ $la->title }}" loading="lazy">
|
||||
@else<div class="a-no-img"><i class="bi bi-image"></i></div>@endif
|
||||
@if($la->rating)<span class="a-rating"><i class="bi bi-star-fill" style="font-size:.6rem"></i>{{ number_format($la->rating,1) }}</span>@endif
|
||||
</div>
|
||||
<div class="a-info">
|
||||
<div class="a-title">{{ $la->title }}</div>
|
||||
<div class="a-meta">{{ $la->release_year }}</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- CTA --}}
|
||||
@if($post->anime)
|
||||
<div class="cta-box">
|
||||
<h3>{{ $post->anime->title }} İzle</h3>
|
||||
<p style="color:var(--text2);font-size:.88rem;margin-bottom:18px">
|
||||
Animexe'de Türkçe altyazılı veya dublajlı ücretsiz izle.
|
||||
</p>
|
||||
<a href="{{ $post->anime?->detailUrl ?? '#' }}" class="cta-btn">
|
||||
<i class="bi bi-play-fill"></i> Hemen İzle
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</article>
|
||||
|
||||
{{-- Sidebar --}}
|
||||
<aside class="blog-sidebar-wrap">
|
||||
@if($related->count())
|
||||
<div style="background:rgba(17,17,31,.8);border:1px solid rgba(0,245,255,.1);border-radius:var(--r2);padding:18px">
|
||||
<div style="font-family:var(--font-display);font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;color:var(--accent2);margin-bottom:14px;font-weight:700">İlgili Yazılar</div>
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
@foreach($related as $rp)
|
||||
<a href="{{ route('blog.show', $rp->slug) }}" class="related-card" style="text-decoration:none">
|
||||
<div class="related-thumb">
|
||||
@if($rp->coverUrl)<img src="{{ $rp->coverUrl }}" alt="" loading="lazy">
|
||||
@else<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:var(--text3)"><i class="bi bi-journal-text"></i></div>@endif
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:.8rem;font-weight:600;color:var(--text);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden">{{ $rp->title }}</div>
|
||||
<div style="font-size:.68rem;color:var(--text3);margin-top:4px">{{ $rp->published_at?->format('d.m.Y') }}</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,114 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Zaman Kapsüllerim | Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
.cap-hero{padding-top:calc(var(--nav-h) + 40px);padding-bottom:32px;text-align:center;background:radial-gradient(ellipse at 50% 0%,rgba(100,80,255,.12) 0%,transparent 70%)}
|
||||
.cap-hero h1{font-size:2rem;font-weight:900;margin-bottom:8px}
|
||||
.cap-hero h1 span{color:#a78bfa}
|
||||
.cap-hero p{color:rgba(255,255,255,.4);font-size:.9rem}
|
||||
.cap-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;padding:0 0 60px}
|
||||
.cap-card{border-radius:16px;padding:20px;position:relative;overflow:hidden;border:1px solid rgba(255,255,255,.08)}
|
||||
.cap-card.locked{background:linear-gradient(135deg,rgba(20,18,40,.9),rgba(10,8,22,.9))}
|
||||
.cap-card.unlocked{background:linear-gradient(135deg,rgba(100,80,255,.12),rgba(60,40,180,.08));border-color:rgba(167,139,250,.25)}
|
||||
.cap-card.opened{background:linear-gradient(135deg,rgba(0,200,100,.06),rgba(0,160,80,.04));border-color:rgba(0,200,100,.2)}
|
||||
.cap-lock-icon{font-size:2rem;margin-bottom:10px;display:block}
|
||||
.cap-anime{font-size:.72rem;font-weight:700;color:rgba(167,139,250,.8);text-transform:uppercase;letter-spacing:.07em;margin-bottom:6px}
|
||||
.cap-date{font-size:.78rem;color:rgba(255,255,255,.35);margin-bottom:14px}
|
||||
.cap-locked-msg{font-size:.82rem;color:rgba(255,255,255,.25);text-align:center;padding:16px 0}
|
||||
.cap-message{font-size:.88rem;line-height:1.6;color:rgba(255,255,255,.8);background:rgba(255,255,255,.04);border-radius:10px;padding:14px;border-left:3px solid #a78bfa;font-style:italic}
|
||||
.cap-open-btn{width:100%;margin-top:14px;padding:10px;border-radius:10px;border:none;background:rgba(167,139,250,.2);color:#a78bfa;font-weight:700;font-size:.88rem;cursor:pointer;font-family:inherit;transition:background .15s}
|
||||
.cap-open-btn:hover{background:rgba(167,139,250,.35)}
|
||||
.cap-tag{display:inline-block;padding:2px 10px;border-radius:20px;font-size:.65rem;font-weight:700;margin-bottom:10px}
|
||||
.cap-tag.locked{background:rgba(255,255,255,.06);color:rgba(255,255,255,.3)}
|
||||
.cap-tag.unlocked{background:rgba(167,139,250,.12);color:#a78bfa}
|
||||
.cap-tag.opened{background:rgba(0,200,100,.1);color:#00c864}
|
||||
|
||||
.cap-countdown{font-size:.78rem;color:rgba(255,255,255,.3);margin-top:8px;text-align:center}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div style="max-width:1100px;margin:0 auto;padding:0 20px">
|
||||
|
||||
<div class="cap-hero">
|
||||
<h1>⏳ Zaman <span>Kapsüllerim</span></h1>
|
||||
<p>Geçmiş benliğinden gelen mesajlar.</p>
|
||||
</div>
|
||||
|
||||
@if($capsules->isEmpty())
|
||||
<div style="text-align:center;padding:80px 20px;color:rgba(255,255,255,.25)">
|
||||
<i class="bi bi-hourglass" style="font-size:3rem;display:block;margin-bottom:16px"></i>
|
||||
Henüz kapsül oluşturmadın.<br>
|
||||
<small style="font-size:.8rem">Bir anime sayfasından ilk kez izlerken kapsül bırakabilirsin.</small>
|
||||
</div>
|
||||
@else
|
||||
<div class="cap-grid">
|
||||
@foreach($capsules as $cap)
|
||||
<div class="cap-card {{ $cap['opened'] ? 'opened' : ($cap['unlocked'] ? 'unlocked' : 'locked') }}" id="cap-{{ $cap['id'] }}">
|
||||
<span class="cap-lock-icon">{{ $cap['opened'] ? '📭' : ($cap['unlocked'] ? '🔓' : '🔒') }}</span>
|
||||
<span class="cap-tag {{ $cap['opened'] ? 'opened' : ($cap['unlocked'] ? 'unlocked' : 'locked') }}">
|
||||
{{ $cap['opened'] ? 'Açıldı' : ($cap['unlocked'] ? 'Açılabilir' : 'Kilitli') }}
|
||||
</span>
|
||||
<div class="cap-anime">
|
||||
<a href="{{ $cap['anime_url'] }}" style="color:inherit;text-decoration:none">{{ $cap['anime'] }}</a>
|
||||
</div>
|
||||
<div class="cap-date">
|
||||
<i class="bi bi-calendar3"></i>
|
||||
Oluşturuldu: {{ $cap['created_at'] }} · Açılış: {{ $cap['unlock_at'] }}
|
||||
</div>
|
||||
|
||||
@if($cap['opened'] || $cap['unlocked'])
|
||||
@if($cap['message'])
|
||||
<div class="cap-message">{{ $cap['message'] }}</div>
|
||||
@else
|
||||
<button class="cap-open-btn" onclick="openCapsule({{ $cap['id'] }}, this)">
|
||||
<i class="bi bi-envelope-open"></i> Kapsülü Aç
|
||||
</button>
|
||||
@endif
|
||||
@else
|
||||
<div class="cap-locked-msg">
|
||||
<i class="bi bi-lock"></i><br>
|
||||
Bu kapsül {{ $cap['unlock_at'] }} tarihinde açılacak
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
@push('scripts')
|
||||
<script>
|
||||
const CSRF = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
|
||||
|
||||
async function openCapsule(id, btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Açılıyor...';
|
||||
const r = await fetch(`/kapsul/${id}/ac`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (d.ok) {
|
||||
const card = document.getElementById('cap-' + id);
|
||||
btn.remove();
|
||||
const msg = document.createElement('div');
|
||||
msg.className = 'cap-message';
|
||||
msg.textContent = d.message;
|
||||
card.appendChild(msg);
|
||||
card.classList.remove('unlocked');
|
||||
card.classList.add('opened');
|
||||
card.querySelector('.cap-lock-icon').textContent = '📭';
|
||||
const tag = card.querySelector('.cap-tag');
|
||||
tag.textContent = 'Açıldı';
|
||||
tag.className = 'cap-tag opened';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Kapsülü Aç';
|
||||
if (window.showToast) showToast(d.error || 'Hata oluştu.', 'error');
|
||||
else alert(d.error || 'Hata oluştu.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,35 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Ödeme Başarısız | Animexe')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.ck-result{max-width:480px;margin:80px auto;text-align:center;padding:0 16px}
|
||||
.ck-result-icon{font-size:4rem;margin-bottom:20px;color:#ff4d6d}
|
||||
.ck-result h1{font-size:1.5rem;font-weight:800;margin-bottom:10px}
|
||||
.ck-result p{color:rgba(255,255,255,.55);margin-bottom:28px;line-height:1.6}
|
||||
.ck-result-btns{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}
|
||||
.ck-result-btn{display:inline-flex;align-items:center;gap:8px;padding:12px 24px;border-radius:10px;font-weight:700;text-decoration:none;transition:opacity .15s}
|
||||
.ck-result-btn.primary{background:linear-gradient(135deg,#00f5ff,#b84dff);color:#fff}
|
||||
.ck-result-btn.secondary{background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:rgba(255,255,255,.7)}
|
||||
.ck-result-btn:hover{opacity:.85;color:inherit}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="ck-result">
|
||||
<div class="ck-result-icon"><i class="bi bi-x-circle-fill"></i></div>
|
||||
<h1>Ödeme Başarısız</h1>
|
||||
<p>
|
||||
Ödeme işlemi tamamlanamadı. Kart bilgilerini kontrol edip tekrar deneyebilirsin
|
||||
veya farklı bir kart kullanabilirsin.
|
||||
</p>
|
||||
<div class="ck-result-btns">
|
||||
<a href="{{ route('premium.plans') }}" class="ck-result-btn primary">
|
||||
<i class="bi bi-arrow-repeat"></i> Tekrar Dene
|
||||
</a>
|
||||
<a href="{{ route('anime.index') }}" class="ck-result-btn secondary">
|
||||
<i class="bi bi-house"></i> Ana Sayfa
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,31 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Ödeme | Animexe')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.ck-pay-wrap{max-width:540px;margin:40px auto;padding:0 16px}
|
||||
.ck-pay-head{text-align:center;margin-bottom:24px}
|
||||
.ck-pay-head h1{font-size:1.2rem;font-weight:700;margin-bottom:6px}
|
||||
.ck-pay-head p{color:rgba(255,255,255,.45);font-size:.85rem}
|
||||
.ck-iyzico-box{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:24px;min-height:400px}
|
||||
.ck-secure{display:flex;align-items:center;gap:8px;justify-content:center;margin-top:16px;font-size:.75rem;color:rgba(255,255,255,.3)}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="ck-pay-wrap">
|
||||
<div class="ck-pay-head">
|
||||
<h1><i class="bi bi-credit-card-fill me-2" style="color:#00f5ff"></i>Kart Bilgileri</h1>
|
||||
<p>{{ $plan->name }} — ₺{{ number_format($plan->price, 0) }} / {{ $plan->duration_days }} gün</p>
|
||||
</div>
|
||||
|
||||
<div class="ck-iyzico-box">
|
||||
{!! $formContent !!}
|
||||
</div>
|
||||
|
||||
<div class="ck-secure">
|
||||
<i class="bi bi-shield-check" style="color:#00f5ff"></i>
|
||||
Kart bilgileriniz iyzico'nun güvenli sunucularında işlenir
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,104 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', $plan->name . ' — Ödeme | Animexe')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.ck-wrap{max-width:860px;margin:40px auto;padding:0 16px;display:grid;grid-template-columns:1fr 1fr;gap:28px}
|
||||
@media(max-width:640px){.ck-wrap{grid-template-columns:1fr}}
|
||||
.ck-plan{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:28px}
|
||||
.ck-plan-name{font-size:1.4rem;font-weight:800;color:#00f5ff;margin-bottom:6px}
|
||||
.ck-plan-price{font-size:2.4rem;font-weight:900;margin:12px 0 4px}
|
||||
.ck-plan-dur{color:rgba(255,255,255,.45);font-size:.85rem;margin-bottom:20px}
|
||||
.ck-features{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px}
|
||||
.ck-features li{color:rgba(255,255,255,.7);font-size:.88rem;display:flex;align-items:center;gap:8px}
|
||||
.ck-features li i{color:#00f5ff;font-size:.9rem}
|
||||
.ck-form{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:28px}
|
||||
.ck-form h2{font-size:1rem;font-weight:700;margin-bottom:20px;color:rgba(255,255,255,.85)}
|
||||
.ck-field{margin-bottom:16px}
|
||||
.ck-field label{display:block;font-size:.78rem;color:rgba(255,255,255,.5);margin-bottom:5px;font-weight:600;text-transform:uppercase;letter-spacing:.04em}
|
||||
.ck-field input{width:100%;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:10px 14px;color:#fff;font-size:.9rem;outline:none;transition:border-color .15s;box-sizing:border-box}
|
||||
.ck-field input:focus{border-color:rgba(0,245,255,.4)}
|
||||
.ck-field input::placeholder{color:rgba(255,255,255,.25)}
|
||||
.ck-field small{display:block;font-size:.73rem;color:rgba(255,255,255,.3);margin-top:4px}
|
||||
.ck-submit{width:100%;padding:13px;background:linear-gradient(135deg,#00f5ff,#b84dff);border:none;border-radius:10px;color:#fff;font-weight:800;font-size:1rem;cursor:pointer;margin-top:6px;transition:opacity .15s}
|
||||
.ck-submit:hover{opacity:.9}
|
||||
.ck-secure{display:flex;align-items:center;gap:8px;justify-content:center;margin-top:14px;font-size:.75rem;color:rgba(255,255,255,.3)}
|
||||
.ck-back{display:inline-flex;align-items:center;gap:6px;color:rgba(255,255,255,.4);font-size:.82rem;margin-bottom:24px;text-decoration:none}
|
||||
.ck-back:hover{color:rgba(255,255,255,.7)}
|
||||
.ck-err{background:rgba(255,60,60,.12);border:1px solid rgba(255,60,60,.3);border-radius:8px;padding:10px 14px;color:#ff6b6b;font-size:.85rem;margin-bottom:16px}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="ck-wrap">
|
||||
<div>
|
||||
<a href="{{ route('premium.plans') }}" class="ck-back"><i class="bi bi-arrow-left"></i> Planlara dön</a>
|
||||
<div class="ck-plan">
|
||||
<div class="ck-plan-name">{{ $plan->name }}</div>
|
||||
<div class="ck-plan-price"><span style="font-size:1.2rem;font-weight:400">₺</span>{{ number_format($plan->price, 0) }}</div>
|
||||
<div class="ck-plan-dur">{{ $plan->duration_days }} günlük premium üyelik</div>
|
||||
@if(($plan->trial_days ?? 0) > 0)
|
||||
<div style="display:inline-flex;align-items:center;gap:6px;padding:5px 12px;border-radius:20px;background:rgba(0,245,255,.1);border:1px solid rgba(0,245,255,.25);color:#00f5ff;font-size:.75rem;font-weight:700;margin-bottom:16px">
|
||||
<i class="bi bi-gift-fill"></i> İlk {{ $plan->trial_days }} gün ücretsiz
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($plan->features))
|
||||
<ul class="ck-features">
|
||||
@foreach($plan->features as $feat)
|
||||
<li><i class="bi bi-check2-circle"></i> {{ $feat }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ck-form">
|
||||
<h2><i class="bi bi-person-fill me-2"></i>Fatura Bilgileri</h2>
|
||||
|
||||
@if($errors->has('general'))
|
||||
<div class="ck-err"><i class="bi bi-exclamation-circle me-1"></i>{{ $errors->first('general') }}</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('checkout.initialize', $plan) }}">
|
||||
@csrf
|
||||
<div class="ck-field">
|
||||
<label>Ad Soyad *</label>
|
||||
<input type="text" name="full_name" value="{{ old('full_name', auth()->user()->name) }}" placeholder="Adınız Soyadınız" required>
|
||||
@error('full_name')<span style="color:#ff6b6b;font-size:.75rem">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
<div class="ck-field">
|
||||
<label>E-posta</label>
|
||||
<input type="text" value="{{ auth()->user()->email }}" disabled style="opacity:.5;cursor:not-allowed">
|
||||
</div>
|
||||
<div class="ck-field">
|
||||
<label>Telefon *</label>
|
||||
<input type="tel" name="phone" value="{{ old('phone') }}" placeholder="05XX XXX XX XX" required>
|
||||
@error('phone')<span style="color:#ff6b6b;font-size:.75rem">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
<div class="ck-field">
|
||||
<label>İl *</label>
|
||||
<input type="text" name="city" value="{{ old('city') }}" placeholder="İstanbul" required>
|
||||
@error('city')<span style="color:#ff6b6b;font-size:.75rem">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
<div class="ck-field">
|
||||
<label>Adres *</label>
|
||||
<input type="text" name="address" value="{{ old('address') }}" placeholder="Mahalle, sokak, bina no..." required>
|
||||
@error('address')<span style="color:#ff6b6b;font-size:.75rem">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
<div class="ck-field">
|
||||
<label>TC Kimlik No <span style="color:rgba(255,255,255,.3)">(isteğe bağlı)</span></label>
|
||||
<input type="text" name="identity_no" value="{{ old('identity_no') }}" placeholder="11 haneli TC no" maxlength="11" inputmode="numeric">
|
||||
<small>iyzico güvenlik doğrulaması için istenir</small>
|
||||
@error('identity_no')<span style="color:#ff6b6b;font-size:.75rem">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
<button type="submit" class="ck-submit">
|
||||
<i class="bi bi-credit-card-fill me-2"></i>Ödemeye Geç — ₺{{ number_format($plan->price, 0) }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="ck-secure">
|
||||
<i class="bi bi-shield-check" style="color:#00f5ff"></i>
|
||||
iyzico altyapısıyla güvenli ödeme
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,29 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Ödeme Başarılı | Animexe')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.ck-result{max-width:480px;margin:80px auto;text-align:center;padding:0 16px}
|
||||
.ck-result-icon{font-size:4rem;margin-bottom:20px;color:#00d4a4}
|
||||
.ck-result h1{font-size:1.5rem;font-weight:800;margin-bottom:10px}
|
||||
.ck-result p{color:rgba(255,255,255,.55);margin-bottom:28px;line-height:1.6}
|
||||
.ck-result-btn{display:inline-flex;align-items:center;gap:8px;padding:12px 28px;border-radius:10px;background:linear-gradient(135deg,#00f5ff,#b84dff);color:#fff;font-weight:700;text-decoration:none;transition:opacity .15s}
|
||||
.ck-result-btn:hover{opacity:.9;color:#fff}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="ck-result">
|
||||
<div class="ck-result-icon"><i class="bi bi-check-circle-fill"></i></div>
|
||||
<h1>Premium Aktif! 🎉</h1>
|
||||
<p>
|
||||
@if(session('checkout_plan_name'))
|
||||
<strong>{{ session('checkout_plan_name') }}</strong> planın aktif edildi.<br>
|
||||
@endif
|
||||
Artık tüm premium özelliklerden yararlanabilirsin.
|
||||
</p>
|
||||
<a href="{{ route('profile.settings') }}" class="ck-result-btn">
|
||||
<i class="bi bi-stars"></i> Kozmetiklerini Ayarla
|
||||
</a>
|
||||
</div>
|
||||
@endsection
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@php
|
||||
$__genreCanon = rtrim(\App\Models\Setting::get('seo_canonical_domain', config('app.url')), '/') . route('genre', $genre->slug, false);
|
||||
$__genreDesc = $genre->name . ' türündeki en iyi anime dizileri ve filmlerini Animexe\'de Türkçe altyazılı veya dublajlı ücretsiz izleyin. ' . $genre->name . ' anime listesi.';
|
||||
@endphp
|
||||
@section('title', $genre->name . ' Anime İzle - Türkçe Altyazılı | Animexe')
|
||||
@section('meta_description', $__genreDesc)
|
||||
@section('meta_keywords', $genre->name . ' anime izle, ' . $genre->name . ' türkçe anime izle, ' . $genre->name . ' anime, türkçe ' . $genre->name . ' anime, animexe ' . strtolower($genre->name))
|
||||
@section('og_title', $genre->name . ' Türündeki Animeler — Animexe')
|
||||
@section('og_description', $__genreDesc)
|
||||
@section('canonical', $__genreCanon)
|
||||
@section('schema')
|
||||
@if((\App\Models\Setting::get('seo_enable_schema','1')) === '1')
|
||||
@php $__gDomain = rtrim(\App\Models\Setting::get('seo_canonical_domain',config('app.url')),'/'); @endphp
|
||||
|
||||
{{-- CollectionPage + ItemList --}}
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context":"https://schema.org",
|
||||
"@@type":"CollectionPage",
|
||||
"name":"{{ addslashes($genre->name) }} Anime İzle - Türkçe Altyazılı",
|
||||
"description":"{{ addslashes($__genreDesc) }}",
|
||||
"url":"{{ $__genreCanon }}",
|
||||
"inLanguage":"tr-TR",
|
||||
"mainEntity":{
|
||||
"@@type":"ItemList",
|
||||
"name":"{{ addslashes($genre->name) }} Anime Listesi",
|
||||
"numberOfItems":{{ $animes->total() }},
|
||||
"itemListElement":[
|
||||
@foreach($animes->take(10) as $i => $ga)
|
||||
{"@@type":"ListItem","position":{{ $i+1 }},"name":"{{ addslashes($ga->title) }} İzle","url":"{{ $__gDomain }}/anime/{{ $ga->slug }}"}{{ $i < min(9,$animes->count()-1) ? ',' : '' }}
|
||||
@endforeach
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{{-- BreadcrumbList --}}
|
||||
@if((\App\Models\Setting::get('seo_enable_breadcrumb','1')) === '1')
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@@context":"https://schema.org",
|
||||
"@@type":"BreadcrumbList",
|
||||
"itemListElement":[
|
||||
{"@@type":"ListItem","position":1,"name":"Ana Sayfa","item":"{{ $__gDomain }}/"},
|
||||
{"@@type":"ListItem","position":2,"name":"Türler","item":"{{ $__gDomain }}/search"},
|
||||
{"@@type":"ListItem","position":3,"name":"{{ addslashes($genre->name) }} Anime İzle","item":"{{ $__genreCanon }}"}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
@endif
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="section" style="padding-top:36px">
|
||||
<div class="section-hdr" style="margin-bottom:18px">
|
||||
<span class="section-title">
|
||||
<i class="bi bi-tag" style="font-size:.85rem;color:var(--accent2)"></i>
|
||||
{{ $genre->name }}
|
||||
</span>
|
||||
<span style="color:var(--text3);font-size:.82rem">{{ $animes->total() }} anime</span>
|
||||
</div>
|
||||
|
||||
@if($animes->count())
|
||||
<div class="a-grid">
|
||||
@foreach($animes as $anime)
|
||||
<a href="{{ $anime->detailUrl }}" class="a-card">
|
||||
<div class="a-poster">
|
||||
@if($anime->coverUrl)
|
||||
<img src="{{ $anime->coverUrl }}" alt="{{ $anime->title }}" loading="lazy">
|
||||
@else
|
||||
<div class="a-no-img"><i class="bi bi-image"></i></div>
|
||||
@endif
|
||||
@if($anime->type==='movie')<span class="a-badge">Film</span>@endif
|
||||
@if($anime->rating)<span class="a-rating"><i class="bi bi-star-fill" style="font-size:.65rem"></i>{{ number_format($anime->rating,1) }}</span>@endif
|
||||
<div class="a-hover-play"><div class="play-circle"><i class="bi bi-play-fill"></i></div></div>
|
||||
</div>
|
||||
<div class="a-info">
|
||||
<div class="a-title">{{ $anime->title }}</div>
|
||||
<div class="a-meta">{{ $anime->release_year }}</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@if($animes->hasPages())
|
||||
<div style="margin-top:32px">{{ $animes->links('frontend.pagination') }}</div>
|
||||
@endif
|
||||
@else
|
||||
<div style="text-align:center;padding:64px 20px;color:var(--text3)">
|
||||
<i class="bi bi-collection" style="font-size:3rem;opacity:.25;display:block;margin-bottom:14px"></i>
|
||||
<p>Bu türde henüz anime bulunmuyor.</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Telif Hakkı & DMCA | Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
.legal-hero{padding-top:calc(var(--nav-h) + 48px);padding-bottom:28px;text-align:center}
|
||||
.legal-hero h1{font-size:1.8rem;font-weight:900;margin-bottom:8px}
|
||||
.legal-hero p{color:rgba(255,255,255,.4);font-size:.9rem}
|
||||
.legal-body{max-width:820px;margin:0 auto;padding:0 20px 80px}
|
||||
.legal-section{margin-bottom:36px}
|
||||
.legal-section h2{font-size:1.05rem;font-weight:800;color:#ff2d7d;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.07)}
|
||||
.legal-section p,.legal-section li{font-size:.88rem;color:rgba(255,255,255,.65);line-height:1.8;margin-bottom:8px}
|
||||
.legal-section ul{padding-left:20px}
|
||||
.legal-section li{margin-bottom:4px}
|
||||
.legal-warn{background:rgba(245,158,11,.07);border:1px solid rgba(245,158,11,.25);border-radius:12px;padding:16px 20px;margin-bottom:32px}
|
||||
.legal-warn p{color:rgba(255,230,150,.7);font-size:.83rem;margin:0;line-height:1.7}
|
||||
.dmca-box{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:14px;padding:24px;margin-bottom:24px}
|
||||
.dmca-box h3{font-size:.95rem;font-weight:800;margin-bottom:14px;color:rgba(255,255,255,.85)}
|
||||
.dmca-box ol{padding-left:20px}
|
||||
.dmca-box li{font-size:.85rem;color:rgba(255,255,255,.6);line-height:1.8;margin-bottom:6px}
|
||||
.dmca-email{display:inline-block;background:rgba(255,45,125,.12);border:1px solid rgba(255,45,125,.25);border-radius:10px;padding:14px 20px;color:#ff2d7d;font-weight:700;font-size:.9rem;text-decoration:none;margin-top:8px}
|
||||
.legal-updated{font-size:.78rem;color:rgba(255,255,255,.25);text-align:right;margin-bottom:32px}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div style="max-width:1200px;margin:0 auto">
|
||||
|
||||
<div class="legal-hero">
|
||||
<h1>Telif Hakkı & DMCA</h1>
|
||||
<p>İçerik kaldırma talepleri ve telif hakkı politikası</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-body">
|
||||
|
||||
<div class="legal-warn">
|
||||
<p><strong>Önemli:</strong> Animexe herhangi bir video içeriğini sunucularında barındırmaz. Tüm videolar üçüncü taraf platformlara embed edilmektedir. Hak sahiplerinin içerik kaldırma taleplerine en kısa sürede yanıt veririz.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>1. Platform Statüsü — Teknik Aracı</h2>
|
||||
<p>Animexe, <strong>5651 sayılı Kanun</strong> kapsamında bir <strong>yer sağlayıcı</strong> ve teknik aracıdır. Platform:</p>
|
||||
<ul>
|
||||
<li>Video içeriklerini kendi sunucularında <strong>barındırmaz, depolamaz veya yüklemez</strong>.</li>
|
||||
<li>Üçüncü taraf video barındırma platformlarından (YouTube, Dailymotion, Drive vb.) iframe/embed yöntemiyle içerik gösterir.</li>
|
||||
<li>İçeriklerin telif hakkı sahipliği, orijinal barındırma platformlarına aittir.</li>
|
||||
<li>İçerik kaldırma taleplerini alır ve ilgili üçüncü taraflarla koordineli biçimde işler.</li>
|
||||
</ul>
|
||||
<p>Anime içerikleri üzerindeki telif hakları; ilgili Japon yapım şirketlerine, yayıncılarına ve lisans sahiplerine aittir. Animexe bu içerikler üzerinde herhangi bir hak iddia etmez.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>2. Telif Hakkı İhlali Bildirimi (DMCA / 5651)</h2>
|
||||
<p>İçeriğinizin izniniz olmadan Platform'da yayımlandığını düşünüyorsanız aşağıdaki bilgilerle bize ulaşın:</p>
|
||||
|
||||
<div class="dmca-box">
|
||||
<h3>📋 Bildirimde Bulunması Gereken Bilgiler</h3>
|
||||
<ol>
|
||||
<li>Hak sahibinin (veya yetkili temsilcinin) <strong>adı, soyadı ve iletişim bilgileri</strong></li>
|
||||
<li>Telif hakkı ihlali iddia edilen <strong>orijinal eserin tanımı</strong></li>
|
||||
<li>Kaldırılmasını talep ettiğiniz <strong>Platform üzerindeki içeriğin tam URL'si</strong></li>
|
||||
<li>Söz konusu kullanımın yasal olarak yetkilendirilmediğine <strong>iyi niyet beyanı</strong></li>
|
||||
<li>Verdiğiniz bilgilerin doğru olduğuna ve hak sahibi ya da yetkili temsilci sıfatıyla hareket ettiğinize dair <strong>beyan</strong></li>
|
||||
<li>İmza (elektronik veya ıslak imza)</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<p>Bildiriminizi şu adrese gönderin:</p>
|
||||
<a class="dmca-email" href="mailto:dmca@animexe.com">
|
||||
✉ dmca@animexe.com
|
||||
</a>
|
||||
<br><br>
|
||||
<p>Geçerli bir bildirim aldıktan sonra ilgili içeriği <strong>en geç 72 saat içinde</strong> kaldırır veya erişimi engelleriz.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>3. Karşı Bildirim (Counter-Notice)</h2>
|
||||
<p>İçeriğinizin hatalı olarak kaldırıldığını düşünüyorsanız karşı bildirimde bulunabilirsiniz. Karşı bildirimde şunlar yer almalıdır:</p>
|
||||
<ul>
|
||||
<li>Kimlik bilgileriniz ve iletişim bilgileriniz</li>
|
||||
<li>Kaldırılan içeriğin açıklaması ve URL'si</li>
|
||||
<li>İçeriğin hatalı kaldırıldığına dair iyi niyet beyanı</li>
|
||||
<li>Verilen bilgilerin doğru olduğuna dair yemin beyanı</li>
|
||||
</ul>
|
||||
<p>Karşı bildirimler orijinal şikâyet sahibine iletilir ve 10 iş günü içinde değerlendirilir.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>4. Tekrarlayan İhlaller Politikası</h2>
|
||||
<p>Birden fazla kez telif hakkı ihlali tespit edilen kullanıcıların hesapları <strong>kalıcı olarak kapatılır</strong>. Platform, bu konuda DMCA tekrarlayan ihlalci politikasına uygun hareket eder.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>5. Hukuki Koruma ve Adil Kullanım</h2>
|
||||
<p>Platform, aşağıdaki ilkeler çerçevesinde hareket ettiğini beyan eder:</p>
|
||||
<ul>
|
||||
<li>ABD hukukunda DMCA Safe Harbor (17 U.S.C. §512), Türk hukukunda 5651 sayılı Kanun kapsamında teknik aracı korumasından yararlanmaktadır.</li>
|
||||
<li>Anime tanıtım görselleri, karakter isimleri ve başlıklar "adil kullanım" (fair use) / tanıtım amacıyla kullanılmakta olup ticari amaç taşımamaktadır.</li>
|
||||
<li>Platform, içeriklerden herhangi bir reklam geliri elde etmemektedir (içerik barındırılmadığından).</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>6. Hukuki Süreç Talepleri</h2>
|
||||
<p>Mahkeme kararı, savcılık müzekkeresi veya resmi hukuki talepleri aşağıdaki adrese iletiniz:</p>
|
||||
<p><a href="mailto:hukuk@animexe.com" style="color:#ff2d7d">hukuk@animexe.com</a></p>
|
||||
<p>Talepler, Türkiye Cumhuriyeti mahkemelerinin yetkisi dahilinde değerlendirilir.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>7. Lisans Talepleri</h2>
|
||||
<p>İçeriklerinizin Platform'da lisanslı ve yasal olarak yayımlanmasını istiyorsanız lisans anlaşması için bizimle iletişime geçin:</p>
|
||||
<p><a href="mailto:lisans@animexe.com" style="color:#ff2d7d">lisans@animexe.com</a></p>
|
||||
</div>
|
||||
|
||||
<div class="legal-updated">Son güncelleme: Nisan 2025 — Animexe Ekibi</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,152 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Gizlilik Politikası | Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
.legal-hero{padding-top:calc(var(--nav-h) + 48px);padding-bottom:28px;text-align:center}
|
||||
.legal-hero h1{font-size:1.8rem;font-weight:900;margin-bottom:8px}
|
||||
.legal-hero p{color:rgba(255,255,255,.4);font-size:.9rem}
|
||||
.legal-body{max-width:820px;margin:0 auto;padding:0 20px 80px}
|
||||
.legal-section{margin-bottom:36px}
|
||||
.legal-section h2{font-size:1.05rem;font-weight:800;color:#ff2d7d;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.07)}
|
||||
.legal-section p,.legal-section li{font-size:.88rem;color:rgba(255,255,255,.65);line-height:1.8;margin-bottom:8px}
|
||||
.legal-section ul{padding-left:20px}
|
||||
.legal-section li{margin-bottom:4px}
|
||||
.legal-warn{background:rgba(59,130,246,.07);border:1px solid rgba(59,130,246,.2);border-radius:12px;padding:16px 20px;margin-bottom:32px}
|
||||
.legal-warn p{color:rgba(180,210,255,.7);font-size:.83rem;margin:0;line-height:1.7}
|
||||
.legal-table{width:100%;border-collapse:collapse;font-size:.83rem;margin-bottom:12px}
|
||||
.legal-table th{text-align:left;color:rgba(255,255,255,.4);font-weight:700;padding:8px 12px;border-bottom:1px solid rgba(255,255,255,.07);font-size:.75rem;text-transform:uppercase}
|
||||
.legal-table td{padding:8px 12px;color:rgba(255,255,255,.6);border-bottom:1px solid rgba(255,255,255,.04)}
|
||||
.legal-updated{font-size:.78rem;color:rgba(255,255,255,.25);text-align:right;margin-bottom:32px}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div style="max-width:1200px;margin:0 auto">
|
||||
|
||||
<div class="legal-hero">
|
||||
<h1>Gizlilik Politikası</h1>
|
||||
<p>Son güncelleme: Nisan 2025</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-body">
|
||||
|
||||
<div class="legal-warn">
|
||||
<p><strong>KVKK Bildirimi:</strong> Bu politika, 6698 sayılı Kişisel Verilerin Korunması Kanunu (KVKK) uyarınca hazırlanmıştır. Animexe olarak kişisel verilerinizin korunmasını ciddiye alıyoruz.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>1. Veri Sorumlusu</h2>
|
||||
<p>Animexe platformu kapsamında kişisel verilerinizin işlenmesinden sorumlu veri sorumlusu, <strong>Animexe</strong>'dir. İletişim için: <a href="mailto:iletisim@animexe.com" style="color:#ff2d7d">iletisim@animexe.com</a></p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>2. Topladığımız Veriler</h2>
|
||||
<table class="legal-table">
|
||||
<tr><th>Veri Türü</th><th>Açıklama</th><th>Zorunluluk</th></tr>
|
||||
<tr><td>Hesap Bilgileri</td><td>Ad, kullanıcı adı, e-posta adresi</td><td>Zorunlu</td></tr>
|
||||
<tr><td>Profil Bilgileri</td><td>Profil fotoğrafı, biyografi (isteğe bağlı)</td><td>İsteğe bağlı</td></tr>
|
||||
<tr><td>Aktivite Verileri</td><td>İzleme geçmişi, oy kullanma, yorum yazma, favori listesi</td><td>Otomatik</td></tr>
|
||||
<tr><td>Teknik Veriler</td><td>IP adresi, tarayıcı türü, cihaz bilgisi, çerezler</td><td>Otomatik</td></tr>
|
||||
<tr><td>İletişim Verileri</td><td>Bize gönderdiğiniz mesajlar</td><td>İsteğe bağlı</td></tr>
|
||||
</table>
|
||||
<p>Kredi kartı veya ödeme bilgisi toplamamaktayız.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>3. Verilerin İşlenme Amaçları</h2>
|
||||
<ul>
|
||||
<li>Hesabınızı oluşturmak ve yönetmek</li>
|
||||
<li>Kişiselleştirilmiş içerik ve öneri sunmak (Ruh Hali Motoru vb.)</li>
|
||||
<li>Kullanıcı güvenliğini sağlamak ve yetkisiz erişimi önlemek</li>
|
||||
<li>Platform hizmetlerini geliştirmek ve teknik sorunları gidermek</li>
|
||||
<li>İstatistiksel analiz yapmak (anonim ve toplu hâlde)</li>
|
||||
<li>Hukuki yükümlülükleri yerine getirmek</li>
|
||||
<li>Hizmet bildirimleri göndermek (e-posta onayı, şifre sıfırlama vb.)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>4. Hukuki İşleme Dayanakları (KVKK md. 5)</h2>
|
||||
<ul>
|
||||
<li><strong>Sözleşme ifası:</strong> Hesap oluşturma ve platform hizmetlerinin sunulması için zorunlu veriler</li>
|
||||
<li><strong>Meşru menfaat:</strong> Platform güvenliği, dolandırıcılık önleme, analitik</li>
|
||||
<li><strong>Açık rıza:</strong> Pazarlama iletişimleri ve isteğe bağlı özellikler</li>
|
||||
<li><strong>Hukuki yükümlülük:</strong> Resmi makam talepleri ve yasal gereklilikler</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>5. Verilerin Paylaşılması</h2>
|
||||
<p>Kişisel verilerinizi aşağıdaki durumlar dışında üçüncü taraflarla satmaz veya kiralamayız:</p>
|
||||
<ul>
|
||||
<li><strong>Teknik hizmet sağlayıcılar:</strong> Sunucu barındırma, e-posta servisi gibi altyapı hizmetleri (işleme sözleşmesi kapsamında)</li>
|
||||
<li><strong>Yasal zorunluluklar:</strong> Yetkili kamu kurum ve kuruluşlarının talepleri</li>
|
||||
<li><strong>Platform güvenliği:</strong> Dolandırıcılık veya yasadışı faaliyetin tespiti halinde ilgili makamlarla paylaşım</li>
|
||||
</ul>
|
||||
<p>Üçüncü taraf embed içerik sağlayıcıları (video platformları) kendi gizlilik politikalarına tabidir; bu politikalar üzerinde kontrolümüz bulunmamaktadır.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>6. Çerezler (Cookies)</h2>
|
||||
<p>Platform, aşağıdaki amaçlarla çerezler kullanmaktadır:</p>
|
||||
<ul>
|
||||
<li><strong>Zorunlu çerezler:</strong> Oturum yönetimi, CSRF koruması — devre dışı bırakılamaz</li>
|
||||
<li><strong>Tercih çerezleri:</strong> Dil, tema ve kullanıcı tercihleri</li>
|
||||
<li><strong>Analitik çerezler:</strong> Anonim kullanım istatistikleri</li>
|
||||
</ul>
|
||||
<p>Tarayıcı ayarlarından çerezleri yönetebilirsiniz. Zorunlu çerezleri reddetmek Platform'un düzgün çalışmasını engelleyebilir.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>7. Veri Saklama Süreleri</h2>
|
||||
<ul>
|
||||
<li>Aktif hesap verileri: Hesap silinene kadar</li>
|
||||
<li>Hesap silindikten sonra: 30 gün içinde kalıcı silme</li>
|
||||
<li>Yasal zorunluluk gerektiren veriler: İlgili mevzuatta öngörülen süre (genellikle 3–10 yıl)</li>
|
||||
<li>Anonimleştirilmiş analitik veriler: Süresiz (kişisel veri içermediğinden)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>8. Veri Güvenliği</h2>
|
||||
<p>Kişisel verilerinizi korumak için aşağıdaki teknik ve idari önlemleri alıyoruz:</p>
|
||||
<ul>
|
||||
<li>HTTPS (SSL/TLS) şifrelemesi</li>
|
||||
<li>Şifreler bcrypt/hash ile saklanır; hiçbir zaman düz metin olarak tutulmaz</li>
|
||||
<li>Veritabanı erişimini kısıtlayan güvenlik duvarları</li>
|
||||
<li>Düzenli güvenlik güncellemeleri ve yama uygulaması</li>
|
||||
<li>Çalışan erişim kontrolü (need-to-know prensibi)</li>
|
||||
</ul>
|
||||
<p>Hiçbir sistem %100 güvenli değildir. Güvenlik ihlali tespit edilmesi halinde sizi ve ilgili makamları KVKK'nın öngördüğü süre içinde (72 saat) bilgilendiririz.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>9. KVKK Kapsamındaki Haklarınız</h2>
|
||||
<p>KVKK'nın 11. maddesi uyarınca aşağıdaki haklara sahipsiniz:</p>
|
||||
<ul>
|
||||
<li>Kişisel verilerinizin işlenip işlenmediğini öğrenme</li>
|
||||
<li>İşlenmişse buna ilişkin bilgi talep etme</li>
|
||||
<li>İşlenme amacını ve bunların amacına uygun kullanılıp kullanılmadığını öğrenme</li>
|
||||
<li>Yurt içinde veya yurt dışında kişisel verilerin aktarıldığı üçüncü kişileri bilme</li>
|
||||
<li>Kişisel verilerin eksik veya yanlış işlenmiş olması hâlinde bunların düzeltilmesini isteme</li>
|
||||
<li>Verilerin silinmesini veya yok edilmesini isteme (yasal sınırlar çerçevesinde)</li>
|
||||
<li>İşlenen verilerin münhasıran otomatik sistemler vasıtasıyla analiz edilmesi suretiyle aleyhinize bir sonucun ortaya çıkmasına itiraz etme</li>
|
||||
<li>Kişisel verilerin kanuna aykırı olarak işlenmesi sebebiyle zarara uğraması hâlinde zararın giderilmesini talep etme</li>
|
||||
</ul>
|
||||
<p>Bu haklarınızı kullanmak için <a href="mailto:iletisim@animexe.com" style="color:#ff2d7d">iletisim@animexe.com</a> adresine e-posta gönderin. Talepler 30 gün içinde yanıtlanır.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>10. Politika Değişiklikleri</h2>
|
||||
<p>Bu politikayı zaman zaman güncelleyebiliriz. Önemli değişiklikler için kayıtlı e-posta adresinize bildirim göndeririz. Değişikliği kabul etmiyorsanız hesabınızı silebilirsiniz.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>11. Şikâyet Hakkı</h2>
|
||||
<p>Kişisel verilerinizin işlenmesiyle ilgili şikayetleriniz için <strong>Kişisel Verileri Koruma Kurumu</strong>'na (KVKK) başvurabilirsiniz: <a href="https://www.kvkk.gov.tr" target="_blank" rel="noopener" style="color:#ff2d7d">www.kvkk.gov.tr</a></p>
|
||||
</div>
|
||||
|
||||
<div class="legal-updated">Son güncelleme: Nisan 2025 — Animexe Ekibi</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,141 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Kullanım Koşulları | Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
.legal-hero{padding-top:calc(var(--nav-h) + 48px);padding-bottom:28px;text-align:center}
|
||||
.legal-hero h1{font-size:1.8rem;font-weight:900;margin-bottom:8px}
|
||||
.legal-hero p{color:rgba(255,255,255,.4);font-size:.9rem}
|
||||
.legal-body{max-width:820px;margin:0 auto;padding:0 20px 80px}
|
||||
.legal-section{margin-bottom:36px}
|
||||
.legal-section h2{font-size:1.05rem;font-weight:800;color:#ff2d7d;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.07)}
|
||||
.legal-section p,.legal-section li{font-size:.88rem;color:rgba(255,255,255,.65);line-height:1.8;margin-bottom:8px}
|
||||
.legal-section ul{padding-left:20px}
|
||||
.legal-section li{margin-bottom:4px}
|
||||
.legal-warn{background:rgba(255,45,125,.07);border:1px solid rgba(255,45,125,.2);border-radius:12px;padding:16px 20px;margin-bottom:32px}
|
||||
.legal-warn p{color:rgba(255,200,200,.7);font-size:.83rem;margin:0;line-height:1.7}
|
||||
.legal-updated{font-size:.78rem;color:rgba(255,255,255,.25);text-align:right;margin-bottom:32px}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div style="max-width:1200px;margin:0 auto">
|
||||
|
||||
<div class="legal-hero">
|
||||
<h1>Kullanım Koşulları</h1>
|
||||
<p>Son güncelleme: Nisan 2025</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-body">
|
||||
|
||||
<div class="legal-warn">
|
||||
<p><strong>Önemli:</strong> Bu platformu kullanarak aşağıdaki koşulların tamamını okuduğunuzu, anladığınızı ve kabul ettiğinizi beyan etmiş olursunuz. Koşulları kabul etmiyorsanız lütfen platformu kullanmayınız.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>1. Genel Bilgiler ve Platform Statüsü</h2>
|
||||
<p>Animexe ("Platform", "Biz", "Site"), yalnızca <strong>tanıtım, eğitim ve araştırma amaçlı</strong> olarak tasarlanmış bir anime topluluğu ve içerik tanıtım platformudur. Platform, kullanıcılara anime hakkında bilgi, tartışma, yorum ve topluluğa dayalı özellikler sunmaktadır.</p>
|
||||
<p>Platform, herhangi bir video içeriğini barındırmaz, depolamaz veya dağıtmaz. Gösterilen içerikler üçüncü taraf kaynaklardan embed yoluyla sunulmakta olup Platform yalnızca teknik bir aracı konumundadır.</p>
|
||||
<p>Platform, 18 yaş ve üzeri kullanıcılara yönelik olup bazı içerikler yetişkin temalı unsurlar içerebilir. 18 yaşın altındaki kullanıcılar platformu kullanmamalıdır.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>2. Sorumluluk Reddi — İçerik</h2>
|
||||
<ul>
|
||||
<li>Platform, üçüncü taraflarca üretilen veya barındırılan içeriklerden <strong>hiçbir şekilde sorumlu değildir</strong>.</li>
|
||||
<li>Embed edilen videolar, resimler veya diğer medya içerikleri ilgili üçüncü taraf platformlara (örn. Google Drive, Dailymotion, Streamtape, vb.) aittir.</li>
|
||||
<li>Platform, üçüncü taraf içeriklerinin doğruluğunu, güncelliğini veya yasallığını garanti etmez.</li>
|
||||
<li>Herhangi bir içeriğin telif hakkı ihlali oluşturduğunu düşünüyorsanız lütfen <a href="{{ route('legal.dmca') }}" style="color:#ff2d7d">Telif Hakkı Bildirimi</a> sayfamızı ziyaret edin.</li>
|
||||
<li>Platform, yalnızca teknik altyapı sağlayan bir aracı niteliğinde olup 5651 sayılı Kanun kapsamında içerik sağlayıcı değil, <strong>yer sağlayıcı</strong> konumundadır.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>3. Kullanıcı Hesabı ve Sorumlulukları</h2>
|
||||
<ul>
|
||||
<li>Kayıt olurken gerçek ve güncel bilgi sağlamak zorundasınız. Yanlış bilgi vermek hesabın askıya alınmasına yol açabilir.</li>
|
||||
<li>Hesap şifrenizin güvenliğinden siz sorumlusunuz. Hesabınızın yetkisiz kullanımını derhal bize bildirmeniz gerekmektedir.</li>
|
||||
<li>Bir kullanıcı, yalnızca bir hesap açabilir. Çoklu hesap açmak yasaktır.</li>
|
||||
<li>Hesabınızı başkasına devredemez veya satamazsınız.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>4. Yasaklı Kullanımlar</h2>
|
||||
<p>Aşağıdaki eylemler kesinlikle yasaktır:</p>
|
||||
<ul>
|
||||
<li>Platform altyapısına zarar verecek saldırılar (DDoS, SQL injection, XSS vb.) gerçekleştirmek</li>
|
||||
<li>Otomatik bot, scraper veya benzeri araçlarla Platform'a erişmek</li>
|
||||
<li>Diğer kullanıcılara yönelik taciz, nefret söylemi, tehdit veya ayrımcılık içeren içerik paylaşmak</li>
|
||||
<li>Gerçek kişilerin kişisel bilgilerini izinsiz olarak paylaşmak (doxing)</li>
|
||||
<li>Yasadışı faaliyetleri teşvik eden içerik paylaşmak</li>
|
||||
<li>Platform'un ticari amaçlarla yetkisiz kullanımı</li>
|
||||
<li>Spam, yanıltıcı veya aldatıcı içerik paylaşmak</li>
|
||||
<li>Reşit olmayan bireyleri içeren müstehcen içerik paylaşmak veya yüklemeye teşebbüs etmek</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>5. Kullanıcı Tarafından Oluşturulan İçerik (UGC)</h2>
|
||||
<p>Platform'a yorum, argüman, kapsül mesajı veya diğer içerikler gönderdiğinizde:</p>
|
||||
<ul>
|
||||
<li>Bu içeriklerin yasalara uygun, doğru ve başkalarının haklarına saygılı olduğunu beyan edersiniz.</li>
|
||||
<li>Platform'a, içeriği görüntüleme, depolama ve kullanıcılara sunma için <strong>gayri münhasır, ücretsiz bir lisans</strong> verirsiniz.</li>
|
||||
<li>Üçüncü tarafların telif hakları, ticari markaları veya özel haklarını ihlal eden içerik paylaşmaktan siz sorumlusunuz.</li>
|
||||
<li>Platform, herhangi bir içeriği önceden haber vermeksizin kaldırma hakkını saklı tutar.</li>
|
||||
<li>Spoiler tespiti dahil her türlü içerik moderasyonu otomatik sistemler ve/veya editörler tarafından yapılabilir; Platform bu kararların kesin doğruluğunu garanti etmez.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>6. Fikri Mülkiyet</h2>
|
||||
<p>Platform'un özgün tasarımı, logosu, kodu ve içerikleri Animexe'ye aittir ve Türk Fikir ve Sanat Eserleri Kanunu kapsamında koruma altındadır.</p>
|
||||
<p>Anime adları, görseller, karakterler ve ilgili materyaller kendi telif hakkı sahiplerine (Japon stüdyolar, yayıncılar vb.) aittir. Bu içeriklerin kullanımı yalnızca tanıtım amacıyla "adil kullanım" ilkesi çerçevesinde gerçekleşmektedir.</p>
|
||||
<p>Platform içeriğini izinsiz kopyalamak, dağıtmak veya ticari amaçla kullanmak yasaktır.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>7. Hesap Askıya Alma ve Sonlandırma</h2>
|
||||
<p>Platform, aşağıdaki durumlarda hesabınızı önceden haber vermeksizin askıya alabilir veya silebilir:</p>
|
||||
<ul>
|
||||
<li>Bu Koşullar'ın ihlali</li>
|
||||
<li>Hukuki bir gereklilik veya resmi makam talebi</li>
|
||||
<li>Diğer kullanıcılara veya platforma zarar veren davranışlar</li>
|
||||
<li>Uzun süreli hesap hareketsizliği (12 ay ve üzeri)</li>
|
||||
</ul>
|
||||
<p>Hesabınızı kendiniz silmek istediğinizde tüm verileriniz 30 gün içinde kalıcı olarak silinir.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>8. Hizmetin Değiştirilmesi ve Sonlandırılması</h2>
|
||||
<p>Platform, herhangi bir zamanda ve önceden bildirimde bulunmaksızın hizmetini, özelliklerini veya bu Koşulları değiştirme hakkını saklı tutar. Değişiklikler yayımlandıktan sonra platformu kullanmaya devam etmeniz yeni koşulları kabul ettiğiniz anlamına gelir.</p>
|
||||
<p>Platform, teknik arıza, bakım veya mücbir sebep nedeniyle hizmetini geçici ya da kalıcı olarak durdurabilir. Bu durumdan doğan hiçbir zarardan sorumlu tutulamaz.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>9. Sorumluluk Sınırlaması</h2>
|
||||
<p>Platform, aşağıdaki durumlardan kaynaklanan zararlardan sorumlu değildir:</p>
|
||||
<ul>
|
||||
<li>Üçüncü taraf içeriklere erişimden veya bu içeriklere güvenmekten kaynaklanan zararlar</li>
|
||||
<li>Hizmet kesintisi veya veri kaybı</li>
|
||||
<li>Kullanıcılar arasındaki anlaşmazlıklar</li>
|
||||
<li>Yetkisiz hesap erişimi (kullanıcı güvenlik ihlalinden kaynaklanıyorsa)</li>
|
||||
<li>Üçüncü taraf site veya hizmetlerinin kullanımı</li>
|
||||
</ul>
|
||||
<p>Platformun toplam sorumluluğu, varsa ödenen üyelik ücretini geçemez.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>10. Uygulanacak Hukuk ve Uyuşmazlık Çözümü</h2>
|
||||
<p>Bu Koşullar, <strong>Türkiye Cumhuriyeti</strong> hukukuna tabidir. Bu Koşullar'dan doğan uyuşmazlıkların çözümünde <strong>Türkiye Cumhuriyeti mahkemeleri</strong> yetkilidir.</p>
|
||||
<p>Herhangi bir anlaşmazlık için öncelikle <a href="mailto:iletisim@animexe.com" style="color:#ff2d7d">iletisim@animexe.com</a> adresine başvurulması tavsiye edilir.</p>
|
||||
</div>
|
||||
|
||||
<div class="legal-section">
|
||||
<h2>11. İletişim</h2>
|
||||
<p>Kullanım Koşulları hakkında sorularınız için: <a href="mailto:iletisim@animexe.com" style="color:#ff2d7d">iletisim@animexe.com</a></p>
|
||||
</div>
|
||||
|
||||
<div class="legal-updated">Son güncelleme: Nisan 2025 — Animexe Ekibi</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,103 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Mesajlar — Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
.msg-wrap{max-width:680px;margin:0 auto;padding:calc(var(--nav-h) + 28px) 16px 64px}
|
||||
.msg-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
|
||||
.msg-header h1{font-size:1.3rem;font-weight:900;color:#fff}
|
||||
|
||||
.msg-list{display:flex;flex-direction:column;gap:4px}
|
||||
.msg-row{
|
||||
display:flex;align-items:center;gap:14px;
|
||||
padding:14px 16px;border-radius:14px;
|
||||
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);
|
||||
text-decoration:none;transition:all .15s;cursor:pointer;
|
||||
}
|
||||
.msg-row:hover{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.12)}
|
||||
.msg-row.unread{background:rgba(0,245,255,.04);border-color:rgba(0,245,255,.12)}
|
||||
|
||||
.msg-ava{
|
||||
width:46px;height:46px;border-radius:50%;flex-shrink:0;overflow:hidden;
|
||||
background:linear-gradient(135deg,var(--accent2),var(--accent));
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:1.1rem;font-weight:800;color:#fff;
|
||||
}
|
||||
.msg-ava img{width:100%;height:100%;object-fit:cover}
|
||||
|
||||
.msg-info{flex:1;min-width:0}
|
||||
.msg-name{font-size:.9rem;font-weight:700;color:#fff;margin-bottom:3px;display:flex;align-items:center;gap:8px}
|
||||
.msg-preview{font-size:.8rem;color:rgba(255,255,255,.4);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.msg-row.unread .msg-preview{color:rgba(255,255,255,.7)}
|
||||
.msg-unread-dot{width:8px;height:8px;border-radius:50%;background:var(--accent2);flex-shrink:0}
|
||||
.msg-time{font-size:.7rem;color:rgba(255,255,255,.25);flex-shrink:0;align-self:flex-start;margin-top:4px}
|
||||
|
||||
.msg-empty{text-align:center;padding:64px 20px;color:rgba(255,255,255,.2)}
|
||||
.msg-empty i{font-size:2.8rem;display:block;margin-bottom:14px;opacity:.2}
|
||||
.msg-empty p{font-size:.9rem}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="msg-wrap">
|
||||
<div class="msg-header">
|
||||
<h1><i class="bi bi-chat-dots" style="color:var(--accent2)"></i> Mesajlar</h1>
|
||||
</div>
|
||||
|
||||
@if($conversations->isEmpty())
|
||||
<div class="msg-empty">
|
||||
<i class="bi bi-chat-square-dots"></i>
|
||||
<p>Henüz mesajın yok.<br>Bir kullanıcının profiline git ve mesaj gönder.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="msg-list">
|
||||
@foreach($conversations as $conv)
|
||||
@php
|
||||
$other = $conv['other'];
|
||||
$last = $conv['last_message'];
|
||||
$unread= $conv['unread'];
|
||||
@endphp
|
||||
<a href="{{ route('messages.show', $conv['id']) }}" class="msg-row {{ $unread > 0 ? 'unread' : '' }}">
|
||||
<div class="msg-ava">
|
||||
@if($other?->avatar)
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($other->avatar) }}" alt="{{ $other->name }}">
|
||||
@else
|
||||
{{ mb_strtoupper(mb_substr($other?->name ?? '?', 0, 1)) }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="msg-info">
|
||||
<div class="msg-name">
|
||||
{{ $other?->name ?? 'Silinmiş Kullanıcı' }}
|
||||
@if($unread > 0)
|
||||
<span style="font-size:.65rem;background:var(--accent2);color:#05050e;padding:1px 7px;border-radius:20px;font-weight:800">{{ $unread }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="msg-preview">
|
||||
@if($last)
|
||||
@if($last->user_id === auth()->id()) <span style="color:rgba(255,255,255,.25)">Sen: </span> @endif
|
||||
@if(str_starts_with($last->body, 'ANIMESHARE::'))
|
||||
@php try { $sd = json_decode(substr($last->body,12),true); } catch(\Throwable $e){ $sd=null; } @endphp
|
||||
<i class="bi bi-play-circle" style="color:var(--accent2)"></i> {{ $sd['title'] ?? 'Anime' }} paylaştı
|
||||
@elseif(str_starts_with($last->body, 'IMAGE::'))
|
||||
<i class="bi bi-image"></i> Fotoğraf
|
||||
@elseif(str_starts_with($last->body, 'GIF::'))
|
||||
<i class="bi bi-file-play"></i> GIF
|
||||
@else
|
||||
{{ Str::limit($last->body, 60) }}
|
||||
@endif
|
||||
@else
|
||||
<em>Henüz mesaj yok</em>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:6px">
|
||||
@if($unread > 0)<div class="msg-unread-dot"></div>@endif
|
||||
<span class="msg-time">
|
||||
@if($conv['updated_at']){{ $conv['updated_at']->diffForHumans(null, true) }}@endif
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,655 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', ($other?->name ?? 'Mesaj') . ' — Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
footer{display:none!important}
|
||||
#page-content{padding:0!important}
|
||||
#ai-fab,#ai-panel{display:none!important}
|
||||
#topnav,#mob-bottom-nav{display:none!important}
|
||||
.w-chat-support{display:none!important}
|
||||
|
||||
.chat-root{
|
||||
display:flex;flex-direction:column;
|
||||
position:fixed;
|
||||
top:0;left:0;right:0;bottom:0;
|
||||
background:#05050e;
|
||||
}
|
||||
|
||||
/* ── Header ─────────────────────────────── */
|
||||
.chat-hdr{
|
||||
display:flex;align-items:center;gap:10px;
|
||||
padding:10px 14px;
|
||||
background:rgba(5,5,14,.95);backdrop-filter:blur(12px);
|
||||
border-bottom:1px solid rgba(255,255,255,.07);
|
||||
flex-shrink:0;z-index:10;
|
||||
}
|
||||
.chat-hdr-back{
|
||||
width:36px;height:36px;border-radius:10px;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
background:rgba(255,255,255,.06);border:none;
|
||||
color:rgba(255,255,255,.6);font-size:1rem;cursor:pointer;
|
||||
text-decoration:none;flex-shrink:0;transition:all .15s;
|
||||
}
|
||||
.chat-hdr-back:hover{background:rgba(255,255,255,.12);color:#fff}
|
||||
.chat-hdr-ava{
|
||||
width:38px;height:38px;border-radius:50%;overflow:hidden;flex-shrink:0;
|
||||
background:linear-gradient(135deg,var(--accent2),var(--accent));
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:.9rem;font-weight:800;color:#fff;text-decoration:none;
|
||||
}
|
||||
.chat-hdr-ava img{width:100%;height:100%;object-fit:cover}
|
||||
.chat-hdr-info{flex:1;min-width:0}
|
||||
.chat-hdr-name{font-size:.9rem;font-weight:800;color:#fff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.chat-hdr-sub{font-size:.68rem;color:rgba(255,255,255,.3)}
|
||||
.chat-hdr-btn{
|
||||
width:36px;height:36px;border-radius:10px;flex-shrink:0;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
background:rgba(255,255,255,.06);border:none;color:rgba(255,255,255,.5);
|
||||
font-size:.9rem;cursor:pointer;text-decoration:none;transition:all .15s;
|
||||
}
|
||||
.chat-hdr-btn:hover{background:rgba(255,255,255,.12);color:#fff}
|
||||
.chat-hdr-btn.call{color:#22c55e}
|
||||
.chat-hdr-btn.call:hover{background:rgba(34,197,94,.15);color:#22c55e}
|
||||
|
||||
/* ── Messages area ──────────────────────── */
|
||||
.chat-body{
|
||||
flex:1;overflow-y:auto;
|
||||
padding:14px 14px 8px;
|
||||
display:flex;flex-direction:column;
|
||||
gap:4px;
|
||||
-webkit-overflow-scrolling:touch;
|
||||
min-height:0;
|
||||
}
|
||||
.chat-body::-webkit-scrollbar{width:3px}
|
||||
.chat-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,.1);border-radius:2px}
|
||||
|
||||
/* ── Bubbles ────────────────────────────── */
|
||||
.chat-msg{display:flex;gap:8px;max-width:80%;align-items:flex-end}
|
||||
.chat-msg.mine{align-self:flex-end;flex-direction:row-reverse}
|
||||
.chat-msg.theirs{align-self:flex-start}
|
||||
.chat-msg-ava{
|
||||
width:28px;height:28px;border-radius:50%;overflow:hidden;flex-shrink:0;
|
||||
background:linear-gradient(135deg,var(--accent2),var(--accent));
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:.65rem;font-weight:800;color:#fff;
|
||||
}
|
||||
.chat-msg-ava img{width:100%;height:100%;object-fit:cover}
|
||||
.chat-msg.mine .chat-msg-ava{display:none}
|
||||
.chat-bubble{
|
||||
padding:9px 14px;border-radius:18px;
|
||||
font-size:.88rem;line-height:1.5;word-break:break-word;max-width:100%;
|
||||
}
|
||||
.chat-msg.mine .chat-bubble{
|
||||
background:var(--accent2);color:#05050e;font-weight:500;border-bottom-right-radius:5px;
|
||||
}
|
||||
.chat-msg.theirs .chat-bubble{
|
||||
background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.08);
|
||||
color:rgba(255,255,255,.9);border-bottom-left-radius:5px;
|
||||
}
|
||||
.chat-time{font-size:.6rem;color:rgba(255,255,255,.25);align-self:flex-end;margin-bottom:3px;flex-shrink:0}
|
||||
.chat-date-sep{
|
||||
text-align:center;font-size:.65rem;color:rgba(255,255,255,.2);
|
||||
padding:12px 0 4px;font-weight:600;letter-spacing:.08em;
|
||||
}
|
||||
|
||||
/* ── Media messages ─────────────────────── */
|
||||
.chat-img{
|
||||
max-width:220px;max-height:220px;border-radius:14px;
|
||||
cursor:pointer;display:block;object-fit:cover;
|
||||
transition:opacity .15s;border:1px solid rgba(255,255,255,.08);
|
||||
}
|
||||
.chat-img:hover{opacity:.88}
|
||||
.chat-gif{max-width:220px;border-radius:14px;display:block}
|
||||
|
||||
/* ── Share card ─────────────────────────── */
|
||||
.share-card{
|
||||
display:flex;gap:10px;align-items:flex-start;
|
||||
background:rgba(0,245,255,.06);border:1px solid rgba(0,245,255,.18);
|
||||
border-radius:14px;padding:10px 12px;text-decoration:none;
|
||||
max-width:260px;transition:all .15s;
|
||||
}
|
||||
.share-card:hover{background:rgba(0,245,255,.12);border-color:rgba(0,245,255,.35)}
|
||||
.share-card-img{width:54px;height:78px;border-radius:8px;overflow:hidden;flex-shrink:0;background:rgba(255,255,255,.05)}
|
||||
.share-card-img img{width:100%;height:100%;object-fit:cover;display:block}
|
||||
.share-card-label{font-size:.58rem;color:var(--accent2);font-weight:700;letter-spacing:.08em;text-transform:uppercase;margin-bottom:4px}
|
||||
.share-card-title{font-size:.8rem;font-weight:800;color:#fff;line-height:1.3;margin-bottom:3px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||||
.share-card-ep{font-size:.7rem;color:rgba(255,255,255,.5);margin-bottom:3px}
|
||||
.share-card-play{font-size:.68rem;color:var(--accent2);display:flex;align-items:center;gap:3px;margin-top:4px;font-weight:700}
|
||||
|
||||
/* ── Footer input ───────────────────────── */
|
||||
.chat-footer{
|
||||
padding:8px 10px;
|
||||
background:rgba(5,5,14,.95);backdrop-filter:blur(12px);
|
||||
border-top:1px solid rgba(255,255,255,.07);
|
||||
flex-shrink:0;
|
||||
padding-bottom:max(8px,env(safe-area-inset-bottom));
|
||||
}
|
||||
.chat-toolbar{
|
||||
display:flex;align-items:center;gap:6px;
|
||||
padding:4px 0 8px;
|
||||
border-bottom:1px solid rgba(255,255,255,.06);
|
||||
margin-bottom:6px;
|
||||
}
|
||||
.chat-tool-btn{
|
||||
width:38px;height:38px;border-radius:10px;border:none;
|
||||
background:rgba(255,255,255,.06);color:rgba(255,255,255,.45);
|
||||
font-size:.88rem;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
transition:all .15s;flex-shrink:0;
|
||||
-webkit-tap-highlight-color:transparent;
|
||||
}
|
||||
.chat-tool-btn:hover{background:rgba(255,255,255,.13);color:#fff}
|
||||
.chat-tool-btn.active{background:rgba(0,245,255,.12);color:var(--accent2)}
|
||||
.chat-form{display:flex;align-items:flex-end;gap:8px}
|
||||
.chat-input{
|
||||
flex:1;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);
|
||||
border-radius:20px;padding:10px 16px;color:#fff;font-size:.9rem;font-family:inherit;
|
||||
resize:none;max-height:120px;overflow-y:auto;min-height:42px;
|
||||
line-height:1.4;outline:none;-webkit-appearance:none;transition:border-color .15s;
|
||||
}
|
||||
.chat-input:focus{border-color:rgba(0,245,255,.35)}
|
||||
.chat-input::placeholder{color:rgba(255,255,255,.25)}
|
||||
.chat-send{
|
||||
width:42px;height:42px;border-radius:50%;flex-shrink:0;
|
||||
background:var(--accent2);border:none;color:#05050e;
|
||||
font-size:1rem;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
transition:all .15s;-webkit-tap-highlight-color:transparent;
|
||||
}
|
||||
.chat-send:hover{background:#00d4de;transform:scale(1.05)}
|
||||
.chat-send:disabled{opacity:.4;transform:none;cursor:default}
|
||||
|
||||
/* ── Emoji picker ───────────────────────── */
|
||||
#chat-emoji-picker{
|
||||
display:none;flex-wrap:wrap;gap:2px;
|
||||
background:rgba(10,10,22,.98);border:1px solid rgba(255,255,255,.1);
|
||||
border-radius:14px;padding:10px;
|
||||
max-height:180px;overflow-y:auto;margin-bottom:6px;
|
||||
scrollbar-width:thin;
|
||||
}
|
||||
#chat-emoji-picker.show{display:flex}
|
||||
.chat-emoji-btn{
|
||||
font-size:1.3rem;width:36px;height:36px;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
border:none;background:none;cursor:pointer;border-radius:8px;
|
||||
transition:background .12s;-webkit-tap-highlight-color:transparent;
|
||||
}
|
||||
.chat-emoji-btn:hover{background:rgba(255,255,255,.1)}
|
||||
|
||||
/* ── GIF picker ─────────────────────────── */
|
||||
#chat-gif-picker{
|
||||
display:none;
|
||||
background:rgba(10,10,22,.98);border:1px solid rgba(255,255,255,.1);
|
||||
border-radius:14px;padding:8px;margin-bottom:6px;
|
||||
}
|
||||
#chat-gif-picker.show{display:block}
|
||||
.chat-gif-search{
|
||||
width:100%;padding:8px 12px;border-radius:10px;
|
||||
background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);
|
||||
color:#fff;font-size:.82rem;font-family:inherit;outline:none;
|
||||
margin-bottom:8px;-webkit-appearance:none;
|
||||
}
|
||||
.chat-gif-search::placeholder{color:rgba(255,255,255,.3)}
|
||||
.chat-gif-grid{
|
||||
display:grid;grid-template-columns:repeat(3,1fr);gap:4px;
|
||||
max-height:160px;overflow-y:auto;
|
||||
}
|
||||
.chat-gif-item{
|
||||
aspect-ratio:1;background:rgba(255,255,255,.05);border-radius:8px;
|
||||
overflow:hidden;cursor:pointer;transition:opacity .15s;
|
||||
}
|
||||
.chat-gif-item:hover{opacity:.8}
|
||||
.chat-gif-item img{width:100%;height:100%;object-fit:cover;display:block}
|
||||
.chat-gif-ph{padding:20px;text-align:center;color:rgba(255,255,255,.25);font-size:.78rem}
|
||||
|
||||
.chat-empty{
|
||||
flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;
|
||||
color:rgba(255,255,255,.2);gap:10px;
|
||||
}
|
||||
.chat-empty i{font-size:2.4rem;opacity:.2}
|
||||
.chat-empty p{font-size:.85rem}
|
||||
|
||||
@media(min-width:640px){
|
||||
.chat-root{max-width:760px;left:50%;transform:translateX(-50%)}
|
||||
.chat-msg{max-width:72%}
|
||||
}
|
||||
@media(max-width:640px){
|
||||
.chat-msg{max-width:88%}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
@php $me = auth()->user(); @endphp
|
||||
<div class="chat-root">
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="chat-hdr">
|
||||
<a href="{{ route('messages.index') }}" class="chat-hdr-back">
|
||||
<i class="bi bi-chevron-left"></i>
|
||||
</a>
|
||||
<a href="{{ $other ? route('user.profile', $other) : '#' }}" class="chat-hdr-ava">
|
||||
@if($other?->avatar)
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($other->avatar) }}" alt="{{ $other->name }}">
|
||||
@else
|
||||
{{ mb_strtoupper(mb_substr($other?->name ?? '?', 0, 1)) }}
|
||||
@endif
|
||||
</a>
|
||||
<div class="chat-hdr-info">
|
||||
<div class="chat-hdr-name">{{ $other?->name ?? 'Silinmiş Kullanıcı' }}</div>
|
||||
@if($other?->username)
|
||||
<div class="chat-hdr-sub">@php echo '@' . e($other->username); @endphp</div>
|
||||
@endif
|
||||
</div>
|
||||
@if($other)
|
||||
{{-- Sesli arama --}}
|
||||
<button class="chat-hdr-btn call" id="call-btn" data-user-id="{{ $other->id }}" title="Sesli ara">
|
||||
<i class="bi bi-telephone-fill"></i>
|
||||
</button>
|
||||
{{-- Profil --}}
|
||||
<a href="{{ route('user.profile', $other) }}" class="chat-hdr-btn" title="Profil">
|
||||
<i class="bi bi-person"></i>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Messages --}}
|
||||
<div class="chat-body" id="chat-body">
|
||||
@if($messages->isEmpty())
|
||||
<div class="chat-empty">
|
||||
<i class="bi bi-chat-dots"></i>
|
||||
<p>Henüz mesaj yok. İlk mesajı sen gönder!</p>
|
||||
</div>
|
||||
@else
|
||||
@php $lastDate = null; @endphp
|
||||
@foreach($messages as $msg)
|
||||
@php
|
||||
$msgDate = $msg->created_at->toDateString();
|
||||
$isMine = $msg->user_id === $me->id;
|
||||
@endphp
|
||||
@if($msgDate !== $lastDate)
|
||||
<div class="chat-date-sep">{{ $msg->created_at->translatedFormat('d F Y') }}</div>
|
||||
@php $lastDate = $msgDate; @endphp
|
||||
@endif
|
||||
<div class="chat-msg {{ $isMine ? 'mine' : 'theirs' }}" data-id="{{ $msg->id }}">
|
||||
@if(!$isMine)
|
||||
<div class="chat-msg-ava">
|
||||
@if($msg->user?->avatar)
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($msg->user->avatar) }}" alt="">
|
||||
@else{{ mb_strtoupper(mb_substr($msg->user?->name ?? '?', 0, 1)) }}@endif
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
@if(str_starts_with($msg->body, 'ANIMESHARE::'))
|
||||
@php try { $sd = json_decode(substr($msg->body,12),true); } catch(\Throwable $e){ $sd=null; } @endphp
|
||||
@if($sd)
|
||||
<a href="{{ url('/watch/'.($sd['slug']??'').'/'.($sd['s']??1).'/'.($sd['e']??1).(($sd['ts']??0)>0?'?t='.$sd['ts']:'')) }}" class="share-card">
|
||||
<div class="share-card-img">
|
||||
@if(!empty($sd['cover']))<img src="{{ $sd['cover'] }}" loading="lazy" alt="">@else<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.2);font-size:1.4rem"><i class="bi bi-play-circle"></i></div>@endif
|
||||
</div>
|
||||
<div>
|
||||
<div class="share-card-label"><i class="bi bi-play-fill"></i> Anime Bölümü</div>
|
||||
<div class="share-card-title">{{ $sd['title'] ?? '' }}</div>
|
||||
<div class="share-card-ep">{{ $sd['s'] ?? '' }}. Sezon {{ $sd['e'] ?? '' }}. Bölüm</div>
|
||||
<div class="share-card-play"><i class="bi bi-play-circle-fill"></i> İzle</div>
|
||||
</div>
|
||||
</a>
|
||||
@else
|
||||
<div class="chat-bubble">{{ $msg->body }}</div>
|
||||
@endif
|
||||
@elseif(str_starts_with($msg->body, 'IMAGE::'))
|
||||
<img src="{{ substr($msg->body, 7) }}" class="chat-img" loading="lazy" alt="Resim"
|
||||
onclick="window.open('{{ substr($msg->body, 7) }}','_blank')">
|
||||
@elseif(str_starts_with($msg->body, 'GIF::'))
|
||||
<img src="{{ substr($msg->body, 5) }}" class="chat-gif" loading="lazy" alt="GIF">
|
||||
@else
|
||||
<div class="chat-bubble">{{ $msg->body }}</div>
|
||||
@endif
|
||||
</div>
|
||||
<span class="chat-time">{{ $msg->created_at->format('H:i') }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Footer --}}
|
||||
<div class="chat-footer">
|
||||
<div id="chat-emoji-picker"></div>
|
||||
<div id="chat-gif-picker">
|
||||
<input type="text" class="chat-gif-search" id="chat-gif-input" placeholder="GIF ara…">
|
||||
<div class="chat-gif-grid" id="chat-gif-grid">
|
||||
<div class="chat-gif-ph">GIF aramak için yaz</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-toolbar">
|
||||
<button class="chat-tool-btn" id="chat-emoji-btn" title="Emoji" style="display:none">
|
||||
<i class="bi bi-emoji-smile"></i>
|
||||
</button>
|
||||
<button class="chat-tool-btn" id="chat-gif-btn" title="GIF">
|
||||
<span style="font-size:.65rem;font-weight:800;letter-spacing:.03em">GIF</span>
|
||||
</button>
|
||||
<label class="chat-tool-btn" title="Resim gönder" style="cursor:pointer">
|
||||
<i class="bi bi-image"></i>
|
||||
<input type="file" id="chat-img-input" accept="image/*" style="display:none">
|
||||
</label>
|
||||
<div style="flex:1"></div>
|
||||
</div>
|
||||
<div class="chat-form">
|
||||
<textarea class="chat-input" id="chat-input"
|
||||
placeholder="Mesaj yaz..." rows="1" maxlength="2000"
|
||||
autocomplete="off" autocorrect="off"></textarea>
|
||||
<button type="button" class="chat-send" id="chat-send">
|
||||
<i class="bi bi-send-fill"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Crisp ve diğer overlay widgetları kapat
|
||||
try { $crisp.push(['do','chat:hide']); } catch(_){}
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
try { $crisp.push(['do','chat:hide']); } catch(_){}
|
||||
// topnav/bottom-nav kesinlikle gizle (layout JS override için)
|
||||
const tn = document.getElementById('topnav');
|
||||
const bn = document.getElementById('mob-bottom-nav');
|
||||
if(tn) tn.style.setProperty('display','none','important');
|
||||
if(bn) bn.style.setProperty('display','none','important');
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
(function(){
|
||||
'use strict';
|
||||
|
||||
const SEND_URL = '{{ route("messages.send", $conversation) }}';
|
||||
const POLL_URL = '{{ route("messages.poll", $conversation) }}';
|
||||
const UPLOAD_URL = '{{ route("messages.upload-image") }}';
|
||||
const TENOR_KEY = '{{ env("TENOR_API_KEY","") }}';
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content || '';
|
||||
const ME_ID = {{ $me->id }};
|
||||
const MY_AVA = {!! json_encode($me->avatar ? \App\Support\MediaUrl::fromStoragePath($me->avatar) : null) !!};
|
||||
const MY_NAME = {!! json_encode($me->name) !!};
|
||||
const IS_DESKTOP = window.innerWidth >= 640;
|
||||
|
||||
let lastId = {{ $messages->last()?->id ?? 0 }};
|
||||
let polling = null;
|
||||
let emojiOpen = false;
|
||||
let gifOpen = false;
|
||||
let gifTimer = null;
|
||||
|
||||
const chatBody = document.getElementById('chat-body');
|
||||
const inputEl = document.getElementById('chat-input');
|
||||
const sendBtn = document.getElementById('chat-send');
|
||||
|
||||
/* ── Scroll ──────────────────────────────── */
|
||||
function scrollBottom(force = false) {
|
||||
const near = chatBody.scrollHeight - chatBody.scrollTop - chatBody.clientHeight < 160;
|
||||
if (force || near) chatBody.scrollTop = chatBody.scrollHeight;
|
||||
}
|
||||
scrollBottom(true);
|
||||
|
||||
/* ── Input auto-grow ─────────────────────── */
|
||||
inputEl.addEventListener('input', () => {
|
||||
inputEl.style.height = 'auto';
|
||||
inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
|
||||
if (inputEl.value) { hideEmoji(); hideGif(); }
|
||||
});
|
||||
inputEl.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); doSend(); }
|
||||
});
|
||||
sendBtn.addEventListener('click', doSend);
|
||||
|
||||
/* ── Escape chars ────────────────────────── */
|
||||
function escH(s) {
|
||||
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
/* ── Render message body ─────────────────── */
|
||||
function renderBody(body) {
|
||||
if (!body) return '<div class="chat-bubble"></div>';
|
||||
if (body.startsWith('ANIMESHARE::')) {
|
||||
try {
|
||||
const d = JSON.parse(body.slice(12));
|
||||
const url = `/watch/${d.slug}/${d.s}/${d.e}${d.ts>0?'?t='+d.ts:''}`;
|
||||
const img = d.cover
|
||||
? `<img src="${escH(d.cover)}" loading="lazy" alt="">`
|
||||
: `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.2)"><i class="bi bi-play-circle" style="font-size:1.4rem"></i></div>`;
|
||||
return `<a href="${escH(url)}" class="share-card">
|
||||
<div class="share-card-img">${img}</div>
|
||||
<div>
|
||||
<div class="share-card-label"><i class="bi bi-play-fill"></i> Anime Bölümü</div>
|
||||
<div class="share-card-title">${escH(d.title||'')}</div>
|
||||
<div class="share-card-ep">${escH(String(d.s))}. Sezon ${escH(String(d.e))}. Bölüm</div>
|
||||
<div class="share-card-play"><i class="bi bi-play-circle-fill"></i> İzle</div>
|
||||
</div>
|
||||
</a>`;
|
||||
} catch(_) {}
|
||||
}
|
||||
if (body.startsWith('IMAGE::')) {
|
||||
const url = escH(body.slice(7));
|
||||
return `<img src="${url}" class="chat-img" loading="lazy" alt="Resim" onclick="window.open('${url}','_blank')">`;
|
||||
}
|
||||
if (body.startsWith('GIF::')) {
|
||||
return `<img src="${escH(body.slice(5))}" class="chat-gif" loading="lazy" alt="GIF">`;
|
||||
}
|
||||
return `<div class="chat-bubble">${escH(body)}</div>`;
|
||||
}
|
||||
|
||||
/* ── Append message to DOM ───────────────── */
|
||||
function appendMsg(id, body, isMine, time, avatar, name) {
|
||||
if (document.querySelector(`.chat-msg[data-id="${id}"]`)) return;
|
||||
document.querySelector('.chat-empty')?.remove();
|
||||
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'chat-msg ' + (isMine ? 'mine' : 'theirs');
|
||||
wrap.dataset.id = id;
|
||||
|
||||
let ava = '';
|
||||
if (!isMine) {
|
||||
ava = avatar
|
||||
? `<div class="chat-msg-ava"><img src="${escH(avatar)}" alt=""></div>`
|
||||
: `<div class="chat-msg-ava">${escH((name||'?').charAt(0).toUpperCase())}</div>`;
|
||||
}
|
||||
wrap.innerHTML = ava + `<div>${renderBody(body)}</div><span class="chat-time">${escH(time)}</span>`;
|
||||
chatBody.appendChild(wrap);
|
||||
scrollBottom();
|
||||
}
|
||||
|
||||
/* ── Send text ───────────────────────────── */
|
||||
async function doSend() {
|
||||
const text = inputEl.value.trim();
|
||||
if (!text) return;
|
||||
sendBtn.disabled = true;
|
||||
inputEl.value = '';
|
||||
inputEl.style.height = 'auto';
|
||||
|
||||
try {
|
||||
const res = await fetch(SEND_URL, {
|
||||
method: 'POST',
|
||||
headers: {'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ body: text }),
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const d = await res.json();
|
||||
appendMsg(d.id, d.body, true, d.created_at, MY_AVA, MY_NAME);
|
||||
if (d.id > lastId) lastId = d.id;
|
||||
scrollBottom(true);
|
||||
} catch(e) {
|
||||
inputEl.value = text;
|
||||
}
|
||||
sendBtn.disabled = false;
|
||||
inputEl.focus();
|
||||
}
|
||||
|
||||
/* ── Send image ──────────────────────────── */
|
||||
document.getElementById('chat-img-input')?.addEventListener('change', async function() {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
this.value = '';
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('image', file);
|
||||
const r = await fetch(UPLOAD_URL, {
|
||||
method: 'POST',
|
||||
headers: {'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json'},
|
||||
body: fd,
|
||||
});
|
||||
const d = await r.json();
|
||||
if (!d.url) throw new Error();
|
||||
const r2 = await fetch(SEND_URL, {
|
||||
method: 'POST',
|
||||
headers: {'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ body: 'IMAGE::' + d.url }),
|
||||
});
|
||||
const d2 = await r2.json();
|
||||
appendMsg(d2.id, d2.body, true, d2.created_at, MY_AVA, MY_NAME);
|
||||
if (d2.id > lastId) lastId = d2.id;
|
||||
scrollBottom(true);
|
||||
} catch(e) {
|
||||
if (window.showToast) showToast('Resim gönderilemedi.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
/* ── Send GIF ────────────────────────────── */
|
||||
window.chatSendGif = async function(gifUrl) {
|
||||
hideGif();
|
||||
try {
|
||||
const r = await fetch(SEND_URL, {
|
||||
method: 'POST',
|
||||
headers: {'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ body: 'GIF::' + gifUrl }),
|
||||
});
|
||||
const d = await r.json();
|
||||
appendMsg(d.id, d.body, true, d.created_at, MY_AVA, MY_NAME);
|
||||
if (d.id > lastId) lastId = d.id;
|
||||
scrollBottom(true);
|
||||
} catch(_) {}
|
||||
};
|
||||
|
||||
/* ── Polling ─────────────────────────────── */
|
||||
async function poll() {
|
||||
try {
|
||||
const res = await fetch(`${POLL_URL}?after=${lastId}`, {
|
||||
headers: {'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF}
|
||||
});
|
||||
const d = await res.json();
|
||||
d.messages.forEach(m => {
|
||||
if (m.user_id !== ME_ID) {
|
||||
appendMsg(m.id, m.body, false, m.created_at, m.avatar, m.name);
|
||||
}
|
||||
if (m.id > lastId) lastId = m.id;
|
||||
});
|
||||
} catch(_) {}
|
||||
}
|
||||
|
||||
function startPoll() { polling = setInterval(poll, 3000); }
|
||||
function stopPoll() { clearInterval(polling); polling = null; }
|
||||
document.addEventListener('visibilitychange', () => document.hidden ? stopPoll() : startPoll());
|
||||
startPoll();
|
||||
|
||||
/* ── Emoji picker ────────────────────────── */
|
||||
const EMOJIS = '😀😂😅😊😍🥰😘😎😭😤😡🥺🤔😴🤩🥳😱🙈🙄😏🤗😬😵👍👎❤️🔥💯✨🎉🎊🎮🎬🎵🍕🍜🍣🎃👻🦊🐱🐶🐼🌸🌙⭐🌈💎🚀🏆🎯🔮';
|
||||
|
||||
function initEmoji() {
|
||||
const p = document.getElementById('chat-emoji-picker');
|
||||
if (!p || p.dataset.built) return;
|
||||
p.dataset.built = '1';
|
||||
[...EMOJIS].forEach(em => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'chat-emoji-btn';
|
||||
btn.textContent = em;
|
||||
btn.addEventListener('click', () => {
|
||||
const pos = inputEl.selectionStart;
|
||||
inputEl.value = inputEl.value.slice(0, pos) + em + inputEl.value.slice(inputEl.selectionEnd);
|
||||
inputEl.selectionStart = inputEl.selectionEnd = pos + em.length;
|
||||
inputEl.dispatchEvent(new Event('input'));
|
||||
inputEl.focus();
|
||||
});
|
||||
p.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleEmoji() {
|
||||
emojiOpen = !emojiOpen;
|
||||
if (emojiOpen) { hideGif(); initEmoji(); }
|
||||
document.getElementById('chat-emoji-picker').classList.toggle('show', emojiOpen);
|
||||
document.getElementById('chat-emoji-btn').classList.toggle('active', emojiOpen);
|
||||
}
|
||||
function hideEmoji() {
|
||||
emojiOpen = false;
|
||||
document.getElementById('chat-emoji-picker')?.classList.remove('show');
|
||||
document.getElementById('chat-emoji-btn')?.classList.remove('active');
|
||||
}
|
||||
|
||||
document.getElementById('chat-emoji-btn')?.addEventListener('click', toggleEmoji);
|
||||
|
||||
// Desktop'ta emoji butonu göster
|
||||
if (IS_DESKTOP) {
|
||||
const eb = document.getElementById('chat-emoji-btn');
|
||||
if (eb) eb.style.display = '';
|
||||
}
|
||||
|
||||
/* ── GIF picker ──────────────────────────── */
|
||||
async function searchGifs(q) {
|
||||
const grid = document.getElementById('chat-gif-grid');
|
||||
if (!grid) return;
|
||||
if (!TENOR_KEY) {
|
||||
grid.innerHTML = '<div class="chat-gif-ph">Tenor API key ayarlanmamış.</div>';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = '<div class="chat-gif-ph">Yükleniyor…</div>';
|
||||
try {
|
||||
const ep = q
|
||||
? `https://tenor.googleapis.com/v2/search?q=${encodeURIComponent(q)}&key=${TENOR_KEY}&limit=12&media_filter=gif`
|
||||
: `https://tenor.googleapis.com/v2/featured?key=${TENOR_KEY}&limit=12&media_filter=gif`;
|
||||
const r = await fetch(ep);
|
||||
const d = await r.json();
|
||||
const results = d.results || [];
|
||||
if (!results.length) { grid.innerHTML = '<div class="chat-gif-ph">Sonuç yok.</div>'; return; }
|
||||
grid.innerHTML = '';
|
||||
results.forEach(g => {
|
||||
const url = g.media_formats?.tinygif?.url || g.media_formats?.gif?.url || '';
|
||||
if (!url) return;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'chat-gif-item';
|
||||
item.innerHTML = `<img src="${escH(url)}" alt="" loading="lazy">`;
|
||||
item.addEventListener('click', () => window.chatSendGif(url));
|
||||
grid.appendChild(item);
|
||||
});
|
||||
} catch(_) { grid.innerHTML = '<div class="chat-gif-ph">Yüklenemedi.</div>'; }
|
||||
}
|
||||
|
||||
function toggleGif() {
|
||||
gifOpen = !gifOpen;
|
||||
if (gifOpen) { hideEmoji(); if (TENOR_KEY) searchGifs(''); setTimeout(() => document.getElementById('chat-gif-input')?.focus(), 80); }
|
||||
document.getElementById('chat-gif-picker').classList.toggle('show', gifOpen);
|
||||
document.getElementById('chat-gif-btn').classList.toggle('active', gifOpen);
|
||||
}
|
||||
function hideGif() {
|
||||
gifOpen = false;
|
||||
document.getElementById('chat-gif-picker')?.classList.remove('show');
|
||||
document.getElementById('chat-gif-btn')?.classList.remove('active');
|
||||
}
|
||||
|
||||
document.getElementById('chat-gif-btn')?.addEventListener('click', toggleGif);
|
||||
document.getElementById('chat-gif-input')?.addEventListener('input', function() {
|
||||
clearTimeout(gifTimer);
|
||||
gifTimer = setTimeout(() => searchGifs(this.value.trim()), 380);
|
||||
});
|
||||
|
||||
/* ── Sesli arama ─────────────────────────── */
|
||||
document.getElementById('call-btn')?.addEventListener('click', async function() {
|
||||
const userId = this.dataset.userId;
|
||||
if (!userId || !window.cpStartCall) { if (window.showToast) showToast('Arama özelliği yüklenemedi.','error'); return; }
|
||||
window.cpConvUserId = userId;
|
||||
window.cpStartCall();
|
||||
});
|
||||
|
||||
/* ── Auto focus ──────────────────────────── */
|
||||
if (IS_DESKTOP) inputEl.focus();
|
||||
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,82 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Bildirimler — Animexe')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.notif-wrap{max-width:700px;margin:0 auto;padding:80px 20px 60px}
|
||||
.notif-hero{margin-bottom:28px}
|
||||
.notif-hero h1{font-size:1.5rem;font-weight:800;color:var(--text);display:flex;align-items:center;gap:10px}
|
||||
.notif-hero p{color:var(--text3);font-size:.88rem;margin-top:4px}
|
||||
|
||||
.notif-card{
|
||||
background:var(--surface);border:1px solid var(--border);border-radius:12px;
|
||||
padding:16px 18px;margin-bottom:10px;display:flex;align-items:flex-start;gap:14px;
|
||||
transition:border-color .15s,background .15s;
|
||||
}
|
||||
.notif-card:hover{border-color:rgba(0,245,255,.25);background:rgba(0,245,255,.02)}
|
||||
.notif-card.unread{border-left:3px solid var(--accent2);background:rgba(0,245,255,.03)}
|
||||
.notif-icon{
|
||||
width:40px;height:40px;border-radius:50%;background:rgba(0,245,255,.1);border:1px solid rgba(0,245,255,.2);
|
||||
display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:1.1rem;color:var(--accent2);
|
||||
}
|
||||
.notif-body{flex:1;min-width:0}
|
||||
.notif-title{font-size:.88rem;font-weight:600;color:var(--text);margin-bottom:3px}
|
||||
.notif-sub{font-size:.78rem;color:var(--text2);line-height:1.5}
|
||||
.notif-time{font-size:.68rem;color:var(--text3);margin-top:5px}
|
||||
.notif-empty{text-align:center;padding:60px 20px;color:var(--text3)}
|
||||
.notif-empty i{font-size:2.5rem;display:block;margin-bottom:12px;opacity:.3}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="notif-wrap">
|
||||
<div class="notif-hero">
|
||||
<h1><i class="bi bi-bell-fill" style="color:var(--accent2)"></i> Bildirimler</h1>
|
||||
<p>Takip ettiğin animelerin yeni bölüm bildirimleri burada görünür.</p>
|
||||
</div>
|
||||
|
||||
@if($notifications->isEmpty())
|
||||
<div class="notif-empty">
|
||||
<i class="bi bi-bell-slash"></i>
|
||||
<div>Henüz bildiriminiz yok.</div>
|
||||
<div style="font-size:.8rem;margin-top:6px">Anime sayfalarından animeleri takip edin, yeni bölüm eklenince bilgilendirilirsiniz.</div>
|
||||
</div>
|
||||
@else
|
||||
@foreach($notifications as $n)
|
||||
@php
|
||||
$d = $n->data;
|
||||
$isEp = $n->type === 'episode';
|
||||
$isAdmin = $n->type === 'admin';
|
||||
$url = $isEp && isset($d['anime_slug'])
|
||||
? route('watch', [$d['anime_slug'], $d['season_number'] ?? 1, $d['episode_number'] ?? 1])
|
||||
: ($isAdmin && !empty($d['url']) ? $d['url'] : '#');
|
||||
@endphp
|
||||
<a href="{{ $url }}" style="text-decoration:none;display:block">
|
||||
<div class="notif-card {{ !$n->is_read ? 'unread' : '' }}">
|
||||
<div class="notif-icon" style="{{ $isAdmin ? 'background:rgba(255,45,125,.1);border-color:rgba(255,45,125,.25);color:var(--accent)' : '' }}">
|
||||
<i class="bi {{ $isAdmin ? ($d['icon'] ?? 'bi-megaphone-fill') : ($isEp ? 'bi-play-circle-fill' : 'bi-bell-fill') }}"></i>
|
||||
</div>
|
||||
<div class="notif-body">
|
||||
@if($isEp)
|
||||
<div class="notif-title">{{ $d['anime_title'] ?? 'Anime' }} — Yeni Bölüm!</div>
|
||||
<div class="notif-sub">
|
||||
Sezon {{ $d['season_number'] ?? 1 }}, {{ $d['episode_number'] ?? 1 }}. Bölüm eklendi
|
||||
@if(!empty($d['episode_title'])) — <em>{{ $d['episode_title'] }}</em>@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="notif-title">{{ $d['title'] ?? 'Bildirim' }}</div>
|
||||
@if(!empty($d['body']))<div class="notif-sub">{{ $d['body'] }}</div>@endif
|
||||
@endif
|
||||
<div class="notif-time">{{ $n->created_at ? \Carbon\Carbon::parse($n->created_at)->diffForHumans() : '' }}</div>
|
||||
</div>
|
||||
@if(!$n->is_read)
|
||||
<div style="width:8px;height:8px;border-radius:50%;background:var(--accent2);flex-shrink:0;margin-top:4px"></div>
|
||||
@endif
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
|
||||
<div class="mt-4">{{ $notifications->links() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,70 @@
|
||||
@if ($paginator->hasPages())
|
||||
<style>
|
||||
.pag{display:flex;align-items:center;justify-content:center;gap:6px;flex-wrap:wrap;padding:8px 0}
|
||||
.pag-btn{
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
min-width:40px;height:40px;padding:0 6px;border-radius:10px;
|
||||
font-size:.85rem;font-weight:600;text-decoration:none;
|
||||
border:1px solid rgba(0,245,255,.12);
|
||||
background:rgba(17,17,31,.85);
|
||||
color:var(--text2,#a0a8c0);
|
||||
transition:background .15s,border-color .15s,color .15s,transform .12s;
|
||||
cursor:pointer;
|
||||
}
|
||||
.pag-btn:hover{
|
||||
background:rgba(0,245,255,.1);
|
||||
border-color:rgba(0,245,255,.35);
|
||||
color:#fff;
|
||||
transform:translateY(-1px);
|
||||
}
|
||||
.pag-btn.active{
|
||||
background:linear-gradient(135deg,var(--accent,#ff2d7d),var(--accent3,#b84dff));
|
||||
border-color:transparent;
|
||||
color:#fff;
|
||||
font-weight:800;
|
||||
box-shadow:0 0 18px rgba(255,45,125,.35);
|
||||
cursor:default;
|
||||
pointer-events:none;
|
||||
}
|
||||
.pag-btn.disabled{
|
||||
opacity:.3;cursor:default;pointer-events:none;
|
||||
}
|
||||
.pag-dots{
|
||||
display:inline-flex;align-items:center;justify-content:center;
|
||||
min-width:40px;height:40px;color:var(--text3,#555);font-size:.9rem;
|
||||
letter-spacing:.1em;
|
||||
}
|
||||
.pag-arrow{font-size:1rem}
|
||||
</style>
|
||||
<nav class="pag" aria-label="Sayfalama">
|
||||
{{-- Önceki --}}
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="pag-btn disabled pag-arrow">‹</span>
|
||||
@else
|
||||
<a class="pag-btn pag-arrow" href="{{ $paginator->previousPageUrl() }}" rel="prev" aria-label="Önceki sayfa">‹</a>
|
||||
@endif
|
||||
|
||||
{{-- Sayfa numaraları --}}
|
||||
@foreach ($elements as $element)
|
||||
@if (is_string($element))
|
||||
<span class="pag-dots">···</span>
|
||||
@endif
|
||||
@if (is_array($element))
|
||||
@foreach ($element as $page => $url)
|
||||
@if ($page == $paginator->currentPage())
|
||||
<span class="pag-btn active" aria-current="page">{{ $page }}</span>
|
||||
@else
|
||||
<a class="pag-btn" href="{{ $url }}">{{ $page }}</a>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- Sonraki --}}
|
||||
@if ($paginator->hasMorePages())
|
||||
<a class="pag-btn pag-arrow" href="{{ $paginator->nextPageUrl() }}" rel="next" aria-label="Sonraki sayfa">›</a>
|
||||
@else
|
||||
<span class="pag-btn disabled pag-arrow">›</span>
|
||||
@endif
|
||||
</nav>
|
||||
@endif
|
||||
@@ -0,0 +1,49 @@
|
||||
{{-- Sade banner reklam — $ad: App\Models\Ad
|
||||
NOT: class/attribute isimleri bilerek nötr ("ad"/"banner" içermez) —
|
||||
adblocker'lar .ad-banner / [data-ad-id] gibi seçicileri cosmetic filter ile gizliyor.
|
||||
Ayrıca .home-section KULLANMA — o class'ta opacity:0 (scroll reveal) var. --}}
|
||||
@if($ad && $ad->media_url)
|
||||
|
||||
@once
|
||||
<style>
|
||||
.hp-feature-slot{
|
||||
max-width:1400px;
|
||||
margin:0 auto;
|
||||
padding:32px clamp(16px,4vw,48px) 40px;
|
||||
position:relative;
|
||||
}
|
||||
.hp-feature-inner{max-width:970px;margin:0 auto}
|
||||
.hp-feature-label{
|
||||
font-size:10px;letter-spacing:.14em;text-transform:uppercase;
|
||||
color:rgba(255,255,255,.28);margin-bottom:7px;text-align:center;
|
||||
}
|
||||
.hp-feature-media{
|
||||
display:block;border-radius:12px;overflow:hidden;
|
||||
border:1px solid rgba(255,255,255,.06);
|
||||
background:rgba(255,255,255,.02);
|
||||
line-height:0;
|
||||
}
|
||||
.hp-feature-media img{width:100%;display:block;max-height:140px;object-fit:cover}
|
||||
@media(max-width:640px){
|
||||
.hp-feature-slot{padding:24px 16px 28px}
|
||||
.hp-feature-media img{max-height:96px}
|
||||
}
|
||||
</style>
|
||||
@endonce
|
||||
|
||||
<div class="hp-feature-slot">
|
||||
<div class="hp-feature-inner">
|
||||
<div class="hp-feature-label">Sponsorlu</div>
|
||||
@if($ad->click_url)
|
||||
<a href="{{ $ad->click_url }}" target="_blank" rel="noopener sponsored"
|
||||
class="hp-feature-media" data-slot-id="{{ $ad->id }}">
|
||||
<img src="{{ $ad->media_url }}" alt="{{ $ad->name }}" loading="lazy">
|
||||
</a>
|
||||
@else
|
||||
<div class="hp-feature-media" data-slot-id="{{ $ad->id }}">
|
||||
<img src="{{ $ad->media_url }}" alt="{{ $ad->name }}" loading="lazy">
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Aktivasyon Kodu Gir — Animexe Premium')
|
||||
@push('styles')
|
||||
<style>
|
||||
#pg{background:#06060d;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:80px 20px 60px}
|
||||
.act-card{
|
||||
width:100%;max-width:480px;
|
||||
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.09);
|
||||
border-radius:24px;padding:44px 40px 40px;position:relative;overflow:hidden;
|
||||
}
|
||||
.act-card::before{
|
||||
content:'';position:absolute;top:-80px;left:50%;transform:translateX(-50%);
|
||||
width:280px;height:200px;border-radius:50%;
|
||||
background:radial-gradient(rgba(0,245,255,.1),transparent 70%);
|
||||
pointer-events:none;
|
||||
}
|
||||
.act-icon{
|
||||
width:64px;height:64px;border-radius:18px;margin:0 auto 20px;
|
||||
background:linear-gradient(135deg,rgba(0,245,255,.15),rgba(184,77,255,.12));
|
||||
border:1px solid rgba(0,245,255,.2);
|
||||
display:flex;align-items:center;justify-content:center;font-size:1.8rem;color:#00f5ff;
|
||||
position:relative;z-index:1;
|
||||
}
|
||||
.act-title{font-size:1.4rem;font-weight:900;color:#fff;text-align:center;margin-bottom:6px;position:relative;z-index:1}
|
||||
.act-sub{font-size:.85rem;color:rgba(255,255,255,.4);text-align:center;margin-bottom:32px;line-height:1.6;position:relative;z-index:1}
|
||||
.act-label{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.4);margin-bottom:8px}
|
||||
.act-input{
|
||||
width:100%;background:rgba(255,255,255,.06);border:1.5px solid rgba(0,245,255,.2);
|
||||
border-radius:14px;padding:16px 20px;color:#fff;font-size:1.1rem;font-weight:800;
|
||||
letter-spacing:.18em;text-align:center;text-transform:uppercase;outline:none;
|
||||
font-family:inherit;transition:border-color .2s,box-shadow .2s;box-sizing:border-box;
|
||||
}
|
||||
.act-input::placeholder{letter-spacing:.05em;font-weight:400;text-transform:none;font-size:.9rem;color:rgba(255,255,255,.25)}
|
||||
.act-input:focus{border-color:#00f5ff;box-shadow:0 0 0 4px rgba(0,245,255,.1)}
|
||||
.act-input.is-invalid{border-color:#ff2d7d!important;box-shadow:0 0 0 4px rgba(255,45,125,.1)!important}
|
||||
.act-error{
|
||||
display:flex;align-items:center;gap:7px;
|
||||
padding:11px 14px;border-radius:10px;margin-top:10px;
|
||||
background:rgba(255,45,125,.08);border:1px solid rgba(255,45,125,.2);
|
||||
font-size:.82rem;color:#ff2d7d;font-weight:600;
|
||||
}
|
||||
.act-btn{
|
||||
width:100%;padding:15px;border-radius:14px;border:none;cursor:pointer;font-family:inherit;
|
||||
font-size:.95rem;font-weight:800;letter-spacing:.03em;margin-top:18px;
|
||||
background:linear-gradient(135deg,#00f5ff 0%,#0080ff 60%,#b84dff 100%);
|
||||
color:#06060d;box-shadow:0 0 30px rgba(0,245,255,.2),0 6px 20px rgba(0,0,0,.4);
|
||||
transition:transform .15s,box-shadow .15s;
|
||||
}
|
||||
.act-btn:hover{transform:translateY(-2px);box-shadow:0 0 40px rgba(0,245,255,.35),0 8px 28px rgba(0,0,0,.5)}
|
||||
.act-divider{display:flex;align-items:center;gap:12px;margin:22px 0;color:rgba(255,255,255,.18);font-size:.75rem}
|
||||
.act-divider::before,.act-divider::after{content:'';flex:1;height:1px;background:rgba(255,255,255,.08)}
|
||||
.act-back{
|
||||
display:flex;align-items:center;justify-content:center;gap:7px;
|
||||
padding:12px;border-radius:12px;border:1px solid rgba(255,255,255,.1);
|
||||
background:rgba(255,255,255,.04);color:rgba(255,255,255,.5);
|
||||
font-size:.84rem;font-weight:700;text-decoration:none;transition:all .15s;
|
||||
}
|
||||
.act-back:hover{border-color:rgba(255,255,255,.2);color:#fff;background:rgba(255,255,255,.08)}
|
||||
.act-hint{
|
||||
margin-top:20px;padding:12px 16px;border-radius:10px;
|
||||
background:rgba(0,245,255,.04);border:1px solid rgba(0,245,255,.1);
|
||||
font-size:.76rem;color:rgba(255,255,255,.35);line-height:1.55;text-align:center;
|
||||
}
|
||||
.act-hint strong{color:rgba(0,245,255,.7)}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div id="pg">
|
||||
<div class="act-card">
|
||||
<div class="act-icon"><i class="bi bi-key-fill"></i></div>
|
||||
<h1 class="act-title">Aktivasyon Kodu Gir</h1>
|
||||
<p class="act-sub">Satın alma sonrası aldığın kodu girerek Premium üyeliğini hemen aktifleştir.</p>
|
||||
|
||||
@guest
|
||||
<div style="text-align:center;padding:8px 0 16px">
|
||||
<a href="{{ route('frontend.login') }}?next={{ urlencode(route('premium.activate')) }}"
|
||||
style="display:inline-flex;align-items:center;gap:8px;padding:13px 28px;border-radius:12px;background:linear-gradient(135deg,#00f5ff,#b84dff);color:#06060d;font-weight:800;font-size:.9rem;text-decoration:none">
|
||||
<i class="bi bi-box-arrow-in-right"></i> Giriş Yap
|
||||
</a>
|
||||
<div style="margin-top:12px;font-size:.78rem;color:rgba(255,255,255,.3)">
|
||||
Hesabın yoksa <a href="{{ route('frontend.register') }}" style="color:#00f5ff;font-weight:700">kayıt ol</a>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<form method="POST" action="{{ route('premium.activate.redeem') }}">
|
||||
@csrf
|
||||
<div class="act-label">Aktivasyon Kodu</div>
|
||||
<input type="text" name="code" id="act-inp"
|
||||
class="act-input {{ $errors->has('code') ? 'is-invalid' : '' }}"
|
||||
placeholder="XXXX-XXXX-XXXX"
|
||||
value="{{ old('code') }}"
|
||||
maxlength="32" autocomplete="off" autocapitalize="characters" spellcheck="false"
|
||||
autofocus>
|
||||
@if($errors->has('code'))
|
||||
<div class="act-error"><i class="bi bi-exclamation-circle-fill"></i> {{ $errors->first('code') }}</div>
|
||||
@endif
|
||||
<button type="submit" class="act-btn">
|
||||
<i class="bi bi-lightning-charge-fill me-1"></i> Aktifleştir
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="act-hint">
|
||||
Kod formatı: <strong>XXXX-XXXX-XXXX</strong> (büyük harf + tire)<br>
|
||||
Kodu doğru girdiğinden emin ol. Büyük/küçük harf fark etmez.
|
||||
</div>
|
||||
@endguest
|
||||
|
||||
<div class="act-divider">veya</div>
|
||||
<a href="{{ route('premium.plans') }}" class="act-back">
|
||||
<i class="bi bi-arrow-left"></i> Premium Planlarına Dön
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@auth
|
||||
<script>
|
||||
(function(){
|
||||
const inp = document.getElementById('act-inp');
|
||||
if(!inp) return;
|
||||
inp.addEventListener('input', function(){
|
||||
let v = this.value.replace(/[^A-Za-z0-9]/g,'').toUpperCase();
|
||||
const parts = v.match(/.{1,4}/g) || [];
|
||||
this.value = parts.join('-').slice(0, 14);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endauth
|
||||
@endsection
|
||||
@@ -0,0 +1,662 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Animexe Premium — Anime Deneyimini Zirveye Taşı')
|
||||
@section('meta_description', 'Animexe Premium ile reklamsız izle, özel kozmotikler kazan, erken erişim fırsatlarından yararlan.')
|
||||
@push('styles')
|
||||
<style>
|
||||
:root{--prm-cyan:#00f5ff;--prm-pink:#ff2d7d;--prm-purple:#b84dff;--prm-gold:#ffd60a;--prm-bg:#06060d}
|
||||
|
||||
/* ── Genel ── */
|
||||
#pg{background:var(--prm-bg);min-height:100vh}
|
||||
.prm-wrap{max-width:1160px;margin:0 auto;padding:0 20px 80px}
|
||||
|
||||
/* ── Hero ── */
|
||||
.prm-hero{
|
||||
position:relative;text-align:center;
|
||||
padding:100px 20px 80px;overflow:hidden;
|
||||
}
|
||||
.prm-hero-bg{
|
||||
position:absolute;inset:0;z-index:0;
|
||||
background:radial-gradient(ellipse 80% 60% at 50% 0%,rgba(0,245,255,.09) 0%,transparent 70%),
|
||||
radial-gradient(ellipse 50% 40% at 80% 30%,rgba(184,77,255,.07) 0%,transparent 60%),
|
||||
radial-gradient(ellipse 40% 30% at 20% 60%,rgba(255,45,125,.06) 0%,transparent 60%);
|
||||
}
|
||||
/* Yıldız parçacıkları */
|
||||
.prm-stars{position:absolute;inset:0;z-index:0;pointer-events:none}
|
||||
.prm-stars span{
|
||||
position:absolute;width:2px;height:2px;border-radius:50%;
|
||||
background:#fff;animation:twinkle var(--d,3s) ease-in-out infinite var(--delay,0s);
|
||||
opacity:0;
|
||||
}
|
||||
@keyframes twinkle{0%,100%{opacity:0;transform:scale(1)}50%{opacity:.7;transform:scale(1.4)}}
|
||||
|
||||
.prm-hero-inner{position:relative;z-index:1}
|
||||
.prm-hero-badge{
|
||||
display:inline-flex;align-items:center;gap:7px;
|
||||
padding:6px 16px;border-radius:30px;
|
||||
background:rgba(0,245,255,.08);border:1px solid rgba(0,245,255,.25);
|
||||
font-size:.75rem;font-weight:800;letter-spacing:.1em;text-transform:uppercase;
|
||||
color:var(--prm-cyan);margin-bottom:24px;
|
||||
}
|
||||
.prm-hero-badge i{font-size:.85rem}
|
||||
.prm-hero-title{
|
||||
font-size:clamp(2.2rem,5vw,3.8rem);font-weight:900;line-height:1.15;
|
||||
margin-bottom:16px;letter-spacing:-.02em;
|
||||
}
|
||||
.prm-hero-title .grad{
|
||||
background:linear-gradient(120deg,var(--prm-cyan) 0%,var(--prm-purple) 50%,var(--prm-pink) 100%);
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
|
||||
}
|
||||
.prm-hero-sub{font-size:1.08rem;color:rgba(255,255,255,.5);max-width:560px;margin:0 auto 40px;line-height:1.65}
|
||||
.prm-hero-stats{display:flex;gap:32px;justify-content:center;flex-wrap:wrap;margin-bottom:8px}
|
||||
.prm-stat{display:flex;flex-direction:column;align-items:center;gap:2px}
|
||||
.prm-stat-val{font-size:1.5rem;font-weight:900;color:#fff;line-height:1}
|
||||
.prm-stat-lbl{font-size:.7rem;color:rgba(255,255,255,.35);text-transform:uppercase;letter-spacing:.08em}
|
||||
|
||||
/* ── Aktivasyon başarı banner ── */
|
||||
.prm-success-banner{
|
||||
display:flex;align-items:center;gap:14px;
|
||||
padding:16px 22px;border-radius:14px;margin-bottom:32px;
|
||||
background:rgba(50,220,100,.08);border:1px solid rgba(50,220,100,.3);
|
||||
animation:slideDown .4s ease;
|
||||
}
|
||||
@keyframes slideDown{from{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:none}}
|
||||
.prm-success-banner i{font-size:1.6rem;color:#32dc64;flex-shrink:0}
|
||||
.prm-success-banner strong{color:#32dc64;font-size:.95rem;display:block;margin-bottom:2px}
|
||||
.prm-success-banner span{font-size:.8rem;color:rgba(255,255,255,.5)}
|
||||
|
||||
/* ── Planlar grid ── */
|
||||
.prm-section-lbl{
|
||||
text-align:center;margin-bottom:48px;
|
||||
}
|
||||
.prm-section-lbl h2{font-size:1.6rem;font-weight:800;color:#fff;margin-bottom:8px}
|
||||
.prm-section-lbl p{font-size:.9rem;color:rgba(255,255,255,.4)}
|
||||
|
||||
.prm-plans{
|
||||
display:grid;grid-template-columns:repeat(3,1fr);gap:18px;
|
||||
margin-bottom:72px;
|
||||
}
|
||||
@media(max-width:900px){.prm-plans{grid-template-columns:1fr}}
|
||||
|
||||
.prm-card{
|
||||
position:relative;border-radius:20px;padding:32px 26px 28px;
|
||||
background:rgba(255,255,255,.03);
|
||||
border:1px solid rgba(255,255,255,.08);
|
||||
transition:transform .2s ease,border-color .2s ease,box-shadow .2s ease;
|
||||
display:flex;flex-direction:column;
|
||||
}
|
||||
.prm-card:hover{transform:translateY(-4px);border-color:rgba(255,255,255,.15);box-shadow:0 20px 60px rgba(0,0,0,.4)}
|
||||
.prm-card.featured{
|
||||
background:linear-gradient(145deg,rgba(0,245,255,.06) 0%,rgba(184,77,255,.05) 60%,rgba(255,45,125,.05) 100%);
|
||||
border-color:rgba(0,245,255,.3);
|
||||
box-shadow:0 0 0 1px rgba(0,245,255,.1),0 20px 60px rgba(0,245,255,.08);
|
||||
}
|
||||
.prm-card.featured:hover{box-shadow:0 0 0 1px rgba(0,245,255,.2),0 28px 70px rgba(0,245,255,.14)}
|
||||
|
||||
/* Badge */
|
||||
.prm-card-badge{
|
||||
position:absolute;top:-13px;left:50%;transform:translateX(-50%);
|
||||
padding:4px 18px;border-radius:20px;font-size:.7rem;font-weight:800;
|
||||
letter-spacing:.08em;text-transform:uppercase;white-space:nowrap;
|
||||
background:linear-gradient(90deg,var(--prm-cyan),var(--prm-purple));
|
||||
color:#06060d;box-shadow:0 0 20px rgba(0,245,255,.3);
|
||||
}
|
||||
.prm-card-badge.gold{background:linear-gradient(90deg,#ffd60a,#ff9f0a);box-shadow:0 0 20px rgba(255,214,10,.3)}
|
||||
.prm-card-badge.pink{background:linear-gradient(90deg,var(--prm-pink),var(--prm-purple));box-shadow:0 0 20px rgba(255,45,125,.3)}
|
||||
|
||||
/* Plan içerik */
|
||||
.prm-plan-name{font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;color:rgba(255,255,255,.4);margin-bottom:10px}
|
||||
.prm-plan-price-wrap{margin-bottom:6px;line-height:1}
|
||||
.prm-plan-price{font-size:2.8rem;font-weight:900;color:#fff;letter-spacing:-.04em}
|
||||
.prm-plan-currency{font-size:1.2rem;font-weight:700;color:rgba(255,255,255,.6);vertical-align:top;margin-top:8px;display:inline-block}
|
||||
.prm-plan-period{font-size:.8rem;color:rgba(255,255,255,.35);margin-top:4px}
|
||||
.prm-plan-per-day{font-size:.72rem;color:rgba(255,255,255,.25);margin-bottom:4px}
|
||||
.prm-plan-trial{
|
||||
display:inline-flex;align-items:center;gap:5px;
|
||||
padding:3px 10px;border-radius:10px;
|
||||
background:rgba(255,214,10,.1);border:1px solid rgba(255,214,10,.25);
|
||||
font-size:.7rem;font-weight:700;color:#ffd60a;margin-bottom:18px;
|
||||
}
|
||||
.prm-divider{height:1px;background:rgba(255,255,255,.07);margin:18px 0}
|
||||
|
||||
/* Feature list */
|
||||
.prm-features{list-style:none;padding:0;margin:0 0 24px;display:flex;flex-direction:column;gap:9px;flex:1}
|
||||
.prm-features li{display:flex;align-items:flex-start;gap:9px;font-size:.83rem;color:rgba(255,255,255,.65);line-height:1.4}
|
||||
.prm-features li i.bi-check-circle-fill{color:var(--prm-cyan);font-size:.85rem;flex-shrink:0;margin-top:1px}
|
||||
.prm-features li i.bi-x-circle{color:rgba(255,255,255,.15);font-size:.85rem;flex-shrink:0;margin-top:1px}
|
||||
.prm-features li.dim{color:rgba(255,255,255,.2)}
|
||||
|
||||
/* CTA Buton */
|
||||
.prm-btn-buy{
|
||||
display:flex;align-items:center;justify-content:center;gap:8px;
|
||||
width:100%;padding:13px;border-radius:12px;border:none;
|
||||
font-size:.9rem;font-weight:800;cursor:pointer;font-family:inherit;
|
||||
text-decoration:none;letter-spacing:.02em;
|
||||
transition:transform .15s,box-shadow .15s,opacity .15s;
|
||||
margin-top:auto;
|
||||
}
|
||||
.prm-btn-buy:hover{transform:translateY(-2px)}
|
||||
.prm-btn-buy.primary{
|
||||
background:linear-gradient(135deg,var(--prm-cyan) 0%,#0080ff 60%,var(--prm-purple) 100%);
|
||||
color:#06060d;box-shadow:0 0 30px rgba(0,245,255,.25),0 6px 20px rgba(0,0,0,.4);
|
||||
}
|
||||
.prm-btn-buy.primary:hover{box-shadow:0 0 40px rgba(0,245,255,.4),0 8px 28px rgba(0,0,0,.5)}
|
||||
.prm-btn-buy.outline{
|
||||
background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.12);
|
||||
color:rgba(255,255,255,.75);
|
||||
}
|
||||
.prm-btn-buy.outline:hover{background:rgba(255,255,255,.09);border-color:rgba(255,255,255,.25);color:#fff}
|
||||
.prm-btn-buy.active-plan{
|
||||
background:rgba(50,220,100,.08);border:1px solid rgba(50,220,100,.3);
|
||||
color:#32dc64;cursor:default;
|
||||
}
|
||||
.prm-btn-buy.active-plan:hover{transform:none}
|
||||
.prm-btn-buy.disabled{
|
||||
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);
|
||||
color:rgba(255,255,255,.2);cursor:not-allowed;
|
||||
}
|
||||
.prm-btn-buy.disabled:hover{transform:none}
|
||||
|
||||
/* ── Nasıl Çalışır ── */
|
||||
.prm-how{
|
||||
display:grid;grid-template-columns:repeat(3,1fr);gap:2px;
|
||||
margin-bottom:72px;background:rgba(255,255,255,.04);
|
||||
border:1px solid rgba(255,255,255,.07);border-radius:20px;overflow:hidden;
|
||||
}
|
||||
@media(max-width:700px){.prm-how{grid-template-columns:1fr}}
|
||||
.prm-how-step{
|
||||
padding:32px 28px;position:relative;
|
||||
background:rgba(6,6,13,.6);
|
||||
}
|
||||
.prm-how-step:not(:last-child)::after{
|
||||
content:'';position:absolute;right:0;top:20%;bottom:20%;
|
||||
width:1px;background:rgba(255,255,255,.07);
|
||||
}
|
||||
@media(max-width:700px){.prm-how-step:not(:last-child)::after{display:none}}
|
||||
.prm-how-num{
|
||||
width:40px;height:40px;border-radius:12px;display:flex;align-items:center;justify-content:center;
|
||||
font-size:1.1rem;font-weight:900;margin-bottom:16px;
|
||||
}
|
||||
.prm-how-num.c1{background:rgba(0,245,255,.12);color:var(--prm-cyan)}
|
||||
.prm-how-num.c2{background:rgba(184,77,255,.12);color:var(--prm-purple)}
|
||||
.prm-how-num.c3{background:rgba(50,220,100,.12);color:#32dc64}
|
||||
.prm-how-step h3{font-size:.95rem;font-weight:800;color:#fff;margin-bottom:8px}
|
||||
.prm-how-step p{font-size:.8rem;color:rgba(255,255,255,.4);line-height:1.6;margin:0}
|
||||
|
||||
/* ── Aktivasyon Kutusu ── */
|
||||
.prm-activate-box{
|
||||
border-radius:20px;padding:44px 36px;margin-bottom:72px;
|
||||
background:linear-gradient(135deg,rgba(0,245,255,.04),rgba(184,77,255,.03));
|
||||
border:1px solid rgba(0,245,255,.15);
|
||||
text-align:center;position:relative;overflow:hidden;
|
||||
}
|
||||
.prm-activate-box::before{
|
||||
content:'';position:absolute;top:-60px;left:50%;transform:translateX(-50%);
|
||||
width:300px;height:200px;border-radius:50%;
|
||||
background:radial-gradient(rgba(0,245,255,.08),transparent 70%);
|
||||
pointer-events:none;
|
||||
}
|
||||
.prm-activate-box h2{font-size:1.45rem;font-weight:800;color:#fff;margin-bottom:8px}
|
||||
.prm-activate-box p{font-size:.88rem;color:rgba(255,255,255,.4);margin-bottom:28px;line-height:1.6}
|
||||
.prm-code-form{display:flex;gap:10px;max-width:500px;margin:0 auto;flex-wrap:wrap;justify-content:center}
|
||||
.prm-code-input{
|
||||
flex:1;min-width:220px;
|
||||
background:rgba(255,255,255,.06);border:1px solid rgba(0,245,255,.25);
|
||||
border-radius:12px;padding:14px 18px;color:#fff;font-size:1rem;font-weight:700;
|
||||
letter-spacing:.12em;text-align:center;text-transform:uppercase;outline:none;
|
||||
font-family:inherit;transition:border-color .2s,box-shadow .2s;
|
||||
}
|
||||
.prm-code-input::placeholder{letter-spacing:.05em;font-weight:400;text-transform:none;font-size:.88rem;color:rgba(255,255,255,.3)}
|
||||
.prm-code-input:focus{border-color:var(--prm-cyan);box-shadow:0 0 0 3px rgba(0,245,255,.12)}
|
||||
.prm-code-input.is-invalid{border-color:var(--prm-pink)!important;box-shadow:0 0 0 3px rgba(255,45,125,.1)!important}
|
||||
.prm-code-btn{
|
||||
padding:14px 28px;border-radius:12px;border:none;cursor:pointer;font-family:inherit;
|
||||
font-size:.9rem;font-weight:800;letter-spacing:.03em;
|
||||
background:linear-gradient(135deg,var(--prm-cyan),var(--prm-purple));
|
||||
color:#06060d;box-shadow:0 0 24px rgba(0,245,255,.2);
|
||||
transition:transform .15s,box-shadow .15s;
|
||||
}
|
||||
.prm-code-btn:hover{transform:translateY(-2px);box-shadow:0 0 36px rgba(0,245,255,.35)}
|
||||
.prm-code-error{
|
||||
width:100%;text-align:center;font-size:.8rem;color:var(--prm-pink);
|
||||
margin-top:8px;font-weight:600;
|
||||
}
|
||||
.prm-activate-login-note{
|
||||
margin-top:14px;font-size:.78rem;color:rgba(255,255,255,.3);
|
||||
}
|
||||
.prm-activate-login-note a{color:var(--prm-cyan);text-decoration:none;font-weight:700}
|
||||
.prm-activate-login-note a:hover{text-decoration:underline}
|
||||
|
||||
/* ── Özellik Showcase ── */
|
||||
.prm-features-grid{
|
||||
display:grid;grid-template-columns:repeat(2,1fr);gap:32px;margin-bottom:72px;
|
||||
}
|
||||
@media(max-width:700px){.prm-features-grid{grid-template-columns:1fr}}
|
||||
.prm-feat-cat{
|
||||
background:rgba(255,255,255,.025);border:1px solid rgba(255,255,255,.07);
|
||||
border-radius:16px;padding:24px;
|
||||
}
|
||||
.prm-feat-cat-hdr{
|
||||
display:flex;align-items:center;gap:10px;margin-bottom:18px;padding-bottom:14px;
|
||||
border-bottom:1px solid rgba(255,255,255,.06);
|
||||
}
|
||||
.prm-feat-cat-icon{
|
||||
width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;
|
||||
font-size:1rem;flex-shrink:0;
|
||||
}
|
||||
.prm-feat-cat-hdr h3{font-size:.9rem;font-weight:800;color:#fff;margin:0}
|
||||
.prm-feat-list{display:flex;flex-direction:column;gap:10px}
|
||||
.prm-feat-item{display:flex;align-items:flex-start;gap:10px}
|
||||
.prm-feat-item-icon{
|
||||
width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;
|
||||
font-size:.75rem;flex-shrink:0;margin-top:1px;
|
||||
}
|
||||
.prm-feat-item-body{flex:1;min-width:0}
|
||||
.prm-feat-item-name{font-size:.82rem;font-weight:700;color:rgba(255,255,255,.8);margin-bottom:2px}
|
||||
.prm-feat-item-desc{font-size:.72rem;color:rgba(255,255,255,.35);line-height:1.45}
|
||||
|
||||
/* Kozmetik Preview */
|
||||
.prm-cosmetics{
|
||||
margin-bottom:72px;text-align:center;
|
||||
}
|
||||
.prm-cosmetics-title{font-size:1.35rem;font-weight:800;color:#fff;margin-bottom:8px}
|
||||
.prm-cosmetics-sub{font-size:.85rem;color:rgba(255,255,255,.35);margin-bottom:36px}
|
||||
.prm-cosmetics-grid{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-bottom:28px}
|
||||
|
||||
/* Username color chips */
|
||||
.uc-chip{
|
||||
padding:5px 16px;border-radius:20px;font-size:.82rem;font-weight:700;
|
||||
background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);
|
||||
cursor:default;white-space:nowrap;
|
||||
}
|
||||
|
||||
/* Frame badges */
|
||||
.frame-badge{
|
||||
width:52px;height:52px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;
|
||||
font-size:1.2rem;font-weight:800;color:#fff;position:relative;
|
||||
}
|
||||
.frame-badge::before{
|
||||
content:'';position:absolute;inset:-3px;border-radius:50%;
|
||||
animation:frame-spin 3s linear infinite;
|
||||
}
|
||||
@keyframes frame-spin{to{transform:rotate(360deg)}}
|
||||
.frame-badge.fr-neon::before{background:conic-gradient(#00f5ff,#b84dff,#00f5ff)}
|
||||
.frame-badge.fr-fire::before{background:conic-gradient(#ff6b35,#ff2d7d,#ff8c00,#ff6b35)}
|
||||
.frame-badge.fr-galaxy::before{background:conic-gradient(#7c3aed,#b84dff,#4a1d96,#7c3aed)}
|
||||
.frame-badge.fr-gold::before{background:conic-gradient(#ffd700,#ff8c00,#ffd700)}
|
||||
.frame-badge.fr-sakura::before{background:conic-gradient(#ff9ec4,#ff2d7d,#ffd6e7,#ff9ec4)}
|
||||
.frame-badge.fr-rainbow::before{background:conic-gradient(#f00,#ff8c00,#ffd700,#0c0,#08f,#8b00ff,#f08,#f00)}
|
||||
.frame-badge.fr-ice::before{background:conic-gradient(#a8edff,#60cfff,#a8edff)}
|
||||
.frame-badge-inner{position:relative;z-index:1;width:44px;height:44px;border-radius:50%;background:#111;display:flex;align-items:center;justify-content:center;font-size:.65rem;text-align:center;padding:2px;color:rgba(255,255,255,.7)}
|
||||
|
||||
/* Comment bg preview */
|
||||
.cb-preview{
|
||||
padding:10px 16px;border-radius:10px;font-size:.8rem;color:#fff;font-weight:600;
|
||||
white-space:nowrap;position:relative;overflow:hidden;
|
||||
}
|
||||
.cb-preview::before{content:'';position:absolute;inset:0;opacity:.7}
|
||||
.cb-fire::before{background:linear-gradient(135deg,#ff6b35,#ff2d7d)}
|
||||
.cb-aurora::before{background:linear-gradient(135deg,#00c8d4,#b84dff)}
|
||||
.cb-stars::before{background:linear-gradient(135deg,#1a1a3a,#4a1d96)}
|
||||
.cb-sakura::before{background:linear-gradient(135deg,#ff9ec4,#ff2d7d)}
|
||||
.cb-neon::before{background:linear-gradient(135deg,#00f5ff,#0040ff)}
|
||||
.cb-galaxy::before{background:linear-gradient(135deg,#0a0020,#7c3aed)}
|
||||
.cb-ice::before{background:linear-gradient(135deg,#a8edff,#006fbf)}
|
||||
.cb-preview span{position:relative;z-index:1}
|
||||
|
||||
/* ── FAQ ── */
|
||||
.prm-faq{margin-bottom:48px}
|
||||
.prm-faq h2{font-size:1.35rem;font-weight:800;color:#fff;margin-bottom:28px;text-align:center}
|
||||
.prm-faq-item{
|
||||
border-bottom:1px solid rgba(255,255,255,.06);overflow:hidden;
|
||||
}
|
||||
.prm-faq-q{
|
||||
display:flex;align-items:center;justify-content:space-between;
|
||||
padding:16px 4px;cursor:pointer;font-size:.9rem;font-weight:700;color:rgba(255,255,255,.75);
|
||||
transition:color .15s;user-select:none;
|
||||
background:none;border:none;width:100%;text-align:left;font-family:inherit;
|
||||
gap:12px;
|
||||
}
|
||||
.prm-faq-q:hover{color:#fff}
|
||||
.prm-faq-q i{flex-shrink:0;transition:transform .25s ease;color:rgba(255,255,255,.3)}
|
||||
.prm-faq-item.open .prm-faq-q i{transform:rotate(45deg);color:var(--prm-cyan)}
|
||||
.prm-faq-a{
|
||||
max-height:0;overflow:hidden;
|
||||
transition:max-height .3s ease,padding .3s ease;
|
||||
font-size:.84rem;color:rgba(255,255,255,.45);line-height:1.7;padding:0 4px;
|
||||
}
|
||||
.prm-faq-item.open .prm-faq-a{max-height:200px;padding-bottom:16px}
|
||||
|
||||
/* ── Mevcut Plan Banner ── */
|
||||
.prm-active-banner{
|
||||
display:flex;align-items:center;gap:16px;padding:18px 24px;border-radius:14px;
|
||||
background:rgba(0,245,255,.06);border:1px solid rgba(0,245,255,.2);margin-bottom:36px;
|
||||
flex-wrap:wrap;
|
||||
}
|
||||
.prm-active-banner i{font-size:1.8rem;color:var(--prm-cyan);flex-shrink:0}
|
||||
.prm-active-banner-body{flex:1;min-width:0}
|
||||
.prm-active-banner-title{font-size:.95rem;font-weight:800;color:#fff;margin-bottom:2px}
|
||||
.prm-active-banner-sub{font-size:.78rem;color:rgba(255,255,255,.4)}
|
||||
.prm-active-banner-action{flex-shrink:0}
|
||||
.prm-active-banner-action a{
|
||||
padding:9px 20px;border-radius:10px;border:1px solid rgba(0,245,255,.3);
|
||||
background:rgba(0,245,255,.07);color:var(--prm-cyan);font-size:.8rem;font-weight:700;
|
||||
text-decoration:none;transition:all .15s;white-space:nowrap;display:inline-block;
|
||||
}
|
||||
.prm-active-banner-action a:hover{background:rgba(0,245,255,.14);border-color:var(--prm-cyan)}
|
||||
|
||||
@media(max-width:600px){
|
||||
.prm-hero{padding:72px 16px 56px}
|
||||
.prm-activate-box{padding:32px 20px}
|
||||
.prm-how-step{padding:24px 20px}
|
||||
.prm-card{padding:26px 20px 22px}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div id="pg">
|
||||
<div class="prm-hero">
|
||||
<div class="prm-hero-bg"></div>
|
||||
<div class="prm-stars" id="prm-stars"></div>
|
||||
<div class="prm-hero-inner">
|
||||
<div class="prm-hero-badge"><i class="bi bi-stars"></i> Animexe Premium</div>
|
||||
<h1 class="prm-hero-title">
|
||||
Anime deneyimini<br><span class="grad">zirveye taşı</span>
|
||||
</h1>
|
||||
<p class="prm-hero-sub">
|
||||
Reklamsız izleme, özel profil kozmetikleri, erken bölüm erişimi ve daha fazlası — hepsi tek bir Premium üyelikte.
|
||||
</p>
|
||||
<div class="prm-hero-stats">
|
||||
<div class="prm-stat"><span class="prm-stat-val">∞</span><span class="prm-stat-lbl">İçerik</span></div>
|
||||
<div class="prm-stat"><span class="prm-stat-val">{{ count($featureGroups ?? []) > 0 ? array_sum(array_map(fn($g) => count($g['perks']), $featureGroups)) : '20+' }}</span><span class="prm-stat-lbl">Özel Özellik</span></div>
|
||||
<div class="prm-stat"><span class="prm-stat-val">7/24</span><span class="prm-stat-lbl">Destek</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="prm-wrap">
|
||||
|
||||
{{-- Aktivasyon başarı mesajı --}}
|
||||
@if(session('activation_success'))
|
||||
<div class="prm-success-banner">
|
||||
<i class="bi bi-patch-check-fill"></i>
|
||||
<div>
|
||||
<strong>Premium aktif! 🎉 {{ session('activation_success.plan') }} planına hoş geldin.</strong>
|
||||
<span>Üyeliğin {{ session('activation_success.expires_at') }} tarihine kadar geçerli.</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Mevcut premium kullanıcı banner --}}
|
||||
@auth
|
||||
@if(auth()->user()->isPremium())
|
||||
<div class="prm-active-banner">
|
||||
<i class="bi bi-shield-fill-check"></i>
|
||||
<div class="prm-active-banner-body">
|
||||
<div class="prm-active-banner-title">Premium üyeliğin aktif 🎉</div>
|
||||
<div class="prm-active-banner-sub">
|
||||
Bitiş tarihi: {{ auth()->user()->premium_expires_at?->format('d MMMM Y') ?? auth()->user()->premium_expires_at?->format('d.m.Y') }}
|
||||
· Süre uzatmak için aşağıdan yeni bir plan satın alabilirsin.
|
||||
</div>
|
||||
</div>
|
||||
<div class="prm-active-banner-action">
|
||||
<a href="{{ route('profile') }}"><i class="bi bi-person-circle me-1"></i>Profili Gör</a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endauth
|
||||
|
||||
{{-- Planlar --}}
|
||||
@if(count($plans ?? []) > 0)
|
||||
<div class="prm-section-lbl">
|
||||
<h2>Planını Seç</h2>
|
||||
<p>Tüm planlar aynı özellikleri içerir — süre ne kadar uzunsa o kadar tasarruf edersin.</p>
|
||||
</div>
|
||||
<div class="prm-plans">
|
||||
@foreach($plans as $idx => $plan)
|
||||
@php
|
||||
$isFeatured = $plan->badge_label && $idx == 1 || ($plan->badge_label && str_contains(strtolower($plan->badge_label), 'popül'));
|
||||
$isActive = auth()->check() && auth()->user()->subscriptions()->where('plan_id', $plan->id)->where('status','active')->exists();
|
||||
$perDay = $plan->duration_days > 0 ? round($plan->price / $plan->duration_days, 2) : 0;
|
||||
$accentCls = $isFeatured ? 'featured' : '';
|
||||
@endphp
|
||||
<div class="prm-card {{ $accentCls }}">
|
||||
@if($plan->badge_label)
|
||||
@php
|
||||
$badgeCls = match(true) {
|
||||
str_contains(strtolower($plan->badge_label), 'değer') => 'gold',
|
||||
str_contains(strtolower($plan->badge_label), 'popül') => '',
|
||||
default => 'pink',
|
||||
};
|
||||
@endphp
|
||||
<div class="prm-card-badge {{ $badgeCls }}">{{ $plan->badge_label }}</div>
|
||||
@endif
|
||||
|
||||
<div class="prm-plan-name">{{ $plan->name }}</div>
|
||||
<div class="prm-plan-price-wrap">
|
||||
<span class="prm-plan-currency">₺</span><span class="prm-plan-price">{{ number_format($plan->price, 0, ',', '.') }}</span>
|
||||
</div>
|
||||
<div class="prm-plan-period">
|
||||
@if($plan->duration_days >= 365)
|
||||
{{ intdiv($plan->duration_days, 365) }} yıl
|
||||
@elseif($plan->duration_days >= 30)
|
||||
{{ intdiv($plan->duration_days, 30) }} ay
|
||||
@else
|
||||
{{ $plan->duration_days }} gün
|
||||
@endif
|
||||
· Günde ≈ ₺{{ $perDay }}
|
||||
</div>
|
||||
|
||||
@if($plan->trial_days)
|
||||
<div class="prm-plan-trial"><i class="bi bi-gift-fill"></i> İlk {{ $plan->trial_days }} gün ücretsiz!</div>
|
||||
@endif
|
||||
|
||||
<div class="prm-divider"></div>
|
||||
|
||||
@if(count($plan->features ?? []) > 0)
|
||||
<ul class="prm-features">
|
||||
@foreach($plan->features as $feat)
|
||||
<li><i class="bi bi-check-circle-fill"></i> {{ $feat }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
@if($isActive)
|
||||
<a class="prm-btn-buy active-plan"><i class="bi bi-check-circle-fill"></i> Mevcut Planın</a>
|
||||
@elseif($plan->purchase_link)
|
||||
<a href="{{ $plan->purchase_link }}" target="_blank" rel="noopener" class="prm-btn-buy {{ $isFeatured ? 'primary' : 'outline' }}">
|
||||
<i class="bi bi-bag-fill"></i> Satın Al
|
||||
</a>
|
||||
@else
|
||||
<span class="prm-btn-buy disabled"><i class="bi bi-clock"></i> Yakında</span>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Nasıl Çalışır --}}
|
||||
<div class="prm-section-lbl">
|
||||
<h2>Nasıl Çalışır?</h2>
|
||||
<p>Üç adımda premium üyeliğini aktifleştir.</p>
|
||||
</div>
|
||||
<div class="prm-how">
|
||||
<div class="prm-how-step">
|
||||
<div class="prm-how-num c1"><i class="bi bi-1-circle-fill"></i></div>
|
||||
<h3>Paket Seç & Öde</h3>
|
||||
<p>Yukarıdaki planlardan birini seç, "Satın Al" butonuna tıkla ve güvenli ödeme sayfasına yönlendir. Ödemeyi tamamla.</p>
|
||||
</div>
|
||||
<div class="prm-how-step">
|
||||
<div class="prm-how-num c2"><i class="bi bi-2-circle-fill"></i></div>
|
||||
<h3>Aktivasyon Kodunu Al</h3>
|
||||
<p>Ödemen onaylandıktan sonra sana özel bir aktivasyon kodu gönderilir. Bu kodu saklaman yeterli.</p>
|
||||
</div>
|
||||
<div class="prm-how-step">
|
||||
<div class="prm-how-num c3"><i class="bi bi-3-circle-fill"></i></div>
|
||||
<h3>Kodu Gir, Keyfini Çıkar</h3>
|
||||
<p>Aşağıdaki kutuya aktivasyon kodunu gir. Anında aktif olur, tüm premium ayrıcalıklar hesabına yüklenir!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Aktivasyon Kutusu --}}
|
||||
<div class="prm-activate-box" id="activate">
|
||||
<h2>Aktivasyon Kodun Var mı?</h2>
|
||||
<p>Satın alma sonrası aldığın kodu girerek Premium üyeliğini hemen aktifleştir.</p>
|
||||
|
||||
@auth
|
||||
<form method="POST" action="{{ route('premium.activate.redeem') }}" class="prm-code-form">
|
||||
@csrf
|
||||
<input type="text" name="code" id="prm-code-inp" class="prm-code-input {{ $errors->has('code') ? 'is-invalid' : '' }}"
|
||||
placeholder="XXXX-XXXX-XXXX" maxlength="32" autocomplete="off" autocapitalize="characters"
|
||||
value="{{ old('code') }}" spellcheck="false">
|
||||
<button type="submit" class="prm-code-btn"><i class="bi bi-lightning-fill me-1"></i>Aktifleştir</button>
|
||||
@if($errors->has('code'))
|
||||
<div class="prm-code-error"><i class="bi bi-exclamation-circle me-1"></i>{{ $errors->first('code') }}</div>
|
||||
@endif
|
||||
</form>
|
||||
@else
|
||||
<form class="prm-code-form">
|
||||
<input type="text" class="prm-code-input" placeholder="XXXX-XXXX-XXXX" disabled>
|
||||
<button type="button" onclick="window.location.href='{{ route('frontend.register') }}?next={{ urlencode(route('premium.activate')) }}'" class="prm-code-btn">
|
||||
<i class="bi bi-person-plus-fill me-1"></i>Giriş Yap & Aktifleştir
|
||||
</button>
|
||||
</form>
|
||||
<div class="prm-activate-login-note">
|
||||
Aktivasyon kodu kullanmak için <a href="{{ route('frontend.register') }}">üye olman</a> veya <a href="{{ route('frontend.login') }}">giriş yapman</a> gerekiyor.
|
||||
</div>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
{{-- Özellikler Showcase --}}
|
||||
@if(count($featureGroups ?? []) > 0)
|
||||
<div class="prm-section-lbl">
|
||||
<h2>Premium Ayrıcalıklar</h2>
|
||||
<p>Premium üyeliğinle birlikte gelen tüm özellikler.</p>
|
||||
</div>
|
||||
<div class="prm-features-grid">
|
||||
@foreach($featureGroups as $group)
|
||||
@php
|
||||
$colors = [
|
||||
'Profil' => ['bg'=>'rgba(184,77,255,.12)','color'=>'#b84dff','icon'=>'bi-person-circle'],
|
||||
'İzleme' => ['bg'=>'rgba(0,245,255,.12)','color'=>'#00f5ff','icon'=>'bi-play-circle-fill'],
|
||||
'Sosyal' => ['bg'=>'rgba(255,45,125,.12)','color'=>'#ff2d7d','icon'=>'bi-chat-heart-fill'],
|
||||
'Hesap' => ['bg'=>'rgba(255,214,10,.12)','color'=>'#ffd60a','icon'=>'bi-shield-fill-check'],
|
||||
];
|
||||
$c = $colors[$group['label']] ?? ['bg'=>'rgba(255,255,255,.08)','color'=>'#fff','icon'=>'bi-star-fill'];
|
||||
@endphp
|
||||
<div class="prm-feat-cat">
|
||||
<div class="prm-feat-cat-hdr">
|
||||
<div class="prm-feat-cat-icon" style="background:{{ $c['bg'] }};color:{{ $c['color'] }}">
|
||||
<i class="bi {{ $c['icon'] }}"></i>
|
||||
</div>
|
||||
<h3>{{ $group['label'] }}</h3>
|
||||
</div>
|
||||
<div class="prm-feat-list">
|
||||
@foreach(array_slice($group['perks'], 0, 6) as $perk)
|
||||
<div class="prm-feat-item">
|
||||
<div class="prm-feat-item-icon" style="background:{{ $c['bg'] }};color:{{ $c['color'] }}">
|
||||
<i class="bi {{ $perk['icon'] ?? 'bi-check' }}"></i>
|
||||
</div>
|
||||
<div class="prm-feat-item-body">
|
||||
<div class="prm-feat-item-name">{{ $perk['name'] }}</div>
|
||||
@if(!empty($perk['desc']))<div class="prm-feat-item-desc">{{ $perk['desc'] }}</div>@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Kozmetik Preview --}}
|
||||
<div class="prm-cosmetics">
|
||||
<div class="prm-cosmetics-title">Profilini Kişiselleştir</div>
|
||||
<div class="prm-cosmetics-sub">Premium üyeler için özel görsel özelleştirmeler</div>
|
||||
|
||||
<div style="display:flex;flex-wrap:wrap;gap:32px;justify-content:center;margin-bottom:36px">
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.3);margin-bottom:14px">Profil Çerçeveleri</div>
|
||||
<div style="display:flex;gap:14px;flex-wrap:wrap;justify-content:center">
|
||||
@foreach(['neon','fire','galaxy','gold','sakura','rainbow','ice'] as $fr)
|
||||
<div class="frame-badge fr-{{ $fr }}">
|
||||
<div class="frame-badge-inner">{{ Str::upper(substr($fr,0,2)) }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.3);margin-bottom:14px">Yorum Arka Planları</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center">
|
||||
@foreach(['fire'=>'Ateş','aurora'=>'Aurora','stars'=>'Yıldız','sakura'=>'Sakura','neon'=>'Neon','galaxy'=>'Galaksi','ice'=>'Buz'] as $bg => $lbl)
|
||||
<div class="cb-preview cb-{{ $bg }}"><span>{{ $lbl }}</span></div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align:center">
|
||||
<div style="font-size:.7rem;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.3);margin-bottom:14px">Kullanıcı Adı Renkleri</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center">
|
||||
@foreach([
|
||||
['gradient'=>'linear-gradient(90deg,#00f5ff,#0080ff)','name'=>'Okyanus'],
|
||||
['gradient'=>'linear-gradient(90deg,#ff2d7d,#b84dff)','name'=>'Galaksi'],
|
||||
['gradient'=>'linear-gradient(90deg,#ffd700,#ff8c00)','name'=>'Altın'],
|
||||
['gradient'=>'linear-gradient(90deg,#32dc64,#00c8d4)','name'=>'Orman'],
|
||||
['gradient'=>'linear-gradient(90deg,#ff6b35,#ff2d7d)','name'=>'Ateş'],
|
||||
['gradient'=>'linear-gradient(90deg,#a8edff,#006fbf)','name'=>'Buz'],
|
||||
['gradient'=>'linear-gradient(90deg,#b84dff,#7c3aed)','name'=>'Mor'],
|
||||
] as $uc)
|
||||
<span class="uc-chip" style="background:{{ $uc['gradient'] }};-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;border-color:rgba(255,255,255,.12)">{{ $uc['name'] }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- FAQ --}}
|
||||
<div class="prm-faq">
|
||||
<h2>Sık Sorulan Sorular</h2>
|
||||
@foreach([
|
||||
['s'=>'Ödeme yaptıktan sonra nasıl aktifleştiririm?','c'=>'Ödemen onaylandıktan sonra sana aktivasyon kodu gönderilir. Bu sayfadaki "Aktivasyon Kodun Var mı?" kısmına kodu girip "Aktifleştir" butonuna tıklaman yeterli.'],
|
||||
['s'=>'Mevcut Premium üyeliğim varken başka bir kod girebilir miyim?','c'=>'Evet! Mevcut üyelik süren bitmeden kod girersen süreler üst üste eklenir (stack). Hiç süre kaybetmezsin.'],
|
||||
['s'=>'Aktivasyon kodum çalışmıyorsa ne yapmalıyım?','c'=>'Kodu büyük harflerle ve tire işaretleriyle birlikte girdiğinden emin ol (XXXX-XXXX-XXXX). Sorun devam ederse destek hattına başvur.'],
|
||||
['s'=>'Premium üyeliğimi iptal edebilir miyim?','c'=>'Premium üyeliğin süre tabanlıdır; otomatik yenileme yoktur. Süre dolunca normal hesabına dönersin.'],
|
||||
['s'=>'Premium özellikler hangi cihazlarda çalışır?','c'=>'Tüm cihazlarda (bilgisayar, tablet, telefon) çalışır. Hesabına giriş yaptığın her yerde premium ayrıcalıklarını kullanabilirsin.'],
|
||||
] as $faq)
|
||||
<div class="prm-faq-item">
|
||||
<button class="prm-faq-q" onclick="this.closest('.prm-faq-item').classList.toggle('open')">
|
||||
<span>{{ $faq['s'] }}</span>
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
<div class="prm-faq-a">{{ $faq['c'] }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</div>{{-- /prm-wrap --}}
|
||||
</div>{{-- /pg --}}
|
||||
|
||||
<script>
|
||||
// Yıldız parçacıkları
|
||||
(function(){
|
||||
const c = document.getElementById('prm-stars');
|
||||
if(!c) return;
|
||||
for(let i=0;i<60;i++){
|
||||
const s = document.createElement('span');
|
||||
s.style.cssText = `left:${Math.random()*100}%;top:${Math.random()*100}%;--d:${2+Math.random()*4}s;--delay:-${Math.random()*4}s`;
|
||||
c.appendChild(s);
|
||||
}
|
||||
})();
|
||||
|
||||
// Kod input: otomatik büyük harf + tire formatı
|
||||
(function(){
|
||||
const inp = document.getElementById('prm-code-inp');
|
||||
if(!inp) return;
|
||||
inp.addEventListener('input', function(){
|
||||
let v = this.value.replace(/[^A-Za-z0-9]/g,'').toUpperCase();
|
||||
const parts = v.match(/.{1,4}/g) || [];
|
||||
this.value = parts.join('-').slice(0, 14);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
@@ -0,0 +1,904 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Profil Ayarları — Animexe')
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
.ps-wrap {
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px 80px;
|
||||
}
|
||||
|
||||
/* Banner + Avatar */
|
||||
.ps-banner-area {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0d0d1a, #1a0a2e);
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
transition: filter .2s;
|
||||
}
|
||||
.ps-banner-area:hover { filter: brightness(0.85); }
|
||||
.ps-banner-img {
|
||||
width: 100%; height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.ps-banner-overlay {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(0,0,0,.45);
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
flex-direction: column; gap: 8px;
|
||||
font-size: .85rem; color: #fff;
|
||||
}
|
||||
.ps-banner-area:hover .ps-banner-overlay { opacity: 1; }
|
||||
.ps-banner-area::after {
|
||||
content: '';
|
||||
position: absolute; inset: 0;
|
||||
background: linear-gradient(to bottom, transparent 60%, rgba(0,0,0,.7));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ps-avatar-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 20px;
|
||||
margin-top: -48px;
|
||||
padding: 0 24px;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.ps-avatar-wrap {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ps-avatar {
|
||||
width: 96px; height: 96px;
|
||||
border-radius: 20px;
|
||||
border: 3px solid var(--ps-color, #00f5ff);
|
||||
object-fit: cover;
|
||||
background: #1a1a2e;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 2.2rem; font-weight: 900; color: #fff;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.5);
|
||||
transition: border-color .3s;
|
||||
}
|
||||
.ps-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.ps-avatar-overlay {
|
||||
position: absolute; inset: 0;
|
||||
border-radius: 18px;
|
||||
background: rgba(0,0,0,.55);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
font-size: 1.2rem; color: #fff;
|
||||
}
|
||||
.ps-avatar-wrap:hover .ps-avatar-overlay { opacity: 1; }
|
||||
.ps-avatar-name {
|
||||
padding-bottom: 8px;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.ps-card {
|
||||
background: rgba(255,255,255,.04);
|
||||
border: 1px solid rgba(255,255,255,.08);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.ps-card-title {
|
||||
font-size: .8rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
color: rgba(255,255,255,.4);
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.ps-card-title i { font-size: 1rem; }
|
||||
|
||||
/* Form elements */
|
||||
.ps-label {
|
||||
display: block;
|
||||
font-size: .8rem;
|
||||
color: rgba(255,255,255,.5);
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.ps-input {
|
||||
width: 100%;
|
||||
background: rgba(255,255,255,.06);
|
||||
border: 1px solid rgba(255,255,255,.1);
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
color: #fff;
|
||||
font-size: .9rem;
|
||||
transition: border-color .2s, background .2s;
|
||||
outline: none;
|
||||
}
|
||||
.ps-input:focus {
|
||||
border-color: var(--ps-color, #00f5ff);
|
||||
background: rgba(255,255,255,.09);
|
||||
}
|
||||
.ps-input::placeholder { color: rgba(255,255,255,.25); }
|
||||
.ps-input.with-prefix { padding-left: 36px; }
|
||||
|
||||
.ps-field { margin-bottom: 16px; position: relative; }
|
||||
.ps-prefix {
|
||||
position: absolute;
|
||||
left: 12px; top: 50%; transform: translateY(-50%);
|
||||
color: rgba(255,255,255,.35);
|
||||
font-size: .9rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ps-textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Color picker */
|
||||
.ps-color-grid {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.ps-color-swatch {
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: transform .15s, border-color .15s;
|
||||
position: relative;
|
||||
}
|
||||
.ps-color-swatch:hover { transform: scale(1.15); }
|
||||
.ps-color-swatch.active { border-color: #fff; transform: scale(1.1); }
|
||||
.ps-color-swatch.active::after {
|
||||
content: '✓';
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: .7rem; font-weight: 900; color: #fff;
|
||||
text-shadow: 0 1px 3px rgba(0,0,0,.6);
|
||||
}
|
||||
.ps-color-custom {
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
border: 2px solid rgba(255,255,255,.2);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Privacy toggle */
|
||||
.ps-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid rgba(255,255,255,.05);
|
||||
}
|
||||
.ps-toggle:last-child { border-bottom: none; }
|
||||
.ps-toggle-label { font-size: .88rem; color: rgba(255,255,255,.8); }
|
||||
.ps-toggle-sub { font-size: .75rem; color: rgba(255,255,255,.35); margin-top: 2px; }
|
||||
.ps-switch {
|
||||
position: relative;
|
||||
width: 44px; height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ps-switch input { opacity: 0; width: 0; height: 0; }
|
||||
.ps-slider {
|
||||
position: absolute; inset: 0;
|
||||
background: rgba(255,255,255,.12);
|
||||
border-radius: 24px;
|
||||
transition: background .2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ps-slider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 18px; height: 18px;
|
||||
left: 3px; top: 3px;
|
||||
background: #fff;
|
||||
border-radius: 50%;
|
||||
transition: transform .2s;
|
||||
}
|
||||
.ps-switch input:checked + .ps-slider { background: var(--ps-color, #00f5ff); }
|
||||
.ps-switch input:checked + .ps-slider::before { transform: translateX(20px); }
|
||||
|
||||
/* Save button */
|
||||
.ps-save {
|
||||
width: 100%;
|
||||
padding: 13px;
|
||||
border-radius: 12px;
|
||||
background: var(--ps-color, #00f5ff);
|
||||
color: #000;
|
||||
font-weight: 800;
|
||||
font-size: .95rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: opacity .2s, transform .1s;
|
||||
letter-spacing: .02em;
|
||||
}
|
||||
.ps-save:hover { opacity: .88; }
|
||||
.ps-save:active { transform: scale(.98); }
|
||||
|
||||
/* Alert */
|
||||
.ps-alert {
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
font-size: .85rem;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.ps-alert-success { background: rgba(63,185,80,.15); border: 1px solid rgba(63,185,80,.3); color: #7ee787; }
|
||||
.ps-alert-error { background: rgba(248,81,73,.12); border: 1px solid rgba(248,81,73,.3); color: #f85149; }
|
||||
|
||||
/* Back link */
|
||||
.ps-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: rgba(255,255,255,.45);
|
||||
font-size: .83rem;
|
||||
margin-bottom: 28px;
|
||||
text-decoration: none;
|
||||
transition: color .2s;
|
||||
}
|
||||
.ps-back:hover { color: rgba(255,255,255,.8); }
|
||||
|
||||
/* Danger zone */
|
||||
.ps-danger { border-color: rgba(248,81,73,.25); }
|
||||
.ps-danger .ps-card-title { color: rgba(248,81,73,.7); }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.ps-banner-area { height: 140px; }
|
||||
.ps-avatar { width: 76px; height: 76px; }
|
||||
.ps-avatar-row { margin-top: -38px; }
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="ps-wrap" id="psWrap" style="--ps-color: {{ $user->profile_color ?? '#00f5ff' }}">
|
||||
|
||||
<a href="{{ route('profile') }}" class="ps-back">
|
||||
<i class="bi bi-arrow-left"></i> Profilime Dön
|
||||
</a>
|
||||
|
||||
@if(session('success'))
|
||||
<div class="ps-alert ps-alert-success">
|
||||
<i class="bi bi-check-circle-fill"></i>{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if($errors->any())
|
||||
<div class="ps-alert ps-alert-error">
|
||||
<i class="bi bi-exclamation-circle-fill"></i>{{ $errors->first() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Banner --}}
|
||||
<form method="POST" action="{{ route('profile.banner') }}" enctype="multipart/form-data" id="bannerForm">
|
||||
@csrf
|
||||
<div class="ps-banner-area" onclick="document.getElementById('bannerInput').click()" title="Kapak fotoğrafı değiştir">
|
||||
@if($user->banner_image)
|
||||
<img class="ps-banner-img" src="{{ \App\Support\MediaUrl::fromStoragePath($user->banner_image) }}" alt="Banner">
|
||||
@else
|
||||
<div class="ps-banner-img" style="background:linear-gradient(135deg,#0d0d1a 0%,#1a0a2e 50%,#0a1a1a 100%);display:flex;align-items:center;justify-content:center">
|
||||
<span style="color:rgba(255,255,255,.1);font-size:3rem"><i class="bi bi-image"></i></span>
|
||||
</div>
|
||||
@endif
|
||||
<div class="ps-banner-overlay">
|
||||
<i class="bi bi-camera" style="font-size:1.5rem"></i>
|
||||
Kapak Fotoğrafı Değiştir
|
||||
</div>
|
||||
</div>
|
||||
<input type="file" id="bannerInput" name="banner" accept="image/*" style="display:none" onchange="this.form.submit()">
|
||||
</form>
|
||||
|
||||
{{-- Avatar row --}}
|
||||
<div class="ps-avatar-row">
|
||||
<form method="POST" action="{{ route('profile.avatar') }}" enctype="multipart/form-data" id="avatarForm">
|
||||
@csrf
|
||||
<div class="ps-avatar-wrap" onclick="document.getElementById('avatarInput').click()" title="Fotoğraf değiştir">
|
||||
<div class="ps-avatar">
|
||||
@if($user->avatar)
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($user->avatar) }}" alt="{{ $user->name }}">
|
||||
@else
|
||||
{{ strtoupper(substr($user->name, 0, 1)) }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="ps-avatar-overlay"><i class="bi bi-camera-fill"></i></div>
|
||||
</div>
|
||||
<input type="file" id="avatarInput" name="avatar" accept="image/*" style="display:none" onchange="this.form.submit()">
|
||||
</form>
|
||||
<div class="ps-avatar-name">{{ $user->name }}</div>
|
||||
</div>
|
||||
|
||||
{{-- Ana bilgiler --}}
|
||||
<form method="POST" action="{{ route('profile.update') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="profile_color" id="profileColorInput" value="{{ $user->profile_color ?? '#00f5ff' }}">
|
||||
|
||||
<div class="ps-card">
|
||||
<div class="ps-card-title"><i class="bi bi-person"></i> Temel Bilgiler</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="ps-field">
|
||||
<label class="ps-label">Görünen Ad *</label>
|
||||
<input type="text" name="name" class="ps-input" value="{{ old('name', $user->name) }}" required maxlength="60">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="ps-field">
|
||||
<label class="ps-label">Kullanıcı Adı <span style="opacity:.5">(opsiyonel)</span></label>
|
||||
<div style="position:relative">
|
||||
<span class="ps-prefix">@</span>
|
||||
<input type="text" name="username" class="ps-input with-prefix"
|
||||
value="{{ old('username', $user->username) }}"
|
||||
placeholder="kullanici_adi" maxlength="30"
|
||||
pattern="[a-zA-Z0-9_\-]+" title="Harf, rakam, _ veya - olabilir">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="ps-field">
|
||||
<label class="ps-label">Hakkımda <span style="opacity:.5">(max 300 karakter)</span></label>
|
||||
<textarea name="bio" class="ps-input ps-textarea" maxlength="300"
|
||||
placeholder="Kendinizden kısaca bahsedin...">{{ old('bio', $user->bio) }}</textarea>
|
||||
<div style="text-align:right;font-size:.72rem;color:rgba(255,255,255,.3);margin-top:4px">
|
||||
<span id="bioCount">{{ strlen($user->bio ?? '') }}</span>/300
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Sosyal linkler --}}
|
||||
<div class="ps-card">
|
||||
<div class="ps-card-title"><i class="bi bi-link-45deg"></i> Sosyal Bağlantılar</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="ps-field">
|
||||
<label class="ps-label"><i class="bi bi-twitter-x" style="font-size:.9rem"></i> Twitter / X</label>
|
||||
<div style="position:relative">
|
||||
<span class="ps-prefix">@</span>
|
||||
<input type="text" name="twitter" class="ps-input with-prefix"
|
||||
value="{{ old('twitter', $user->twitter) }}"
|
||||
placeholder="kullanici_adi" maxlength="50">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="ps-field">
|
||||
<label class="ps-label"><i class="bi bi-instagram" style="font-size:.9rem"></i> Instagram</label>
|
||||
<div style="position:relative">
|
||||
<span class="ps-prefix">@</span>
|
||||
<input type="text" name="instagram" class="ps-input with-prefix"
|
||||
value="{{ old('instagram', $user->instagram) }}"
|
||||
placeholder="kullanici_adi" maxlength="50">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="ps-field">
|
||||
<label class="ps-label"><i class="bi bi-discord" style="font-size:.9rem"></i> Discord</label>
|
||||
<input type="text" name="discord" class="ps-input"
|
||||
value="{{ old('discord', $user->discord) }}"
|
||||
placeholder="Kullanıcı#0000 veya kullanici" maxlength="80">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="ps-field">
|
||||
<label class="ps-label"><i class="bi bi-globe2" style="font-size:.9rem"></i> Website</label>
|
||||
<input type="url" name="website" class="ps-input"
|
||||
value="{{ old('website', $user->website) }}"
|
||||
placeholder="https://..." maxlength="200">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Profil rengi --}}
|
||||
<div class="ps-card">
|
||||
<div class="ps-card-title"><i class="bi bi-palette"></i> Profil Rengi</div>
|
||||
<p style="font-size:.8rem;color:rgba(255,255,255,.4);margin-bottom:16px">
|
||||
Profil sayfasındaki vurgu rengini seç.
|
||||
</p>
|
||||
<div class="ps-color-grid">
|
||||
@php
|
||||
$colors = [
|
||||
'#00f5ff' => 'Siyan',
|
||||
'#ff2d7d' => 'Pembe',
|
||||
'#a371f7' => 'Mor',
|
||||
'#3fb950' => 'Yeşil',
|
||||
'#f0883e' => 'Turuncu',
|
||||
'#ffd700' => 'Altın',
|
||||
'#58a6ff' => 'Mavi',
|
||||
'#ff6b6b' => 'Kırmızı',
|
||||
'#e8d44d' => 'Sarı',
|
||||
'#56d364' => 'Açık Yeşil',
|
||||
];
|
||||
$currentColor = old('profile_color', $user->profile_color ?? '#00f5ff');
|
||||
@endphp
|
||||
@foreach($colors as $hex => $name)
|
||||
<div class="ps-color-swatch {{ $currentColor === $hex ? 'active' : '' }}"
|
||||
style="background:{{ $hex }}"
|
||||
title="{{ $name }}"
|
||||
onclick="setColor('{{ $hex }}', this)"></div>
|
||||
@endforeach
|
||||
<input type="color" class="ps-color-custom" id="customColorPicker"
|
||||
value="{{ $currentColor }}"
|
||||
title="Özel renk"
|
||||
onchange="setColor(this.value, null)">
|
||||
</div>
|
||||
<div style="margin-top:12px;font-size:.78rem;color:rgba(255,255,255,.3)">
|
||||
Seçili: <span id="colorLabel" style="color:var(--ps-color)">{{ $currentColor }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Gizlilik --}}
|
||||
<div class="ps-card">
|
||||
<div class="ps-card-title"><i class="bi bi-shield-check"></i> Gizlilik</div>
|
||||
<div class="ps-toggle">
|
||||
<div>
|
||||
<div class="ps-toggle-label">İzleme Listemi Göster</div>
|
||||
<div class="ps-toggle-sub">Diğer kullanıcılar listenizi görebilir</div>
|
||||
</div>
|
||||
<label class="ps-switch">
|
||||
<input type="checkbox" name="show_watchlist" value="1" {{ $user->show_watchlist ? 'checked' : '' }}>
|
||||
<span class="ps-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="ps-toggle">
|
||||
<div>
|
||||
<div class="ps-toggle-label">İzleme Aktivitemi Göster</div>
|
||||
<div class="ps-toggle-sub">Izleme heatmap ve geçmişi herkese açık</div>
|
||||
</div>
|
||||
<label class="ps-switch">
|
||||
<input type="checkbox" name="show_activity" value="1" {{ $user->show_activity ? 'checked' : '' }}>
|
||||
<span class="ps-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="ps-save">
|
||||
<i class="bi bi-check2 me-2"></i>Değişiklikleri Kaydet
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{{-- Şifre değişikliği --}}
|
||||
<form method="POST" action="{{ route('profile.password') }}" style="margin-top:20px">
|
||||
@csrf
|
||||
<div class="ps-card">
|
||||
<div class="ps-card-title"><i class="bi bi-lock"></i> Şifre Değiştir</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<div class="ps-field">
|
||||
<label class="ps-label">Mevcut Şifre</label>
|
||||
<input type="password" name="current_password" class="ps-input" required autocomplete="current-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="ps-field">
|
||||
<label class="ps-label">Yeni Şifre</label>
|
||||
<input type="password" name="password" class="ps-input" required autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="ps-field">
|
||||
<label class="ps-label">Yeni Şifre (Tekrar)</label>
|
||||
<input type="password" name="password_confirmation" class="ps-input" required autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="ps-save" style="margin-top:16px;background:rgba(255,255,255,.12);color:#fff">
|
||||
<i class="bi bi-lock me-2"></i>Şifreyi Güncelle
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{-- ── Premium Kozmetik Ayarları ────────────────────────────────── --}}
|
||||
@if(auth()->user()->isPremium())
|
||||
<form method="POST" action="{{ route('premium.cosmetics.save') }}">
|
||||
@csrf
|
||||
@php $u = auth()->user(); @endphp
|
||||
|
||||
<div class="ps-card" style="border:1px solid rgba(0,245,255,.18);background:linear-gradient(135deg,rgba(0,245,255,.03),rgba(184,77,255,.03))">
|
||||
<div class="ps-card-title" style="color:#00f5ff">
|
||||
<i class="bi bi-stars"></i> Premium Kozmetik
|
||||
<span style="font-size:.72rem;font-weight:400;color:rgba(0,245,255,.6);margin-left:8px">Sadece premium üyelere özel</span>
|
||||
</div>
|
||||
|
||||
{{-- GIF Avatar --}}
|
||||
@if($u->hasPerk('gif_avatar'))
|
||||
<div class="ps-field" style="margin-bottom:20px">
|
||||
<label class="ps-label"><i class="bi bi-image-fill" style="color:#00f5ff"></i> GIF Profil Fotoğrafı</label>
|
||||
<input type="hidden" name="gif_avatar" id="gifAvatarInput" value="{{ old('gif_avatar', $u->gif_avatar) }}">
|
||||
|
||||
{{-- Mevcut seçim önizleme --}}
|
||||
<div id="gifAvatarPreview" style="margin-bottom:10px;{{ $u->gif_avatar ? '' : 'display:none' }}">
|
||||
<div style="display:flex;align-items:center;gap:10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:10px 14px;">
|
||||
<img id="gifAvatarThumb" src="{{ $u->gif_avatar ?? '' }}" alt="GIF"
|
||||
style="width:56px;height:56px;border-radius:10px;object-fit:cover;border:1px solid rgba(255,255,255,.12)">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:.78rem;color:rgba(255,255,255,.7);margin-bottom:4px">Seçili GIF</div>
|
||||
<div id="gifAvatarUrl" style="font-size:.65rem;color:rgba(255,255,255,.3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">{{ $u->gif_avatar ?? '' }}</div>
|
||||
</div>
|
||||
<button type="button" onclick="clearGifAvatar()" style="background:rgba(255,80,80,.12);border:1px solid rgba(255,80,80,.25);color:#ff5050;border-radius:8px;padding:5px 10px;font-size:.75rem;cursor:pointer;flex-shrink:0">
|
||||
<i class="bi bi-x-lg"></i> Kaldır
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Arama --}}
|
||||
<div style="display:flex;gap:8px;margin-bottom:8px">
|
||||
<input type="text" id="gifAvatarSearch" class="ps-input" placeholder="anime gif ara..." style="flex:1"
|
||||
oninput="debounceGifSearch(this.value)">
|
||||
<button type="button" onclick="debounceGifSearch(document.getElementById('gifAvatarSearch').value,true)"
|
||||
style="background:rgba(0,245,255,.1);border:1px solid rgba(0,245,255,.25);color:#00f5ff;border-radius:10px;padding:0 14px;font-size:.85rem;cursor:pointer;flex-shrink:0;white-space:nowrap">
|
||||
<i class="bi bi-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Sonuç grid --}}
|
||||
<div id="gifAvatarGrid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px;max-height:280px;overflow-y:auto;scrollbar-width:thin"></div>
|
||||
<div id="gifAvatarMsg" style="font-size:.72rem;color:rgba(255,255,255,.3);margin-top:6px;text-align:center"></div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Yorum Arkaplanı --}}
|
||||
@if($u->hasPerk('comment_bg'))
|
||||
<div class="ps-field" style="margin-bottom:24px">
|
||||
<label class="ps-label"><i class="bi bi-fire" style="color:#ff6b35"></i> Yorum Arkaplanı Efekti</label>
|
||||
<div class="prem-picker" style="grid-template-columns:repeat(auto-fill,minmax(100px,1fr))">
|
||||
<label class="prem-pick-item {{ !$u->comment_bg ? 'active' : '' }}">
|
||||
<input type="radio" name="comment_bg" value="">
|
||||
<div style="width:100%;height:36px;border-radius:7px;background:rgba(255,255,255,.05);display:flex;align-items:center;justify-content:center;font-size:.75rem;color:rgba(255,255,255,.3)">— Yok —</div>
|
||||
<span class="prem-pick-lbl">Yok</span>
|
||||
</label>
|
||||
@foreach(\App\Services\PremiumFeatures::COMMENT_BACKGROUNDS as $key => $bg)
|
||||
<label class="prem-pick-item {{ $u->comment_bg === $key ? 'active' : '' }}">
|
||||
<input type="radio" name="comment_bg" value="{{ $key }}">
|
||||
<div class="comment-wrap" data-bg="{{ $key }}" style="width:100%;min-height:36px;border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:.75rem;overflow:hidden">
|
||||
<span>{{ $bg['label'] }}</span>
|
||||
</div>
|
||||
<span class="prem-pick-lbl">{{ $bg['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Kullanıcı Adı Rengi --}}
|
||||
@if($u->hasPerk('username_color'))
|
||||
<div class="ps-field" style="margin-bottom:24px">
|
||||
<label class="ps-label"><i class="bi bi-palette-fill" style="color:#b84dff"></i> Kullanıcı Adı Rengi</label>
|
||||
<div class="prem-picker" style="grid-template-columns:repeat(auto-fill,minmax(120px,1fr))">
|
||||
<label class="prem-pick-item {{ !$u->username_color ? 'active' : '' }}">
|
||||
<input type="radio" name="username_color" value="">
|
||||
<span class="prem-pick-preview" style="color:rgba(200,200,220,.55)">{{ $u->name }}</span>
|
||||
<span class="prem-pick-lbl">Varsayılan</span>
|
||||
</label>
|
||||
@foreach(\App\Services\PremiumFeatures::USERNAME_COLORS as $key => $color)
|
||||
<label class="prem-pick-item {{ $u->username_color === $key ? 'active' : '' }}">
|
||||
<input type="radio" name="username_color" value="{{ $key }}">
|
||||
<span class="prem-pick-preview username-color-{{ $key }}">{{ $u->name }}</span>
|
||||
<span class="prem-pick-lbl">{{ $color['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Profil Çerçevesi --}}
|
||||
@if($u->hasPerk('profile_frame'))
|
||||
<div class="ps-field" style="margin-bottom:24px">
|
||||
<label class="ps-label"><i class="bi bi-circle-fill" style="color:#ffd700"></i> Profil Çerçevesi</label>
|
||||
<div class="prem-picker" style="grid-template-columns:repeat(auto-fill,minmax(82px,1fr))">
|
||||
<label class="prem-pick-item {{ !$u->profile_frame ? 'active' : '' }}">
|
||||
<input type="radio" name="profile_frame" value="">
|
||||
<div style="width:48px;height:48px;border-radius:50%;background:rgba(255,255,255,.07);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:1.1rem;color:rgba(255,255,255,.3)">{{ mb_substr($u->name,0,1) }}</div>
|
||||
<span class="prem-pick-lbl">Yok</span>
|
||||
</label>
|
||||
@foreach(\App\Services\PremiumFeatures::PROFILE_FRAMES as $key => $frame)
|
||||
<label class="prem-pick-item {{ $u->profile_frame === $key ? 'active' : '' }}">
|
||||
<input type="radio" name="profile_frame" value="{{ $key }}">
|
||||
<div class="avatar-wrap" style="width:48px;height:48px">
|
||||
<div class="avatar-frame frame-{{ $key }}"></div>
|
||||
<div style="width:48px;height:48px;border-radius:50%;background:rgba(255,255,255,.12);display:flex;align-items:center;justify-content:center;font-weight:800;font-size:1.1rem;color:rgba(255,255,255,.6);position:relative;z-index:3">{{ mb_substr($u->name,0,1) }}</div>
|
||||
</div>
|
||||
<span class="prem-pick-lbl">{{ $frame['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Özel Rozet/Unvan --}}
|
||||
@if($u->hasPerk('profile_badge'))
|
||||
<div class="ps-field" style="margin-bottom:24px">
|
||||
<label class="ps-label"><i class="bi bi-award-fill" style="color:#ffd700"></i> Özel Rozet / Unvan</label>
|
||||
<input type="text" name="profile_badge" class="ps-input" value="{{ old('profile_badge', $u->profile_badge) }}"
|
||||
placeholder="Anime Uzmanı" maxlength="32">
|
||||
<div style="font-size:.72rem;color:rgba(255,255,255,.35);margin-top:4px">Max 32 karakter. Kullanıcı adının yanında görünür.</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Profil Müziği --}}
|
||||
@if($u->hasPerk('profile_music'))
|
||||
<div class="ps-field" style="margin-bottom:24px">
|
||||
<label class="ps-label"><i class="bi bi-music-note-beamed" style="color:#00f5ff"></i> Profil Müziği URL</label>
|
||||
<input type="url" name="profile_music_url" class="ps-input" value="{{ old('profile_music_url', $u->profile_music_url) }}"
|
||||
placeholder="https://www.youtube.com/watch?v=... veya direkt MP3 URL">
|
||||
<div style="font-size:.72rem;color:rgba(255,255,255,.35);margin-top:4px">YouTube, SoundCloud veya direkt ses dosyası URL'si. Profilini ziyaret edenlere çalar.</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Profil Arka Planı --}}
|
||||
@if($u->hasPerk('profile_bg'))
|
||||
<div class="ps-field" style="margin-bottom:24px">
|
||||
<label class="ps-label"><i class="bi bi-stars" style="color:#b84dff"></i> Profil Arka Planı</label>
|
||||
<div class="prem-picker" style="grid-template-columns:repeat(auto-fill,minmax(100px,1fr))">
|
||||
<label class="prem-pick-item {{ !$u->profile_bg ? 'active' : '' }}">
|
||||
<input type="radio" name="profile_bg" value="">
|
||||
<div style="width:100%;height:36px;border-radius:7px;background:rgba(255,255,255,.05);display:flex;align-items:center;justify-content:center;font-size:.75rem;color:rgba(255,255,255,.3)">— Yok —</div>
|
||||
<span class="prem-pick-lbl">Varsayılan</span>
|
||||
</label>
|
||||
@foreach(\App\Services\PremiumFeatures::PROFILE_BACKGROUNDS as $key => $bg)
|
||||
@php [$c1,$c2] = explode(',', $bg['preview']); @endphp
|
||||
<label class="prem-pick-item {{ $u->profile_bg === $key ? 'active' : '' }}">
|
||||
<input type="radio" name="profile_bg" value="{{ $key }}">
|
||||
<div style="width:100%;height:36px;border-radius:7px;background:linear-gradient(135deg,{{ $c1 }},{{ $c2 }});display:flex;align-items:center;justify-content:center;font-size:.72rem;color:rgba(255,255,255,.8);font-weight:700">{{ $bg['label'] }}</div>
|
||||
<span class="prem-pick-lbl">{{ $bg['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Yorum Aura / Parıltı --}}
|
||||
@if($u->hasPerk('comment_glow'))
|
||||
<div class="ps-field" style="margin-bottom:24px">
|
||||
<label class="ps-label"><i class="bi bi-brightness-high-fill" style="color:#00f5ff"></i> Yorum Aura / Parıltı</label>
|
||||
<div class="prem-picker" style="grid-template-columns:repeat(auto-fill,minmax(90px,1fr))">
|
||||
<label class="prem-pick-item {{ !$u->comment_glow ? 'active' : '' }}">
|
||||
<input type="radio" name="comment_glow" value="">
|
||||
<div style="width:44px;height:44px;border-radius:50%;border:1.5px solid rgba(255,255,255,.15);display:flex;align-items:center;justify-content:center;font-size:.7rem;color:rgba(255,255,255,.3)">—</div>
|
||||
<span class="prem-pick-lbl">Yok</span>
|
||||
</label>
|
||||
@foreach(\App\Services\PremiumFeatures::COMMENT_GLOWS as $key => $glow)
|
||||
<label class="prem-pick-item {{ $u->comment_glow === $key ? 'active' : '' }}">
|
||||
<input type="radio" name="comment_glow" value="{{ $key }}">
|
||||
<div style="width:44px;height:44px;border-radius:50%;border:1.5px solid {{ $glow['color'] }};box-shadow:0 0 12px {{ $glow['color'] }},0 0 24px {{ $glow['color'] }}44;background:{{ $glow['color'] }}22"></div>
|
||||
<span class="prem-pick-lbl">{{ $glow['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Kullanıcı Adı Animasyonu --}}
|
||||
@if($u->hasPerk('username_effect'))
|
||||
<div class="ps-field" style="margin-bottom:24px">
|
||||
<label class="ps-label"><i class="bi bi-lightning-charge-fill" style="color:#ffd700"></i> Kullanıcı Adı Animasyonu</label>
|
||||
<div class="prem-picker" style="grid-template-columns:repeat(auto-fill,minmax(110px,1fr))">
|
||||
<label class="prem-pick-item {{ !$u->username_effect ? 'active' : '' }}">
|
||||
<input type="radio" name="username_effect" value="">
|
||||
<span class="prem-pick-preview" style="color:rgba(200,200,220,.6)">{{ $u->name }}</span>
|
||||
<span class="prem-pick-lbl">Yok</span>
|
||||
</label>
|
||||
@foreach(\App\Services\PremiumFeatures::USERNAME_EFFECTS as $key => $eff)
|
||||
<label class="prem-pick-item {{ $u->username_effect === $key ? 'active' : '' }}">
|
||||
<input type="radio" name="username_effect" value="{{ $key }}">
|
||||
<span class="prem-pick-preview username-effect-{{ $key }}" @if($key==='glitch') data-text="{{ $u->name }}" @endif>{{ $u->name }}</span>
|
||||
<span class="prem-pick-lbl">{{ $eff['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Yorum İmzası --}}
|
||||
@if($u->hasPerk('comment_signature'))
|
||||
<div class="ps-field" style="margin-bottom:24px">
|
||||
<label class="ps-label"><i class="bi bi-pen-fill" style="color:#ff9ec4"></i> Yorum İmzası</label>
|
||||
<input type="text" name="comment_signature" class="ps-input" value="{{ old('comment_signature', $u->comment_signature) }}"
|
||||
placeholder="— Anime severinden sevgiyle ✨" maxlength="100">
|
||||
<div style="font-size:.72rem;color:rgba(255,255,255,.35);margin-top:4px">Max 100 karakter. Her yorumun altında küçük yazıyla görünür.</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Sayfa Giriş Efekti --}}
|
||||
@if($u->hasPerk('entry_effect'))
|
||||
<div class="ps-field" style="margin-bottom:24px">
|
||||
<label class="ps-label"><i class="bi bi-play-circle-fill" style="color:#3fb950"></i> Sayfa Giriş Efekti</label>
|
||||
<div class="prem-picker" style="grid-template-columns:repeat(auto-fill,minmax(100px,1fr))">
|
||||
<label class="prem-pick-item {{ !$u->entry_effect ? 'active' : '' }}">
|
||||
<input type="radio" name="entry_effect" value="">
|
||||
<div style="font-size:1.4rem">🚫</div>
|
||||
<span class="prem-pick-lbl">Yok</span>
|
||||
</label>
|
||||
@foreach(\App\Services\PremiumFeatures::ENTRY_EFFECTS as $key => $eff)
|
||||
<label class="prem-pick-item {{ $u->entry_effect === $key ? 'active' : '' }}">
|
||||
<input type="radio" name="entry_effect" value="{{ $key }}">
|
||||
<div style="font-size:1.4rem">{{ ['fade'=>'✨','slide'=>'⬆️','zoom'=>'🔍','glitch'=>'⚡','wave'=>'🌊'][$key] ?? '✨' }}</div>
|
||||
<span class="prem-pick-lbl">{{ $eff['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Animasyonlu Banner --}}
|
||||
@if($u->hasPerk('animated_banner'))
|
||||
<div class="ps-field" style="margin-bottom:24px">
|
||||
<div class="ps-toggle" style="padding:14px 16px;background:rgba(255,255,255,.04);border-radius:12px;border:1px solid rgba(255,255,255,.08);border-bottom:1px solid rgba(255,255,255,.08)">
|
||||
<div>
|
||||
<div class="ps-toggle-label" style="color:#fff"><i class="bi bi-image-alt" style="color:#00f5ff;margin-right:6px"></i> Animasyonlu Profil Bannerı</div>
|
||||
<div class="ps-toggle-sub">Profil bannerın parıltılı parçacık efektiyle canlanır</div>
|
||||
</div>
|
||||
<label class="ps-switch">
|
||||
<input type="checkbox" name="animated_banner" value="1" {{ $u->animated_banner ? 'checked' : '' }}>
|
||||
<span class="ps-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<button type="submit" class="ps-save" style="background:linear-gradient(135deg,rgba(0,245,255,.15),rgba(184,77,255,.15));border:1px solid rgba(0,245,255,.3);color:#00f5ff">
|
||||
<i class="bi bi-stars me-2"></i>Premium Ayarları Kaydet
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@else
|
||||
{{-- Premium değil — yükseltme banner'ı --}}
|
||||
<div class="ps-card" style="border:1px solid rgba(255,215,0,.2);background:linear-gradient(135deg,rgba(255,215,0,.04),rgba(255,140,0,.04));text-align:center;padding:32px 24px">
|
||||
<div style="font-size:2rem;margin-bottom:12px">✨</div>
|
||||
<div style="font-weight:700;font-size:1rem;color:#ffd700;margin-bottom:6px">Premium Kozmetikler</div>
|
||||
<div style="font-size:.85rem;color:rgba(255,255,255,.5);margin-bottom:16px">
|
||||
Animasyonlu yorum arkaplanları, renkli kullanıcı adı, profil çerçevesi ve daha fazlası için premium üye ol.
|
||||
</div>
|
||||
<a href="{{ route('premium.plans') }}" class="ps-save" style="display:inline-flex;background:linear-gradient(135deg,#ff8c00,#ffd700);color:#000;font-weight:800;text-decoration:none;padding:10px 28px;border-radius:10px">
|
||||
<i class="bi bi-stars me-2"></i>Premium'a Geç
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Premium picker: radio seçilince parent label'a .active sınıfı ekle
|
||||
document.querySelectorAll('.prem-picker input[type="radio"]').forEach(radio => {
|
||||
radio.addEventListener('change', () => {
|
||||
const picker = radio.closest('.prem-picker');
|
||||
picker.querySelectorAll('.prem-pick-item').forEach(el => el.classList.remove('active'));
|
||||
radio.closest('.prem-pick-item').classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
const wrap = document.getElementById('psWrap');
|
||||
|
||||
function setColor(hex, el) {
|
||||
wrap.style.setProperty('--ps-color', hex);
|
||||
document.getElementById('profileColorInput').value = hex;
|
||||
document.getElementById('colorLabel').textContent = hex;
|
||||
document.getElementById('colorLabel').style.color = hex;
|
||||
document.getElementById('customColorPicker').value = hex;
|
||||
|
||||
document.querySelectorAll('.ps-color-swatch').forEach(s => s.classList.remove('active'));
|
||||
if (el) el.classList.add('active');
|
||||
}
|
||||
|
||||
// Bio karakter sayacı
|
||||
const bioTextarea = document.querySelector('[name="bio"]');
|
||||
const bioCount = document.getElementById('bioCount');
|
||||
if (bioTextarea) {
|
||||
bioTextarea.addEventListener('input', () => {
|
||||
bioCount.textContent = bioTextarea.value.length;
|
||||
});
|
||||
}
|
||||
|
||||
// Banner önizleme
|
||||
document.getElementById('bannerInput').addEventListener('change', function() {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const img = document.querySelector('.ps-banner-img');
|
||||
if (img.tagName === 'IMG') {
|
||||
img.src = e.target.result;
|
||||
} else {
|
||||
const newImg = document.createElement('img');
|
||||
newImg.className = 'ps-banner-img';
|
||||
newImg.src = e.target.result;
|
||||
img.replaceWith(newImg);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
// Avatar önizleme
|
||||
document.getElementById('avatarInput').addEventListener('change', function() {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const avatarDiv = document.querySelector('.ps-avatar');
|
||||
avatarDiv.innerHTML = `<img src="${e.target.result}" alt="Avatar">`;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
// ── GIF Avatar Picker ─────────────────────────────────────────────────────
|
||||
let gifSearchTimer = null;
|
||||
|
||||
function debounceGifSearch(q, immediate = false) {
|
||||
clearTimeout(gifSearchTimer);
|
||||
if (!q.trim()) { document.getElementById('gifAvatarGrid').innerHTML = ''; return; }
|
||||
const delay = immediate ? 0 : 500;
|
||||
gifSearchTimer = setTimeout(() => searchGifAvatar(q.trim()), delay);
|
||||
}
|
||||
|
||||
async function searchGifAvatar(q) {
|
||||
const grid = document.getElementById('gifAvatarGrid');
|
||||
const msg = document.getElementById('gifAvatarMsg');
|
||||
grid.innerHTML = '<div style="grid-column:1/-1;text-align:center;padding:20px;color:rgba(255,255,255,.3);font-size:.8rem"><i class="bi bi-arrow-repeat"></i> Aranıyor...</div>';
|
||||
msg.textContent = '';
|
||||
try {
|
||||
const res = await fetch(`/comments/gif-search?q=${encodeURIComponent(q)}`);
|
||||
const data = await res.json();
|
||||
const gifs = data.results ?? [];
|
||||
if (!gifs.length) {
|
||||
grid.innerHTML = '';
|
||||
msg.textContent = 'Sonuç bulunamadı.';
|
||||
return;
|
||||
}
|
||||
grid.innerHTML = gifs.map(g => `
|
||||
<div onclick="selectGifAvatar('${g.url.replace(/'/g,"\\'")}','${g.preview.replace(/'/g,"\\'")}','${(g.url).replace(/'/g,"\\'")}' )"
|
||||
style="cursor:pointer;border-radius:8px;overflow:hidden;border:2px solid transparent;transition:border-color .15s,transform .15s;aspect-ratio:1;background:#111"
|
||||
onmouseover="this.style.borderColor='rgba(0,245,255,.5)';this.style.transform='scale(1.04)'"
|
||||
onmouseout="this.style.borderColor='transparent';this.style.transform='scale(1)'">
|
||||
<img src="${g.preview}" alt="" loading="lazy" style="width:100%;height:100%;object-fit:cover;display:block">
|
||||
</div>`).join('');
|
||||
} catch(e) {
|
||||
grid.innerHTML = '';
|
||||
msg.textContent = 'Arama başarısız oldu.';
|
||||
}
|
||||
}
|
||||
|
||||
function selectGifAvatar(url, preview) {
|
||||
document.getElementById('gifAvatarInput').value = url;
|
||||
document.getElementById('gifAvatarThumb').src = preview;
|
||||
document.getElementById('gifAvatarUrl').textContent = url;
|
||||
document.getElementById('gifAvatarPreview').style.display = '';
|
||||
document.getElementById('gifAvatarGrid').innerHTML = '';
|
||||
document.getElementById('gifAvatarSearch').value = '';
|
||||
document.getElementById('gifAvatarMsg').textContent = '';
|
||||
}
|
||||
|
||||
function clearGifAvatar() {
|
||||
document.getElementById('gifAvatarInput').value = '';
|
||||
document.getElementById('gifAvatarPreview').style.display = 'none';
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,825 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', $user->name . ' — Profil — Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
/* ═══════════════════════════ PROFILE ═══════════════════════════ */
|
||||
|
||||
/* ── Hero banner ──────────────────────────────────────────────── */
|
||||
.pr-hero{
|
||||
position:relative;overflow:hidden;
|
||||
background:#05050e;
|
||||
padding-bottom:0;
|
||||
}
|
||||
.pr-hero-bg{
|
||||
position:absolute;inset:0;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 20% 50%, rgba(0,245,255,.06), transparent),
|
||||
radial-gradient(ellipse 60% 80% at 80% 30%, rgba(255,45,125,.05), transparent);
|
||||
}
|
||||
.pr-hero-inner{
|
||||
position:relative;z-index:1;
|
||||
max-width:960px;margin:0 auto;
|
||||
padding:calc(var(--nav-h) + 32px) 24px 0;
|
||||
}
|
||||
.pr-top{display:flex;align-items:flex-start;gap:20px;flex-wrap:wrap;margin-bottom:28px}
|
||||
.pr-top-main{display:flex;align-items:flex-end;gap:20px;flex:1;min-width:0}
|
||||
|
||||
/* ── Avatar ───────────────────────────────────────────────────── */
|
||||
.pr-avatar{
|
||||
width:96px;height:96px;border-radius:24px;flex-shrink:0;
|
||||
background:linear-gradient(135deg,var(--accent2),var(--accent));
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:2.4rem;font-weight:900;color:#fff;overflow:hidden;
|
||||
box-shadow:0 0 0 3px rgba(0,245,255,.2),0 16px 48px rgba(0,0,0,.5);
|
||||
}
|
||||
.pr-info{flex:1;min-width:0;padding-bottom:4px}
|
||||
.pr-name{font-size:1.7rem;font-weight:900;color:#fff;line-height:1.1;margin-bottom:4px}
|
||||
.pr-handle{font-size:.84rem;color:rgba(255,255,255,.35);margin-bottom:10px}
|
||||
.pr-badges{display:flex;gap:7px;flex-wrap:wrap}
|
||||
.pbadge{padding:4px 12px;border-radius:20px;font-size:.7rem;font-weight:700;letter-spacing:.05em;display:inline-flex;align-items:center;gap:5px}
|
||||
.pbadge-admin{background:rgba(255,45,125,.15);border:1px solid rgba(255,45,125,.35);color:var(--accent)}
|
||||
.pbadge-premium{background:rgba(255,208,0,.12);border:1px solid rgba(255,208,0,.3);color:#ffd700}
|
||||
.pbadge-member{background:rgba(0,245,255,.07);border:1px solid rgba(0,245,255,.2);color:var(--accent2)}
|
||||
|
||||
/* ── Stat pills ───────────────────────────────────────────────── */
|
||||
.pr-stats{
|
||||
display:flex;gap:6px;flex-wrap:wrap;
|
||||
padding:20px 0 0;
|
||||
border-top:1px solid rgba(255,255,255,.06);
|
||||
margin-top:8px;
|
||||
}
|
||||
.pr-stat{
|
||||
display:flex;flex-direction:column;align-items:center;
|
||||
padding:10px 20px;border-radius:12px;
|
||||
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);
|
||||
min-width:80px;flex:1;
|
||||
}
|
||||
.pr-stat .v{font-size:1.25rem;font-weight:800;color:#fff;line-height:1}
|
||||
.pr-stat .l{font-size:.62rem;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.35);margin-top:4px;font-weight:600}
|
||||
|
||||
/* ── Tab nav ──────────────────────────────────────────────────── */
|
||||
.pr-tabs{
|
||||
display:flex;gap:2px;
|
||||
border-top:1px solid rgba(255,255,255,.06);
|
||||
margin-top:20px;
|
||||
overflow-x:auto;scrollbar-width:none;
|
||||
}
|
||||
.pr-tabs::-webkit-scrollbar{display:none}
|
||||
.pr-tab{
|
||||
padding:12px 18px;background:none;border:none;
|
||||
border-bottom:2px solid transparent;
|
||||
color:rgba(255,255,255,.35);font-size:.82rem;font-weight:600;
|
||||
cursor:pointer;font-family:inherit;transition:all .15s;
|
||||
white-space:nowrap;flex-shrink:0;display:flex;align-items:center;gap:6px;
|
||||
margin-bottom:-1px;
|
||||
}
|
||||
.pr-tab:hover{color:rgba(255,255,255,.65)}
|
||||
.pr-tab.on{color:var(--accent2);border-bottom-color:var(--accent2)}
|
||||
|
||||
/* ── Body ─────────────────────────────────────────────────────── */
|
||||
.pr-body{max-width:960px;margin:0 auto;padding:32px 24px 64px}
|
||||
.pr-pane{display:none}
|
||||
.pr-pane.on{display:block}
|
||||
|
||||
/* ── Section header ───────────────────────────────────────────── */
|
||||
.pr-sec-h{
|
||||
font-size:.68rem;text-transform:uppercase;letter-spacing:.12em;
|
||||
color:rgba(255,255,255,.3);font-weight:700;
|
||||
margin-bottom:16px;display:flex;align-items:center;gap:8px;
|
||||
}
|
||||
.pr-sec-h::before{content:'';width:3px;height:14px;background:linear-gradient(180deg,var(--accent2),var(--accent));border-radius:2px}
|
||||
|
||||
/* ── Continue watching cards ──────────────────────────────────── */
|
||||
.cw-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:12px}
|
||||
.cw-card{
|
||||
border-radius:10px;overflow:hidden;
|
||||
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);
|
||||
text-decoration:none;display:block;
|
||||
transition:transform .15s,border-color .15s;
|
||||
}
|
||||
.cw-card:hover{transform:translateY(-3px);border-color:rgba(0,245,255,.25)}
|
||||
.cw-thumb{position:relative;aspect-ratio:2/3;overflow:hidden}
|
||||
.cw-thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .3s}
|
||||
.cw-card:hover .cw-thumb img{transform:scale(1.05)}
|
||||
.cw-thumb-ph{width:100%;height:100%;background:rgba(255,255,255,.04);display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.2);font-size:2rem}
|
||||
.cw-grad{position:absolute;inset:0;background:linear-gradient(to top,rgba(0,0,0,.85) 0%,transparent 55%)}
|
||||
.cw-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:38px;height:38px;border-radius:50%;background:rgba(255,45,125,.85);display:flex;align-items:center;justify-content:center}
|
||||
.cw-play i{color:#fff;font-size:.95rem}
|
||||
.cw-progress{position:absolute;bottom:28px;left:10px;right:10px}
|
||||
.cw-prog-bar{height:3px;background:rgba(255,255,255,.2);border-radius:2px;overflow:hidden}
|
||||
.cw-prog-fill{height:100%;background:var(--accent);border-radius:2px}
|
||||
.cw-ep{position:absolute;bottom:8px;left:10px;font-size:.62rem;color:rgba(255,255,255,.6);font-weight:600}
|
||||
.cw-info{padding:8px 10px}
|
||||
.cw-info-title{font-size:.76rem;font-weight:600;color:rgba(255,255,255,.8);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
|
||||
/* ── Achievement grid ─────────────────────────────────────────── */
|
||||
.ach-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}
|
||||
.ach-card{
|
||||
display:flex;align-items:center;gap:14px;
|
||||
background:rgba(255,255,255,.03);
|
||||
border:1px solid rgba(255,255,255,.07);
|
||||
border-radius:12px;padding:14px 16px;
|
||||
transition:border-color .15s;
|
||||
}
|
||||
.ach-card.earned{background:rgba(255,255,255,.05)}
|
||||
.ach-card:not(.earned){opacity:.35}
|
||||
.ach-icon-wrap{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;flex-shrink:0}
|
||||
.ach-body .ach-title{font-size:.85rem;font-weight:700;color:#fff;margin-bottom:2px}
|
||||
.ach-body .ach-desc{font-size:.72rem;color:rgba(255,255,255,.35);line-height:1.4}
|
||||
.ach-body .ach-date{font-size:.66rem;margin-top:4px;font-weight:600}
|
||||
|
||||
/* ── Watchlist tabs ───────────────────────────────────────────── */
|
||||
.wl-filter{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:20px}
|
||||
.wl-filter-btn{
|
||||
padding:6px 14px;border-radius:20px;border:1px solid rgba(255,255,255,.1);
|
||||
background:none;color:rgba(255,255,255,.45);font-size:.78rem;font-weight:600;
|
||||
cursor:pointer;font-family:inherit;transition:all .15s;
|
||||
}
|
||||
.wl-filter-btn:hover{border-color:rgba(255,255,255,.25);color:rgba(255,255,255,.7)}
|
||||
.wl-filter-btn.on{background:rgba(0,245,255,.1);border-color:rgba(0,245,255,.3);color:var(--accent2)}
|
||||
.wl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}
|
||||
.wl-card{
|
||||
border-radius:9px;overflow:hidden;text-decoration:none;display:block;
|
||||
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);
|
||||
transition:transform .15s,border-color .15s;
|
||||
}
|
||||
.wl-card:hover{transform:translateY(-2px);border-color:rgba(255,255,255,.18)}
|
||||
.wl-card img{width:100%;aspect-ratio:2/3;object-fit:cover;display:block}
|
||||
.wl-card-ph{aspect-ratio:2/3;background:rgba(255,255,255,.04);display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.2)}
|
||||
.wl-card-title{padding:7px 8px;font-size:.72rem;font-weight:600;color:rgba(255,255,255,.7);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
|
||||
/* ── Heatmap ──────────────────────────────────────────────────── */
|
||||
.heatmap-wrap{
|
||||
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);
|
||||
border-radius:14px;padding:20px 24px;
|
||||
}
|
||||
.heatmap-scroll{overflow-x:auto;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.1) transparent;padding-bottom:6px}
|
||||
.heatmap-grid{display:flex;gap:3px;min-width:max-content}
|
||||
.hm-col{display:flex;flex-direction:column;gap:3px}
|
||||
.hm-cell{width:13px;height:13px;border-radius:3px}
|
||||
.heatmap-legend{display:flex;align-items:center;gap:10px;margin-top:14px;font-size:.7rem;color:rgba(255,255,255,.3)}
|
||||
.heatmap-legend-cells{display:flex;gap:3px;align-items:center}
|
||||
.heatmap-stats{display:flex;gap:20px;margin-top:12px;font-size:.78rem;color:rgba(255,255,255,.4)}
|
||||
|
||||
/* ── Notes ────────────────────────────────────────────────────── */
|
||||
.note-card{
|
||||
background:rgba(255,255,255,.03);border:1px solid rgba(255,215,0,.12);
|
||||
border-radius:12px;padding:16px 18px;margin-bottom:10px;
|
||||
}
|
||||
.note-meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:8px}
|
||||
.note-anime{font-size:.8rem;font-weight:700;color:var(--accent2);text-decoration:none}
|
||||
.note-anime:hover{text-decoration:underline}
|
||||
.note-ep{font-size:.72rem;color:rgba(255,255,255,.3)}
|
||||
.note-ts{font-size:.72rem;color:#ffd700;display:inline-flex;align-items:center;gap:4px}
|
||||
.note-date{font-size:.68rem;color:rgba(255,255,255,.25);margin-left:auto}
|
||||
.note-text{font-size:.87rem;color:rgba(255,255,255,.65);line-height:1.6}
|
||||
|
||||
/* ── Comments ─────────────────────────────────────────────────── */
|
||||
.cm-card{
|
||||
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);
|
||||
border-radius:12px;padding:14px 16px;margin-bottom:10px;
|
||||
}
|
||||
.cm-top{display:flex;align-items:center;gap:10px;margin-bottom:10px}
|
||||
.cm-ava{width:30px;height:30px;border-radius:10px;background:linear-gradient(135deg,var(--accent2),var(--accent));display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:800;color:#fff;flex-shrink:0}
|
||||
.cm-name{font-size:.84rem;font-weight:700;color:rgba(255,255,255,.8)}
|
||||
.cm-time{font-size:.72rem;color:rgba(255,255,255,.25);margin-left:auto}
|
||||
.cm-text{font-size:.88rem;color:rgba(255,255,255,.6);line-height:1.6}
|
||||
.cm-gif{max-width:200px;border-radius:8px;margin-top:8px}
|
||||
|
||||
.pr-empty{text-align:center;padding:56px 20px;color:rgba(255,255,255,.2);font-size:.9rem}
|
||||
.pr-empty i{font-size:2.5rem;display:block;margin-bottom:12px;opacity:.25}
|
||||
|
||||
/* ── Responsive ───────────────────────────────────────────────── */
|
||||
@media(max-width:600px){
|
||||
.pr-hero-inner{padding:calc(var(--nav-h) + 16px) 16px 0}
|
||||
.pr-body{padding:24px 16px 48px}
|
||||
.pr-avatar{width:72px;height:72px;border-radius:18px;font-size:1.8rem}
|
||||
.pr-name{font-size:1.3rem}
|
||||
.pr-stat{padding:8px 12px;min-width:60px}
|
||||
.pr-stat .v{font-size:1rem}
|
||||
.ach-grid{grid-template-columns:1fr}
|
||||
.cw-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr))}
|
||||
.pr-top{flex-direction:column;gap:14px}
|
||||
.pr-top-main{align-items:flex-start}
|
||||
.pr-edit-btn{align-self:stretch;justify-content:center}
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
PROFILE PAGE PREMIUM ENHANCEMENTS
|
||||
═══════════════════════════════════════════════════ */
|
||||
|
||||
/* Avatar animated rainbow border */
|
||||
@keyframes avatar-border-spin{
|
||||
0%{background-position:0% 50%}
|
||||
100%{background-position:200% 50%}
|
||||
}
|
||||
.pr-avatar{
|
||||
position:relative;
|
||||
}
|
||||
.pr-avatar::before{
|
||||
content:'';position:absolute;inset:-2px;border-radius:26px;z-index:-1;
|
||||
background:linear-gradient(135deg,var(--accent2),var(--accent),var(--accent3),var(--accent2));
|
||||
background-size:300% 300%;
|
||||
animation:avatar-border-spin 3s linear infinite;
|
||||
}
|
||||
|
||||
/* Username gradient */
|
||||
.pr-name{
|
||||
background:linear-gradient(135deg,#fff 0%,rgba(255,255,255,.85) 60%,rgba(240,244,255,.6) 100%);
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
|
||||
}
|
||||
|
||||
/* Stats strip enhanced */
|
||||
.pr-stats{
|
||||
background:rgba(255,255,255,.02);
|
||||
border-radius:14px;
|
||||
padding:14px;
|
||||
border:1px solid rgba(255,255,255,.05);
|
||||
gap:8px;
|
||||
}
|
||||
.pr-stat{
|
||||
border-radius:10px;
|
||||
background:rgba(255,255,255,.04);
|
||||
border:1px solid rgba(255,255,255,.07);
|
||||
transition:all .2s;
|
||||
}
|
||||
.pr-stat:hover{
|
||||
background:rgba(0,245,255,.06);
|
||||
border-color:rgba(0,245,255,.2);
|
||||
transform:translateY(-2px);
|
||||
}
|
||||
.pr-stat .v{
|
||||
background:linear-gradient(135deg,var(--accent2),var(--accent));
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
|
||||
font-size:1.35rem;
|
||||
}
|
||||
|
||||
/* Tab enhanced */
|
||||
.pr-tab.on{
|
||||
color:var(--accent2);
|
||||
text-shadow:0 0 12px rgba(0,245,255,.3);
|
||||
}
|
||||
|
||||
/* Entrance animations */
|
||||
@keyframes pr-fade-up{
|
||||
from{opacity:0;transform:translateY(18px)}
|
||||
to{opacity:1;transform:none}
|
||||
}
|
||||
.pr-avatar{animation:pr-fade-up .5s cubic-bezier(.22,1,.36,1) .1s both}
|
||||
.pr-info{animation:pr-fade-up .5s cubic-bezier(.22,1,.36,1) .2s both}
|
||||
.pr-stats{animation:pr-fade-up .5s cubic-bezier(.22,1,.36,1) .3s both}
|
||||
.pr-tabs{animation:pr-fade-up .4s cubic-bezier(.22,1,.36,1) .4s both}
|
||||
|
||||
/* Achievement cards enhanced */
|
||||
.ach-card.earned{
|
||||
border:1px solid rgba(255,255,255,.1);
|
||||
background:linear-gradient(135deg,rgba(255,255,255,.05),rgba(0,245,255,.02));
|
||||
transition:all .2s;
|
||||
}
|
||||
.ach-card.earned:hover{
|
||||
border-color:rgba(0,245,255,.2);
|
||||
transform:translateY(-2px);
|
||||
box-shadow:0 8px 24px rgba(0,0,0,.4),0 0 20px rgba(0,245,255,.05);
|
||||
}
|
||||
|
||||
/* Watchlist cards enhanced */
|
||||
.wl-card:hover{
|
||||
transform:translateY(-3px);
|
||||
border-color:rgba(0,245,255,.2);
|
||||
box-shadow:0 8px 24px rgba(0,0,0,.4);
|
||||
}
|
||||
|
||||
/* Heatmap wrap */
|
||||
.heatmap-wrap{
|
||||
background:linear-gradient(135deg,rgba(255,255,255,.03),rgba(0,245,255,.01));
|
||||
border:1px solid rgba(0,245,255,.07);
|
||||
}
|
||||
|
||||
/* Profile edit button */
|
||||
.pr-edit-btn{
|
||||
transition:all .18s;
|
||||
}
|
||||
.pr-edit-btn:hover{
|
||||
background:rgba(0,245,255,.12);
|
||||
border-color:rgba(0,245,255,.4);
|
||||
color:var(--accent2);
|
||||
box-shadow:0 0 20px rgba(0,245,255,.15);
|
||||
}
|
||||
|
||||
/* Body panes reveal */
|
||||
.pr-body{animation:pr-fade-up .5s cubic-bezier(.22,1,.36,1) .2s both}
|
||||
|
||||
/* Profile color accent on section headers */
|
||||
.pr-sec-h::before{
|
||||
box-shadow:0 0 10px rgba(0,245,255,.5);
|
||||
}
|
||||
|
||||
/* ── Swipe history ───────────────────────────────────── */
|
||||
.swipe-filter {
|
||||
padding: 5px 13px; border-radius: 999px; font-size: .68rem;
|
||||
background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.08);
|
||||
color: var(--text3); cursor: pointer; font-family: var(--font);
|
||||
transition: all .15s; display: inline-flex; align-items: center; gap: 5px;
|
||||
}
|
||||
.swipe-filter:hover { border-color: rgba(0,245,255,.25); color: var(--text2); }
|
||||
.swipe-filter.on { background: rgba(0,245,255,.1); border-color: var(--accent2); color: var(--accent2); }
|
||||
.swipe-f-count {
|
||||
font-size: .6rem; background: rgba(255,255,255,.08); border-radius: 999px;
|
||||
padding: 1px 6px; color: var(--text3);
|
||||
}
|
||||
.swipe-filter.on .swipe-f-count { background: rgba(0,245,255,.15); color: var(--accent2); }
|
||||
|
||||
.swipe-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.swipe-item {
|
||||
text-decoration: none; border-radius: 12px; overflow: hidden;
|
||||
background: var(--surface2); display: flex; flex-direction: column;
|
||||
transition: transform .18s, box-shadow .18s;
|
||||
}
|
||||
.swipe-item:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,.4); }
|
||||
.swipe-poster {
|
||||
position: relative; aspect-ratio: 2/3; overflow: hidden;
|
||||
}
|
||||
.swipe-poster img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.swipe-poster-ph {
|
||||
width: 100%; height: 100%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: var(--text3); font-size: 1.5rem;
|
||||
}
|
||||
.swipe-dir-badge {
|
||||
position: absolute; top: 6px; right: 6px;
|
||||
width: 22px; height: 22px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: .6rem; font-weight: 700;
|
||||
}
|
||||
.badge-like { background: rgba(0,245,255,.18); color: var(--accent2); border: 1px solid rgba(0,245,255,.3); }
|
||||
.badge-skip { background: rgba(255,45,125,.15); color: #ff2d7d; border: 1px solid rgba(255,45,125,.3); }
|
||||
.swipe-info { padding: 7px 8px 9px; }
|
||||
.swipe-title {
|
||||
font-size: .68rem; font-weight: 600; color: var(--text);
|
||||
line-height: 1.3; margin-bottom: 4px;
|
||||
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
||||
}
|
||||
.swipe-meta { display: flex; gap: 6px; font-size: .6rem; color: var(--text3); flex-wrap: wrap; }
|
||||
.swipe-item.skip .swipe-poster { opacity: .65; }
|
||||
.swipe-item.skip .swipe-poster img { filter: grayscale(25%); }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
|
||||
@php $pColor = $user->profile_color ?? '#00f5ff'; @endphp
|
||||
|
||||
{{-- Hero --}}
|
||||
<div class="pr-hero" style="--pr-color:{{ $pColor }}">
|
||||
|
||||
{{-- Banner --}}
|
||||
@if($user->banner_image)
|
||||
<div style="position:absolute;inset:0;overflow:hidden">
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($user->banner_image) }}" alt=""
|
||||
style="width:100%;height:100%;object-fit:cover;opacity:.35;filter:blur(1px)">
|
||||
<div style="position:absolute;inset:0;background:linear-gradient(to bottom,rgba(5,5,14,.3) 0%,rgba(5,5,14,.85) 100%)"></div>
|
||||
</div>
|
||||
@else
|
||||
<div class="pr-hero-bg" style="background:radial-gradient(ellipse 80% 60% at 20% 50%,{{ $pColor }}18,transparent),radial-gradient(ellipse 60% 80% at 80% 30%,{{ $pColor }}10,transparent)"></div>
|
||||
@endif
|
||||
|
||||
<div class="pr-hero-inner">
|
||||
|
||||
{{-- Top row: avatar + info + edit button --}}
|
||||
<div class="pr-top">
|
||||
<div class="pr-top-main">
|
||||
{{-- Avatar --}}
|
||||
<div class="pr-avatar" style="background:linear-gradient(135deg,{{ $pColor }},{{ $pColor }}88);box-shadow:0 0 0 3px {{ $pColor }}33,0 16px 48px rgba(0,0,0,.5)">
|
||||
@if($user->avatar)
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($user->avatar) }}" alt="{{ $user->name }}"
|
||||
style="width:100%;height:100%;object-fit:cover;border-radius:22px">
|
||||
@else
|
||||
{{ mb_strtoupper(mb_substr($user->name,0,1)) }}
|
||||
@endif
|
||||
</div>
|
||||
<div class="pr-info">
|
||||
<div class="pr-name">{{ $user->name }}</div>
|
||||
@if($user->username)
|
||||
<div class="pr-handle">@php echo '@' . e($user->username); @endphp</div>
|
||||
@endif
|
||||
@if($user->bio)
|
||||
<div style="font-size:.83rem;color:rgba(255,255,255,.55);margin-bottom:10px;max-width:500px;line-height:1.5">{{ $user->bio }}</div>
|
||||
@endif
|
||||
|
||||
{{-- Sosyal linkler --}}
|
||||
@if($user->twitter || $user->instagram || $user->discord || $user->website)
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px;align-items:center">
|
||||
@if($user->twitter)
|
||||
<a href="https://x.com/{{ $user->twitter }}" target="_blank" rel="noopener"
|
||||
style="color:rgba(255,255,255,.5);font-size:.82rem;text-decoration:none;display:flex;align-items:center;gap:4px;transition:color .2s"
|
||||
onmouseover="this.style.color='{{ $pColor }}'" onmouseout="this.style.color='rgba(255,255,255,.5)'">
|
||||
<i class="bi bi-twitter-x"></i> {{ $user->twitter }}
|
||||
</a>
|
||||
@endif
|
||||
@if($user->instagram)
|
||||
<a href="https://instagram.com/{{ $user->instagram }}" target="_blank" rel="noopener"
|
||||
style="color:rgba(255,255,255,.5);font-size:.82rem;text-decoration:none;display:flex;align-items:center;gap:4px;transition:color .2s"
|
||||
onmouseover="this.style.color='{{ $pColor }}'" onmouseout="this.style.color='rgba(255,255,255,.5)'">
|
||||
<i class="bi bi-instagram"></i> {{ $user->instagram }}
|
||||
</a>
|
||||
@endif
|
||||
@if($user->discord)
|
||||
<span style="color:rgba(255,255,255,.5);font-size:.82rem;display:flex;align-items:center;gap:4px">
|
||||
<i class="bi bi-discord"></i> {{ $user->discord }}
|
||||
</span>
|
||||
@endif
|
||||
@if($user->website)
|
||||
<a href="{{ $user->website }}" target="_blank" rel="noopener"
|
||||
style="color:rgba(255,255,255,.5);font-size:.82rem;text-decoration:none;display:flex;align-items:center;gap:4px;transition:color .2s"
|
||||
onmouseover="this.style.color='{{ $pColor }}'" onmouseout="this.style.color='rgba(255,255,255,.5)'">
|
||||
<i class="bi bi-globe2"></i> {{ parse_url($user->website, PHP_URL_HOST) }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pr-badges">
|
||||
@if($user->role==='admin')
|
||||
<span class="pbadge pbadge-admin"><i class="bi bi-shield-fill"></i> Admin</span>
|
||||
@elseif($user->role==='moderator')
|
||||
<span class="pbadge pbadge-admin"><i class="bi bi-shield-half"></i> Moderatör</span>
|
||||
@endif
|
||||
@if($user->isPremium())
|
||||
<span class="pbadge pbadge-premium"><i class="bi bi-star-fill"></i> Premium</span>
|
||||
@else
|
||||
<span class="pbadge pbadge-member"><i class="bi bi-person-check"></i> Üye</span>
|
||||
@endif
|
||||
<span class="pbadge" style="background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);color:rgba(255,255,255,.35)"><i class="bi bi-calendar3"></i> {{ $user->created_at->format('Y') }}'den beri</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Ayarlar butonu — ayrı satırda, üstte sabit --}}
|
||||
<a href="{{ route('profile.settings') }}" class="pr-edit-btn"
|
||||
style="flex-shrink:0;padding:8px 16px;border-radius:10px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.12);color:rgba(255,255,255,.6);font-size:.82rem;text-decoration:none;display:flex;align-items:center;gap:7px;transition:all .2s;white-space:nowrap;align-self:flex-start;margin-top:4px"
|
||||
onmouseover="this.style.background='rgba(255,255,255,.12)';this.style.color='#fff'"
|
||||
onmouseout="this.style.background='rgba(255,255,255,.07)';this.style.color='rgba(255,255,255,.6)'">
|
||||
<i class="bi bi-pencil-square"></i> Profili Düzenle
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- Stats --}}
|
||||
<div class="pr-stats">
|
||||
<div class="pr-stat">
|
||||
<span class="v">{{ $watchStats['episodes'] }}</span>
|
||||
<span class="l">Bölüm</span>
|
||||
</div>
|
||||
<div class="pr-stat">
|
||||
<span class="v">{{ $watchStats['hours'] }}</span>
|
||||
<span class="l">Saat</span>
|
||||
</div>
|
||||
<div class="pr-stat">
|
||||
<span class="v">{{ $watchStats['watchlist'] }}</span>
|
||||
<span class="l">Listede</span>
|
||||
</div>
|
||||
<div class="pr-stat">
|
||||
<span class="v">{{ $commentCount }}</span>
|
||||
<span class="l">Yorum</span>
|
||||
</div>
|
||||
@if($allAchievements->count())
|
||||
<div class="pr-stat">
|
||||
<span class="v">{{ $achievements->count() }}</span>
|
||||
<span class="l">Başarım</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Tab nav --}}
|
||||
<div class="pr-tabs" id="pr-tabs">
|
||||
@if($continueItems->count())
|
||||
<button class="pr-tab on" data-pane="continue"><i class="bi bi-play-circle"></i> Devam Et</button>
|
||||
@endif
|
||||
<button class="pr-tab {{ !$continueItems->count() ? 'on' : '' }}" data-pane="watchlist"><i class="bi bi-bookmark"></i> Listem</button>
|
||||
<button class="pr-tab" data-pane="activity"><i class="bi bi-graph-up"></i> Aktivite</button>
|
||||
@if($allAchievements->count())
|
||||
<button class="pr-tab" data-pane="achievements"><i class="bi bi-trophy"></i> Başarımlar</button>
|
||||
@endif
|
||||
<button class="pr-tab" data-pane="notes"><i class="bi bi-journal-text"></i> Notlar @if($episodeNotes->count())<span style="font-size:.68rem;background:rgba(0,245,255,.15);color:var(--accent2);border-radius:10px;padding:1px 6px;margin-left:2px">{{ $episodeNotes->count() }}</span>@endif</button>
|
||||
<button class="pr-tab" data-pane="comments"><i class="bi bi-chat-dots"></i> Yorumlar</button>
|
||||
<button class="pr-tab" data-pane="swipes"><i class="bi bi-hand-index-thumb"></i> Keşfet @if($swipeHistory->count())<span style="font-size:.65rem;background:rgba(0,245,255,.12);color:var(--accent2);border-radius:10px;padding:1px 6px;margin-left:2px">{{ $swipeHistory->count() }}</span>@endif</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Body --}}
|
||||
<div class="pr-body">
|
||||
|
||||
{{-- DEVAM ET --}}
|
||||
@if($continueItems->count())
|
||||
<div class="pr-pane on" id="pane-continue">
|
||||
<div class="pr-sec-h"><span>Kaldığın Yerden Devam Et</span></div>
|
||||
<div class="cw-grid">
|
||||
@foreach($continueItems as $cw)
|
||||
@php $anime = $cw->anime; @endphp
|
||||
@if(!$anime) @continue @endif
|
||||
<a href="{{ route('watch',[$anime->slug,$cw->season_number,$cw->episode_number]) }}" class="cw-card">
|
||||
<div class="cw-thumb">
|
||||
@if($anime->cover_image)
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($anime->cover_image) }}" alt="">
|
||||
@else
|
||||
<div class="cw-thumb-ph"><i class="bi bi-image"></i></div>
|
||||
@endif
|
||||
<div class="cw-grad"></div>
|
||||
<div class="cw-play"><i class="bi bi-play-fill"></i></div>
|
||||
<div class="cw-progress">
|
||||
<div class="cw-prog-bar"><div class="cw-prog-fill" style="width:{{ $cw->percent_complete }}%"></div></div>
|
||||
</div>
|
||||
<div class="cw-ep">S{{ str_pad($cw->season_number,2,'0',STR_PAD_LEFT) }}E{{ str_pad($cw->episode_number,2,'0',STR_PAD_LEFT) }} · {{ $cw->percent_complete }}%</div>
|
||||
</div>
|
||||
<div class="cw-info"><div class="cw-info-title">{{ $anime->title }}</div></div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- İZLEME LİSTESİ --}}
|
||||
<div class="pr-pane {{ !$continueItems->count() ? 'on' : '' }}" id="pane-watchlist">
|
||||
<div class="pr-sec-h" style="display:flex;align-items:center;justify-content:space-between">
|
||||
<span>İzleme Listesi</span>
|
||||
@if(auth()->user()->hasPerk('watchlist_export'))
|
||||
<div style="display:flex;gap:6px">
|
||||
<a href="{{ route('watchlist.export', ['format'=>'csv']) }}" style="font-size:.72rem;color:rgba(0,245,255,.7);text-decoration:none;display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border:1px solid rgba(0,245,255,.2);border-radius:6px">
|
||||
<i class="bi bi-download"></i> CSV
|
||||
</a>
|
||||
<a href="{{ route('watchlist.export', ['format'=>'json']) }}" style="font-size:.72rem;color:rgba(0,245,255,.7);text-decoration:none;display:inline-flex;align-items:center;gap:4px;padding:4px 10px;border:1px solid rgba(0,245,255,.2);border-radius:6px">
|
||||
<i class="bi bi-download"></i> JSON
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@if($watchlistItems->count())
|
||||
{{-- Filter buttons --}}
|
||||
<div class="wl-filter">
|
||||
<button class="wl-filter-btn on" data-wl="all">Tümü</button>
|
||||
@foreach(\App\Models\Watchlist::STATUSES as $status => $label)
|
||||
@if(isset($watchlistItems[$status]) && $watchlistItems[$status]->count())
|
||||
<button class="wl-filter-btn" data-wl="{{ $status }}">{{ $label }} <span style="opacity:.5">({{ $watchlistItems[$status]->count() }})</span></button>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@foreach(\App\Models\Watchlist::STATUSES as $status => $label)
|
||||
@if(isset($watchlistItems[$status]) && $watchlistItems[$status]->count())
|
||||
<div class="wl-section wl-grid" data-wl="{{ $status }}" style="margin-bottom:24px">
|
||||
@foreach($watchlistItems[$status] as $wl)
|
||||
@if(!$wl->anime) @continue @endif
|
||||
<a href="{{ $wl->anime->detailUrl }}" class="wl-card">
|
||||
@if($wl->anime->cover_image)
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($wl->anime->cover_image) }}" alt="" loading="lazy">
|
||||
@else
|
||||
<div class="wl-card-ph"><i class="bi bi-image"></i></div>
|
||||
@endif
|
||||
<div class="wl-card-title">{{ $wl->anime->title }}</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
@else
|
||||
<div class="pr-empty"><i class="bi bi-bookmark"></i>Liste henüz boş.</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- AKTİVİTE / HEATMAP --}}
|
||||
<div class="pr-pane" id="pane-activity">
|
||||
<div class="pr-sec-h"><span>İzleme Aktivitesi — Son 1 Yıl</span></div>
|
||||
@php
|
||||
$today = now()->startOfDay();
|
||||
$start = $today->copy()->subDays(364);
|
||||
$gridStart= $start->copy()->startOfWeek(\Carbon\Carbon::MONDAY);
|
||||
$weeks = [];
|
||||
$cur = $gridStart->copy();
|
||||
while ($cur->lte($today)) {
|
||||
$week = [];
|
||||
for ($d = 0; $d < 7; $d++) {
|
||||
$key = $cur->format('Y-m-d');
|
||||
$week[] = ['date'=>$key,'count'=>$heatmapRaw[$key]??0,'past'=>$cur->lte($today)&&$cur->gte($start)];
|
||||
$cur->addDay();
|
||||
}
|
||||
$weeks[] = $week;
|
||||
}
|
||||
$maxCount = !empty($heatmapRaw) ? max($heatmapRaw) : 1;
|
||||
$totalDays = collect($heatmapRaw)->filter()->count();
|
||||
$totalEpYear= array_sum($heatmapRaw);
|
||||
@endphp
|
||||
<div class="heatmap-wrap">
|
||||
<div class="heatmap-scroll">
|
||||
<div class="heatmap-grid">
|
||||
@foreach($weeks as $week)
|
||||
<div class="hm-col">
|
||||
@foreach($week as $cell)
|
||||
@php
|
||||
$alpha = 0;
|
||||
$bg = 'rgba(255,255,255,.05)';
|
||||
if ($cell['past'] && $cell['count'] > 0) {
|
||||
$intensity = min(1, $cell['count'] / max($maxCount,1));
|
||||
$alpha = 0.18 + $intensity * 0.82;
|
||||
$bg = "rgba(0,245,255,{$alpha})";
|
||||
} elseif (!$cell['past']) {
|
||||
$bg = 'transparent';
|
||||
}
|
||||
@endphp
|
||||
<div class="hm-cell" title="{{ $cell['past'] ? $cell['date'].': '.($cell['count']?$cell['count'].' bölüm':'boş') : '' }}"
|
||||
style="background:{{ $bg }}"></div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="heatmap-legend">
|
||||
<span>Az</span>
|
||||
<div class="heatmap-legend-cells">
|
||||
<div style="width:13px;height:13px;border-radius:3px;background:rgba(255,255,255,.05)"></div>
|
||||
<div style="width:13px;height:13px;border-radius:3px;background:rgba(0,245,255,.2)"></div>
|
||||
<div style="width:13px;height:13px;border-radius:3px;background:rgba(0,245,255,.45)"></div>
|
||||
<div style="width:13px;height:13px;border-radius:3px;background:rgba(0,245,255,.7)"></div>
|
||||
<div style="width:13px;height:13px;border-radius:3px;background:rgba(0,245,255,1)"></div>
|
||||
</div>
|
||||
<span>Çok</span>
|
||||
</div>
|
||||
<div class="heatmap-stats">
|
||||
<span><i class="bi bi-calendar3 me-1"></i>{{ $totalDays }} aktif gün</span>
|
||||
<span><i class="bi bi-play-circle me-1"></i>{{ $totalEpYear }} bölüm izlendi</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- BAŞARIMLAR --}}
|
||||
@if($allAchievements->count())
|
||||
<div class="pr-pane" id="pane-achievements">
|
||||
@php $earnedIds = $achievements->pluck('achievement_id')->toArray(); @endphp
|
||||
<div class="pr-sec-h"><span>Başarımlar <span style="opacity:.4;font-weight:400">{{ $achievements->count() }}/{{ $allAchievements->count() }}</span></span></div>
|
||||
<div class="ach-grid">
|
||||
@foreach($allAchievements->sortByDesc(fn($a)=>in_array($a->id,$earnedIds)) as $ach)
|
||||
@php $earned = in_array($ach->id, $earnedIds); @endphp
|
||||
<div class="ach-card {{ $earned ? 'earned' : '' }}" style="{{ $earned ? 'border-color:'.substr($ach->color,0,7).'33' : '' }}">
|
||||
<div class="ach-icon-wrap" style="background:{{ $earned ? substr($ach->color,0,7).'1a' : 'rgba(255,255,255,.04)' }}">
|
||||
<i class="bi {{ $ach->icon }}" style="color:{{ $earned ? $ach->color : 'rgba(255,255,255,.2)' }}"></i>
|
||||
</div>
|
||||
<div class="ach-body">
|
||||
<div class="ach-title">{{ $ach->title }}</div>
|
||||
<div class="ach-desc">{{ $ach->description }}</div>
|
||||
@if($earned)
|
||||
@php $ea = $achievements->firstWhere('achievement_id',$ach->id); @endphp
|
||||
<div class="ach-date" style="color:{{ $ach->color }}">{{ $ea?->earned_at?->format('d.m.Y') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- NOTLAR --}}
|
||||
<div class="pr-pane" id="pane-notes">
|
||||
<div class="pr-sec-h"><span>Bölüm Notlarım</span></div>
|
||||
@forelse($episodeNotes as $note)
|
||||
<div class="note-card">
|
||||
<div class="note-meta">
|
||||
@if($note->anime)<a href="{{ $note->anime->detailUrl }}" class="note-anime">{{ $note->anime->title }}</a>@endif
|
||||
@if($note->episode)<span class="note-ep">{{ $note->episode->episode_number }}. Bölüm</span>@endif
|
||||
@if($note->timestamp_at)<span class="note-ts"><i class="bi bi-clock"></i> {{ $note->timestamp_label }}</span>@endif
|
||||
<span class="note-date">{{ $note->created_at->format('d.m.Y') }}</span>
|
||||
</div>
|
||||
<div class="note-text">{{ $note->content }}</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="pr-empty"><i class="bi bi-journal-text"></i>Henüz not almadın.<br><small style="font-size:.78rem;opacity:.6">İzlerken bölüm içindeki not ikonuna tıklayarak not alabilirsin.</small></div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{-- YORUMLAR --}}
|
||||
{{-- ── Keşfet Geçmişi ──────────────────────────────────────── --}}
|
||||
<div class="pr-pane" id="pane-swipes">
|
||||
<div class="pr-sec-h" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px">
|
||||
<span>Keşfet Geçmişi</span>
|
||||
@if($swipeHistory->count())
|
||||
<div style="display:flex;gap:14px;font-size:.68rem;color:var(--text3)">
|
||||
<span style="color:var(--accent2)">
|
||||
<i class="bi bi-heart-fill" style="margin-right:3px"></i>
|
||||
{{ $swipeHistory->where('direction','like')->count() }} beğeni
|
||||
</span>
|
||||
<span>
|
||||
<i class="bi bi-x-lg" style="margin-right:3px;color:#ff2d7d"></i>
|
||||
{{ $swipeHistory->where('direction','skip')->count() }} geç
|
||||
</span>
|
||||
<a href="{{ route('discover') }}" style="color:var(--accent2);text-decoration:none">
|
||||
<i class="bi bi-arrow-right" style="margin-right:3px"></i>Devam et
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($swipeHistory->isEmpty())
|
||||
<div class="pr-empty">
|
||||
<i class="bi bi-hand-index-thumb"></i>
|
||||
Henüz hiç anime keşfetmedin. <a href="{{ route('discover') }}" style="color:var(--accent2);text-decoration:none">Hemen başla →</a>
|
||||
</div>
|
||||
@else
|
||||
|
||||
{{-- Filter buttons --}}
|
||||
<div style="display:flex;gap:6px;margin-bottom:16px;flex-wrap:wrap">
|
||||
<button class="swipe-filter on" data-f="all">Tümü <span class="swipe-f-count">{{ $swipeHistory->count() }}</span></button>
|
||||
<button class="swipe-filter" data-f="like"><i class="bi bi-heart-fill" style="color:var(--accent2);font-size:.75rem"></i> Beğendiklerim <span class="swipe-f-count">{{ $swipeHistory->where('direction','like')->count() }}</span></button>
|
||||
<button class="swipe-filter" data-f="skip"><i class="bi bi-x-lg" style="color:#ff2d7d;font-size:.75rem"></i> Geçtiklerim <span class="swipe-f-count">{{ $swipeHistory->where('direction','skip')->count() }}</span></button>
|
||||
</div>
|
||||
|
||||
<div class="swipe-grid" id="swipeGrid">
|
||||
@foreach($swipeHistory as $sw)
|
||||
@php $a = $sw->anime; @endphp
|
||||
<a href="{{ $a->detailUrl }}" class="swipe-item {{ $sw->direction }}" data-dir="{{ $sw->direction }}" title="{{ $a->title }}">
|
||||
<div class="swipe-poster">
|
||||
@if($a->coverUrl)
|
||||
<img src="{{ $a->coverUrl }}" alt="{{ $a->title }}" loading="lazy">
|
||||
@else
|
||||
<div class="swipe-poster-ph"><i class="bi bi-film"></i></div>
|
||||
@endif
|
||||
<div class="swipe-dir-badge {{ $sw->direction === 'like' ? 'badge-like' : 'badge-skip' }}">
|
||||
@if($sw->direction === 'like')
|
||||
<i class="bi bi-heart-fill"></i>
|
||||
@else
|
||||
<i class="bi bi-x-lg"></i>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="swipe-info">
|
||||
<div class="swipe-title">{{ $a->title }}</div>
|
||||
<div class="swipe-meta">
|
||||
@if($a->release_year)<span>{{ $a->release_year }}</span>@endif
|
||||
@if($a->rating)<span style="color:#fbbf24"><i class="bi bi-star-fill" style="font-size:.55rem"></i> {{ number_format($a->rating,1) }}</span>@endif
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="pr-pane" id="pane-comments">
|
||||
<div class="pr-sec-h"><span>Son Yorumlar</span></div>
|
||||
@if($recentComments->isEmpty())
|
||||
<div class="pr-empty"><i class="bi bi-chat-dots"></i>Henüz yorum yok.</div>
|
||||
@else
|
||||
@foreach($recentComments as $c)
|
||||
<div class="cm-card">
|
||||
<div class="cm-top">
|
||||
<div class="cm-ava">{{ mb_strtoupper(mb_substr($user->name,0,1)) }}</div>
|
||||
<span class="cm-name">{{ $user->name }}</span>
|
||||
<span class="cm-time">{{ $c->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
@if($c->content)<div class="cm-text">{{ $c->content }}</div>@endif
|
||||
@if($c->gif_url)<img src="{{ $c->gif_url }}" alt="" class="cm-gif">@endif
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
@push('scripts')
|
||||
<script>
|
||||
// Tab switching
|
||||
document.querySelectorAll('.pr-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.pr-tab').forEach(t => t.classList.remove('on'));
|
||||
document.querySelectorAll('.pr-pane').forEach(p => p.classList.remove('on'));
|
||||
btn.classList.add('on');
|
||||
document.getElementById('pane-' + btn.dataset.pane)?.classList.add('on');
|
||||
});
|
||||
});
|
||||
|
||||
// Watchlist filter
|
||||
document.querySelectorAll('.wl-filter-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.wl-filter-btn').forEach(b => b.classList.remove('on'));
|
||||
btn.classList.add('on');
|
||||
const target = btn.dataset.wl;
|
||||
document.querySelectorAll('.wl-section').forEach(sec => {
|
||||
sec.style.display = (target === 'all' || sec.dataset.wl === target) ? 'grid' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Swipe history filter
|
||||
document.querySelectorAll('.swipe-filter').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.swipe-filter').forEach(b => b.classList.remove('on'));
|
||||
btn.classList.add('on');
|
||||
const f = btn.dataset.f;
|
||||
document.querySelectorAll('#swipeGrid .swipe-item').forEach(item => {
|
||||
item.style.display = (f === 'all' || item.dataset.dir === f) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,529 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', $user->name . ' — Profil — Animexe')
|
||||
@section('meta_description', ($user->bio ? $user->bio . ' — ' : '') . $user->name . ' Animexe profili.')
|
||||
@push('styles')
|
||||
<style>
|
||||
.pub-hero{
|
||||
position:relative;overflow:hidden;background:#05050e;
|
||||
}
|
||||
.pub-hero-animated{
|
||||
background:linear-gradient(135deg,#05050e,#0a0520);
|
||||
}
|
||||
.pub-hero-bg{
|
||||
position:absolute;inset:0;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 20% 50%,rgba(0,245,255,.06),transparent),
|
||||
radial-gradient(ellipse 60% 80% at 80% 30%,rgba(255,45,125,.05),transparent);
|
||||
}
|
||||
.pub-hero-inner{
|
||||
position:relative;z-index:1;max-width:900px;margin:0 auto;
|
||||
padding:calc(var(--nav-h) + 36px) 24px 0;
|
||||
}
|
||||
.pub-top{display:flex;align-items:flex-start;gap:20px;flex-wrap:wrap;margin-bottom:28px}
|
||||
.pub-top-main{display:flex;align-items:flex-end;gap:20px;flex:1;min-width:0}
|
||||
.pub-avatar{
|
||||
width:96px;height:96px;border-radius:24px;flex-shrink:0;
|
||||
background:linear-gradient(135deg,var(--accent2),var(--accent));
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:2.4rem;font-weight:900;color:#fff;overflow:hidden;
|
||||
box-shadow:0 0 0 3px rgba(0,245,255,.2),0 16px 48px rgba(0,0,0,.5);
|
||||
}
|
||||
.pub-info{flex:1;min-width:0;padding-bottom:4px}
|
||||
.pub-name{font-size:1.7rem;font-weight:900;color:#fff;line-height:1.1;margin-bottom:4px}
|
||||
.pub-handle{font-size:.84rem;color:rgba(255,255,255,.35);margin-bottom:10px}
|
||||
.pub-badges{display:flex;gap:7px;flex-wrap:wrap}
|
||||
.pbadge{padding:4px 12px;border-radius:20px;font-size:.7rem;font-weight:700;letter-spacing:.05em;display:inline-flex;align-items:center;gap:5px}
|
||||
.pbadge-admin{background:rgba(255,45,125,.15);border:1px solid rgba(255,45,125,.35);color:var(--accent)}
|
||||
.pbadge-premium{background:rgba(255,208,0,.12);border:1px solid rgba(255,208,0,.3);color:#ffd700}
|
||||
.pbadge-member{background:rgba(0,245,255,.07);border:1px solid rgba(0,245,255,.2);color:var(--accent2)}
|
||||
|
||||
.pub-stats{
|
||||
display:flex;gap:6px;flex-wrap:wrap;
|
||||
padding:20px 0 0;border-top:1px solid rgba(255,255,255,.06);margin-top:8px;
|
||||
}
|
||||
.pub-stat{
|
||||
display:flex;flex-direction:column;align-items:center;
|
||||
padding:10px 20px;border-radius:12px;
|
||||
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);
|
||||
min-width:80px;flex:1;
|
||||
}
|
||||
.pub-stat .v{font-size:1.25rem;font-weight:800;color:#fff;line-height:1}
|
||||
.pub-stat .l{font-size:.62rem;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.35);margin-top:4px;font-weight:600}
|
||||
|
||||
.pub-actions{display:flex;gap:8px;flex-wrap:wrap;align-self:flex-start;flex-shrink:0}
|
||||
.pub-btn{
|
||||
padding:8px 16px;border-radius:10px;font-size:.82rem;font-weight:700;
|
||||
display:inline-flex;align-items:center;gap:7px;cursor:pointer;
|
||||
border:none;font-family:inherit;transition:all .2s;text-decoration:none;white-space:nowrap;
|
||||
}
|
||||
.pub-btn-follow{background:var(--accent2);color:#05050e}
|
||||
.pub-btn-follow:hover{background:#00d4de}
|
||||
.pub-btn-follow.following{background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.15);color:rgba(255,255,255,.6)}
|
||||
.pub-btn-follow.following:hover{background:rgba(255,80,80,.12);border-color:rgba(255,80,80,.3);color:#ff5050}
|
||||
.pub-btn-msg{background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.12);color:rgba(255,255,255,.6)}
|
||||
.pub-btn-msg:hover{background:rgba(255,255,255,.12);color:#fff}
|
||||
.pub-btn-edit{background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.12);color:rgba(255,255,255,.6)}
|
||||
.pub-btn-edit:hover{background:rgba(255,255,255,.12);color:#fff}
|
||||
|
||||
.pub-compat{
|
||||
display:flex;align-items:center;gap:10px;
|
||||
background:rgba(0,245,255,.06);border:1px solid rgba(0,245,255,.15);
|
||||
border-radius:12px;padding:10px 16px;margin-top:14px;
|
||||
}
|
||||
.pub-compat-bar{flex:1;height:6px;background:rgba(255,255,255,.08);border-radius:3px;overflow:hidden}
|
||||
.pub-compat-fill{height:100%;background:linear-gradient(90deg,var(--accent2),var(--accent));border-radius:3px;transition:width .8s ease}
|
||||
.pub-compat-label{font-size:.75rem;color:var(--accent2);font-weight:700;white-space:nowrap}
|
||||
|
||||
.pub-tabs{
|
||||
display:flex;gap:2px;border-top:1px solid rgba(255,255,255,.06);
|
||||
margin-top:20px;overflow-x:auto;scrollbar-width:none;
|
||||
}
|
||||
.pub-tabs::-webkit-scrollbar{display:none}
|
||||
.pub-tab{
|
||||
padding:12px 18px;background:none;border:none;
|
||||
border-bottom:2px solid transparent;
|
||||
color:rgba(255,255,255,.35);font-size:.82rem;font-weight:600;
|
||||
cursor:pointer;font-family:inherit;transition:all .15s;
|
||||
white-space:nowrap;flex-shrink:0;display:flex;align-items:center;gap:6px;
|
||||
margin-bottom:-1px;
|
||||
}
|
||||
.pub-tab:hover{color:rgba(255,255,255,.65)}
|
||||
.pub-tab.on{color:var(--accent2);border-bottom-color:var(--accent2)}
|
||||
|
||||
.pub-body{max-width:900px;margin:0 auto;padding:32px 24px 64px}
|
||||
.pub-pane{display:none}
|
||||
.pub-pane.on{display:block}
|
||||
|
||||
.pub-sec-h{
|
||||
font-size:.68rem;text-transform:uppercase;letter-spacing:.12em;
|
||||
color:rgba(255,255,255,.3);font-weight:700;
|
||||
margin-bottom:16px;display:flex;align-items:center;gap:8px;
|
||||
}
|
||||
.pub-sec-h::before{content:'';width:3px;height:14px;background:linear-gradient(180deg,var(--accent2),var(--accent));border-radius:2px}
|
||||
|
||||
.pub-wl-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:10px}
|
||||
.pub-wl-card{
|
||||
border-radius:9px;overflow:hidden;text-decoration:none;display:block;
|
||||
background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);
|
||||
transition:transform .15s,border-color .15s;
|
||||
}
|
||||
.pub-wl-card:hover{transform:translateY(-2px);border-color:rgba(255,255,255,.18)}
|
||||
.pub-wl-card img{width:100%;aspect-ratio:2/3;object-fit:cover;display:block}
|
||||
.pub-wl-card-ph{aspect-ratio:2/3;background:rgba(255,255,255,.04);display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.2)}
|
||||
.pub-wl-card-title{padding:7px 8px;font-size:.72rem;font-weight:600;color:rgba(255,255,255,.7);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
|
||||
.pub-ach-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px}
|
||||
.pub-ach-card{
|
||||
display:flex;align-items:center;gap:14px;
|
||||
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);
|
||||
border-radius:12px;padding:14px 16px;
|
||||
}
|
||||
.pub-ach-icon{width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:1.4rem;flex-shrink:0}
|
||||
.pub-ach-title{font-size:.85rem;font-weight:700;color:#fff;margin-bottom:2px}
|
||||
.pub-ach-desc{font-size:.72rem;color:rgba(255,255,255,.35);line-height:1.4}
|
||||
|
||||
.pub-comment-card{
|
||||
background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);
|
||||
border-radius:12px;padding:14px 16px;margin-bottom:10px;
|
||||
}
|
||||
.pub-cm-top{display:flex;align-items:center;gap:10px;margin-bottom:8px}
|
||||
.pub-cm-anime{font-size:.78rem;font-weight:600;color:var(--accent2);text-decoration:none}
|
||||
.pub-cm-anime:hover{text-decoration:underline}
|
||||
.pub-cm-time{font-size:.7rem;color:rgba(255,255,255,.25);margin-left:auto}
|
||||
.pub-cm-text{font-size:.87rem;color:rgba(255,255,255,.6);line-height:1.6}
|
||||
|
||||
.pub-empty{text-align:center;padding:48px 20px;color:rgba(255,255,255,.2);font-size:.9rem}
|
||||
.pub-empty i{font-size:2.5rem;display:block;margin-bottom:12px;opacity:.25}
|
||||
|
||||
@media(max-width:600px){
|
||||
.pub-hero-inner{padding:calc(var(--nav-h) + 16px) 16px 0}
|
||||
.pub-body{padding:24px 16px 48px}
|
||||
.pub-avatar{width:72px;height:72px;border-radius:18px;font-size:1.8rem}
|
||||
.pub-name{font-size:1.3rem}
|
||||
.pub-top{flex-direction:column;gap:14px}
|
||||
.pub-ach-grid{grid-template-columns:1fr}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$pColor = $user->profile_color ?? '#00f5ff';
|
||||
$profBg = ($user->profile_bg && $user->hasPerk('profile_bg')) ? $user->profile_bg : null;
|
||||
$animBanner = $user->hasPerk('animated_banner') && $user->animated_banner;
|
||||
$watchRank = $user->watchRank();
|
||||
$musicUrl = ($user->profile_music_url && $user->hasPerk('profile_music')) ? $user->profile_music_url : null;
|
||||
@endphp
|
||||
|
||||
<div class="pub-hero {{ $profBg ? 'profile-bg-'.$profBg : '' }} {{ $animBanner ? 'pub-hero-animated' : '' }}" style="--pr-color:{{ $pColor }}">
|
||||
@if($animBanner)
|
||||
<canvas id="banner-canvas" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:0"></canvas>
|
||||
@endif
|
||||
@if($user->banner_image)
|
||||
<div style="position:absolute;inset:0;overflow:hidden;z-index:0">
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($user->banner_image) }}" alt=""
|
||||
style="width:100%;height:100%;object-fit:cover;opacity:.35;filter:blur(1px)">
|
||||
<div style="position:absolute;inset:0;background:linear-gradient(to bottom,rgba(5,5,14,.3) 0%,rgba(5,5,14,.85) 100%)"></div>
|
||||
</div>
|
||||
@else
|
||||
<div class="pub-hero-bg" style="background:radial-gradient(ellipse 80% 60% at 20% 50%,{{ $pColor }}18,transparent),radial-gradient(ellipse 60% 80% at 80% 30%,{{ $pColor }}10,transparent)"></div>
|
||||
@endif
|
||||
|
||||
<div class="pub-hero-inner">
|
||||
<div class="pub-top">
|
||||
<div class="pub-top-main">
|
||||
@php
|
||||
$hasFrame = $user->profile_frame && $user->hasPerk('profile_frame');
|
||||
$avatarSrc = $user->gif_avatar && $user->hasPerk('gif_avatar')
|
||||
? $user->gif_avatar
|
||||
: ($user->avatar ? \App\Support\MediaUrl::fromStoragePath($user->avatar) : null);
|
||||
@endphp
|
||||
<div class="{{ $hasFrame ? 'avatar-wrap' : '' }}" style="{{ $hasFrame ? 'width:96px;height:96px' : '' }}">
|
||||
@if($hasFrame)
|
||||
<div class="avatar-frame frame-{{ $user->profile_frame }}"></div>
|
||||
@endif
|
||||
<div class="pub-avatar" style="{{ $hasFrame ? 'border-radius:50%;position:relative;z-index:3' : 'background:linear-gradient(135deg,'.$pColor.','.$pColor.'88);box-shadow:0 0 0 3px '.$pColor.'33,0 16px 48px rgba(0,0,0,.5)' }}">
|
||||
@if($avatarSrc)
|
||||
<img src="{{ $avatarSrc }}" alt="{{ $user->name }}"
|
||||
style="width:100%;height:100%;object-fit:cover;border-radius:inherit">
|
||||
@else
|
||||
{{ mb_strtoupper(mb_substr($user->name,0,1)) }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="pub-info">
|
||||
@php
|
||||
$unColor = $user->username_color && $user->hasPerk('username_color') ? 'username-color-'.$user->username_color : '';
|
||||
$unEffect = $user->username_effect && $user->hasPerk('username_effect') ? 'username-effect-'.$user->username_effect : '';
|
||||
@endphp
|
||||
<div class="pub-name {{ $unColor }} {{ $unEffect }}"
|
||||
@if($user->username_effect==='glitch' && $unEffect) data-text="{{ $user->name }}" @endif>{{ $user->name }}</div>
|
||||
@if($user->username)
|
||||
<div class="pub-handle">@php echo '@' . e($user->username); @endphp</div>
|
||||
@endif
|
||||
@if($user->bio)
|
||||
<div style="font-size:.83rem;color:rgba(255,255,255,.55);margin-bottom:10px;max-width:500px;line-height:1.5">{{ $user->bio }}</div>
|
||||
@endif
|
||||
<div class="pub-badges">
|
||||
@if($user->role==='admin')
|
||||
<span class="pbadge pbadge-admin"><i class="bi bi-shield-fill"></i> Admin</span>
|
||||
@elseif($user->role==='moderator')
|
||||
<span class="pbadge pbadge-admin"><i class="bi bi-shield-half"></i> Moderatör</span>
|
||||
@endif
|
||||
@if($user->isPremium())
|
||||
<span class="pbadge pbadge-premium"><i class="bi bi-star-fill"></i> Premium</span>
|
||||
@else
|
||||
<span class="pbadge pbadge-member"><i class="bi bi-person-check"></i> Üye</span>
|
||||
@endif
|
||||
@if($user->profile_badge && $user->hasPerk('profile_badge'))
|
||||
<span class="premium-badge user-badge-custom">{{ $user->profile_badge }}</span>
|
||||
@endif
|
||||
@if($watchRank)
|
||||
<span class="pbadge" style="background:rgba(255,255,255,.06);border:1px solid {{ $watchRank['color'] }}44;color:{{ $watchRank['color'] }}">
|
||||
<i class="bi {{ $watchRank['icon'] }}"></i> {{ $watchRank['label'] }}
|
||||
</span>
|
||||
@endif
|
||||
<span class="pbadge" style="background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);color:rgba(255,255,255,.35)"><i class="bi bi-calendar3"></i> {{ $user->created_at->format('Y') }}'den beri</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pub-actions">
|
||||
@if($isOwnProfile)
|
||||
<a href="{{ route('profile.settings') }}" class="pub-btn pub-btn-edit">
|
||||
<i class="bi bi-pencil-square"></i> Profili Düzenle
|
||||
</a>
|
||||
@elseif(auth()->check())
|
||||
<button id="follow-btn" onclick="toggleFollow()"
|
||||
class="pub-btn pub-btn-follow {{ $isFollowing ? 'following' : '' }}"
|
||||
data-following="{{ $isFollowing ? '1' : '0' }}">
|
||||
<i class="bi {{ $isFollowing ? 'bi-person-check-fill' : 'bi-person-plus' }}" id="follow-icon"></i>
|
||||
<span id="follow-label">{{ $isFollowing ? 'Takip Ediliyor' : 'Takip Et' }}</span>
|
||||
</button>
|
||||
<form method="POST" action="{{ route('messages.start', $user) }}">
|
||||
@csrf
|
||||
<button type="submit" class="pub-btn pub-btn-msg">
|
||||
<i class="bi bi-chat-dots"></i> Mesaj
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($compatibility !== null)
|
||||
<div class="pub-compat">
|
||||
<i class="bi bi-heart-pulse" style="color:var(--accent2);font-size:1rem;flex-shrink:0"></i>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-size:.72rem;color:rgba(255,255,255,.4);margin-bottom:5px">Anime zevk uyumu</div>
|
||||
<div class="pub-compat-bar"><div class="pub-compat-fill" id="compat-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
<span class="pub-compat-label" id="compat-pct">%{{ $compatibility }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pub-stats">
|
||||
<div class="pub-stat"><span class="v">{{ $watchStats['episodes'] }}</span><span class="l">Bölüm</span></div>
|
||||
<div class="pub-stat"><span class="v">{{ $watchStats['hours'] }}</span><span class="l">Saat</span></div>
|
||||
<div class="pub-stat"><span class="v" id="follower-count">{{ $followerCount }}</span><span class="l">Takipçi</span></div>
|
||||
<div class="pub-stat"><span class="v">{{ $followingCount }}</span><span class="l">Takip</span></div>
|
||||
<div class="pub-stat"><span class="v">{{ $commentCount }}</span><span class="l">Yorum</span></div>
|
||||
</div>
|
||||
|
||||
<div class="pub-tabs">
|
||||
<button class="pub-tab on" onclick="switchTab('watchlist',this)"><i class="bi bi-bookmark"></i> İzleme Listesi</button>
|
||||
<button class="pub-tab" onclick="switchTab('achievements',this)"><i class="bi bi-trophy"></i> Başarımlar</button>
|
||||
<button class="pub-tab" onclick="switchTab('comments',this)"><i class="bi bi-chat-dots"></i> Yorumlar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($musicUrl)
|
||||
<div id="profile-music-bar" style="
|
||||
position:fixed;bottom:0;left:0;right:0;z-index:900;
|
||||
background:rgba(5,5,14,.92);backdrop-filter:blur(12px);
|
||||
border-top:1px solid rgba(0,245,255,.15);
|
||||
display:flex;align-items:center;gap:12px;padding:10px 20px;
|
||||
font-size:.82rem;
|
||||
">
|
||||
<button id="music-toggle" onclick="toggleMusic()" style="
|
||||
background:rgba(0,245,255,.12);border:1px solid rgba(0,245,255,.3);color:#00f5ff;
|
||||
border-radius:50%;width:36px;height:36px;display:flex;align-items:center;justify-content:center;
|
||||
cursor:pointer;flex-shrink:0;font-size:1rem;
|
||||
"><i class="bi bi-play-fill" id="music-icon"></i></button>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="color:rgba(255,255,255,.5);font-size:.7rem;margin-bottom:2px">{{ $user->name }} · Profil Müziği</div>
|
||||
<div id="music-progress-bar" style="width:100%;height:3px;background:rgba(255,255,255,.1);border-radius:2px;overflow:hidden">
|
||||
<div id="music-progress-fill" style="width:0%;height:100%;background:linear-gradient(90deg,#00f5ff,#b84dff);transition:width .5s linear;border-radius:2px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="document.getElementById('profile-music-bar').remove()" style="
|
||||
background:none;border:none;color:rgba(255,255,255,.3);cursor:pointer;font-size:1.1rem;padding:4px;
|
||||
"><i class="bi bi-x"></i></button>
|
||||
<audio id="profile-audio" src="" preload="none"></audio>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pub-body">
|
||||
|
||||
{{-- İzleme Listesi --}}
|
||||
<div class="pub-pane on" id="pane-watchlist">
|
||||
@php $watching = $watchlistItems->get('watching',[]);
|
||||
$completed = $watchlistItems->get('completed',[]);
|
||||
$planToWatch = $watchlistItems->get('plan_to_watch',[]); @endphp
|
||||
|
||||
@if($watchlistItems->isEmpty())
|
||||
<div class="pub-empty"><i class="bi bi-bookmark"></i> Henüz izleme listesi yok.</div>
|
||||
@else
|
||||
@foreach(['watching'=>'İzliyor','completed'=>'Tamamladı','plan_to_watch'=>'İzleyecek'] as $status => $label)
|
||||
@php $items = $watchlistItems->get($status, collect()); @endphp
|
||||
@if($items->count())
|
||||
<div class="pub-sec-h">{{ $label }} <span style="color:rgba(255,255,255,.2)">({{ $items->count() }})</span></div>
|
||||
<div class="pub-wl-grid" style="margin-bottom:28px">
|
||||
@foreach($items->take(12) as $wl)
|
||||
@if($wl->anime)
|
||||
<a href="{{ $wl->anime->detailUrl }}" class="pub-wl-card">
|
||||
@if($wl->anime->cover_image)
|
||||
<img src="{{ \App\Support\MediaUrl::fromStoragePath($wl->anime->cover_image) }}" alt="{{ $wl->anime->title }}" loading="lazy">
|
||||
@else
|
||||
<div class="pub-wl-card-ph"><i class="bi bi-image"></i></div>
|
||||
@endif
|
||||
<div class="pub-wl-card-title">{{ $wl->anime->title }}</div>
|
||||
</a>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Başarımlar --}}
|
||||
<div class="pub-pane" id="pane-achievements">
|
||||
@if($achievements->isEmpty())
|
||||
<div class="pub-empty"><i class="bi bi-trophy"></i> Henüz başarım kazanılmamış.</div>
|
||||
@else
|
||||
<div class="pub-ach-grid">
|
||||
@foreach($achievements as $ua)
|
||||
<div class="pub-ach-card">
|
||||
<div class="pub-ach-icon" style="background:{{ substr($ua->achievement->color ?? '#ffffff',0,7) }}22">
|
||||
<i class="bi {{ $ua->achievement->icon ?? 'bi-trophy' }}" style="color:{{ $ua->achievement->color ?? '#ffffff' }}"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="pub-ach-title">{{ $ua->achievement->name }}</div>
|
||||
<div class="pub-ach-desc">{{ $ua->achievement->description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Yorumlar --}}
|
||||
<div class="pub-pane" id="pane-comments">
|
||||
@if($recentComments->isEmpty())
|
||||
<div class="pub-empty"><i class="bi bi-chat-dots"></i> Henüz yorum yok.</div>
|
||||
@else
|
||||
@foreach($recentComments as $c)
|
||||
@php $ep = $c->episode; $anime = $ep?->anime ?? $c->anime; @endphp
|
||||
<div class="pub-comment-card">
|
||||
<div class="pub-cm-top">
|
||||
@if($anime)
|
||||
<a href="{{ $anime->detailUrl }}" class="pub-cm-anime"><i class="bi bi-play-circle"></i> {{ $anime->title }}</a>
|
||||
@endif
|
||||
<span class="pub-cm-time">{{ $c->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
<div class="pub-cm-text">{{ Str::limit($c->content, 200) }}</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function switchTab(name, btn) {
|
||||
document.querySelectorAll('.pub-pane').forEach(p => p.classList.remove('on'));
|
||||
document.querySelectorAll('.pub-tab').forEach(t => t.classList.remove('on'));
|
||||
document.getElementById('pane-' + name).classList.add('on');
|
||||
btn.classList.add('on');
|
||||
}
|
||||
|
||||
// Uyum barı animasyonu
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fill = document.getElementById('compat-fill');
|
||||
if (fill) setTimeout(() => fill.style.width = '{{ $compatibility ?? 0 }}%', 300);
|
||||
});
|
||||
|
||||
// ── Animasyonlu Banner ────────────────────────────────────────────────────
|
||||
@if($animBanner)
|
||||
(function(){
|
||||
const canvas = document.getElementById('banner-canvas');
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const color = '{{ $pColor }}';
|
||||
let W, H, particles = [];
|
||||
function resize(){
|
||||
W = canvas.width = canvas.offsetWidth;
|
||||
H = canvas.height = canvas.offsetHeight;
|
||||
}
|
||||
resize();
|
||||
window.addEventListener('resize', resize);
|
||||
for (let i = 0; i < 55; i++) {
|
||||
particles.push({
|
||||
x: Math.random()*1000, y: Math.random()*300,
|
||||
r: Math.random()*2+.5, vx: (Math.random()-.5)*.4,
|
||||
vy: (Math.random()-.5)*.4, a: Math.random()*.6+.1
|
||||
});
|
||||
}
|
||||
function hex2rgb(h){ const r=parseInt(h.slice(1,3),16),g=parseInt(h.slice(3,5),16),b=parseInt(h.slice(5,7),16); return `${r},${g},${b}`; }
|
||||
const rgb = hex2rgb(color.length===7 ? color : '#00f5ff');
|
||||
function draw(){
|
||||
ctx.clearRect(0,0,W,H);
|
||||
particles.forEach(p => {
|
||||
p.x += p.vx; p.y += p.vy;
|
||||
if (p.x<0) p.x=W; if (p.x>W) p.x=0;
|
||||
if (p.y<0) p.y=H; if (p.y>H) p.y=0;
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x,p.y,p.r,0,Math.PI*2);
|
||||
ctx.fillStyle = `rgba(${rgb},${p.a})`;
|
||||
ctx.fill();
|
||||
});
|
||||
// connecting lines
|
||||
particles.forEach((a,i) => {
|
||||
particles.slice(i+1).forEach(b => {
|
||||
const d = Math.hypot(a.x-b.x,a.y-b.y);
|
||||
if (d < 90) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(a.x,a.y); ctx.lineTo(b.x,b.y);
|
||||
ctx.strokeStyle = `rgba(${rgb},${(1-d/90)*.12})`;
|
||||
ctx.lineWidth = .6;
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
});
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
draw();
|
||||
})();
|
||||
@endif
|
||||
|
||||
// ── Profil Müziği ─────────────────────────────────────────────────────────
|
||||
@if($musicUrl)
|
||||
(function(){
|
||||
const audio = document.getElementById('profile-audio');
|
||||
const icon = document.getElementById('music-icon');
|
||||
const fill = document.getElementById('music-progress-fill');
|
||||
if (!audio) return;
|
||||
|
||||
const src = {{ json_encode($musicUrl) }};
|
||||
// YouTube/SoundCloud embedding via iframe is complex; for direct audio URLs autoplay
|
||||
if (src.match(/\.(mp3|ogg|wav|m4a|aac|flac)(\?|$)/i)) {
|
||||
audio.src = src;
|
||||
audio.volume = 0.5;
|
||||
} else {
|
||||
// Not a direct audio file — show the bar with a link instead
|
||||
const bar = document.getElementById('profile-music-bar');
|
||||
if (bar) {
|
||||
const btn = document.getElementById('music-toggle');
|
||||
btn.innerHTML = '<i class="bi bi-box-arrow-up-right"></i>';
|
||||
btn.onclick = () => window.open(src, '_blank');
|
||||
fill.style.width = '100%';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
if (audio.duration) fill.style.width = (audio.currentTime/audio.duration*100)+'%';
|
||||
});
|
||||
audio.addEventListener('ended', () => {
|
||||
icon.className = 'bi bi-play-fill';
|
||||
fill.style.width = '0%';
|
||||
});
|
||||
|
||||
window.toggleMusic = function(){
|
||||
if (audio.paused) { audio.play(); icon.className = 'bi bi-pause-fill'; }
|
||||
else { audio.pause(); icon.className = 'bi bi-play-fill'; }
|
||||
};
|
||||
})();
|
||||
@else
|
||||
window.toggleMusic = function(){};
|
||||
@endif
|
||||
|
||||
// Takip toggle
|
||||
const FOLLOW_URL = '{{ route("user.follow", $user) }}';
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content || '';
|
||||
|
||||
async function toggleFollow() {
|
||||
const btn = document.getElementById('follow-btn');
|
||||
if (!btn || btn.disabled) return;
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const res = await fetch(FOLLOW_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json' }
|
||||
});
|
||||
const d = await res.json();
|
||||
|
||||
const icon = document.getElementById('follow-icon');
|
||||
const label = document.getElementById('follow-label');
|
||||
const cnt = document.getElementById('follower-count');
|
||||
|
||||
if (d.following) {
|
||||
btn.classList.add('following');
|
||||
icon.className = 'bi bi-person-check-fill';
|
||||
label.textContent = 'Takip Ediliyor';
|
||||
} else {
|
||||
btn.classList.remove('following');
|
||||
icon.className = 'bi bi-person-plus';
|
||||
label.textContent = 'Takip Et';
|
||||
}
|
||||
if (cnt) cnt.textContent = d.followers_count;
|
||||
} catch(e) {}
|
||||
|
||||
btn.disabled = false;
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,229 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', ($q ? '"'.$q.'" — Arama' : 'Tüm Animeler') . ' — Animexe')
|
||||
@section('meta_description', $q ? '"' . $q . '" için anime arama sonuçları — Animexe\'de Türkçe anime izleyin.' : 'Animexe\'de tüm anime dizi ve filmlerini keşfedin. Tür, durum ve yıla göre filtreleyin.')
|
||||
@section('robots', ($q || request()->has('genre') || request()->has('type') || request()->has('year') || $results->currentPage() > 1) ? 'noindex, follow' : 'index, follow')
|
||||
@push('styles')
|
||||
<style>
|
||||
/* ═══════════════════════════════════════════════════
|
||||
SEARCH PAGE PREMIUM
|
||||
═══════════════════════════════════════════════════ */
|
||||
.srch-hero{
|
||||
background:linear-gradient(180deg,rgba(8,8,20,.98) 0%,rgba(6,6,13,.99) 100%);
|
||||
padding:clamp(28px,5vw,48px) clamp(16px,4vw,48px) clamp(24px,3.5vw,36px);
|
||||
border-bottom:1px solid rgba(0,245,255,.1);
|
||||
position:relative;overflow:hidden;
|
||||
}
|
||||
.srch-hero::before{
|
||||
content:'';position:absolute;top:-60%;left:-10%;right:-10%;bottom:-20%;
|
||||
background:
|
||||
radial-gradient(ellipse 60% 50% at 30% 50%,rgba(0,245,255,.05),transparent),
|
||||
radial-gradient(ellipse 50% 40% at 70% 50%,rgba(255,45,125,.04),transparent);
|
||||
pointer-events:none;
|
||||
}
|
||||
.srch-hero::after{
|
||||
content:'';position:absolute;bottom:0;left:5%;right:5%;height:1px;
|
||||
background:linear-gradient(90deg,transparent,var(--accent2),var(--accent),transparent);
|
||||
opacity:.4;
|
||||
}
|
||||
.srch-hero h1{
|
||||
font-family:var(--font-display);
|
||||
font-size:clamp(1.3rem,3vw,2rem);
|
||||
font-weight:900;margin-bottom:22px;letter-spacing:.05em;text-transform:uppercase;
|
||||
background:linear-gradient(135deg,#fff 0%,var(--accent2) 55%,var(--accent) 100%);
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
|
||||
animation:srch-in .5s cubic-bezier(.22,1,.36,1) both;
|
||||
}
|
||||
@keyframes srch-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
||||
.srch-form{
|
||||
display:flex;flex-direction:column;gap:10px;max-width:1100px;
|
||||
animation:srch-in .5s cubic-bezier(.22,1,.36,1) .1s both;
|
||||
}
|
||||
.srch-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
||||
.srch-inp-wrap{position:relative;flex:1 1 260px;min-width:0}
|
||||
.srch-inp-wrap i{position:absolute;left:13px;top:50%;transform:translateY(-50%);color:var(--accent2);font-size:.9rem;pointer-events:none;opacity:.8}
|
||||
.srch-inp-wrap input{
|
||||
width:100%;padding:13px 16px 13px 44px;
|
||||
background:rgba(17,17,31,.92);border:1px solid rgba(0,245,255,.22);
|
||||
border-radius:12px;color:var(--text);font-size:.94rem;outline:none;
|
||||
transition:border .18s,box-shadow .18s,background .18s;
|
||||
}
|
||||
.srch-inp-wrap input:focus{
|
||||
border-color:var(--accent2);
|
||||
box-shadow:0 0 0 3px rgba(0,245,255,.14),0 0 28px rgba(0,245,255,.08);
|
||||
background:rgba(0,245,255,.05);
|
||||
}
|
||||
.srch-inp-wrap input::placeholder{color:var(--text3)}
|
||||
.srch-sel{
|
||||
padding:11px 10px;
|
||||
background:#11111f;border:1px solid rgba(0,245,255,.15);
|
||||
border-radius:10px;color:var(--text);font-size:.82rem;outline:none;cursor:pointer;
|
||||
flex:1 1 120px;transition:border .18s;
|
||||
-webkit-appearance:none;appearance:none;
|
||||
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%2300f5ff' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
background-repeat:no-repeat;
|
||||
background-position:right 10px center;
|
||||
padding-right:30px;
|
||||
}
|
||||
.srch-sel option{background:#11111f;color:var(--text)}
|
||||
.srch-sel:focus{border-color:var(--accent2);box-shadow:0 0 0 2px rgba(0,245,255,.1)}
|
||||
.srch-sel-sort{
|
||||
border-color:rgba(255,45,125,.4);
|
||||
background:#1a0d14;
|
||||
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23ff2d7d' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
|
||||
background-repeat:no-repeat;
|
||||
background-position:right 10px center;
|
||||
font-weight:700;
|
||||
color:#ff8ab0;
|
||||
}
|
||||
.srch-sel-sort option{background:#1a0d14;color:#fff}
|
||||
.srch-sel-sort:focus{border-color:var(--accent3)}
|
||||
.srch-btn{
|
||||
padding:11px 22px;border-radius:10px;border:none;
|
||||
background:linear-gradient(135deg,var(--accent),var(--accent3));
|
||||
color:#fff;font-size:.88rem;font-weight:800;cursor:pointer;
|
||||
transition:transform .14s,box-shadow .18s;flex-shrink:0;
|
||||
box-shadow:0 0 24px rgba(255,45,125,.3),0 4px 16px rgba(0,0,0,.3);
|
||||
}
|
||||
.srch-btn:hover{transform:translateY(-1px);box-shadow:0 0 32px rgba(255,45,125,.45);opacity:1}
|
||||
.srch-sort-label{
|
||||
font-size:.72rem;font-weight:700;color:var(--text3);letter-spacing:.06em;
|
||||
text-transform:uppercase;white-space:nowrap;padding-left:2px;
|
||||
}
|
||||
.srch-count{
|
||||
color:var(--text2);font-size:.85rem;margin-bottom:16px;
|
||||
animation:srch-in .4s cubic-bezier(.22,1,.36,1) .15s both;
|
||||
}
|
||||
.srch-count strong{color:var(--accent2)}
|
||||
|
||||
/* Card grid reveal animation */
|
||||
.a-grid .a-card{
|
||||
animation:srch-card-in .45s cubic-bezier(.22,1,.36,1) both;
|
||||
}
|
||||
@keyframes srch-card-in{
|
||||
from{opacity:0;transform:translateY(16px) scale(.97)}
|
||||
to{opacity:1;transform:none}
|
||||
}
|
||||
|
||||
@media(max-width:600px){
|
||||
.srch-hero{padding:24px 14px 20px}
|
||||
.srch-hero h1{font-size:1.05rem;margin-bottom:12px}
|
||||
.srch-form{gap:8px}
|
||||
.srch-sel{font-size:.8rem;padding:9px 8px;flex:1 1 90px}
|
||||
.srch-btn{width:100%;justify-content:center;padding:11px;display:flex;align-items:center;gap:6px}
|
||||
.srch-sort-label{display:none}
|
||||
}
|
||||
@media(max-width:380px){
|
||||
.srch-sel{flex:1 1 70px;font-size:.75rem;padding:8px 5px}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="srch-hero">
|
||||
<div style="max-width:1600px;margin:0 auto">
|
||||
<h1>{{ $q ? '"'.$q.'" için sonuçlar' : 'Tüm Animeler' }}</h1>
|
||||
<form action="{{ route('search') }}" method="GET" class="srch-form">
|
||||
{{-- Satır 1: Arama kutusu + Ara butonu --}}
|
||||
<div class="srch-row">
|
||||
<div class="srch-inp-wrap">
|
||||
<i class="bi bi-search"></i>
|
||||
<input type="text" name="q" value="{{ $q }}" placeholder="Anime adı, alternatif isim...">
|
||||
</div>
|
||||
<button type="submit" class="srch-btn"><i class="bi bi-search"></i> Ara</button>
|
||||
</div>
|
||||
{{-- Satır 2: Filtreler + Sıralama --}}
|
||||
<div class="srch-row">
|
||||
<select name="type" class="srch-sel">
|
||||
<option value="">Format (Hepsi)</option>
|
||||
<option value="series" {{ $type==='series' ?'selected':'' }}>Dizi</option>
|
||||
<option value="movie" {{ $type==='movie' ?'selected':'' }}>Film</option>
|
||||
<option value="ova" {{ $type==='ova' ?'selected':'' }}>OVA</option>
|
||||
<option value="ona" {{ $type==='ona' ?'selected':'' }}>ONA</option>
|
||||
<option value="special" {{ $type==='special'?'selected':'' }}>Special</option>
|
||||
</select>
|
||||
<select name="status" class="srch-sel">
|
||||
<option value="">Durum (Hepsi)</option>
|
||||
<option value="ongoing" {{ $status==='ongoing' ?'selected':'' }}>Devam Ediyor</option>
|
||||
<option value="completed"{{ $status==='completed'?'selected':'' }}>Tamamlandı</option>
|
||||
<option value="upcoming" {{ $status==='upcoming' ?'selected':'' }}>Yakında</option>
|
||||
</select>
|
||||
<select name="genre" class="srch-sel">
|
||||
<option value="">Tür (Hepsi)</option>
|
||||
@foreach($genres as $g)
|
||||
<option value="{{ $g->slug }}" {{ $genre===$g->slug?'selected':'' }}>{{ $g->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<select name="year" class="srch-sel">
|
||||
<option value="">Yıl (Hepsi)</option>
|
||||
@foreach($years as $y)
|
||||
<option value="{{ $y }}" {{ (string)$year===(string)$y?'selected':'' }}>{{ $y }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<span class="srch-sort-label"><i class="bi bi-sort-down"></i> Sırala:</span>
|
||||
<select name="sort" class="srch-sel srch-sel-sort" onchange="this.form.submit()">
|
||||
<option value="popular" {{ $sort==='popular' ?'selected':'' }}>🔥 En Popüler</option>
|
||||
<option value="rating" {{ $sort==='rating' ?'selected':'' }}>⭐ En Yüksek Puan</option>
|
||||
<option value="newest" {{ $sort==='newest' ?'selected':'' }}>🆕 En Yeni</option>
|
||||
<option value="oldest" {{ $sort==='oldest' ?'selected':'' }}>📅 En Eski</option>
|
||||
<option value="az" {{ $sort==='az' ?'selected':'' }}>🔤 A → Z</option>
|
||||
<option value="za" {{ $sort==='za' ?'selected':'' }}>🔤 Z → A</option>
|
||||
@auth
|
||||
<option value="personalized" {{ $sort==='personalized' ?'selected':'' }}>✨ Sana Özel</option>
|
||||
@endauth
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
@php
|
||||
$sortLabels = [
|
||||
'popular' => '🔥 En Popüler',
|
||||
'rating' => '⭐ En Yüksek Puan',
|
||||
'newest' => '🆕 En Yeni',
|
||||
'oldest' => '📅 En Eski',
|
||||
'az' => '🔤 A → Z',
|
||||
'za' => '🔤 Z → A',
|
||||
'personalized'=> '✨ Sana Özel',
|
||||
];
|
||||
@endphp
|
||||
<p class="srch-count">
|
||||
Toplam <strong>{{ $results->total() }}</strong> anime bulundu
|
||||
<span style="margin-left:10px;font-size:.78rem;color:var(--text3)">· {{ $sortLabels[$sort] ?? '' }}</span>
|
||||
</p>
|
||||
|
||||
@if($results->count())
|
||||
<div class="a-grid">
|
||||
@foreach($results as $anime)
|
||||
<a href="{{ $anime->slug ? route('anime.show', $anime->slug) : '#' }}" class="a-card">
|
||||
<div class="a-poster">
|
||||
@if($anime->coverUrl)
|
||||
<img src="{{ $anime->coverUrl }}" alt="{{ $anime->title }}" loading="lazy">
|
||||
@else
|
||||
<div class="a-no-img"><i class="bi bi-image"></i></div>
|
||||
@endif
|
||||
@if($anime->type==='movie')<span class="a-badge">Film</span>@endif
|
||||
@if($anime->rating)<span class="a-rating"><i class="bi bi-star-fill" style="font-size:.65rem"></i>{{ number_format($anime->rating,1) }}</span>@endif
|
||||
<div class="a-hover-play"><div class="play-circle"><i class="bi bi-play-fill"></i></div></div>
|
||||
</div>
|
||||
<div class="a-info">
|
||||
<div class="a-title">{{ $anime->title }}</div>
|
||||
<div class="a-meta">{{ $anime->release_year }}@if($anime->episode_count) · {{ $anime->episode_count }}bö@endif</div>
|
||||
</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if($results->hasPages())
|
||||
<div style="margin-top:32px">{{ $results->links('frontend.pagination') }}</div>
|
||||
@endif
|
||||
@else
|
||||
<div style="text-align:center;padding:64px 20px;color:var(--text3)">
|
||||
<i class="bi bi-search" style="font-size:3rem;opacity:.25;display:block;margin-bottom:14px"></i>
|
||||
<p>Sonuç bulunamadı. Farklı bir arama terimi deneyin.</p>
|
||||
<a href="{{ route('search') }}" style="color:var(--accent);margin-top:12px;display:inline-block">Tümünü Göster</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
@@ -0,0 +1,222 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Anime Mahkemesi | Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
.trib-hero{padding-top:calc(var(--nav-h) + 40px);padding-bottom:32px;text-align:center;background:radial-gradient(ellipse at 50% 0%,rgba(255,45,125,.12) 0%,transparent 70%)}
|
||||
.trib-hero h1{font-size:2rem;font-weight:900;letter-spacing:-.02em;margin-bottom:8px}
|
||||
.trib-hero h1 span{color:#ff2d7d}
|
||||
.trib-hero p{color:rgba(255,255,255,.45);font-size:.95rem}
|
||||
.trib-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px;padding:0 0 40px}
|
||||
.trib-card{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);border-radius:14px;padding:20px;transition:border-color .2s,transform .2s;cursor:pointer;text-decoration:none;display:block;color:#fff}
|
||||
.trib-card:hover{border-color:rgba(255,45,125,.3);transform:translateY(-2px)}
|
||||
.trib-card-anime{font-size:.72rem;color:rgba(255,45,125,.8);font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:8px}
|
||||
.trib-card-q{font-size:.98rem;font-weight:700;line-height:1.4;margin-bottom:14px}
|
||||
.trib-sides{display:flex;gap:8px;margin-bottom:14px}
|
||||
.trib-side{flex:1;padding:6px 10px;border-radius:8px;font-size:.75rem;font-weight:700;text-align:center}
|
||||
.trib-side.a{background:rgba(0,200,100,.1);color:#00c864;border:1px solid rgba(0,200,100,.2)}
|
||||
.trib-side.b{background:rgba(255,60,60,.1);color:#ff3c3c;border:1px solid rgba(255,60,60,.2)}
|
||||
.trib-side.c{background:rgba(59,130,246,.1);color:#3b82f6;border:1px solid rgba(59,130,246,.2)}
|
||||
.trib-side.d{background:rgba(245,158,11,.1);color:#f59e0b;border:1px solid rgba(245,158,11,.2)}
|
||||
.trib-side.e{background:rgba(167,139,250,.1);color:#a78bfa;border:1px solid rgba(167,139,250,.2)}
|
||||
.trib-side.f{background:rgba(236,72,153,.1);color:#ec4899;border:1px solid rgba(236,72,153,.2)}
|
||||
.trib-meta{display:flex;gap:12px;font-size:.72rem;color:rgba(255,255,255,.3)}
|
||||
.trib-status-open{color:#00c864;font-weight:700}
|
||||
.trib-status-closed{color:rgba(255,255,255,.3)}
|
||||
.trib-create-btn{display:inline-flex;align-items:center;gap:8px;padding:10px 24px;border-radius:20px;background:rgba(255,45,125,.15);border:1px solid rgba(255,45,125,.3);color:#ff2d7d;font-weight:700;font-size:.88rem;cursor:pointer;text-decoration:none;transition:all .15s;font-family:inherit}
|
||||
.trib-create-btn:hover{background:rgba(255,45,125,.28)}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:1000;display:none;align-items:center;justify-content:center;padding:16px}
|
||||
.modal-overlay.on{display:flex}
|
||||
.modal-box{background:#0e0e1a;border:1px solid rgba(255,255,255,.1);border-radius:18px;padding:28px;width:100%;max-width:520px;max-height:90vh;overflow-y:auto}
|
||||
.modal-title{font-size:1.15rem;font-weight:900;margin-bottom:20px}
|
||||
.mf-label{font-size:.75rem;font-weight:700;color:rgba(255,255,255,.45);text-transform:uppercase;letter-spacing:.07em;margin-bottom:6px}
|
||||
.mf-input{width:100%;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:10px 14px;color:#fff;font-size:.9rem;font-family:inherit;outline:none;margin-bottom:14px;resize:none}
|
||||
.mf-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
.mf-btn{width:100%;padding:12px;border-radius:12px;border:none;background:rgba(255,45,125,.2);color:#ff2d7d;font-weight:700;font-size:.9rem;cursor:pointer;font-family:inherit;transition:background .15s}
|
||||
.mf-btn:hover{background:rgba(255,45,125,.35)}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div style="max-width:1200px;margin:0 auto;padding:0 20px">
|
||||
|
||||
<div class="trib-hero">
|
||||
<h1>⚖️ Anime <span>Mahkemesi</span></h1>
|
||||
<p>Tartışmalı kararlar, karakterler, sahneler — topluluk yargılıyor.</p>
|
||||
@auth
|
||||
<br>
|
||||
<button class="trib-create-btn" onclick="document.getElementById('create-modal').classList.add('on')">
|
||||
<i class="bi bi-plus-lg"></i> Dava Aç
|
||||
</button>
|
||||
@endauth
|
||||
</div>
|
||||
|
||||
<div class="trib-grid">
|
||||
@forelse($tribunals as $t)
|
||||
<a class="trib-card" href="{{ route('tribunal.show', $t) }}">
|
||||
<div class="trib-card-anime">{{ $t->anime->title }}</div>
|
||||
<div class="trib-card-q">{{ $t->question }}</div>
|
||||
<div class="trib-sides" style="grid-template-columns:repeat({{ min(count($t->allSides()), 4) }},1fr)">
|
||||
@foreach($t->allSides() as $key => $label)
|
||||
<div class="trib-side {{ $key }}">{{ Str::limit($label, 20) }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="trib-meta">
|
||||
<span class="trib-status-{{ $t->status }}">{{ $t->status === 'open' ? '🟢 Açık' : '🔴 Kapalı' }}</span>
|
||||
<span><i class="bi bi-people"></i> {{ $t->votes_count }} oy</span>
|
||||
<span>{{ $t->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
</a>
|
||||
@empty
|
||||
<div style="grid-column:1/-1;text-align:center;padding:60px;color:rgba(255,255,255,.3)">
|
||||
<i class="bi bi-balance" style="font-size:3rem;display:block;margin-bottom:12px"></i>
|
||||
Henüz dava açılmamış.
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{ $tribunals->links('frontend.pagination') }}
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Dava Açma Modalı --}}
|
||||
@auth
|
||||
<div class="modal-overlay" id="create-modal" onclick="if(event.target===this)this.classList.remove('on')">
|
||||
<div class="modal-box">
|
||||
<div class="modal-title">⚖️ Yeni Dava Aç</div>
|
||||
<div class="mf-label">Anime</div>
|
||||
<input class="mf-input" type="text" id="cm-anime-search" placeholder="Anime adı ara..." autocomplete="off" oninput="searchAnime(this.value)">
|
||||
<div id="cm-anime-results" style="margin-bottom:14px;display:none;background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.1);border-radius:10px;overflow:hidden;max-height:160px;overflow-y:auto"></div>
|
||||
<input type="hidden" id="cm-anime-id">
|
||||
|
||||
<div class="mf-label">Soru / Karar</div>
|
||||
<textarea class="mf-input" id="cm-question" rows="2" maxlength="280" placeholder="Örn: Sasuke'nin Konoha'yı terk etmesi doğru muydu?"></textarea>
|
||||
|
||||
<div id="cm-sides-wrap">
|
||||
<div class="mf-row" style="margin-bottom:0">
|
||||
<div>
|
||||
<div class="mf-label" style="color:#00c864">Taraf A (Yeşil)</div>
|
||||
<input class="mf-input" id="cm-side-a" maxlength="100" placeholder="Örn: Haklıydı">
|
||||
</div>
|
||||
<div>
|
||||
<div class="mf-label" style="color:#ff3c3c">Taraf B (Kırmızı)</div>
|
||||
<input class="mf-input" id="cm-side-b" maxlength="100" placeholder="Örn: Haksızdı">
|
||||
</div>
|
||||
</div>
|
||||
<div id="cm-extra-sides"></div>
|
||||
</div>
|
||||
<div style="margin-bottom:14px">
|
||||
<button type="button" id="cm-add-side-btn" onclick="addExtraSide()"
|
||||
style="background:none;border:1px dashed rgba(255,255,255,.2);border-radius:8px;color:rgba(255,255,255,.4);font-size:.78rem;padding:6px 14px;cursor:pointer;font-family:inherit;transition:all .15s;width:100%"
|
||||
onmouseover="this.style.borderColor='rgba(255,255,255,.4)';this.style.color='#fff'"
|
||||
onmouseout="this.style.borderColor='rgba(255,255,255,.2)';this.style.color='rgba(255,255,255,.4)'">
|
||||
<i class="bi bi-plus-lg"></i> Seçenek Ekle (max 4 ekstra)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mf-label">Kapanış Tarihi (isteğe bağlı)</div>
|
||||
<input class="mf-input" type="date" id="cm-closes-at" min="{{ now()->addDay()->format('Y-m-d') }}">
|
||||
|
||||
<div id="cm-error" style="color:#ff4444;font-size:.8rem;margin-bottom:10px;display:none"></div>
|
||||
<button class="mf-btn" onclick="submitTribunal()">Davayı Aç</button>
|
||||
</div>
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
@endsection
|
||||
@push('scripts')
|
||||
<script>
|
||||
const CSRF = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
|
||||
let animeSearchTimer = null;
|
||||
|
||||
function searchAnime(q) {
|
||||
clearTimeout(animeSearchTimer);
|
||||
if (q.length < 2) { document.getElementById('cm-anime-results').style.display = 'none'; return; }
|
||||
animeSearchTimer = setTimeout(async () => {
|
||||
const r = await fetch(`/search?q=${encodeURIComponent(q)}&json=1`);
|
||||
if (!r.ok) return;
|
||||
const d = await r.json();
|
||||
const res = document.getElementById('cm-anime-results');
|
||||
const items = (d.animes || d.results || []).slice(0, 6);
|
||||
if (!items.length) { res.style.display = 'none'; return; }
|
||||
res.style.display = 'block';
|
||||
res.innerHTML = items.map(a => `<div onclick="selectAnime(${a.id},'${a.title.replace(/'/g,"\\'")}')"
|
||||
style="padding:8px 12px;cursor:pointer;font-size:.85rem;border-bottom:1px solid rgba(255,255,255,.05);display:flex;align-items:center;gap:10px"
|
||||
onmouseover="this.style.background='rgba(255,255,255,.07)'" onmouseout="this.style.background=''">
|
||||
${a.cover ? `<img src="${a.cover}" style="width:28px;height:40px;object-fit:cover;border-radius:3px;flex-shrink:0">` : '<div style="width:28px;height:40px;background:#222;border-radius:3px;flex-shrink:0"></div>'}
|
||||
<span>${a.title}</span>
|
||||
</div>`).join('');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function selectAnime(id, title) {
|
||||
document.getElementById('cm-anime-id').value = id;
|
||||
document.getElementById('cm-anime-search').value = title;
|
||||
document.getElementById('cm-anime-results').style.display = 'none';
|
||||
}
|
||||
|
||||
const SIDE_COLORS = ['#00c864','#ff3c3c','#3b82f6','#f59e0b','#a78bfa','#ec4899'];
|
||||
const SIDE_NAMES = ['A (Yeşil)','B (Kırmızı)','C (Mavi)','D (Turuncu)','E (Mor)','F (Pembe)'];
|
||||
let extraSideCount = 0;
|
||||
|
||||
function addExtraSide() {
|
||||
if (extraSideCount >= 4) return;
|
||||
extraSideCount++;
|
||||
const idx = extraSideCount + 1; // 0=a,1=b, so extra starts at 2 (c)
|
||||
const color = SIDE_COLORS[idx] || '#ffffff';
|
||||
const name = SIDE_NAMES[idx] || ('Seçenek ' + (idx+1));
|
||||
const wrap = document.getElementById('cm-extra-sides');
|
||||
const div = document.createElement('div');
|
||||
div.id = 'cm-extra-' + extraSideCount;
|
||||
div.style.marginTop = '10px';
|
||||
div.innerHTML = `
|
||||
<div style="display:flex;align-items:center;gap:6px">
|
||||
<div style="font-size:.72rem;font-weight:700;color:${color};text-transform:uppercase;letter-spacing:.07em;white-space:nowrap">Taraf ${name}</div>
|
||||
<button type="button" onclick="removeExtraSide(${extraSideCount})"
|
||||
style="margin-left:auto;background:none;border:none;color:rgba(255,80,80,.5);cursor:pointer;font-size:.8rem;padding:0 4px">✕</button>
|
||||
</div>
|
||||
<input class="mf-input" id="cm-extra-val-${extraSideCount}" maxlength="100" placeholder="Seçenek metni..." style="margin-top:4px">`;
|
||||
wrap.appendChild(div);
|
||||
if (extraSideCount >= 4) document.getElementById('cm-add-side-btn').style.display = 'none';
|
||||
}
|
||||
|
||||
function removeExtraSide(n) {
|
||||
document.getElementById('cm-extra-' + n)?.remove();
|
||||
extraSideCount = Math.max(0, extraSideCount - 1);
|
||||
document.getElementById('cm-add-side-btn').style.display = '';
|
||||
// Renumber visible extras
|
||||
const extras = document.getElementById('cm-extra-sides').children;
|
||||
extraSideCount = extras.length;
|
||||
}
|
||||
|
||||
async function submitTribunal() {
|
||||
const err = document.getElementById('cm-error');
|
||||
err.style.display = 'none';
|
||||
const animeId = document.getElementById('cm-anime-id').value;
|
||||
const question = document.getElementById('cm-question').value.trim();
|
||||
const sideA = document.getElementById('cm-side-a').value.trim();
|
||||
const sideB = document.getElementById('cm-side-b').value.trim();
|
||||
const closesAt = document.getElementById('cm-closes-at').value;
|
||||
|
||||
// Extra sides
|
||||
const extraSides = [];
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
const v = document.getElementById('cm-extra-val-' + i)?.value.trim();
|
||||
if (v) extraSides.push(v);
|
||||
}
|
||||
|
||||
if (!animeId) { err.textContent = 'Anime seçin.'; err.style.display = 'block'; return; }
|
||||
if (question.length < 10) { err.textContent = 'Soru en az 10 karakter olmalı.'; err.style.display = 'block'; return; }
|
||||
if (!sideA || !sideB) { err.textContent = 'A ve B taraflarını doldurun.'; err.style.display = 'block'; return; }
|
||||
|
||||
const r = await fetch('/mahkeme', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json' },
|
||||
body: JSON.stringify({ anime_id: animeId, question, side_a: sideA, side_b: sideB, extra_sides: extraSides, closes_at: closesAt || null }),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (d.url) window.location.href = d.url;
|
||||
else { err.textContent = d.error || 'Hata oluştu.'; err.style.display = 'block'; }
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,253 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Mahkeme: ' . Str::limit($tribunal->question, 60) . ' | Animexe')
|
||||
@php
|
||||
// Sabit renk paleti — a,b,c,d,e,f
|
||||
$palette = [
|
||||
'a' => ['bg'=>'rgba(0,200,100,.08)', 'bg2'=>'rgba(0,200,100,.22)', 'border'=>'rgba(0,200,100,.25)', 'border2'=>'#00c864', 'text'=>'#00c864', 'bar'=>'#00c864'],
|
||||
'b' => ['bg'=>'rgba(255,60,60,.08)', 'bg2'=>'rgba(255,60,60,.22)', 'border'=>'rgba(255,60,60,.25)', 'border2'=>'#ff3c3c', 'text'=>'#ff3c3c', 'bar'=>'#ff3c3c'],
|
||||
'c' => ['bg'=>'rgba(59,130,246,.08)', 'bg2'=>'rgba(59,130,246,.22)', 'border'=>'rgba(59,130,246,.25)', 'border2'=>'#3b82f6', 'text'=>'#3b82f6', 'bar'=>'#3b82f6'],
|
||||
'd' => ['bg'=>'rgba(245,158,11,.08)', 'bg2'=>'rgba(245,158,11,.22)', 'border'=>'rgba(245,158,11,.25)', 'border2'=>'#f59e0b', 'text'=>'#f59e0b', 'bar'=>'#f59e0b'],
|
||||
'e' => ['bg'=>'rgba(167,139,250,.08)','bg2'=>'rgba(167,139,250,.22)','border'=>'rgba(167,139,250,.25)','border2'=>'#a78bfa','text'=>'#a78bfa','bar'=>'#a78bfa'],
|
||||
'f' => ['bg'=>'rgba(236,72,153,.08)', 'bg2'=>'rgba(236,72,153,.22)', 'border'=>'rgba(236,72,153,.25)', 'border2'=>'#ec4899','text'=>'#ec4899','bar'=>'#ec4899'],
|
||||
];
|
||||
$cols = count($allSides) <= 2 ? '1fr 1fr' : (count($allSides) <= 3 ? '1fr 1fr 1fr' : 'repeat(auto-fit,minmax(140px,1fr))');
|
||||
@endphp
|
||||
@push('styles')
|
||||
<style>
|
||||
.tb-wrap{max-width:900px;margin:0 auto;padding:calc(var(--nav-h) + 32px) 20px 60px}
|
||||
.tb-anime-link{font-size:.75rem;color:rgba(255,45,125,.8);font-weight:700;text-transform:uppercase;letter-spacing:.08em;text-decoration:none;display:inline-block;margin-bottom:12px}
|
||||
.tb-question{font-size:1.5rem;font-weight:900;line-height:1.35;margin-bottom:14px}
|
||||
.tb-status{display:inline-block;padding:3px 12px;border-radius:20px;font-size:.72rem;font-weight:700;margin-bottom:20px}
|
||||
.tb-status.open{background:rgba(0,200,100,.15);color:#00c864;border:1px solid rgba(0,200,100,.25)}
|
||||
.tb-status.closed{background:rgba(255,255,255,.06);color:rgba(255,255,255,.4);border:1px solid rgba(255,255,255,.1)}
|
||||
|
||||
/* Vote Card */
|
||||
.tb-votebar{background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:24px;margin-bottom:24px}
|
||||
.tb-sides{display:grid;gap:10px;margin-bottom:20px;grid-template-columns:{{ $cols }}}
|
||||
.tb-vote-btn{padding:13px 10px;border-radius:12px;border:2px solid transparent;cursor:pointer;font-weight:800;font-size:.88rem;font-family:inherit;transition:all .2s;text-align:center;line-height:1.3}
|
||||
|
||||
/* Multi-bar */
|
||||
.tb-multibar{display:flex;flex-direction:column;gap:8px}
|
||||
.tb-bar-row{display:flex;align-items:center;gap:10px}
|
||||
.tb-bar-label{font-size:.72rem;font-weight:700;white-space:nowrap;min-width:80px;max-width:120px;overflow:hidden;text-overflow:ellipsis}
|
||||
.tb-bar-track{flex:1;height:8px;border-radius:99px;background:rgba(255,255,255,.07);overflow:hidden}
|
||||
.tb-bar-fill{height:100%;border-radius:99px;transition:width .5s cubic-bezier(.22,1,.36,1)}
|
||||
.tb-bar-pct{font-size:.72rem;color:rgba(255,255,255,.4);min-width:40px;text-align:right}
|
||||
|
||||
/* Arguments */
|
||||
.tb-args{margin-top:28px}
|
||||
.tb-args-title{font-size:.8rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.3);margin-bottom:16px}
|
||||
.tb-arg-card{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);border-radius:12px;padding:16px;margin-bottom:10px}
|
||||
.tb-arg-side-badge{display:inline-block;padding:2px 10px;border-radius:20px;font-size:.68rem;font-weight:800;margin-bottom:8px}
|
||||
.tb-arg-body{font-size:.9rem;line-height:1.5;margin-bottom:10px}
|
||||
.tb-arg-meta{display:flex;align-items:center;gap:12px;font-size:.72rem;color:rgba(255,255,255,.3)}
|
||||
.tb-arg-vote-btn{background:none;border:1px solid rgba(255,255,255,.1);border-radius:20px;padding:3px 12px;color:rgba(255,255,255,.4);cursor:pointer;font-size:.72rem;font-family:inherit;display:inline-flex;align-items:center;gap:5px;transition:all .15s}
|
||||
.tb-arg-vote-btn:hover,.tb-arg-vote-btn.on{background:rgba(255,215,0,.12);border-color:rgba(255,215,0,.3);color:#ffd700}
|
||||
|
||||
/* Argue form */
|
||||
.tb-argue-form{background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.07);border-radius:14px;padding:20px;margin-top:24px}
|
||||
.tb-argue-title{font-size:.88rem;font-weight:700;margin-bottom:12px}
|
||||
.tb-argue-sides{display:grid;gap:8px;margin-bottom:12px;grid-template-columns:{{ $cols }}}
|
||||
.tb-argue-side-btn{padding:9px 8px;border-radius:10px;cursor:pointer;font-weight:700;font-size:.8rem;font-family:inherit;border:2px solid transparent;transition:all .15s;text-align:center}
|
||||
textarea.tb-inp{width:100%;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:10px 14px;color:#fff;font-size:.88rem;font-family:inherit;resize:none;outline:none;margin-bottom:10px;box-sizing:border-box}
|
||||
.tb-submit-btn{padding:10px 24px;border-radius:10px;border:none;background:rgba(255,45,125,.2);color:#ff2d7d;font-weight:700;font-size:.88rem;cursor:pointer;font-family:inherit;transition:background .15s}
|
||||
.tb-submit-btn:hover{background:rgba(255,45,125,.35)}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div class="tb-wrap">
|
||||
|
||||
<a class="tb-anime-link" href="{{ $tribunal->anime?->detailUrl ?? '#' }}">
|
||||
<i class="bi bi-arrow-left"></i> {{ $tribunal->anime->title }}
|
||||
</a>
|
||||
|
||||
<div class="tb-status {{ $tribunal->status }}">
|
||||
{{ $tribunal->status === 'open' ? '🟢 Dava Açık' : '🔴 Dava Kapalı' }}
|
||||
@if($tribunal->closes_at && $tribunal->status === 'open')
|
||||
· {{ $tribunal->closes_at->diffForHumans() }} kapanıyor
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="tb-question">{{ $tribunal->question }}</div>
|
||||
|
||||
{{-- Oy sistemi --}}
|
||||
<div class="tb-votebar">
|
||||
<div class="tb-sides">
|
||||
@foreach($allSides as $key => $label)
|
||||
@php $c = $palette[$key] ?? $palette['a']; @endphp
|
||||
<button id="vote-{{ $key }}" class="tb-vote-btn"
|
||||
style="background:{{ $c['bg'] }};color:{{ $c['text'] }};border-color:{{ $c['border'] }}"
|
||||
data-bg="{{ $c['bg'] }}" data-bg2="{{ $c['bg2'] }}" data-border="{{ $c['border'] }}" data-border2="{{ $c['border2'] }}"
|
||||
onclick="castVote('{{ $key }}')">
|
||||
{{ $label }}
|
||||
<div style="font-size:.7rem;font-weight:400;opacity:.7;margin-top:3px">
|
||||
<span id="vcnt-{{ $key }}">{{ $voteCounts[$key] ?? 0 }}</span> oy
|
||||
· <span id="vpct-{{ $key }}">{{ $total > 0 ? round(($voteCounts[$key]??0)/$total*100) : 0 }}%</span>
|
||||
</div>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Çok taraflı bar --}}
|
||||
<div class="tb-multibar">
|
||||
@foreach($allSides as $key => $label)
|
||||
@php $c = $palette[$key] ?? $palette['a']; $pct = $total > 0 ? round(($voteCounts[$key]??0)/$total*100) : 0; @endphp
|
||||
<div class="tb-bar-row">
|
||||
<div class="tb-bar-label" style="color:{{ $c['text'] }}">{{ $label }}</div>
|
||||
<div class="tb-bar-track">
|
||||
<div class="tb-bar-fill" id="bar-{{ $key }}" style="width:{{ $pct }}%;background:{{ $c['bar'] }}"></div>
|
||||
</div>
|
||||
<div class="tb-bar-pct" id="bpct-{{ $key }}">{{ $pct }}%</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div style="margin-top:10px;font-size:.72rem;color:rgba(255,255,255,.25)">
|
||||
Toplam <span id="total-votes">{{ $total }}</span> oy
|
||||
@auth @if($myVote) · <span style="color:rgba(255,255,255,.5)">Oyunuz: <strong>{{ $allSides[$myVote] ?? $myVote }}</strong></span> @endif @endauth
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Argümanlar --}}
|
||||
<div class="tb-args">
|
||||
<div class="tb-args-title"><i class="bi bi-chat-quote"></i> Argümanlar</div>
|
||||
<div id="arg-list">
|
||||
@forelse($arguments as $arg)
|
||||
@php $c2 = $palette[$arg['side']] ?? $palette['a']; @endphp
|
||||
<div class="tb-arg-card" id="arg-{{ $arg['id'] }}">
|
||||
<span class="tb-arg-side-badge"
|
||||
style="background:{{ $c2['bg'] }};color:{{ $c2['text'] }}">{{ $allSides[$arg['side']] ?? $arg['side'] }}</span>
|
||||
<div class="tb-arg-body">{{ $arg['body'] }}</div>
|
||||
<div class="tb-arg-meta">
|
||||
<span>{{ $arg['name'] ?? $arg['username'] }}</span>
|
||||
<span>{{ $arg['created_at'] }}</span>
|
||||
<button class="tb-arg-vote-btn {{ $arg['voted'] ? 'on' : '' }}" id="avgbtn-{{ $arg['id'] }}"
|
||||
onclick="argVote({{ $arg['id'] }})">
|
||||
<i class="bi bi-hand-thumbs-up"></i>
|
||||
<span id="avgcnt-{{ $arg['id'] }}">{{ $arg['vote_count'] }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div id="no-args" style="text-align:center;padding:30px;color:rgba(255,255,255,.25);font-size:.85rem">
|
||||
Henüz argüman girilmemiş. İlk sen gir!
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Argüman formu --}}
|
||||
@auth
|
||||
@if(!$myArgument && $tribunal->status === 'open')
|
||||
<div class="tb-argue-form">
|
||||
<div class="tb-argue-title">⚔️ Senin Argümanın — Hangi taraftasın?</div>
|
||||
<div class="tb-argue-sides">
|
||||
@foreach($allSides as $key => $label)
|
||||
@php $c3 = $palette[$key] ?? $palette['a']; @endphp
|
||||
<button id="arg-side-{{ $key }}" class="tb-argue-side-btn"
|
||||
style="background:{{ $c3['bg'] }};color:{{ $c3['text'] }};border-color:{{ $c3['border'] }}"
|
||||
data-bg="{{ $c3['bg'] }}" data-bg2="{{ $c3['bg2'] }}" data-border="{{ $c3['border'] }}" data-border2="{{ $c3['border2'] }}"
|
||||
onclick="selectArgSide('{{ $key }}')">{{ $label }}</button>
|
||||
@endforeach
|
||||
</div>
|
||||
<textarea class="tb-inp" id="arg-body" rows="3" maxlength="500" placeholder="Argümanını yaz (min 10, max 500 karakter)..."></textarea>
|
||||
<div id="arg-err" style="color:#ff4444;font-size:.78rem;margin-bottom:8px;display:none"></div>
|
||||
<button class="tb-submit-btn" onclick="submitArg()">Argümanı Gönder</button>
|
||||
</div>
|
||||
@elseif($myArgument)
|
||||
<div style="padding:14px;background:rgba(0,200,100,.06);border:1px solid rgba(0,200,100,.2);border-radius:12px;font-size:.85rem;color:rgba(255,255,255,.5);margin-top:24px">
|
||||
✅ Argümanınızı gönderdiniz.
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div style="margin-top:20px;text-align:center;font-size:.85rem;color:rgba(255,255,255,.3)">
|
||||
<a href="{{ route('frontend.login') }}" style="color:#ff2d7d">Giriş yap</a> — argüman girmek için
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
@push('scripts')
|
||||
<script>
|
||||
const CSRF = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
|
||||
const IS_AUTH = {{ auth()->check() ? 'true' : 'false' }};
|
||||
const MY_VOTE = {{ $myVote ? json_encode($myVote) : 'null' }};
|
||||
let selectedArgSide = null;
|
||||
|
||||
// İlk yüklemede aktif oyu vurgula
|
||||
if (MY_VOTE) activateVoteBtn(MY_VOTE);
|
||||
|
||||
function activateVoteBtn(side) {
|
||||
document.querySelectorAll('.tb-vote-btn').forEach(b => {
|
||||
const isMe = b.id === 'vote-' + side;
|
||||
b.style.background = isMe ? b.dataset.bg2 : b.dataset.bg;
|
||||
b.style.borderColor = isMe ? b.dataset.border2 : b.dataset.border;
|
||||
});
|
||||
}
|
||||
|
||||
function selectArgSide(s) {
|
||||
selectedArgSide = s;
|
||||
document.querySelectorAll('.tb-argue-side-btn').forEach(b => {
|
||||
const isMe = b.id === 'arg-side-' + s;
|
||||
b.style.background = isMe ? b.dataset.bg2 : b.dataset.bg;
|
||||
b.style.borderColor = isMe ? b.dataset.border2 : b.dataset.border;
|
||||
});
|
||||
}
|
||||
|
||||
async function castVote(side) {
|
||||
if (!IS_AUTH) { window.location.href = '{{ route('frontend.login') }}'; return; }
|
||||
const r = await fetch('{{ route('tribunal.vote', $tribunal) }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json' },
|
||||
body: JSON.stringify({ side }),
|
||||
});
|
||||
const d = await r.json();
|
||||
updateVoteBar(d.voted, d.counts, d.total);
|
||||
}
|
||||
|
||||
function updateVoteBar(voted, counts, total) {
|
||||
activateVoteBtn(voted);
|
||||
document.getElementById('total-votes').textContent = total;
|
||||
for (const [key, cnt] of Object.entries(counts)) {
|
||||
const pct = total > 0 ? Math.round(cnt / total * 100) : 0;
|
||||
const fill = document.getElementById('bar-' + key);
|
||||
const pctEl = document.getElementById('bpct-' + key);
|
||||
const vcnt = document.getElementById('vcnt-' + key);
|
||||
const vpct = document.getElementById('vpct-' + key);
|
||||
if (fill) fill.style.width = pct + '%';
|
||||
if (pctEl) pctEl.textContent = pct + '%';
|
||||
if (vcnt) vcnt.textContent = cnt;
|
||||
if (vpct) vpct.textContent = pct + '%';
|
||||
}
|
||||
}
|
||||
|
||||
async function argVote(id) {
|
||||
if (!IS_AUTH) { window.location.href = '{{ route('frontend.login') }}'; return; }
|
||||
const r = await fetch(`/arguman/${id}/oy`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const d = await r.json();
|
||||
document.getElementById('avgcnt-' + id).textContent = d.vote_count;
|
||||
document.getElementById('avgbtn-' + id).classList.toggle('on', d.voted);
|
||||
}
|
||||
|
||||
async function submitArg() {
|
||||
const err = document.getElementById('arg-err');
|
||||
const body = document.getElementById('arg-body').value.trim();
|
||||
err.style.display = 'none';
|
||||
if (!selectedArgSide) { err.textContent = 'Taraf seçin.'; err.style.display = 'block'; return; }
|
||||
if (body.length < 10) { err.textContent = 'Argüman en az 10 karakter olmalı.'; err.style.display = 'block'; return; }
|
||||
|
||||
const r = await fetch('{{ route('tribunal.argue', $tribunal) }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json' },
|
||||
body: JSON.stringify({ side: selectedArgSide, body }),
|
||||
});
|
||||
const d = await r.json();
|
||||
if (d.ok) window.location.reload();
|
||||
else { err.textContent = d.error || 'Hata.'; err.style.display = 'block'; }
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,274 @@
|
||||
@extends('frontend.layouts.app')
|
||||
@section('title', 'Watch Party — ' . $party->episode->anime->title . ' | Animexe')
|
||||
@push('styles')
|
||||
<style>
|
||||
body{background:#05050e}
|
||||
#pg{padding-top:0}
|
||||
.wp-wrap{display:flex;max-width:1600px;margin:0 auto;padding-top:var(--nav-h);min-height:calc(100vh - var(--nav-h));gap:0}
|
||||
.wp-main{flex:1;min-width:0;background:#020208;display:flex;flex-direction:column}
|
||||
.wp-side{width:320px;flex-shrink:0;background:rgba(10,10,22,.98);border-left:1px solid rgba(255,45,125,.15);display:flex;flex-direction:column;height:calc(100vh - var(--nav-h));position:sticky;top:var(--nav-h)}
|
||||
|
||||
/* Video */
|
||||
.wp-vwrap{position:relative;width:100%;aspect-ratio:16/9;background:#000}
|
||||
.wp-vwrap iframe,.wp-vwrap video{position:absolute;inset:0;width:100%;height:100%;border:none}
|
||||
|
||||
/* Host bar */
|
||||
.wp-hostbar{padding:12px 16px;background:rgba(255,45,125,.06);border-bottom:1px solid rgba(255,45,125,.15);display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||||
.wp-roomcode{font-size:1.4rem;font-weight:900;letter-spacing:.18em;color:#ff2d7d;cursor:pointer;user-select:all}
|
||||
.wp-hostbtn{padding:6px 16px;border-radius:20px;border:1px solid rgba(255,45,125,.4);background:rgba(255,45,125,.1);color:#ff2d7d;font-size:.8rem;font-weight:700;cursor:pointer;font-family:inherit;transition:all .15s}
|
||||
.wp-hostbtn:hover{background:rgba(255,45,125,.25)}
|
||||
.wp-sync-status{font-size:.72rem;color:rgba(255,255,255,.3);margin-left:auto}
|
||||
|
||||
/* Members */
|
||||
.wp-members{padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.06)}
|
||||
.wp-members-title{font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;color:rgba(255,255,255,.3);margin-bottom:8px}
|
||||
.wp-member-list{display:flex;flex-wrap:wrap;gap:6px}
|
||||
.wp-member{display:flex;align-items:center;gap:6px;padding:4px 10px;background:rgba(255,255,255,.06);border-radius:20px;font-size:.76rem;color:#fff}
|
||||
.wp-member.host{border:1px solid rgba(255,215,0,.3);background:rgba(255,215,0,.06)}
|
||||
|
||||
/* Chat */
|
||||
.wp-chat{flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:8px;scroll-behavior:smooth}
|
||||
.wc-msg{max-width:100%}
|
||||
.wc-meta{font-size:.68rem;color:rgba(255,255,255,.3);margin-bottom:3px}
|
||||
.wc-bubble{display:inline-block;padding:7px 12px;border-radius:0 10px 10px 10px;background:rgba(255,255,255,.07);font-size:.84rem;color:#fff;word-break:break-word;max-width:100%;line-height:1.4}
|
||||
.wc-mine .wc-bubble{background:rgba(255,45,125,.15);border-radius:10px 0 10px 10px}
|
||||
.wc-mine{align-self:flex-end;text-align:right}
|
||||
.wc-mine .wc-meta{text-align:right}
|
||||
|
||||
/* Chat input */
|
||||
.wp-chat-input{padding:12px 16px;border-top:1px solid rgba(255,255,255,.07);display:flex;gap:8px;flex-shrink:0}
|
||||
.wp-chat-input input{flex:1;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:9px 14px;color:#fff;font-size:.84rem;font-family:inherit;outline:none}
|
||||
.wp-chat-input button{padding:9px 16px;border-radius:10px;border:none;background:rgba(255,45,125,.2);color:#ff2d7d;font-size:.9rem;cursor:pointer;flex-shrink:0;transition:background .15s}
|
||||
.wp-chat-input button:hover{background:rgba(255,45,125,.35)}
|
||||
|
||||
/* Episode info */
|
||||
.wp-ep-info{padding:14px 16px;border-bottom:1px solid rgba(255,255,255,.06);display:flex;gap:12px;align-items:center}
|
||||
.wp-ep-info img{width:48px;height:72px;border-radius:8px;object-fit:cover;flex-shrink:0}
|
||||
.wp-ep-title{font-size:.84rem;font-weight:700;color:#fff;margin-bottom:2px}
|
||||
.wp-ep-sub{font-size:.72rem;color:rgba(255,255,255,.35)}
|
||||
</style>
|
||||
@endpush
|
||||
@section('content')
|
||||
<div class="wp-wrap">
|
||||
|
||||
{{-- ── Ana video alanı ─────────────────────────────────────────── --}}
|
||||
<div class="wp-main">
|
||||
|
||||
{{-- Video --}}
|
||||
<div class="wp-vwrap" id="wp-vwrap">
|
||||
@if($party->episode->m3u8_url || $party->episode->video_url)
|
||||
<video id="wp-vd" controls playsinline preload="metadata"
|
||||
@if($party->episode->m3u8_url) data-src="{{ $party->episode->m3u8_url }}" @endif
|
||||
@if($party->episode->video_url) src="{{ $party->episode->video_url }}" @endif>
|
||||
</video>
|
||||
@else
|
||||
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:rgba(255,255,255,.3);font-size:.9rem">
|
||||
<div style="text-align:center"><i class="bi bi-play-circle" style="font-size:3rem;display:block;margin-bottom:8px"></i>Video yüklenemedi</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Host kontrol barı --}}
|
||||
<div class="wp-hostbar">
|
||||
<div>
|
||||
<div style="font-size:.68rem;color:rgba(255,255,255,.3);margin-bottom:2px">Oda Kodu</div>
|
||||
<div class="wp-roomcode" onclick="copyCode()" title="Kopyalamak için tıkla">{{ $party->room_code }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:.68rem;color:rgba(255,255,255,.3);margin-bottom:4px">
|
||||
Ev Sahibi: <span id="wp-host-name" style="color:#fff">{{ $party->host->name ?? '—' }}</span>
|
||||
@auth @if(auth()->id() === $party->host_user_id)
|
||||
<span style="background:rgba(255,215,0,.15);color:#ffd700;padding:1px 7px;border-radius:10px;font-size:.65rem;margin-left:5px">Sensin</span>
|
||||
@endif @endauth
|
||||
</div>
|
||||
<div id="wp-member-count" style="font-size:.72rem;color:rgba(255,255,255,.35)">0 üye aktif</div>
|
||||
</div>
|
||||
@auth @if(auth()->id() === $party->host_user_id)
|
||||
<button class="wp-hostbtn" onclick="hostPlay()"><i class="bi bi-play-fill"></i> Oynat</button>
|
||||
<button class="wp-hostbtn" onclick="hostPause()"><i class="bi bi-pause-fill"></i> Durdur</button>
|
||||
@endif @endauth
|
||||
<span class="wp-sync-status" id="wp-sync-status">⏱ bekleniyor...</span>
|
||||
<a href="{{ route('watch', [$party->episode->anime->slug, $party->episode->season->season_number ?? 1, $party->episode->episode_number]) }}"
|
||||
style="margin-left:auto;font-size:.76rem;color:rgba(255,255,255,.3);text-decoration:none">
|
||||
<i class="bi bi-box-arrow-up-right"></i> Tam oynatıcı
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- ── Sağ panel: Üyeler + Chat ─────────────────────────────────── --}}
|
||||
<div class="wp-side">
|
||||
|
||||
{{-- Bölüm bilgisi --}}
|
||||
<div class="wp-ep-info">
|
||||
@if($party->episode->anime->coverUrl)
|
||||
<img src="{{ $party->episode->anime->coverUrl }}" alt="">
|
||||
@endif
|
||||
<div>
|
||||
<div class="wp-ep-title">{{ $party->episode->anime->title }}</div>
|
||||
<div class="wp-ep-sub">S{{ $party->episode->season->season_number ?? 1 }}E{{ $party->episode->episode_number }}
|
||||
{{ $party->episode->title ? '— ' . $party->episode->title : '' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Aktif üyeler --}}
|
||||
<div class="wp-members">
|
||||
<div class="wp-members-title"><i class="bi bi-people"></i> Aktif Üyeler</div>
|
||||
<div class="wp-member-list" id="wp-members"></div>
|
||||
</div>
|
||||
|
||||
{{-- Chat --}}
|
||||
<div class="wp-chat" id="wp-chat"></div>
|
||||
|
||||
{{-- Chat input --}}
|
||||
@auth
|
||||
<div class="wp-chat-input">
|
||||
<input type="text" id="wp-chat-inp" placeholder="Mesaj yaz..." maxlength="200"
|
||||
onkeydown="if(event.key==='Enter')sendChatMsg()">
|
||||
<button onclick="sendChatMsg()"><i class="bi bi-send-fill"></i></button>
|
||||
</div>
|
||||
@else
|
||||
<div style="padding:12px;text-align:center;font-size:.78rem;color:rgba(255,255,255,.3)">
|
||||
<a href="{{ route('frontend.login') }}" style="color:#ff2d7d">Giriş yap</a> — sohbete katıl
|
||||
</div>
|
||||
@endauth
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
@push('scripts')
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||
<script>
|
||||
const WP_CSRF = document.querySelector('meta[name="csrf-token"]')?.content ?? '';
|
||||
const WP_ROOM = '{{ $party->room_code }}';
|
||||
const WP_IS_HOST = {{ auth()->check() && auth()->id() === $party->host_user_id ? 'true' : 'false' }};
|
||||
const WP_ME_NAME = {{ auth()->check() ? json_encode(auth()->user()->name) : 'null' }};
|
||||
const WP_IS_AUTH = {{ auth()->check() ? 'true' : 'false' }};
|
||||
|
||||
// HLS init
|
||||
const vd = document.getElementById('wp-vd');
|
||||
if (vd) {
|
||||
const src = vd.dataset.src;
|
||||
if (src && Hls.isSupported()) {
|
||||
const hls = new Hls();
|
||||
hls.loadSource(src);
|
||||
hls.attachMedia(vd);
|
||||
} else if (src && vd.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
vd.src = src;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Chat (basit localStorage tabanlı, polling) ────────────────
|
||||
// Gerçek WebSocket olmadığı için sync sırasında chat mesajlarını da taşı
|
||||
let chatMessages = [];
|
||||
let lastChatTs = 0;
|
||||
|
||||
function addChatMsg(msg, isMine) {
|
||||
chatMessages.push({ ...msg, mine: isMine });
|
||||
renderChat();
|
||||
}
|
||||
|
||||
function renderChat() {
|
||||
const el = document.getElementById('wp-chat');
|
||||
if (!el) return;
|
||||
el.innerHTML = chatMessages.map(m => `
|
||||
<div class="wc-msg ${m.mine ? 'wc-mine' : ''}">
|
||||
<div class="wc-meta">${escHtml(m.name || '?')} · ${m.time || ''}</div>
|
||||
<div class="wc-bubble">${escHtml(m.body)}</div>
|
||||
</div>`).join('');
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function sendChatMsg() {
|
||||
const inp = document.getElementById('wp-chat-inp');
|
||||
const body = inp?.value.trim();
|
||||
if (!body) return;
|
||||
inp.value = '';
|
||||
const now = new Date().toLocaleTimeString('tr-TR', { hour:'2-digit', minute:'2-digit' });
|
||||
addChatMsg({ name: WP_ME_NAME, body, time: now }, true);
|
||||
// Broadcast via sync payload (next poll)
|
||||
pendingChatMsg = { name: WP_ME_NAME, body, time: now };
|
||||
}
|
||||
|
||||
let pendingChatMsg = null;
|
||||
|
||||
// ── Party Sync ─────────────────────────────────────────────────
|
||||
function renderMembers(members) {
|
||||
const el = document.getElementById('wp-members');
|
||||
const cnt = document.getElementById('wp-member-count');
|
||||
if (el) el.innerHTML = members.map(m => `
|
||||
<div class="wp-member ${m.is_host ? 'host' : ''}">
|
||||
${m.is_host ? '<i class="bi bi-star-fill" style="font-size:.6rem;color:#ffd700"></i>' : ''}
|
||||
${escHtml(m.name || '?')}
|
||||
</div>`).join('');
|
||||
if (cnt) cnt.textContent = members.length + ' üye aktif';
|
||||
}
|
||||
|
||||
async function doSync() {
|
||||
const body = WP_IS_HOST
|
||||
? { current_sec: Math.floor(vd?.currentTime ?? 0), is_playing: !(vd?.paused ?? true) }
|
||||
: {};
|
||||
|
||||
try {
|
||||
const r = await fetch(`/party/${WP_ROOM}/sync`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': WP_CSRF, 'Accept': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const d = await r.json();
|
||||
renderMembers(d.members || []);
|
||||
|
||||
// Non-host senkron
|
||||
if (!WP_IS_HOST && vd) {
|
||||
const diff = Math.abs(vd.currentTime - d.current_sec);
|
||||
if (diff > 2) vd.currentTime = d.current_sec;
|
||||
if (d.is_playing && vd.paused) vd.play().catch(()=>{});
|
||||
if (!d.is_playing && !vd.paused) vd.pause();
|
||||
}
|
||||
|
||||
document.getElementById('wp-sync-status').textContent = '✅ senkron ' + new Date().toLocaleTimeString('tr-TR',{hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
||||
pendingChatMsg = null;
|
||||
} catch(e) {
|
||||
document.getElementById('wp-sync-status').textContent = '❌ bağlantı kesildi';
|
||||
}
|
||||
}
|
||||
|
||||
function hostPlay() { if (vd) { vd.play().catch(()=>{}); } }
|
||||
function hostPause() { if (vd) vd.pause(); }
|
||||
|
||||
function copyCode() {
|
||||
navigator.clipboard?.writeText(WP_ROOM).then(() => {
|
||||
if (window.showToast) showToast('Oda kodu kopyalandı: ' + WP_ROOM, 'success');
|
||||
else alert('Oda kodu kopyalandı: ' + WP_ROOM);
|
||||
});
|
||||
}
|
||||
|
||||
// İlk join
|
||||
if (WP_IS_AUTH) {
|
||||
fetch(`/party/${WP_ROOM}/join`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': WP_CSRF, 'Accept': 'application/json' },
|
||||
body: JSON.stringify({}),
|
||||
}).then(r => r.json()).then(d => {
|
||||
if (d.ok) {
|
||||
renderMembers(d.members || []);
|
||||
if (!WP_IS_HOST && vd && d.current_sec > 0) {
|
||||
vd.currentTime = d.current_sec;
|
||||
}
|
||||
}
|
||||
}).catch(()=>{});
|
||||
}
|
||||
|
||||
setInterval(doSync, 3000);
|
||||
|
||||
// Ayrılma temizliği
|
||||
window.addEventListener('beforeunload', () => {
|
||||
navigator.sendBeacon(`/party/${WP_ROOM}/leave`, new Blob([JSON.stringify({})], { type: 'application/json' }));
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
||||
|
||||
{{-- Statik sayfalar --}}
|
||||
@foreach($staticPages as $page)
|
||||
<url>
|
||||
<loc>{{ $domain }}{{ $page['loc'] }}</loc>
|
||||
<priority>{{ $page['priority'] }}</priority>
|
||||
<changefreq>{{ $page['changefreq'] }}</changefreq>
|
||||
<lastmod>{{ now()->toAtomString() }}</lastmod>
|
||||
</url>
|
||||
@endforeach
|
||||
|
||||
{{-- Türler --}}
|
||||
@foreach($genres as $genre)
|
||||
<url>
|
||||
<loc>{{ $domain }}/genre/{{ $genre->slug }}</loc>
|
||||
<priority>0.7</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
@if($genre->updated_at)
|
||||
<lastmod>{{ $genre->updated_at->toAtomString() }}</lastmod>
|
||||
@endif
|
||||
</url>
|
||||
@endforeach
|
||||
|
||||
{{-- Animeler --}}
|
||||
@foreach($animes as $anime)
|
||||
<url>
|
||||
<loc>{{ $domain }}/anime/{{ $anime->slug }}</loc>
|
||||
<priority>{{ $anime->rating >= 8 ? '0.9' : ($anime->rating >= 6 ? '0.8' : '0.6') }}</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
@if($anime->updated_at)
|
||||
<lastmod>{{ $anime->updated_at->toAtomString() }}</lastmod>
|
||||
@endif
|
||||
@if($anime->cover_image_url)
|
||||
<image:image>
|
||||
<image:loc>{{ $anime->cover_image_url }}</image:loc>
|
||||
<image:title>{{ htmlspecialchars($anime->title) }}</image:title>
|
||||
</image:image>
|
||||
@endif
|
||||
</url>
|
||||
@endforeach
|
||||
|
||||
</urlset>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap>
|
||||
<loc>{{ $domain }}/sitemap-main.xml</loc>
|
||||
<lastmod>{{ now()->toAtomString() }}</lastmod>
|
||||
</sitemap>
|
||||
<sitemap>
|
||||
<loc>{{ $domain }}/sitemap-animes.xml</loc>
|
||||
<lastmod>{{ now()->toAtomString() }}</lastmod>
|
||||
</sitemap>
|
||||
<sitemap>
|
||||
<loc>{{ $domain }}/sitemap-videos.xml</loc>
|
||||
<lastmod>{{ now()->toAtomString() }}</lastmod>
|
||||
</sitemap>
|
||||
<sitemap>
|
||||
<loc>{{ $domain }}/sitemap-blog.xml</loc>
|
||||
<lastmod>{{ now()->toAtomString() }}</lastmod>
|
||||
</sitemap>
|
||||
</sitemapindex>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
|
||||
|
||||
@foreach($animes as $anime)
|
||||
<url>
|
||||
<loc>{{ $domain }}/anime/{{ $anime->slug }}</loc>
|
||||
<priority>{{ $anime->rating >= 8 ? '0.9' : ($anime->rating >= 6 ? '0.8' : '0.7') }}</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
<lastmod>{{ $anime->updated_at?->toAtomString() ?? now()->toAtomString() }}</lastmod>
|
||||
@if($anime->cover_image_url)
|
||||
<image:image>
|
||||
<image:loc>{{ $anime->cover_image_url }}</image:loc>
|
||||
<image:title>{{ htmlspecialchars($anime->title . ' İzle - Türkçe Altyazılı') }}</image:title>
|
||||
<image:caption>{{ htmlspecialchars($anime->title) }} anime Türkçe altyazılı ücretsiz izle — Animexe</image:caption>
|
||||
</image:image>
|
||||
@endif
|
||||
</url>
|
||||
@endforeach
|
||||
|
||||
</urlset>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"
|
||||
xmlns:news="http://www.google.com/schemas/sitemap-news/0.9">
|
||||
|
||||
@foreach($posts as $post)
|
||||
<url>
|
||||
<loc>{{ $domain }}/blog/{{ $post->slug }}</loc>
|
||||
<priority>0.75</priority>
|
||||
<changefreq>monthly</changefreq>
|
||||
<lastmod>{{ $post->updated_at?->toAtomString() ?? now()->toAtomString() }}</lastmod>
|
||||
@if($post->cover_image)
|
||||
<image:image>
|
||||
<image:loc>{{ $post->cover_image }}</image:loc>
|
||||
<image:title>{{ htmlspecialchars($post->title) }}</image:title>
|
||||
</image:image>
|
||||
@endif
|
||||
@if($post->published_at && $post->published_at->gt(now()->subDays(2)))
|
||||
<news:news>
|
||||
<news:publication>
|
||||
<news:name>Animexe Blog</news:name>
|
||||
<news:language>tr</news:language>
|
||||
</news:publication>
|
||||
<news:publication_date>{{ $post->published_at->toAtomString() }}</news:publication_date>
|
||||
<news:title>{{ htmlspecialchars($post->title) }}</news:title>
|
||||
@if($post->excerpt)<news:keywords>{{ htmlspecialchars(\Illuminate\Support\Str::limit($post->excerpt, 200)) }}</news:keywords>@endif
|
||||
</news:news>
|
||||
@endif
|
||||
</url>
|
||||
@endforeach
|
||||
|
||||
</urlset>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
|
||||
@foreach($staticPages as $page)
|
||||
<url>
|
||||
<loc>{{ $domain }}{{ $page['loc'] }}</loc>
|
||||
<priority>{{ $page['priority'] }}</priority>
|
||||
<changefreq>{{ $page['changefreq'] }}</changefreq>
|
||||
<lastmod>{{ now()->toAtomString() }}</lastmod>
|
||||
</url>
|
||||
@endforeach
|
||||
|
||||
@foreach($genres as $genre)
|
||||
<url>
|
||||
<loc>{{ $domain }}/genre/{{ $genre->slug }}</loc>
|
||||
<priority>0.7</priority>
|
||||
<changefreq>weekly</changefreq>
|
||||
<lastmod>{{ $genre->updated_at?->toAtomString() ?? now()->toAtomString() }}</lastmod>
|
||||
</url>
|
||||
@endforeach
|
||||
|
||||
</urlset>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
|
||||
|
||||
@foreach($animes as $anime)
|
||||
<url>
|
||||
<loc>{{ $domain }}/anime/{{ $anime->slug }}</loc>
|
||||
<video:video>
|
||||
<video:thumbnail_loc>{{ $anime->cover_image_url ?? $domain . '/logo.jpg' }}</video:thumbnail_loc>
|
||||
<video:title>{{ htmlspecialchars($anime->title . ' İzle - Türkçe Altyazılı') }}</video:title>
|
||||
<video:description>{{ htmlspecialchars(\Illuminate\Support\Str::limit(strip_tags($anime->description ?? $anime->title . ' Türkçe altyazılı ücretsiz izle — Animexe'), 200)) }}</video:description>
|
||||
<video:player_loc>{{ $anime->first_ep_watch_url }}</video:player_loc>
|
||||
@foreach($anime->episodes->take(3) as $ep)
|
||||
@php $sNum = optional($ep->season)->season_number ?? 1; @endphp
|
||||
<video:tag>{{ htmlspecialchars($anime->title . ' ' . $ep->episode_number . '. Bölüm İzle') }}</video:tag>
|
||||
@endforeach
|
||||
<video:tag>anime izle</video:tag>
|
||||
<video:tag>türkçe anime</video:tag>
|
||||
<video:tag>{{ htmlspecialchars($anime->title) }} izle</video:tag>
|
||||
<video:family_friendly>yes</video:family_friendly>
|
||||
<video:requires_subscription>no</video:requires_subscription>
|
||||
<video:live>no</video:live>
|
||||
</video:video>
|
||||
</url>
|
||||
@endforeach
|
||||
|
||||
</urlset>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user