first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:07:47 +03:00
commit 5285a0dd86
522 changed files with 41738 additions and 0 deletions

View File

@@ -0,0 +1,578 @@
<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>