first commit

This commit is contained in:
nahakubuilde
2025-06-21 23:21:18 +01:00
commit e3e775a693
9 changed files with 940 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
<!-- Search Modal -->
<div class="modal fade" id="searchModal" tabindex="-1" aria-labelledby="searchModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content bg-dark text-light">
<div class="modal-header border-0 pb-1">
<h5 class="modal-title" id="searchModalLabel">Search Notes</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pt-1">
<input type="text" id="modal-search-input" class="form-control mb-3" placeholder="Search all notes...">
<div id="modal-search-results" style="max-height: 300px; overflow-y: auto;"></div>
</div>
</div>
</div>
</div>
<!-- Search Modal JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function() {
var searchModal = document.getElementById('searchModal');
var openSearchBtn = document.getElementById('open-search-modal');
var modalInput = document.getElementById('modal-search-input');
var modalResults = document.getElementById('modal-search-results');
if (openSearchBtn && searchModal && modalInput && modalResults) {
openSearchBtn.addEventListener('click', function() {
var modal = new bootstrap.Modal(searchModal);
modal.show();
setTimeout(function() { modalInput.focus(); }, 300);
});
modalInput.addEventListener('input', function() {
var q = modalInput.value.trim();
if (!q) { modalResults.innerHTML = ''; return; }
// AJAX search
fetch(`/search?q=${encodeURIComponent(q)}`)
.then(r => r.json())
.then(results => {
if (!results.length) { modalResults.innerHTML = '<div class="text-muted small">No results found.</div>'; return; }
modalResults.innerHTML = results.map(r =>
`<div class="mb-2">
<a href="${r.url}" class="fw-bold text-primary">${r.title}</a><br>
<span class="small text-light">...${r.snippet}...</span>
</div>`
).join('');
});
});
// Clear results/input on close
searchModal.addEventListener('hidden.bs.modal', function() {
modalInput.value = '';
modalResults.innerHTML = '';
});
}
});
</script>
+309
View File
@@ -0,0 +1,309 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Flask Blog{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github-dark.min.css">
<link type="text/css" href="{{ url_for('static', filename='css/app_custom_css.css' ) }}" rel="stylesheet">
<script>
// Apply sidebar width and minimized state before page paint to prevent flicker
(function() {
try {
var savedWidth = localStorage.getItem('sidebar-width');
var minimized = localStorage.getItem('sidebar-minimized') === 'true';
var style = document.createElement('style');
var css = '';
if (minimized) {
css += '#sidebar { width: 48px !important; min-width: 48px !important; max-width: 48px !important; overflow-x: hidden; padding-left: 0.2rem; padding-right: 0.2rem; transition: none !important; }';
} else if (savedWidth) {
css += '#sidebar { width: ' + savedWidth + ' !important; transition: none !important; }';
}
if (css) {
style.innerHTML = css;
style.setAttribute('id', 'sidebar-initial-style');
document.head.appendChild(style);
}
} catch (e) {}
})();
</script>
{% block head %}{% endblock %}
</head>
<body style="overflow: hidden;">
<nav class="navbar navbar-expand-lg navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('index') }}">Flask Blog</a>
<div class="collapse navbar-collapse">
<ul class="navbar-nav ms-auto">
{# <li class="nav-item">
<a class="nav-link" href="{{ url_for('create_note') }}">Create Note</a>
</li> #}
</ul>
</div>
</div>
</nav>
<div class="container-fluid" style="height: calc(100vh - 56px);">
<div class="row" style="flex-wrap:nowrap; height: 100%;">
<aside id="sidebar" class="sidebar" style="height: 100%; overflow-y: auto;">
<div class="d-flex align-items-center gap-2 mb-3" style="margin-top: 0.2em;">
<a href="{{ url_for('create_note') }}" class="btn btn-primary btn-sm px-2 py-1" title="New Note" style="font-size:1.1em;"><i class="fa fa-plus"></i></a>
<button id="open-search-modal" class="btn btn-outline-secondary btn-sm px-2 py-1" title="Search" style="font-size:1.1em;"><i class="fa fa-search"></i></button>
<button id="toggle-all-dirs" class="btn btn-outline-secondary btn-sm px-2 py-1" title="Expand/Collapse All" style="font-size:1.1em;"><i class="fa fa-folder"></i></button>
<button class="sidebar-toggle btn btn-outline-secondary btn-sm px-2 py-1" title="Toggle Sidebar" style="font-size:1.1em;margin-left:auto;"><i class="fa fa-angle-double-left"></i></button>
</div>
<div class="resize-handle" style="position:absolute; right:0; top:0; bottom:0; width:4px; cursor:ew-resize;"></div>
<ul class="nav flex-column file-tree">
{% if notes_tree and notes_tree|length > 0 %}
{% macro render_tree(tree, current_path='', level=0) %}
{% for node in tree %}
{% if node.type == 'dir' %}
{% set dir_path = current_path + '/' + node.name if current_path else node.name %}
<li class="nav-item file-tree-dir">
<span class="file-tree-toggle" tabindex="0" data-path="{{ dir_path }}" style="position: relative; display: block; padding: 6px 0;">
<div style="padding-left: {{ level * 16 }}px;">
<!-- Tree line indicators -->
{% if level > 0 %}
<span style="position: absolute; left: {{ (level - 1) * 16 + 7 }}px; top: 0; bottom: 0; border-left: 1px dotted rgba(255,255,255,0.1);"></span>
<span style="position: absolute; left: {{ (level - 1) * 16 + 7 }}px; width: 9px; top: 50%; border-top: 1px dotted rgba(255,255,255,0.1);"></span>
{% endif %}
<!-- Folder icon and name -->
<i class="fa {% if active_path and dir_path in active_path %}fa-folder-open text-info{% else %}fa-folder text-warning{% endif %}" style="margin-right: 5px; width: 16px; text-align: center;"></i>
<span style="margin-left: 2px; opacity: 0.9;">{{ node.name }}</span>
</div>
</span>
<ul class="nav flex-column file-tree-children" style="display: {% if active_path and dir_path in active_path %}block{% else %}none{% endif %};">
{{ render_tree(node.children, dir_path, level + 1) }}
</ul>
</li>
{% elif node.type == 'file' and node.name.endswith('.md') %}
<li class="nav-item file-tree-file">
<a class="nav-link {% if node.path == current_note %}active{% endif %}"
href="{{ url_for('note', note_path=node.path) }}"
style="position: relative; display: block; padding: 4px 0;">
<div style="padding-left: {{ level * 16 }}px;">
<!-- Tree line indicators -->
{% if level > 0 %}
<span style="position: absolute; left: {{ (level - 1) * 16 + 7 }}px; top: 0; bottom: 0; border-left: 1px dotted rgba(255,255,255,0.1);"></span>
<span style="position: absolute; left: {{ (level - 1) * 16 + 7 }}px; width: 9px; top: 50%; border-top: 1px dotted rgba(255,255,255,0.1);"></span>
{% endif %}
<!-- File icon and name -->
<i class="fa fa-file-text-o" style="margin-right: 5px; width: 16px; text-align: center; opacity: 0.7;"></i>
<span style="opacity: 0.85;">{{ node.display_name if node.display_name else node.name[:-3] }}</span>
</div>
</a>
</li>
{% endif %}
{% endfor %}
{% endmacro %}
{{ render_tree(notes_tree) }}
{% else %}
<li class="nav-item text-muted">No notes found.</li>
{% endif %}
</ul>
</aside>
<main class="main-content col py-4" style="height: 100%; overflow-y: auto;">
<!-- Compact Breadcrumbs inside main content -->
<nav aria-label="breadcrumb" style="margin-bottom: 0.7em;">
<ol class="breadcrumb p-0 m-0 bg-transparent" style="font-size: 0.97em;">
{% for crumb in breadcrumbs %}
<li class="breadcrumb-item {% if loop.last %}active{% endif %}" {% if loop.last %}aria-current="page"{% endif %} style="padding-right: 0;">
{% if crumb.url %}
<a href="{{ crumb.url }}" class="text-info text-decoration-none" style="opacity: 0.9; transition: opacity 0.2s, color 0.2s;" onmouseover="this.style.opacity='1'; this.style.color='#17a2b8';" onmouseout="this.style.opacity='0.9'; this.style.color='';">{{ crumb.name }}</a>
{% else %}
<span class="text-light" style="opacity: 0.95;">{{ crumb.name }}</span>
{% endif %}
{% if not loop.last %}
<span class="text-light" style="opacity: 0.6; margin: 0 0.3em;">&gt;</span>
{% endif %}
</li>
{% endfor %}
</ol>
</nav>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alert alert-info">{{ messages[0] }}</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
</div>
</div>
<footer class="text-center mt-3 mb-2 text-muted" style="position: absolute; width: 100%; bottom: 0; left: 0;">
&copy; 2025 Flask Blog
</footer>
<!-- Move Bootstrap JS above custom scripts -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Move main JS to end of body -->
<script>
// Highlight code blocks in EasyMDE preview
document.addEventListener('DOMContentLoaded', function() {
// Sidebar width and minimize persistence
const sidebar = document.getElementById('sidebar');
if (!sidebar) return; // Prevent errors if sidebar is missing
const toggleBtn = sidebar.querySelector('.sidebar-toggle');
const resizeHandle = sidebar.querySelector('.resize-handle');
const savedWidth = localStorage.getItem('sidebar-width');
const minimized = localStorage.getItem('sidebar-minimized') === 'true';
// Remove initial style tag if present
const initialStyle = document.getElementById('sidebar-initial-style');
if (initialStyle) initialStyle.remove();
// Set initial state
if (savedWidth && !minimized) sidebar.style.width = savedWidth;
if (minimized) {
sidebar.classList.add('minimized');
sidebar.style.width = '48px';
sidebar.style.minWidth = '48px';
sidebar.style.maxWidth = '48px';
sidebar.style.paddingLeft = '0.2rem';
sidebar.style.paddingRight = '0.2rem';
toggleBtn.innerHTML = '<i class="fa fa-angle-double-right"></i>';
} else {
sidebar.classList.remove('minimized');
sidebar.style.minWidth = '';
sidebar.style.maxWidth = '';
sidebar.style.paddingLeft = '';
sidebar.style.paddingRight = '';
toggleBtn.innerHTML = '<i class="fa fa-angle-double-left"></i>';
}
toggleBtn.addEventListener('click', function() {
const isMin = sidebar.classList.toggle('minimized');
localStorage.setItem('sidebar-minimized', isMin);
if (isMin) {
sidebar.style.width = '48px';
sidebar.style.minWidth = '48px';
sidebar.style.maxWidth = '48px';
sidebar.style.paddingLeft = '0.2rem';
sidebar.style.paddingRight = '0.2rem';
toggleBtn.innerHTML = '<i class="fa fa-angle-double-right"></i>';
} else {
const width = localStorage.getItem('sidebar-width') || '220px';
sidebar.style.width = width;
sidebar.style.minWidth = '';
sidebar.style.maxWidth = '';
sidebar.style.paddingLeft = '';
sidebar.style.paddingRight = '';
toggleBtn.innerHTML = '<i class="fa fa-angle-double-left"></i>';
}
});
// Sidebar resizing with handle
let isResizing = false;
let startX = 0;
let startWidth = 0;
resizeHandle.addEventListener('mousedown', function(e) {
if (!sidebar.classList.contains('minimized')) {
isResizing = true;
startX = e.clientX;
startWidth = sidebar.offsetWidth;
document.body.style.cursor = 'ew-resize';
e.preventDefault();
e.stopPropagation();
}
});
document.addEventListener('mousemove', function(e) {
if (!isResizing) return;
let newWidth = startWidth + (e.clientX - startX);
newWidth = Math.max(48, Math.min(350, newWidth));
sidebar.style.width = newWidth + 'px';
});
document.addEventListener('mouseup', function() {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
if (!sidebar.classList.contains('minimized')) {
localStorage.setItem('sidebar-width', sidebar.style.width);
}
}
});
// Highlight.js for EasyMDE preview using MutationObserver
const observer = new MutationObserver(function() {
document.querySelectorAll('.editor-preview pre code, .editor-preview-active pre code').forEach(function(block) {
if (!block.classList.contains('hljs')) {
hljs.highlightElement(block);
}
});
});
observer.observe(document.body, { subtree: true, childList: true });
});
</script>
<script>
// File tree expand/collapse with state persistence
document.addEventListener('DOMContentLoaded', function() {
const toggleAllBtn = document.getElementById('toggle-all-dirs');
let isAllExpanded = false;
// Load expanded folders from localStorage
let expandedFolders = new Set(JSON.parse(localStorage.getItem('expanded-folders') || '[]'));
// Initialize folders state
document.querySelectorAll('.file-tree-toggle').forEach(function(toggle) {
const path = toggle.getAttribute('data-path');
const parent = toggle.parentElement;
const children = parent.querySelector('.file-tree-children');
const icon = toggle.querySelector('i');
if (expandedFolders.has(path)) {
children.style.display = 'block';
icon.className = 'fa fa-folder-open text-info';
}
});
// Individual folder toggle with state persistence
document.querySelectorAll('.file-tree-toggle').forEach(function(toggle) {
toggle.addEventListener('click', function() {
const path = this.getAttribute('data-path');
const parent = toggle.parentElement;
const children = parent.querySelector('.file-tree-children');
const icon = toggle.querySelector('i');
if (children) {
const isOpen = children.style.display === 'block';
children.style.display = isOpen ? 'none' : 'block';
icon.className = isOpen ? 'fa fa-folder text-warning' : 'fa fa-folder-open text-info';
// Update localStorage
if (isOpen) {
expandedFolders.delete(path);
} else {
expandedFolders.add(path);
}
localStorage.setItem('expanded-folders', JSON.stringify([...expandedFolders]));
}
});
});
// Expand/collapse all button with state persistence
toggleAllBtn.addEventListener('click', function() {
isAllExpanded = !isAllExpanded;
const icon = toggleAllBtn.querySelector('i');
icon.className = isAllExpanded ? 'fa fa-folder-open' : 'fa fa-folder';
document.querySelectorAll('.file-tree-toggle').forEach(function(toggle) {
const path = toggle.getAttribute('data-path');
const children = toggle.parentElement.querySelector('.file-tree-children');
const folderIcon = toggle.querySelector('i');
if (children) {
children.style.display = isAllExpanded ? 'block' : 'none';
folderIcon.className = isAllExpanded ? 'fa fa-folder-open text-info' : 'fa fa-folder text-warning';
if (isAllExpanded) {
expandedFolders.add(path);
} else {
expandedFolders.delete(path);
}
}
});
// Update localStorage
localStorage.setItem('expanded-folders', JSON.stringify([...expandedFolders]));
});
});
</script>
{% include '_search_modal.html' %}
{% block scripts %}{% endblock %}
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block title %}Create Note - Flask Blog{% endblock %}
{% block content %}
<form method="post">
<div class="mb-3">
<label for="title" class="form-label">Title</label>
<input id="title" name="title" class="form-control" required>
</div>
<div class="mb-3">
<label for="content" class="form-label">Content</label>
<textarea id="content" name="content" class="form-control" rows="10"></textarea>
</div>
<button type="submit" class="btn btn-primary">Create</button>
<a href="{{ url_for('index') }}" class="btn btn-secondary">Cancel</a>
</form>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
<script>
const easyMDE = new EasyMDE({
element: document.getElementById('content')
});
</script>
{% endblock %}
+23
View File
@@ -0,0 +1,23 @@
{% extends 'base.html' %}
{% block title %}Edit {{ title }} - Flask Blog{% endblock %}
{% block content %}
<form method="post">
<div class="mb-3">
<label for="content" class="form-label">Content</label>
<textarea id="content" name="content" class="form-control" rows="12">{{ content }}</textarea>
</div>
<button type="submit" class="btn btn-primary">Save</button>
<a href="{{ url_for('note', note_path=current_note) }}" class="btn btn-secondary">Cancel</a>
</form>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
<script>
const easyMDE = new EasyMDE({
element: document.getElementById('content')
});
</script>
{% endblock %}
+21
View File
@@ -0,0 +1,21 @@
{% extends 'base.html' %}
{% block title %}Flask Blog{% endblock %}
{% block content %}
{% if notes %}
<ul class="list-group">
{% for note in notes %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<a href="{{ url_for('note', slug=note.slug) }}">{{ note.title }}</a>
<span>
<a href="{{ url_for('edit_note', slug=note.slug) }}" class="btn btn-sm btn-outline-secondary">Edit</a>
<form action="{{ url_for('delete_note', slug=note.slug) }}" method="post" style="display:inline;">
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this note?');">Delete</button>
</form>
</span>
</li>
{% endfor %}
</ul>
{% else %}
<p>No notes found.</p>
{% endif %}
{% endblock %}
+19
View File
@@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% block title %}{{ title }} - Flask Blog{% endblock %}
{% block content %}
<div class="mb-3">
<h1>{{ title }}</h1>
<div class="mb-4">
<a href="{{ url_for('edit_note', note_path=current_note) }}" class="btn btn-outline-secondary btn-sm">Edit</a>
<form action="{{ url_for('delete_note', note_path=current_note) }}" method="post" style="display:inline;">
<button type="submit" class="btn btn-outline-danger btn-sm" onclick="return confirm('Delete this note?');">Delete</button>
</form>
<a href="{{ url_for('index') }}" class="btn btn-link btn-sm">Back to Notes</a>
</div>
<div class="card card-body">
{{ html_content|safe }}
</div>
</div>
{% endblock %}