Files
go_nuxt_admin/app/pages/admin/blog/posts/[id].vue
Beyhan Oğur 5285a0dd86 first commit
2026-04-26 22:07:47 +03:00

326 lines
14 KiB
Vue
Raw 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 Düzenle</h2>
<NuxtLink to="/admin/blog/posts" class="btn btn-secondary">
<i class="fas fa-arrow-left me-2"></i> Geri Dön
</NuxtLink>
</div>
<div v-if="loadingData" 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 class="card">
<div class="card-body">
<form @submit.prevent="updatePost">
<div class="row">
<div class="col-md-8">
<!-- Title -->
<div class="mb-3">
<label class="form-label">Başlık</label>
<input v-model="formData.title" type="text" class="form-control" required>
</div>
<!-- Content -->
<div class="mb-3">
<label class="form-label">İçerik</label>
<textarea v-model="formData.content" class="form-control" rows="10" required></textarea>
<div class="form-text">HTML içeriği girebilirsiniz.</div>
</div>
<!-- Tags -->
<div class="card mb-3">
<div class="card-header">Etiketler</div>
<div class="card-body" style="max-height: 200px; overflow-y: auto;">
<div v-if="availableTags.length === 0" class="text-muted small">Etiket bulunamadı.</div>
<div v-for="tag in availableTags" :key="tag.ID" class="form-check">
<input class="form-check-input" type="checkbox" :value="tag.ID" v-model="selectedTags" :id="`tag-${tag.ID}`">
<label class="form-check-label" :for="`tag-${tag.ID}`">
{{ tag.name }}
</label>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Categories -->
<div class="card mb-3">
<div class="card-header">Kategoriler</div>
<div class="card-body" style="max-height: 300px; overflow-y: auto;">
<div v-for="cat in categories" :key="cat.ID" class="form-check">
<input class="form-check-input" type="checkbox" :value="cat.ID" v-model="formData.category_ids" :id="`cat-${cat.ID}`">
<label class="form-check-label" :for="`cat-${cat.ID}`">
{{ cat.title }}
</label>
</div>
</div>
</div>
<!-- Image Upload & Options -->
<div class="card mb-3">
<div class="card-header">Öne Çıkan Görsel</div>
<div class="card-body">
<div class="mb-3 text-center">
<div v-if="imagePreview" class="mb-3">
<img :src="imagePreview" class="img-fluid rounded" style="max-height: 200px;">
<button type="button" class="btn btn-sm btn-danger mt-2" @click="removeImage">Değiştir / Kaldır</button>
</div>
<div v-else>
<div class="mb-3">
<i class="fas fa-image fa-3x text-muted"></i>
</div>
<input type="file" class="form-control" @change="handleFileUpload" accept="image/*">
</div>
</div>
<div v-if="imageFile">
<hr>
<h6>Resim Ayarları</h6>
<div class="row g-2">
<div class="col-6">
<label class="form-label small">Genişlik (px)</label>
<input v-model="imageOptions.width" type="number" class="form-control form-control-sm" placeholder="Opsiyonel">
</div>
<div class="col-6">
<label class="form-label small">Yükseklik (px)</label>
<input v-model="imageOptions.height" type="number" class="form-control form-control-sm" placeholder="Opsiyonel">
</div>
<div class="col-6">
<label class="form-label small">Kalite (1-100)</label>
<input v-model="imageOptions.quality" type="number" class="form-control form-control-sm" min="1" max="100">
</div>
<div class="col-6">
<label class="form-label small">Format</label>
<select v-model="imageOptions.format" class="form-select form-select-sm">
<option value="jpeg">JPEG</option>
<option value="png">PNG</option>
<option value="webp">WebP</option>
<option value="avif">AVIF</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Update Button -->
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary" :disabled="saving">
<span v-if="saving" class="spinner-border spinner-border-sm me-2"></span>
{{ saving ? 'Güncelleniyor...' : 'Güncelle' }}
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Category } from '~~/types/category';
import type { Post } from '~~/types/post';
import type { Tag, TagResponse } from '~~/types/tag';
import Swal from 'sweetalert2';
definePageMeta({
layout: 'admin',
middleware: 'admin'
});
const config = useRuntimeConfig();
const { data: authData } = useAuth();
const route = useRoute();
const router = useRouter();
const loadingData = ref(true);
const saving = ref(false);
const categories = ref<Category[]>([]);
const availableTags = ref<Tag[]>([]);
const selectedTags = ref<number[]>([]);
const imageFile = ref<File | null>(null);
const imagePreview = ref<string | null>(null);
const formData = ref({
title: '',
content: '',
category_ids: [] as number[],
});
const imageOptions = ref({
width: null as number | null,
height: null as number | null,
quality: 80,
format: 'jpeg'
});
const postId = route.params.id as string;
const fetchData = async () => {
try {
const [catRes, tagRes] = await Promise.all([
$fetch<Category[]>('/api/v1/categories', {
baseURL: config.public.BASE_API_URL,
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` }
}),
$fetch<TagResponse>('/api/v1/admin/tags', { // Fetching all tags
baseURL: config.public.BASE_API_URL,
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` },
params: { limit: 100 }
})
]);
categories.value = catRes || [];
availableTags.value = tagRes?.data || [];
// After fetching data, fetch post
await fetchPost();
} catch (e) {
console.error('Veriler yüklenemedi', e);
loadingData.value = false;
}
};
const fetchPost = async () => {
try {
const res = await $fetch<Post>(`/api/v1/posts/${postId}`, {
baseURL: config.public.BASE_API_URL,
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` }
});
if (res) {
formData.value.title = res.title;
formData.value.content = res.content;
formData.value.category_ids = res.categories?.map(c => c.ID) || [];
// Pre-select tags
if (res.tags) {
selectedTags.value = res.tags.map(t => t.ID);
}
// Image
if (res.images) {
// Should parse if JSON or string
let imgPath = res.images;
if (imgPath.startsWith('[') || imgPath.startsWith('{')) {
try {
const parsed = JSON.parse(imgPath);
if (Array.isArray(parsed) && parsed.length > 0) imgPath = parsed[0];
} catch(e) {}
}
imagePreview.value = getImageUrl(imgPath);
}
}
} catch (e) {
console.error('Post yüklenemedi', e);
Swal.fire('Hata', 'Gönderi bulunamadı veya yüklenemedi.', 'error');
router.push('/admin/blog/posts');
} finally {
loadingData.value = false;
}
};
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 handleFileUpload = (event: Event) => {
const target = event.target as HTMLInputElement;
if (target.files && target.files[0]) {
const file = target.files[0];
imageFile.value = file;
imagePreview.value = URL.createObjectURL(file);
// Default format based on uploaded file or keep jpeg
if (file.type === 'image/png') imageOptions.value.format = 'png';
else if (file.type === 'image/webp') imageOptions.value.format = 'webp';
else imageOptions.value.format = 'jpeg';
}
};
const removeImage = () => {
imageFile.value = null;
imagePreview.value = null;
};
const updatePost = async () => {
saving.value = true;
try {
const data = new FormData();
data.append('title', formData.value.title);
data.append('content', formData.value.content);
if (formData.value.category_ids.length > 0) {
data.append('category_ids', formData.value.category_ids.join(','));
}
if (selectedTags.value.length > 0) {
data.append('tag_ids', selectedTags.value.join(','));
}
// Optimize & Append Image if exists
if (imageFile.value) {
const optimizeData = new FormData();
optimizeData.append('file', imageFile.value);
if (imageOptions.value.width) optimizeData.append('width', imageOptions.value.width.toString());
if (imageOptions.value.height) optimizeData.append('height', imageOptions.value.height.toString());
optimizeData.append('quality', imageOptions.value.quality.toString());
optimizeData.append('format', imageOptions.value.format);
// Fetch as Blob from server/api/optimize
const optimizedBlob = await $fetch<Blob>('/api/optimize', {
method: 'POST',
body: optimizeData,
responseType: 'blob'
});
const ext = imageOptions.value.format === 'jpg' ? 'jpeg' : imageOptions.value.format;
const filename = `img-${Date.now()}.${ext === 'jpeg' ? 'jpg' : ext}`;
const optimizedFile = new File([optimizedBlob], filename, { type: `image/${ext}` });
data.append('images', optimizedFile);
}
await $fetch(`/api/v1/posts/${postId}`, {
method: 'PUT',
baseURL: config.public.BASE_API_URL,
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` },
body: data
});
Swal.fire({
title: 'Başarılı',
text: 'Gönderi güncellendi.',
icon: 'success',
showCancelButton: true,
confirmButtonText: 'Listeye Dön',
cancelButtonText: 'Kalsın'
}).then((result) => {
if (result.isConfirmed) {
router.push('/admin/blog/posts');
}
});
} catch (error: any) {
console.error(error);
Swal.fire('Hata', error.data?.message || 'Bir hata oluştu.', 'error');
} finally {
saving.value = false;
}
};
onMounted(() => {
fetchData();
});
</script>