Prechádzať zdrojové kódy

Ticket #5076 - Profile badge. Core.

AntonLV 1 rok pred
rodič
commit
82bb877e5c

+ 7 - 0
modules/base/groups/classes/BxBaseModGroupsTemplate.php

@@ -69,6 +69,8 @@ class BxBaseModGroupsTemplate extends BxBaseModProfileTemplate
         if($this->_oConfig->isUseCoverAsThumb())
             $aParams['show_avatar'] = false;
 
+        $aParams['show_badge'] = false;
+
         return parent::prepareCover($aData, $aParams);
     }     
 
@@ -164,6 +166,11 @@ class BxBaseModGroupsTemplate extends BxBaseModProfileTemplate
         return $bResult;
     }
 
+    protected function _isUnitBadge($aData, $sTemplateName = 'unit.html')
+    {
+        return false;
+    }
+
     function _image ($sField, $sTranscodeObject, $sNoImage, $aData, $bSubstituteNoImage = true)
     {
         $CNF = &$this->_oConfig->CNF;

+ 8 - 0
modules/base/profile/classes/BxBaseModProfileFormEntry.php

@@ -59,6 +59,14 @@ class BxBaseModProfileFormEntry extends BxBaseModGeneralFormEntry
             );
         }
 
+        if (($sKey = 'FIELD_BADGE') && !empty($CNF[$sKey]) && isset($this->aInputs[$CNF[$sKey]])) {
+            $this->_aImageFields[$CNF[$sKey]] = [
+                'storage_object' => $CNF['OBJECT_STORAGE_BADGE'],
+                'images_transcoder' => $CNF['OBJECT_IMAGES_TRANSCODER_BADGE'],
+                'uploaders' => $CNF['OBJECT_UPLOADERS_PICTURE'],
+            ];
+        }
+
         foreach ($this->_aImageFields as $sField => $aParams) {
             $this->aInputs[$sField]['storage_object'] = $aParams['storage_object'];
             $this->aInputs[$sField]['uploaders'] = !empty($this->aInputs[$sField]['value']) ? unserialize($this->aInputs[$sField]['value']) : $aParams['uploaders'];

+ 2 - 0
modules/base/profile/classes/BxBaseModProfileFormsEntryHelper.php

@@ -161,6 +161,8 @@ class BxBaseModProfileFormsEntryHelper extends BxBaseModGeneralFormsEntryHelper
             $oForm->processFiles($CNF['FIELD_PICTURE'], $iContentId, false);
         if (isset($CNF['FIELD_COVER']))
             $oForm->processFiles($CNF['FIELD_COVER'], $iContentId, false);
+        if (isset($CNF['FIELD_BADGE']))
+            $oForm->processFiles($CNF['FIELD_BADGE'], $iContentId, false);
 
         if(isset($CNF['FIELD_ALLOW_POST_TO']) && !empty($aContentInfo[$CNF['FIELD_ALLOW_POST_TO']]) && ($oPrivacy = BxDolPrivacy::getObjectInstance($CNF['OBJECT_PRIVACY_POST'])) !== false)
             $oPrivacy->reassociateGroupCustomWithContent($oProfile->id(), $iContentId, (int)$aContentInfo[$CNF['FIELD_ALLOW_POST_TO']]);

+ 38 - 1
modules/base/profile/classes/BxBaseModProfileModule.php

@@ -1073,7 +1073,12 @@ class BxBaseModProfileModule extends BxBaseModGeneralModule implements iBxDolCon
     {
         return $this->_serviceEntityForm('editDataForm', $iContentId, $this->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT_COVER']);
     }
-    
+
+    public function serviceEntityEditBadge($iContentId = 0)
+    {
+        return $this->_serviceEntityForm('editDataForm', $iContentId, $this->_oConfig->CNF['OBJECT_FORM_ENTRY_DISPLAY_EDIT_BADGE'], 'checkAllowedChangeBadge');
+    }
+
     public function serviceEntityEditSettings($iContentId = 0)
     {
         $CNF = &$this->_oConfig->CNF;
@@ -1783,6 +1788,14 @@ class BxBaseModProfileModule extends BxBaseModGeneralModule implements iBxDolCon
         return $this->checkAllowedViewProfileImage($aDataEntry);
     }
     
+    /**
+     * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
+     */
+    public function checkAllowedViewBadgeImage ($aDataEntry, $isPerformAction = false)
+    {
+        return $this->checkAllowedViewProfileImage($aDataEntry);
+    }
+    
     /**
      * @return CHECK_ACTION_RESULT_ALLOWED if access is granted or error message if access is forbidden.
      */
@@ -1822,6 +1835,30 @@ class BxBaseModProfileModule extends BxBaseModGeneralModule implements iBxDolCon
         return _t('_sys_txt_access_denied');
     }
 
+    public function checkAllowedChangeBadge ($aDataEntry, $isPerformAction = false)
+    {
+        if(empty($aDataEntry) || !is_array($aDataEntry))
+            return _t('_sys_txt_not_found');
+
+        // moderator always has access
+        if($this->_isModerator($isPerformAction))
+            return CHECK_ACTION_RESULT_ALLOWED;
+
+        // owner (checked by account instead of author!) always have access
+        $oProfile = BxDolProfile::getInstanceByContentAndType($aDataEntry[$this->_oConfig->CNF['FIELD_ID']], $this->_aModule['name']);
+        if(!$oProfile)
+            return _t('_sys_txt_error_occured');
+
+        if($oProfile->getAccountId() != $this->_iAccountId)
+            return _t('_sys_txt_access_denied');
+
+        $aCheck = checkActionModule($this->_iProfileId, 'change badge', $this->getName(), false);
+        if($aCheck[CHECK_ACTION_RESULT] !== CHECK_ACTION_RESULT_ALLOWED)
+            return $aCheck[CHECK_ACTION_MESSAGE];
+
+        return CHECK_ACTION_RESULT_ALLOWED;
+    }
+
     /**
      * Check if user can change cover image
      */

+ 67 - 15
modules/base/profile/classes/BxBaseModProfileTemplate.php

@@ -150,14 +150,6 @@ class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
 
         $bPublic = $isCheckPrivateContent && !empty($CNF['OBJECT_PRIVACY_VIEW']) ? $this->isProfilePublic($aData) : true;
 
-        $bPublicThumb = true;
-        if($isCheckPrivateContent && $oModule->checkAllowedViewProfileImage($aData) !== CHECK_ACTION_RESULT_ALLOWED)
-            $bPublicThumb = false;
-
-        $bPublicCover = true;
-        if($isCheckPrivateContent && $oModule->checkAllowedViewCoverImage($aData) !== CHECK_ACTION_RESULT_ALLOWED)
-            $bPublicCover = false;
-
         $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $this->MODULE);
         $iProfile = $oProfile->id();
 
