Files
TimeTrack/templates/note_view.html

1469 lines
39 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) %}
<button type="button" class="btn btn-secondary" onclick="showShareModal()">
<span class="icon">🔗</span>
Share
</button>
<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>
<!-- Share Modal -->
<div id="share-modal" class="modal" style="display: none;">
<div class="modal-overlay" onclick="hideShareModal()"></div>
<div class="modal-content modal-large">
<div class="modal-header">
<h3 class="modal-title">Share Note</h3>
<button type="button" class="modal-close" onclick="hideShareModal()">&times;</button>
</div>
<div class="modal-body">
<!-- Create New Share Form -->
<div class="share-create-section">
<h4>Create New Share Link</h4>
<form id="create-share-form" class="modern-form">
<div class="form-row">
<div class="form-group">
<label for="expires_in_days" class="form-label">Expiration</label>
<select id="expires_in_days" name="expires_in_days" class="form-control">
<option value="">Never expires</option>
<option value="1">1 day</option>
<option value="7" selected>7 days</option>
<option value="30">30 days</option>
<option value="90">90 days</option>
</select>
</div>
<div class="form-group">
<label for="max_views" class="form-label">View Limit</label>
<input type="number"
id="max_views"
name="max_views"
class="form-control"
min="1"
placeholder="Unlimited">
</div>
</div>
<div class="form-group">
<label for="password" class="form-label">Password Protection (Optional)</label>
<input type="password"
id="password"
name="password"
class="form-control"
placeholder="Leave empty for no password">
</div>
<button type="submit" class="btn btn-primary">
<span class="icon"></span>
Create Share Link
</button>
</form>
</div>
<!-- Existing Shares List -->
<div class="shares-list-section">
<h4>Active Share Links</h4>
<div id="shares-list" class="shares-list">
<div class="loading">Loading shares...</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* Share Modal Styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
max-height: 90vh;
overflow-y: auto;
margin: 2rem;
}
.modal-large {
max-width: 800px;
width: 100%;
}
.modal-header {
padding: 1.5rem;
border-bottom: 1px solid #e5e7eb;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
color: #1f2937;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #6b7280;
border-radius: 6px;
transition: all 0.2s;
}
.modal-close:hover {
background: #f3f4f6;
color: #1f2937;
}
.modal-body {
padding: 1.5rem;
}
.share-create-section {
background: #f8f9fa;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2rem;
}
.shares-list-section h4 {
margin-bottom: 1rem;
}
.shares-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.share-item {
background: white;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
transition: all 0.2s;
gap: 1rem;
}
.share-item:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.share-item.expired {
opacity: 0.6;
background: #f8f9fa;
}
.share-info {
flex: 1;
min-width: 0;
}
.share-url {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.share-url input {
flex: 1;
padding: 0.5rem;
border: 1px solid #e5e7eb;
border-radius: 6px;
font-family: monospace;
font-size: 0.875rem;
min-width: 0;
}
.share-meta {
display: flex;
flex-wrap: wrap;
gap: 1rem;
font-size: 0.875rem;
color: #6b7280;
}
.share-meta-item {
display: flex;
align-items: center;
gap: 0.25rem;
}
.share-actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex-shrink: 0;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.875rem;
border-radius: 6px;
white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.btn-sm.btn-secondary {
background: white;
color: #667eea;
border: 2px solid #e5e7eb;
}
.btn-sm.btn-secondary:hover {
border-color: #667eea;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.15);
}
.btn-sm.btn-danger {
background: white;
color: #dc2626;
border: 2px solid #fee2e2;
}
.btn-sm.btn-danger:hover {
background: #dc2626;
color: white;
border-color: #dc2626;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.2);
}
.copy-feedback {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
background: #10b981;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
opacity: 0;
transition: opacity 0.3s;
}
.copy-feedback.show {
opacity: 1;
}
.empty-state {
text-align: center;
padding: 2rem;
color: #6b7280;
}
/* Form styles in modal */
.form-row {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.form-row .form-group {
flex: 1;
}
@media (max-width: 640px) {
.form-row {
flex-direction: column;
}
.share-item {
flex-direction: column;
align-items: stretch;
}
.share-actions {
margin-top: 1rem;
justify-content: flex-end;
}
}
</style>
<script>
let shareModal = null;
function showShareModal() {
shareModal = document.getElementById('share-modal');
shareModal.style.display = 'flex';
loadShares();
}
function hideShareModal() {
if (shareModal) {
shareModal.style.display = 'none';
}
}
async function loadShares() {
const sharesList = document.getElementById('shares-list');
sharesList.innerHTML = '<div class="loading">Loading shares...</div>';
try {
const response = await fetch(`/api/notes/{{ note.slug }}/shares`);
const data = await response.json();
if (data.success) {
if (data.shares.length === 0) {
sharesList.innerHTML = '<div class="empty-state">No share links created yet.</div>';
} else {
sharesList.innerHTML = data.shares.map(share => createShareItem(share)).join('');
}
} else {
sharesList.innerHTML = '<div class="error">Failed to load shares.</div>';
}
} catch (error) {
sharesList.innerHTML = '<div class="error">Failed to load shares.</div>';
}
}
function createShareItem(share) {
const isExpired = share.is_expired;
const isLimitReached = share.is_view_limit_reached;
const isInvalid = !share.is_valid;
return `
<div class="share-item ${isInvalid ? 'expired' : ''}">
<div class="share-info">
<div class="share-url">
<input type="text"
value="${share.url}"
readonly
id="share-url-${share.id}"
${isInvalid ? 'disabled' : ''}>
${!isInvalid ? `
<button class="btn btn-sm btn-secondary" onclick="copyShareUrl(${share.id})">
<span class="icon">📋</span>
Copy
</button>
` : ''}
</div>
<div class="share-meta">
<div class="share-meta-item">
<span class="icon">👁️</span>
<span>${share.view_count}${share.max_views ? `/${share.max_views}` : ''} views</span>
</div>
${share.expires_at ? `
<div class="share-meta-item">
<span class="icon">⏰</span>
<span>${isExpired ? 'Expired' : 'Expires'} ${new Date(share.expires_at).toLocaleDateString()}</span>
</div>
` : ''}
${share.has_password ? `
<div class="share-meta-item">
<span class="icon">🔒</span>
<span>Password protected</span>
</div>
` : ''}
<div class="share-meta-item">
<span class="icon">👤</span>
<span>Created by ${share.created_by}</span>
</div>
</div>
</div>
<div class="share-actions">
<button class="btn btn-sm btn-danger" onclick="deleteShare(${share.id})">
<span class="icon">🗑️</span>
Delete
</button>
</div>
</div>
`;
}
function copyShareUrl(shareId) {
const input = document.getElementById(`share-url-${shareId}`);
input.select();
document.execCommand('copy');
// Show feedback
showCopyFeedback();
}
function showCopyFeedback() {
let feedback = document.getElementById('copy-feedback');
if (!feedback) {
feedback = document.createElement('div');
feedback.id = 'copy-feedback';
feedback.className = 'copy-feedback';
feedback.textContent = 'Link copied to clipboard!';
document.body.appendChild(feedback);
}
feedback.classList.add('show');
setTimeout(() => {
feedback.classList.remove('show');
}, 2000);
}
async function deleteShare(shareId) {
if (!confirm('Are you sure you want to delete this share link?')) {
return;
}
try {
const response = await fetch(`/api/notes/shares/${shareId}`, {
method: 'DELETE'
});
const data = await response.json();
if (response.ok) {
loadShares();
} else {
alert('Failed to delete share: ' + (data.error || 'Unknown error'));
}
} catch (error) {
alert('Failed to delete share');
}
}
// Create share form handler
document.getElementById('create-share-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = {};
// Convert form data to object
const expiresInDays = formData.get('expires_in_days');
if (expiresInDays) {
data.expires_in_days = parseInt(expiresInDays);
}
const maxViews = formData.get('max_views');
if (maxViews) {
data.max_views = parseInt(maxViews);
}
const password = formData.get('password');
if (password) {
data.password = password;
}
try {
const response = await fetch(`/api/notes/{{ note.slug }}/shares`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
e.target.reset();
loadShares();
// Auto-copy the new share URL
setTimeout(() => {
const input = document.getElementById(`share-url-${result.share.id}`);
if (input) {
input.select();
document.execCommand('copy');
showCopyFeedback();
}
}, 100);
} else {
alert('Failed to create share: ' + (result.error || 'Unknown error'));
}
} catch (error) {
alert('Failed to create share');
}
});
</script>
{% endblock %}