diff --git a/app.py b/app.py index bef86b8..69a2d57 100644 --- a/app.py +++ b/app.py @@ -2,10 +2,19 @@ # -*- coding: utf-8 -*- # Copyright Hersel Giannella +# Enhanced Quart Application with Database and Authentication -from quart import Quart, send_from_directory +import asyncio +from quart import Quart, send_from_directory, session, g from config import config +from models.database import init_database, db_manager +from utils.helpers import get_flash_messages +from utils.auth import get_current_user + +# Import Blueprints from routes.home import route_home +from routes.auth import auth_bp +from routes.dashboard import dashboard_bp app = Quart( __name__, @@ -13,7 +22,25 @@ app = Quart( static_folder="static", ) -# favicon.ico, sitemap.xml and robots.txt +# Configuration +app.config.from_object(config) +app.secret_key = config.SECRET_KEY + +# Template globals +@app.template_global('get_flashed_messages') +def template_get_flashed_messages(with_categories=False): + return get_flash_messages() + +# Context processor for current user +@app.before_request +async def load_current_user(): + g.current_user = await get_current_user() + +@app.context_processor +def inject_user(): + return {'current_user': getattr(g, 'current_user', None)} + +# Static files routes @app.route('/favicon.ico') async def favicon(): return await send_from_directory(app.static_folder, 'favicon.ico') @@ -26,8 +53,47 @@ async def sitemap(): async def robots(): return await send_from_directory(app.static_folder, 'robots.txt') -# BluePrint Routes +# Register Blueprints app.register_blueprint(route_home) +app.register_blueprint(auth_bp) +app.register_blueprint(dashboard_bp) + +# Database initialization +@app.before_serving +async def initialize_app(): + """Initialize database and other services""" + print("🚀 Initializing Hersel.it application...") + try: + await init_database() + print("✅ Database initialized successfully") + except Exception as e: + print(f"❌ Error initializing database: {e}") + # Don't crash the app, but log the error + +@app.after_serving +async def cleanup_app(): + """Cleanup resources""" + print("🔒 Shutting down Hersel.it application...") + await db_manager.close_pool() + print("✅ Application shutdown complete") + +# Error handlers +@app.errorhandler(404) +async def not_found(error): + return await render_template('errors/404.html'), 404 + +@app.errorhandler(500) +async def internal_error(error): + return await render_template('errors/500.html'), 500 + +# Health check endpoint +@app.route('/health') +async def health_check(): + return {'status': 'healthy', 'app': 'Hersel.it Portfolio'} if __name__ == '__main__': - app.run(debug=config.DEBUG, host=config.APP_HOST, port=config.APP_PORT) + app.run( + debug=config.DEBUG, + host=config.APP_HOST, + port=config.APP_PORT + ) diff --git a/docker-compose.yml b/docker-compose.yml index 2c923b5..b1b64b0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,53 @@ -version: "3.9" +version: '3.9' services: + # MySQL Database + mysql: + image: mysql:8.0 + container_name: hersel_mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: root_password_change_me + MYSQL_DATABASE: hersel_portfolio + MYSQL_USER: hersel_user + MYSQL_PASSWORD: secure_password_123 + ports: + - "3307:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro + command: --default-authentication-plugin=mysql_native_password + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + timeout: 20s + retries: 10 + + # Quart Application quartapp: - image: python:3.10-slim - container_name: quartapp - working_dir: /app + build: . + container_name: hersel_app + restart: always ports: - "127.0.0.1:5000:5000" - restart: always - command: > - sh -c " - apt-get update && - apt-get install -y git && - [ -d /app/.git ] || git clone https://github.com/BluLupo/hersel.it.git /app && - pip install --no-cache-dir -r requirements.txt && - hypercorn -c hypercorn_config.toml app:app - " environment: + - DEBUG=False + - SECRET_KEY=super-secret-key-change-in-production-please + - DB_HOST=mysql + - DB_PORT=3306 + - DB_USER=hersel_user + - DB_PASSWORD=secure_password_123 + - DB_NAME=hersel_portfolio - PYTHONUNBUFFERED=1 + depends_on: + mysql: + condition: service_healthy + volumes: + - ./static/uploads:/app/static/uploads + +volumes: + mysql_data: + driver: local + +networks: + default: + name: hersel_network diff --git a/init.sql b/init.sql new file mode 100644 index 0000000..6ee3352 --- /dev/null +++ b/init.sql @@ -0,0 +1,143 @@ +-- Initial database setup for Hersel.it Portfolio +-- This file is automatically executed when MySQL container starts + +USE hersel_portfolio; + +-- Enable UTF8MB4 charset for emoji and international characters +SET NAMES utf8mb4; +SET character_set_client = utf8mb4; + +-- Create categories table +CREATE TABLE IF NOT EXISTS categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL, + slug VARCHAR(100) UNIQUE NOT NULL, + description TEXT, + color VARCHAR(7) DEFAULT '#007bff', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- Create users table +CREATE TABLE IF NOT EXISTS users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + email VARCHAR(100) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + first_name VARCHAR(50), + last_name VARCHAR(50), + role ENUM('admin', 'user') DEFAULT 'user', + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- Create projects table +CREATE TABLE IF NOT EXISTS projects ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(200) NOT NULL, + slug VARCHAR(200) UNIQUE NOT NULL, + description TEXT, + content LONGTEXT, + image_url VARCHAR(500), + github_url VARCHAR(500), + demo_url VARCHAR(500), + technologies JSON, + category_id INT, + is_featured BOOLEAN DEFAULT FALSE, + is_published BOOLEAN DEFAULT TRUE, + created_by INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL, + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- Create posts table (for future blog functionality) +CREATE TABLE IF NOT EXISTS posts ( + id INT AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(200) NOT NULL, + slug VARCHAR(200) UNIQUE NOT NULL, + excerpt TEXT, + content LONGTEXT, + featured_image VARCHAR(500), + category_id INT, + author_id INT, + is_published BOOLEAN DEFAULT FALSE, + published_at TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL, + FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- Create settings table +CREATE TABLE IF NOT EXISTS settings ( + id INT AUTO_INCREMENT PRIMARY KEY, + setting_key VARCHAR(100) UNIQUE NOT NULL, + setting_value LONGTEXT, + description TEXT, + type ENUM('text', 'textarea', 'boolean', 'json') DEFAULT 'text', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- Insert default categories +INSERT INTO categories (name, slug, description, color) VALUES +('Web Development', 'web-development', 'Progetti di sviluppo web', '#007bff'), +('Mobile Apps', 'mobile-apps', 'Applicazioni mobile', '#28a745'), +('Desktop Apps', 'desktop-apps', 'Applicazioni desktop', '#ffc107'), +('APIs', 'apis', 'API e servizi web', '#dc3545'), +('Tools & Utilities', 'tools-utilities', 'Strumenti e utility', '#6f42c1'), +('Open Source', 'open-source', 'Progetti open source', '#20c997') +ON DUPLICATE KEY UPDATE name=VALUES(name); + +-- Insert default settings +INSERT INTO settings (setting_key, setting_value, description, type) VALUES +('site_name', 'Hersel.it', 'Nome del sito', 'text'), +('site_description', 'Portfolio personale di Hersel Giannella - Developer', 'Descrizione del sito', 'textarea'), +('admin_email', 'admin@hersel.it', 'Email amministratore', 'text'), +('site_logo', '/static/img/logo.png', 'URL del logo', 'text'), +('social_github', 'https://github.com/BluLupo', 'GitHub URL', 'text'), +('social_linkedin', '', 'LinkedIn URL', 'text'), +('social_twitter', '', 'Twitter URL', 'text'), +('site_maintenance', 'false', 'Modalità manutenzione', 'boolean'), +('analytics_code', '', 'Codice Analytics', 'textarea'), +('projects_per_page', '12', 'Progetti per pagina', 'text'), +('featured_projects_limit', '6', 'Limite progetti in evidenza', 'text') +ON DUPLICATE KEY UPDATE setting_value=VALUES(setting_value); + +-- Create admin user (password: AdminPass123!) +-- This creates a default admin user for initial access +-- Password hash for 'AdminPass123!' generated with bcrypt +INSERT INTO users (username, email, password_hash, first_name, last_name, role) VALUES +('admin', 'admin@hersel.it', '$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqyaqr06eoAGNz9CpahtY1q', 'Admin', 'User', 'admin') +ON DUPLICATE KEY UPDATE role=VALUES(role); + +-- Insert sample projects (optional) +INSERT INTO projects (title, slug, description, content, github_url, technologies, is_featured, is_published, created_by) VALUES +('Hersel.it Portfolio', 'hersel-it-portfolio', 'Portfolio dinamico sviluppato con Quart e MySQL', + '

