Files
project-managment/templates/base.html
T
sankalp 22826fe78b feat(templates): add push notification prompt UI and JS logic
Enable real-time user notifications to improve engagement and ensure timely updates.
2026-06-30 10:22:21 +05:45

346 lines
20 KiB
HTML

{% load static tailwind_cli %}
<!DOCTYPE html>
<html lang="en" class="theme-{{ color_theme }}">
<head>
<script>
(function() {
if (localStorage.getItem('darkMode') === 'true') {
document.documentElement.classList.add('dark');
}
if (localStorage.getItem('sidebarOpen') !== 'true') {
document.documentElement.classList.add('sidebar-collapsed');
}
})();
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="var(--color-primary, #3b82f6)">
<meta name="color-scheme" content="light dark">
{% if branding and branding.favicon %}
<link rel="icon" href="{{ branding.favicon.url }}">
{% endif %}
<title>{{ brand_name }} - {% block title %}Dashboard{% endblock %}</title>
{% block extra_head %}{% endblock %}
<link href="https://fonts.bunny.net/css?family=instrument-sans:400,500,600,700" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
{% tailwind_css %}
<style>
html { background-color: #f9fafb; color-scheme: light; }
html.dark { background-color: #030712; color-scheme: dark; }
:root { --sidebar-width: 16rem; }
.theme-blue { --color-primary: #3b82f6; --color-primary-hover: #2563eb; }
.theme-emerald { --color-primary: #10b981; --color-primary-hover: #059669; }
.theme-green { --color-primary: #22c55e; --color-primary-hover: #16a34a; }
.theme-indigo { --color-primary: #6366f1; --color-primary-hover: #4f46e5; }
.theme-orange { --color-primary: #f97316; --color-primary-hover: #ea580c; }
.theme-purple { --color-primary: #a855f7; --color-primary-hover: #9333ea; }
.theme-red { --color-primary: #ef4444; --color-primary-hover: #dc2626; }
.theme-rose { --color-primary: #f43f5e; --color-primary-hover: #e11d48; }
.theme-sky { --color-primary: #0ea5e9; --color-primary-hover: #0284c7; }
.theme-teal { --color-primary: #14b8a6; --color-primary-hover: #0d9488; }
.theme-violet { --color-primary: #8b5cf6; --color-primary-hover: #7c3aed; }
.theme-amber { --color-primary: #f59e0b; --color-primary-hover: #d97706; }
.theme-yellow { --color-primary: #eab308; --color-primary-hover: #ca8a04; }
.theme-pink { --color-primary: #ec4899; --color-primary-hover: #db2777; }
.theme-cyan { --color-primary: #06b6d4; --color-primary-hover: #0891b2; }
.theme-lime { --color-primary: #84cc16; --color-primary-hover: #65a30d; }
.theme-slate { --color-primary: #64748b; --color-primary-hover: #475569; }
.theme-gray { --color-primary: #6b7280; --color-primary-hover: #4b5563; }
.theme-zinc { --color-primary: #71717a; --color-primary-hover: #52525b; }
* { font-family: 'Instrument Sans', system-ui, sans-serif; }
body, :not(input):not(textarea):not([contenteditable=true]) { cursor: default; }
button, a, select, input[type=checkbox], input[type=radio], input[type=submit], [role=button], [role=switch], label { cursor: pointer !important; user-select: none; }
input[type=text], input[type=email], input[type=password], input[type=url], input[type=search], input[type=number], textarea, [contenteditable=true] { cursor: text !important; }
.dk-sidebar-link { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem 0.75rem; border-radius: 0.5rem; font-size: 0.9375rem; font-weight: 500; transition: all 0.15s ease; position: relative; white-space: nowrap; }
.dk-sidebar-link-active { background-color: color-mix(in srgb, var(--color-primary, #3b82f6) 20%, transparent); color: var(--color-primary, #3b82f6); }
.dk-sidebar-link-active i { color: var(--color-primary, #3b82f6); }
.dk-sidebar-link-active::before { content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 3px; height: 20px; background-color: var(--color-primary, #3b82f6); border-radius: 0 3px 3px 0; }
.dk-sidebar-link-inactive { color: #6b7280; }
.dk-sidebar-link-inactive:hover { color: #111827; background-color: #f3f4f6; }
.dark .dk-sidebar-link-inactive { color: #9ca3af; }
.dark .dk-sidebar-link-inactive:hover { color: #fff; background-color: rgba(255,255,255,0.05); }
.dk-sidebar-collapsed .dk-sidebar-link { justify-content: center; padding: 0.625rem; gap: 0; }
.dk-sidebar-collapsed .dk-sidebar-link i { margin: 0; font-size: 1.125rem; }
.dk-sidebar-collapsed .dk-sidebar-link span, .dk-sidebar-collapsed .dk-sidebar-label, .dk-sidebar-collapsed .dk-badge, .dk-sidebar-collapsed .dk-sidebar-brand-text { display: none; }
.dk-sidebar-collapsed .dk-sidebar-group { text-align: center; }
.dk-sidebar-collapsed .dk-sidebar-group::after { content: '—'; display: block; color: #d1d5db; font-size: 0.75rem; margin: 0.5rem 0; }
.dark .dk-sidebar-collapsed .dk-sidebar-group::after { color: #374151; }
.dk-sidebar-collapsed .dk-sidebar-group span { display: none; }
.dk-sidebar-collapsed .dk-sidebar-brand { justify-content: center; padding: 0 0.5rem; }
.dk-stat-card { background-color: #fff; border-radius: 0.75rem; padding: 1.25rem; border: 1px solid #e5e7eb; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
.dark .dk-stat-card { background-color: #030712; border-color: #1f2937; }
.dk-badge { display: inline-flex; align-items: center; padding: 0.125rem 0.625rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 500; }
.dk-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; padding: 0.5rem 1rem; border-radius: 0.5rem; font-size: 0.875rem; font-weight: 500; transition: all 0.15s ease; cursor: pointer; border: none; }
.dk-btn:focus { outline: none; box-shadow: 0 0 0 2px var(--tw-ring-offset-shadow,0 0 transparent), 0 0 0 4px rgba(59,130,246,0.5); }
.dk-btn-primary { background-color: var(--color-primary, #2563eb); color: #fff !important; }
.dk-btn-primary:hover { background-color: var(--color-primary-hover, #1d4ed8); color: #fff !important; }
.dk-btn-ghost { color: #4b5563; background: transparent; }
.dk-btn-ghost:hover { background-color: #f3f4f6; }
.dark .dk-btn-ghost { color: #d1d5db; }
.dark .dk-btn-ghost:hover { background-color: #1f2937; }
a { color: var(--color-primary, #3b82f6); }
a:hover { color: var(--color-primary-hover, #2563eb); }
.dark .bg-primary { background-color: var(--color-primary, #3b82f6); }
.text-primary { color: var(--color-primary, #3b82f6); }
.border-primary { border-color: var(--color-primary, #3b82f6); }
.ring-primary:focus { --tw-ring-color: var(--color-primary, #3b82f6); }
input[type=checkbox]:checked { background-color: var(--color-primary, #3b82f6); border-color: var(--color-primary, #3b82f6); }
#dm-btn { background-color: #d1d5db; transition: background-color 0.2s ease-in-out; }
.dark #dm-btn { background-color: var(--color-primary, #3b82f6) !important; }
#dm-btn span { transform: translateX(0); transition: transform 0.2s ease-in-out; }
.dark #dm-btn span { transform: translateX(16px) !important; }
[class*="theme-"] .bg-blue-50 { background-color: color-mix(in srgb, var(--color-primary) 8%, transparent); }
[class*="theme-"] .bg-blue-100 { background-color: color-mix(in srgb, var(--color-primary) 15%, transparent); }
[class*="theme-"] .bg-blue-500 { background-color: var(--color-primary); }
[class*="theme-"] .bg-blue-600 { background-color: var(--color-primary-hover); }
[class*="theme-"] .text-blue-500 { color: var(--color-primary); }
[class*="theme-"] .text-blue-600 { color: var(--color-primary); }
[class*="theme-"] .text-blue-700 { color: var(--color-primary-hover); }
[class*="theme-"] .text-blue-400 { color: color-mix(in srgb, var(--color-primary) 70%, white); }
[class*="theme-"] .hover\:bg-blue-50:hover { background-color: color-mix(in srgb, var(--color-primary) 8%, transparent); }
[class*="theme-"] .hover\:bg-blue-100:hover { background-color: color-mix(in srgb, var(--color-primary) 15%, transparent); }
[class*="theme-"] .ring-blue-500 { --tw-ring-color: var(--color-primary); }
[class*="theme-"] .focus\:ring-blue-500:focus { --tw-ring-color: var(--color-primary); }
[class*="theme-"] .border-blue-200 { border-color: color-mix(in srgb, var(--color-primary) 25%, white); }
[class*="theme-"] .border-blue-300 { border-color: color-mix(in srgb, var(--color-primary) 40%, white); }
[class*="theme-"] .from-blue-500 { --tw-gradient-from: var(--color-primary); }
[class*="theme-"] .to-blue-600 { --tw-gradient-to: var(--color-primary-hover); }
[class*="theme-"] .bg-blue-900\/10 { background-color: color-mix(in srgb, var(--color-primary) 10%, transparent); }
[class*="theme-"] .bg-blue-900\/20 { background-color: color-mix(in srgb, var(--color-primary) 20%, transparent); }
[class*="theme-"] .bg-blue-900\/30 { background-color: color-mix(in srgb, var(--color-primary) 30%, transparent); }
[class*="theme-"] .dark .text-blue-400 { color: color-mix(in srgb, var(--color-primary) 80%, white); }
[class*="theme-"] .dark .bg-blue-900\/10 { background-color: color-mix(in srgb, var(--color-primary) 15%, transparent); }
[class*="theme-"] .dark .bg-blue-900\/20 { background-color: color-mix(in srgb, var(--color-primary) 25%, transparent); }
[class*="theme-"] .dark .bg-blue-900\/30 { background-color: color-mix(in srgb, var(--color-primary) 35%, transparent); }
[class*="theme-"] .dark .text-blue-400 { color: color-mix(in srgb, var(--color-primary) 80%, white); }
.dk-card { background-color: #fff; border-radius: 0.75rem; border: 1px solid #e5e7eb; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
.dark .dk-card { background-color: #030712; border-color: #1f2937; }
.dk-card-header { padding: 1rem 1.5rem; border-bottom: 1px solid #e5e7eb; }
.dark .dk-card-header { border-color: #1f2937; }
.dk-card-body { padding: 1.5rem; }
[x-cloak] { display: none !important; }
html.sidebar-collapsed #sidebar-container > aside { width: 4rem !important; }
html.sidebar-collapsed #sidebar-container > aside .dk-sidebar-brand-text,
html.sidebar-collapsed #sidebar-container > aside .dk-sidebar-link span,
html.sidebar-collapsed #sidebar-container > aside .dk-sidebar-label,
html.sidebar-collapsed #sidebar-container > aside .dk-badge,
html.sidebar-collapsed #sidebar-container > aside .dk-sidebar-group span { display: none; }
html.dark .chart-fill { fill: rgba(59,130,246,0.1); }
html.dark .chart-line { stroke: #3b82f6; }
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.dark .bg-gray-750 { background-color: #111827; }
@media (max-width: 640px) { .dk-stat-card { padding: 1rem; } }
input:not([type=checkbox]):not([type=radio]):not([type=color]), select, textarea { background-color: #fff; }
.dark input:not([type=checkbox]):not([type=radio]):not([type=color]), .dark select, .dark textarea { background-color: #111827; }
</style>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
<script>
document.addEventListener('alpine:init', function() {
Alpine.data('confirmModal', function() {
return {
show: false,
title: 'Confirm',
message: '',
confirmText: 'Confirm',
danger: false,
onConfirm: null,
onCancel: null,
open: function(opts) {
this.title = opts.title || 'Confirm';
this.message = opts.message || 'Are you sure?';
this.confirmText = opts.confirmText || 'Confirm';
this.danger = opts.danger || false;
this.onConfirm = opts.onConfirm || null;
this.onCancel = opts.onCancel || null;
this.show = true;
},
confirm: function() {
this.show = false;
if (typeof this.onConfirm === 'function') this.onConfirm();
},
cancel: function() {
this.show = false;
if (typeof this.onCancel === 'function') this.onCancel();
}
};
});
});
window.confirmAction = function(opts) {
var el = document.getElementById('confirm-modal');
if (el && typeof Alpine !== 'undefined') Alpine.$data(el).open(opts);
};
window.showToast = function(message, type) {
var container = document.getElementById('toast-container');
if (!container) return;
var el = document.createElement('div');
el.className = 'flex items-center gap-2 px-4 py-3 rounded-lg shadow-lg text-white text-sm font-medium transition-all duration-300 translate-y-2 opacity-0';
if (type === 'error') el.classList.add('bg-red-500');
else if (type === 'warning') el.classList.add('bg-amber-500');
else el.classList.add('bg-emerald-500');
el.textContent = message;
container.appendChild(el);
requestAnimationFrame(function() { el.classList.remove('translate-y-2', 'opacity-0'); });
setTimeout(function() {
el.classList.add('translate-y-2', 'opacity-0');
setTimeout(function() { el.remove(); }, 300);
}, 4000);
};
window.copyToClipboard = function(elementId, message) {
var input = document.getElementById(elementId);
if (!input) return;
input.select();
input.setSelectionRange(0, 99999);
navigator.clipboard.writeText(input.value).then(function() {
window.showToast(message, 'success');
}).catch(function() {
document.execCommand('copy');
window.showToast(message, 'success');
});
};
</script>
<script defer src="https://cdn.jsdelivr.net/npm/toolcool-color-picker/dist/toolcool-color-picker.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
<script>
function toggleDarkMode() {
const isDark = document.documentElement.classList.toggle('dark');
localStorage.setItem('darkMode', isDark);
}
</script>
</head>
<body class="bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 antialiased" x-data="{ sidebarOpen: localStorage.getItem('sidebarOpen') === 'true', mobileSidebar: false, navStyle: '{{ navigation_style }}' }">
<div class="flex h-screen overflow-hidden">
<div id="sidebar-container" class="h-full">{% if navigation_style == 'sidebar' %}{% include "components/sidebar.html" %}{% endif %}</div>
<div id="mobile-sidebar-container" class="h-full">{% include "components/mobile_sidebar.html" %}</div>
<div class="flex-1 flex flex-col min-w-0 overflow-hidden">
{% include "components/topbar.html" %}
<div id="topnav-container">{% if navigation_style == 'top' %}{% include "components/top_nav.html" %}{% endif %}</div>
<main id="main-content" class="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8">
{% block breadcrumb %}{% endblock %}
{% block content %}{% endblock %}
</main>
<footer class="shrink-0 border-t border-gray-200 dark:border-gray-600 bg-white dark:bg-gray-950 px-6 py-4">
<div class="flex items-center justify-between text-xs text-gray-400 dark:text-gray-500">
<p>&copy; {% now "Y" %} {{ copyright_name }}. All rights reserved.</p>
<p>Made by <a href="https://www.sankalptharu.com.np" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 font-medium">Sankalp Tharu</a></p>
</div>
</footer>
</div>
</div>
<div id="push-prompt" class="hidden fixed bottom-24 right-4 z-[999] max-w-sm w-full bg-white dark:bg-gray-950 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 p-5 transition-all duration-300">
<div class="flex items-start gap-4">
<div class="shrink-0 w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/></svg>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-gray-900 dark:text-white">Enable notifications?</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Get notified about ticket updates and mentions in real time.</p>
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<button onclick="dismissPush()" class="px-3 py-1.5 text-xs font-medium rounded-lg border border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800">Not now</button>
<button onclick="enablePush()" class="px-3 py-1.5 text-xs font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700">Enable</button>
</div>
</div>
{% include "components/confirm_modal.html" %}
{% include "components/toast_container.html" %}
<script>
function initTooltips() {
document.querySelectorAll('[data-tip]').forEach(el => {
el.addEventListener('mouseenter', function() {
const tip = document.createElement('div');
tip.className = 'absolute z-50 px-2 py-1 text-xs font-medium text-white bg-gray-900 dark:bg-gray-800 rounded shadow-lg whitespace-nowrap';
tip.textContent = this.dataset.tip;
this.appendChild(tip);
});
el.addEventListener('mouseleave', function() {
const tip = this.querySelector('div:last-child');
if (tip) tip.remove();
});
});
}
document.body.addEventListener('htmx:afterRequest', function(evt) {
if (evt.detail.target.id === 'main-content') {
window.scrollTo(0, 0);
}
});
document.body.addEventListener('notifications-cleared', function() {
document.getElementById('notif-badge').innerText = '0';
var container = document.getElementById('notif-bell-container');
if (container && container.__x) {
container.__x.$data.open = false;
}
});
document.addEventListener('htmx:afterSettle', initTooltips);
initTooltips();
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.startsWith(name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
function doPushSubscribe(reg) {
const vapidKey = "{{ vapid_public_key }}";
if (!vapidKey) return;
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidKey,
}).then(sub => {
fetch('/api/notifications/push/subscribe/', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCookie('csrftoken')},
body: JSON.stringify({
endpoint: sub.endpoint,
auth_key: btoa(String.fromCharCode(...new Uint8Array(sub.getKey('auth')))),
p256dh_key: btoa(String.fromCharCode(...new Uint8Array(sub.getKey('p256dh')))),
}),
});
}).catch(() => {});
}
const vapidPublicKey = "{{ vapid_public_key }}";
if (vapidPublicKey && 'serviceWorker' in navigator && 'PushManager' in window) {
navigator.serviceWorker.register('/sw.js').then(reg => {
if (Notification.permission === 'granted') {
doPushSubscribe(reg);
} else if (Notification.permission === 'default' && !localStorage.getItem('push_declined')) {
document.getElementById('push-prompt').classList.remove('hidden');
window.__pushReg = reg;
}
}).catch(() => {});
}
window.enablePush = function() {
const reg = window.__pushReg;
if (!reg) return;
Notification.requestPermission().then(perm => {
if (perm === 'granted') {
doPushSubscribe(reg);
}
document.getElementById('push-prompt').classList.add('hidden');
});
};
window.dismissPush = function() {
localStorage.setItem('push_declined', '1');
document.getElementById('push-prompt').classList.add('hidden');
};
</script>
{% block extra_scripts %}{% endblock %}
</body>
</html>