first commit
This commit is contained in:
442
views/admin/layout.html
Normal file
442
views/admin/layout.html
Normal file
@@ -0,0 +1,442 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user