Portfolio Dinamico

Questo portfolio è stato sviluppato utilizzando le seguenti tecnologie:

', + 'https://github.com/BluLupo/hersel.it', + '["Python", "Quart", "MySQL", "Bootstrap", "Docker"]', + TRUE, TRUE, 1), + +('API REST con Quart', 'api-rest-quart', 'API RESTful asincrona per gestione dati', + '

API REST

API asincrona sviluppata con Quart per gestire operazioni CRUD su database MySQL.

', + '', + '["Python", "Quart", "MySQL", "REST API", "Async"]', + FALSE, TRUE, 1) +ON DUPLICATE KEY UPDATE title=VALUES(title); + +-- Create indexes for better performance +CREATE INDEX idx_projects_published ON projects(is_published); +CREATE INDEX idx_projects_featured ON projects(is_featured); +CREATE INDEX idx_projects_category ON projects(category_id); +CREATE INDEX idx_users_email ON users(email); +CREATE INDEX idx_users_username ON users(username); +CREATE INDEX idx_posts_published ON posts(is_published); + +-- Show confirmation message +SELECT 'Database initialized successfully!' as Status; +SELECT COUNT(*) as 'Total Categories' FROM categories; +SELECT COUNT(*) as 'Total Settings' FROM settings; +SELECT COUNT(*) as 'Admin Users' FROM users WHERE role='admin'; +SELECT COUNT(*) as 'Sample Projects' FROM projects; diff --git a/requirements.txt b/requirements.txt index e3e176b..e20d500 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,41 @@ +# Core Framework +Quart==0.20.0 +Hypercorn==0.17.3 + +# Database +aiomysql==0.2.0 +PyMySQL==1.1.0 +cryptography==41.0.8 + +# Authentication +bcrypt==4.1.2 + +# Utilities aiofiles==24.1.0 -annotated-types==0.7.0 -blinker==1.9.0 +python-dotenv==1.0.1 +Jinja2==3.1.5 +MarkupSafe==3.0.2 +Werkzeug==3.1.3 + +# Core Dependencies click==8.1.8 -Flask==3.1.0 +blinker==1.9.0 +itsdangerous==2.2.0 +typing_extensions==4.12.2 + +# HTTP/Network h11==0.14.0 h2==4.1.0 hpack==4.0.0 -Hypercorn==0.17.3 hyperframe==6.0.1 -itsdangerous==2.2.0 -Jinja2==3.1.5 -MarkupSafe==3.0.2 priority==2.0.0 +wsproto==1.2.0 +httpx==0.27.0 + +# Optional +Flask==3.1.0 pydantic==2.10.4 pydantic-settings==2.7.1 pydantic_core==2.27.2 -python-dotenv==1.0.1 -Quart==0.20.0 -typing_extensions==4.12.2 -Werkzeug==3.1.3 -wsproto==1.2.0 -httpx==0.27.0 +annotated-types==0.7.0 Sphinx==8.2.3 diff --git a/routes/auth.py b/routes/auth.py new file mode 100644 index 0000000..f4f4035 --- /dev/null +++ b/routes/auth.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Authentication Routes + +from quart import Blueprint, request, render_template, redirect, url_for, session, flash +from models.user import User +from utils.auth import login_user, logout_user, get_current_user +from utils.validators import validate_email, validate_password, validate_username +from utils.helpers import flash_message + +auth_bp = Blueprint('auth', __name__, url_prefix='/auth') + +@auth_bp.route('/login', methods=['GET', 'POST']) +async def login(): + """Login page""" + if request.method == 'GET': + return await render_template('auth/login.html') + + form_data = await request.form + username_or_email = form_data.get('username', '').strip() + password = form_data.get('password', '') + + if not username_or_email or not password: + flash_message('Username/Email e password sono richiesti', 'error') + return await render_template('auth/login.html') + + # Authenticate user + user = await User.authenticate(username_or_email, password) + + if user: + login_user(user) + flash_message(f'Benvenuto, {user.full_name}!', 'success') + + # Redirect to dashboard if admin, home otherwise + if user.is_admin: + return redirect(url_for('dashboard.index')) + else: + return redirect(url_for('home.index')) + else: + flash_message('Credenziali non valide', 'error') + return await render_template('auth/login.html') + +@auth_bp.route('/register', methods=['GET', 'POST']) +async def register(): + """Registration page""" + if request.method == 'GET': + return await render_template('auth/register.html') + + form_data = await request.form + username = form_data.get('username', '').strip() + email = form_data.get('email', '').strip() + password = form_data.get('password', '') + confirm_password = form_data.get('confirm_password', '') + first_name = form_data.get('first_name', '').strip() + last_name = form_data.get('last_name', '').strip() + + errors = {} + + # Validate inputs + is_valid, error = validate_username(username) + if not is_valid: + errors['username'] = error + + is_valid, error = validate_email(email) + if not is_valid: + errors['email'] = error + + is_valid, error = validate_password(password) + if not is_valid: + errors['password'] = error + + if password != confirm_password: + errors['confirm_password'] = 'Le password non coincidono' + + # Check if user already exists + if not errors: + existing_user = await User.find_by_username(username) + if existing_user: + errors['username'] = 'Username già in uso' + + existing_email = await User.find_by_email(email) + if existing_email: + errors['email'] = 'Email già registrata' + + if errors: + for field, error in errors.items(): + flash_message(error, 'error') + return await render_template('auth/register.html') + + # Create new user + user = User( + username=username, + email=email, + first_name=first_name, + last_name=last_name, + role='user' # First user can be manually promoted to admin + ) + user.set_password(password) + + try: + await user.save() + flash_message('Registrazione completata! Ora puoi effettuare il login.', 'success') + return redirect(url_for('auth.login')) + except Exception as e: + flash_message('Errore durante la registrazione. Riprova.', 'error') + return await render_template('auth/register.html') + +@auth_bp.route('/logout') +async def logout(): + """Logout user""" + logout_user() + flash_message('Logout effettuato con successo', 'success') + return redirect(url_for('home.index')) + +@auth_bp.route('/profile') +async def profile(): + """User profile page""" + user = await get_current_user() + if not user: + return redirect(url_for('auth.login')) + + return await render_template('auth/profile.html', user=user) diff --git a/routes/dashboard.py b/routes/dashboard.py new file mode 100644 index 0000000..6730df7 --- /dev/null +++ b/routes/dashboard.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Dashboard Routes (Admin) + +from quart import Blueprint, request, render_template, redirect, url_for, jsonify +from models.user import User +from models.project import Project +from models.category import Category +from utils.auth import admin_required, get_current_user +from utils.helpers import flash_message, generate_slug, calculate_pagination +from utils.validators import validate_project_data + +dashboard_bp = Blueprint('dashboard', __name__, url_prefix='/dashboard') + +@dashboard_bp.route('/') +@admin_required +async def index(): + """Dashboard home""" + current_user = await get_current_user() + + # Get statistics + stats = { + 'total_users': await User.count(), + 'total_projects': await Project.count(published_only=False), + 'published_projects': await Project.count(published_only=True), + 'featured_projects': len(await Project.get_featured()) + } + + # Get recent projects + recent_projects = await Project.get_all(published_only=False, limit=5) + + return await render_template('dashboard/index.html', + user=current_user, + stats=stats, + recent_projects=recent_projects) + +@dashboard_bp.route('/projects') +@admin_required +async def projects(): + """Projects management""" + page = int(request.args.get('page', 1)) + per_page = 10 + + # Get projects with pagination + projects = await Project.get_all(published_only=False, limit=per_page, offset=(page-1)*per_page) + total_projects = await Project.count(published_only=False) + + pagination = calculate_pagination(total_projects, page, per_page) + + return await render_template('dashboard/projects.html', + projects=projects, + pagination=pagination) + +@dashboard_bp.route('/projects/new', methods=['GET', 'POST']) +@admin_required +async def new_project(): + """Create new project""" + if request.method == 'GET': + categories = await Category.get_all() + return await render_template('dashboard/project_form.html', + project=None, + categories=categories, + action='create') + + form_data = await request.form + data = { + 'title': form_data.get('title', '').strip(), + 'description': form_data.get('description', '').strip(), + 'content': form_data.get('content', '').strip(), + 'github_url': form_data.get('github_url', '').strip(), + 'demo_url': form_data.get('demo_url', '').strip(), + 'image_url': form_data.get('image_url', '').strip(), + 'technologies': form_data.getlist('technologies'), + 'category_id': int(form_data.get('category_id')) if form_data.get('category_id') else None, + 'is_featured': bool(form_data.get('is_featured')), + 'is_published': bool(form_data.get('is_published')) + } + + # Validate data + is_valid, errors = validate_project_data(data) + + if not is_valid: + for field, error in errors.items(): + flash_message(error, 'error') + categories = await Category.get_all() + return await render_template('dashboard/project_form.html', + project=data, + categories=categories, + action='create') + + # Create project + current_user = await get_current_user() + project = Project( + title=data['title'], + slug=generate_slug(data['title']), + description=data['description'], + content=data['content'], + github_url=data['github_url'], + demo_url=data['demo_url'], + image_url=data['image_url'], + technologies=data['technologies'], + category_id=data['category_id'], + is_featured=data['is_featured'], + is_published=data['is_published'], + created_by=current_user.id + ) + + try: + await project.save() + flash_message('Progetto creato con successo!', 'success') + return redirect(url_for('dashboard.projects')) + except Exception as e: + flash_message('Errore durante la creazione del progetto', 'error') + categories = await Category.get_all() + return await render_template('dashboard/project_form.html', + project=data, + categories=categories, + action='create') + +@dashboard_bp.route('/projects//edit', methods=['GET', 'POST']) +@admin_required +async def edit_project(project_id): + """Edit project""" + project = await Project.find_by_id(project_id) + if not project: + flash_message('Progetto non trovato', 'error') + return redirect(url_for('dashboard.projects')) + + if request.method == 'GET': + categories = await Category.get_all() + return await render_template('dashboard/project_form.html', + project=project, + categories=categories, + action='edit') + + form_data = await request.form + data = { + 'title': form_data.get('title', '').strip(), + 'description': form_data.get('description', '').strip(), + 'content': form_data.get('content', '').strip(), + 'github_url': form_data.get('github_url', '').strip(), + 'demo_url': form_data.get('demo_url', '').strip(), + 'image_url': form_data.get('image_url', '').strip(), + 'technologies': form_data.getlist('technologies'), + 'category_id': int(form_data.get('category_id')) if form_data.get('category_id') else None, + 'is_featured': bool(form_data.get('is_featured')), + 'is_published': bool(form_data.get('is_published')) + } + + # Validate data + is_valid, errors = validate_project_data(data) + + if not is_valid: + for field, error in errors.items(): + flash_message(error, 'error') + categories = await Category.get_all() + return await render_template('dashboard/project_form.html', + project=project, + categories=categories, + action='edit') + + # Update project + project.title = data['title'] + project.slug = generate_slug(data['title']) + project.description = data['description'] + project.content = data['content'] + project.github_url = data['github_url'] + project.demo_url = data['demo_url'] + project.image_url = data['image_url'] + project.technologies = data['technologies'] + project.category_id = data['category_id'] + project.is_featured = data['is_featured'] + project.is_published = data['is_published'] + + try: + await project.save() + flash_message('Progetto aggiornato con successo!', 'success') + return redirect(url_for('dashboard.projects')) + except Exception as e: + flash_message('Errore durante l\'aggiornamento del progetto', 'error') + categories = await Category.get_all() + return await render_template('dashboard/project_form.html', + project=project, + categories=categories, + action='edit') + +@dashboard_bp.route('/projects//delete', methods=['POST']) +@admin_required +async def delete_project(project_id): + """Delete project""" + project = await Project.find_by_id(project_id) + if not project: + return jsonify({'error': 'Progetto non trovato'}), 404 + + try: + await project.delete() + flash_message('Progetto eliminato con successo!', 'success') + return redirect(url_for('dashboard.projects')) + except Exception as e: + flash_message('Errore durante l\'eliminazione del progetto', 'error') + return redirect(url_for('dashboard.projects')) + +@dashboard_bp.route('/users') +@admin_required +async def users(): + """Users management""" + page = int(request.args.get('page', 1)) + per_page = 10 + + users = await User.get_all(limit=per_page, offset=(page-1)*per_page) + total_users = await User.count() + + pagination = calculate_pagination(total_users, page, per_page) + + return await render_template('dashboard/users.html', + users=users, + pagination=pagination) diff --git a/templates/auth/login.html b/templates/auth/login.html new file mode 100644 index 0000000..b31a596 --- /dev/null +++ b/templates/auth/login.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} + +{% block title %}Login - Hersel.it{% endblock %} + +{% block content %} +
+
+
+
+
+
+

