support articles

This commit is contained in:
2025-06-24 09:43:31 +02:00
parent fed00ff2a0
commit 875e20304b
11 changed files with 1018 additions and 21 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,56 @@
"""add help articles table
Revision ID: add_help_articles_table
Revises: c94c2b2b9f2e
Create Date: 2024-12-19 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy import text
# revision identifiers, used by Alembic.
revision = 'add_help_articles_table'
down_revision = 'c94c2b2b9f2e'
branch_labels = None
depends_on = None
def upgrade():
conn = op.get_bind()
result = conn.execute(text("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'help_articles'
);
"""))
exists = result.scalar()
if not exists:
op.create_table('help_articles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('category', sa.String(length=50), nullable=False),
sa.Column('body', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('created_by', sa.Integer(), nullable=False),
sa.Column('is_published', sa.Boolean(), nullable=True, server_default='true'),
sa.Column('order_index', sa.Integer(), nullable=True, server_default='0'),
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
# Create indexes for better performance
op.create_index('idx_help_articles_category', 'help_articles', ['category'])
op.create_index('idx_help_articles_published', 'help_articles', ['is_published'])
op.create_index('idx_help_articles_order', 'help_articles', ['order_index'])
op.create_index('idx_help_articles_created_at', 'help_articles', ['created_at'])
def downgrade():
op.drop_index('idx_help_articles_category', table_name='help_articles')
op.drop_index('idx_help_articles_published', table_name='help_articles')
op.drop_index('idx_help_articles_order', table_name='help_articles')
op.drop_index('idx_help_articles_created_at', table_name='help_articles')
op.drop_table('help_articles')

View File

@@ -538,3 +538,52 @@ class Instance(db.Model):
def __repr__(self): def __repr__(self):
return f'<Instance {self.name}>' return f'<Instance {self.name}>'
class HelpArticle(db.Model):
__tablename__ = 'help_articles'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(200), nullable=False)
category = db.Column(db.String(50), nullable=False) # getting-started, user-management, file-management, communication, security, administration
body = db.Column(db.Text, nullable=False) # Rich text content
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
created_by = db.Column(db.Integer, db.ForeignKey('user.id', ondelete='CASCADE'), nullable=False)
is_published = db.Column(db.Boolean, default=True)
order_index = db.Column(db.Integer, default=0) # For ordering articles within categories
# Relationships
creator = db.relationship('User', backref=db.backref('created_help_articles', cascade='all, delete-orphan'), foreign_keys=[created_by])
def __repr__(self):
return f'<HelpArticle {self.title} ({self.category})>'
@classmethod
def get_categories(cls):
"""Get all available categories with their display names"""
return {
'getting-started': 'Getting Started',
'user-management': 'User Management',
'file-management': 'File Management',
'communication': 'Communication',
'security': 'Security & Privacy',
'administration': 'Administration'
}
@classmethod
def get_articles_by_category(cls, category, published_only=True):
"""Get articles for a specific category"""
query = cls.query.filter_by(category=category)
if published_only:
query = query.filter_by(is_published=True)
return query.order_by(cls.order_index.asc(), cls.created_at.desc()).all()
@classmethod
def get_all_published(cls):
"""Get all published articles grouped by category"""
articles = cls.query.filter_by(is_published=True).order_by(cls.order_index.asc(), cls.created_at.desc()).all()
grouped = {}
for article in articles:
if article.category not in grouped:
grouped[article.category] = []
grouped[article.category].append(article)
return grouped

View File

@@ -254,3 +254,188 @@ def get_usage_stats():
return jsonify(stats) return jsonify(stats)
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500
@admin.route('/help-articles/<int:article_id>', methods=['PUT'])
@login_required
def update_help_article(article_id):
"""Update a help article"""
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
try:
article = HelpArticle.query.get_or_404(article_id)
title = request.form.get('title')
category = request.form.get('category')
body = request.form.get('body')
order_index = int(request.form.get('order_index', 0))
is_published = request.form.get('is_published') == 'true'
if not title or not category or not body:
return jsonify({'error': 'Title, category, and body are required'}), 400
# Validate category
valid_categories = HelpArticle.get_categories().keys()
if category not in valid_categories:
return jsonify({'error': 'Invalid category'}), 400
article.title = title
article.category = category
article.body = body
article.order_index = order_index
article.is_published = is_published
article.updated_at = datetime.utcnow()
db.session.commit()
# Log the event
log_event(
event_type='help_article_update',
details={
'article_id': article.id,
'title': article.title,
'category': article.category,
'updated_by': f"{current_user.username} {current_user.last_name}"
},
user_id=current_user.id
)
db.session.commit()
return jsonify({'success': True, 'message': 'Article updated successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@admin.route('/help-articles/<int:article_id>', methods=['DELETE'])
@login_required
def delete_help_article(article_id):
"""Delete a help article"""
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
try:
article = HelpArticle.query.get_or_404(article_id)
# Log the event before deletion
log_event(
event_type='help_article_delete',
details={
'article_id': article.id,
'title': article.title,
'category': article.category,
'deleted_by': f"{current_user.username} {current_user.last_name}"
},
user_id=current_user.id
)
db.session.delete(article)
db.session.commit()
return jsonify({'success': True, 'message': 'Article deleted successfully'})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
# Help Articles API endpoints
@admin.route('/help-articles', methods=['GET'])
@login_required
def get_help_articles():
"""Get all help articles"""
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
articles = HelpArticle.query.order_by(HelpArticle.category.asc(), HelpArticle.order_index.asc(), HelpArticle.created_at.desc()).all()
articles_data = []
for article in articles:
articles_data.append({
'id': article.id,
'title': article.title,
'category': article.category,
'body': article.body,
'created_at': article.created_at.isoformat() if article.created_at else None,
'updated_at': article.updated_at.isoformat() if article.updated_at else None,
'created_by': article.created_by,
'is_published': article.is_published,
'order_index': article.order_index
})
return jsonify({'articles': articles_data})
@admin.route('/help-articles', methods=['POST'])
@login_required
def create_help_article():
"""Create a new help article"""
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
try:
title = request.form.get('title')
category = request.form.get('category')
body = request.form.get('body')
order_index = int(request.form.get('order_index', 0))
is_published = request.form.get('is_published') == 'true'
if not title or not category or not body:
return jsonify({'error': 'Title, category, and body are required'}), 400
# Validate category
valid_categories = HelpArticle.get_categories().keys()
if category not in valid_categories:
return jsonify({'error': 'Invalid category'}), 400
article = HelpArticle(
title=title,
category=category,
body=body,
order_index=order_index,
is_published=is_published,
created_by=current_user.id
)
db.session.add(article)
db.session.commit()
# Log the event
log_event(
event_type='help_article_create',
details={
'article_id': article.id,
'title': article.title,
'category': article.category,
'created_by': f"{current_user.username} {current_user.last_name}"
},
user_id=current_user.id
)
db.session.commit()
return jsonify({'success': True, 'message': 'Article created successfully', 'article_id': article.id})
except Exception as e:
db.session.rollback()
return jsonify({'error': str(e)}), 500
@admin.route('/help-articles/<int:article_id>', methods=['GET'])
@login_required
def get_help_article(article_id):
"""Get a specific help article"""
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized'}), 403
article = HelpArticle.query.get_or_404(article_id)
article_data = {
'id': article.id,
'title': article.title,
'category': article.category,
'body': article.body,
'created_at': article.created_at.isoformat() if article.created_at else None,
'updated_at': article.updated_at.isoformat() if article.updated_at else None,
'created_by': article.created_by,
'is_published': article.is_published,
'order_index': article.order_index
}
return jsonify({'article': article_data})

View File

@@ -2,9 +2,11 @@ from flask import Blueprint, jsonify, request, current_app, make_response, flash
from functools import wraps from functools import wraps
from models import ( from models import (
KeyValueSettings, User, Room, Conversation, RoomFile, KeyValueSettings, User, Room, Conversation, RoomFile,
SiteSettings, DocuPulseSettings, Event, Mail, ManagementAPIKey, PasswordSetupToken, PasswordResetToken SiteSettings, DocuPulseSettings, Event, Mail, ManagementAPIKey, PasswordSetupToken, PasswordResetToken,
HelpArticle
) )
from extensions import db, csrf from extensions import db, csrf
from utils import log_event
from datetime import datetime, timedelta from datetime import datetime, timedelta
import os import os
import jwt import jwt
@@ -591,3 +593,4 @@ def get_version_info(current_user):
'git_branch': 'unknown', 'git_branch': 'unknown',
'deployed_at': 'unknown' 'deployed_at': 'unknown'
}), 500 }), 500