@@ -166,24 +158,34 @@ class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
 
         // get profile's title
         $sTitle = $oModule->serviceProfileName($iContentId);
+        $sTitleAttr = bx_html_attribute($sTitle);
 
         $sText = $sSummary = '';
         if(!empty($CNF['FIELD_TEXT']) && !empty($aData[$CNF['FIELD_TEXT']])) {
             $sText = $this->getText($aData);
             $sSummary = $this->getSummary($aData, $sTitle, $sText, $sUrl);
         }
-        
-        $sCoverUrl = $bPublicCover ? $this->urlCoverUnit($aData, false) : '';
+
+        $sCoverUrl = '';
+        if(!$isCheckPrivateContent || $oModule->checkAllowedViewCoverImage($aData) === CHECK_ACTION_RESULT_ALLOWED)
+            $sCoverUrl = $this->urlCoverUnit($aData, false);
         $bCoverUrl = !empty($sCoverUrl);
 
         if(empty($sCoverUrl) && ($iCoverId = (int)getParam('sys_unit_cover_profile')) != 0)
             $sCoverUrl = BxDolTranscoder::getObjectInstance(BX_DOL_TRANSCODER_OBJ_COVER_UNIT_PROFILE)->getFileUrlById($iCoverId);
         if(empty($sCoverUrl))
             $sCoverUrl = $this->getImageUrl('cover.svg');