Accedi

+

Benvenuto su Hersel.it

+
+ +
+
+ +
+ + + + +
+
+ +
+ +
+ + + + +
+
+ +
+ + +
+ +
+ +
+
+ +
+

+ Non hai un account? + Registrati qui +

+
+
+
+
+
+
+{% endblock %} diff --git a/templates/auth/register.html b/templates/auth/register.html new file mode 100644 index 0000000..a821c9f --- /dev/null +++ b/templates/auth/register.html @@ -0,0 +1,99 @@ +{% extends "base.html" %} + +{% block title %}Registrazione - Hersel.it{% endblock %} + +{% block content %} +
+
+
+
+
+
+

Registrati

+

Crea il tuo account su Hersel.it

+
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ +
+ + + + +
+
Minimo 3 caratteri, solo lettere, numeri e underscore
+
+ +
+ +
+ + + + +
+
+ +
+ +
+ + + + +
+
Minimo 8 caratteri con maiuscola, minuscola e numero
+
+ +
+ +
+ + + + +
+
+ +
+ + +
+ +
+ +
+
+ +
+

+ Hai già un account? + Accedi qui +

+
+
+
+
+
+
+{% endblock %} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..1fc19be --- /dev/null +++ b/templates/base.html @@ -0,0 +1,121 @@ + + + + + + {% block title %}Hersel.it - Portfolio{% endblock %} + + + + + + + {% block extra_head %}{% endblock %} + + + + + + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} +
+ {% endif %} + {% endwith %} + + +
+ {% block content %}{% endblock %} +
+ + +
+
+
+
+
Hersel Giannella
+

