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

578 lines
22 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>Ayarlar</h2>
</div>
<ul class="nav nav-tabs mb-4" id="settingsTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="general-tab" data-bs-toggle="tab" data-bs-target="#general" type="button"
role="tab" aria-controls="general" aria-selected="true">Genel Ayarlar</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="hero-tab" data-bs-toggle="tab" data-bs-target="#hero" type="button" role="tab"
aria-controls="hero" aria-selected="false">Hero (Banner) Ayarları</button>
</li>
</ul>
<div class="tab-content" id="settingsTabContent">
<!-- GENERAL SETTINGS TAB -->
<div class="tab-pane fade show active" id="general" role="tabpanel" aria-labelledby="general-tab">
<div class="card">
<div class="card-body">
<form @submit.prevent="updateSettings">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Site Başlığı</label>
<input v-model="settingForm.title" type="text" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Slogan</label>
<input v-model="settingForm.slogan" type="text" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Telefon</label>
<input v-model="settingForm.phone" type="text" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Email</label>
<input v-model="settingForm.email" type="email" class="form-control">
</div>
<div class="col-md-12 mb-3">
<label class="form-label">Adres</label>
<textarea v-model="settingForm.address" class="form-control" rows="2"></textarea>
</div>
<div class="col-md-12 mb-3">
<label class="form-label">Harita Embed Code</label>
<textarea v-model="settingForm.map_embed" class="form-control" rows="3"></textarea>
</div>
<div class="col-md-12 mb-3">
<label class="form-label">Copyright Metni</label>
<input v-model="settingForm.copyright" type="text" class="form-control">
</div>
</div>
<h5 class="mt-4">SEO Ayarları</h5>
<hr>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Meta Başlık</label>
<input v-model="settingForm.meta_title" type="text" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Meta ıklama</label>
<input v-model="settingForm.meta_description" type="text" class="form-control">
</div>
</div>
<h5 class="mt-4">Sosyal Medya</h5>
<hr>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">Facebook</label>
<input v-model="settingForm.facebook" type="text" class="form-control">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Twitter / X</label>
<input v-model="settingForm.x" type="text" class="form-control">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Instagram</label>
<input v-model="settingForm.instagram" type="text" class="form-control">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Linkedin</label>
<input v-model="settingForm.linkedin" type="text" class="form-control">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Pinterest</label>
<input v-model="settingForm.pinterest" type="text" class="form-control">
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Whatsapp</label>
<input v-model="settingForm.whatsapp" type="text" class="form-control">
</div>
</div>
<h5 class="mt-4">Logolar</h5>
<hr>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Beyaz Logo (Dark Mod için)</label>
<input @change="handleFileUpload($event, 'w_logo')" type="file" class="form-control">
<div v-if="settingForm.w_logo && typeof settingForm.w_logo === 'string'" class="mt-2">
<img :src="config.public.BASE_API_URL + settingForm.w_logo" height="50" class="bg-dark p-1">
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Siyah Logo (Light Mod için)</label>
<input @change="handleFileUpload($event, 'b_logo')" type="file" class="form-control">
<div v-if="settingForm.b_logo && typeof settingForm.b_logo === 'string'" class="mt-2">
<img :src="config.public.BASE_API_URL + settingForm.b_logo" height="50" class="bg-light border p-1">
</div>
</div>
</div>
<h5 class="mt-4">Resim Optimizasyon Ayarları (Logo İçin)</h5>
<hr>
<div class="row bg-light p-3 rounded mx-1">
<div class="col-md-3 mb-2">
<label class="form-label">Format</label>
<select v-model="imageSettings.format" class="form-select">
<option value="avif">AVIF</option>
<option value="webp">WEBP</option>
<option value="png">PNG</option>
<option value="jpg">JPG</option>
</select>
</div>
<div class="col-md-3 mb-2">
<label class="form-label">Kalite (1-100)</label>
<input v-model="imageSettings.quality" type="number" min="1" max="100" class="form-control">
</div>
<div class="col-md-3 mb-2">
<label class="form-label">Genişlik (px)</label>
<input v-model="imageSettings.width" type="number" class="form-control" placeholder="Opsiyonel">
</div>
<div class="col-md-3 mb-2">
<label class="form-label">Yükseklik (px)</label>
<input v-model="imageSettings.height" type="number" class="form-control" placeholder="Opsiyonel">
</div>
</div>
<div class="mt-4 text-end">
<button type="submit" class="btn btn-primary" :disabled="loading">
<span v-if="loading" class="spinner-border spinner-border-sm me-2"></span>
Kaydet
</button>
</div>
</form>
</div>
</div>
</div>
<!-- HERO SETTINGS TAB -->
<div class="tab-pane fade" id="hero" role="tabpanel" aria-labelledby="hero-tab">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center bg-white">
<h5 class="mb-0">Hero Listesi</h5>
<button class="btn btn-success btn-sm" @click="openHeroModal()"><i class="fas fa-plus me-2"></i> Yeni
Ekle</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th>Resim</th>
<th>Başlık</th>
<th>Alt Başlıklar</th>
<th>Durum</th>
<th class="text-end" width="100">İşlemler</th>
</tr>
</thead>
<tbody>
<tr v-for="hero in heroes" :key="hero.ID">
<td>
<img v-if="hero.image" :src="config.public.BASE_API_URL + hero.image"
style="max-width: 60px; height: 40px; object-fit: cover;" class="rounded border">
<span v-else>-</span>
</td>
<td>{{ hero.title }}</td>
<td>
<small class="d-block text-muted">{{ hero.text1 }}</small>
<small class="d-block text-muted">{{ hero.text2 }}</small>
</td>
<td>
<span class="badge" :class="hero.is_active ? 'bg-success' : 'bg-secondary'">
{{ hero.is_active ? 'Aktif' : 'Pasif' }}
</span>
</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary" @click="openHeroModal(hero)" title="Düzenle">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-danger" @click="deleteHero(hero.ID)" title="Sil">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
<tr v-if="heroes.length === 0">
<td colspan="5" class="text-center py-4">Kayıt bulunamadı.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- HERO MODAL -->
<div class="modal fade" id="heroModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ isEditingHero ? 'Hero Düzenle' : 'Yeni Hero Ekle' }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form @submit.prevent="saveHero">
<div class="row">
<div class="col-md-12 mb-3">
<label class="form-label">Başlık</label>
<input v-model="heroForm.title" type="text" class="form-control" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Text 1</label>
<input v-model="heroForm.text1" type="text" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Text 2</label>
<input v-model="heroForm.text2" type="text" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Text 4</label>
<input v-model="heroForm.text4" type="text" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Text 5</label>
<input v-model="heroForm.text5" type="text" class="form-control">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Renk</label>
<div class="input-group">
<input v-model="heroForm.color" type="color" class="form-control form-control-color"
title="Renk Seçin">
<input v-model="heroForm.color" type="text" class="form-control" placeholder="#ffffff">
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Durum</label>
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" v-model="heroForm.is_active" id="heroActive">
<label class="form-check-label" for="heroActive">Aktif</label>
</div>
</div>
<div class="col-md-12 mb-3">
<label class="form-label">Görsel</label>
<input @change="handleHeroFileUpload($event)" type="file" class="form-control">
<div v-if="heroForm.image && typeof heroForm.image === 'string'" class="mt-2">
<small>Mevcut Görsel:</small>
<img :src="config.public.BASE_API_URL + heroForm.image" height="80" class="d-block mt-1 rounded">
</div>
</div>
<div class="col-md-12 mb-3">
<div class="p-3 bg-light rounded border">
<h6>Resim Optimizasyon Ayarları</h6>
<div class="row">
<div class="col-md-3 mb-2">
<label class="form-label">Format</label>
<select v-model="imageSettings.format" class="form-select">
<option value="avif">AVIF</option>
<option value="webp">WEBP</option>
<option value="png">PNG</option>
<option value="jpg">JPG</option>
</select>
</div>
<div class="col-md-3 mb-2">
<label class="form-label">Kalite</label>
<input v-model="imageSettings.quality" type="number" min="1" max="100" class="form-control">
</div>
<div class="col-md-3 mb-2">
<label class="form-label">Genişlik</label>
<input v-model="imageSettings.width" type="number" class="form-control" placeholder="Opsiyonel">
</div>
<div class="col-md-3 mb-2">
<label class="form-label">Yükseklik</label>
<input v-model="imageSettings.height" type="number" class="form-control"
placeholder="Opsiyonel">
</div>
</div>
</div>
</div>
</div>
<div class="text-end">
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">İptal</button>
<button type="submit" class="btn btn-primary" :disabled="loading">
<span v-if="loading" class="spinner-border spinner-border-sm me-2"></span>
Kaydet
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { Setting } from '~~/types/setting';
import type { Hero } from '~~/types/hero';
import Swal from 'sweetalert2';
definePageMeta({
layout: 'admin',
middleware: 'admin'
});
const config = useRuntimeConfig();
const { data: authData } = useAuth();
const loading = ref(false);
// --- SETTINGS ---
const settingForm = ref<Partial<Setting>>({});
const w_logo_file = ref<File | null>(null);
const b_logo_file = ref<File | null>(null);
const imageSettings = ref({
format: 'avif',
quality: 80,
width: null as number | null,
height: null as number | null
});
// Fetch Settings
const fetchSettings = async () => {
try {
const data = await $fetch<Setting>('/api/v1/setting', {
baseURL: config.public.BASE_API_URL,
});
if (data) {
settingForm.value = { ...data };
}
} catch (error) {
console.error('Settings fetch error:', error);
}
};
const handleFileUpload = (event: Event, type: 'w_logo' | 'b_logo') => {
const target = event.target as HTMLInputElement;
if (target.files && target.files[0]) {
if (type === 'w_logo') w_logo_file.value = target.files[0];
if (type === 'b_logo') b_logo_file.value = target.files[0];
}
};
const optimizeImage = async (file: File): Promise<File> => {
const formData = new FormData();
formData.append('file', file);
formData.append('format', imageSettings.value.format);
formData.append('quality', String(imageSettings.value.quality));
if (imageSettings.value.width) formData.append('width', String(imageSettings.value.width));
if (imageSettings.value.height) formData.append('height', String(imageSettings.value.height));
const optimizedBlob = await $fetch<Blob>('/api/optimize', {
method: 'POST',
body: formData,
responseType: 'blob'
});
const ext = imageSettings.value.format === 'jpg' ? 'jpeg' : imageSettings.value.format;
const filename = `img-${Date.now()}-${Math.floor(Math.random() * 10000)}.${ext === 'jpeg' ? 'jpg' : ext}`;
const optimizedFile = new File([optimizedBlob], filename, {
type: `image/${ext}`
});
return optimizedFile;
};
const updateSettings = async () => {
loading.value = true;
try {
const formData = new FormData();
Object.keys(settingForm.value).forEach(key => {
const value = (settingForm.value as any)[key];
if (value !== null && value !== undefined && key !== 'w_logo' && key !== 'b_logo' && key !== 'ID' && key !== 'CreatedAt' && key !== 'UpdatedAt' && key !== 'DeletedAt') {
formData.append(key, String(value));
}
});
if (w_logo_file.value) {
const optimized = await optimizeImage(w_logo_file.value);
formData.append('w_logo', optimized);
}
if (b_logo_file.value) {
const optimized = await optimizeImage(b_logo_file.value);
formData.append('b_logo', optimized);
}
formData.append('is_active', 'true');
let url = '/api/v1/setting';
let method: 'POST' | 'PUT' = 'POST';
if (settingForm.value.ID) {
url = `/api/v1/setting/${settingForm.value.ID}`;
method = 'PUT';
}
await $fetch(url, {
method: method,
baseURL: config.public.BASE_API_URL,
body: formData,
headers: {
Authorization: `Bearer ${(authData.value as any)?.accessToken}`
}
});
Swal.fire('Başarılı', 'Ayarlar güncellendi.', 'success');
fetchSettings(); // Refresh
} catch (error) {
console.error(error);
Swal.fire('Hata', 'Güncelleme sırasında hata oluştu.', 'error');
} finally {
loading.value = false;
}
};
// --- HERO ---
const heroes = ref<Hero[]>([]);
const heroForm = ref<Partial<Hero>>({});
const heroImageFile = ref<File | null>(null);
const isEditingHero = ref(false);
let heroModal: any = null;
const fetchHeroes = async () => {
try {
const response = await $fetch<any>('/api/v1/heroes', {
baseURL: config.public.BASE_API_URL,
});
if (response && Array.isArray(response.data)) {
heroes.value = response.data;
} else if (Array.isArray(response)) {
heroes.value = response;
} else if (response && typeof response === 'object') {
heroes.value = [response]; // Wrap single object
} else {
heroes.value = [];
}
} catch (error) {
console.error('Hero fetch error:', error);
}
};
const openHeroModal = (hero: Hero | null = null) => {
if (hero) {
isEditingHero.value = true;
heroForm.value = { ...hero };
} else {
isEditingHero.value = false;
heroForm.value = { is_active: true };
}
heroImageFile.value = null;
// Initialize Modal
const modalEl = document.getElementById('heroModal');
if (modalEl) {
// @ts-ignore
heroModal = new bootstrap.Modal(modalEl);
heroModal.show();
}
};
const handleHeroFileUpload = (event: Event) => {
const target = event.target as HTMLInputElement;
if (target.files && target.files[0]) {
heroImageFile.value = target.files[0];
}
};
const saveHero = async () => {
loading.value = true;
try {
const formData = new FormData();
Object.keys(heroForm.value).forEach(key => {
const value = (heroForm.value as any)[key];
if (value !== null && value !== undefined && key !== 'image' && key !== 'ID' && key !== 'CreatedAt' && key !== 'UpdatedAt' && key !== 'DeletedAt') {
formData.append(key, String(value));
}
});
if (heroImageFile.value) {
const optimized = await optimizeImage(heroImageFile.value);
formData.append('image', optimized);
}
// Ensure is_active sent as string 'true'/'false' if backend expects it
// formData.set('is_active', String(heroForm.value.is_active));
let url = '/api/v1/hero';
let method: 'POST' | 'PUT' = 'POST';
if (isEditingHero.value && heroForm.value.ID) {
url = `/api/v1/hero/${heroForm.value.ID}`;
method = 'PUT';
}
await $fetch(url, {
method: method,
baseURL: config.public.BASE_API_URL,
body: formData,
headers: {
Authorization: `Bearer ${(authData.value as any)?.accessToken}`
}
});
if (heroModal) heroModal.hide();
Swal.fire('Başarılı', `Hero ${isEditingHero.value ? 'güncellendi' : 'eklendi'}.`, 'success');
fetchHeroes();
} catch (error) {
console.error(error);
Swal.fire('Hata', 'İşlem başarısız.', 'error');
} finally {
loading.value = false;
}
};
const deleteHero = async (id: number) => {
const result = await Swal.fire({
title: 'Emin misiniz?',
text: "Bu hero kaydını silmek istediğinize emin misiniz?",
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Evet, Sil',
cancelButtonText: 'İptal'
});
if (result.isConfirmed) {
try {
await $fetch(`/api/v1/hero/${id}`, {
method: 'DELETE',
baseURL: config.public.BASE_API_URL,
headers: {
Authorization: `Bearer ${(authData.value as any)?.accessToken}`
}
});
Swal.fire('Silindi!', 'Kayıt başarıyla silindi.', 'success');
fetchHeroes();
} catch (error) {
console.error(error);
Swal.fire('Hata', 'Silme işlemi başarısız.', 'error');
}
}
};
onMounted(() => {
fetchSettings();
fetchHeroes();
});
</script>
<style scoped>
/* Tabs styling */
.nav-tabs .nav-link {
color: #495057;
}
.nav-tabs .nav-link.active {
font-weight: bold;
color: #0d6efd;
}
</style>