Files
Beyhan Oğur 5285a0dd86 first commit
2026-04-26 22:07:47 +03:00

253 lines
11 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Gönderi Yönetimi</h2>
<NuxtLink to="/admin/blog/posts/create" class="btn btn-primary">
<i class="fas fa-plus me-2"></i> Yeni Gönderi
</NuxtLink>
</div>
<!-- Filter / Search -->
<div class="card mb-4">
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<select v-model="filterStatus" class="form-select" @change="fetchPosts(1)">
<option value="">Tümü</option>
<option value="only">Silinenler (Çöp Kutusu)</option>
<option value="with">Tümü (Silinenler Dahil)</option>
</select>
</div>
<!-- Future: Add search input here -->
</div>
</div>
</div>
<!-- Posts List -->
<div class="card mb-4">
<div class="card-body">
<div v-if="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Yükleniyor...</span>
</div>
</div>
<div v-else-if="posts.length === 0" class="text-center py-5 text-muted">
Kayıt bulunamadı.
</div>
<div v-else class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>ID</th>
<th>Resim</th>
<th>Başlık</th>
<th>Kategoriler</th>
<th>Etiketler</th>
<th>Durum</th>
<th>Tarih</th>
<th class="text-end">İşlemler</th>
</tr>
</thead>
<tbody>
<tr v-for="post in posts" :key="post.ID">
<td>{{ post.ID }}</td>
<td>
<img v-if="getFirstImage(post.images)" :src="getImageUrl(getFirstImage(post.images)!)"
alt="thumbnail" class="rounded" style="width: 50px; height: 50px; object-fit: cover;">
<div v-else class="bg-light rounded d-flex align-items-center justify-content-center text-muted"
style="width: 50px; height: 50px;">
<i class="fas fa-image"></i>
</div>
</td>
<td>
<div class="fw-bold">{{ post.title }}</div>
<small class="text-muted">{{ post.slug }}</small>
</td>
<td>
<span v-for="cat in post.categories" :key="cat.ID" class="badge bg-info text-dark me-1">
{{ cat.title }}
</span>
</td>
<td>
<span v-for="tag in post.tags" :key="tag.ID" class="badge bg-secondary me-1">
{{ tag.name }}
</span>
</td>
<td>
<span v-if="post.DeletedAt" class="badge bg-danger">Silindi</span>
<span v-else class="badge bg-success">Aktif</span>
</td>
<td>
<div>{{ formatDate(post.CreatedAt) }}</div>
</td>
<td class="text-end">
<div v-if="!post.DeletedAt">
<NuxtLink :to="`/admin/blog/posts/${post.ID}`" class="btn btn-sm btn-outline-primary me-2">
<i class="fas fa-edit"></i>
</NuxtLink>
<button class="btn btn-sm btn-outline-danger" @click="deletePost(post.ID)">
<i class="fas fa-trash"></i>
</button>
</div>
<div v-else>
<button class="btn btn-sm btn-outline-success" @click="restorePost(post.ID)">
<i class="fas fa-trash-restore"></i> Geri Yükle
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<nav v-if="pagination.total > pagination.per_page" class="mt-4">
<ul class="pagination justify-content-center">
<li class="page-item" :class="{ disabled: pagination.page === 1 }">
<button class="page-link" @click="fetchPosts(pagination.page - 1)">Önceki</button>
</li>
<li v-for="page in Math.ceil(pagination.total / pagination.per_page)" :key="page"
class="page-item" :class="{ active: pagination.page === page }">
<button class="page-link" @click="fetchPosts(page)">{{ page }}</button>
</li>
<li class="page-item" :class="{ disabled: pagination.page >= Math.ceil(pagination.total / pagination.per_page) }">
<button class="page-link" @click="fetchPosts(pagination.page + 1)">Sonraki</button>
</li>
</ul>
</nav>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Post, PostResponse } from '~~/types/post';
import Swal from 'sweetalert2';
definePageMeta({
layout: 'admin',
middleware: 'admin'
});
const config = useRuntimeConfig();
const { data: authData } = useAuth();
const posts = ref<Post[]>([]);
const loading = ref(false);
const filterStatus = ref(''); // '' = active (default from API perspective usually?), or we need to handle default
// Default listing in doc: /api/v1/admin/posts
// Trashed only: ?trashed=only
// With trashed: ?trashed=with
const pagination = ref({
page: 1,
per_page: 10,
total: 0
});
const fetchPosts = async (page = 1) => {
loading.value = true;
try {
const query: any = { page, limit: pagination.value.per_page };
if (filterStatus.value) {
query.trashed = filterStatus.value;
}
const res = await $fetch<PostResponse>('/api/v1/admin/posts', {
baseURL: config.public.BASE_API_URL,
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` },
query
});
posts.value = res.data || [];
pagination.value = res.meta;
} catch (e) {
console.error(e);
posts.value = [];
} finally {
loading.value = false;
}
};
const deletePost = async (id: number) => {
const result = await Swal.fire({
title: 'Emin misiniz?',
text: "Bu gönderi silinecek (Çöp kutusuna taşınacak).",
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Evet, Sil',
cancelButtonText: 'İptal',
confirmButtonColor: '#d33'
});
if (!result.isConfirmed) return;
try {
await $fetch(`/api/v1/posts/${id}`, {
method: 'DELETE',
baseURL: config.public.BASE_API_URL,
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` }
});
Swal.fire('Silindi', 'Gönderi silindi.', 'success');
fetchPosts(pagination.value.page);
} catch (error) {
console.error(error);
Swal.fire('Hata', 'Silme işlemi başarısız.', 'error');
}
};
const restorePost = async (id: number) => {
// Implement restore logic if API supports it, otherwise basic delete is usually soft delete.
// If current delete IS soft delete, usually there's a restore endpoint or update with deleted_at=null.
// Assuming for now we just show it for concept, but strict restore endpoint might be needed.
// For now, let's keep it simple. If no explicit restore endpoint documented, maybe we can't restore easily from UI yet.
// But wait, the user asked to implement based on admin_post.md.
// Use case "sadece soft delete olmuslar listesi" implies we can see them.
// Usually a PUT to /api/v1/admin/posts/{id}/restore or similar.
// Since not documented, I will skip implementation of ACTUAL restore call but keep the button or maybe comment it out
// until confirmed. Or I can assume standard behaviour if I knew the backend.
// Let's assume standard soft delete REST pattern often allows toggle or specific endpoint.
// I'll leave the button but functionality might be missing.
// Actually, let's look at the docs again.
// Docs only show GET lists.
// I will remove Restore button functionality for now to avoid errors, or just show alert 'Not implemented'.
Swal.fire('Bilgi', 'Geri yükleme özelliği henüz API tarafında dökümante edilmedi.', 'info');
};
const getFirstImage = (imagesStr: string): string | null => {
try {
// API returns "images": "uploads/posts/..." OR JSON string "[\"...\"]"
// Let's try to parse if it looks like JSON
if (imagesStr.startsWith('[') || imagesStr.startsWith('{')) {
const parsed = JSON.parse(imagesStr);
if (Array.isArray(parsed) && parsed.length > 0) return parsed[0];
}
return imagesStr;
} catch (e) {
return imagesStr;
}
};
const getImageUrl = (path: string) => {
if (path.startsWith('http')) return path;
if (path.startsWith('/')) return `${config.public.BASE_API_URL}${path}`;
return `${config.public.BASE_API_URL}/${path}`;
};
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('tr-TR', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
onMounted(() => {
fetchPosts();
});
</script>