-        
-        $sThumbUrl = $this->_isUnitThumb($aData, $sTemplate) && $bPublicThumb ? $this->_getUnitThumbUrl($sTemplateSize, $aData, false) : '';
+
+        $sThumbUrl = '';
+        if($this->_isUnitThumb($aData, $sTemplate) && (!$isCheckPrivateContent || $oModule->checkAllowedViewProfileImage($aData) === CHECK_ACTION_RESULT_ALLOWED))
+            $sThumbUrl = $this->_getUnitThumbUrl($sTemplateSize, $aData, false);
         $bThumbUrl = !empty($sThumbUrl);
 
+        $sBadgeUrl = '';
+        if($this->_isUnitBadge($aData, $sTemplate) && (!$isCheckPrivateContent || $oModule->checkAllowedViewBadgeImage($aData) === CHECK_ACTION_RESULT_ALLOWED))
+            $sBadgeUrl = $this->_getUnitBadgeUrl($sTemplateSize, $aData, false);
+        $bBadgeUrl = !empty($sBadgeUrl);
+
         $aTmplVarsThumbnail = array(
             'class_size' => '',
             'bx_if:show_thumb_image' => array(
@@ -236,11 +238,20 @@ class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
                     'title' => $sTitle,
                 )
             ),
+            'bx_if:show_badge' => [
+                'condition' => $bBadgeUrl,
+                'content' => $bBadgeUrl ? [
+                    'size' => $sTemplateSize,
+                    'badge_url' => $sBadgeUrl,
+                    'badge_link' => $aData[$CNF['FIELD_BADGE_LINK']],
+                    'title_attr' => $sTitleAttr,
+                ] : []
+            ],
             'content_url' => $bPublic ? $sUrl : 'javascript:void(0);',
             'content_click' => !$bPublic ? 'javascript:bx_alert(' . bx_js_string('"' . _t('_sys_access_denied_to_private_content') . '"') . ');' : '',
             'content_target' => !empty($aParams['link_target']) ? $aParams['link_target'] : '_self',
             'title' => $sTitle,
-            'title_attr' => bx_html_attribute($sTitle),
+            'title_attr' => $sTitleAttr,
             'addon' => !empty($aData['addon']) ? $aData['addon'] : '',
             'module_name' => _t($CNF['T']['txt_sample_single']),
             'ts' => $aData[$CNF['FIELD_ADDED']],
@@ -315,6 +326,7 @@ class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
         $sShowData = isset($aParams['show_data']) ? $aParams['show_data'] : '';
         $bShowCover = !isset($aParams['show_cover']) || $aParams['show_cover'] === true;
         $bShowAvatar = !isset($aParams['show_avatar']) || $aParams['show_avatar'] === true;
+        $bShowBadge = !isset($aParams['show_badge']) || $aParams['show_badge'] === true;
         $sAddCode = "";
         
 
@@ -528,6 +540,22 @@ class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
                 ]), true, true);
             }
         }
+        
+        //--- Process Badge
+        $bTmplVarsShowBadge = false;
+        $aTmplVarsShowBadge = [];
+
+        if($bShowBadge) {
+            $sUrlBadge = $this->_image($CNF['FIELD_BADGE'], $CNF['OBJECT_IMAGES_TRANSCODER_BADGE'], 'no-picture-thumb.png', $aData, false);
+
+            if(($bTmplVarsShowBadge = !empty($sUrlBadge))) 
+                $aTmplVarsShowBadge = [
+                    'size' => 'ava-big',
+                    'badge_url' => $sUrlBadge,
+                    'badge_link' => $aData[$CNF['FIELD_BADGE_LINK']],
+                    'title_attr' => bx_html_attribute($sTitle),
+                ];
+        }
 
         //--- Process Actions menu
         $sActionsMenu = '';
