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