Files
yogibook_aury_new/public/userdetails.php
T
2026-10-07 16:42:59 +02:00

1132 lines
50 KiB
PHP

<?php
require_once('include/headscript.php');
/**
* Profilo utente: modifica dati personali (email esclusa) e cambio password.
* Connessione unica PDO (singleton). $iduserlogin dalla sessione autenticata.
*/
$pdo = DBHandlerSelect::getInstance()->getConnection();
$iduserlogin = (int) $iduserlogin;
if (!function_exists('e')) {
function e($value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
}
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
if (empty($_SESSION['csrf_profile'])) {
$_SESSION['csrf_profile'] = bin2hex(random_bytes(32));
}
$csrf = $_SESSION['csrf_profile'];
$profileFeedback = null; // ['type' => 'success'|'error', 'messages' => [...]]
$passwordFeedback = null;
$avatarFeedback = null;
$postedProfile = null; // valori reinviati in caso di errore
/* Avatar */
$avatarDir = 'upload/users/';
$avatarDefault = 'meditation.png';
$avatarMaxBytes = 5 * 1024 * 1024; // 5 MB
$avatarSide = 400; // lato in px dell'immagine salvata
/** Solo i file creati da questa pagina (nome casuale) possono essere cancellati dal disco */
function isOwnAvatarFile($name): bool
{
return is_string($name) && preg_match('/^[a-f0-9]{32}\.(jpg|png|webp)$/', $name) === 1;
}
/* -------------------------------------------------------------------------
* Dati di supporto
* ---------------------------------------------------------------------- */
function loadUser(PDO $pdo, int $id)
{
$stmt = $pdo->prepare(
"SELECT id, email, password, first_name, last_name, phone, address,
country_id, birthday, avatar, created_at
FROM auth_users
WHERE id = :id
LIMIT 1"
);
$stmt->execute([':id' => $id]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
$user = loadUser($pdo, $iduserlogin);
if (!$user) {
http_response_code(403);
exit('Utente non trovato.');
}
// Elenco paesi (se la tabella non è leggibile il campo viene nascosto)
$countries = [];
try {
$q = $pdo->query("SELECT id, name FROM auth_countries ORDER BY name");
$countries = $q ? $q->fetchAll(PDO::FETCH_KEY_PAIR) : [];
} catch (Throwable $ex) {
$countries = [];
}
/* -------------------------------------------------------------------------
* Salvataggio (POST)
* ---------------------------------------------------------------------- */
$action = $_POST['action'] ?? '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $action !== '') {
if (!hash_equals($csrf, (string) ($_POST['csrf'] ?? ''))) {
$fb = ['type' => 'error', 'messages' => ['La sessione è scaduta. Ricarica la pagina e riprova.']];
if ($action === 'password') {
$passwordFeedback = $fb;
} elseif ($action === 'avatar' || $action === 'avatar_remove') {
$avatarFeedback = $fb;
} else {
$profileFeedback = $fb;
}
} elseif ($action === 'profile') {
/* ---------- Dati personali ---------- */
$firstName = trim($_POST['first_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$address = trim($_POST['address'] ?? '');
$birthdayInput = trim($_POST['birthday'] ?? '');
$countryInput = trim((string) ($_POST['country_id'] ?? ''));
$errors = [];
if ($firstName === '') {
$errors[] = 'Il nome è obbligatorio.';
}
if ($lastName === '') {
$errors[] = 'Il cognome è obbligatorio.';
}
foreach (['nome' => $firstName, 'cognome' => $lastName, 'indirizzo' => $address] as $label => $val) {
if (mb_strlen($val) > 191) {
$errors[] = "Il campo $label è troppo lungo (max 191 caratteri).";
}
}
// Telefono facoltativo
if ($phone !== '' && !preg_match('/^[0-9+\s().\/-]{6,30}$/', $phone)) {
$errors[] = 'Il numero di telefono non è valido.';
}
// Data di nascita facoltativa, GG/MM/AAAA
$birthdayDb = null;
if ($birthdayInput !== '') {
$bObj = DateTime::createFromFormat('!d/m/Y', $birthdayInput);
$valid = $bObj && $bObj->format('d/m/Y') === $birthdayInput;
if (!$valid) {
$errors[] = 'La data di nascita non è valida. Usa il formato GG/MM/AAAA.';
} elseif ($bObj > new DateTime('today') || (int) $bObj->format('Y') < 1900) {
$errors[] = 'La data di nascita non è plausibile.';
} else {
$birthdayDb = $bObj->format('Y-m-d');
}
}
// Paese: se l'elenco non è disponibile si mantiene il valore attuale
if (empty($countries)) {
$countryId = $user['country_id'];
} elseif ($countryInput === '') {
$countryId = null;
} elseif (isset($countries[(int) $countryInput])) {
$countryId = (int) $countryInput;
} else {
$errors[] = 'Il paese selezionato non è valido.';
$countryId = $user['country_id'];
}
if (empty($errors)) {
$sql = "UPDATE auth_users SET
first_name = :first_name,
last_name = :last_name,
phone = :phone,
address = :address,
birthday = :birthday,
country_id = :country_id,
updated_at = NOW()
WHERE id = :id";
$stmt = $pdo->prepare($sql);
$ok = $stmt->execute([
':first_name' => $firstName,
':last_name' => $lastName,
':phone' => $phone !== '' ? $phone : null,
':address' => $address !== '' ? $address : null,
':birthday' => $birthdayDb,
':country_id' => $countryId,
':id' => $iduserlogin,
]);
if ($ok) {
$profileFeedback = ['type' => 'success', 'messages' => ['Modifiche salvate.']];
$user = loadUser($pdo, $iduserlogin);
$firstname = $user['first_name']; // aggiorna il saluto nella topbar/pagina
} else {
$profileFeedback = ['type' => 'error', 'messages' => ['Salvataggio non riuscito. Riprova.']];
}
} else {
$profileFeedback = ['type' => 'error', 'messages' => $errors];
$postedProfile = array_merge($user, [
'first_name' => $firstName,
'last_name' => $lastName,
'phone' => $phone,
'address' => $address,
'country_id' => $countryInput,
'birthday_display' => $birthdayInput,
]);
}
} elseif ($action === 'avatar') {
/* ---------- Caricamento foto profilo ---------- */
$f = $_FILES['avatar'] ?? null;
if (!$f || $f['error'] === UPLOAD_ERR_NO_FILE) {
$avatarFeedback = ['type' => 'error', 'messages' => ['Seleziona un\'immagine da caricare.']];
} elseif ($f['error'] === UPLOAD_ERR_INI_SIZE || $f['error'] === UPLOAD_ERR_FORM_SIZE || $f['size'] > $avatarMaxBytes) {
$avatarFeedback = ['type' => 'error', 'messages' => ['L\'immagine supera la dimensione massima di 5 MB.']];
} elseif ($f['error'] !== UPLOAD_ERR_OK) {
$avatarFeedback = ['type' => 'error', 'messages' => ['Caricamento non riuscito. Riprova.']];
} else {
$mimeMap = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']);
$info = @getimagesize($f['tmp_name']);
if (!isset($mimeMap[$mime]) || !$info) {
$avatarFeedback = ['type' => 'error', 'messages' => ['Formato non consentito. Carica un\'immagine JPG, PNG o WebP.']];
} else {
if (!is_dir($avatarDir)) {
@mkdir($avatarDir, 0755, true);
}
$base = bin2hex(random_bytes(16));
$fileName = null;
// Con GD: ritaglio quadrato centrale, ridimensionamento e ricodifica in JPG
// (rimuove anche dati EXIF e qualsiasi contenuto non grafico)
if (function_exists('imagecreatefromstring') && function_exists('imagejpeg')) {
$src = @imagecreatefromstring((string) file_get_contents($f['tmp_name']));
if ($src) {
// Foto da smartphone: corregge l'orientamento EXIF
if ($mime === 'image/jpeg' && function_exists('exif_read_data')) {
$exif = @exif_read_data($f['tmp_name']);
$orientation = (int) ($exif['Orientation'] ?? 1);
$angles = [3 => 180, 6 => -90, 8 => 90];
if (isset($angles[$orientation])) {
$rotated = imagerotate($src, $angles[$orientation], 0);
if ($rotated) {
imagedestroy($src);
$src = $rotated;
}
}
}
$w = imagesx($src);
$h = imagesy($src);
$side = min($w, $h);
$out = min($avatarSide, $side);
$dst = imagecreatetruecolor($out, $out);
imagefill($dst, 0, 0, imagecolorallocate($dst, 255, 255, 255));
imagecopyresampled($dst, $src, 0, 0, (int) (($w - $side) / 2), (int) (($h - $side) / 2), $out, $out, $side, $side);
if (imagejpeg($dst, $avatarDir . $base . '.jpg', 85)) {
$fileName = $base . '.jpg';
}
imagedestroy($src);
imagedestroy($dst);
}
}
// Senza GD (o formato non gestito da GD): salva il file originale
if ($fileName === null && move_uploaded_file($f['tmp_name'], $avatarDir . $base . '.' . $mimeMap[$mime])) {
$fileName = $base . '.' . $mimeMap[$mime];
}
if ($fileName === null) {
$avatarFeedback = ['type' => 'error', 'messages' => ['Salvataggio dell\'immagine non riuscito. Riprova.']];
} else {
$stmt = $pdo->prepare("UPDATE auth_users SET avatar = :avatar, updated_at = NOW() WHERE id = :id");
if ($stmt->execute([':avatar' => $fileName, ':id' => $iduserlogin])) {
$old = $user['avatar'] ?? '';
if (isOwnAvatarFile($old) && is_file($avatarDir . $old)) {
@unlink($avatarDir . $old);
}
$user = loadUser($pdo, $iduserlogin);
$avatarFeedback = ['type' => 'success', 'messages' => ['Foto profilo aggiornata.']];
} else {
@unlink($avatarDir . $fileName);
$avatarFeedback = ['type' => 'error', 'messages' => ['Salvataggio dell\'immagine non riuscito. Riprova.']];
}
}
}
}
} elseif ($action === 'avatar_remove') {
/* ---------- Rimozione foto profilo ---------- */
$stmt = $pdo->prepare("UPDATE auth_users SET avatar = NULL, updated_at = NOW() WHERE id = :id");
if ($stmt->execute([':id' => $iduserlogin])) {
$old = $user['avatar'] ?? '';
if (isOwnAvatarFile($old) && is_file($avatarDir . $old)) {
@unlink($avatarDir . $old);
}
$user = loadUser($pdo, $iduserlogin);
$avatarFeedback = ['type' => 'success', 'messages' => ['Foto profilo rimossa.']];
} else {
$avatarFeedback = ['type' => 'error', 'messages' => ['Rimozione non riuscita. Riprova.']];
}
} elseif ($action === 'password') {
/* ---------- Cambio password ---------- */
$current = (string) ($_POST['current_password'] ?? '');
$new = (string) ($_POST['new_password'] ?? '');
$confirm = (string) ($_POST['confirm_password'] ?? '');
// Limite tentativi sulla password attuale: 5 errori, poi pausa di 15 minuti
$fails = $_SESSION['pwd_fail_count'] ?? 0;
$lockUntil = $_SESSION['pwd_lock_until'] ?? 0;
if ($lockUntil > time()) {
$minutes = (int) ceil(($lockUntil - time()) / 60);
$passwordFeedback = ['type' => 'error', 'messages' => ["Troppi tentativi errati. Riprova tra $minutes minuti."]];
} elseif ($current === '' || $new === '' || $confirm === '') {
$passwordFeedback = ['type' => 'error', 'messages' => ['Compila tutti e tre i campi.']];
} elseif (!password_verify($current, $user['password'])) {
$fails++;
$_SESSION['pwd_fail_count'] = $fails;
if ($fails >= 5) {
$_SESSION['pwd_lock_until'] = time() + 15 * 60;
$_SESSION['pwd_fail_count'] = 0;
}
$passwordFeedback = ['type' => 'error', 'messages' => ['La password attuale non è corretta.']];
} elseif (mb_strlen($new) < 8) {
$passwordFeedback = ['type' => 'error', 'messages' => ['La nuova password deve avere almeno 8 caratteri.']];
} elseif (!preg_match('/[A-Za-z]/', $new) || !preg_match('/[0-9]/', $new)) {
$passwordFeedback = ['type' => 'error', 'messages' => ['La nuova password deve contenere almeno una lettera e un numero.']];
} elseif ($new !== $confirm) {
$passwordFeedback = ['type' => 'error', 'messages' => ['Le due nuove password non coincidono.']];
} elseif (password_verify($new, $user['password'])) {
$passwordFeedback = ['type' => 'error', 'messages' => ['La nuova password deve essere diversa da quella attuale.']];
} else {
$stmt = $pdo->prepare(
"UPDATE auth_users
SET password = :pwd, remember_token = NULL, updated_at = NOW()
WHERE id = :id"
);
$ok = $stmt->execute([
':pwd' => password_hash($new, PASSWORD_DEFAULT),
':id' => $iduserlogin,
]);
if ($ok) {
unset($_SESSION['pwd_fail_count'], $_SESSION['pwd_lock_until']);
session_regenerate_id(true);
$user = loadUser($pdo, $iduserlogin);
$passwordFeedback = ['type' => 'success', 'messages' => ['Password aggiornata.']];
} else {
$passwordFeedback = ['type' => 'error', 'messages' => ['Aggiornamento della password non riuscito. Riprova.']];
}
}
}
}
/* -------------------------------------------------------------------------
* Valori da mostrare nel modulo
* ---------------------------------------------------------------------- */
$form = $postedProfile ?? $user;
if (!isset($form['birthday_display'])) {
$form['birthday_display'] = !empty($user['birthday']) ? date('d/m/Y', strtotime($user['birthday'])) : '';
}
$fullName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
$avatarValue = (string) ($user['avatar'] ?? '');
$avatarUrl = null;
if ($avatarValue !== '' && preg_match('#^https?://#i', $avatarValue)) {
$avatarUrl = $avatarValue;
} elseif ($avatarValue !== '' && is_file($avatarDir . basename($avatarValue))) {
$avatarUrl = $avatarDir . rawurlencode(basename($avatarValue));
}
$hasCustomAvatar = $avatarUrl !== null;
if (!$hasCustomAvatar) {
$avatarUrl = $avatarDir . $avatarDefault;
}
$memberSince = !empty($user['created_at']) ? date('d/m/Y', strtotime($user['created_at'])) : null;
function renderFeedback(?array $fb): void
{
if (!$fb) {
return;
}
$cls = $fb['type'] === 'success' ? 'success' : 'danger';
echo '<div class="alert alert-' . $cls . '" role="alert">';
if (count($fb['messages']) === 1) {
echo e($fb['messages'][0]);
} else {
echo '<ul class="mb-0 ps-3">';
foreach ($fb['messages'] as $m) {
echo '<li>' . e($m) . '</li>';
}
echo '</ul>';
}
echo '</div>';
}
?>
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8" />
<title>YogiBook - Il mio profilo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta content="YogiBook - Prenotazione facile YogaSoul" name="description" />
<meta content="Advanced Creative Solutions" name="author" />
<link rel="shortcut icon" href="assets/images/favicon.ico">
<link href="assets/css/bootstrap.min.css" id="bootstrap-style" rel="stylesheet" type="text/css" />
<link href="assets/css/icons.min.css" rel="stylesheet" type="text/css" />
<link href="assets/css/app.min.css" id="app-style" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@10"></script>
<link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
$(function() {
$.datepicker.regional['it'] = {
closeText: 'Chiudi',
prevText: '&#x3C;Prec',
nextText: 'Succ&#x3E;',
currentText: 'Oggi',
monthNames: ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'],
monthNamesShort: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'],
dayNames: ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'],
dayNamesShort: ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'],
dayNamesMin: ['Do', 'Lu', 'Ma', 'Me', 'Gi', 'Ve', 'Sa'],
weekHeader: 'Sm',
firstDay: 1,
isRTL: false
};
$.datepicker.setDefaults($.datepicker.regional['it']);
$("#birthday").datepicker({
dateFormat: "dd/mm/yy",
changeYear: true,
changeMonth: true,
yearRange: "-100:+0",
maxDate: 0,
defaultDate: "-30y"
});
$("#clearBirthday").on("click", function(ev) {
ev.preventDefault();
$("#birthday").val("");
});
$("#current_password, #new_password, #confirm_password").on("input", checkPasswordForm);
checkPasswordForm();
});
/* ---------- Foto profilo ---------- */
function pickAvatar() {
document.getElementById('avatarInput').click();
}
function handleAvatarChange(input) {
var status = document.getElementById('avatarStatus');
status.className = 'field-hint';
if (!input.files.length) return;
var f = input.files[0];
var allowed = ['image/jpeg', 'image/png', 'image/webp'];
var serverMax = 5 * 1024 * 1024;
if (allowed.indexOf(f.type) === -1) {
status.textContent = 'Formato non valido: usa JPG, PNG o WebP.';
status.className = 'field-hint hint-ko';
input.value = '';
return;
}
if (f.size > 25 * 1024 * 1024) {
status.textContent = 'Immagine troppo grande.';
status.className = 'field-hint hint-ko';
input.value = '';
return;
}
status.textContent = 'Preparazione immagine...';
// Ritaglio quadrato e riduzione a 400 px direttamente nel browser
cropSquare(f, 400, 0.85).then(function(blob) {
var dt = new DataTransfer();
dt.items.add(new File([blob], 'avatar.jpg', {
type: 'image/jpeg'
}));
input.files = dt.files;
return true;
}).catch(function() {
// Browser vecchio: si invia l'originale se rientra nel limite
return f.size <= serverMax;
}).then(function(ok) {
if (!ok) {
status.textContent = 'Immagine troppo grande (max 5 MB).';
status.className = 'field-hint hint-ko';
input.value = '';
return;
}
document.getElementById('avatarPreview').src = URL.createObjectURL(input.files[0]);
status.textContent = 'Caricamento in corso...';
document.getElementById('avatarForm').submit();
});
}
function cropSquare(file, size, quality) {
return new Promise(function(resolve, reject) {
var url = URL.createObjectURL(file);
var img = new Image();
img.onload = function() {
var side = Math.min(img.naturalWidth, img.naturalHeight);
var sx = (img.naturalWidth - side) / 2;
var sy = (img.naturalHeight - side) / 2;
var out = Math.min(size, side);
var canvas = document.createElement('canvas');
canvas.width = out;
canvas.height = out;
var ctx = canvas.getContext('2d');
ctx.fillStyle = '#fff'; // sfondo bianco per PNG trasparenti
ctx.fillRect(0, 0, out, out);
ctx.drawImage(img, sx, sy, side, side, 0, 0, out, out);
URL.revokeObjectURL(url);
canvas.toBlob(function(b) {
b ? resolve(b) : reject();
}, 'image/jpeg', quality);
};
img.onerror = function() {
URL.revokeObjectURL(url);
reject();
};
img.src = url;
});
}
function confirmRemoveAvatar() {
Swal.fire({
title: "Rimuovere la foto?",
text: "Tornerà l'immagine predefinita.",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#3085d6",
confirmButtonText: "Sì, rimuovi",
cancelButtonText: "Annulla"
}).then(function(result) {
if (result.isConfirmed) {
document.getElementById('avatarRemoveForm').submit();
}
});
}
/* ---------- Password ---------- */
function togglePassword(btn) {
var input = document.getElementById(btn.getAttribute('data-target'));
var show = input.type === 'password';
input.type = show ? 'text' : 'password';
btn.querySelector('i').className = show ? 'fas fa-eye-slash' : 'fas fa-eye';
btn.setAttribute('aria-label', show ? 'Nascondi password' : 'Mostra password');
}
function passwordScore(pwd) {
var score = 0;
if (pwd.length >= 8) score++;
if (pwd.length >= 12) score++;
if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) score++;
if (/[0-9]/.test(pwd)) score++;
if (/[^A-Za-z0-9]/.test(pwd)) score++;
return score;
}
function checkPasswordForm() {
var current = document.getElementById('current_password').value;
var pwd = document.getElementById('new_password').value;
var confirm = document.getElementById('confirm_password').value;
var bar = document.getElementById('strengthBar');
var label = document.getElementById('strengthLabel');
var match = document.getElementById('matchHint');
var btn = document.getElementById('btnPassword');
// Robustezza
var levels = [{
w: '0%',
c: '#e5e7eb',
t: ''
},
{
w: '20%',
c: '#dc3545',
t: 'Debole'
},
{
w: '40%',
c: '#f59e0b',
t: 'Debole'
},
{
w: '60%',
c: '#f59e0b',
t: 'Discreta'
},
{
w: '80%',
c: '#1ebf73',
t: 'Buona'
},
{
w: '100%',
c: '#16a34a',
t: 'Ottima'
}
];
var lv = pwd.length ? levels[passwordScore(pwd)] : levels[0];
bar.style.width = lv.w;
bar.style.background = lv.c;
label.textContent = pwd.length ? lv.t : 'Almeno 8 caratteri, con lettere e numeri.';
// Corrispondenza
if (!confirm.length) {
match.textContent = '';
match.className = 'field-hint';
} else if (pwd === confirm) {
match.innerHTML = '<i class="fas fa-check"></i> Le password coincidono';
match.className = 'field-hint hint-ok';
} else {
match.innerHTML = '<i class="fas fa-xmark"></i> Le password non coincidono';
match.className = 'field-hint hint-ko';
}
var valid = current.length > 0 &&
pwd.length >= 8 && /[A-Za-z]/.test(pwd) && /[0-9]/.test(pwd) &&
pwd === confirm;
btn.disabled = !valid;
}
</script>
<style>
.cert-card {
border: none;
border-radius: 14px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
}
.cert-card .card-body {
padding: 26px 30px;
}
.section-title {
font-size: 15px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #6b7280;
margin-bottom: 4px;
}
.section-lead {
color: #9ca3af;
font-size: 14px;
margin-bottom: 22px;
}
.form-field {
margin-bottom: 20px;
}
.form-field label {
display: block;
font-weight: 600;
font-size: 14px;
color: #374151;
margin-bottom: 6px;
}
.form-field .form-control,
.form-field .form-select {
border-radius: 8px;
border: 1px solid #e2e4e9;
padding: 10px 12px;
}
.form-field .form-control:focus,
.form-field .form-select:focus {
border-color: #1ebf73;
box-shadow: 0 0 0 3px rgba(30, 191, 115, 0.12);
}
.btn-save {
border-radius: 8px;
padding: 10px 26px;
font-weight: 600;
}
/* Intestazione profilo */
.profile-head {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.avatar-wrap {
position: relative;
flex-shrink: 0;
}
.profile-avatar {
width: 88px;
height: 88px;
border-radius: 50%;
object-fit: cover;
background: #eef0f3;
display: block;
}
.avatar-edit {
position: absolute;
right: -2px;
bottom: -2px;
width: 32px;
height: 32px;
border-radius: 50%;
border: 3px solid #fff;
background: #1ebf73;
color: #fff;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.avatar-edit:hover {
background: #18a863;
}
.avatar-actions {
display: flex;
gap: 16px;
margin-top: 10px;
}
.avatar-link {
background: none;
border: 0;
padding: 0;
font-size: 13px;
font-weight: 600;
color: #1ebf73;
}
.avatar-link:hover {
text-decoration: underline;
}
.avatar-link-danger {
color: #dc3545;
}
.avatar-edit:focus-visible,
.avatar-link:focus-visible {
outline: 2px solid #1ebf73;
outline-offset: 2px;
}
.profile-name {
font-size: 20px;
font-weight: 700;
color: #1f2937;
margin: 0 0 2px;
}
.profile-meta {
color: #6b7280;
font-size: 14px;
}
.profile-meta i {
width: 16px;
color: #9ca3af;
}
/* Email non modificabile */
.locked-field {
position: relative;
}
.locked-field .form-control {
background: #f6f7f9;
color: #6b7280;
padding-right: 40px;
}
.locked-field .lock-icon {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
}
.field-hint {
display: block;
margin-top: 6px;
font-size: 12px;
color: #9ca3af;
}
.field-hint a {
color: #6b7280;
}
.hint-ok {
color: #198754;
}
.hint-ko {
color: #dc3545;
}
/* Password */
.pwd-wrap {
position: relative;
}
.pwd-wrap .form-control {
padding-right: 46px;
}
.pwd-toggle {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
border: 0;
border-radius: 6px;
background: transparent;
color: #9ca3af;
}
.pwd-toggle:hover {
color: #1ebf73;
}
.pwd-toggle:focus-visible {
outline: 2px solid #1ebf73;
outline-offset: 1px;
}
.strength-track {
height: 6px;
border-radius: 3px;
background: #eef0f3;
margin-top: 8px;
overflow: hidden;
}
.strength-bar {
height: 100%;
width: 0;
transition: width 0.25s, background 0.25s;
}
@media (prefers-reduced-motion: reduce) {
.strength-bar {
transition: none;
}
}
.security-note {
font-size: 12px;
color: #9ca3af;
margin: 4px 0 18px;
}
</style>
</head>
<body>
<div id="layout-wrapper">
<header id="page-topbar" class="isvertical-topbar">
<div class="navbar-header">
<div class="d-flex">
<?php include('include/logoarea.php'); ?>
<button type="button" class="btn btn-sm px-3 font-size-24 header-item waves-effect vertical-menu-btn">
<i class="bx bx-menu align-middle"></i>
</button>
<div class="page-title-box align-self-center d-none d-md-block">
<h4 class="page-title mb-0">Il mio profilo</h4>
</div>
</div>
<div class="d-flex">
<?php include('include/languageselection.php'); ?>
<?php include('include/profiletopbar.php'); ?>
</div>
</div>
</header>
<?php include('include/sidebar.php'); ?>
<header class="ishorizontal-topbar">
<div class="navbar-header">
<div class="d-flex"></div>
</div>
<div class="topnav">
<div class="container-fluid">
<nav class="navbar navbar-light navbar-expand-lg topnav-menu"></nav>
</div>
</div>
</header>
<div class="main-content">
<div class="page-content">
<div class="container-fluid">
<!-- Intestazione -->
<div class="row">
<div class="col-12">
<div class="card cert-card mb-4">
<div class="card-body">
<div class="profile-head">
<div class="avatar-wrap">
<img src="<?php echo e($avatarUrl); ?>" class="profile-avatar" id="avatarPreview" alt="Foto profilo">
<button type="button" class="avatar-edit" onclick="pickAvatar()" aria-label="Cambia foto profilo">
<i class="fas fa-camera"></i>
</button>
</div>
<div>
<p class="profile-name"><?php echo e($fullName !== '' ? $fullName : $user['email']); ?></p>
<div class="profile-meta">
<i class="fas fa-envelope"></i> <?php echo e($user['email']); ?>
</div>
<?php if ($memberSince) : ?>
<div class="profile-meta">
<i class="fas fa-calendar"></i> Iscritto dal <?php echo e($memberSince); ?>
</div>
<?php endif; ?>
<div class="avatar-actions">
<button type="button" class="avatar-link" onclick="pickAvatar()">
<?php echo $hasCustomAvatar ? 'Cambia foto' : 'Carica una foto'; ?>
</button>
<?php if ($hasCustomAvatar) : ?>
<button type="button" class="avatar-link avatar-link-danger" onclick="confirmRemoveAvatar()">Rimuovi</button>
<?php endif; ?>
</div>
<span class="field-hint" id="avatarStatus" aria-live="polite">JPG, PNG o WebP, max 5 MB. Verrà ritagliata in formato quadrato.</span>
</div>
</div>
<?php if ($avatarFeedback) : ?>
<div class="mt-3 mb-n3"><?php renderFeedback($avatarFeedback); ?></div>
<?php endif; ?>
<form id="avatarForm" method="post" enctype="multipart/form-data" class="d-none">
<input type="hidden" name="action" value="avatar">
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
<input type="file" id="avatarInput" name="avatar" accept="image/jpeg,image/png,image/webp" onchange="handleAvatarChange(this)">
</form>
<form id="avatarRemoveForm" method="post" class="d-none">
<input type="hidden" name="action" value="avatar_remove">
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
</form>
</div>
</div>
</div>
</div>
<!-- Dati personali -->
<div class="row">
<div class="col-12">
<div class="card cert-card mb-4" id="dati">
<div class="card-body">
<div class="section-title">Dati personali</div>
<p class="section-lead">Tieni aggiornati i tuoi dati: li usiamo per le prenotazioni e per contattarti.</p>
<?php renderFeedback($profileFeedback); ?>
<form method="post" action="#dati" novalidate>
<input type="hidden" name="action" value="profile">
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
<div class="row">
<div class="col-md-6">
<div class="form-field">
<label for="first_name">Nome</label>
<input type="text" id="first_name" name="first_name" class="form-control"
value="<?php echo e($form['first_name']); ?>" maxlength="191" autocomplete="given-name" required>
</div>
</div>
<div class="col-md-6">
<div class="form-field">
<label for="last_name">Cognome</label>
<input type="text" id="last_name" name="last_name" class="form-control"
value="<?php echo e($form['last_name']); ?>" maxlength="191" autocomplete="family-name" required>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="form-field">
<label for="email">Email</label>
<div class="locked-field">
<input type="email" id="email" class="form-control"
value="<?php echo e($user['email']); ?>" disabled>
<i class="fas fa-lock lock-icon" aria-hidden="true"></i>
</div>
<span class="field-hint">L'email serve per l'accesso e non si può modificare da qui. Per cambiarla contatta la segreteria.</span>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-field">
<label for="phone">Telefono</label>
<input type="tel" id="phone" name="phone" class="form-control"
value="<?php echo e($form['phone']); ?>" maxlength="30" autocomplete="tel" placeholder="+39 333 1234567">
</div>
</div>
<div class="col-md-6">
<div class="form-field">
<label for="birthday">Data di nascita</label>
<input type="text" id="birthday" name="birthday" class="form-control"
value="<?php echo e($form['birthday_display']); ?>" placeholder="GG/MM/AAAA" autocomplete="off">
<span class="field-hint"><a href="#" id="clearBirthday">Rimuovi data</a></span>
</div>
</div>
</div>
<div class="row">
<?php if (!empty($countries)) : ?>
<div class="col-md-6">
<div class="form-field">
<label for="country_id">Paese</label>
<select id="country_id" name="country_id" class="form-select" autocomplete="country">
<option value="">Seleziona un paese</option>
<?php foreach ($countries as $cid => $cname) : ?>
<option value="<?php echo (int) $cid; ?>" <?php echo ((string) $form['country_id'] === (string) $cid) ? 'selected' : ''; ?>>
<?php echo e($cname); ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<?php endif; ?>
<div class="<?php echo !empty($countries) ? 'col-md-6' : 'col-12'; ?>">
<div class="form-field">
<label for="address">Indirizzo</label>
<input type="text" id="address" name="address" class="form-control"
value="<?php echo e($form['address']); ?>" maxlength="191" autocomplete="street-address" placeholder="Via, numero civico, città">
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-save">Salva modifiche</button>
</form>
</div>
</div>
</div>
</div>
<!-- Password -->
<div class="row">
<div class="col-12">
<div class="card cert-card" id="password">
<div class="card-body">
<div class="section-title">Password</div>
<p class="section-lead">Per sicurezza ti chiediamo la password attuale prima di impostarne una nuova.</p>
<?php renderFeedback($passwordFeedback); ?>
<form method="post" action="#password" autocomplete="on">
<input type="hidden" name="action" value="password">
<input type="hidden" name="csrf" value="<?php echo e($csrf); ?>">
<!-- Aiuta i password manager ad associare la password all'account -->
<input type="email" name="pm_username" value="<?php echo e($user['email']); ?>" autocomplete="username" class="d-none" readonly tabindex="-1" aria-hidden="true">
<div class="row">
<div class="col-md-6">
<div class="form-field">
<label for="current_password">Password attuale</label>
<div class="pwd-wrap">
<input type="password" id="current_password" name="current_password" class="form-control"
autocomplete="current-password" required>
<button type="button" class="pwd-toggle" data-target="current_password"
onclick="togglePassword(this)" aria-label="Mostra password">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-field">
<label for="new_password">Nuova password</label>
<div class="pwd-wrap">
<input type="password" id="new_password" name="new_password" class="form-control"
autocomplete="new-password" minlength="8" required>
<button type="button" class="pwd-toggle" data-target="new_password"
onclick="togglePassword(this)" aria-label="Mostra password">
<i class="fas fa-eye"></i>
</button>
</div>
<div class="strength-track">
<div class="strength-bar" id="strengthBar"></div>
</div>
<span class="field-hint" id="strengthLabel">Almeno 8 caratteri, con lettere e numeri.</span>
</div>
</div>
<div class="col-md-6">
<div class="form-field">
<label for="confirm_password">Conferma nuova password</label>
<div class="pwd-wrap">
<input type="password" id="confirm_password" name="confirm_password" class="form-control"
autocomplete="new-password" minlength="8" required>
<button type="button" class="pwd-toggle" data-target="confirm_password"
onclick="togglePassword(this)" aria-label="Mostra password">
<i class="fas fa-eye"></i>
</button>
</div>
<span class="field-hint" id="matchHint" aria-live="polite"></span>
</div>
</div>
</div>
<p class="security-note">
Dopo il cambio, gli accessi con "Ricordami" sugli altri dispositivi verranno disattivati.
</p>
<button type="submit" class="btn btn-primary btn-save" id="btnPassword">Aggiorna password</button>
</form>
</div>
</div>
</div>
</div>
</div>
<?php include('include/footer.php'); ?>
</div>
</div>
</div>
<script src="assets/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="assets/libs/metismenujs/metismenujs.min.js"></script>
<script src="assets/libs/simplebar/simplebar.min.js"></script>
<script src="assets/libs/eva-icons/eva.min.js"></script>
<script src="assets/js/app.js"></script>
</body>
</html>