@@ -558,13 +586,21 @@ class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
             'bx_if:show_title_as_tag' => [
                 'condition' => !$bUseAsAuthor,
                 'content' => [
-                    'title' => $sTitle
+                    'title' => $sTitle,
+                    'bx_if:show_badge' => [
+                        'condition' => $bTmplVarsShowBadge,
+                        'content' => $aTmplVarsShowBadge
+                    ]
                 ]
             ],
             'bx_if:show_title_as_text' => [
                 'condition' => $bUseAsAuthor,
                 'content' => [
-                    'title' => $sTitle
+                    'title' => $sTitle,
+                    'bx_if:show_badge' => [
+                        'condition' => $bTmplVarsShowBadge,
+                        'content' => $aTmplVarsShowBadge
+                    ]
                 ]
             ],
             'bx_if:show_avatar' => [
@@ -575,6 +611,10 @@ class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
                 'condition' => !$bTmplVarsShowAvatar,
                 'content' => [true]
             ],
+            'bx_if:show_badge' => [
+                'condition' => $bTmplVarsShowBadge,
+                'content' => $aTmplVarsShowBadge
+            ],
             'badges' => $oModule->serviceGetBadges($aData[$CNF['FIELD_ID']]),
             'action_menu' => $sActionsMenu,
             'meta' => $sMetaMenu,
@@ -822,6 +862,18 @@ class BxBaseModProfileTemplate extends BxBaseModGeneralTemplate
         else
             return $this->_getUnitThumbUrl($this->_sUnitSizeDefault, $aData, $bSubstituteNoImage);
     }
+    
+    protected function _isUnitBadge($aData, $sTemplateName = 'unit.html')
+    {
+        return true;
+    }
+
+    protected function _getUnitBadgeUrl($sSize, $aData, $bSubstituteNoImage = true)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        return $this->_image($CNF['FIELD_BADGE'], $CNF['OBJECT_IMAGES_TRANSCODER_BADGE'], 'no-picture-thumb.png', $aData, $bSubstituteNoImage);
+    }
 }
 
 /** @} */

+ 88 - 0
modules/base/profile/classes/BxBaseModProfileUploaderBadgeCrop.php

@@ -0,0 +1,88 @@
+<?php defined('BX_DOL') or die('hack attempt');
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    BaseProfile Base classes for profile modules
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+class BxBaseModProfileUploaderBadgeCrop extends BxTemplUploaderCrop
+{
+    protected $_sModule;
+    protected $_oModule;
+
+    public function __construct ($aObject, $sStorageObject, $sUniqId, $oTemplate)
+    {
+        if(!empty($this->_sModule) && empty($this->_oModule))
+            $this->_oModule = BxDolModule::getInstance($this->_sModule);
+
+        parent::__construct($aObject, $sStorageObject, $sUniqId, $oTemplate ? $oTemplate : $this->_oModule->_oTemplate);
+    }
+
+    public function deleteGhostsForProfile($iProfileId, $iContentId = false)
+    {
+        if (!$iContentId)
+            return parent::deleteGhostsForProfile($iProfileId, $iContentId);
+
+        $CNF = $this->_oModule->_oConfig->CNF;
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+        
+        $iCount = 0;
+
+        $oStorage = BxDolStorage::getObjectInstance($this->_sStorageObject);
+
+        $aGhosts = $oStorage->getGhosts($iProfileId, $iContentId, $iContentId ? true : false);
+        foreach ($aGhosts as $aFile) {
+            // for cover image delete only unassigned ghosts and currently set covers
+            if ($aFile['id'] == $aContentInfo[$CNF['FIELD_BADGE']] || !in_array($aFile['id'], [$aContentInfo[$CNF['FIELD_PICTURE']], $aContentInfo[$CNF['FIELD_COVER']]]))
+                $iCount += $oStorage->deleteFile($aFile['id']);
+        }
+
+        return $iCount;
+    }
+    
+    public function getGhosts($iProfileId, $sFormat, $sImagesTranscoder = false, $iContentId = false)
+    {
+        $s = parent::getGhosts($iProfileId, $sFormat, $sImagesTranscoder, $iContentId);
+        if (!$s || !$iContentId) // if we're creating new profile return all ghosts
+            return $s;
+
+        $a = array();
+        if ($sFormat == 'array')
+            $a = $s;
+        else if ($sFormat == 'json')
+            $a = json_decode($s, true);
+
+        if (!$a)
+            return $s;
+
+        // filter out thumbnails
+        $CNF = $this->_oModule->_oConfig->CNF;
+        $aContentInfo = $this->_oModule->_oDb->getContentInfoById($iContentId);
+        $aResult = array();
+        foreach ($a as $aFile) {
+            // for cover image show only unassigned ghosts and currently set covers
+            if ($aFile['file_id'] == $aContentInfo[$CNF['FIELD_BADGE']] || !in_array($aFile['file_id'], [$aContentInfo[$CNF['FIELD_PICTURE']], $aContentInfo[$CNF['FIELD_COVER']]]))
+                $aResult[$aFile['file_id']] = $aFile;
+        }
+
+        if ('array' == $sFormat) {
+            return $aResult;
+        }
+        else if ('json' == $sFormat) {
+            return json_encode($aResult);
+        } else { // html format is not suported for this data type
+            return false;
+        }
+    }
+
+    protected function isAdmin ($iContentId = 0)
+    {
+        return $this->_oModule->_isModerator (false);
+    }
+}
+
+/** @} */

