feat: add custom avatar functionality (#248)

Co-authored-by: Dante Bradshaw <plansuperior@gmail.com>
This commit is contained in:
Miguel Ribeiro 2024-03-24 16:12:44 +01:00 committed by GitHub
parent 40bcf3485e
commit 1dbebd3918
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 387 additions and 41 deletions

View File

@ -0,0 +1,39 @@
<?php
require_once '../../includes/connect_endpoint.php';
session_start();
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
die(json_encode([
"success" => false,
"message" => translate('session_expired', $i18n)
]));
}
$input = json_decode(file_get_contents('php://input'), true);
if (isset($input['avatar'])) {
$avatar = "images/uploads/logos/avatars/".$input['avatar'];
$sql = "SELECT avatar FROM user";
$stmt = $db->prepare($sql);
$result = $stmt->execute();
$userAvatar = $result->fetchArray(SQLITE3_ASSOC)['avatar'];
// Check if $avatar matches the avatar in the user table
if ($avatar === $userAvatar) {
echo json_encode(array("success" => false));
} else {
// The avatars do not match
$filePath = "../../" . $avatar;
if (file_exists($filePath)) {
unlink($filePath);
echo json_encode(array("success" => true, "message" => translate("success", $i18n)));
} else {
echo json_encode(array("success" => false, "message" => translate("error", $i18n)));
}
}
} else {
echo json_encode(array("success" => false, "message" => translate("error", $i18n)));
}
?>

View File

