feat(badges): zibll-style badge modal with category grid, detail view, and wear UX
This commit is contained in:
@@ -911,6 +911,7 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
|
||||
|| (function_exists('aksrt_is_user_home_page') && aksrt_is_user_home_page())
|
||||
|| (function_exists('aksrt_is_user_center_page') && aksrt_is_user_center_page());
|
||||
if ($is_badge_surface) {
|
||||
$enqueue_module('medal');
|
||||
$badges_js_path_global = get_template_directory() . '/assets/js/user-badges.js';
|
||||
if (file_exists($badges_js_path_global) && !wp_script_is('aksrt-user-badges', 'enqueued')) {
|
||||
wp_enqueue_script(
|
||||
|
||||
@@ -145,7 +145,7 @@ class UserRuntime {
|
||||
* verify:array{enabled:bool,public_display:bool,allow_apply:bool,disable_desc:string,desc:string,types:array<string,array{slug:string,label:string,badge_text:string,fee:float,free_applications:int,require_real_name:bool}>},
|
||||
* level:array{enabled:bool,public_display:bool,source:string,label_prefix:string,thresholds:array<int,int>},
|
||||
* medal:array{enabled:bool,public_display:bool,limit:int,categories:array<int,array{cat_name:string,items:array<int,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int}>}>,items_by_slug:array<string,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int,cat_name:string}>},
|
||||
* badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array<int,array{slug:string,label:string,tone:string,icon_url?:string}>},
|
||||
* badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array<int,array{slug:string,label:string,tone:string,desc:string,icon_url?:string}>},
|
||||
* report:array{enabled:bool,public_entry:bool,public_notice:bool,ban_notice_text:string,categories:array<int,array{slug:string,label:string}>}
|
||||
* }
|
||||
*/
|
||||
@@ -204,6 +204,7 @@ class UserRuntime {
|
||||
'slug' => $item['slug'],
|
||||
'label' => $item['name'],
|
||||
'tone' => 'info',
|
||||
'desc' => (string) ($item['desc'] ?? ''),
|
||||
'icon_url' => $item['icon'],
|
||||
];
|
||||
}, $medal['items_by_slug'])),
|
||||
|
||||
+235
-103
@@ -9,6 +9,10 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class Badges {
|
||||
|
||||
/**
|
||||
* Owned badges storage (3.4.0-dev rewrite): user meta holds an
|
||||
* associative map `slug => grant time (Y-m-d H:i:s)`, newest first.
|
||||
*/
|
||||
public const META_OWNED = 'aksrt_user_badges';
|
||||
public const META_WEAR = 'aksrt_wear_badge';
|
||||
|
||||
@@ -95,106 +99,173 @@ class Badges {
|
||||
}
|
||||
|
||||
/**
|
||||
* zibll zib_get_user_medal_info_modal lite.
|
||||
* Badge list modal HTML (zibll zib_get_user_medal_info_modal parity).
|
||||
*
|
||||
* Head (avatar + owner + worn medal) + category-grouped medal card grid.
|
||||
* Each card deep-links into the single badge modal via the remote-modal
|
||||
* contract (data-toggle="AksrtRemoteModal" + data-remote).
|
||||
*/
|
||||
public static function render_info_modal_html(int $user_id): string {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return '<div class="aksrt-badge-modal"><p>' . esc_html__('用户不存在', 'aksrt') . '</p></div>';
|
||||
return '<div class="medal-modal"><p class="medal-modal__empty">' . esc_html__('用户不存在', 'aksrt') . '</p></div>';
|
||||
}
|
||||
$current = get_current_user_id();
|
||||
$is_self = $current > 0 && $current === $user_id;
|
||||
$display = $is_self ? __('我', 'aksrt') : $user->display_name;
|
||||
$avatar = function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($user_id, ['size' => 64])
|
||||
: get_avatar_url($user_id, ['size' => 64]);
|
||||
$data = self::list_for_user($user_id, $is_self || self::can_manually_set($current));
|
||||
$items = is_array($data['items'] ?? null) ? $data['items'] : [];
|
||||
$wear = (string) ($data['wear'] ?? '');
|
||||
? aksrt_get_avatar_url($user_id, ['size' => 80])
|
||||
: get_avatar_url($user_id, ['size' => 80]);
|
||||
$data = self::list_for_user($user_id, $is_self || self::can_manually_set($current));
|
||||
$categories = is_array($data['categories'] ?? null) ? $data['categories'] : [];
|
||||
$wear = (string) ($data['wear'] ?? '');
|
||||
$map = self::get_dictionary_map();
|
||||
|
||||
$html = '<div class="aksrt-badge-modal">';
|
||||
$html .= '<div class="aksrt-badge-modal__head">';
|
||||
$html .= '<img class="aksrt-badge-modal__avatar" src="' . esc_url($avatar) . '" alt="" width="40" height="40" />';
|
||||
$html .= '<div class="aksrt-badge-modal__title">' . esc_html(sprintf(__('%s的徽章', 'aksrt'), $display));
|
||||
if ($wear !== '') {
|
||||
$map = self::get_dictionary_map();
|
||||
if (isset($map[$wear])) {
|
||||
$html .= ' <span class="aksrt-badge-modal__wear-tag">' . esc_html(sprintf(__('佩戴:%s', 'aksrt'), (string) $map[$wear]['label'])) . '</span>';
|
||||
// Head slot: worn medal icon with tooltip, or an empty-state tag (zibll).
|
||||
$wear_slot = '';
|
||||
if ($wear !== '' && isset($map[$wear])) {
|
||||
$wear_label = (string) ($map[$wear]['label'] ?? $wear);
|
||||
$wear_icon = (string) ($map[$wear]['icon_url'] ?? '');
|
||||
if ($wear_icon !== '') {
|
||||
$wear_slot = '<img class="medal-icon img-icon ml3" src="' . esc_url($wear_icon) . '" title="' . esc_attr(sprintf(__('当前佩戴:%s', 'aksrt'), $wear_label)) . '" alt="' . esc_attr($wear_label) . '" />';
|
||||
}
|
||||
} else {
|
||||
$html .= ' <span class="badg badg-sm c-red">' . esc_html__('暂无徽章', 'aksrt') . '</span>';
|
||||
} elseif ((int) ($data['count'] ?? 0) === 0) {
|
||||
$wear_slot = '<span class="badg badg-sm c-red ml6">' . esc_html__('暂无徽章', 'aksrt') . '</span>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
$html = '<div class="medal-modal">';
|
||||
$html .= '<button type="button" class="aksrt-remote-modal__close" data-aksrt-remote-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">×</button>';
|
||||
$html .= '<div class="medal-modal__head">';
|
||||
$html .= '<img class="medal-modal__avatar" src="' . esc_url((string) $avatar) . '" alt="" width="42" height="42" />';
|
||||
$html .= '<div class="medal-modal__title"><span class="em09">' . esc_html(sprintf(__('%s的徽章', 'aksrt'), $display)) . '</span>' . $wear_slot . '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="aksrt-badge-modal__list mini-scrollbar">';
|
||||
if (!$items) {
|
||||
$html .= '<div class="aksrt-badge-modal__empty">' . esc_html__('暂无徽章', 'aksrt') . '</div>';
|
||||
$html .= '<div class="medal-modal__scroll mini-scrollbar">';
|
||||
if (!$categories) {
|
||||
$html .= '<div class="empty-state empty-state--sm">'
|
||||
. '<div class="empty-state__icon"><i class="fas fa-award" aria-hidden="true"></i></div>'
|
||||
. '<div class="empty-state__title">' . esc_html__('暂无徽章', 'aksrt') . '</div>'
|
||||
. '<div class="empty-state__desc">' . esc_html__('积极参与社区互动即可获得徽章', 'aksrt') . '</div>'
|
||||
. '</div>';
|
||||
} else {
|
||||
foreach ($items as $item) {
|
||||
$slug = (string) ($item['slug'] ?? '');
|
||||
$label = (string) ($item['label'] ?? $slug);
|
||||
$icon = (string) ($item['icon_url'] ?? '');
|
||||
$is_wear = !empty($item['wear']);
|
||||
$html .= '<div class="aksrt-badge-modal__item' . ($is_wear ? ' is-wear' : '') . '" data-slug="' . esc_attr($slug) . '">';
|
||||
if ($icon !== '') {
|
||||
$html .= '<img class="medal-icon img-icon" src="' . esc_url($icon) . '" alt="" width="40" height="40" loading="lazy" />';
|
||||
} else {
|
||||
$html .= '<span class="aksrt-badge-modal__icon-fa"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
foreach ($categories as $cat) {
|
||||
$html .= '<div class="medal-cat-box">';
|
||||
$html .= '<div class="medal-cat-head"><b class="medal-cat-name">' . esc_html((string) ($cat['cat_name'] ?? '')) . '</b></div>';
|
||||
$html .= '<div class="medal-list-box">';
|
||||
foreach ((array) ($cat['items'] ?? []) as $item) {
|
||||
$html .= self::render_card_link($user_id, is_array($item) ? $item : []);
|
||||
}
|
||||
$html .= '<div class="aksrt-badge-modal__meta"><div class="aksrt-badge-modal__name">' . esc_html($label) . '</div>';
|
||||
if ($is_wear) {
|
||||
$html .= '<div class="em09 c-blue">' . esc_html__('当前佩戴', 'aksrt') . '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
if ($is_self && !$is_wear) {
|
||||
$html .= '<button type="button" class="but jb-blue but-sm" data-aksrt-badge-wear="' . esc_attr($slug) . '">' . esc_html__('佩戴此徽章', 'aksrt') . '</button>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
$html .= '</div></div>';
|
||||
}
|
||||
}
|
||||
$html .= '</div></div>';
|
||||
$html .= '</div>';
|
||||
|
||||
// Footer: quick jump to my own badges while browsing someone else (zibll).
|
||||
if (!$is_self && $current > 0) {
|
||||
$my_url = add_query_arg(['action' => 'aksrt_user_badge_info_modal', 'id' => $current], admin_url('admin-ajax.php'));
|
||||
$html .= '<div class="medal-modal__buts">';
|
||||
$html .= '<a class="but c-blue" href="' . esc_url($my_url) . '" data-toggle="AksrtRemoteModal" data-remote="' . esc_url($my_url) . '" data-class="full-sm" mobile-bottom="true"><i class="fas fa-medal" aria-hidden="true"></i> ' . esc_html__('查看我的徽章', 'aksrt') . '</a>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single badge card modal.
|
||||
* Single medal card inside the list grid; deep-links to the detail modal.
|
||||
*
|
||||
* @param array{slug?:string,label?:string,icon_url?:string,owned?:bool,wear?:bool} $item
|
||||
*/
|
||||
protected static function render_card_link(int $user_id, array $item): string {
|
||||
$slug = (string) ($item['slug'] ?? '');
|
||||
$label = (string) ($item['label'] ?? $slug);
|
||||
$icon = (string) ($item['icon_url'] ?? '');
|
||||
$owned = !empty($item['owned']);
|
||||
$wear = !empty($item['wear']);
|
||||
if ($slug === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$icon_html = $icon !== ''
|
||||
? '<img class="medal-icon img-icon" src="' . esc_url($icon) . '" alt="' . esc_attr($label) . '" loading="lazy" decoding="async" />'
|
||||
: '<span class="medal-icon medal-icon--fa"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
|
||||
$card = '<div class="medal-card' . ($owned ? ' is-has' : ' no-has') . ($wear ? ' is-wear' : '') . '">';
|
||||
$card .= $icon_html;
|
||||
$card .= '<div class="medal-card__name">' . esc_html($label) . '</div>';
|
||||
if ($wear) {
|
||||
$card .= '<div class="medal-card__wear">' . esc_html__('佩戴中', 'aksrt') . '</div>';
|
||||
}
|
||||
$card .= '</div>';
|
||||
|
||||
$url = add_query_arg(
|
||||
['action' => 'aksrt_single_badge_info_modal', 'name' => $slug, 'user_id' => $user_id],
|
||||
admin_url('admin-ajax.php')
|
||||
);
|
||||
|
||||
return '<a class="medal-list" href="' . esc_url($url) . '" data-toggle="AksrtRemoteModal" data-remote="' . esc_url($url) . '" data-class="modal-mini" mobile-bottom="true">' . $card . '</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Single badge detail modal (zibll zib_get_single_medal_info_modal parity).
|
||||
*
|
||||
* medal-background.svg backdrop + large icon + name + desc + obtain time,
|
||||
* with wear action for the owner and a back link into the list modal.
|
||||
*/
|
||||
public static function render_single_modal_html(string $slug, int $user_id = 0): string {
|
||||
$map = self::get_dictionary_map();
|
||||
$map = self::get_dictionary_map();
|
||||
$slug = sanitize_key($slug);
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
return '<div class="aksrt-badge-modal"><p>' . esc_html__('徽章不存在', 'aksrt') . '</p></div>';
|
||||
return '<div class="medal-modal medal-modal--single"><p class="medal-modal__empty">' . esc_html__('徽章不存在', 'aksrt') . '</p></div>';
|
||||
}
|
||||
$item = $map[$slug];
|
||||
$current = get_current_user_id();
|
||||
$is_self = $user_id > 0 && $current === $user_id;
|
||||
$owned = $user_id > 0 ? self::get_owned($user_id) : [];
|
||||
$has = in_array($slug, $owned, true);
|
||||
$wear = $user_id > 0 ? self::get_wear($user_id) : '';
|
||||
$is_wear = $has && $wear === $slug;
|
||||
$item = $map[$slug];
|
||||
$current = get_current_user_id();
|
||||
$is_self = $user_id > 0 && $current === $user_id;
|
||||
$owned_map = $user_id > 0 ? self::get_owned_map($user_id) : [];
|
||||
$has = isset($owned_map[$slug]);
|
||||
$is_wear = $has && self::get_wear($user_id) === $slug;
|
||||
|
||||
$html = '<div class="aksrt-badge-modal aksrt-badge-modal--single">';
|
||||
$html .= '<button type="button" class="aksrt-remote-modal__close" data-aksrt-remote-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">×</button>';
|
||||
$html .= '<div class="aksrt-badge-modal__card">';
|
||||
if (!empty($item['icon_url'])) {
|
||||
$html .= '<img class="medal-icon img-icon" src="' . esc_url((string) $item['icon_url']) . '" alt="" width="72" height="72" />';
|
||||
} else {
|
||||
$html .= '<span class="aksrt-badge-modal__icon-fa lg"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
}
|
||||
$html .= '<div class="aksrt-badge-modal__name">' . esc_html((string) $item['label']) . '</div>';
|
||||
$label = (string) ($item['label'] ?? $slug);
|
||||
$desc = (string) ($item['desc'] ?? '');
|
||||
$icon = (string) ($item['icon_url'] ?? '');
|
||||
$bg = get_template_directory_uri() . '/assets/img/medal/medal-background.svg';
|
||||
|
||||
$icon_html = $icon !== ''
|
||||
? '<img class="medal-icon img-icon" src="' . esc_url($icon) . '" alt="' . esc_attr($label) . '" loading="lazy" decoding="async" />'
|
||||
: '<span class="medal-icon medal-icon--fa lg"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
|
||||
// Obtain state line (self only, zibll).
|
||||
$state = '';
|
||||
if ($is_self) {
|
||||
$html .= $has
|
||||
? '<div class="c-blue em09 mt6">' . esc_html__('已获得', 'aksrt') . '</div>'
|
||||
: '<div class="c-yellow em09 mt6">' . esc_html__('暂未获得', 'aksrt') . '</div>';
|
||||
$state = $has
|
||||
? '<div class="medal-single-state c-blue">' . esc_html(sprintf(__('%s 获得', 'aksrt'), (string) $owned_map[$slug])) . '</div>'
|
||||
: '<div class="medal-single-state c-yellow">' . esc_html__('暂未获得', 'aksrt') . '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
$html = '<div class="medal-modal medal-modal--single">';
|
||||
$html .= '<button type="button" class="aksrt-remote-modal__close" data-aksrt-remote-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">×</button>';
|
||||
$html .= '<div class="medal-single-card">';
|
||||
$html .= '<div class="medal-single-bg"><img class="fit-cover" src="' . esc_url($bg) . '" alt="" /></div>';
|
||||
$html .= '<div class="medal-single-main">' . $icon_html;
|
||||
$html .= '<div class="medal-single-name">' . esc_html($label) . '</div>';
|
||||
if ($desc !== '') {
|
||||
$html .= '<div class="medal-single-desc muted-2-color">' . esc_html($desc) . '</div>';
|
||||
}
|
||||
$html .= $state . '</div></div>';
|
||||
|
||||
if ($is_self && $has && !$is_wear) {
|
||||
$html .= '<div class="aksrt-badge-modal__actions"><button type="button" class="but jb-blue btn-block" data-aksrt-badge-wear="' . esc_attr($slug) . '">' . esc_html__('佩戴此徽章', 'aksrt') . '</button></div>';
|
||||
$html .= '<div class="medal-modal__actions"><button type="button" class="but jb-blue btn-block radius" data-aksrt-badge-wear="' . esc_attr($slug) . '">' . esc_html__('佩戴此徽章', 'aksrt') . '</button></div>';
|
||||
} elseif ($is_self && $is_wear) {
|
||||
$html .= '<div class="aksrt-badge-modal__actions"><span class="badg btn-block c-blue">' . esc_html__('当前佩戴此徽章', 'aksrt') . '</span></div>';
|
||||
$html .= '<div class="medal-modal__actions"><span class="badg btn-block c-blue">' . esc_html__('当前佩戴此徽章', 'aksrt') . '</span></div>';
|
||||
}
|
||||
|
||||
// Back to the full badge list modal.
|
||||
if ($user_id > 0) {
|
||||
$back_url = add_query_arg(['action' => 'aksrt_user_badge_info_modal', 'id' => $user_id], admin_url('admin-ajax.php'));
|
||||
$html .= '<a class="medal-modal__back muted-2-color em09" href="' . esc_url($back_url) . '" data-toggle="AksrtRemoteModal" data-remote="' . esc_url($back_url) . '" data-class="full-sm" mobile-bottom="true"><i class="fas fa-angle-left" aria-hidden="true"></i> ' . esc_html__('返回徽章列表', 'aksrt') . '</a>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
@@ -300,7 +371,7 @@ class Badges {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{slug:string,label:string,tone:string,icon_url:string}>
|
||||
* @return list<array{slug:string,label:string,tone:string,desc:string,icon_url:string}>
|
||||
*/
|
||||
public static function get_dictionary(): array {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
@@ -318,6 +389,7 @@ class Badges {
|
||||
'slug' => $slug,
|
||||
'label' => (string) ($item['label'] ?? $slug),
|
||||
'tone' => (string) ($item['tone'] ?? 'neutral'),
|
||||
'desc' => (string) ($item['desc'] ?? ''),
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
];
|
||||
}
|
||||
@@ -325,7 +397,7 @@ class Badges {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array{slug:string,label:string,tone:string,icon_url:string}>
|
||||
* @return array<string,array{slug:string,label:string,tone:string,desc:string,icon_url:string}>
|
||||
*/
|
||||
public static function get_dictionary_map(): array {
|
||||
$map = [];
|
||||
@@ -336,48 +408,64 @@ class Badges {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
* Owned badge map: slug => grant time (`Y-m-d H:i:s`), newest first.
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public static function get_owned(int $user_id): array {
|
||||
public static function get_owned_map(int $user_id): array {
|
||||
if ($user_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
$raw = get_user_meta($user_id, self::META_OWNED, true);
|
||||
if (!is_array($raw)) {
|
||||
$raw = preg_split('/[\s,]+/', (string) $raw, -1, PREG_SPLIT_NO_EMPTY);
|
||||
return [];
|
||||
}
|
||||
$map = self::get_dictionary_map();
|
||||
$out = [];
|
||||
foreach ((array) $raw as $slug) {
|
||||
foreach ($raw as $slug => $time) {
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$out[] = $slug;
|
||||
$out[$slug] = is_string($time) ? $time : '';
|
||||
}
|
||||
return array_values(array_unique($out));
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owned badge slugs, newest first.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function get_owned(int $user_id): array {
|
||||
return array_keys(self::get_owned_map($user_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the owned set; existing slugs keep their grant time, new slugs
|
||||
* are stamped with the current time. Order follows the input list.
|
||||
*
|
||||
* @param list<string> $slugs
|
||||
*/
|
||||
public static function set_owned(int $user_id, array $slugs): void {
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
$map = self::get_dictionary_map();
|
||||
$clean = [];
|
||||
$map = self::get_dictionary_map();
|
||||
$existing = self::get_owned_map($user_id);
|
||||
$now = current_time('Y-m-d H:i:s');
|
||||
$clean = [];
|
||||
foreach ($slugs as $slug) {
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug !== '' && isset($map[$slug])) {
|
||||
$clean[] = $slug;
|
||||
if ($slug === '' || !isset($map[$slug]) || isset($clean[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$clean[$slug] = $existing[$slug] ?? $now;
|
||||
}
|
||||
$clean = array_values(array_unique($clean));
|
||||
update_user_meta($user_id, self::META_OWNED, $clean);
|
||||
|
||||
$wear = self::get_wear($user_id);
|
||||
if ($wear !== '' && !in_array($wear, $clean, true)) {
|
||||
if ($wear !== '' && !isset($clean[$wear])) {
|
||||
delete_user_meta($user_id, self::META_WEAR);
|
||||
}
|
||||
}
|
||||
@@ -441,8 +529,8 @@ class Badges {
|
||||
if (in_array($slug, $owned, true)) {
|
||||
return true;
|
||||
}
|
||||
$owned[] = $slug;
|
||||
self::set_owned($user_id, $owned);
|
||||
// Newest first (zibll medal ordering); grant time stamped by set_owned.
|
||||
self::set_owned($user_id, array_merge([$slug], $owned));
|
||||
/**
|
||||
* Fires after a badge is granted.
|
||||
*
|
||||
@@ -491,54 +579,98 @@ class Badges {
|
||||
}
|
||||
|
||||
/**
|
||||
* Public DTO for REST / modals.
|
||||
* Public DTO for REST / modals: catalog grouped by category with per-item
|
||||
* ownership state. Unearned badges are only exposed to the owner (or to
|
||||
* managers via $include_private), matching zibll visibility rules.
|
||||
*
|
||||
* @return array{enabled:bool,public_display:bool,wear:string,items:list<array>}
|
||||
* @return array{enabled:bool,public_display:bool,is_self:bool,can_wear:bool,user_id:int,wear:string,count:int,show_unearned:bool,categories:list<array{cat_name:string,items:list<array{slug:string,label:string,desc:string,icon_url:string,owned:bool,wear:bool,time:string}>}>}
|
||||
*/
|
||||
public static function list_for_user(int $user_id, bool $include_private = false): array {
|
||||
$enabled = self::enabled();
|
||||
$public = self::public_display();
|
||||
$viewer = get_current_user_id();
|
||||
$is_self = $viewer > 0 && $viewer === $user_id;
|
||||
$can_see = $include_private
|
||||
|| $public
|
||||
|| ($viewer > 0 && $viewer === $user_id)
|
||||
|| $is_self
|
||||
|| self::can_manually_set($viewer);
|
||||
|
||||
if (!$enabled || !$can_see || $user_id <= 0) {
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'public_display' => $public,
|
||||
'is_self' => $is_self,
|
||||
'can_wear' => $is_self,
|
||||
'user_id' => $user_id,
|
||||
'wear' => '',
|
||||
'items' => [],
|
||||
'count' => 0,
|
||||
'show_unearned' => false,
|
||||
'categories' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$map = self::get_dictionary_map();
|
||||
$owned = self::get_owned($user_id);
|
||||
$wear = self::get_wear($user_id);
|
||||
// Prefer worn first (Zibll).
|
||||
if ($wear !== '') {
|
||||
$owned = array_values(array_unique(array_merge([$wear], $owned)));
|
||||
}
|
||||
$items = [];
|
||||
foreach ($owned as $slug) {
|
||||
if (!isset($map[$slug])) {
|
||||
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
|
||||
$medal = is_array($settings['medal'] ?? null) ? $settings['medal'] : [];
|
||||
$catalog = is_array($medal['categories'] ?? null) ? $medal['categories'] : [];
|
||||
|
||||
$owned_map = self::get_owned_map($user_id);
|
||||
$wear = self::get_wear($user_id);
|
||||
$show_unearned = $include_private || $is_self || self::can_manually_set($viewer);
|
||||
|
||||
$categories = [];
|
||||
foreach ($catalog as $cat) {
|
||||
$cat_name = (string) ($cat['cat_name'] ?? '');
|
||||
$rows = is_array($cat['items'] ?? null) ? $cat['items'] : [];
|
||||
$has_rows = [];
|
||||
$no_rows = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$slug = sanitize_key((string) ($row['slug'] ?? ''));
|
||||
if ($slug === '') {
|
||||
continue;
|
||||
}
|
||||
$has = isset($owned_map[$slug]);
|
||||
if (!$has && !$show_unearned) {
|
||||
continue;
|
||||
}
|
||||
$item = [
|
||||
'slug' => $slug,
|
||||
'label' => (string) ($row['name'] ?? $slug),
|
||||
'desc' => (string) ($row['desc'] ?? ''),
|
||||
'icon_url' => (string) ($row['icon'] ?? ''),
|
||||
'owned' => $has,
|
||||
'wear' => $has && $slug === $wear,
|
||||
'time' => $has ? (string) $owned_map[$slug] : '',
|
||||
];
|
||||
// Owned first within each category (zibll is-has / no-has order).
|
||||
if ($has) {
|
||||
$has_rows[] = $item;
|
||||
} else {
|
||||
$no_rows[] = $item;
|
||||
}
|
||||
}
|
||||
$items = array_merge($has_rows, $no_rows);
|
||||
if (!$items) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'slug' => $slug,
|
||||
'label' => $map[$slug]['label'],
|
||||
'tone' => $map[$slug]['tone'],
|
||||
'icon_url' => $map[$slug]['icon_url'],
|
||||
'wear' => ($slug === $wear),
|
||||
$categories[] = [
|
||||
'cat_name' => $cat_name,
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'public_display' => $public,
|
||||
'is_self' => $is_self,
|
||||
'can_wear' => $is_self,
|
||||
'user_id' => $user_id,
|
||||
'wear' => $wear,
|
||||
'items' => $items,
|
||||
'count' => count($owned_map),
|
||||
'show_unearned' => $show_unearned,
|
||||
'categories' => $categories,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+24
-15
@@ -166,20 +166,30 @@ class Home {
|
||||
if (class_exists('AKSRT\\User\\Badges')) {
|
||||
$list = Badges::list_for_user($user_id, true);
|
||||
$badge_count = 0;
|
||||
foreach ((array) ($list['items'] ?? []) as $item) {
|
||||
if ($badge_count >= $limit) {
|
||||
$done = false;
|
||||
foreach ((array) ($list['categories'] ?? []) as $cat) {
|
||||
foreach ((array) ($cat['items'] ?? []) as $item) {
|
||||
if (empty($item['owned'])) {
|
||||
continue;
|
||||
}
|
||||
if ($badge_count >= $limit) {
|
||||
$done = true;
|
||||
break;
|
||||
}
|
||||
$signals[] = [
|
||||
'type' => 'badge',
|
||||
'slug' => (string) ($item['slug'] ?? ''),
|
||||
'label' => (string) ($item['label'] ?? ''),
|
||||
'icon' => 'award',
|
||||
'tone' => 'info',
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
'wear' => !empty($item['wear']),
|
||||
];
|
||||
$badge_count++;
|
||||
}
|
||||
if ($done) {
|
||||
break;
|
||||
}
|
||||
$signals[] = [
|
||||
'type' => 'badge',
|
||||
'slug' => (string) ($item['slug'] ?? ''),
|
||||
'label' => (string) ($item['label'] ?? ''),
|
||||
'icon' => 'award',
|
||||
'tone' => (string) ($item['tone'] ?? 'neutral'),
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
'wear' => !empty($item['wear']),
|
||||
];
|
||||
$badge_count++;
|
||||
}
|
||||
} else {
|
||||
$badge_map = [];
|
||||
@@ -189,9 +199,8 @@ class Home {
|
||||
}
|
||||
}
|
||||
$assigned = get_user_meta($user_id, 'aksrt_user_badges', true);
|
||||
if (!is_array($assigned)) {
|
||||
$assigned = preg_split('/[\s,]+/', (string) $assigned, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
// Storage shape: associative map slug => grant time.
|
||||
$assigned = is_array($assigned) ? array_keys($assigned) : [];
|
||||
$wear = sanitize_key((string) get_user_meta($user_id, 'aksrt_wear_badge', true));
|
||||
$assigned = array_values(array_filter(array_map(static function ($slug) {
|
||||
return sanitize_key((string) $slug);
|
||||
|
||||
Reference in New Issue
Block a user