+ 2 - 2
modules/base/profile/classes/BxBaseModProfileUploaderCoverCrop.php

@@ -40,7 +40,7 @@ class BxBaseModProfileUploaderCoverCrop extends BxTemplUploaderHTML5
         foreach ($aGhosts as $aFile) {
 
             // for cover image delete only unassigned ghosts and currently set covers
-            if ($aFile['id'] == $aContentInfo[$CNF['FIELD_COVER']] || $aFile['id'] != $aContentInfo[$CNF['FIELD_PICTURE']])
+            if ($aFile['id'] == $aContentInfo[$CNF['FIELD_COVER']] || !in_array($aFile['id'], [$aContentInfo[$CNF['FIELD_BADGE']], $aContentInfo[$CNF['FIELD_PICTURE']]]))
                 $iCount += $oStorage->deleteFile($aFile['id']);
 
         }
@@ -70,7 +70,7 @@ class BxBaseModProfileUploaderCoverCrop extends BxTemplUploaderHTML5
         foreach ($a as $aFile) {
 
             // for cover image show only unassigned ghosts and currently set covers
-            if ($aFile['file_id'] == $aContentInfo[$CNF['FIELD_COVER']] || $aFile['file_id'] != $aContentInfo[$CNF['FIELD_PICTURE']])
+            if ($aFile['file_id'] == $aContentInfo[$CNF['FIELD_COVER']] || !in_array(aFile['file_id'], [$aContentInfo[$CNF['FIELD_BADGE']], $aContentInfo[$CNF['FIELD_PICTURE']]]))
                 $aResult[$aFile['file_id']] = $aFile;
 
         }

+ 2 - 2
modules/base/profile/classes/BxBaseModProfileUploaderPictureCrop.php

@@ -39,7 +39,7 @@ class BxBaseModProfileUploaderPictureCrop extends BxTemplUploaderCrop
         foreach ($aGhosts as $aFile) {
 
             // for profile image delete only unassigned ghosts and currently set profile pictures
-            if ($aFile['id'] == $aContentInfo[$CNF['FIELD_PICTURE']] || $aFile['id'] != $aContentInfo[$CNF['FIELD_COVER']])
+            if ($aFile['id'] == $aContentInfo[$CNF['FIELD_PICTURE']] || !in_array($aFile['id'], [$aContentInfo[$CNF['FIELD_BADGE']], $aContentInfo[$CNF['FIELD_COVER']]]))
                 $iCount += $oStorage->deleteFile($aFile['id']);
 
         }
