254 lines
12 KiB
Vue
254 lines
12 KiB
Vue
<template>
|
||
<div>
|
||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||
<h2>Yeni Gönderi Ekle</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 class="card">
|
||
<div class="card-body">
|
||
<form @submit.prevent="savePost">
|
||
<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-if="categories.length === 0" class="text-muted small">Kategori bulunamadı.</div>
|
||
<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">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">
|
||
</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>
|
||
|
||
<!-- Publish Button -->
|
||
<div class="d-grid gap-2">
|
||
<button type="submit" class="btn btn-primary" :disabled="loading">
|
||
<span v-if="loading" class="spinner-border spinner-border-sm me-2"></span>
|
||
{{ loading ? 'İşleniyor...' : 'Yayınla' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { Category } from '~~/types/category';
|
||
import type { Tag, TagResponse } from '~~/types/tag';
|
||
import Swal from 'sweetalert2';
|
||
|
||
definePageMeta({
|
||
layout: 'admin',
|
||
middleware: 'admin'
|
||
});
|
||
|
||
const config = useRuntimeConfig();
|
||
const { data: authData } = useAuth();
|
||
const router = useRouter();
|
||
|
||
const loading = 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'
|
||
});
|
||
|
||
// Fetch Categories and Tags
|
||
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 (admin endpoint for safety/completeness)
|
||
baseURL: config.public.BASE_API_URL,
|
||
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` },
|
||
params: { limit: 100 } // Get a reasonable amount of tags
|
||
})
|
||
]);
|
||
|
||
categories.value = catRes || [];
|
||
availableTags.value = tagRes?.data || [];
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
};
|
||
|
||
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 savePost = async () => {
|
||
loading.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 Image and append to FormData
|
||
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);
|
||
|
||
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', {
|
||
method: 'POST',
|
||
baseURL: config.public.BASE_API_URL,
|
||
headers: { Authorization: `Bearer ${(authData.value as any)?.accessToken}` },
|
||
body: data
|
||
});
|
||
|
||
Swal.fire('Başarılı', 'Gönderi başarıyla oluşturuldu.', 'success');
|
||
router.push('/admin/blog/posts');
|
||
|
||
} catch (error: any) {
|
||
console.error(error);
|
||
Swal.fire('Hata', error.data?.message || 'Bir hata oluştu.', 'error');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
};
|
||
|
||
onMounted(() => {
|
||
fetchData();
|
||
});
|
||
</script>
|