@ -85,6 +85,95 @@
$row = $result->fetchArray(SQLITE3_ASSOC);
$mainCurrencyId = $row['main_currency'];
function sanitizeFilename($filename) {
$filename = preg_replace("/[^a-zA-Z0-9\s]/", "", $filename);
$filename = str_replace(" ", "-", $filename);
$filename = str_replace(".", "", $filename);
return $filename;
}
function validateFileExtension($fileExtension) {
$allowedExtensions = ['png', 'jpg', 'jpeg', 'gif', 'jtif', 'webp'];
return in_array($fileExtension, $allowedExtensions);
}
function resizeAndUploadAvatar($uploadedFile, $uploadDir, $name) {
$targetWidth = 80;
$targetHeight = 80;
$timestamp = time();
$originalFileName = $uploadedFile['name'];
$fileExtension = strtolower(pathinfo($originalFileName, PATHINFO_EXTENSION));
$fileExtension = validateFileExtension($fileExtension) ? $fileExtension : 'png';
$fileName = $timestamp . '-avatars-' . sanitizeFilename($name) . '.' . $fileExtension;
$uploadFile = $uploadDir . $fileName;
if (move_uploaded_file($uploadedFile['tmp_name'], $uploadFile)) {
$fileInfo = getimagesize($uploadFile);
if ($fileInfo !== false) {
$width = $fileInfo[0];
$height = $fileInfo[1];
// Load the image based on its format
if ($fileExtension === 'png') {
$image = imagecreatefrompng($uploadFile);
} elseif ($fileExtension === 'jpg' || $fileExtension === 'jpeg') {
$image = imagecreatefromjpeg($uploadFile);
} elseif ($fileExtension === 'gif') {
$image = imagecreatefromgif($uploadFile);
} elseif ($fileExtension === 'webp') {
$image = imagecreatefromwebp($uploadFile);
} else {
// Handle other image formats as needed
return "";
}
// Enable alpha channel (transparency) for PNG images
if ($fileExtension === 'png') {
imagesavealpha($image, true);
}
$newWidth = $width;
$newHeight = $height;
if ($width > $targetWidth) {
$newWidth = $targetWidth;
$newHeight = ($targetWidth / $width) * $height;
}
if ($newHeight > $targetHeight) {
$newWidth = ($targetHeight / $newHeight) * $newWidth;
$newHeight = $targetHeight;
}
$resizedImage = imagecreatetruecolor($newWidth, $newHeight);
imagesavealpha($resizedImage, true);
$transparency = imagecolorallocatealpha($resizedImage, 0, 0, 0, 127);
imagefill($resizedImage, 0, 0, $transparency);
imagecopyresampled($resizedImage, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
if ($fileExtension === 'png') {
imagepng($resizedImage, $uploadFile);
} elseif ($fileExtension === 'jpg' || $fileExtension === 'jpeg') {
imagejpeg($resizedImage, $uploadFile);
} elseif ($fileExtension === 'gif') {
imagegif($resizedImage, $uploadFile);
} elseif ($fileExtension === 'webp') {
imagewebp($resizedImage, $uploadFile);
} else {
return "";
}
imagedestroy($image);
imagedestroy($resizedImage);
return "images/uploads/logos/avatars/".$fileName;
}
}
return "";
}
if (isset($_SESSION['username']) && isset($_POST['username']) && isset($_POST['email']) && isset($_POST['avatar'])) {
$oldUsername = $_SESSION['username'];
$username = validate($_POST['username']);
@ -93,6 +182,22 @@
$main_currency = $_POST['main_currency'];
$language = $_POST['language'];
if (! empty($_FILES['profile_pic']["name"])) {
$file = $_FILES['profile_pic'];
$fileType = mime_content_type($_FILES['profile_pic']['tmp_name']);
if (strpos($fileType, 'image') === false) {
$response = [
"success" => false,
"errorMessage" => translate('fill_all_fields', $i18n)
];
echo json_encode($response);
exit();
}
$name = $file['name'];
$avatar = resizeAndUploadAvatar($_FILES['profile_pic'], '../../images/uploads/logos/avatars/', $name);
}
if (isset($_POST['password']) && $_POST['password'] != "") {
$password = $_POST['password'];
if (isset($_POST['confirm_password'])) {

View File

@ -58,7 +58,7 @@
<nav>
<div class="dropdown">
<button class="dropbtn" onClick="toggleDropdown()">
<img src="images/avatars/<?= $userData['avatar'] ?>.svg" alt="me" id="avatar">
<img src="<?= $userData['avatar'] ?>" alt="me" id="avatar">
<span id="user"><?= $username ?></span>
</button>
<div class="dropdown-content">

View File

@ -97,6 +97,8 @@ $i18n = [
'icons' => "Icons",
'payment_icons' => "Zahlungsweisen Icons",
// Settings page
'upload_avatar' => "Avatar hochladen",
'file_type_error' => "Dateityp nicht unterstützt",
'user_details' => "Benutzerdetails",
"household" => "Haushalt",
"save_member" => "Mitglied speichern",

View File

@ -97,6 +97,8 @@ $i18n = [
'icons' => "Εικονίδια",
'payment_icons' => "Εικονίδια Payment",
// Settings page
'upload_avatar' => "μεταφόρτωση άβαταρ",
'file_type_error' => "Το αρχείο πρέπει να είναι τύπου jpg, jpeg, png, webp ή gif",
'user_details' => "Λεπτομέρειες χρήστη",
"household" => "Νοικοκυριό",
"save_member" => "Αποθήκευση μέλους",

View File

@ -97,6 +97,8 @@ $i18n = [
'icons' => "Icons",
'payment_icons' => "Payment Icons",
// Settings page
'upload_avatar' => "Upload Avatar",
'file_type_error' => "The file type supplied is not supported.",
'user_details' => "User Details",
"household" => "Household",
"save_member" => "Save Member",

View File

@ -97,6 +97,8 @@ $i18n = [
'icons' => "Iconos",
'payment_icons' => "Iconos de Pago",
// Settings page
'upload_avatar' => "Subir avatar",
'file_type_error' => "El archivo debe ser una imagen en formato PNG, JPG, WEBP o SVG",
'user_details' => "Detalles del Usuario",
"household" => "Hogar",
"save_member" => "Guardar Miembro",

View File

@ -97,6 +97,8 @@ $i18n = [
'icons' => "Icônes",
'payment_icons' => "Icônes de paiement",
// Page de paramètres
'upload_avatar' => "Télécharger un Avatar",
'file_type_error' => "Le type de fichier n'est pas pris en charge",
'user_details' => "Détails de l'utilisateur",
"household" => "Ménage",
"save_member" => "Enregistrer le membre",

View File

@ -97,6 +97,8 @@ $i18n = [
'icons' => "アイコン",
'payment_icons' => "支払いアイコン",
// Settings page
'upload_avatar' => "アバターをアップロードする",
'file_type_error' => "ファイルタイプが許可されていません",
'user_details' => "ユーザー詳細",
"household" => "世帯",
"save_member" => "世帯員を保存",

View File

@ -97,6 +97,8 @@ $i18n = [
'icons' => "Ícones",
'payment_icons' => "Ícones de Pagamentos",
// Settings page
'upload_avatar' => "Enviar avatar",
'file_type_error' => "Tipo de ficheiro não permitido",
'user_details' => "Detalhes do utilizador",
"household" => "Agregado",
"save_member" => "Guardar Membro",

View File

@ -95,6 +95,8 @@ $i18n = [
'icons' => "Ícones",
'payment_icons' => "Ícones de pagamento",
// Settings page
'upload_avatar' => "Carregar avatar",
'file_type_error' => "Tipo de arquivo não permitido",
'user_details' => "Informações do Usuário",
"household" => "Membros",
"save_member" => "Salvar membro",

View File

@ -97,6 +97,8 @@ $i18n = [
'icons' => "İkonlar",
'payment_icons' => "Ödeme İkonları",
// Settings page
'upload_avatar' => "Avatarı yükle",
'file_type_error' => "Dosya türü izin verilmiyor",
'user_details' => "Kullanıcı Detayları",
"household" => "Hane",
"save_member" => "Üyeyi Kaydet",

View File

@ -104,6 +104,8 @@ $i18n = [
'payment_icons' => "支付图标",
// 设置页面
'upload_avatar' => "上传头像",
'file_type_error' => "文件类型不允许",
'user_details' => "用户详情",
"household" => "家庭",
"save_member" => "保存成员",

View File

@ -97,6 +97,8 @@ $i18n = [
'icons' => "圖示",
'payment_icons' => "付款圖示",
// 設定頁面
'upload_avatar' => "上传头像",
'file_type_error' => "文件类型不允许",
'user_details' => "使用者詳細資訊",
"household" => "家庭",
"save_member" => "儲存成員",

View File

@ -1,3 +1,3 @@
<?php
$version = "v1.17.3";
$version = "v1.18.0";
?>

25
migrations/000013.php Normal file
View File

@ -0,0 +1,25 @@
<?php
/**
* This migration script updates the avatar field of the user table to use the new avatar path.
*/
/** @noinspection PhpUndefinedVariableInspection */
$sql = "SELECT avatar FROM user";
$stmt = $db->prepare($sql);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row) {
$avatar = $row['avatar'];
if (strlen($avatar) < 2) {
$avatarFullPath = "images/avatars/" . $avatar . ".svg";
$sql = "UPDATE user SET avatar = :avatarFullPath";
$stmt = $db->prepare($sql);
$stmt->bindValue(':avatarFullPath', $avatarFullPath, SQLITE3_TEXT);
$stmt->execute();
}
}
?>

View File

@ -43,7 +43,7 @@ if (isset($_POST['username'])) {
$confirm_password = $_POST['confirm_password'];
$main_currency = $_POST['main_currency'];
$language = $_POST['language'];
$avatar = "0";
$avatar = "images/avatars/0.svg";
if ($password != $confirm_password) {
$passwordMismatch = true;

View File

@ -12,10 +12,61 @@ function closeAvatarSelect() {
avatarSelect.classList.remove("is-open");
}
function changeAvatar(number) {
document.getElementById("avatarImg").src = "images/avatars/" + number + ".svg";
document.getElementById("avatarUser").value = number;
document.querySelectorAll('.avatar-option').forEach((avatar) => {
avatar.addEventListener("click", () => {
changeAvatar(avatar.src);
document.getElementById('avatarUser').value = avatar.getAttribute('data-src');
closeAvatarSelect();
})
});
function changeAvatar(src) {
document.getElementById("avatarImg").src = src;
}
function successfulUpload(field, msg) {
var reader = new FileReader();
if (field.files.length === 0) {
return;
}
if (! ['image/jpeg', 'image/png', 'image/gif', 'image/jtif', 'image/webp'].includes(field.files[0]['type'])) {
showErrorMessage(msg);
return;
}
reader.onload = function() {
changeAvatar(reader.result);
};
reader.readAsDataURL(field.files[0]);
closeAvatarSelect();
}
function deleteAvatar(path) {
fetch('/endpoints/user/delete_avatar.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ avatar: path }),
})
.then(response => response.json())
.then(data => {
if (data.success) {
var avatarContainer = document.querySelector(`.avatar-container[data-src="${path}"]`);
if (avatarContainer) {
avatarContainer.remove();
}
showSuccessMessage();
} else {
showErrorMessage();
}
})
.catch((error) => {
console.error('Error:', error);
});
}
function addMemberButton(memberId) {
@ -681,7 +732,6 @@ function deletePaymentMethod(paymentId) {
}
function savePaymentMethodsSorting() {
console.log("should save");
const paymentMethods = document.getElementById('payments-list');
const paymentMethodIds = Array.from(paymentMethods.children).map(paymentMethod => paymentMethod.dataset.paymentid);
@ -733,8 +783,7 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json())
.then(data => {
if (data.success) {
var newAvatar = document.getElementById("avatarUser").value;
document.getElementById("avatar").src = "images/avatars/" + newAvatar + ".svg";
document.getElementById("avatar").src = document.getElementById("avatarImg").src;
var newUsername = document.getElementById("username").value;
document.getElementById("user").textContent = newUsername;
showSuccessMessage(data.message);

View File

@ -13,12 +13,12 @@
<header>
<h2><?= translate('user_details', $i18n) ?></h2>
</header>
<form action="endpoints/user/saveuser.php" method="post" id="userForm">
<form action="endpoints/user/saveuser.php" method="post" id="userForm" enctype="multipart/form-data">
<div class="user-form">
<div class="fields">
<div>
<div class="user-avatar">
<img src="images/avatars/<?= $userData['avatar'] ?>.svg" alt="avatar" class="avatar" id="avatarImg" onClick="toggleAvatarSelect()"/>
<img src="<?= $userData['avatar'] ?>" alt="avatar" class="avatar" id="avatarImg" onClick="toggleAvatarSelect()"/>
<span class="edit-avatar" onClick="toggleAvatarSelect()">
<img src="images/siteicons/editavatar.png" title="Change avatar" />
</span>
@ -27,18 +27,27 @@
<input type="hidden" name="avatar" value="<?= $userData['avatar'] ?>" id="avatarUser"/>
<div class="avatar-select" id="avatarSelect">
<div class="avatar-list">
<img src="images/avatars/0.svg" onClick="changeAvatar(0)" />
<img src="images/avatars/1.svg" onClick="changeAvatar(1)" />
<img src="images/avatars/2.svg" onClick="changeAvatar(2)" />
<img src="images/avatars/3.svg" onClick="changeAvatar(3)" />
<img src="images/avatars/4.svg" onClick="changeAvatar(4)" />
<img src="images/avatars/5.svg" onClick="changeAvatar(5)" />
<img src="images/avatars/6.svg" onClick="changeAvatar(6)" />
<img src="images/avatars/7.svg" onClick="changeAvatar(7)" />
<img src="images/avatars/8.svg" onClick="changeAvatar(8)" />
<img src="images/avatars/9.svg" onClick="changeAvatar(9)" />
<?php foreach (scandir('images/avatars') as $index => $image) :?>
<?php if (! str_starts_with($image, '.')) :?>
<img src="images/avatars/<?=$image?>" alt="<?=$image?>" class="avatar-option" data-src="images/avatars/<?=$image?>"/>
<?php endif ?>
<?php endforeach ?>
<?php foreach (scandir('images/uploads/logos/avatars') as $index => $image) :?>
<?php if (! str_starts_with($image, '.')) :?>
<div class="avatar-container" data-src="<?=$image?>">
<img src="images/uploads/logos/avatars/<?=$image?>" alt="<?=$image?>" class="avatar-option" data-src="images/uploads/logos/avatars/<?=$image?>"/>
<div class="remove-avatar" onclick="deleteAvatar('<?=$image?>')" title="Delete avatar">
<i class="fa-solid fa-xmark"></i>
</div>
</div>
<?php endif ?>
<?php endforeach ?>
<label for="profile_pic" class="add-avatar" title="<?= translate('upload_avatar', $i18n) ?>">
<i class="fa-solid fa-arrow-up-from-bracket"></i>
</label>
</div>
<input type="file" id="profile_pic"class="hidden-input" name="profile_pic" accept="image/jpeg, image/png, image/gif, image/webp" onChange="successfulUpload(this, '<?= translate('file_type_error', $i18n) ?>')" />
</div>
</div>
<div class="grow">
<div class="form-group">

View File

@ -21,6 +21,8 @@ sleep 1
chmod -R 755 /var/www/html/db/
chown -R www-data:www-data /var/www/html/db/
mkdir -p /var/www/html/images/uploads/logos/avatars
# Change permissions on the logos directory
chmod -R 755 /var/www/html/images/uploads/logos
chown -R www-data:www-data /var/www/html/images/uploads/logos

View File

@ -85,6 +85,24 @@ input[type="button"].secondary-button:hover {
background-color: #111;
}
.avatar-select .avatar-list .remove-avatar {
background-color: #222;
}
.avatar-select .avatar-list > img,
.avatar-select .avatar-list .avatar-container > img {
border: 1px solid #999;
}
.avatar-select .avatar-list > img:hover,
.avatar-select .avatar-list .avatar-container > img:hover {
border: 1px solid #EEE;
}
.avatar-select .avatar-list .remove-avatar:hover {
background-color: #666;
}
.toast {
border: 1px solid #333;
background: #222;

View File

@ -104,6 +104,7 @@ header .logo .logo-image {
.dropbtn > img {
width: 30px;
height: 30px;
object-fit: contain;
}
.dropdown-content {
@ -493,12 +494,23 @@ main > .contain {
}
}
header #avatar {
border-radius: 50%;
}
.user-form .user-avatar {
position: relative;
}
.user-form .user-avatar > img {
cursor: pointer;
width: 80px;
height: 80px;
object-fit: contain;
max-width: 80px;
border-radius: 50%;
border: 1px solid #ccc;
box-sizing: border-box;
}
.user-form .user-avatar .edit-avatar {
@ -536,12 +548,16 @@ main > .contain {
position: absolute;
padding: 20px;
box-sizing: border-box;
width: 300px;
width: 336px;
max-width: 100%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 3;
}
.avatar-option {
border-radius: 50%;
}
@media (max-width: 768px) {
.avatar-select {
left: 50%;
@ -555,14 +571,70 @@ main > .contain {
.avatar-select .avatar-list {
display: flex;
gap: 14px;
gap: 18px;
flex-wrap: wrap;
justify-content: space-around;
}
.avatar-select .avatar-list > img {
width: 40px;
.avatar-select .avatar-list > img,
.avatar-select .avatar-list .avatar-container > img {
width: 60px;
height: 60px;
object-fit: contain;
cursor: pointer;
border: 1px solid #ccc;
box-sizing: border-box
}
.avatar-select .avatar-list > img:hover,
.avatar-select .avatar-list .avatar-container > img:hover {
border: 1px solid #222;
}
.avatar-select .avatar-list .avatar-container {
position: relative;
height: 60px;
}
.avatar-select label.add-avatar {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
border: 1px solid #007bff;
border-radius: 50%;
cursor: pointer;
margin: 0px;
box-sizing: border-box;
color: #007bff;
}
.avatar-select label.add-avatar:hover {
border-color: #8FBFFA;
color: #8FBFFA;
}
.avatar-select .avatar-list .remove-avatar {
position: absolute;
top: -4px;
right: -11px;
background-color: #FFF;
border-radius: 50%;
cursor: pointer;
display: flex;
font-weight: 600;
align-items: center;
justify-content: center;
border: 1px solid #ccc;
width: 25px;
height: 25px;
box-sizing: border-box;
font-size: 8px;
}
.avatar-select .avatar-list .remove-avatar:hover {
background-color: #eee;
}
.user-form .fields .grow {
@ -797,7 +869,8 @@ input[type="text"].short {
}
input[type="submit"],
input[type="button"] {
input[type="button"],
button.button {
padding: 12px 30px;
font-size: 16px;
background-color: #007bff;
@ -809,12 +882,14 @@ input[type="button"] {
border: 2px solid #007bff;
}
input[type="button"].secondary-button {
input[type="button"].secondary-button,
button.button.secondary-button {
background-color: #FFFFFF;
color: #007bff;
}
input[type="button"].secondary-button:hover {
input[type="button"].secondary-button:hover,
button.button.secondary-button:hover {
background-color: #EEEEEE;
color: #0056b3;
border-color: #0056b3;
@ -836,12 +911,14 @@ input[type="submit"]:hover {
}
input[type="submit"]:disabled,
input[type="button"]:disabled {
input[type="button"]:disabled,
button.button:disabled {
background-color: #ccc;
border-color: #ccc;
}
input[type="button"].left {
input[type="button"].left
button.button.left {
margin-right: auto;
}