@@ -69,7 +69,7 @@ class BxBaseModProfileUploaderPictureCrop extends BxTemplUploaderCrop
         foreach ($a as $aFile) {
 
             // for profile pictures show only unassigned ghosts and currently set profile pictures
-            if (($aFile['file_id'] == $aContentInfo[$CNF['FIELD_PICTURE']] || $aFile['file_id'] != $aContentInfo[$CNF['FIELD_COVER']]))
+            if (($aFile['file_id'] == $aContentInfo[$CNF['FIELD_PICTURE']] || !in_array($aFile['file_id'], [$aContentInfo[$CNF['FIELD_BADGE']], $aContentInfo[$CNF['FIELD_COVER']]])))
                 $aResult[$aFile['file_id']] = $aFile;            
         }
 

+ 8 - 1
modules/base/profile/template/cover.html

@@ -23,7 +23,14 @@
                     </bx_if:show_avatar>
                     <div class="bx-base-pofile-cover-user bx-def-margin-sec-topbottom">
                         <div class="bx-base-profile-cover-title-bages">
-                            <h1 class="bx-def-unit-info">__title__ </h1>
+                            <h1 class="bx-def-unit-info">
+                                __title__
+                                <bx_if:show_badge>
+                                    <a class="bx-base-pofile-unit-badge bx-def-__size__-badge-size" href="__badge_link__" title="__title_attr__">
+                                        <img class="bx-base-pofile-unit-badge bx-def-__size__-badge-size rounded-full box-border" src="__badge_url__" alt="__title_attr__" />
+                                    </a>
+                                </bx_if:show_badge>
+                            </h1>
                             <div class="bx-base-profile-cover-meta flex flex-row flex-wrap justify-center lg:justify-start text-sm">__meta__</div>
                         </div>
                         <div class="bx-base-pofile-cover-actions bx-def-margin-sec-leftright-neg" onclick="event.stopPropagation();">__action_menu__</div>

+ 16 - 2
modules/base/profile/template/cover_block.html

@@ -28,10 +28,24 @@
                 <div class="bx-base-pofile-cover-user bx-def-margin-sec-topbottom">
                     <div class="bx-base-profile-cover-title-bages">
                         <bx_if:show_title_as_tag>
-                            <h1 class="bx-def-unit-info">__title__</h1>
+                            <h1 class="bx-def-unit-info">
+                                __title__
+                                <bx_if:show_badge>
+                                    <a class="bx-base-pofile-unit-badge bx-def-__size__-badge-size" href="__badge_link__" title="__title_attr__">
+                                        <img class="bx-base-pofile-unit-badge bx-def-__size__-badge-size rounded-full box-border" src="__badge_url__" alt="__title_attr__" />
+                                    </a>
+                                </bx_if:show_badge>
+                            </h1>
                         </bx_if:show_title_as_tag>
                         <bx_if:show_title_as_text>
-                            <div class="bx-def-unit-info bx-def-font-h1">__title__</div>
+                            <div class="bx-def-unit-info bx-def-font-h1">
+                                __title__
+                                <bx_if:show_badge>
+                                    <a class="bx-base-pofile-unit-badge bx-def-__size__-badge-size" href="__badge_link__" title="__title_attr__">
+                                        <img class="bx-base-pofile-unit-badge bx-def-__size__-badge-size rounded-full box-border" src="__badge_url__" alt="__title_attr__" />
+                                    </a>
+                                </bx_if:show_badge>
+                            </div>
                         </bx_if:show_title_as_text>
                         <div class="bx-base-profile-cover-bages bx-base-bages-container">__badges__</div>
                     </div>

+ 5 - 0
modules/base/profile/template/unit.html

@@ -16,6 +16,11 @@
         <div class="bx-def-unit-info-cnt box-border overflow-hidden">
             <div class="bx-def-unit-line truncate">
                 <a class="bx-def-unit-info-title" href="__content_url__" target="__content_target__" onclick="__content_click__">__title__</a>
