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); $row = $result->fetchArray(SQLITE3_ASSOC);
$mainCurrencyId = $row['main_currency']; $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'])) { if (isset($_SESSION['username']) && isset($_POST['username']) && isset($_POST['email']) && isset($_POST['avatar'])) {
$oldUsername = $_SESSION['username']; $oldUsername = $_SESSION['username'];
$username = validate($_POST['username']); $username = validate($_POST['username']);
@ -93,6 +182,22 @@
$main_currency = $_POST['main_currency']; $main_currency = $_POST['main_currency'];
$language = $_POST['language']; $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'] != "") { if (isset($_POST['password']) && $_POST['password'] != "") {
$password = $_POST['password']; $password = $_POST['password'];
if (isset($_POST['confirm_password'])) { if (isset($_POST['confirm_password'])) {

View File

@ -58,7 +58,7 @@
<nav> <nav>
<div class="dropdown"> <div class="dropdown">
<button class="dropbtn" onClick="toggleDropdown()"> <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> <span id="user"><?= $username ?></span>
</button> </button>
<div class="dropdown-content"> <div class="dropdown-content">

View File

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

View File

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

View File

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

View File

@ -97,6 +97,8 @@ $i18n = [
'icons' => "Iconos", 'icons' => "Iconos",
'payment_icons' => "Iconos de Pago", 'payment_icons' => "Iconos de Pago",
// Settings page // 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", 'user_details' => "Detalles del Usuario",
"household" => "Hogar", "household" => "Hogar",
"save_member" => "Guardar Miembro", "save_member" => "Guardar Miembro",
@ -223,4 +225,4 @@ $i18n = [
]; ];
?> ?>

View File

@ -97,6 +97,8 @@ $i18n = [
'icons' => "Icônes", 'icons' => "Icônes",
'payment_icons' => "Icônes de paiement", 'payment_icons' => "Icônes de paiement",
// Page de paramètres // 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", 'user_details' => "Détails de l'utilisateur",
"household" => "Ménage", "household" => "Ménage",
"save_member" => "Enregistrer le membre", "save_member" => "Enregistrer le membre",
@ -223,4 +225,4 @@ $i18n = [
]; ];
?> ?>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,3 @@
<?php <?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']; $confirm_password = $_POST['confirm_password'];
$main_currency = $_POST['main_currency']; $main_currency = $_POST['main_currency'];
$language = $_POST['language']; $language = $_POST['language'];
$avatar = "0"; $avatar = "images/avatars/0.svg";
if ($password != $confirm_password) { if ($password != $confirm_password) {
$passwordMismatch = true; $passwordMismatch = true;

View File

@ -12,10 +12,61 @@ function closeAvatarSelect() {
avatarSelect.classList.remove("is-open"); avatarSelect.classList.remove("is-open");
} }
function changeAvatar(number) { document.querySelectorAll('.avatar-option').forEach((avatar) => {
document.getElementById("avatarImg").src = "images/avatars/" + number + ".svg"; avatar.addEventListener("click", () => {
document.getElementById("avatarUser").value = number; changeAvatar(avatar.src);
closeAvatarSelect(); 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) { function addMemberButton(memberId) {
@ -681,7 +732,6 @@ function deletePaymentMethod(paymentId) {
} }
function savePaymentMethodsSorting() { function savePaymentMethodsSorting() {
console.log("should save");
const paymentMethods = document.getElementById('payments-list'); const paymentMethods = document.getElementById('payments-list');
const paymentMethodIds = Array.from(paymentMethods.children).map(paymentMethod => paymentMethod.dataset.paymentid); const paymentMethodIds = Array.from(paymentMethods.children).map(paymentMethod => paymentMethod.dataset.paymentid);
@ -733,8 +783,7 @@ document.addEventListener('DOMContentLoaded', function() {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
var newAvatar = document.getElementById("avatarUser").value; document.getElementById("avatar").src = document.getElementById("avatarImg").src;
document.getElementById("avatar").src = "images/avatars/" + newAvatar + ".svg";
var newUsername = document.getElementById("username").value; var newUsername = document.getElementById("username").value;
document.getElementById("user").textContent = newUsername; document.getElementById("user").textContent = newUsername;
showSuccessMessage(data.message); showSuccessMessage(data.message);

View File

@ -13,31 +13,40 @@
<header> <header>
<h2><?= translate('user_details', $i18n) ?></h2> <h2><?= translate('user_details', $i18n) ?></h2>
</header> </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="user-form">
<div class="fields"> <div class="fields">
<div> <div>
<div class="user-avatar"> <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()"> <span class="edit-avatar" onClick="toggleAvatarSelect()">
<img src="images/siteicons/editavatar.png" title="Change avatar" /> <img src="images/siteicons/editavatar.png" title="Change avatar" />
</span> </span>
</div> </div>
<input type="hidden" name="avatar" value="<?= $userData['avatar'] ?>" id="avatarUser"/> <input type="hidden" name="avatar" value="<?= $userData['avatar'] ?>" id="avatarUser"/>
<div class="avatar-select" id="avatarSelect"> <div class="avatar-select" id="avatarSelect">
<div class="avatar-list"> <div class="avatar-list">
<img src="images/avatars/0.svg" onClick="changeAvatar(0)" /> <?php foreach (scandir('images/avatars') as $index => $image) :?>
<img src="images/avatars/1.svg" onClick="changeAvatar(1)" /> <?php if (! str_starts_with($image, '.')) :?>
<img src="images/avatars/2.svg" onClick="changeAvatar(2)" /> <img src="images/avatars/<?=$image?>" alt="<?=$image?>" class="avatar-option" data-src="images/avatars/<?=$image?>"/>
<img src="images/avatars/3.svg" onClick="changeAvatar(3)" /> <?php endif ?>
<img src="images/avatars/4.svg" onClick="changeAvatar(4)" /> <?php endforeach ?>
<img src="images/avatars/5.svg" onClick="changeAvatar(5)" /> <?php foreach (scandir('images/uploads/logos/avatars') as $index => $image) :?>
<img src="images/avatars/6.svg" onClick="changeAvatar(6)" /> <?php if (! str_starts_with($image, '.')) :?>
<img src="images/avatars/7.svg" onClick="changeAvatar(7)" /> <div class="avatar-container" data-src="<?=$image?>">
<img src="images/avatars/8.svg" onClick="changeAvatar(8)" /> <img src="images/uploads/logos/avatars/<?=$image?>" alt="<?=$image?>" class="avatar-option" data-src="images/uploads/logos/avatars/<?=$image?>"/>
<img src="images/avatars/9.svg" onClick="changeAvatar(9)" /> <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> </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> </div>
<div class="grow"> <div class="grow">
@ -619,4 +628,4 @@
<?php <?php
require_once 'includes/footer.php'; require_once 'includes/footer.php';
?> ?>

View File

@ -21,6 +21,8 @@ sleep 1
chmod -R 755 /var/www/html/db/ chmod -R 755 /var/www/html/db/
chown -R www-data:www-data /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 # Change permissions on the logos directory
chmod -R 755 /var/www/html/images/uploads/logos chmod -R 755 /var/www/html/images/uploads/logos
chown -R www-data:www-data /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; 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 { .toast {
border: 1px solid #333; border: 1px solid #333;
background: #222; background: #222;

View File

@ -103,7 +103,8 @@ header .logo .logo-image {
.dropbtn > img { .dropbtn > img {
width: 30px; width: 30px;
height: 30 px; height: 30px;
object-fit: contain;
} }
.dropdown-content { .dropdown-content {
@ -493,12 +494,23 @@ main > .contain {
} }
} }
header #avatar {
border-radius: 50%;
}
.user-form .user-avatar { .user-form .user-avatar {
position: relative; position: relative;
} }
.user-form .user-avatar > img { .user-form .user-avatar > img {
cursor: pointer; 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 { .user-form .user-avatar .edit-avatar {
@ -536,12 +548,16 @@ main > .contain {
position: absolute; position: absolute;
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
width: 300px; width: 336px;
max-width: 100%; max-width: 100%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 3; z-index: 3;
} }
.avatar-option {
border-radius: 50%;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.avatar-select { .avatar-select {
left: 50%; left: 50%;
@ -555,14 +571,70 @@ main > .contain {
.avatar-select .avatar-list { .avatar-select .avatar-list {
display: flex; display: flex;
gap: 14px; gap: 18px;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-around;
} }
.avatar-select .avatar-list > img { .avatar-select .avatar-list > img,
width: 40px; .avatar-select .avatar-list .avatar-container > img {
width: 60px;
height: 60px;
object-fit: contain;
cursor: pointer; 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 { .user-form .fields .grow {
@ -797,7 +869,8 @@ input[type="text"].short {
} }
input[type="submit"], input[type="submit"],
input[type="button"] { input[type="button"],
button.button {
padding: 12px 30px; padding: 12px 30px;
font-size: 16px; font-size: 16px;
background-color: #007bff; background-color: #007bff;
@ -809,12 +882,14 @@ input[type="button"] {
border: 2px solid #007bff; border: 2px solid #007bff;
} }
input[type="button"].secondary-button { input[type="button"].secondary-button,
button.button.secondary-button {
background-color: #FFFFFF; background-color: #FFFFFF;
color: #007bff; color: #007bff;
} }
input[type="button"].secondary-button:hover { input[type="button"].secondary-button:hover,
button.button.secondary-button:hover {
background-color: #EEEEEE; background-color: #EEEEEE;
color: #0056b3; color: #0056b3;
border-color: #0056b3; border-color: #0056b3;
@ -836,12 +911,14 @@ input[type="submit"]:hover {
} }
input[type="submit"]:disabled, input[type="submit"]:disabled,
input[type="button"]:disabled { input[type="button"]:disabled,
button.button:disabled {
background-color: #ccc; background-color: #ccc;
border-color: #ccc; border-color: #ccc;
} }
input[type="button"].left { input[type="button"].left
button.button.left {
margin-right: auto; margin-right: auto;
} }
@ -1509,4 +1586,4 @@ input[type="text"]:disabled {
.filtermenu-submenu-content.is-open { .filtermenu-submenu-content.is-open {
display: block; display: block;
} }