442 lines
20 KiB
HTML
442 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="tr" data-bs-theme="light">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Admin Paneli</title>
|
||
<!-- Bootstrap CSS -->
|
||
<link href="/assets/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<!-- Bootstrap Icons -->
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||
<!-- Admin Theme CSS -->
|
||
<link href="/admin/css/theme.css" rel="stylesheet">
|
||
<!-- HTMX -->
|
||
<script src="/assets/htmx/htmx.js"></script>
|
||
<!-- jQuery -->
|
||
<script src="/assets/jquery/dist/jquery.min.js"></script>
|
||
<!-- Alpine.js -->
|
||
<script defer src="/assets/alpinejs/dist/cdn.min.js"></script>
|
||
<!-- SweetAlert2 -->
|
||
<link rel="stylesheet" href="/assets/sweetalert2/dist/sweetalert2.min.css">
|
||
<script src="/assets/sweetalert2/dist/sweetalert2.all.min.js"></script>
|
||
<!-- Main JS -->
|
||
<script src="/admin/js/main.js"></script>
|
||
</head>
|
||
|
||
<body x-data>
|
||
|
||
<!-- Sidebar -->
|
||
<nav id="sidebar" :class="{ 'show': $store.sidebar.open }">
|
||
<div class="sidebar-header">
|
||
<h5 class="mb-0">AdminPanel</h5>
|
||
</div>
|
||
<div class="sidebar-nav list-group list-group-flush mt-3">
|
||
<a href="#" class="nav-link active" hx-get="/admin/content/dashboard" hx-target="#main-content"
|
||
hx-push-url="true">
|
||
<i class="bi bi-speedometer2 me-2"></i> Dashboard
|
||
</a>
|
||
<a href="#" class="nav-link" hx-get="/admin/content/users" hx-target="#main-content" hx-push-url="true">
|
||
<i class="bi bi-people me-2"></i> Kullanıcılar
|
||
</a>
|
||
<div class="nav-item">
|
||
<a class="nav-link collapsed" data-bs-toggle="collapse" href="#blogMenu" role="button"
|
||
aria-expanded="false" aria-controls="blogMenu">
|
||
<i class="bi bi-journal-text me-2"></i> Blog
|
||
</a>
|
||
<div class="collapse" id="blogMenu">
|
||
<div class="list-group list-group-flush ms-3">
|
||
<a href="#" class="list-group-item list-group-item-action" hx-get="/admin/content/posts"
|
||
hx-target="#main-content" hx-push-url="true">Yazılar</a>
|
||
<a href="#" class="list-group-item list-group-item-action" hx-get="/admin/content/categories"
|
||
hx-target="#main-content" hx-push-url="true">Kategoriler</a>
|
||
<a href="#" class="list-group-item list-group-item-action" hx-get="/admin/content/tags"
|
||
hx-target="#main-content" hx-push-url="true">Taglar</a>
|
||
<a href="#" class="list-group-item list-group-item-action"
|
||
hx-get="/admin/content/category-views" hx-target="#main-content" hx-push-url="true">Kategori
|
||
Görünümleri</a>
|
||
<a href="#" class="list-group-item list-group-item-action" hx-get="/admin/content/comments"
|
||
hx-target="#main-content" hx-push-url="true">Yorumlar</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="nav-item">
|
||
<a class="nav-link collapsed" data-bs-toggle="collapse" href="#productMenu" role="button"
|
||
aria-expanded="false" aria-controls="productMenu">
|
||
<i class="bi bi-box me-2"></i> Ürünler
|
||
</a>
|
||
<div class="collapse" id="productMenu">
|
||
<div class="list-group list-group-flush ms-3">
|
||
<a href="#" class="list-group-item list-group-item-action" hx-get="/admin/content/products"
|
||
hx-target="#main-content" hx-push-url="true">Ürünler</a>
|
||
<a href="#" class="list-group-item list-group-item-action"
|
||
hx-get="/admin/content/product-categories" hx-target="#main-content"
|
||
hx-push-url="true">Kategoriler</a>
|
||
<a href="#" class="list-group-item list-group-item-action" hx-get="/admin/content/product-tags"
|
||
hx-target="#main-content" hx-push-url="true">Etiketler</a>
|
||
<!-- Sepetler (Carts) -->
|
||
<a href="#" class="list-group-item list-group-item-action border-top mt-2"
|
||
hx-get="/admin/content/carts" hx-target="#main-content" hx-push-url="true">
|
||
<i class="bi bi-cart3 me-1"></i> Sepetler
|
||
</a>
|
||
<!-- Ürün İçgörüleri -->
|
||
<a href="#" class="list-group-item list-group-item-action"
|
||
hx-get="/admin/content/product-comments" hx-target="#main-content" hx-push-url="true">
|
||
<i class="bi bi-chat-left-text me-1"></i> Yorumlar
|
||
</a>
|
||
<a href="#" class="list-group-item list-group-item-action"
|
||
hx-get="/admin/content/product-category-views" hx-target="#main-content" hx-push-url="true">
|
||
<i class="bi bi-eye me-1"></i> Görüntülenmeler
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a href="#" class="nav-link" hx-get="/admin/content/settings" hx-target="#main-content" hx-push-url="true">
|
||
<i class="bi bi-gear me-2"></i> Ayarlar
|
||
</a>
|
||
<a href="/logout" class="nav-link text-danger mt-5">
|
||
<i class="bi bi-box-arrow-right me-2"></i> Çıkış
|
||
</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- Header -->
|
||
<header id="main-header">
|
||
<button class="btn btn-link link-body-emphasis d-lg-none me-3" @click="$store.sidebar.toggle()">
|
||
<i class="bi bi-list fs-4"></i>
|
||
</button>
|
||
|
||
<h5 class="mb-0 d-none d-lg-block">Dashboard</h5>
|
||
|
||
<div class="ms-auto d-flex align-items-center gap-3">
|
||
<!-- Theme Toggle -->
|
||
<button class="btn btn-link nav-link" @click="$store.theme.toggle()">
|
||
<i class="bi" :class="$store.theme.mode === 'light' ? 'bi-moon-stars' : 'bi-sun'"></i>
|
||
<span class="d-none d-md-inline ms-1"
|
||
x-text="$store.theme.mode === 'light' ? 'Koyu Mod' : 'Aydınlık Mod'"></span>
|
||
</button>
|
||
|
||
<!-- User Dropdown (Optional) -->
|
||
<div class="dropdown">
|
||
<a href="#" class="d-flex align-items-center link-body-emphasis text-decoration-none dropdown-toggle"
|
||
data-bs-toggle="dropdown" aria-expanded="false">
|
||
<img id="adminAvatar" src="https://github.com/mdo.png" alt="" width="32" height="32"
|
||
class="rounded-circle me-2">
|
||
<strong id="adminName">Admin</strong>
|
||
</a>
|
||
<ul class="dropdown-menu dropdown-menu-end text-small shadow">
|
||
<li><a class="dropdown-item" href="#">Profil</a></li>
|
||
<li>
|
||
<hr class="dropdown-divider">
|
||
</li>
|
||
<li><a class="dropdown-item" href="/logout">Çıkış Yap</a></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- Main Content -->
|
||
<div id="main-content-wrapper">
|
||
<main id="main-content" class="p-4 fade-in">
|
||
{{embed}}
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Global Modal Container for HTMX -->
|
||
<div class="modal fade" id="userModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content" id="modal-area">
|
||
<!-- Modal Content will be loaded here via HTMX -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bootstrap Bundle with Popper -->
|
||
<script src="/assets/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
|
||
|
||
<script>
|
||
document.addEventListener('htmx:afterSwap', function (event) {
|
||
// Only handle modal area swaps
|
||
if (event.detail.target.id === 'modal-area') {
|
||
// If the response is not empty (i.e., form content or error), show the modal
|
||
if (event.detail.target.innerHTML.trim() !== "") {
|
||
const el = document.getElementById('userModal');
|
||
let modal = bootstrap.Modal.getInstance(el);
|
||
if (!modal) {
|
||
modal = new bootstrap.Modal(el);
|
||
}
|
||
modal.show();
|
||
}
|
||
}
|
||
});
|
||
|
||
// Initialize Quill if injected via HTMX into any target
|
||
document.addEventListener('htmx:afterSwap', function (event) {
|
||
try {
|
||
var target = event.detail.target;
|
||
if (!target || !target.querySelector) return;
|
||
var editorEl = target.querySelector('#quillEditor');
|
||
if (!editorEl) return;
|
||
|
||
function doInit() {
|
||
if (editorEl.__quillInitialized) return;
|
||
var textarea = target.querySelector('#contentInput');
|
||
var quill = new Quill(editorEl, {
|
||
theme: 'snow',
|
||
modules: {
|
||
toolbar: {
|
||
container: [['bold', 'italic', 'underline'], [{ 'list': 'ordered' }, { 'list': 'bullet' }], ['link', 'image'], ['code-block'], ['clean']],
|
||
handlers: {
|
||
image: function () {
|
||
try { window.showImageModal(quill); } catch (e) { console.error('open image modal failed', e); }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
editorEl.__quillInitialized = true;
|
||
if (textarea && textarea.value && textarea.value.trim() !== '') {
|
||
quill.clipboard.dangerouslyPasteHTML(textarea.value);
|
||
}
|
||
if (textarea && textarea.form) {
|
||
textarea.form.addEventListener('submit', function () { textarea.value = quill.root.innerHTML; });
|
||
}
|
||
}
|
||
|
||
if (window.Quill) {
|
||
doInit();
|
||
} else {
|
||
// load CSS if missing
|
||
if (!document.querySelector('link[href="/assets/quill/dist/quill.snow.css"]')) {
|
||
var l = document.createElement('link'); l.rel = 'stylesheet'; l.href = '/assets/quill/dist/quill.snow.css'; document.head.appendChild(l);
|
||
}
|
||
if (!document.querySelector('script[src="/assets/quill/dist/quill.js"]')) {
|
||
var s = document.createElement('script'); s.src = '/assets/quill/dist/quill.js'; s.onload = doInit; document.body.appendChild(s);
|
||
} else {
|
||
// script already present, try init shortly
|
||
setTimeout(doInit, 50);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Quill HTMX init error', e);
|
||
}
|
||
});
|
||
|
||
// Global SweetAlert2 Helpers
|
||
const Toast = Swal.mixin({
|
||
toast: true,
|
||
position: 'top-end',
|
||
showConfirmButton: false,
|
||
timer: 3000,
|
||
timerProgressBar: true,
|
||
didOpen: (toast) => {
|
||
toast.addEventListener('mouseenter', Swal.stopTimer)
|
||
toast.addEventListener('mouseleave', Swal.resumeTimer)
|
||
}
|
||
});
|
||
|
||
function showToast(icon, title) {
|
||
Toast.fire({
|
||
icon: icon,
|
||
title: title
|
||
});
|
||
}
|
||
|
||
function confirmDelete(event, form) {
|
||
event.preventDefault();
|
||
Swal.fire({
|
||
title: 'Emin misiniz?',
|
||
text: "Bu işlem geri alınamaz!",
|
||
icon: 'warning',
|
||
showCancelButton: true,
|
||
confirmButtonColor: '#d33',
|
||
cancelButtonColor: '#3085d6',
|
||
confirmButtonText: 'Evet, Sil!',
|
||
cancelButtonText: 'İptal'
|
||
}).then((result) => {
|
||
if (result.isConfirmed) {
|
||
form.submit();
|
||
}
|
||
});
|
||
return false;
|
||
}
|
||
|
||
function confirmRestore(event, form) {
|
||
event.preventDefault();
|
||
Swal.fire({
|
||
title: 'Geri Yükle?',
|
||
text: "Bu kullanıcı tekrar aktif edilecek.",
|
||
icon: 'question',
|
||
showCancelButton: true,
|
||
confirmButtonColor: '#28a745',
|
||
cancelButtonColor: '#3085d6',
|
||
confirmButtonText: 'Evet, Geri Yükle!',
|
||
cancelButtonText: 'İptal'
|
||
}).then((result) => {
|
||
if (result.isConfirmed) {
|
||
form.submit();
|
||
}
|
||
});
|
||
return false;
|
||
}
|
||
|
||
// Check for success/error parameters in URL
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const successMsg = urlParams.get('success');
|
||
const errorMsg = urlParams.get('error');
|
||
|
||
if (successMsg) {
|
||
showToast('success', successMsg);
|
||
// Clean URL
|
||
window.history.replaceState({}, document.title, window.location.pathname);
|
||
}
|
||
if (errorMsg) {
|
||
showToast('error', errorMsg);
|
||
window.history.replaceState({}, document.title, window.location.pathname);
|
||
}
|
||
});
|
||
|
||
// Listen for the closeModal event triggered by HX-Trigger
|
||
document.addEventListener('closeModal', function () {
|
||
const el = document.getElementById('userModal');
|
||
const modal = bootstrap.Modal.getInstance(el);
|
||
if (modal) {
|
||
modal.hide();
|
||
// Clear content after hiding
|
||
setTimeout(() => {
|
||
document.getElementById('modal-area').innerHTML = "";
|
||
}, 500);
|
||
}
|
||
});
|
||
</script>
|
||
<script>
|
||
// Populate admin dropdown with real name/email/avatar
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
fetch('/admin/me', { credentials: 'same-origin' })
|
||
.then(function (res) { if (!res.ok) throw new Error('no'); return res.json(); })
|
||
.then(function (data) {
|
||
if (!data) return;
|
||
try {
|
||
var nameEl = document.getElementById('adminName');
|
||
var avatarEl = document.getElementById('adminAvatar');
|
||
if (nameEl) nameEl.textContent = data.name || data.email || 'Admin';
|
||
if (avatarEl && data.avatar) avatarEl.src = data.avatar;
|
||
} catch (e) { }
|
||
}).catch(function () { /* ignore */ });
|
||
});
|
||
</script>
|
||
<style>
|
||
/* Ensure editor images behave */
|
||
.ql-figure img {
|
||
display: block;
|
||
margin: auto;
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
</style>
|
||
|
||
<!-- Image Modal (global for admin) -->
|
||
<div class="modal fade" id="imageModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">Resim Ekle</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Kapat"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="mb-2">
|
||
<label class="form-label">Resim URL</label>
|
||
<input id="imageUrlInput" class="form-control" placeholder="https://...">
|
||
</div>
|
||
<div class="mb-2 row g-2">
|
||
<div class="col-6"><input id="imageWidthInput" class="form-control" placeholder="Genişlik (px)">
|
||
</div>
|
||
<div class="col-6"><input id="imageHeightInput" class="form-control"
|
||
placeholder="Yükseklik (px)"></div>
|
||
</div>
|
||
<div class="mb-2">
|
||
<img id="imagePreviewModal" src="" alt="preview"
|
||
style="display:none;max-width:100%;border:1px solid #ddd;padding:4px;">
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">İptal</button>
|
||
<button id="imageInsertBtn" type="button" class="btn btn-primary">Ekle</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Register a block embed blot to hold images so code-block doesn't swallow them
|
||
(function registerFigureBlot() {
|
||
if (!window.Quill) return;
|
||
try {
|
||
var BlockEmbed = Quill.import('blots/block/embed');
|
||
class FigureBlot extends BlockEmbed {
|
||
static blotName = 'figure';
|
||
static tagName = 'figure';
|
||
static className = 'ql-figure';
|
||
static create(value) {
|
||
let node = super.create();
|
||
let img = document.createElement('img');
|
||
img.setAttribute('src', value.src);
|
||
if (value.style) img.style.cssText = value.style;
|
||
node.appendChild(img);
|
||
return node;
|
||
}
|
||
static value(node) {
|
||
let img = node.querySelector('img');
|
||
return { src: img.getAttribute('src'), style: img.style.cssText };
|
||
}
|
||
}
|
||
Quill.register(FigureBlot);
|
||
} catch (e) {
|
||
// ignore if already registered or Quill not ready
|
||
console.debug('FigureBlot register failed', e);
|
||
}
|
||
})();
|
||
|
||
// Show image modal and insert into provided quill instance
|
||
window.showImageModal = function (quill) {
|
||
if (!quill) return;
|
||
const modalEl = document.getElementById('imageModal');
|
||
const urlInput = document.getElementById('imageUrlInput');
|
||
const wInput = document.getElementById('imageWidthInput');
|
||
const hInput = document.getElementById('imageHeightInput');
|
||
const preview = document.getElementById('imagePreviewModal');
|
||
urlInput.value = '';
|
||
wInput.value = '';
|
||
hInput.value = '';
|
||
preview.style.display = 'none';
|
||
|
||
urlInput.addEventListener('input', function onChange() {
|
||
const v = urlInput.value.trim();
|
||
if (v) { preview.src = v; preview.style.display = 'block'; } else { preview.style.display = 'none'; }
|
||
});
|
||
|
||
var bsModal = new bootstrap.Modal(modalEl);
|
||
bsModal.show();
|
||
|
||
const insertBtn = document.getElementById('imageInsertBtn');
|
||
const handler = function () {
|
||
let src = urlInput.value.trim();
|
||
if (!src) return alert('URL girin');
|
||
if (src.charAt(0) === '/') src = window.location.origin + src;
|
||
let w = parseInt(wInput.value) || 0;
|
||
let h = parseInt(hInput.value) || 0;
|
||
let style = 'max-width:100%;height:auto;';
|
||
if (w) style = 'width:' + w + 'px;' + (h ? 'height:' + h + 'px;' : 'height:auto;');
|
||
if (h && !w) style = 'height:' + h + 'px;max-width:100%;';
|
||
var range = quill.getSelection(true) || { index: quill.getLength() };
|
||
quill.insertEmbed(range.index, 'figure', { src: src, style: style });
|
||
bsModal.hide();
|
||
insertBtn.removeEventListener('click', handler);
|
||
};
|
||
insertBtn.addEventListener('click', handler);
|
||
};
|
||
</script>
|
||
</body>
|
||
|
||
</html> |