+                <bx_if:show_badge>
+                    <a class="bx-base-pofile-unit-badge bx-def-__size__-badge-size" href="__badge_link__" title="__title_attr__">
+                        <img class="bx-base-pofile-unit-badge bx-def-__size__-badge-size rounded-full box-border" src="__badge_url__" alt="__title_attr__" />
+                    </a>
+                </bx_if:show_badge>
             </div>
             <div class="bx-def-unit-line text-sm text-gray-500 dark:text-gray-400">__addon__</div>
         </div>

+ 7 - 1
modules/base/profile/template/unit_with_cover.html

@@ -17,7 +17,13 @@
             </bx_if:show_thumbnail>
         </a>
         <div class="bx-base-pofile-unit-title p-4 box-border text-center truncate">
-            <a href="__content_url__" onclick="__content_click__" class="hover:underline text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white font-semibold">__title__</a>__badges__
+            <a href="__content_url__" onclick="__content_click__" class="hover:underline text-gray-700 dark:text-gray-200 hover:text-gray-900 dark:hover:text-white font-semibold">__title__</a>
+            <bx_if:show_badge>
+                <a class="bx-base-pofile-unit-badge bx-def-__size__-badge-size" href="__badge_link__" title="__title_attr__">
+                    <img class="bx-base-pofile-unit-badge bx-def-__size__-badge-size rounded-full box-border" src="__badge_url__" alt="__title_attr__" />
+                </a>
+            </bx_if:show_badge>
+            __badges__
         </div>
         <span class="text-sm text-gray-500 dark:text-gray-400">__addon__</span>
         <div class="bx-base-pofile-unit-info">

+ 3 - 0
modules/base/profile/template/unit_wo_links.html

@@ -12,5 +12,8 @@
     </div>
     <div class="bx-def-unit-info bx-def-__size__-size-min-height bx-def-__size__-size-max-height flex items-center max-w-32 ml-4">
         <span class="bx-def-unit-info-title">__title__</span>
+        <bx_if:show_badge>
+            <img class="bx-base-pofile-unit-badge bx-def-__size__-badge-size rounded-full box-border" src="__badge_url__" alt="__title_attr__" />
+        </bx_if:show_badge>
     </div>
 </div>

+ 8 - 1
modules/boonex/artificer/data/template/mod_profile/cover.html

@@ -28,7 +28,14 @@
                     </bx_if:show_avatar_placeholder>
                     <div class="bx-base-pofile-cover-user relative flex flex-col lg:flex-row lg:items-start lg:justify-end mt-6 sm:pb-1 lg:w-full box-border">
                         <div class="bx-base-profile-cover-title-bages min-w-0 flex-1 lg:mt-14 lg:mr-12 mb-2">
-                            <h1 class="bx-def-unit-info pl-1 text-gray-900 dark:text-gray-50 text-center ltr:lg:text-left rtl:lg:text-right text-4xl leading-14 font-bold truncate">__title__ </h1>
+                            <h1 class="bx-def-unit-info pl-1 text-gray-900 dark:text-gray-50 text-center ltr:lg:text-left rtl:lg:text-right text-4xl leading-14 font-bold truncate">
+                                __title__
+                                <bx_if:show_badge>
+                                    <a class="bx-base-pofile-unit-badge bx-def-__size__-badge-size" href="__badge_link__" title="__title_attr__">
+                                        <img class="bx-base-pofile-unit-badge bx-def-__size__-badge-size rounded-full box-border" src="__badge_url__" alt="__title_attr__" />
+                                    </a>
+                                </bx_if:show_badge>
+                            </h1>
                             <div class="bx-base-profile-cover-meta-holder mt-2 lg:h-8">
                                 <div class="bx-base-profile-cover-meta lg:absolute flex flex-row flex-wrap justify-center lg:justify-start text-sm">__meta__</div>
                             </div>

+ 16 - 2
modules/boonex/artificer/data/template/mod_profile/cover_block.html

@@ -32,10 +32,24 @@
                 <div class="bx-base-pofile-cover-user relative flex flex-col flex-colmt-6 pt-6 sm:pb-1">
                     <div class="bx-base-profile-cover-title-bages min-w-0 flex-1 mb-4">
                         <bx_if:show_title_as_tag>
