Files
TimeTrack/templates/note_view.html

965 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "layout.html" %}
{% block content %}
<div class="note-view-container">
<!-- Page Header -->
<div class="page-header">
<div class="header-content">
<div class="header-left">
<h1 class="page-title">{{ note.title }}</h1>
<div class="page-meta">
<span class="visibility-badge visibility-{{ note.visibility.value.lower() }}">
{% if note.visibility.value == 'Private' %}🔒{% elif note.visibility.value == 'Team' %}👥{% else %}🏢{% endif %}
{{ note.visibility.value }}
</span>
{% if note.is_pinned %}
<span class="pin-badge">
<span class="icon">📌</span>
Pinned
</span>
{% endif %}
<span class="meta-divider"></span>
<span class="author">
<span class="icon">👤</span>
{{ note.created_by.username }}
</span>
<span class="meta-divider"></span>
<span class="date">
<span class="icon">📅</span>
Created {{ note.created_at|format_date }}
</span>
{% if note.updated_at > note.created_at %}
<span class="meta-divider"></span>
<span class="date">
<span class="icon">🔄</span>
Updated {{ note.updated_at|format_date }}
</span>
{% endif %}
</div>
</div>
<div class="header-actions">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="downloadDropdown" data-toggle="dropdown">
<span class="icon">⬇️</span>
Download
</button>
<div class="dropdown-menu">
<a class="dropdown-item" href="{{ url_for('notes_download.download_note', slug=note.slug, format='md') }}">
<span class="icon">📄</span>
Markdown (.md)
</a>
<a class="dropdown-item" href="{{ url_for('notes_download.download_note', slug=note.slug, format='html') }}">
<span class="icon">🌐</span>
HTML (.html)
</a>
<a class="dropdown-item" href="{{ url_for('notes_download.download_note', slug=note.slug, format='txt') }}">
<span class="icon">📃</span>
Plain Text (.txt)
</a>
</div>
</div>
<a href="{{ url_for('notes.view_note_mindmap', slug=note.slug) }}" class="btn btn-secondary">
<span class="icon">🧠</span>
Mind Map
</a>
{% if note.can_user_edit(g.user) %}
<a href="{{ url_for('notes.edit_note', slug=note.slug) }}" class="btn btn-primary">
<span class="icon">✏️</span>
Edit
</a>
<form method="POST" action="{{ url_for('notes.delete_note', slug=note.slug) }}"
style="display: inline;"
onsubmit="return confirm('Are you sure you want to delete this note?')">
<button type="submit" class="btn btn-danger">
<span class="icon">🗑️</span>
Delete
</button>
</form>
{% endif %}
<a href="{{ url_for('notes.notes_list') }}" class="btn btn-secondary">
<span class="icon"></span>
Back to Notes
</a>
</div>
</div>
</div>
<!-- Note Metadata Card -->
{% if note.project or note.task or note.tags or note.folder %}
<div class="metadata-card">
<div class="metadata-grid">
{% if note.folder %}
<div class="metadata-item">
<span class="metadata-label">
<span class="icon">📁</span>
Folder
</span>
<span class="metadata-value">
<a href="{{ url_for('notes.notes_list', folder=note.folder) }}" class="metadata-link">
{{ note.folder }}
</a>
</span>
</div>
{% endif %}
{% if note.project %}
<div class="metadata-item">
<span class="metadata-label">
<span class="icon">📋</span>
Project
</span>
<span class="metadata-value">
<a href="{{ url_for('manage_project_tasks', project_id=note.project.id) }}" class="metadata-link">
{{ note.project.code }} - {{ note.project.name }}
</a>
</span>
</div>
{% endif %}
{% if note.task %}
<div class="metadata-item">
<span class="metadata-label">
<span class="icon"></span>
Task
</span>
<span class="metadata-value">
<a href="{{ url_for('view_task', task_id=note.task.id) }}" class="metadata-link">
#{{ note.task.id }} - {{ note.task.title }}
</a>
</span>
</div>
{% endif %}
{% if note.tags %}
<div class="metadata-item">
<span class="metadata-label">
<span class="icon">🏷️</span>
Tags
</span>
<span class="metadata-value">
<div class="tags-list">
{% for tag in note.get_tags_list() %}
<a href="{{ url_for('notes.notes_list', tag=tag) }}" class="tag-chip">
{{ tag }}
</a>
{% endfor %}
</div>
</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Note Content -->
<div class="content-card">
<div class="markdown-content">
{{ note.render_html()|safe }}
</div>
</div>
<!-- Linked Notes Section -->
{% if outgoing_links or incoming_links or note.can_user_edit(g.user) %}
<div class="linked-notes-card">
<div class="card-header">
<h2 class="section-title">
<span class="icon">🔗</span>
Linked Notes
</h2>
{% if note.can_user_edit(g.user) %}
<button id="add-link-btn" class="btn btn-sm btn-primary">
<span class="icon">+</span>
Add Link
</button>
{% endif %}
</div>
{% if outgoing_links or incoming_links %}
<div class="linked-notes-grid">
{% for link in outgoing_links %}
<div class="linked-note-item">
<div class="link-direction outgoing">
<span class="direction-icon"></span>
<span class="link-type">{{ link.link_type|title }}</span>
</div>
<div class="linked-note-content">
<h4 class="linked-note-title">
<a href="{{ url_for('notes.view_note', slug=link.target_note.slug) }}">
{{ link.target_note.title }}
</a>
</h4>
<p class="linked-note-preview">{{ link.target_note.get_preview(100) }}</p>
<div class="linked-note-meta">
<span class="visibility-badge visibility-{{ link.target_note.visibility.value.lower() }} small">
{{ link.target_note.visibility.value }}
</span>
<span class="date">{{ link.target_note.updated_at.strftime('%b %d, %Y') }}</span>
</div>
</div>
{% if note.can_user_edit(g.user) %}
<button class="remove-link-btn" data-target-id="{{ link.target_note_id }}" title="Remove link">
<span class="icon">×</span>
</button>
{% endif %}
</div>
{% endfor %}
{% for link in incoming_links %}
<div class="linked-note-item">
<div class="link-direction incoming">
<span class="direction-icon"></span>
<span class="link-type">{{ link.link_type|title }}</span>
</div>
<div class="linked-note-content">
<h4 class="linked-note-title">
<a href="{{ url_for('notes.view_note', slug=link.source_note.slug) }}">
{{ link.source_note.title }}
</a>
</h4>
<p class="linked-note-preview">{{ link.source_note.get_preview(100) }}</p>
<div class="linked-note-meta">
<span class="visibility-badge visibility-{{ link.source_note.visibility.value.lower() }} small">
{{ link.source_note.visibility.value }}
</span>
<span class="date">{{ link.source_note.updated_at.strftime('%b %d, %Y') }}</span>
</div>
</div>
{% if note.can_user_edit(g.user) %}
<button class="remove-link-btn" data-target-id="{{ link.source_note_id }}" title="Remove link">
<span class="icon">×</span>
</button>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state small">
<p class="empty-message">No linked notes yet.</p>
</div>
{% endif %}
</div>
{% endif %}
</div>
<!-- Add Link Modal -->
{% if note.can_user_edit(g.user) %}
<div id="linkModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Link to Another Note</h3>
<button class="modal-close" onclick="closeLinkModal()">&times;</button>
</div>
<form id="linkForm" onsubmit="submitLink(event)">
<div class="modal-body">
<div class="form-group">
<label for="targetNote">Select Note:</label>
<select id="targetNote" name="target_note_id" class="form-control" required>
<option value="">Choose a note...</option>
{% for other_note in linkable_notes %}
<option value="{{ other_note.id }}">
{{ other_note.title }}
({{ other_note.visibility.value }})
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="linkType">Link Type:</label>
<select id="linkType" name="link_type" class="form-control">
<option value="related">Related</option>
<option value="references">References</option>
<option value="parent">Parent</option>
<option value="child">Child</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeLinkModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Add Link</button>
</div>
</form>
</div>
</div>
{% endif %}
<style>
/* Note view specific styles following the new design system */
.note-view-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
/* Page Header - Time Tracking style */
.page-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 2rem;
color: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 2rem;
}
.page-title {
font-size: 2rem;
font-weight: 700;
margin: 0;
color: white;
}
.header-actions {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
/* Button styles */
.btn {
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-weight: 600;
transition: all 0.3s ease;
border: 2px solid transparent;
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-decoration: none;
cursor: pointer;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: white;
color: #667eea;
border-color: #e5e7eb;
}
.btn-secondary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-color: #667eea;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(239, 68, 68, 0.3);
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(16, 185, 129, 0.3);
}
.btn .icon {
font-size: 1.1em;
}
/* Page Meta */
.page-meta {
display: flex;
align-items: center;
gap: 0.75rem;
flex-wrap: wrap;
font-size: 0.875rem;
color: rgba(255, 255, 255, 0.9);
margin-top: 0.5rem;
}
.meta-divider {
color: rgba(255, 255, 255, 0.5);
}
.pin-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.75rem;
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
font-weight: 500;
}
/* Visibility badges for header */
.page-header .visibility-badge {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
}
/* Metadata Card */
.metadata-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
}
.metadata-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
}
.metadata-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.metadata-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
color: #495057;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.metadata-value {
padding-left: 1.5rem;
}
.metadata-link {
color: var(--primary-color);
text-decoration: none;
transition: color 0.2s ease;
}
.metadata-link:hover {
color: #0056b3;
text-decoration: underline;
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag-chip {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.75rem;
background: #e9ecef;
color: #495057;
border-radius: 12px;
font-size: 0.875rem;
text-decoration: none;
transition: all 0.2s ease;
}
.tag-chip:hover {
background: #dee2e6;
color: #333;
transform: translateY(-1px);
}
/* Content Card */
.content-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 2.5rem;
margin-bottom: 2rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
min-height: 400px;
}
/* Markdown Content Styles */
.markdown-content {
line-height: 1.8;
color: #333;
}
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 2rem;
margin-bottom: 1rem;
font-weight: 600;
line-height: 1.3;
}
.markdown-content h1:first-child {
margin-top: 0;
}
.markdown-content h1 { font-size: 2rem; }
.markdown-content h2 { font-size: 1.5rem; }
.markdown-content h3 { font-size: 1.25rem; }
.markdown-content h4 { font-size: 1.125rem; }
.markdown-content h5 { font-size: 1rem; }
.markdown-content h6 { font-size: 0.875rem; }
.markdown-content p {
margin-bottom: 1.25rem;
}
.markdown-content code {
background: #f8f9fa;
padding: 0.125rem 0.375rem;
border-radius: 3px;
font-size: 0.875em;
color: #e83e8c;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace;
}
.markdown-content pre {
background: #f8f9fa;
border: 1px solid #e9ecef;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
margin-bottom: 1.5rem;
}
.markdown-content pre code {
background: none;
padding: 0;
color: inherit;
}
.markdown-content blockquote {
border-left: 4px solid var(--primary-color);
padding-left: 1.5rem;
margin: 1.5rem 0;
color: #6c757d;
font-style: italic;
}
.markdown-content ul,
.markdown-content ol {
margin-bottom: 1.25rem;
padding-left: 2rem;
}
.markdown-content li {
margin-bottom: 0.5rem;
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.5rem;
overflow: hidden;
border-radius: 6px;
box-shadow: 0 0 0 1px #dee2e6;
}
.markdown-content th,
.markdown-content td {
padding: 0.75rem;
text-align: left;
}
.markdown-content th {
background: #f8f9fa;
font-weight: 600;
border-bottom: 2px solid #dee2e6;
}
.markdown-content tr:not(:last-child) td {
border-bottom: 1px solid #e9ecef;
}
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 6px;
margin: 1.5rem 0;
}
.markdown-content a {
color: var(--primary-color);
text-decoration: none;
}
.markdown-content a:hover {
text-decoration: underline;
}
/* Linked Notes Card */
.linked-notes-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card-header {
background: #f8f9fa;
padding: 1.25rem 1.5rem;
border-bottom: 1px solid #e9ecef;
display: flex;
justify-content: space-between;
align-items: center;
}
.section-title {
font-size: 1.25rem;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.linked-notes-grid {
padding: 1.5rem;
}
.linked-note-item {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
border: 1px solid #e9ecef;
border-radius: 6px;
margin-bottom: 1rem;
position: relative;
transition: all 0.2s ease;
}
.linked-note-item:hover {
border-color: #dee2e6;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.link-direction {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
min-width: 60px;
}
.direction-icon {
font-size: 1.5rem;
color: #6c757d;
}
.link-type {
font-size: 0.75rem;
color: #6c757d;
text-transform: capitalize;
}
.link-direction.outgoing .direction-icon {
color: #28a745;
}
.link-direction.incoming .direction-icon {
color: #17a2b8;
}
.linked-note-content {
flex: 1;
}
.linked-note-title {
font-size: 1.125rem;
margin: 0 0 0.5rem 0;
}
.linked-note-title a {
color: #333;
text-decoration: none;
}
.linked-note-title a:hover {
color: var(--primary-color);
}
.linked-note-preview {
color: #6c757d;
font-size: 0.875rem;
line-height: 1.5;
margin: 0 0 0.5rem 0;
}
.linked-note-meta {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.75rem;
color: #6c757d;
}
.visibility-badge.small {
font-size: 0.7rem;
padding: 0.125rem 0.375rem;
}
.remove-link-btn {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: #dc3545;
color: white;
border: none;
width: 24px;
height: 24px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
}
.linked-note-item:hover .remove-link-btn {
opacity: 1;
}
.remove-link-btn:hover {
background: #c82333;
}
/* Empty State */
.empty-state.small {
padding: 2rem;
text-align: center;
}
.empty-message {
color: #6c757d;
font-style: italic;
margin: 0;
}
/* Dropdown Enhancements */
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
min-width: 200px;
padding: 0.5rem 0;
margin: 0.125rem 0 0;
background-color: white;
border: 1px solid #dee2e6;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.dropdown-menu.show {
display: block;
}
.dropdown-item {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.5rem 1rem;
color: #333;
text-decoration: none;
transition: background 0.2s ease;
}
.dropdown-item:hover {
background-color: #f8f9fa;
}
/* Responsive Design */
@media (max-width: 768px) {
.note-view-container {
padding: 1rem;
}
.page-header .header-content {
flex-direction: column;
gap: 1rem;
}
.header-actions {
width: 100%;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.header-actions .btn {
flex: 1;
min-width: 120px;
}
.content-card {
padding: 1.5rem;
}
.metadata-grid {
grid-template-columns: 1fr;
}
.linked-note-item {
flex-direction: column;
}
.link-direction {
flex-direction: row;
width: 100%;
justify-content: center;
}
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Download dropdown functionality
const downloadBtn = document.getElementById('downloadDropdown');
const downloadMenu = downloadBtn.nextElementSibling;
downloadBtn.addEventListener('click', function(e) {
e.stopPropagation();
downloadMenu.classList.toggle('show');
});
// Close dropdown when clicking outside
document.addEventListener('click', function() {
downloadMenu.classList.remove('show');
});
{% if note.can_user_edit(g.user) %}
// Link modal functionality
const addLinkBtn = document.getElementById('add-link-btn');
if (addLinkBtn) {
addLinkBtn.addEventListener('click', function() {
document.getElementById('linkModal').style.display = 'block';
});
}
// Remove link functionality
document.querySelectorAll('.remove-link-btn').forEach(btn => {
btn.addEventListener('click', function() {
const targetId = this.getAttribute('data-target-id');
if (confirm('Are you sure you want to remove this link?')) {
fetch(`/api/notes/{{ note.id }}/link`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
target_note_id: parseInt(targetId)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while removing the link');
});
}
});
});
{% endif %}
});
{% if note.can_user_edit(g.user) %}
// Modal functions
function closeLinkModal() {
document.getElementById('linkModal').style.display = 'none';
document.getElementById('linkForm').reset();
}
function submitLink(event) {
event.preventDefault();
const formData = new FormData(event.target);
const targetNoteId = formData.get('target_note_id');
const linkType = formData.get('link_type');
fetch(`/api/notes/{{ note.id }}/link`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
target_note_id: parseInt(targetNoteId),
link_type: linkType
})
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.message || 'Server error');
});
}
return response.json();
})
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error: ' + error.message);
});
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('linkModal');
if (event.target == modal) {
closeLinkModal();
}
}
{% endif %}
</script>
{% endblock %}