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:
Claude
2025-11-13 15:29:10 +00:00
parent aa2c704bfb
commit 425e66a473
5 changed files with 211 additions and 25 deletions

5
app.py
View File

@@ -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_TRACK_MODIFICATIONS'] = config.SQLALCHEMY_TRACK_MODIFICATIONS
app.config['SQLALCHEMY_ECHO'] = config.SQLALCHEMY_ECHO 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 # Initialize extensions
db.init_app(app) db.init_app(app)
bcrypt.init_app(app) bcrypt.init_app(app)

View File

@@ -7,13 +7,42 @@
Admin dashboard routes for managing portfolio content 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 flask_login import login_required, current_user
from werkzeug.utils import secure_filename
from models import db, Profile, Skill, Project, ProjectTag, SocialLink from models import db, Profile, Skill, Project, ProjectTag, SocialLink
import os
route_admin = Blueprint('admin', __name__, url_prefix='/admin') 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('/') @route_admin.route('/')
@login_required @login_required
def dashboard(): def dashboard():
@@ -134,10 +163,22 @@ def projects_manage():
def project_add(): def project_add():
"""Add new project""" """Add new project"""
if request.method == 'POST': 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( project = Project(
title=request.form.get('title'), title=request.form.get('title'),
description=request.form.get('description'), description=request.form.get('description'),
image_url=request.form.get('image_url'), image_url=image_url,
demo_url=request.form.get('demo_url'), demo_url=request.form.get('demo_url'),
github_url=request.form.get('github_url'), github_url=request.form.get('github_url'),
display_order=int(request.form.get('display_order', 0)), 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) project = Project.query.get_or_404(project_id)
if request.method == 'POST': 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.title = request.form.get('title', project.title)
project.description = request.form.get('description', project.description) 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.demo_url = request.form.get('demo_url', project.demo_url)
project.github_url = request.form.get('github_url', project.github_url) project.github_url = request.form.get('github_url', project.github_url)
project.display_order = int(request.form.get('display_order', project.display_order)) project.display_order = int(request.form.get('display_order', project.display_order))

View File

@@ -6,7 +6,7 @@
{% block content %} {% block content %}
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<form method="POST"> <form method="POST" enctype="multipart/form-data">
<div class="mb-3"> <div class="mb-3">
<label for="title" class="form-label">Titolo *</label> <label for="title" class="form-label">Titolo *</label>
<input type="text" class="form-control" id="title" name="title" <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> <textarea class="form-control" id="description" name="description" rows="4" required>{{ project.description if project else '' }}</textarea>
</div> </div>
<div class="row"> <div class="mb-3">
<div class="col-md-6 mb-3"> <label class="form-label">Immagine del Progetto</label>
<label for="image_url" class="form-label">URL Immagine</label> <div class="row">
<input type="text" class="form-control" id="image_url" name="image_url" <div class="col-md-6">
value="{{ project.image_url if project else '' }}" placeholder="img/project.webp"> <label for="image_file" class="form-label text-muted small">Upload Immagine</label>
<small class="text-muted">Percorso relativo alla cartella static/</small> <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> </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"> <div class="col-md-6 mb-3">
<label for="github_url" class="form-label">URL GitHub</label> <label for="github_url" class="form-label">URL GitHub</label>
<input type="url" class="form-control" id="github_url" name="github_url" <input type="url" class="form-control" id="github_url" name="github_url"
value="{{ project.github_url if project else '' }}"> value="{{ project.github_url if project else '' }}">
</div> </div>
</div>
<div class="row">
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<label for="demo_url" class="form-label">URL Demo</label> <label for="demo_url" class="form-label">URL Demo</label>
<input type="url" class="form-control" id="demo_url" name="demo_url" <input type="url" class="form-control" id="demo_url" name="demo_url"
value="{{ project.demo_url if project else '' }}"> value="{{ project.demo_url if project else '' }}">
</div> </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> <label for="display_order" class="form-label">Ordine Visualizzazione</label>
<input type="number" class="form-control" id="display_order" name="display_order" <input type="number" class="form-control" id="display_order" name="display_order"
value="{{ project.display_order if project else 0 }}"> value="{{ project.display_order if project else 0 }}">
</div> </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> <label for="animation_delay" class="form-label">Delay Animazione</label>
<input type="text" class="form-control" id="animation_delay" name="animation_delay" <input type="text" class="form-control" id="animation_delay" name="animation_delay"
value="{{ project.animation_delay if project else '0s' }}" placeholder="0.2s"> value="{{ project.animation_delay if project else '0s' }}" placeholder="0.2s">
</div> </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>
<div class="mb-3"> <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> <small class="text-muted">Formato: Nome:colore, Nome:colore (es: Python:bg-primary, Flask:bg-info)</small>
</div> </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"> <div class="d-flex gap-2">
<button type="submit" class="btn btn-gradient"> <button type="submit" class="btn btn-gradient">
<i class="fas fa-save me-2"></i>Salva <i class="fas fa-save me-2"></i>Salva

View File

@@ -19,9 +19,17 @@
<div class="col-md-2"> <div class="col-md-2">
<input type="text" class="form-control" name="category" placeholder="Categoria"> <input type="text" class="form-control" name="category" placeholder="Categoria">
</div> </div>
<div class="col-md-2"> <div class="col-md-1">
<input type="number" class="form-control" name="display_order" placeholder="Ordine" value="0"> <input type="number" class="form-control" name="display_order" placeholder="Ordine" value="0">
</div> </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"> <div class="col-md-2">
<button type="submit" class="btn btn-gradient w-100"> <button type="submit" class="btn btn-gradient w-100">
<i class="fas fa-plus me-2"></i>Aggiungi <i class="fas fa-plus me-2"></i>Aggiungi
@@ -63,6 +71,9 @@
{% endif %} {% endif %}
</td> </td>
<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"> <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?')"> <button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Sicuro di voler eliminare?')">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
@@ -70,6 +81,48 @@
</form> </form>
</td> </td>
</tr> </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 %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

@@ -10,10 +10,10 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST" action="{{ url_for('admin.social_link_add') }}" class="row g-3"> <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> <input type="text" class="form-control" name="platform_name" placeholder="Nome (es. LinkedIn)" required>
</div> </div>
<div class="col-md-4"> <div class="col-md-3">
<input type="url" class="form-control" name="url" placeholder="URL completo" required> <input type="url" class="form-control" name="url" placeholder="URL completo" required>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
@@ -22,6 +22,14 @@
<div class="col-md-1"> <div class="col-md-1">
<input type="number" class="form-control" name="display_order" placeholder="Ordine" value="0"> <input type="number" class="form-control" name="display_order" placeholder="Ordine" value="0">
</div> </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"> <div class="col-md-2">
<button type="submit" class="btn btn-gradient w-100"> <button type="submit" class="btn btn-gradient w-100">
<i class="fas fa-plus me-2"></i>Aggiungi <i class="fas fa-plus me-2"></i>Aggiungi
@@ -60,6 +68,9 @@
{% endif %} {% endif %}
</td> </td>
<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"> <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?')"> <button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Sicuro di voler eliminare?')">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
@@ -67,6 +78,52 @@
</form> </form>
</td> </td>
</tr> </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 %} {% endfor %}
</tbody> </tbody>
</table> </table>