first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:30:42 +03:00
commit 4d92991817
1982 changed files with 284835 additions and 0 deletions

442
views/admin/layout.html Normal file
View 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>