feat(life): replace multiple tags with single category for posts
Add default category support and enforce one category per Life Post Update UI filters, forms, and translations to reflect category semantics
This commit is contained in:
@@ -37,6 +37,10 @@ export interface NamedEntity {
|
||||
translations?: TranslationMap;
|
||||
}
|
||||
|
||||
export interface LifeCategory extends NamedEntity {
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface Skill extends NamedEntity {
|
||||
hasItemDrop: boolean;
|
||||
}
|
||||
@@ -256,7 +260,7 @@ export interface LifePost {
|
||||
updatedAt: string;
|
||||
author: UserSummary | null;
|
||||
updatedBy: UserSummary | null;
|
||||
tags: NamedEntity[];
|
||||
category: NamedEntity | null;
|
||||
commentPreview: LifeComment[];
|
||||
commentCount: number;
|
||||
reactionCounts: LifeReactionCounts;
|
||||
@@ -273,7 +277,7 @@ export interface LifePostsParams {
|
||||
cursor?: string | null;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
tagId?: string | number;
|
||||
categoryId?: string | number;
|
||||
language?: string;
|
||||
}
|
||||
|
||||
@@ -320,7 +324,7 @@ export interface Options {
|
||||
acquisitionMethods: NamedEntity[];
|
||||
itemTags: NamedEntity[];
|
||||
maps: NamedEntity[];
|
||||
lifeTags: NamedEntity[];
|
||||
lifeCategories: LifeCategory[];
|
||||
}
|
||||
|
||||
export interface AuthUser {
|
||||
@@ -558,7 +562,7 @@ export interface DailyChecklistPayload {
|
||||
|
||||
export interface LifePostPayload {
|
||||
body: string;
|
||||
tagIds: number[];
|
||||
categoryId: number;
|
||||
languageCode?: string | null;
|
||||
}
|
||||
|
||||
@@ -876,7 +880,7 @@ export const api = {
|
||||
cursor: params.cursor ?? undefined,
|
||||
limit: params.limit,
|
||||
search: params.search?.trim(),
|
||||
tagId: params.tagId,
|
||||
categoryId: params.categoryId,
|
||||
language: params.language
|
||||
})}`
|
||||
),
|
||||
@@ -945,13 +949,13 @@ export const api = {
|
||||
reorderDailyChecklistItems: (ids: number[]) =>
|
||||
sendJson<DailyChecklistItem[]>('/api/admin/daily-checklist/order', 'PUT', { ids }),
|
||||
deleteDailyChecklistItem: (id: string | number) => deleteJson(`/api/admin/daily-checklist/${id}`),
|
||||
config: (type: ConfigType) => getJson<Array<Skill | NamedEntity>>(`/api/admin/config/${type}`),
|
||||
createConfig: (type: ConfigType, payload: { name: string; translations?: TranslationMap; hasItemDrop?: boolean }) =>
|
||||
sendJson<Skill | NamedEntity>(`/api/admin/config/${type}`, 'POST', payload),
|
||||
config: (type: ConfigType) => getJson<Array<Skill | LifeCategory | NamedEntity>>(`/api/admin/config/${type}`),
|
||||
createConfig: (type: ConfigType, payload: { name: string; translations?: TranslationMap; hasItemDrop?: boolean; isDefault?: boolean }) =>
|
||||
sendJson<Skill | LifeCategory | NamedEntity>(`/api/admin/config/${type}`, 'POST', payload),
|
||||
reorderConfig: (type: ConfigType, ids: number[]) =>
|
||||
sendJson<Array<Skill | NamedEntity>>(`/api/admin/config/${type}/order`, 'PUT', { ids }),
|
||||
updateConfig: (type: ConfigType, id: number, payload: { name: string; translations?: TranslationMap; hasItemDrop?: boolean }) =>
|
||||
sendJson<Skill | NamedEntity>(`/api/admin/config/${type}/${id}`, 'PUT', payload),
|
||||
sendJson<Array<Skill | LifeCategory | NamedEntity>>(`/api/admin/config/${type}/order`, 'PUT', { ids }),
|
||||
updateConfig: (type: ConfigType, id: number, payload: { name: string; translations?: TranslationMap; hasItemDrop?: boolean; isDefault?: boolean }) =>
|
||||
sendJson<Skill | LifeCategory | NamedEntity>(`/api/admin/config/${type}/${id}`, 'PUT', payload),
|
||||
deleteConfig: (type: ConfigType, id: number) => deleteJson(`/api/admin/config/${type}/${id}`),
|
||||
pokemon: (params: Record<string, string | number | undefined>) =>
|
||||
getJson<Pokemon[]>(`/api/pokemon${buildQuery(params)}`),
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
type Habitat,
|
||||
type Item,
|
||||
type Language,
|
||||
type LifeCategory,
|
||||
type NamedEntity,
|
||||
type Permission,
|
||||
type PermissionPayload,
|
||||
@@ -69,7 +70,7 @@ type AdminTab =
|
||||
type AdminGroup = 'content' | 'configuration' | 'localization' | 'access';
|
||||
type AdminNavItem = { key: AdminTab; label: string; permission: string | string[] };
|
||||
type AdminNavGroup = { key: AdminGroup; label: string; items: AdminNavItem[] };
|
||||
type EditableConfig = (NamedEntity | Skill) & { hasItemDrop?: boolean };
|
||||
type EditableConfig = (NamedEntity | Skill | LifeCategory) & { hasItemDrop?: boolean; isDefault?: boolean };
|
||||
|
||||
const adminTabIcons: Record<AdminTab, AppIcon> = {
|
||||
users: iconProfile,
|
||||
@@ -132,7 +133,7 @@ const adminNavigationGroups = computed<AdminNavGroup[]>(() => {
|
||||
});
|
||||
const tabs = computed<AdminNavItem[]>(() => adminNavigationGroups.value.flatMap((group) => group.items));
|
||||
|
||||
const configTypes = computed<Array<{ key: ConfigType; label: string; supportsItemDrop?: boolean }>>(() => [
|
||||
const configTypes = computed<Array<{ key: ConfigType; label: string; supportsItemDrop?: boolean; supportsDefault?: boolean }>>(() => [
|
||||
{ key: 'pokemon-types', label: t('config.pokemonTypes') },
|
||||
{ key: 'skills', label: t('config.skills'), supportsItemDrop: true },
|
||||
{ key: 'environments', label: t('config.environments') },
|
||||
@@ -141,7 +142,7 @@ const configTypes = computed<Array<{ key: ConfigType; label: string; supportsIte
|
||||
{ key: 'item-usages', label: t('config.itemUsages') },
|
||||
{ key: 'acquisition-methods', label: t('config.acquisitionMethods') },
|
||||
{ key: 'maps', label: t('config.maps') },
|
||||
{ key: 'life-tags', label: t('config.lifeTags') }
|
||||
{ key: 'life-tags', label: t('config.lifeCategories'), supportsDefault: true }
|
||||
]);
|
||||
|
||||
const activeTab = ref<AdminTab>('config');
|
||||
@@ -162,7 +163,7 @@ const currentUser = ref<AuthUser | null>(null);
|
||||
const busy = ref(false);
|
||||
const contentLoading = ref(false);
|
||||
const message = ref('');
|
||||
const configForm = ref({ id: 0, name: '', translations: {} as TranslationMap, hasItemDrop: false });
|
||||
const configForm = ref({ id: 0, name: '', translations: {} as TranslationMap, hasItemDrop: false, isDefault: false });
|
||||
const checklistForm = ref({ id: 0, title: '', translations: {} as TranslationMap });
|
||||
const languageForm = ref({ code: '', name: '', enabled: true, isDefault: false, sortOrder: 0 });
|
||||
const wordingForm = ref({ key: '', locale: defaultLocale, value: '', defaultValue: '', placeholders: [] as string[] });
|
||||
@@ -375,7 +376,7 @@ async function loadLanguages() {
|
||||
}
|
||||
|
||||
function resetConfigForm() {
|
||||
configForm.value = { id: 0, name: '', translations: {}, hasItemDrop: false };
|
||||
configForm.value = { id: 0, name: '', translations: {}, hasItemDrop: false, isDefault: false };
|
||||
}
|
||||
|
||||
function resetChecklistForm() {
|
||||
@@ -435,7 +436,13 @@ function closeConfigModal() {
|
||||
}
|
||||
|
||||
function editConfig(item: EditableConfig) {
|
||||
configForm.value = { id: item.id, name: item.baseName ?? item.name, translations: item.translations ?? {}, hasItemDrop: item.hasItemDrop === true };
|
||||
configForm.value = {
|
||||
id: item.id,
|
||||
name: item.baseName ?? item.name,
|
||||
translations: item.translations ?? {},
|
||||
hasItemDrop: item.hasItemDrop === true,
|
||||
isDefault: item.isDefault === true
|
||||
};
|
||||
configModalOpen.value = true;
|
||||
}
|
||||
|
||||
@@ -709,7 +716,8 @@ async function saveConfig() {
|
||||
const payload = {
|
||||
name: configBaseNameForSave(),
|
||||
translations: configForm.value.translations,
|
||||
hasItemDrop: selectedConfig.value.supportsItemDrop ? configForm.value.hasItemDrop : undefined
|
||||
hasItemDrop: selectedConfig.value.supportsItemDrop ? configForm.value.hasItemDrop : undefined,
|
||||
isDefault: selectedConfig.value.supportsDefault ? configForm.value.isDefault : undefined
|
||||
};
|
||||
|
||||
if (configForm.value.id) {
|
||||
@@ -1270,7 +1278,9 @@ onMounted(() => {
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<span class="reorderable-row-title">
|
||||
{{ item.name }}<span v-if="item.hasItemDrop" class="config-flag">{{ t('pages.admin.hasItemDrop') }}</span>
|
||||
{{ item.name }}
|
||||
<span v-if="item.hasItemDrop" class="config-flag">{{ t('pages.admin.hasItemDrop') }}</span>
|
||||
<span v-if="item.isDefault" class="config-flag">{{ t('pages.admin.defaultCategory') }}</span>
|
||||
</span>
|
||||
<span class="row-actions">
|
||||
<button v-if="can('admin.config.update')" type="button" :disabled="busy" @click="editConfig(item)">
|
||||
@@ -1799,6 +1809,12 @@ onMounted(() => {
|
||||
{{ t('pages.admin.hasItemDrop') }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="selectedConfig.supportsDefault" class="check-row">
|
||||
<label>
|
||||
<input v-model="configForm.isDefault" type="checkbox" />
|
||||
{{ t('pages.admin.defaultCategory') }}
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
|
||||
@@ -35,10 +35,10 @@ import {
|
||||
type AiModerationStatus,
|
||||
type AuthUser,
|
||||
type Language,
|
||||
type LifeCategory,
|
||||
type LifeComment,
|
||||
type LifePost,
|
||||
type LifeReactionType,
|
||||
type NamedEntity
|
||||
type LifeReactionType
|
||||
} from '../services/api';
|
||||
|
||||
type LifeCommentPageState = {
|
||||
@@ -54,7 +54,7 @@ type LifeCommentPageState = {
|
||||
|
||||
const { locale, t } = useI18n();
|
||||
const posts = ref<LifePost[]>([]);
|
||||
const lifeTags = ref<NamedEntity[]>([]);
|
||||
const lifeCategories = ref<LifeCategory[]>([]);
|
||||
const languages = ref<Language[]>([]);
|
||||
const currentUser = ref<AuthUser | null>(null);
|
||||
const loading = ref(true);
|
||||
@@ -63,10 +63,10 @@ const authReady = ref(false);
|
||||
const busy = ref(false);
|
||||
const searchDraft = ref('');
|
||||
const submittedSearch = ref('');
|
||||
const activeTagId = ref('all');
|
||||
const activeCategoryId = ref('all');
|
||||
const activeLanguageCode = ref('all');
|
||||
const body = ref('');
|
||||
const selectedTagIds = ref<string[]>([]);
|
||||
const selectedCategoryId = ref('');
|
||||
const editingPostId = ref<number | null>(null);
|
||||
const postModalOpen = ref(false);
|
||||
const formError = ref('');
|
||||
@@ -97,7 +97,7 @@ let postsRequestId = 0;
|
||||
const nextCursor = ref<string | null>(null);
|
||||
const hasMorePosts = ref(false);
|
||||
const loadMorePaused = ref(false);
|
||||
const allTagValue = 'all';
|
||||
const allCategoryValue = 'all';
|
||||
const allLanguageValue = 'all';
|
||||
|
||||
const reactionOptions = [
|
||||
@@ -117,21 +117,25 @@ const canReact = computed(() => can('life.reactions.set'));
|
||||
const charactersLeft = computed(() => Math.max(0, bodyMaxLength - body.value.length));
|
||||
const isEditing = computed(() => editingPostId.value !== null);
|
||||
const searchQuery = computed(() => submittedSearch.value.trim());
|
||||
const selectedFeedTagId = computed(() => {
|
||||
const tagId = Number(activeTagId.value);
|
||||
return activeTagId.value === allTagValue || !Number.isInteger(tagId) || tagId <= 0 ? undefined : tagId;
|
||||
const selectedFeedCategoryId = computed(() => {
|
||||
const categoryId = Number(activeCategoryId.value);
|
||||
return activeCategoryId.value === allCategoryValue || !Number.isInteger(categoryId) || categoryId <= 0 ? undefined : categoryId;
|
||||
});
|
||||
const selectedFeedLanguageCode = computed(() =>
|
||||
activeLanguageCode.value === allLanguageValue ? undefined : activeLanguageCode.value
|
||||
);
|
||||
const tagFilterOptions = computed<TabOption[]>(() => [
|
||||
{ value: allTagValue, label: t('pages.life.allTags') },
|
||||
...lifeTags.value.map((tag) => ({ value: String(tag.id), label: tag.name }))
|
||||
const categoryFilterOptions = computed<TabOption[]>(() => [
|
||||
{ value: allCategoryValue, label: t('pages.life.allCategories') },
|
||||
...lifeCategories.value.map((category) => ({ value: String(category.id), label: category.name }))
|
||||
]);
|
||||
const languageFilterOptions = computed<TabOption[]>(() => [
|
||||
{ value: allLanguageValue, label: t('pages.life.allLanguages') },
|
||||
...languages.value.map((language) => ({ value: language.code, label: language.name }))
|
||||
]);
|
||||
const defaultLifeCategoryId = computed(() => {
|
||||
const category = lifeCategories.value.find((item) => item.isDefault);
|
||||
return category ? String(category.id) : '';
|
||||
});
|
||||
const postModalTitle = computed(() => (isEditing.value ? t('pages.life.editPost') : t('pages.life.newPost')));
|
||||
const submitLabel = computed(() => {
|
||||
if (busy.value) return isEditing.value ? t('pages.life.updating') : t('pages.life.publishing');
|
||||
@@ -158,13 +162,19 @@ async function loadCurrentUser() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLifeTags() {
|
||||
async function loadLifeCategories() {
|
||||
try {
|
||||
const options = await api.options();
|
||||
lifeTags.value = options.lifeTags;
|
||||
lifeCategories.value = options.lifeCategories;
|
||||
|
||||
if (activeTagId.value !== allTagValue && !lifeTags.value.some((tag) => String(tag.id) === activeTagId.value)) {
|
||||
activeTagId.value = allTagValue;
|
||||
if (activeCategoryId.value !== allCategoryValue && !lifeCategories.value.some((category) => String(category.id) === activeCategoryId.value)) {
|
||||
activeCategoryId.value = allCategoryValue;
|
||||
}
|
||||
if (!isEditing.value && postModalOpen.value && !selectedCategoryId.value) {
|
||||
selectedCategoryId.value = defaultLifeCategoryId.value;
|
||||
}
|
||||
if (!isEditing.value && selectedCategoryId.value && !lifeCategories.value.some((category) => String(category.id) === selectedCategoryId.value)) {
|
||||
selectedCategoryId.value = defaultLifeCategoryId.value;
|
||||
}
|
||||
} catch (error) {
|
||||
loadError.value = error instanceof Error && error.message ? error.message : t('errors.loadFailed');
|
||||
@@ -199,7 +209,7 @@ async function loadPosts() {
|
||||
const page = await api.lifePosts({
|
||||
limit: lifePostPageSize,
|
||||
search: searchQuery.value,
|
||||
tagId: selectedFeedTagId.value,
|
||||
categoryId: selectedFeedCategoryId.value,
|
||||
language: selectedFeedLanguageCode.value
|
||||
});
|
||||
if (requestId !== postsRequestId) {
|
||||
@@ -241,7 +251,7 @@ async function loadMorePosts() {
|
||||
cursor,
|
||||
limit: lifePostPageSize,
|
||||
search: searchQuery.value,
|
||||
tagId: selectedFeedTagId.value,
|
||||
categoryId: selectedFeedCategoryId.value,
|
||||
language: selectedFeedLanguageCode.value
|
||||
});
|
||||
if (requestId !== postsRequestId) {
|
||||
@@ -266,7 +276,7 @@ async function loadMorePosts() {
|
||||
|
||||
function resetForm() {
|
||||
body.value = '';
|
||||
selectedTagIds.value = [];
|
||||
selectedCategoryId.value = '';
|
||||
editingPostId.value = null;
|
||||
formError.value = '';
|
||||
}
|
||||
@@ -274,13 +284,14 @@ function resetForm() {
|
||||
function payload() {
|
||||
return {
|
||||
body: body.value.trim(),
|
||||
tagIds: selectedLifeTagIds(),
|
||||
categoryId: selectedLifeCategoryId() ?? 0,
|
||||
languageCode: selectedFeedLanguageCode.value ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function selectedLifeTagIds() {
|
||||
return selectedTagIds.value.map((tagId) => Number(tagId)).filter((tagId) => Number.isInteger(tagId) && tagId > 0);
|
||||
function selectedLifeCategoryId() {
|
||||
const categoryId = Number(selectedCategoryId.value);
|
||||
return Number.isInteger(categoryId) && categoryId > 0 ? categoryId : null;
|
||||
}
|
||||
|
||||
function submitSearch() {
|
||||
@@ -314,16 +325,17 @@ function retryLoadMore() {
|
||||
|
||||
function matchesCurrentFilters(post: LifePost) {
|
||||
const keyword = searchQuery.value.toLowerCase();
|
||||
const tagId = selectedFeedTagId.value;
|
||||
const categoryId = selectedFeedCategoryId.value;
|
||||
const matchesSearch = keyword === '' || post.body.toLowerCase().includes(keyword);
|
||||
const matchesTag = tagId === undefined || post.tags.some((tag) => tag.id === tagId);
|
||||
const matchesCategory = categoryId === undefined || post.category?.id === categoryId;
|
||||
const matchesLanguage =
|
||||
selectedFeedLanguageCode.value === undefined || post.moderationLanguageCode === selectedFeedLanguageCode.value;
|
||||
return matchesSearch && matchesTag && matchesLanguage;
|
||||
return matchesSearch && matchesCategory && matchesLanguage;
|
||||
}
|
||||
|
||||
function openCreatePostModal() {
|
||||
resetForm();
|
||||
selectedCategoryId.value = defaultLifeCategoryId.value;
|
||||
postModalOpen.value = true;
|
||||
void nextTick(() => bodyInput.value?.focus());
|
||||
}
|
||||
@@ -344,9 +356,9 @@ async function submitPost() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedLifeTagIds().length === 0) {
|
||||
formError.value = t('pages.life.tagRequired');
|
||||
document.getElementById('life-post-tags')?.focus();
|
||||
if (selectedLifeCategoryId() === null) {
|
||||
formError.value = t('pages.life.categoryRequired');
|
||||
document.getElementById('life-post-category')?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -696,7 +708,7 @@ async function toggleReaction(post: LifePost, reactionType: LifeReactionType) {
|
||||
function startEdit(post: LifePost) {
|
||||
editingPostId.value = post.id;
|
||||
body.value = post.body;
|
||||
selectedTagIds.value = post.tags.map((tag) => String(tag.id));
|
||||
selectedCategoryId.value = post.category ? String(post.category.id) : '';
|
||||
formError.value = '';
|
||||
postModalOpen.value = true;
|
||||
void nextTick(() => bodyInput.value?.focus());
|
||||
@@ -879,7 +891,7 @@ function observeLoadMore() {
|
||||
}
|
||||
|
||||
watch([loadMoreSentinel, hasMorePosts, loading, loadingMore, loadMorePaused], observeLoadMore, { flush: 'post' });
|
||||
watch(activeTagId, () => {
|
||||
watch(activeCategoryId, () => {
|
||||
void loadPosts();
|
||||
});
|
||||
watch(activeLanguageCode, () => {
|
||||
@@ -889,7 +901,7 @@ watch(activeLanguageCode, () => {
|
||||
});
|
||||
watch(locale, () => {
|
||||
void loadLanguages();
|
||||
void loadLifeTags();
|
||||
void loadLifeCategories();
|
||||
void loadPosts();
|
||||
});
|
||||
|
||||
@@ -898,7 +910,7 @@ onMounted(() => {
|
||||
document.addEventListener('keydown', closeReactionPickerFromKeyboard);
|
||||
void loadCurrentUser();
|
||||
void loadLanguages();
|
||||
void loadLifeTags();
|
||||
void loadLifeCategories();
|
||||
void loadPosts();
|
||||
removeAuthListener = onAuthTokenChange(() => {
|
||||
void loadCurrentUser();
|
||||
@@ -981,13 +993,14 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="life-post-tags">{{ t('pages.life.tags') }}</label>
|
||||
<label for="life-post-category">{{ t('pages.life.category') }}</label>
|
||||
<TagsSelect
|
||||
id="life-post-tags"
|
||||
v-model="selectedTagIds"
|
||||
:options="lifeTags"
|
||||
:placeholder="t('pages.life.tagPlaceholder')"
|
||||
:search-placeholder="t('pages.life.searchTags')"
|
||||
id="life-post-category"
|
||||
v-model="selectedCategoryId"
|
||||
:options="lifeCategories"
|
||||
:multiple="false"
|
||||
:placeholder="t('pages.life.categoryPlaceholder')"
|
||||
:search-placeholder="t('pages.life.searchCategories')"
|
||||
dropdown-strategy="fixed"
|
||||
/>
|
||||
</div>
|
||||
@@ -1015,7 +1028,7 @@ onUnmounted(() => {
|
||||
</Modal>
|
||||
|
||||
<Tabs id="life-language-filter" v-model="activeLanguageCode" :tabs="languageFilterOptions" :label="t('pages.life.languages')" />
|
||||
<Tabs id="life-tag-filter" v-model="activeTagId" :tabs="tagFilterOptions" :label="t('pages.life.tags')" />
|
||||
<Tabs id="life-category-filter" v-model="activeCategoryId" :tabs="categoryFilterOptions" :label="t('pages.life.category')" />
|
||||
|
||||
<section class="life-feed" :aria-busy="loading || loadingMore" :aria-label="t('pages.life.kicker')">
|
||||
<div v-if="loading" class="life-feed__list" :aria-label="t('pages.life.loading')">
|
||||
@@ -1083,8 +1096,8 @@ onUnmounted(() => {
|
||||
|
||||
<p class="life-post__body">{{ post.body }}</p>
|
||||
|
||||
<div v-if="post.tags.length" class="life-post__tags" :aria-label="t('pages.life.tags')">
|
||||
<span v-for="tag in post.tags" :key="tag.id" class="life-post__tag">{{ tag.name }}</span>
|
||||
<div v-if="post.category" class="life-post__tags" :aria-label="t('pages.life.category')">
|
||||
<span class="life-post__tag">{{ post.category.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="life-post__engagement">
|
||||
|
||||
@@ -593,7 +593,7 @@ function contentTypeLabel(contentType: string): string {
|
||||
'item-usages': t('config.itemUsages'),
|
||||
'acquisition-methods': t('config.acquisitionMethods'),
|
||||
maps: t('config.maps'),
|
||||
'life-tags': t('config.lifeTags')
|
||||
'life-tags': t('config.lifeCategories')
|
||||
};
|
||||
return labels[contentType] ?? t('pages.profile.otherContributions');
|
||||
}
|
||||
@@ -726,8 +726,8 @@ onMounted(() => {
|
||||
|
||||
<p class="life-post__body">{{ post.body }}</p>
|
||||
|
||||
<div v-if="post.tags.length" class="life-post__tags" :aria-label="t('pages.life.tags')">
|
||||
<span v-for="tag in post.tags" :key="tag.id" class="life-post__tag">{{ tag.name }}</span>
|
||||
<div v-if="post.category" class="life-post__tags" :aria-label="t('pages.life.category')">
|
||||
<span class="life-post__tag">{{ post.category.name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="profile-feed-card__metrics">
|
||||
|
||||
Reference in New Issue
Block a user