View File

@@ -1,5 +1,5 @@
from flask import Blueprint, render_template, redirect, url_for from flask import Blueprint, render_template, redirect, url_for, request
from models import SiteSettings from models import SiteSettings, HelpArticle
import os import os
def init_public_routes(public_bp): def init_public_routes(public_bp):
@@ -38,6 +38,30 @@ def init_public_routes(public_bp):
"""Help Center page""" """Help Center page"""
return render_template('public/help.html') return render_template('public/help.html')
@public_bp.route('/help/articles')
def help_articles():
"""Display help articles by category"""
category = request.args.get('category', '')
# Get all published articles grouped by category
all_articles = HelpArticle.get_all_published()
categories = HelpArticle.get_categories()
# If a specific category is requested, filter to that category
if category and category in categories:
articles = HelpArticle.get_articles_by_category(category)
category_name = categories[category]
else:
articles = []
category_name = None
return render_template('public/help_articles.html',
articles=articles,
all_articles=all_articles,
categories=categories,
current_category=category,
category_name=category_name)
@public_bp.route('/contact') @public_bp.route('/contact')
def contact(): def contact():
"""Contact page""" """Contact page"""

View File

@@ -2,6 +2,37 @@
{% block title %}Support Articles - DocuPulse{% endblock %} {% block title %}Support Articles - DocuPulse{% endblock %}
{% block extra_css %}
<link href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.css" rel="stylesheet">
<style>
.article-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.article-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.category-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.status-badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
.note-editor {
border: 1px solid var(--border-color);
border-radius: 0.375rem;
}
.note-editor.note-frame {
border-color: var(--border-color);
}
.note-editor .note-editing-area {
background-color: var(--white);
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
@@ -10,18 +41,373 @@
<h1 class="h3 mb-0" style="color: var(--primary-color);"> <h1 class="h3 mb-0" style="color: var(--primary-color);">
<i class="fas fa-life-ring me-2"></i>Support Articles <i class="fas fa-life-ring me-2"></i>Support Articles
</h1> </h1>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createArticleModal">
<i class="fas fa-plus me-2"></i>Create New Article
</button>
</div> </div>
<div class="card border-0 shadow-sm"> <!-- Articles List -->
<div class="card-body"> <div class="row" id="articlesList">
<div class="text-center py-5"> <!-- Articles will be loaded here via AJAX -->
<i class="fas fa-file-alt text-muted" style="font-size: 3rem; opacity: 0.5;"></i> </div>
<h4 class="text-muted mt-3">Support Articles</h4> </div>
<p class="text-muted">Content coming soon...</p> </div>
</div>
<!-- Create Article Modal -->
<div class="modal fade" id="createArticleModal" tabindex="-1" aria-labelledby="createArticleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createArticleModalLabel">Create New Help Article</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="createArticleForm">
<div class="modal-body">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="articleTitle" class="form-label">Title</label>
<input type="text" class="form-control" id="articleTitle" name="title" required>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="articleCategory" class="form-label">Category</label>
<select class="form-select" id="articleCategory" name="category" required>
<option value="">Select Category</option>
<option value="getting-started">Getting Started</option>
<option value="user-management">User Management</option>
<option value="file-management">File Management</option>
<option value="communication">Communication</option>
<option value="security">Security & Privacy</option>
<option value="administration">Administration</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<label for="articleBody" class="form-label">Content</label>
<textarea class="form-control" id="articleBody" name="body" rows="15" required></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="orderIndex" class="form-label">Order Index</label>
<input type="number" class="form-control" id="orderIndex" name="order_index" value="0" min="0">
<div class="form-text">Lower numbers appear first in the category</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isPublished" name="is_published" checked>
<label class="form-check-label" for="isPublished">
Publish immediately
</label>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Create Article</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Article Modal -->
<div class="modal fade" id="editArticleModal" tabindex="-1" aria-labelledby="editArticleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editArticleModalLabel">Edit Help Article</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form id="editArticleForm">
<input type="hidden" id="editArticleId" name="id">
<div class="modal-body">
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<label for="editArticleTitle" class="form-label">Title</label>
<input type="text" class="form-control" id="editArticleTitle" name="title" required>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="editArticleCategory" class="form-label">Category</label>
<select class="form-select" id="editArticleCategory" name="category" required>
<option value="">Select Category</option>
<option value="getting-started">Getting Started</option>
<option value="user-management">User Management</option>
<option value="file-management">File Management</option>
<option value="communication">Communication</option>
<option value="security">Security & Privacy</option>
<option value="administration">Administration</option>
</select>
</div>
</div>
</div>
<div class="mb-3">
<label for="editArticleBody" class="form-label">Content</label>
<textarea class="form-control" id="editArticleBody" name="body" rows="15" required></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editOrderIndex" class="form-label">Order Index</label>
<input type="number" class="form-control" id="editOrderIndex" name="order_index" value="0" min="0">
<div class="form-text">Lower numbers appear first in the category</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="editIsPublished" name="is_published">
<label class="form-check-label" for="editIsPublished">
Published
</label>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Update Article</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteArticleModal" tabindex="-1" aria-labelledby="deleteArticleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteArticleModalLabel">Delete Article</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this article? This action cannot be undone.</p>
<p class="text-muted" id="deleteArticleTitle"></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDelete">Delete Article</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote-bs4.min.js"></script>
<script>
$(document).ready(function() {
// Initialize Summernote for rich text editing
$('#articleBody, #editArticleBody').summernote({
height: 300,
toolbar: [
['style', ['style']],
['font', ['bold', 'underline', 'italic', 'clear']],
['fontname', ['fontname']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['height', ['height']],
['table', ['table']],
['insert', ['link', 'picture']],
['view', ['fullscreen', 'codeview', 'help']]
],
styleTags: [
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
]
});
// Load articles on page load
loadArticles();
// Handle create article form submission
$('#createArticleForm').on('submit', function(e) {
e.preventDefault();
createArticle();
});
// Handle edit article form submission
$('#editArticleForm').on('submit', function(e) {
e.preventDefault();
updateArticle();
});
// Handle delete confirmation
$('#confirmDelete').on('click', function() {
deleteArticle();
});
});
function loadArticles() {
$.get('/api/admin/help-articles', function(data) {
const articlesList = $('#articlesList');
articlesList.empty();
if (data.articles.length === 0) {
articlesList.html(`
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body text-center py-5">
<i class="fas fa-file-alt text-muted" style="font-size: 3rem; opacity: 0.5;"></i>
<h4 class="text-muted mt-3">No Articles Yet</h4>
<p class="text-muted">Create your first help article to get started.</p>
</div>
</div>
</div>
`);
return;
}
data.articles.forEach(article => {
const categoryNames = {
'getting-started': 'Getting Started',
'user-management': 'User Management',
'file-management': 'File Management',
'communication': 'Communication',
'security': 'Security & Privacy',
'administration': 'Administration'
};
const card = $(`
<div class="col-lg-6 col-xl-4 mb-4">
<div class="card article-card border-0 shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="badge category-badge" style="background-color: var(--primary-color);">
${categoryNames[article.category]}
</span>
<span class="badge status-badge ${article.is_published ? 'bg-success' : 'bg-warning'}">
${article.is_published ? 'Published' : 'Draft'}
</span>
</div>
<h5 class="card-title mb-2">${article.title}</h5>
<p class="card-text text-muted small mb-3">
${article.body.substring(0, 100)}${article.body.length > 100 ? '...' : ''}
</p>
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
Order: ${article.order_index} | Created: ${new Date(article.created_at).toLocaleDateString()}
</small>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary" onclick="editArticle(${article.id})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-danger" onclick="confirmDelete(${article.id}, '${article.title}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
</div>
`);
articlesList.append(card);
});
});
}
function createArticle() {
const formData = new FormData($('#createArticleForm')[0]);
formData.set('body', $('#articleBody').summernote('code'));
formData.set('is_published', $('#isPublished').is(':checked'));
$.ajax({
url: '/api/admin/help-articles',
method: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
$('#createArticleModal').modal('hide');
$('#createArticleForm')[0].reset();
$('#articleBody').summernote('code', '');
loadArticles();
showAlert('Article created successfully!', 'success');
},
error: function(xhr) {
showAlert('Error creating article: ' + xhr.responseJSON?.error || 'Unknown error', 'danger');
}
});
}
function editArticle(articleId) {
$.get(`/api/admin/help-articles/${articleId}`, function(data) {
$('#editArticleId').val(data.article.id);
$('#editArticleTitle').val(data.article.title);
$('#editArticleCategory').val(data.article.category);
$('#editArticleBody').summernote('code', data.article.body);
$('#editOrderIndex').val(data.article.order_index);
$('#editIsPublished').prop('checked', data.article.is_published);
$('#editArticleModal').modal('show');
});
}
function updateArticle() {
const formData = new FormData($('#editArticleForm')[0]);
formData.set('body', $('#editArticleBody').summernote('code'));
formData.set('is_published', $('#editIsPublished').is(':checked'));
$.ajax({
url: `/api/admin/help-articles/${$('#editArticleId').val()}`,
method: 'PUT',
data: formData,
processData: false,
contentType: false,
success: function(response) {
$('#editArticleModal').modal('hide');
loadArticles();
showAlert('Article updated successfully!', 'success');
},
error: function(xhr) {
showAlert('Error updating article: ' + xhr.responseJSON?.error || 'Unknown error', 'danger');
}
});
}
function confirmDelete(articleId, title) {
$('#deleteArticleId').val(articleId);
$('#deleteArticleTitle').text(title);
$('#deleteArticleModal').modal('show');
}
function deleteArticle() {
const articleId = $('#deleteArticleId').val();
$.ajax({
url: `/api/admin/help-articles/${articleId}`,
method: 'DELETE',
success: function(response) {
$('#deleteArticleModal').modal('hide');
loadArticles();
showAlert('Article deleted successfully!', 'success');
},
error: function(xhr) {
showAlert('Error deleting article: ' + xhr.responseJSON?.error || 'Unknown error', 'danger');
}
});
}
function showAlert(message, type) {
const alertHtml = `
<div class="alert alert-${type} alert-dismissible fade show" role="alert">
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-triangle'} me-2"></i>
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
$('.container-fluid').prepend(alertHtml);
}
</script>
{% endblock %}

View File

@@ -203,7 +203,7 @@
</div> </div>
<h4 class="fw-bold mb-3">Getting Started</h4> <h4 class="fw-bold mb-3">Getting Started</h4>
<p class="text-muted mb-4">Learn the basics of DocuPulse and set up your workspace</p> <p class="text-muted mb-4">Learn the basics of DocuPulse and set up your workspace</p>
<a href="#getting-started" class="btn btn-outline-primary">View Articles</a> <a href="{{ url_for('public.help_articles', category='getting-started') }}" class="btn btn-outline-primary">View Articles</a>
</div> </div>
</div> </div>
<div class="col-lg-4 col-md-6"> <div class="col-lg-4 col-md-6">
@@ -213,7 +213,7 @@
</div> </div>
<h4 class="fw-bold mb-3">User Management</h4> <h4 class="fw-bold mb-3">User Management</h4>
<p class="text-muted mb-4">Manage users, permissions, and team collaboration</p> <p class="text-muted mb-4">Manage users, permissions, and team collaboration</p>
<a href="#user-management" class="btn btn-outline-primary">View Articles</a> <a href="{{ url_for('public.help_articles', category='user-management') }}" class="btn btn-outline-primary">View Articles</a>
</div> </div>
</div> </div>
<div class="col-lg-4 col-md-6"> <div class="col-lg-4 col-md-6">
@@ -223,7 +223,7 @@
</div> </div>
<h4 class="fw-bold mb-3">File Management</h4> <h4 class="fw-bold mb-3">File Management</h4>
<p class="text-muted mb-4">Upload, organize, and manage your documents</p> <p class="text-muted mb-4">Upload, organize, and manage your documents</p>
<a href="#file-management" class="btn btn-outline-primary">View Articles</a> <a href="{{ url_for('public.help_articles', category='file-management') }}" class="btn btn-outline-primary">View Articles</a>
</div> </div>
</div> </div>
<div class="col-lg-4 col-md-6"> <div class="col-lg-4 col-md-6">
@@ -233,7 +233,7 @@
</div> </div>
<h4 class="fw-bold mb-3">Communication</h4> <h4 class="fw-bold mb-3">Communication</h4>
<p class="text-muted mb-4">Use messaging and collaboration features</p> <p class="text-muted mb-4">Use messaging and collaboration features</p>
<a href="#communication" class="btn btn-outline-primary">View Articles</a> <a href="{{ url_for('public.help_articles', category='communication') }}" class="btn btn-outline-primary">View Articles</a>
</div> </div>
</div> </div>
<div class="col-lg-4 col-md-6"> <div class="col-lg-4 col-md-6">
@@ -243,7 +243,7 @@
</div> </div>
<h4 class="fw-bold mb-3">Security & Privacy</h4> <h4 class="fw-bold mb-3">Security & Privacy</h4>
<p class="text-muted mb-4">Learn about security features and data protection</p> <p class="text-muted mb-4">Learn about security features and data protection</p>
<a href="#security" class="btn btn-outline-primary">View Articles</a> <a href="{{ url_for('public.help_articles', category='security') }}" class="btn btn-outline-primary">View Articles</a>
</div> </div>
</div> </div>
<div class="col-lg-4 col-md-6"> <div class="col-lg-4 col-md-6">
@@ -253,7 +253,7 @@
</div> </div>
<h4 class="fw-bold mb-3">Administration</h4> <h4 class="fw-bold mb-3">Administration</h4>
<p class="text-muted mb-4">Configure settings and manage your organization</p> <p class="text-muted mb-4">Configure settings and manage your organization</p>
<a href="#administration" class="btn btn-outline-primary">View Articles</a> <a href="{{ url_for('public.help_articles', category='administration') }}" class="btn btn-outline-primary">View Articles</a>
</div> </div>
</div> </div>
</div> </div>
@@ -296,7 +296,7 @@
</button> </button>
<div id="faq3" class="collapse" data-bs-parent="#faqAccordion"> <div id="faq3" class="collapse" data-bs-parent="#faqAccordion">
<div class="faq-answer"> <div class="faq-answer">
DocuPulse supports a wide range of file types including documents (PDF, DOCX, DOC, TXT, RTF), spreadsheets (XLSX, XLS, ODS), presentations (PPTX, PPT), images (JPG, PNG, GIF, SVG), archives (ZIP, RAR, 7Z), code files (PY, JS, HTML, CSS), audio/video files, and CAD/design files. Individual files can be up to 10MB, and the platform includes storage limits that can be configured by administrators. DocuPulse supports a wide range of file types including documents (PDF, DOCX, DOC, TXT, RTF), spreadsheets (XLSX, XLS, ODS), presentations (PPTX, PPT), images (JPG, PNG, GIF, SVG), archives (ZIP, RAR, 7Z), code files (PY, JS, HTML, CSS), audio/video files, and CAD/design files.
</div> </div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,294 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% if category_name %}{{ category_name }} - {% endif %}Help Articles - DocuPulse</title>
<meta name="description" content="Browse help articles and documentation for DocuPulse organized by category.">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/colors.css') }}?v={{ 'css/colors.css'|asset_version }}">
<style>
.category-nav {
background: var(--white);
border-radius: 15px;
padding: 20px;
box-shadow: 0 5px 15px var(--shadow-color);
margin-bottom: 30px;
}
.category-link {
display: block;
padding: 12px 20px;
margin: 5px 0;
border-radius: 10px;
text-decoration: none;
color: var(--text-dark);
transition: all 0.3s ease;
border: 2px solid transparent;
}
.category-link:hover {
background-color: var(--bg-color);
color: var(--primary-color);
transform: translateX(5px);
}
.category-link.active {
background-color: var(--primary-color);
color: var(--white);
border-color: var(--primary-color);
}
.article-card {
background: var(--white);
border-radius: 15px;
padding: 30px;
box-shadow: 0 5px 15px var(--shadow-color);
margin-bottom: 25px;
border: none;
transition: transform 0.3s ease;
}
.article-card:hover {
transform: translateY(-2px);
}
.article-title {
color: var(--primary-color);
margin-bottom: 15px;
}
.article-content {
line-height: 1.7;
color: var(--text-dark);
}
.article-content h1, .article-content h2, .article-content h3,
.article-content h4, .article-content h5, .article-content h6 {
color: var(--primary-color);
margin-top: 25px;
margin-bottom: 15px;
}
.article-content p {
margin-bottom: 15px;
}
.article-content ul, .article-content ol {
margin-bottom: 15px;
padding-left: 20px;
}
.article-content li {
margin-bottom: 5px;
}
.article-content code {
background-color: var(--bg-color);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
.article-content pre {
background-color: var(--bg-color);
padding: 15px;
border-radius: 8px;
overflow-x: auto;
margin: 15px 0;
}
.article-content blockquote {
border-left: 4px solid var(--primary-color);
padding-left: 20px;
margin: 20px 0;
font-style: italic;
color: var(--text-muted);
}
.article-content table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.article-content th, .article-content td {
border: 1px solid var(--border-color);
padding: 10px;
text-align: left;
}
.article-content th {
background-color: var(--bg-color);
font-weight: 600;
}
.breadcrumb-nav {
background: var(--white);
border-radius: 10px;
padding: 15px 20px;
margin-bottom: 30px;
box-shadow: 0 2px 8px var(--shadow-color);
}
.breadcrumb-item a {
color: var(--primary-color);
text-decoration: none;
}
.breadcrumb-item.active {
color: var(--text-muted);
}
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-state i {
font-size: 4rem;
color: var(--text-muted);
opacity: 0.5;
margin-bottom: 20px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
transform: translateY(-2px);
box-shadow: 0 5px 15px var(--primary-opacity-15);
filter: brightness(1.1);
}
.btn-outline-primary {
border: 2px solid var(--primary-color);
color: var(--primary-color);
border-radius: 25px;
padding: 12px 30px;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-outline-primary:hover {
background: rgba(var(--primary-color-rgb), 0.05);
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(var(--primary-color-rgb), 0.1);
}
</style>
</head>
<body>
{% include 'components/header_nav.html' %}
<!-- Breadcrumb Navigation -->
<div class="container mt-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb breadcrumb-nav">
<li class="breadcrumb-item"><a href="{{ url_for('public.help_center') }}">Help Center</a></li>
<li class="breadcrumb-item active" aria-current="page">
{% if category_name %}{{ category_name }}{% else %}All Articles{% endif %}
</li>
</ol>
</nav>
</div>
<!-- Main Content -->
<div class="container">
<div class="row">
<!-- Category Navigation -->
<div class="col-lg-3">
<div class="category-nav">
<h5 class="mb-3" style="color: var(--primary-color);">
<i class="fas fa-folder me-2"></i>Categories
</h5>
<a href="{{ url_for('public.help_articles') }}"
class="category-link {% if not current_category %}active{% endif %}">
<i class="fas fa-th-large me-2"></i>All Articles
</a>
{% for category_key, category_name in categories.items() %}
<a href="{{ url_for('public.help_articles', category=category_key) }}"
class="category-link {% if current_category == category_key %}active{% endif %}">
<i class="fas fa-{{ 'rocket' if category_key == 'getting-started' else 'users' if category_key == 'user-management' else 'folder' if category_key == 'file-management' else 'comments' if category_key == 'communication' else 'shield-alt' if category_key == 'security' else 'cog' }} me-2"></i>
{{ category_name }}
{% if all_articles.get(category_key) %}
<span class="badge bg-secondary float-end">{{ all_articles[category_key]|length }}</span>
{% endif %}
</a>
{% endfor %}
</div>
</div>
<!-- Articles Content -->
<div class="col-lg-9">
{% if category_name %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 mb-0" style="color: var(--primary-color);">
<i class="fas fa-{{ 'rocket' if current_category == 'getting-started' else 'users' if current_category == 'user-management' else 'folder' if current_category == 'file-management' else 'comments' if current_category == 'communication' else 'shield-alt' if current_category == 'security' else 'cog' }} me-2"></i>
{{ category_name }}
</h1>
<a href="{{ url_for('public.help_center') }}" class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>Back to Help Center
</a>
</div>
{% else %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h2 mb-0" style="color: var(--primary-color);">
<i class="fas fa-book me-2"></i>All Help Articles
</h1>
<a href="{{ url_for('public.help_center') }}" class="btn btn-outline-primary">
<i class="fas fa-arrow-left me-2"></i>Back to Help Center
</a>
</div>
{% endif %}
{% if articles %}
{% for article in articles %}
<div class="article-card">
<h2 class="article-title">{{ article.title }}</h2>
<div class="article-content">
{{ article.body|safe }}
</div>
<hr class="my-4">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<i class="fas fa-calendar me-1"></i>
Updated: {{ article.updated_at.strftime('%B %d, %Y') if article.updated_at else article.created_at.strftime('%B %d, %Y') }}
</small>
<span class="badge" style="background-color: var(--primary-color);">
{{ categories[article.category] }}
</span>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<i class="fas fa-file-alt"></i>
<h3 class="text-muted">No Articles Found</h3>
<p class="text-muted">
{% if current_category %}
No articles are available in the "{{ category_name }}" category yet.
{% else %}
No help articles are available yet.
{% endif %}
</p>
<a href="{{ url_for('public.help_center') }}" class="btn btn-primary">
<i class="fas fa-arrow-left me-2"></i>Back to Help Center
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% include 'components/footer_nav.html' %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>