support articles
This commit is contained in:
Binary file not shown.
Binary file not shown.
56
migrations/versions/add_help_articles_table.py
Normal file
56
migrations/versions/add_help_articles_table.py
Normal 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')
|
||||||
49
models.py
49
models.py
@@ -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
|
||||||
Binary file not shown.
185
routes/admin.py
185
routes/admin.py
@@ -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})
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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>
|
||||||
|
|||||||
294
templates/public/help_articles.html
Normal file
294
templates/public/help_articles.html
Normal 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>
|
||||||
Reference in New Issue
Block a user