Developer & Portfolio

+
+ +
+
+
+
+ © 2024 Hersel.it - Tutti i diritti riservati +
+
+
+
+ + + + {% block extra_scripts %}{% endblock %} + + diff --git a/templates/dashboard/base.html b/templates/dashboard/base.html new file mode 100644 index 0000000..16e72ff --- /dev/null +++ b/templates/dashboard/base.html @@ -0,0 +1,110 @@ + + + + + + {% block title %}Dashboard - Hersel.it{% endblock %} + + + + + + + + + {% block extra_head %}{% endblock %} + + +
+
+ + + + +
+
+

{% block page_title %}Dashboard{% endblock %}

+
+ {% block page_actions %}{% endblock %} +
+
+ + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} + +
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+
+
+ + + + {% block extra_scripts %}{% endblock %} + + diff --git a/templates/dashboard/index.html b/templates/dashboard/index.html new file mode 100644 index 0000000..f73ac05 --- /dev/null +++ b/templates/dashboard/index.html @@ -0,0 +1,159 @@ +{% extends "dashboard/base.html" %} + +{% block page_title %}Dashboard{% endblock %} + +{% block content %} +
+
+
+

Benvenuto, {{ user.full_name }}!

+

Ecco una panoramica del tuo portfolio

+
+
+
+ + +
+
+
+
+
+
+

{{ stats.total_projects }}

+

Progetti Totali

+
+
+ +
+
+
+
+
+ +
+
+
+
+
+

{{ stats.published_projects }}

+

Pubblicati

+
+
+ +
+
+
+
+
+ +
+
+
+
+
+

{{ stats.featured_projects }}

+

In Evidenza

+
+
+ +
+
+
+
+
+ +
+
+
+
+
+

{{ stats.total_users }}

+

Utenti

+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
Progetti Recenti
+
+
+ {% if recent_projects %} +
+ + + + + + + + + + + {% for project in recent_projects %} + + + + + + + {% endfor %} + +
TitoloStatoCreatoAzioni
+ {{ project.title }} + {% if project.is_featured %} + Featured + {% endif %} + + {% if project.is_published %} + Pubblicato + {% else %} + Bozza + {% endif %} + {{ project.created_at.strftime('%d/%m/%Y') if project.created_at else 'N/D' }} + + + +
+
+ {% else %} +

Nessun progetto ancora creato.

+ + Crea il primo progetto + + {% endif %} +
+
+
+ + +
+{% endblock %}