feat(avatar): implement avatar upload processing and validation, update user interface for avatar display

This commit is contained in:
2026-09-11 16:00:48 +02:00
parent e012d6d21c
commit 35ce20cd59
12 changed files with 210 additions and 57 deletions
+4
View File
@@ -12,3 +12,7 @@ Thumbs.db
.idea/
.continue/
.continueignore
# Uploads
/assets/uploads/*
!/assets/uploads/**/.gitkeep
+4 -1
View File
@@ -783,7 +783,10 @@ require_once __DIR__ . '/includes/header.php';
<td class="py-3 px-4">
<div class="flex items-center space-x-2.5">
<?php if (!empty($usr['avatar_url'])): ?>
<img src="<?= htmlspecialchars($usr['avatar_url']) ?>" alt="Avatar" class="w-7 h-7 rounded-full object-cover border border-slate-200 dark:border-slate-700">
<img src="<?= htmlspecialchars($usr['avatar_url']) ?>" alt="Avatar" class="w-7 h-7 rounded-full object-cover border border-slate-200 dark:border-slate-700" onerror="this.classList.add('hidden'); if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');">
<div class="hidden w-7 h-7 rounded-full bg-indigo-100 dark:bg-indigo-900/60 text-indigo-700 dark:text-indigo-300 font-bold flex items-center justify-center text-[10px]">
<?= strtoupper(substr($usr['name'], 0, 1)) ?>
</div>
<?php else: ?>
<div class="w-7 h-7 rounded-full bg-indigo-100 dark:bg-indigo-900/60 text-indigo-700 dark:text-indigo-300 font-bold flex items-center justify-center text-[10px]">
<?= strtoupper(substr($usr['name'], 0, 1)) ?>
+3 -3
View File
@@ -174,12 +174,12 @@ $pageTitle = "Fociz — Uitnodiging Accepteren";
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Wachtwoord (min. 8 tekens)</label>
<input type="password" name="password" required placeholder="••••••••" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-sm transition">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Wachtwoord (min. 12 tekens)</label>
<input type="password" name="password" required minlength="12" placeholder="••••••••••••" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-sm transition">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Bevestig wachtwoord</label>
<input type="password" name="confirm_password" required placeholder="••••••••" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-sm transition">
<input type="password" name="confirm_password" required minlength="12" placeholder="••••••••••••" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-sm transition">
</div>
</div>
+9 -7
View File
@@ -185,16 +185,16 @@ $pageTitle = "Fociz — Wachtwoord Opnieuw Instellen";
<input type="hidden" name="token" value="<?= htmlspecialchars($token) ?>">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Nieuw wachtwoord</label>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Nieuw wachtwoord (min. 12 tekens)</label>
<div class="relative">
<input
type="password"
name="password"
id="pwd"
required
minlength="8"
minlength="12"
autofocus
placeholder="Minimaal 8 tekens"
placeholder="Minimaal 12 tekens"
class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-sm transition"
oninput="checkPasswordStrength(this.value)"
>
@@ -206,6 +206,7 @@ $pageTitle = "Fociz — Wachtwoord Opnieuw Instellen";
<div id="bar3" class="h-1 flex-1 rounded-full bg-slate-200 dark:bg-slate-800 transition-colors"></div>
<span id="strengthText" class="text-[10px] text-slate-400 pl-1 font-medium"></span>
</div>
<p class="text-[10px] text-slate-400 dark:text-slate-500 mt-1">Minimaal 12 tekens (incl. hoofdletter, kleine letter, cijfer en speciaal teken).</p>
</div>
<div>
@@ -215,7 +216,8 @@ $pageTitle = "Fociz — Wachtwoord Opnieuw Instellen";
type="password"
name="confirm_password"
required
placeholder="Herhaal wachtwoord"
minlength="12"
placeholder="Herhaal wachtwoord (min. 12 tekens)"
class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-sm transition"
>
</div>
@@ -257,9 +259,9 @@ $pageTitle = "Fociz — Wachtwoord Opnieuw Instellen";
if (!val) return;
var score = 0;
if (val.length >= 8) score++;
if (/[A-Z]/.test(val) && /[0-9]/.test(val)) score++;
if (/[^A-Za-z0-9]/.test(val) && val.length >= 10) score++;
if (val.length >= 12) score++;
if (/[A-Z]/.test(val) && /[a-z]/.test(val) && /[0-9]/.test(val)) score++;
if (/[^A-Za-z0-9]/.test(val) && val.length >= 12) score++;
if (score === 1) {
b1.className = 'h-1 flex-1 rounded-full bg-amber-500';
+3 -3
View File
@@ -195,12 +195,12 @@ $pageTitle = "Fociz — Eerste Initialisatie";
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Wachtwoord (min. 8 tekens)</label>
<input type="password" name="password" required placeholder="••••••••" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-sm transition">
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Wachtwoord (min. 12 tekens)</label>
<input type="password" name="password" required minlength="12" placeholder="••••••••••••" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-sm transition">
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">Bevestig wachtwoord</label>
<input type="password" name="confirm_password" required placeholder="••••••••" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-sm transition">
<input type="password" name="confirm_password" required minlength="12" placeholder="••••••••••••" class="w-full bg-slate-50 dark:bg-[#0c0f17] border border-slate-300 dark:border-slate-800 rounded-xl px-4 py-2.5 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-sm transition">
</div>
</div>
</div>
+1 -1
View File
@@ -678,7 +678,7 @@ function renderCards() {
if (card.assignee_name) {
const initial = card.assignee_name.charAt(0).toUpperCase();
const avatarMarkup = card.assignee_avatar
? `<img src="${escapeHtml(card.assignee_avatar)}" class="w-5 h-5 rounded-full object-cover flex-shrink-0 bg-white dark:bg-slate-900 border border-indigo-200 dark:border-indigo-800/60">`
? `<img src="${escapeHtml(card.assignee_avatar)}" class="w-5 h-5 rounded-full object-cover flex-shrink-0 bg-white dark:bg-slate-900 border border-indigo-200 dark:border-indigo-800/60" onerror="this.classList.add('hidden'); if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');"><div class="hidden w-5 h-5 rounded-full bg-gradient-to-tr from-indigo-600 to-sky-500 text-white font-bold text-[9px] flex items-center justify-center flex-shrink-0">${initial}</div>`
: `<div class="w-5 h-5 rounded-full bg-gradient-to-tr from-indigo-600 to-sky-500 text-white font-bold text-[9px] flex items-center justify-center flex-shrink-0">${initial}</div>`;
assigneeHtml = `
<div class="flex items-center space-x-1.5" title="Toegewezen aan: ${escapeHtml(card.assignee_name)}">
+1
View File
@@ -146,6 +146,7 @@ function ensure_tables_exist(PDO $pdo): void {
"ALTER TABLE users ADD COLUMN notification_settings TEXT NULL AFTER remember_token_expires_at",
"ALTER TABLE users ADD COLUMN theme VARCHAR(20) NOT NULL DEFAULT 'dark' AFTER notification_settings",
"ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255) NULL AFTER theme",
"ALTER TABLE users MODIFY COLUMN avatar_url MEDIUMTEXT NULL",
"ALTER TABLE users ADD COLUMN backup_codes TEXT NULL AFTER two_factor_enabled",
"ALTER TABLE users ADD COLUMN default_jira_filter_code VARCHAR(50) NULL AFTER avatar_url",
"ALTER TABLE users ADD COLUMN fociz_widget_layout TEXT NULL AFTER default_jira_filter_code",
+1 -1
View File
@@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS `users` (
`remember_token` VARCHAR(64) NULL,
`remember_token_expires_at` DATETIME NULL,
`notification_settings` TEXT NULL,
`avatar_url` VARCHAR(255) NULL,
`avatar_url` MEDIUMTEXT NULL,
`default_jira_filter_code` VARCHAR(50) NULL,
`fociz_widget_layout` TEXT NULL,
`last_mail_check_at` DATETIME NULL,
+139
View File
@@ -384,6 +384,141 @@ function ensure_theme_column(PDO $db): void {
}
}
/**
* Valideert en normaliseert een avatar URL.
* Controleert of geüploade bestanden nog fysiek aanwezig zijn op de server (bijv. na container herstart).
* Indien een bestand ontbreekt, wordt de database automatisch hersteld (self-healing) en null geretourneerd.
*/
function fociz_get_valid_avatar_url(?string $avatarUrl, ?int $userId = null): ?string {
if (empty($avatarUrl)) {
return null;
}
// 1. Data-URI (base64) of meegeleverde repository preset SVG: altijd persistent en geldig
if (str_starts_with($avatarUrl, 'data:image/') || str_starts_with($avatarUrl, '/assets/avatars/')) {
return $avatarUrl;
}
// 2. Geüpload bestand (/assets/uploads/...): controleer fysieke aanwezigheid op server
if (str_starts_with($avatarUrl, '/assets/uploads/')) {
$localPath = dirname(__DIR__) . '/' . ltrim($avatarUrl, '/');
if (file_exists($localPath)) {
return $avatarUrl;
}
// Fysiek bestand ontbreekt (bijv. door container herstart of git deploy)
if ($userId) {
try {
$db = get_db_connection();
$stmt = $db->prepare("UPDATE users SET avatar_url = NULL WHERE id = ? AND avatar_url = ?");
$stmt->execute([$userId, $avatarUrl]);
if (isset($_SESSION['user_avatar_url']) && $_SESSION['user_avatar_url'] === $avatarUrl) {
unset($_SESSION['user_avatar_url']);
}
} catch (Throwable $e) {
error_log("Self-healing avatar cleanup notice: " . $e->getMessage());
}
}
return null;
}
return $avatarUrl;
}
/**
* Verwerkt een geüploade avatar-afbeelding.
* - Knipt vierkant uit en comprimeert naar max. 256x256 via GD
* - Converteert naar Base64 Data URI voor 100% gegarandeerde persistentie in MariaDB
* - Slaat tevens optioneel een backup op in assets/uploads/avatars/
*/
function fociz_process_avatar_upload(array $file, int $userId): array {
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $allowedTypes)) {
return ['success' => false, 'error' => 'Alleen JPG, PNG, WebP of GIF afbeeldingen zijn toegestaan.'];
}
$ext = match($mime) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif',
default => 'jpg'
};
$rawContent = file_get_contents($file['tmp_name']);
if ($rawContent === false || strlen($rawContent) === 0) {
return ['success' => false, 'error' => 'Kon geüploade afbeelding niet inlezen.'];
}
$finalMime = $mime;
$finalData = $rawContent;
// Verklein en comprimeer naar max 256x256 vierkant via GD
if (extension_loaded('gd') && function_exists('imagecreatefromstring')) {
try {
$srcImg = @imagecreatefromstring($rawContent);
if ($srcImg !== false) {
$w = imagesx($srcImg);
$h = imagesy($srcImg);
$maxDim = 256;
$minDim = min($w, $h);
$cropX = (int)(($w - $minDim) / 2);
$cropY = (int)(($h - $minDim) / 2);
$targetSize = min($maxDim, $minDim);
$dstImg = imagecreatetruecolor($targetSize, $targetSize);
imagealphablending($dstImg, false);
imagesavealpha($dstImg, true);
imagecopyresampled($dstImg, $srcImg, 0, 0, $cropX, $cropY, $targetSize, $targetSize, $minDim, $minDim);
ob_start();
if ($mime === 'image/png') {
imagepng($dstImg, null, 8);
$finalMime = 'image/png';
} elseif (function_exists('imagewebp') && ($mime === 'image/webp' || $mime === 'image/jpeg')) {
imagewebp($dstImg, null, 85);
$finalMime = 'image/webp';
} else {
imagejpeg($dstImg, null, 85);
$finalMime = 'image/jpeg';
}
$compressed = ob_get_clean();
if (!empty($compressed)) {
$finalData = $compressed;
}
imagedestroy($dstImg);
imagedestroy($srcImg);
}
} catch (Throwable $e) {
error_log("fociz avatar resize notice: " . $e->getMessage());
}
}
// Bouw data URI voor permanente opslag in MariaDB
$dataUri = 'data:' . $finalMime . ';base64,' . base64_encode($finalData);
// Sla bestand ook lokaal op als backup / snelle cache
$uploadDir = dirname(__DIR__) . '/assets/uploads/avatars';
if (!is_dir($uploadDir)) {
@mkdir($uploadDir, 0755, true);
}
$filename = "avatar_{$userId}_" . time() . "_" . bin2hex(random_bytes(4)) . ".{$ext}";
@file_put_contents($uploadDir . '/' . $filename, $finalData);
return [
'success' => true,
'data_uri' => $dataUri,
'web_path' => "/assets/uploads/avatars/{$filename}"
];
}
/**
* Genereert een cryptografisch veilig dagsessie-token dat strikt geldig is tot 23:59:59 van vandaag.
* Voorkomt dat gebruikers tussentijds worden uitgelogd bij idle timeouts of browserherstart.
@@ -534,6 +669,8 @@ function get_logged_in_user(): ?array {
$_SESSION['auth_fingerprint'] = $clientFingerprint;
$_SESSION['session_expires_at'] = strtotime('today 23:59:59');
$_SESSION['last_activity'] = $now;
$user['avatar_url'] = fociz_get_valid_avatar_url($user['avatar_url'] ?? null, (int)$user['id']);
$_SESSION['user_avatar_url'] = $user['avatar_url'];
return $user;
}
// Gebruiker bestaat niet meer
@@ -568,6 +705,8 @@ function get_logged_in_user(): ?array {
$_SESSION['last_activity'] = $now;
$_SESSION['login_time'] = $now;
$_SESSION['last_rotated'] = $now;
$user['avatar_url'] = fociz_get_valid_avatar_url($user['avatar_url'] ?? null, (int)$user['id']);
$_SESSION['user_avatar_url'] = $user['avatar_url'];
return $user;
}
// Token is ongeldig of van gisteren
+4 -1
View File
@@ -253,7 +253,10 @@ require_once __DIR__ . '/env_badge.php';
<a href="/profile.php" class="flex items-center space-x-2.5 group hover:opacity-90 transition px-2 py-1.5 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800/60" title="Mijn Profiel & Instellingen">
<div class="w-7 h-7 rounded-lg bg-gradient-to-tr from-indigo-600 to-sky-500 p-0.5 flex-shrink-0 shadow-xs overflow-hidden">
<?php if (!empty($currentUser['avatar_url'])): ?>
<img src="<?= htmlspecialchars($currentUser['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-[6px] object-cover bg-white dark:bg-slate-950">
<img src="<?= htmlspecialchars($currentUser['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-[6px] object-cover bg-white dark:bg-slate-950" onerror="this.classList.add('hidden'); if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');">
<div class="hidden w-full h-full rounded-[6px] bg-white dark:bg-slate-950 flex items-center justify-center font-bold text-[11px] text-indigo-600 dark:text-indigo-400 uppercase">
<?= strtoupper(substr($currentUser['name'], 0, 1)) ?>
</div>
<?php else: ?>
<div class="w-full h-full rounded-[6px] bg-white dark:bg-slate-950 flex items-center justify-center font-bold text-[11px] text-indigo-600 dark:text-indigo-400 uppercase">
<?= strtoupper(substr($currentUser['name'], 0, 1)) ?>
+35 -37
View File
@@ -219,43 +219,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = "De afbeelding is te groot (maximaal 4MB toegestaan).";
$messageType = 'error';
} else {
$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $allowedTypes)) {
$message = "Alleen JPG, PNG, WebP of GIF afbeeldingen zijn toegestaan.";
$processed = fociz_process_avatar_upload($file, $userId);
if (!$processed['success']) {
$message = $processed['error'] ?? "Fout bij verwerken van de afbeelding.";
$messageType = 'error';
} else {
$ext = match($mime) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/gif' => 'gif',
default => 'jpg'
};
$uploadDir = __DIR__ . '/assets/uploads/avatars';
if (!is_dir($uploadDir)) {
@mkdir($uploadDir, 0755, true);
}
$filename = "avatar_{$userId}_" . time() . "_" . bin2hex(random_bytes(4)) . ".{$ext}";
$targetPath = $uploadDir . '/' . $filename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
$webPath = "/assets/uploads/avatars/{$filename}";
$avatarUri = $processed['data_uri'];
$stmtUp = $db->prepare("UPDATE users SET avatar_url = ? WHERE id = ?");
$stmtUp->execute([$webPath, $userId]);
$_SESSION['user_avatar_url'] = $webPath;
$user['avatar_url'] = $webPath;
$message = "Uw profielfoto is succesvol geüpload en ingesteld!";
$stmtUp->execute([$avatarUri, $userId]);
$_SESSION['user_avatar_url'] = $avatarUri;
$user['avatar_url'] = $avatarUri;
$message = "Uw profielfoto is succesvol geüpload en permanent bewaard!";
$messageType = 'success';
} else {
$message = "Er is een fout opgetreden bij het verplaatsen van het geüploade bestand.";
$messageType = 'error';
}
}
}
}
@@ -277,6 +252,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmtUser = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmtUser->execute([$userId]);
$userProfile = $stmtUser->fetch();
if ($userProfile) {
$userProfile['avatar_url'] = fociz_get_valid_avatar_url($userProfile['avatar_url'] ?? null, $userId);
}
// Outlook status ophalen
$stmtOutlook = $db->prepare("SELECT * FROM integrations WHERE user_id = ? AND provider = 'OUTLOOK'");
@@ -316,7 +294,10 @@ require_once __DIR__ . '/includes/header.php';
<div class="flex items-center space-x-4 sm:space-x-5">
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-2xl bg-gradient-to-tr from-indigo-600 to-sky-500 p-0.5 shadow-sm flex-shrink-0 overflow-hidden">
<?php if (!empty($userProfile['avatar_url'])): ?>
<img src="<?= htmlspecialchars($userProfile['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-2xl object-cover bg-white dark:bg-[#0c0f17]">
<img src="<?= htmlspecialchars($userProfile['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-2xl object-cover bg-white dark:bg-[#0c0f17]" onerror="this.classList.add('hidden'); if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');">
<div class="hidden w-full h-full rounded-2xl bg-white dark:bg-[#0c0f17] flex items-center justify-center font-bold text-2xl sm:text-3xl text-indigo-600 dark:text-indigo-400 uppercase">
<?= strtoupper(substr($userProfile['name'], 0, 1)) ?>
</div>
<?php else: ?>
<div class="w-full h-full rounded-2xl bg-white dark:bg-[#0c0f17] flex items-center justify-center font-bold text-2xl sm:text-3xl text-indigo-600 dark:text-indigo-400 uppercase">
<?= strtoupper(substr($userProfile['name'], 0, 1)) ?>
@@ -1071,16 +1052,17 @@ require_once __DIR__ . '/includes/header.php';
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Nieuw Wachtwoord (min. 8 tekens)
Nieuw Wachtwoord (min. 12 tekens)
</label>
<input type="password" name="new_password" required minlength="8" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<input type="password" name="new_password" required minlength="12" placeholder="Minimaal 12 tekens" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<p class="text-[10px] text-slate-400 dark:text-slate-500 mt-1">Minimaal 12 tekens (incl. hoofdletter, kleine letter, cijfer en speciaal teken).</p>
</div>
<div>
<label class="block text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1">
Bevestig Nieuw Wachtwoord
</label>
<input type="password" name="confirm_password" required minlength="8" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
<input type="password" name="confirm_password" required minlength="12" placeholder="Herhaal nieuw wachtwoord" class="w-full bg-white dark:bg-slate-900/60 border border-slate-200 dark:border-slate-800 rounded-xl px-3.5 py-2 text-xs text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-indigo-500">
</div>
<div class="flex justify-end pt-2">
@@ -1151,6 +1133,22 @@ function previewUploadAvatar(input) {
nameEl.textContent = '📄 ' + file.name + ' (' + Math.round(file.size / 1024) + ' KB)';
nameEl.classList.remove('hidden');
}
// Live thumbnail weergave
const reader = new FileReader();
reader.onload = function(e) {
let prevImg = document.getElementById('uploadAvatarLivePreview');
if (!prevImg && nameEl && nameEl.parentNode) {
prevImg = document.createElement('img');
prevImg.id = 'uploadAvatarLivePreview';
prevImg.className = 'w-16 h-16 rounded-full mx-auto my-2 object-cover border-2 border-indigo-500 shadow-sm';
nameEl.parentNode.insertBefore(prevImg, nameEl);
}
if (prevImg) {
prevImg.src = e.target.result;
prevImg.classList.remove('hidden');
}
};
reader.readAsDataURL(file);
}
}
+4 -1
View File
@@ -216,7 +216,10 @@ require_once __DIR__ . '/includes/header.php';
<div class="flex items-center space-x-2.5">
<div class="w-8 h-8 rounded-xl bg-gradient-to-tr from-indigo-600 to-sky-500 p-0.5 flex-shrink-0 overflow-hidden shadow-2xs">
<?php if (!empty($u['avatar_url'])): ?>
<img src="<?= htmlspecialchars($u['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-[10px] object-cover bg-white dark:bg-slate-900">
<img src="<?= htmlspecialchars($u['avatar_url']) ?>" alt="Avatar" class="w-full h-full rounded-[10px] object-cover bg-white dark:bg-slate-900" onerror="this.classList.add('hidden'); if(this.nextElementSibling) this.nextElementSibling.classList.remove('hidden');">
<div class="hidden w-full h-full rounded-[10px] bg-white dark:bg-slate-900 flex items-center justify-center font-bold text-xs text-indigo-600 dark:text-indigo-400 uppercase">
<?= strtoupper(substr($u['name'], 0, 1)) ?>
</div>
<?php else: ?>
<div class="w-full h-full rounded-[10px] bg-white dark:bg-slate-900 flex items-center justify-center font-bold text-xs text-indigo-600 dark:text-indigo-400 uppercase">
<?= strtoupper(substr($u['name'], 0, 1)) ?>