-                            <h1 class="bx-def-unit-info text-2xl font-bold text-gray-900 dark:text-gray-50 text-center sm:truncate">__title__</h1>
+                            <h1 class="bx-def-unit-info text-2xl font-bold text-gray-900 dark:text-gray-50 text-center sm:truncate">
+                                __title__
+                                <bx_if:show_badge>
+                                    <a class="bx-base-pofile-unit-badge bx-def-__size__-badge-size" href="__badge_link__" title="__title_attr__">
+                                        <img class="bx-base-pofile-unit-badge bx-def-__size__-badge-size rounded-full box-border" src="__badge_url__" alt="__title_attr__" />
+                                    </a>
+                                </bx_if:show_badge>
+                            </h1>
                         </bx_if:show_title_as_tag>
                         <bx_if:show_title_as_text>
-                            <div class="bx-def-unit-info text-2xl font-bold text-gray-900 dark:text-gray-50 text-center sm:truncate">__title__</div>
+                            <div class="bx-def-unit-info text-2xl font-bold text-gray-900 dark:text-gray-50 text-center sm:truncate">
+                                __title__
+                                <bx_if:show_badge>
+                                    <a class="bx-base-pofile-unit-badge bx-def-__size__-badge-size" href="__badge_link__" title="__title_attr__">
+                                        <img class="bx-base-pofile-unit-badge bx-def-__size__-badge-size rounded-full box-border" src="__badge_url__" alt="__title_attr__" />
+                                    </a>
+                                </bx_if:show_badge>
+                            </div>
                         </bx_if:show_title_as_text>
                         <div class="bx-base-profile-cover-meta-holder">
                             <div class="bx-base-profile-cover-meta flex flex-row flex-wrap justify-center mt-6 md:mt-2 -ml-1  text-base font-medium">__meta__</div>

+ 1 - 0
tailwind.config.js

@@ -63,6 +63,7 @@ module.exports = {
     'bx-def-unit-alert', 'bx-def-unit-alert-small', 'bx-def-unit-alert-middle',
     'bx-def-label',
     'bx-def-icon-size', 'bx-def-thumb-size', 'bx-def-ava-size', 'bx-def-ava-big-size',
+    'bx-def-icon-badge-size', 'bx-def-thumb-badge-size', 'bx-def-ava-badge-size', 'bx-def-ava-big-badge-size',
     'bx-def-color-bg-box-active', 
 
     'bx-form-caption', 'bx-form-value', 'bx-form-required', 'bx-form-warn', 

+ 13 - 0
template/css/tailwind.css

@@ -1276,6 +1276,15 @@
     }
 
     /* Common elements: Units */
+    .bx-def-icon-badge-size,
+    .bx-def-thumb-badge-size,
+    .bx-def-ava-badge-size {
+        @apply h-6 w-6;
+    }
+    .bx-def-ava-big-badge-size {
+        @apply h-8 w-8;
+    }
+
     .bx-artificer .bx-def-icon-size,
     .bx-artificer .bx-profile-avatar .bx-def-thumb-size,
     .bx-artificer .bx-profile-stats .bx-ps-user .bx-def-thumb-size {
@@ -1311,6 +1320,10 @@
         @apply text-lg lg:text-xl font-semibold;
     }
 
+    .bx-base-pofile-unit-badge {
+        @apply inline-flex align-middle;
+    }
+
     .bx-artificer .bx-base-pofile-unit-online {
         @apply absolute w-2.5 h-2.5 right-0.5 bottom-0.5 ring-1 ring-inset ring-white dark:ring-gray-800 bg-green-500 rounded-full;
     }

+ 2 - 2
template/css/uploaders.css

@@ -17,8 +17,8 @@
     display: flex;
     flex-direction: column;
     position: relative;
-    min-width: 100px;
-    min-height: 40px;
+    min-width: 6.25rem;
+    min-height: 6.25rem;
 }
 
 .bx-form-input-files-result{