578 lines
22 KiB
Vue
578 lines
22 KiB
Vue
<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 Açı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> |