batman
This commit is contained in:
commit
345a801c40
33 changed files with 5499 additions and 0 deletions
332
app/templates/base.html
Normal file
332
app/templates/base.html
Normal file
|
@ -0,0 +1,332 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Vim Docs{% endblock %}</title>
|
||||
|
||||
<!-- Material Design Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css">
|
||||
|
||||
<!-- CascadyaCove Nerd Font -->
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'CascadyaCove NF';
|
||||
src: url('/static/fonts/CascadiaCove.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {% if current_user.is_authenticated %}'{{ current_user.theme_color }}'{% else %}'#50fa7b'{% endif %},
|
||||
'primary-dark': {% if current_user.is_authenticated %}'{{ current_user.theme_color }}'{% else %}'#39bd5e'{% endif %},
|
||||
'primary-light': {% if current_user.is_authenticated %}'{{ current_user.theme_color }}'{% else %}'#7dfb96'{% endif %},
|
||||
'gray': {
|
||||
800: '#282a36',
|
||||
900: '#1e1f29',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ['CascadyaCove NF', 'monospace']
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-300 min-h-screen font-sans">
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="flex min-h-screen {% if request.cookies.get('sidebar_collapsed') == 'true' %}sidebar-hidden{% endif %}">
|
||||
<!-- Sidebar -->
|
||||
<aside class="w-64 bg-gray-800 h-screen flex-shrink-0 fixed left-0 top-0 z-10 overflow-y-auto transition-all ease-in-out duration-300">
|
||||
<div class="p-4 border-b border-gray-700 flex items-center">
|
||||
<h1 class="text-xl font-semibold text-white flex items-center">
|
||||
<i class="mdi mdi-vim text-primary text-2xl mr-2"></i> Vim Docs
|
||||
</h1>
|
||||
</div>
|
||||
<nav class="py-4">
|
||||
<ul class="list-none">
|
||||
<li class="my-1">
|
||||
<a href="{{ url_for('main.index') }}" class="flex items-center px-4 py-2 text-gray-300 hover:text-primary hover:bg-gray-700 rounded-md transition-all {{ 'bg-primary/10 text-primary' if request.endpoint == 'main.index' }}">
|
||||
<i class="mdi mdi-home mr-3"></i> Home
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="my-4">
|
||||
<div class="block font-medium text-gray-400 px-4 py-2 flex items-center">
|
||||
<i class="mdi mdi-folder-multiple-outline mr-3"></i> Categories
|
||||
</div>
|
||||
<ul class="ml-3 pl-3 border-l border-gray-700 my-1" id="category-tree">
|
||||
<!-- Categories will be loaded here via JS -->
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="my-4">
|
||||
<div class="block font-medium text-gray-400 px-4 py-2 flex items-center">
|
||||
<i class="mdi mdi-file-document-multiple-outline mr-3"></i> Documents
|
||||
</div>
|
||||
<ul class="ml-3 pl-3 border-l border-gray-700 my-1">
|
||||
<li>
|
||||
<a href="{{ url_for('main.new_document') }}" class="flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors {{ 'text-primary' if request.endpoint == 'main.new_document' }}">
|
||||
<i class="mdi mdi-plus-circle-outline mr-2 text-sm"></i> New Document
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ url_for('main.index') }}?view=recent" class="flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors">
|
||||
<i class="mdi mdi-clock-outline mr-2 text-sm"></i> Recent Documents
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="my-1">
|
||||
<a href="{{ url_for('main.new_document') }}" class="flex items-center px-4 py-2 bg-primary text-black hover:bg-primary-dark rounded-md transition-all">
|
||||
<i class="mdi mdi-plus-circle mr-3"></i> New Document
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="flex-1 flex flex-col ml-64 transition-all ease-in-out duration-300">
|
||||
<!-- Header -->
|
||||
<header class="bg-gray-800 border-b border-gray-700 p-4 flex justify-between items-center sticky top-0 z-10">
|
||||
<div class="flex items-center">
|
||||
<button id="toggle-sidebar" class="p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 transition-colors mr-3">
|
||||
<i class="mdi mdi-menu"></i>
|
||||
</button>
|
||||
<h2 class="text-white text-lg font-medium">{% block header_title %}Dashboard{% endblock %}</h2>
|
||||
|
||||
<div class="relative ml-4">
|
||||
<div class="flex items-center h-9 rounded-md border border-gray-700 bg-gray-900">
|
||||
<i class="mdi mdi-magnify text-gray-400 mx-3"></i>
|
||||
<input type="text" id="search-input"
|
||||
class="bg-transparent border-0 outline-none text-white w-64"
|
||||
placeholder="Search documents...">
|
||||
</div>
|
||||
<div id="search-results" class="absolute left-0 right-0 top-full mt-1 bg-gray-800 rounded-md shadow-lg z-20 max-h-96 overflow-y-auto hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
{% block header_actions %}{% endblock %}
|
||||
|
||||
<div class="relative ml-2">
|
||||
<button id="user-menu-button" class="p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 transition-colors flex items-center">
|
||||
<span class="w-8 h-8 bg-primary/20 rounded-full flex items-center justify-center text-primary mr-2">
|
||||
{{ current_user.username[0].upper() }}
|
||||
</span>
|
||||
<span class="hidden md:inline mr-1">{{ current_user.username }}</span>
|
||||
<i class="mdi mdi-chevron-down text-sm"></i>
|
||||
</button>
|
||||
|
||||
<div id="user-dropdown" class="absolute right-0 mt-2 py-2 w-48 bg-gray-800 rounded-md shadow-lg z-20 hidden">
|
||||
<a href="{{ url_for('auth.settings') }}" class="{{ 'bg-gray-700' if request.endpoint == 'auth.settings' }} block px-4 py-2 text-gray-300 hover:bg-gray-700 hover:text-white w-full text-left">
|
||||
<i class="mdi mdi-cog-outline mr-2"></i> Settings
|
||||
</a>
|
||||
<a href="{{ url_for('auth.logout') }}" class="block px-4 py-2 text-gray-300 hover:bg-gray-700 hover:text-white w-full text-left">
|
||||
<i class="mdi mdi-logout-variant mr-2"></i> Sign Out
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Content area -->
|
||||
<div class="flex-1 p-6 overflow-auto">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Toggle sidebar
|
||||
const toggleBtn = document.getElementById('toggle-sidebar');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', function() {
|
||||
document.querySelector('body > div').classList.toggle('sidebar-hidden');
|
||||
|
||||
// Save preference in cookie
|
||||
const isCollapsed = document.querySelector('body > div').classList.contains('sidebar-hidden');
|
||||
document.cookie = `sidebar_collapsed=${isCollapsed}; path=/; max-age=31536000`;
|
||||
});
|
||||
}
|
||||
|
||||
// User dropdown
|
||||
const userMenuBtn = document.getElementById('user-menu-button');
|
||||
const userDropdown = document.getElementById('user-dropdown');
|
||||
|
||||
if (userMenuBtn && userDropdown) {
|
||||
userMenuBtn.addEventListener('click', function(e) {
|
||||
e.stopPropagation();
|
||||
userDropdown.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!userMenuBtn.contains(e.target) && !userDropdown.contains(e.target)) {
|
||||
userDropdown.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Load categories
|
||||
loadCategories();
|
||||
|
||||
// Search functionality
|
||||
setupSearch();
|
||||
});
|
||||
|
||||
function loadCategories() {
|
||||
const categoryTree = document.getElementById('category-tree');
|
||||
if (!categoryTree) return;
|
||||
|
||||
fetch('/api/categories')
|
||||
.then(response => response.json())
|
||||
.then(categories => {
|
||||
categoryTree.innerHTML = ''; // Clear existing items
|
||||
|
||||
if (categories.length === 0) {
|
||||
categoryTree.innerHTML = '<li class="px-4 py-2 text-gray-500">No categories found</li>';
|
||||
return;
|
||||
}
|
||||
|
||||
categories.forEach(category => {
|
||||
categoryTree.appendChild(createCategoryItem(category));
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading categories:', error);
|
||||
categoryTree.innerHTML = '<li class="px-4 py-2 text-red-500">Error loading categories</li>';
|
||||
});
|
||||
}
|
||||
|
||||
function createCategoryItem(category) {
|
||||
const li = document.createElement('li');
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = `/category/${category.id}`;
|
||||
a.className = 'flex items-center py-1 px-2 text-gray-400 hover:text-primary rounded transition-colors';
|
||||
a.innerHTML = `<i class="mdi ${category.icon} mr-2 text-sm"></i> ${category.name}`;
|
||||
li.appendChild(a);
|
||||
|
||||
if (category.children && category.children.length > 0) {
|
||||
const childrenUl = document.createElement('ul');
|
||||
childrenUl.className = 'ml-2 pl-2 border-l border-gray-700 my-1';
|
||||
|
||||
category.children.forEach(child => {
|
||||
childrenUl.appendChild(createCategoryItem(child));
|
||||
});
|
||||
|
||||
li.appendChild(childrenUl);
|
||||
}
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
function setupSearch() {
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchResults = document.getElementById('search-results');
|
||||
|
||||
if (!searchInput || !searchResults) return;
|
||||
|
||||
// Debounce function
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function() {
|
||||
const context = this;
|
||||
const args = arguments;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
const performSearch = debounce(function() {
|
||||
const query = searchInput.value.trim();
|
||||
|
||||
if (query.length < 2) {
|
||||
searchResults.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/api/search?q=${encodeURIComponent(query)}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
searchResults.innerHTML = '';
|
||||
|
||||
if (data.results.length === 0) {
|
||||
searchResults.innerHTML = '<div class="p-4 text-gray-400">No results found</div>';
|
||||
} else {
|
||||
data.results.forEach(result => {
|
||||
const item = document.createElement('a');
|
||||
item.href = `/document/${result.id}`;
|
||||
item.className = 'block p-3 hover:bg-gray-700 border-b border-gray-700 last:border-0';
|
||||
|
||||
let tagsHtml = '';
|
||||
if (result.tags && result.tags.length > 0) {
|
||||
tagsHtml = '<div class="flex gap-1 mt-1">' +
|
||||
result.tags.map(tag => `<span class="text-xs px-2 py-1 bg-primary/20 text-primary rounded">${tag}</span>`).join('') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
let matchHtml = '';
|
||||
if (result.match) {
|
||||
const regex = new RegExp(`(${query})`, 'gi');
|
||||
matchHtml = `<div class="text-sm text-gray-400 mt-1">${result.match.replace(regex, '<span class="bg-primary/30 text-primary">$1</span>')}</div>`;
|
||||
}
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="font-medium text-white">${result.title}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">${result.category || 'Uncategorized'}</div>
|
||||
${tagsHtml}
|
||||
${matchHtml}
|
||||
`;
|
||||
|
||||
searchResults.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
searchResults.classList.remove('hidden');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Search error:', error);
|
||||
searchResults.innerHTML = '<div class="p-4 text-red-400">Error performing search</div>';
|
||||
searchResults.classList.remove('hidden');
|
||||
});
|
||||
}, 300);
|
||||
|
||||
searchInput.addEventListener('input', performSearch);
|
||||
searchInput.addEventListener('focus', function() {
|
||||
if (searchInput.value.trim().length >= 2) {
|
||||
searchResults.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
|
||||
searchResults.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% else %}
|
||||
<div class="bg-gray-900 min-h-screen">
|
||||
{% block auth_content %}{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Add a link
Reference in a new issue