Add edit modals and image upload for admin dashboard
User Interface Improvements: - Added edit modal for skills with activate/deactivate checkbox - Added edit modal for social links with activate/deactivate checkbox - Skills and social links now default to "active" when created - Better UX with inline editing instead of separate pages Image Upload Feature: - Implemented file upload for project images - Support for png, jpg, jpeg, gif, webp (max 16 MB) - Automatic filename sanitization and timestamp prefixing - Preview of current image in edit mode - Option to upload file OR enter manual URL - Files saved to static/img/ directory Modified Files: - app.py: Added upload configuration (MAX_CONTENT_LENGTH, UPLOAD_FOLDER, ALLOWED_EXTENSIONS) - routes/admin.py: Added save_uploaded_file() helper and file handling in project routes - templates/admin/skills.html: Added edit modal with is_active checkbox - templates/admin/social_links.html: Added edit modal with is_active checkbox - templates/admin/project_form.html: Added file upload input with preview Benefits: - No more "inactive" items when creating new entries - Easy toggle of active/inactive state - Professional image upload with validation - Better user experience overall
This commit is contained in:
5
app.py
5
app.py
@@ -24,6 +24,11 @@ app.config['SQLALCHEMY_DATABASE_URI'] = config.SQLALCHEMY_DATABASE_URI
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = config.SQLALCHEMY_TRACK_MODIFICATIONS
|
||||
app.config['SQLALCHEMY_ECHO'] = config.SQLALCHEMY_ECHO
|
||||
|
||||
# File upload configuration
|
||||
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16 MB max file size
|
||||
app.config['UPLOAD_FOLDER'] = 'static/img'
|
||||
app.config['ALLOWED_EXTENSIONS'] = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
bcrypt.init_app(app)
|
||||
|
||||
@@ -7,13 +7,42 @@
|
||||
Admin dashboard routes for managing portfolio content
|
||||
"""
|
||||
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, current_app
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.utils import secure_filename
|
||||
from models import db, Profile, Skill, Project, ProjectTag, SocialLink
|
||||
import os
|
||||
|
||||
route_admin = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
|
||||
def allowed_file(filename):
|
||||
"""Check if file extension is allowed"""
|
||||
return '.' in filename and \
|
||||
filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']
|
||||
|
||||
|
||||
def save_uploaded_file(file):
|
||||
"""Save uploaded file and return the relative path"""
|
||||
if file and allowed_file(file.filename):
|
||||
filename = secure_filename(file.filename)
|
||||
# Add timestamp to avoid collisions
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_')
|
||||
filename = timestamp + filename
|
||||
|
||||
# Create upload folder if it doesn't exist
|
||||
upload_folder = current_app.config['UPLOAD_FOLDER']
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
|
||||
filepath = os.path.join(upload_folder, filename)
|
||||
file.save(filepath)
|
||||
|
||||
# Return relative path from static folder
|
||||
return f"img/{filename}"
|
||||
return None
|
||||
|
||||
|
||||
@route_admin.route('/')
|
||||
@login_required
|
||||
def dashboard():
|
||||
@@ -134,10 +163,22 @@ def projects_manage():
|
||||
def project_add():
|
||||
"""Add new project"""
|
||||
if request.method == 'POST':
|
||||
# Handle image upload
|
||||
image_url = request.form.get('image_url') # Manual URL input
|
||||
if 'image_file' in request.files:
|
||||
file = request.files['image_file']
|
||||
if file.filename: # Se è stato selezionato un file
|
||||
uploaded_path = save_uploaded_file(file)
|
||||
if uploaded_path:
|
||||
image_url = uploaded_path
|
||||
else:
|
||||
flash('Formato immagine non valido. Usa: png, jpg, jpeg, gif, webp', 'danger')
|
||||
return redirect(url_for('admin.project_add'))
|
||||
|
||||
project = Project(
|
||||
title=request.form.get('title'),
|
||||
description=request.form.get('description'),
|
||||
image_url=request.form.get('image_url'),
|
||||
image_url=image_url,
|
||||
demo_url=request.form.get('demo_url'),
|
||||
github_url=request.form.get('github_url'),
|
||||
display_order=int(request.form.get('display_order', 0)),
|
||||
@@ -180,9 +221,21 @@ def project_edit(project_id):
|
||||
project = Project.query.get_or_404(project_id)
|
||||
|
||||
if request.method == 'POST':
|
||||
# Handle image upload
|
||||
image_url = request.form.get('image_url', project.image_url)
|
||||
if 'image_file' in request.files:
|
||||
file = request.files['image_file']
|
||||
if file.filename: # Se è stato selezionato un file
|
||||
uploaded_path = save_uploaded_file(file)
|
||||
if uploaded_path:
|
||||
image_url = uploaded_path
|
||||
else:
|
||||
flash('Formato immagine non valido. Usa: png, jpg, jpeg, gif, webp', 'danger')
|
||||
return redirect(url_for('admin.project_edit', project_id=project_id))
|
||||
|
||||
project.title = request.form.get('title', project.title)
|
||||
project.description = request.form.get('description', project.description)
|
||||
project.image_url = request.form.get('image_url', project.image_url)
|
||||
project.image_url = image_url
|
||||
project.demo_url = request.form.get('demo_url', project.demo_url)
|
||||
project.github_url = request.form.get('github_url', project.github_url)
|
||||
project.display_order = int(request.form.get('display_order', project.display_order))
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Titolo *</label>
|
||||
<input type="text" class="form-control" id="title" name="title"
|
||||
@@ -18,39 +18,65 @@
|
||||
<textarea class="form-control" id="description" name="description" rows="4" required>{{ project.description if project else '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Immagine del Progetto</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="image_url" class="form-label">URL Immagine</label>
|
||||
<div class="col-md-6">
|
||||
<label for="image_file" class="form-label text-muted small">Upload Immagine</label>
|
||||
<input type="file" class="form-control" id="image_file" name="image_file" accept="image/*">
|
||||
<small class="text-muted">Formati supportati: png, jpg, jpeg, gif, webp (max 16 MB)</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="image_url" class="form-label text-muted small">Oppure inserisci URL manualmente</label>
|
||||
<input type="text" class="form-control" id="image_url" name="image_url"
|
||||
value="{{ project.image_url if project else '' }}" placeholder="img/project.webp">
|
||||
<small class="text-muted">Percorso relativo alla cartella static/</small>
|
||||
</div>
|
||||
</div>
|
||||
{% if project and project.image_url %}
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Immagine attuale:</small><br>
|
||||
<img src="{{ url_for('static', filename=project.image_url) }}" alt="{{ project.title }}" style="max-width: 200px; max-height: 150px;" class="img-thumbnail">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="github_url" class="form-label">URL GitHub</label>
|
||||
<input type="url" class="form-control" id="github_url" name="github_url"
|
||||
value="{{ project.github_url if project else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="demo_url" class="form-label">URL Demo</label>
|
||||
<input type="url" class="form-control" id="demo_url" name="demo_url"
|
||||
value="{{ project.demo_url if project else '' }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="display_order" class="form-label">Ordine Visualizzazione</label>
|
||||
<input type="number" class="form-control" id="display_order" name="display_order"
|
||||
value="{{ project.display_order if project else 0 }}">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="col-md-4 mb-3">
|
||||
<label for="animation_delay" class="form-label">Delay Animazione</label>
|
||||
<input type="text" class="form-control" id="animation_delay" name="animation_delay"
|
||||
value="{{ project.animation_delay if project else '0s' }}" placeholder="0.2s">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="form-check mt-4">
|
||||
<input type="checkbox" class="form-check-input" id="is_published" name="is_published"
|
||||
{% if not project or project.is_published %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_published">
|
||||
Pubblica il progetto
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -61,14 +87,6 @@
|
||||
<small class="text-muted">Formato: Nome:colore, Nome:colore (es: Python:bg-primary, Flask:bg-info)</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="is_published" name="is_published"
|
||||
{% if not project or project.is_published %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_published">
|
||||
Pubblica il progetto
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-gradient">
|
||||
<i class="fas fa-save me-2"></i>Salva
|
||||
|
||||
@@ -19,9 +19,17 @@
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" name="category" placeholder="Categoria">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-1">
|
||||
<input type="number" class="form-control" name="display_order" placeholder="Ordine" value="0">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<div class="form-check mt-2">
|
||||
<input type="checkbox" class="form-check-input" name="is_active" id="is_active_new" checked>
|
||||
<label class="form-check-label" for="is_active_new">
|
||||
Attiva
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-gradient w-100">
|
||||
<i class="fas fa-plus me-2"></i>Aggiungi
|
||||
@@ -63,6 +71,9 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary me-1" data-bs-toggle="modal" data-bs-target="#editModal{{ skill.id }}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form method="POST" action="{{ url_for('admin.skill_delete', skill_id=skill.id) }}" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Sicuro di voler eliminare?')">
|
||||
<i class="fas fa-trash"></i>
|
||||
@@ -70,6 +81,48 @@
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal{{ skill.id }}" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Modifica Competenza</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('admin.skill_edit', skill_id=skill.id) }}">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nome</label>
|
||||
<input type="text" class="form-control" name="name" value="{{ skill.name }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Icona (Font Awesome class)</label>
|
||||
<input type="text" class="form-control" name="icon_class" value="{{ skill.icon_class }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Categoria</label>
|
||||
<input type="text" class="form-control" name="category" value="{{ skill.category or '' }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Ordine di Visualizzazione</label>
|
||||
<input type="number" class="form-control" name="display_order" value="{{ skill.display_order }}">
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" name="is_active" id="is_active_{{ skill.id }}" {% if skill.is_active %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_active_{{ skill.id }}">
|
||||
Competenza Attiva
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-gradient">Salva Modifiche</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('admin.social_link_add') }}" class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" name="platform_name" placeholder="Nome (es. LinkedIn)" required>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<input type="url" class="form-control" name="url" placeholder="URL completo" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
@@ -22,6 +22,14 @@
|
||||
<div class="col-md-1">
|
||||
<input type="number" class="form-control" name="display_order" placeholder="Ordine" value="0">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-check mt-2">
|
||||
<input type="checkbox" class="form-check-input" name="is_active" id="is_active_new" checked>
|
||||
<label class="form-check-label" for="is_active_new">
|
||||
Attivo
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-gradient w-100">
|
||||
<i class="fas fa-plus me-2"></i>Aggiungi
|
||||
@@ -60,6 +68,9 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary me-1" data-bs-toggle="modal" data-bs-target="#editModal{{ link.id }}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<form method="POST" action="{{ url_for('admin.social_link_delete', link_id=link.id) }}" class="d-inline">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Sicuro di voler eliminare?')">
|
||||
<i class="fas fa-trash"></i>
|
||||
@@ -67,6 +78,52 @@
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal{{ link.id }}" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Modifica Link Social</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<form method="POST" action="{{ url_for('admin.social_link_edit', link_id=link.id) }}">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Nome Piattaforma</label>
|
||||
<input type="text" class="form-control" name="platform_name" value="{{ link.platform_name }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="url" class="form-control" name="url" value="{{ link.url }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Icona (Font Awesome class)</label>
|
||||
<input type="text" class="form-control" name="icon_class" value="{{ link.icon_class }}" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Ordine di Visualizzazione</label>
|
||||
<input type="number" class="form-control" name="display_order" value="{{ link.display_order }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Delay Animazione (es. 0.1s)</label>
|
||||
<input type="text" class="form-control" name="animation_delay" value="{{ link.animation_delay }}">
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" name="is_active" id="is_active_{{ link.id }}" {% if link.is_active %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_active_{{ link.id }}">
|
||||
Link Attivo
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
|
||||
<button type="submit" class="btn btn-gradient">Salva Modifiche</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user