add article and pubications
This commit is contained in:
@@ -8,6 +8,7 @@ from routes.documents import documents_bp
|
||||
from routes.users import users_bp
|
||||
from routes.payments import payments_bp
|
||||
from routes.subscriptions import subscriptions_bp
|
||||
from routes.articles import articles_bp
|
||||
|
||||
def create_app(test_config=None):
|
||||
# create and configure the app
|
||||
@@ -50,6 +51,7 @@ def create_app(test_config=None):
|
||||
app.register_blueprint(users_bp, url_prefix="/users")
|
||||
app.register_blueprint(payments_bp, url_prefix="/payments")
|
||||
app.register_blueprint(subscriptions_bp, url_prefix="/subscriptions")
|
||||
app.register_blueprint(articles_bp, url_prefix="/articles")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
Binary file not shown.
BIN
server/models/publications/__pycache__/articles.cpython-313.pyc
Normal file
BIN
server/models/publications/__pycache__/articles.cpython-313.pyc
Normal file
Binary file not shown.
146
server/models/publications/articles.py
Normal file
146
server/models/publications/articles.py
Normal file
@@ -0,0 +1,146 @@
|
||||
import sqlite3
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, List
|
||||
|
||||
@dataclass
|
||||
class ArticleModel:
|
||||
id: Optional[int] = None
|
||||
title: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
author_id: Optional[int] = None
|
||||
author_name: Optional[str] = None # To hold joint first_name + last_name
|
||||
created_at: Optional[str] = None
|
||||
|
||||
class Articles:
|
||||
def __init__(self, db_path="instance/app_database.db"):
|
||||
self.db_path = db_path
|
||||
self._create_articles_table()
|
||||
|
||||
def _create_articles_table(self):
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS articles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
author_id INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (author_id) REFERENCES users (id)
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def add_article(self, title: str, content: str, author_id: int) -> Optional[int]:
|
||||
"""Insert a new article and return its ID."""
|
||||
try:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO articles (title, content, author_id)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(title, content, author_id),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
except sqlite3.Error as e:
|
||||
print(f"Error adding article: {e}")
|
||||
return None
|
||||
|
||||
def get_article(self, article_id: int) -> Optional[ArticleModel]:
|
||||
"""Fetch a single article by ID, joining users to get the author's name."""
|
||||
try:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT a.id, a.title, a.content, a.author_id, u.first_name, u.last_name, a.created_at
|
||||
FROM articles a
|
||||
JOIN users u ON a.author_id = u.id
|
||||
WHERE a.id = ?
|
||||
""",
|
||||
(article_id,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return None
|
||||
|
||||
author_name = f"{row[4] or ''} {row[5] or ''}".strip() or "Autor necunoscut"
|
||||
return ArticleModel(
|
||||
id=row[0],
|
||||
title=row[1],
|
||||
content=row[2],
|
||||
author_id=row[3],
|
||||
author_name=author_name,
|
||||
created_at=row[6],
|
||||
)
|
||||
except sqlite3.Error as e:
|
||||
print(f"Error getting article: {e}")
|
||||
return None
|
||||
|
||||
def get_all_articles(self) -> List[ArticleModel]:
|
||||
"""Fetch all articles ordered by created_at DESC (newest first)."""
|
||||
try:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT a.id, a.title, a.content, a.author_id, u.first_name, u.last_name, a.created_at
|
||||
FROM articles a
|
||||
JOIN users u ON a.author_id = u.id
|
||||
ORDER BY a.created_at DESC
|
||||
"""
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
articles = []
|
||||
for row in rows:
|
||||
author_name = f"{row[4] or ''} {row[5] or ''}".strip() or "Autor necunoscut"
|
||||
articles.append(
|
||||
ArticleModel(
|
||||
id=row[0],
|
||||
title=row[1],
|
||||
content=row[2],
|
||||
author_id=row[3],
|
||||
author_name=author_name,
|
||||
created_at=row[6],
|
||||
)
|
||||
)
|
||||
return articles
|
||||
except sqlite3.Error as e:
|
||||
print(f"Error fetching all articles: {e}")
|
||||
return []
|
||||
|
||||
def update_article(self, article_id: int, title: str, content: str) -> bool:
|
||||
"""Update the title and content of an article."""
|
||||
try:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE articles
|
||||
SET title = ?, content = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(title, content, article_id),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
except sqlite3.Error as e:
|
||||
print(f"Error updating article: {e}")
|
||||
return False
|
||||
|
||||
def delete_article(self, article_id: int) -> bool:
|
||||
"""Delete an article by ID."""
|
||||
try:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM articles WHERE id = ?", (article_id,))
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
except sqlite3.Error as e:
|
||||
print(f"Error deleting article: {e}")
|
||||
return False
|
||||
@@ -20,6 +20,7 @@ class UserModel:
|
||||
otp_code: Optional[str] = None
|
||||
otp_expiration: Optional[str] = None
|
||||
active: Optional[int] = None
|
||||
can_create_articles: Optional[int] = 0
|
||||
|
||||
class Users:
|
||||
def __init__(self, db_path="instance/app_database.db"):
|
||||
@@ -46,10 +47,15 @@ class Users:
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
otp_code TEXT,
|
||||
otp_expiration TIMESTAMPTZ,
|
||||
active INTEGER DEFAULT 1
|
||||
active INTEGER DEFAULT 1,
|
||||
can_create_articles INTEGER DEFAULT 0
|
||||
);
|
||||
"""
|
||||
)
|
||||
try:
|
||||
cursor.execute("ALTER TABLE users ADD COLUMN can_create_articles INTEGER DEFAULT 0;")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
conn.commit()
|
||||
|
||||
def update_user_otp(self, user_id, otp_code, expiration):
|
||||
@@ -117,7 +123,8 @@ class Users:
|
||||
created_at=row[11],
|
||||
otp_code=row[12],
|
||||
otp_expiration=row[13],
|
||||
active=row[14]
|
||||
active=row[14],
|
||||
can_create_articles=row[15] if len(row) > 15 else 0
|
||||
)
|
||||
|
||||
def register_user(self, email, password, workspace_id):
|
||||
@@ -141,10 +148,10 @@ class Users:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO users (workspace_id, first_name, last_name, email, password, address, profession, role, status, profile_pic)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO users (workspace_id, first_name, last_name, email, password, address, profession, role, status, profile_pic, can_create_articles)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(user.workspace_id, user.first_name, user.last_name, user.email, user.password, user.address, user.profession, user.role, user.status, user.profile_pic),
|
||||
(user.workspace_id, user.first_name, user.last_name, user.email, user.password, user.address, user.profession, user.role, user.status, user.profile_pic, user.can_create_articles or 0),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
@@ -174,7 +181,8 @@ class Users:
|
||||
created_at=row[11],
|
||||
otp_code=row[12],
|
||||
otp_expiration=row[13],
|
||||
active=row[14]
|
||||
active=row[14],
|
||||
can_create_articles=row[15] if len(row) > 15 else 0
|
||||
)
|
||||
|
||||
def get_user_by_email(self, email: str) -> UserModel | None:
|
||||
@@ -201,7 +209,8 @@ class Users:
|
||||
created_at=row[11],
|
||||
otp_code=row[12],
|
||||
otp_expiration=row[13],
|
||||
active=row[14]
|
||||
active=row[14],
|
||||
can_create_articles=row[15] if len(row) > 15 else 0
|
||||
)
|
||||
|
||||
def get_users_by_workspace_id(self, workspace_id):
|
||||
@@ -225,7 +234,8 @@ class Users:
|
||||
created_at=row[11],
|
||||
otp_code=row[12],
|
||||
otp_expiration=row[13],
|
||||
active=row[14]
|
||||
active=row[14],
|
||||
can_create_articles=row[15] if len(row) > 15 else 0
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
@@ -251,13 +261,14 @@ class Users:
|
||||
created_at=row[11],
|
||||
otp_code=row[12],
|
||||
otp_expiration=row[13],
|
||||
active=row[14]
|
||||
active=row[14],
|
||||
can_create_articles=row[15] if len(row) > 15 else 0
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
def update_user(self, user_id, first_name=None, last_name=None, email=None, password = None, address = None, profession = None, role = None, status = None, profile_pic=None, active=None):
|
||||
if first_name is None and last_name is None and email is None and password is None and address is None and profession is None and role is None and status is None and profile_pic is None and active is None:
|
||||
def update_user(self, user_id, first_name=None, last_name=None, email=None, password = None, address = None, profession = None, role = None, status = None, profile_pic=None, active=None, can_create_articles=None):
|
||||
if first_name is None and last_name is None and email is None and password is None and address is None and profession is None and role is None and status is None and profile_pic is None and active is None and can_create_articles is None:
|
||||
return False
|
||||
|
||||
fields = []
|
||||
@@ -293,6 +304,9 @@ class Users:
|
||||
if active is not None:
|
||||
fields.append("active = ?")
|
||||
params.append(active)
|
||||
if can_create_articles is not None:
|
||||
fields.append("can_create_articles = ?")
|
||||
params.append(can_create_articles)
|
||||
|
||||
params.append(user_id)
|
||||
query = f"UPDATE users SET {', '.join(fields)} WHERE id = ?"
|
||||
|
||||
111
server/routes/articles.py
Normal file
111
server/routes/articles.py
Normal file
@@ -0,0 +1,111 @@
|
||||
from flask import Blueprint, request, jsonify, render_template
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
from models.publications.articles import Articles, ArticleModel
|
||||
from models.users import Users
|
||||
from models.audit import Audit, AuditModel
|
||||
|
||||
articles_bp = Blueprint("articles", __name__)
|
||||
audit = Audit()
|
||||
|
||||
@articles_bp.route("/editor", methods=["GET"])
|
||||
@jwt_required()
|
||||
def show_editor():
|
||||
token = request.args.get("token", "")
|
||||
article_id = request.args.get("article_id", "None")
|
||||
return render_template("editor.html", token=token, article_id=article_id)
|
||||
|
||||
@articles_bp.route("/", methods=["GET"])
|
||||
@jwt_required()
|
||||
def get_all_articles():
|
||||
articles_repo = Articles()
|
||||
articles = articles_repo.get_all_articles()
|
||||
return jsonify([vars(a) for a in articles]), 200
|
||||
|
||||
@articles_bp.route("/<int:article_id>", methods=["GET"])
|
||||
@jwt_required()
|
||||
def get_article(article_id):
|
||||
articles_repo = Articles()
|
||||
article = articles_repo.get_article(article_id)
|
||||
if not article:
|
||||
return jsonify({"error": "Articolul nu a fost gasit"}), 404
|
||||
return jsonify(vars(article)), 200
|
||||
|
||||
@articles_bp.route("/add", methods=["POST"])
|
||||
@jwt_required()
|
||||
def add_article():
|
||||
current_user_id = int(get_jwt_identity())
|
||||
user_repo = Users()
|
||||
user = user_repo.get_user(current_user_id)
|
||||
|
||||
if not user:
|
||||
return jsonify({"error": "Utilizatorul nu a fost gasit"}), 404
|
||||
|
||||
# Verifică dacă utilizatorul are permisiunea de a crea articole
|
||||
if not getattr(user, 'can_create_articles', 0) == 1:
|
||||
audit.new_entry(AuditModel(user_id=current_user_id, action="Attempt to add article without permission", status="403 - Forbidden"))
|
||||
return jsonify({"error": "Nu aveti permisiunea de a publica articole"}), 403
|
||||
|
||||
data = request.get_json()
|
||||
title = data.get("title")
|
||||
content = data.get("content")
|
||||
|
||||
if not title or not content:
|
||||
return jsonify({"error": "Titlul si continutul sunt obligatorii"}), 400
|
||||
|
||||
articles_repo = Articles()
|
||||
article_id = articles_repo.add_article(title, content, current_user_id)
|
||||
|
||||
if article_id:
|
||||
audit.new_entry(AuditModel(user_id=current_user_id, action=f"Added article: {title}", status="201 - Created"))
|
||||
return jsonify({"message": "Articol adaugat cu succes", "id": article_id}), 201
|
||||
|
||||
return jsonify({"error": "Eroare la adaugarea articolului"}), 500
|
||||
|
||||
@articles_bp.route("/update/<int:article_id>", methods=["PUT"])
|
||||
@jwt_required()
|
||||
def update_article(article_id):
|
||||
current_user_id = int(get_jwt_identity())
|
||||
articles_repo = Articles()
|
||||
article = articles_repo.get_article(article_id)
|
||||
|
||||
if not article:
|
||||
return jsonify({"error": "Articolul nu a fost gasit"}), 404
|
||||
|
||||
# Permite modificarea doar dacă utilizatorul curent este autorul articolului
|
||||
if article.author_id != current_user_id:
|
||||
audit.new_entry(AuditModel(user_id=current_user_id, action=f"Attempt to update article ID {article_id} owned by other user", status="403 - Forbidden"))
|
||||
return jsonify({"error": "Puteti modifica doar articolele scrise de dumneavoastra"}), 403
|
||||
|
||||
data = request.get_json()
|
||||
title = data.get("title")
|
||||
content = data.get("content")
|
||||
|
||||
if not title or not content:
|
||||
return jsonify({"error": "Titlul si continutul sunt obligatorii"}), 400
|
||||
|
||||
if articles_repo.update_article(article_id, title, content):
|
||||
audit.new_entry(AuditModel(user_id=current_user_id, action=f"Updated article ID: {article_id}", status="200 - OK"))
|
||||
return jsonify({"message": "Articol modificat cu succes"}), 200
|
||||
|
||||
return jsonify({"error": "Nu s-a putut modifica articolul"}), 500
|
||||
|
||||
@articles_bp.route("/delete/<int:article_id>", methods=["DELETE"])
|
||||
@jwt_required()
|
||||
def delete_article(article_id):
|
||||
current_user_id = int(get_jwt_identity())
|
||||
articles_repo = Articles()
|
||||
article = articles_repo.get_article(article_id)
|
||||
|
||||
if not article:
|
||||
return jsonify({"error": "Articolul nu a fost gasit"}), 404
|
||||
|
||||
# Permite ștergerea doar dacă utilizatorul curent este autorul articolului
|
||||
if article.author_id != current_user_id:
|
||||
audit.new_entry(AuditModel(user_id=current_user_id, action=f"Attempt to delete article ID {article_id} owned by other user", status="403 - Forbidden"))
|
||||
return jsonify({"error": "Puteti sterge doar articolele scrise de dumneavoastra"}), 403
|
||||
|
||||
if articles_repo.delete_article(article_id):
|
||||
audit.new_entry(AuditModel(user_id=current_user_id, action=f"Deleted article ID: {article_id}", status="200 - OK"))
|
||||
return jsonify({"message": "Articol sters cu succes"}), 200
|
||||
|
||||
return jsonify({"error": "Nu s-a putut sterge articolul"}), 500
|
||||
@@ -94,9 +94,7 @@ def verify_code():
|
||||
return jsonify({"error": "Missing email or verification code"}), 400
|
||||
|
||||
user = users.get_user_by_email(email)
|
||||
#-----------------------------------------------> for testing only remove in prod
|
||||
#if email != 'test@test.com':
|
||||
#-----------------------------------------------> for testing only remove in prod
|
||||
|
||||
if not user or user.otp_code != code:
|
||||
entry = AuditModel(user_id=user.id, action=f"Attempt to verify code: {email}", status='401 - Invalid code!')
|
||||
audit.new_entry(entry)
|
||||
@@ -235,7 +233,8 @@ def me():
|
||||
'profile_pic': user.profile_pic,
|
||||
'created_at': user.created_at,
|
||||
'otp_code': user.otp_code,
|
||||
'otp_expiration': user.otp_expiration
|
||||
'otp_expiration': user.otp_expiration,
|
||||
'can_create_articles': user.can_create_articles
|
||||
}), 200
|
||||
|
||||
|
||||
|
||||
@@ -207,6 +207,8 @@ def add_standard():
|
||||
# Sursa este folderul unde Flet salvează implicit upload-urile
|
||||
source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path))
|
||||
dest_path = os.path.join(DOCUMENTS_ROOT, path)
|
||||
print(source_path)
|
||||
print(dest_path)
|
||||
|
||||
if os.path.exists(source_path):
|
||||
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||
@@ -265,8 +267,8 @@ def delete_standard(id):
|
||||
def add_custom():
|
||||
user_id = get_jwt_identity()
|
||||
data = request.get_json()
|
||||
name = data.get("name")
|
||||
path = data.get("path")
|
||||
name = data.get("name", "").strip()
|
||||
path = data.get("path", "").strip()
|
||||
access = data.get("access")
|
||||
|
||||
if not name or not path:
|
||||
@@ -274,17 +276,31 @@ def add_custom():
|
||||
audit.new_entry(entry)
|
||||
return jsonify({"error": "Missing name or path"}), 400
|
||||
|
||||
# Physical file move from upload folder to documents root
|
||||
source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path))
|
||||
dest_path = os.path.join(DOCUMENTS_ROOT, path)
|
||||
# Physical file move from upload folder to documents/Custom folder
|
||||
filename = os.path.basename(path)
|
||||
# The 'path' variable here is the filename from the client, not the full path.
|
||||
# We construct the full path for the database entry later.
|
||||
|
||||
source_path = os.path.join(BASE_DIR, "client", "assets", "uploads", os.path.basename(path))
|
||||
dest_dir = os.path.normpath(os.path.join(DOCUMENTS_ROOT, "Custom"))
|
||||
dest_path = os.path.join(dest_dir, filename) # This is the actual destination path for shutil.move
|
||||
print(source_path)
|
||||
print(dest_path)
|
||||
if os.path.exists(source_path):
|
||||
# Ensure the destination directory exists (for custom docs we usually put them in root or a 'custom' folder)
|
||||
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||
os.makedirs(dest_dir, exist_ok=True) # Ensure the destination directory exists
|
||||
shutil.move(source_path, dest_path)
|
||||
# Update the 'path' variable to be stored in the database to reflect its new location
|
||||
db_path = f"Custom/{filename}"
|
||||
else:
|
||||
# Log an error and return a response if the source file doesn't exist
|
||||
error_message = f"Source file not found for custom document: {source_path}"
|
||||
print(f"ERROR: {error_message}") # For immediate debug visibility
|
||||
entry = AuditModel(user_id=user_id, action=f"Failed to create custom document (file not found): {name}", status="404 - File not found")
|
||||
audit.new_entry(entry)
|
||||
return jsonify({"error": "Uploaded file not found on server."}), 404
|
||||
|
||||
docs_custom = DocumentsCustom()
|
||||
custom = DocumentsCustomModel(user_id=user_id, name=name, path=path, access=access)
|
||||
custom = DocumentsCustomModel(user_id=user_id, name=name, path=db_path, access=access) # Use db_path here
|
||||
result = docs_custom.new_entry(custom)
|
||||
|
||||
if result:
|
||||
@@ -292,6 +308,10 @@ def add_custom():
|
||||
audit.new_entry(entry)
|
||||
return jsonify({"message": "Custom document created successfully", "id": result}), 201
|
||||
|
||||
# If we reach here, it means the DB entry failed after the file was moved.
|
||||
# This is a potential issue, as the file is moved but not recorded.
|
||||
entry = AuditModel(user_id=user_id, action=f"Failed to create custom document (DB entry failed): {name}", status="500 - DB error")
|
||||
audit.new_entry(entry)
|
||||
return jsonify({"error": "Failed to create custom document"}), 500
|
||||
|
||||
@documents_bp.route("/customs", methods=["GET"])
|
||||
|
||||
@@ -86,7 +86,8 @@ def update_user(user_id):
|
||||
role=data.get("role"),
|
||||
status=data.get("status"),
|
||||
profile_pic=data.get("profile_pic"),
|
||||
active=data.get("active")
|
||||
active=data.get("active"),
|
||||
can_create_articles=data.get("can_create_articles")
|
||||
)
|
||||
|
||||
if success:
|
||||
|
||||
171
server/templates/editor.html
Normal file
171
server/templates/editor.html
Normal file
@@ -0,0 +1,171 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ro">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Editor Publicație</title>
|
||||
<!-- EasyMDE CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/easymde/dist/easymde.min.css">
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
background-color: #f3f4f6;
|
||||
padding: 30px 10px;
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
.editor-card {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 35px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
.editor-toolbar {
|
||||
border: 1px solid #d1d5db !important;
|
||||
border-radius: 8px 8px 0 0 !important;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
.CodeMirror {
|
||||
border: 1px solid #d1d5db !important;
|
||||
border-radius: 0 0 8px 8px !important;
|
||||
min-height: 400px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="editor-card">
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
<svg class="text-primary me-2" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 20h9"></path>
|
||||
<path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"></path>
|
||||
</svg>
|
||||
<h2 class="m-0 fw-bold text-dark" id="editor-title-label">Adaugă Articol Nou</h2>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="title" class="form-label fw-bold text-secondary">Titlu Articol</label>
|
||||
<input type="text" class="form-control form-control-lg border-2" id="title" placeholder="Introduceți un titlu captivant...">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="content" class="form-label fw-bold text-secondary">Conținut Publicație</label>
|
||||
<textarea id="content"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end gap-3">
|
||||
<button class="btn btn-light border btn-lg px-4" onclick="window.close()">Renunță</button>
|
||||
<button class="btn btn-primary btn-lg px-5" id="save-btn" onclick="saveArticle()">Salvează</button>
|
||||
</div>
|
||||
|
||||
<div class="alert mt-4 d-none" id="status-alert" role="alert"></div>
|
||||
</div>
|
||||
|
||||
<!-- EasyMDE JS -->
|
||||
<script src="https://unpkg.com/easymde/dist/easymde.min.js"></script>
|
||||
<script>
|
||||
// Inițializare editor Markdown
|
||||
const easyMDE = new EasyMDE({
|
||||
element: document.getElementById('content'),
|
||||
spellChecker: false,
|
||||
placeholder: "Scrieți conținutul publicației folosind formatarea din bara de sus...",
|
||||
autosave: {
|
||||
enabled: true,
|
||||
uniqueId: "article_editor_autosave_temp",
|
||||
delay: 2000,
|
||||
},
|
||||
toolbar: [
|
||||
"bold", "italic", "strikethrough", "|",
|
||||
"heading-1", "heading-2", "heading-3", "|",
|
||||
"quote", "unordered-list", "ordered-list", "|",
|
||||
"link", "image", "table", "horizontal-rule", "|",
|
||||
"preview", "side-by-side", "fullscreen", "|",
|
||||
"guide"
|
||||
]
|
||||
});
|
||||
|
||||
// Preluare parametri din șablonul Jinja
|
||||
const articleId = "{{ article_id }}";
|
||||
const token = "{{ token }}";
|
||||
|
||||
// Dacă avem ID de articol, înseamnă că suntem în modul editare
|
||||
if (articleId && articleId !== "None") {
|
||||
document.getElementById('editor-title-label').innerText = "Editează Articolul";
|
||||
|
||||
// Încarcă datele articolului de pe server
|
||||
fetch(`/articles/${articleId}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error("Nu s-au putut încărca datele articolului.");
|
||||
return res.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.title) document.getElementById('title').value = data.title;
|
||||
if (data.content) easyMDE.value(data.content);
|
||||
})
|
||||
.catch(err => {
|
||||
showStatus(err.message, "danger");
|
||||
});
|
||||
}
|
||||
|
||||
function showStatus(message, type) {
|
||||
const alertDiv = document.getElementById('status-alert');
|
||||
alertDiv.className = `alert alert-${type} mt-4`;
|
||||
alertDiv.innerText = message;
|
||||
alertDiv.classList.remove('d-none');
|
||||
}
|
||||
|
||||
function saveArticle() {
|
||||
const title = document.getElementById('title').value.trim();
|
||||
const content = easyMDE.value().trim();
|
||||
|
||||
if (!title || !content) {
|
||||
showStatus("Titlul și conținutul sunt obligatorii!", "danger");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = { title, content };
|
||||
const url = (articleId && articleId !== "None") ? `/articles/update/${articleId}` : '/articles/add';
|
||||
const method = (articleId && articleId !== "None") ? 'PUT' : 'POST';
|
||||
|
||||
const saveBtn = document.getElementById('save-btn');
|
||||
saveBtn.disabled = true;
|
||||
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(async res => {
|
||||
const data = await res.json();
|
||||
if (res.status === 200 || res.status === 201) {
|
||||
showStatus("Publicația a fost salvată cu succes! Această fereastră se va închide...", "success");
|
||||
// Șterge salvările automate temporare
|
||||
easyMDE.clearAutosavedValue();
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 1500);
|
||||
} else {
|
||||
throw new Error(data.error || "A apărut o eroare la salvare.");
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
showStatus(err.message, "danger");
|
||||
saveBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user