refactor(life): remove life categories and ratings
Drop life_tags and life_post_ratings tables and related schema Remove category selection and rating UI from Life posts Simplify Life feed filters and API endpoints
This commit is contained in:
@@ -85,10 +85,6 @@ const changeLabelKeys: Record<string, string> = {
|
||||
有掉落物: 'pages.admin.hasItemDrop',
|
||||
'Has trading': 'pages.admin.hasTrading',
|
||||
'有 Trading': 'pages.admin.hasTrading',
|
||||
'Default category': 'pages.admin.defaultCategory',
|
||||
默认分类: 'pages.admin.defaultCategory',
|
||||
Rateable: 'pages.admin.rateableCategory',
|
||||
可评分: 'pages.admin.rateableCategory',
|
||||
ChangeLog: 'pages.admin.changeLog'
|
||||
};
|
||||
|
||||
|
||||
@@ -74,11 +74,6 @@ export interface NamedEntity {
|
||||
translations?: TranslationMap;
|
||||
}
|
||||
|
||||
export interface LifeCategory extends NamedEntity {
|
||||
isDefault: boolean;
|
||||
isRateable: boolean;
|
||||
}
|
||||
|
||||
export interface GameVersion extends NamedEntity {
|
||||
changeLog: string;
|
||||
}
|
||||
@@ -494,11 +489,7 @@ export interface LifePost {
|
||||
updatedAt: string;
|
||||
author: UserSummary | null;
|
||||
updatedBy: UserSummary | null;
|
||||
category: (NamedEntity & { isRateable: boolean }) | null;
|
||||
gameVersion: GameVersion | null;
|
||||
ratingAverage: number | null;
|
||||
ratingCount: number;
|
||||
myRating: number | null;
|
||||
commentPreview: LifeComment[];
|
||||
commentCount: number;
|
||||
reactionCounts: LifeReactionCounts;
|
||||
@@ -515,11 +506,9 @@ export interface LifePostsParams {
|
||||
cursor?: string | null;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
categoryId?: string | number;
|
||||
language?: string;
|
||||
gameVersionId?: string | number;
|
||||
rateable?: boolean | null;
|
||||
sort?: 'latest' | 'oldest' | 'top-rated';
|
||||
sort?: 'latest' | 'oldest';
|
||||
}
|
||||
|
||||
export interface CommentPageParams {
|
||||
@@ -775,7 +764,6 @@ export interface Options {
|
||||
acquisitionMethods: NamedEntity[];
|
||||
itemTags: NamedEntity[];
|
||||
maps: NamedEntity[];
|
||||
lifeCategories: LifeCategory[];
|
||||
gameVersions: GameVersion[];
|
||||
dishFlavors: NamedEntity[];
|
||||
}
|
||||
@@ -935,7 +923,6 @@ export type ConfigType =
|
||||
| 'favorite-things'
|
||||
| 'acquisition-methods'
|
||||
| 'maps'
|
||||
| 'life-tags'
|
||||
| 'game-versions'
|
||||
| 'dish-flavors';
|
||||
|
||||
@@ -1059,7 +1046,6 @@ export interface DailyChecklistPayload {
|
||||
|
||||
export interface LifePostPayload {
|
||||
body: string;
|
||||
categoryId: number;
|
||||
gameVersionId?: number | null;
|
||||
languageCode?: string | null;
|
||||
}
|
||||
@@ -1395,10 +1381,8 @@ export const api = {
|
||||
cursor: params.cursor ?? undefined,
|
||||
limit: params.limit,
|
||||
search: params.search,
|
||||
categoryId: params.categoryId,
|
||||
language: params.language,
|
||||
gameVersionId: params.gameVersionId,
|
||||
rateable: params.rateable === null ? undefined : params.rateable,
|
||||
sort: params.sort
|
||||
})}`
|
||||
),
|
||||
@@ -1456,10 +1440,8 @@ export const api = {
|
||||
cursor: params.cursor ?? undefined,
|
||||
limit: params.limit,
|
||||
search: params.search?.trim(),
|
||||
categoryId: params.categoryId,
|
||||
language: params.language,
|
||||
gameVersionId: params.gameVersionId,
|
||||
rateable: params.rateable === null ? undefined : params.rateable,
|
||||
sort: params.sort
|
||||
})}`
|
||||
),
|
||||
@@ -1531,9 +1513,6 @@ export const api = {
|
||||
sendJson<ThreadSummary>(`/api/admin/threads/${id}/lock`, 'PUT', { locked }),
|
||||
deleteThread: (id: string | number) => deleteJson(`/api/admin/threads/${id}`),
|
||||
deleteThreadMessage: (id: string | number) => deleteJson(`/api/admin/thread-messages/${id}`),
|
||||
setLifeRating: (id: string | number, rating: number) =>
|
||||
sendJson<LifePost>(`/api/life-posts/${id}/rating`, 'PUT', { rating }),
|
||||
deleteLifeRating: (id: string | number) => deleteAndGetJson<LifePost>(`/api/life-posts/${id}/rating`),
|
||||
createLifeComment: (postId: string | number, payload: LifeCommentPayload) =>
|
||||
sendJson<LifeComment>(`/api/life-posts/${postId}/comments`, 'POST', payload),
|
||||
lifeComments: (postId: string | number, params: CommentPageParams = {}) =>
|
||||
@@ -1599,20 +1578,20 @@ 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 | LifeCategory | GameVersion | NamedEntity>>(`/api/admin/config/${type}`),
|
||||
config: (type: ConfigType) => getJson<Array<Skill | GameVersion | NamedEntity>>(`/api/admin/config/${type}`),
|
||||
createConfig: (
|
||||
type: ConfigType,
|
||||
payload: { name: string; translations?: TranslationMap; hasItemDrop?: boolean; hasTrading?: boolean; isDefault?: boolean; isRateable?: boolean; changeLog?: string }
|
||||
payload: { name: string; translations?: TranslationMap; hasItemDrop?: boolean; hasTrading?: boolean; changeLog?: string }
|
||||
) =>
|
||||
sendJson<Skill | LifeCategory | GameVersion | NamedEntity>(`/api/admin/config/${type}`, 'POST', payload),
|
||||
sendJson<Skill | GameVersion | NamedEntity>(`/api/admin/config/${type}`, 'POST', payload),
|
||||
reorderConfig: (type: ConfigType, ids: number[]) =>
|
||||
sendJson<Array<Skill | LifeCategory | GameVersion | NamedEntity>>(`/api/admin/config/${type}/order`, 'PUT', { ids }),
|
||||
sendJson<Array<Skill | GameVersion | NamedEntity>>(`/api/admin/config/${type}/order`, 'PUT', { ids }),
|
||||
updateConfig: (
|
||||
type: ConfigType,
|
||||
id: number,
|
||||
payload: { name: string; translations?: TranslationMap; hasItemDrop?: boolean; hasTrading?: boolean; isDefault?: boolean; isRateable?: boolean; changeLog?: string }
|
||||
payload: { name: string; translations?: TranslationMap; hasItemDrop?: boolean; hasTrading?: boolean; changeLog?: string }
|
||||
) =>
|
||||
sendJson<Skill | LifeCategory | GameVersion | NamedEntity>(`/api/admin/config/${type}/${id}`, 'PUT', payload),
|
||||
sendJson<Skill | GameVersion | 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 | boolean | undefined>) =>
|
||||
getJson<Pokemon[]>(`/api/pokemon${buildQuery(params)}`),
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
type Habitat,
|
||||
type Item,
|
||||
type Language,
|
||||
type LifeCategory,
|
||||
type NamedEntity,
|
||||
type Permission,
|
||||
type PermissionPayload,
|
||||
@@ -93,11 +92,9 @@ 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 | LifeCategory | GameVersion) & {
|
||||
type EditableConfig = (NamedEntity | Skill | GameVersion) & {
|
||||
hasItemDrop?: boolean;
|
||||
hasTrading?: boolean;
|
||||
isDefault?: boolean;
|
||||
isRateable?: boolean;
|
||||
changeLog?: string;
|
||||
};
|
||||
type RateLimitPolicyForm = {
|
||||
@@ -207,8 +204,6 @@ const configTypes = computed<
|
||||
label: string;
|
||||
supportsItemDrop?: boolean;
|
||||
supportsTrading?: boolean;
|
||||
supportsDefault?: boolean;
|
||||
supportsRateable?: boolean;
|
||||
supportsChangeLog?: boolean;
|
||||
}>
|
||||
>(() => [
|
||||
@@ -218,7 +213,6 @@ const configTypes = computed<
|
||||
{ key: 'favorite-things', label: t('config.favoriteThings') },
|
||||
{ key: 'acquisition-methods', label: t('config.acquisitionMethods') },
|
||||
{ key: 'maps', label: t('config.maps') },
|
||||
{ key: 'life-tags', label: t('config.lifeCategories'), supportsDefault: true, supportsRateable: true },
|
||||
{ key: 'game-versions', label: t('config.gameVersions'), supportsChangeLog: true },
|
||||
{ key: 'dish-flavors', label: t('config.dishFlavors') }
|
||||
]);
|
||||
@@ -255,8 +249,6 @@ const configForm = ref({
|
||||
translations: {} as TranslationMap,
|
||||
hasItemDrop: false,
|
||||
hasTrading: false,
|
||||
isDefault: false,
|
||||
isRateable: false,
|
||||
changeLog: ''
|
||||
});
|
||||
const checklistForm = ref({ id: 0, title: '', translations: {} as TranslationMap });
|
||||
@@ -618,7 +610,7 @@ async function loadLanguages() {
|
||||
}
|
||||
|
||||
function resetConfigForm() {
|
||||
configForm.value = { id: 0, name: '', translations: {}, hasItemDrop: false, hasTrading: false, isDefault: false, isRateable: false, changeLog: '' };
|
||||
configForm.value = { id: 0, name: '', translations: {}, hasItemDrop: false, hasTrading: false, changeLog: '' };
|
||||
}
|
||||
|
||||
function resetChecklistForm() {
|
||||
@@ -729,8 +721,6 @@ function editConfig(item: EditableConfig) {
|
||||
translations: item.translations ?? {},
|
||||
hasItemDrop: item.hasItemDrop === true,
|
||||
hasTrading: item.hasTrading === true,
|
||||
isDefault: item.isDefault === true,
|
||||
isRateable: item.isRateable === true,
|
||||
changeLog: item.changeLog ?? ''
|
||||
};
|
||||
configModalOpen.value = true;
|
||||
@@ -1115,8 +1105,6 @@ async function saveConfig() {
|
||||
translations: configForm.value.translations,
|
||||
hasItemDrop: selectedConfig.value.supportsItemDrop ? configForm.value.hasItemDrop : undefined,
|
||||
hasTrading: selectedConfig.value.supportsTrading ? configForm.value.hasTrading : undefined,
|
||||
isDefault: selectedConfig.value.supportsDefault ? configForm.value.isDefault : undefined,
|
||||
isRateable: selectedConfig.value.supportsRateable ? configForm.value.isRateable : undefined,
|
||||
changeLog: selectedConfig.value.supportsChangeLog ? configForm.value.changeLog : undefined
|
||||
};
|
||||
|
||||
@@ -2172,8 +2160,6 @@ onMounted(() => {
|
||||
{{ item.name }}
|
||||
<span v-if="item.hasItemDrop" class="config-flag">{{ t('pages.admin.hasItemDrop') }}</span>
|
||||
<span v-if="item.hasTrading" class="config-flag">{{ t('pages.admin.hasTrading') }}</span>
|
||||
<span v-if="item.isDefault" class="config-flag">{{ t('pages.admin.defaultCategory') }}</span>
|
||||
<span v-if="item.isRateable" class="config-flag">{{ t('pages.admin.rateableCategory') }}</span>
|
||||
</span>
|
||||
<span class="row-actions">
|
||||
<button v-if="can('admin.config.update')" type="button" :disabled="busy" @click="editConfig(item)">
|
||||
@@ -3015,18 +3001,6 @@ onMounted(() => {
|
||||
{{ t('pages.admin.hasTrading') }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="selectedConfig.supportsDefault" class="check-row">
|
||||
<label>
|
||||
<input v-model="configForm.isDefault" type="checkbox" />
|
||||
{{ t('pages.admin.defaultCategory') }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="selectedConfig.supportsRateable" class="check-row">
|
||||
<label>
|
||||
<input v-model="configForm.isRateable" type="checkbox" />
|
||||
{{ t('pages.admin.rateableCategory') }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="selectedConfig.supportsChangeLog" class="field">
|
||||
<label for="config-change-log">{{ t('pages.admin.changeLog') }}</label>
|
||||
<textarea id="config-change-log" v-model="configForm.changeLog"></textarea>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute } from 'vue-router';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import LifeRatingControl from '../components/LifeRatingControl.vue';
|
||||
import LifeReactionUsersModal from '../components/LifeReactionUsersModal.vue';
|
||||
import LoadMoreSentinel from '../components/LoadMoreSentinel.vue';
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
@@ -64,8 +63,6 @@ const commentErrors = ref<Record<string, string>>({});
|
||||
const reactionPickerPostId = ref<number | null>(null);
|
||||
const reactionBusyPostId = ref<number | null>(null);
|
||||
const reactionErrors = ref<Record<number, string>>({});
|
||||
const ratingBusyPostId = ref<number | null>(null);
|
||||
const ratingErrors = ref<Record<number, string>>({});
|
||||
const moderationBusyPostId = ref<number | null>(null);
|
||||
const moderationErrors = ref<Record<number, string>>({});
|
||||
const reactionUsersModal = ref<{ postId: number; reactionType: LifeReactionType | null } | null>(null);
|
||||
@@ -89,7 +86,6 @@ function can(permissionKey: string) {
|
||||
const canComment = computed(() => can('life.comments.create'));
|
||||
const canLikeComments = computed(() => can('life.comments.like'));
|
||||
const canReact = computed(() => can('life.reactions.set'));
|
||||
const canRate = computed(() => can('life.ratings.set'));
|
||||
const canCommentOnPost = computed(() => canComment.value && post.value?.moderationStatus === 'approved');
|
||||
const commentSortOptions = computed<Array<{ value: CommentSort; label: string }>>(() => [
|
||||
{ value: 'oldest', label: t('pages.life.sortOldest') },
|
||||
@@ -279,10 +275,6 @@ function isReactionBusy(postId: number) {
|
||||
return reactionBusyPostId.value === postId;
|
||||
}
|
||||
|
||||
function isRatingBusy(postId: number) {
|
||||
return ratingBusyPostId.value === postId;
|
||||
}
|
||||
|
||||
function canManage(currentPost: LifePost) {
|
||||
return (currentUser.value?.id === currentPost.author?.id && can('life.posts.update')) || can('life.posts.update-any');
|
||||
}
|
||||
@@ -316,10 +308,6 @@ function canUseReactions() {
|
||||
return canReact.value && reactionBusyPostId.value === null;
|
||||
}
|
||||
|
||||
function canUseRatings(currentPost: LifePost) {
|
||||
return canRate.value && ratingBusyPostId.value === null && currentPost.moderationStatus === 'approved' && currentPost.category?.isRateable === true;
|
||||
}
|
||||
|
||||
function reactionTotal(currentPost: LifePost) {
|
||||
return reactionOptions.reduce((count, option) => count + (currentPost.reactionCounts[option.type] ?? 0), 0);
|
||||
}
|
||||
@@ -546,25 +534,6 @@ async function toggleReaction(currentPost: LifePost, reactionType: LifeReactionT
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleRating(currentPost: LifePost, rating: number) {
|
||||
if (!canUseRatings(currentPost)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ratingBusyPostId.value = currentPost.id;
|
||||
clearRatingError(currentPost.id);
|
||||
|
||||
try {
|
||||
const updatedPost =
|
||||
currentPost.myRating === rating ? await api.deleteLifeRating(currentPost.id) : await api.setLifeRating(currentPost.id, rating);
|
||||
replacePost(updatedPost);
|
||||
} catch (error) {
|
||||
setRatingError(currentPost.id, error instanceof Error && error.message ? error.message : t('pages.life.ratingFailed'));
|
||||
} finally {
|
||||
ratingBusyPostId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setReactionError(postId: number, message: string) {
|
||||
reactionErrors.value = { ...reactionErrors.value, [postId]: message };
|
||||
}
|
||||
@@ -575,16 +544,6 @@ function clearReactionError(postId: number) {
|
||||
reactionErrors.value = nextErrors;
|
||||
}
|
||||
|
||||
function setRatingError(postId: number, message: string) {
|
||||
ratingErrors.value = { ...ratingErrors.value, [postId]: message };
|
||||
}
|
||||
|
||||
function clearRatingError(postId: number) {
|
||||
const nextErrors = { ...ratingErrors.value };
|
||||
delete nextErrors[postId];
|
||||
ratingErrors.value = nextErrors;
|
||||
}
|
||||
|
||||
function setCommentError(key: string, message: string) {
|
||||
commentErrors.value = { ...commentErrors.value, [key]: message };
|
||||
}
|
||||
@@ -929,8 +888,7 @@ onUnmounted(() => {
|
||||
|
||||
<p class="life-post__body">{{ post.body }}</p>
|
||||
|
||||
<div v-if="post.category || post.gameVersion" class="life-post__tags" :aria-label="t('pages.life.postMeta')">
|
||||
<span v-if="post.category" class="life-post__tag">{{ post.category.name }}</span>
|
||||
<div v-if="post.gameVersion" class="life-post__tags" :aria-label="t('pages.life.postMeta')">
|
||||
<span v-if="post.gameVersion" class="life-post__tag life-post__tag--version">
|
||||
<Icon :icon="iconVersion" class="ui-icon" aria-hidden="true" />
|
||||
{{ post.gameVersion.name }}
|
||||
@@ -944,16 +902,6 @@ onUnmounted(() => {
|
||||
|
||||
<div class="life-post__engagement">
|
||||
<div class="life-post__engagement-actions">
|
||||
<LifeRatingControl
|
||||
v-if="post.category?.isRateable"
|
||||
:rating-average="post.ratingAverage"
|
||||
:rating-count="post.ratingCount"
|
||||
:my-rating="post.myRating"
|
||||
:disabled="!canUseRatings(post)"
|
||||
:busy="isRatingBusy(post.id)"
|
||||
@rate="toggleRating(post, $event)"
|
||||
/>
|
||||
|
||||
<div class="life-reactions">
|
||||
<div class="life-reaction-control">
|
||||
<button
|
||||
@@ -1068,7 +1016,6 @@ onUnmounted(() => {
|
||||
<span>{{ post.moderationReason }}</span>
|
||||
</p>
|
||||
|
||||
<p v-if="ratingErrors[post.id]" class="life-form__error" role="alert">{{ ratingErrors[post.id] }}</p>
|
||||
<p v-if="moderationErrors[post.id]" class="life-form__error" role="alert">{{ moderationErrors[post.id] }}</p>
|
||||
<p v-if="reactionErrors[post.id]" class="life-form__error" role="alert">{{ reactionErrors[post.id] }}</p>
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
import FilterPanel from '../components/FilterPanel.vue';
|
||||
import LifeRatingControl from '../components/LifeRatingControl.vue';
|
||||
import LifeReactionUsersModal from '../components/LifeReactionUsersModal.vue';
|
||||
import LoadMoreSentinel from '../components/LoadMoreSentinel.vue';
|
||||
import Modal from '../components/Modal.vue';
|
||||
@@ -43,7 +42,6 @@ import {
|
||||
type CommentSort,
|
||||
type GameVersion,
|
||||
type Language,
|
||||
type LifeCategory,
|
||||
type LifeComment,
|
||||
type LifePost,
|
||||
type LifePostsPage,
|
||||
@@ -62,13 +60,12 @@ type LifeCommentPageState = {
|
||||
error: string;
|
||||
};
|
||||
|
||||
type LifePostSort = 'latest' | 'oldest' | 'top-rated';
|
||||
type LifePostSort = 'latest' | 'oldest';
|
||||
type LifeFeedScope = 'all' | 'following';
|
||||
type PendingLifeDelete = { type: 'post'; post: LifePost } | { type: 'comment'; post: LifePost; comment: LifeComment };
|
||||
|
||||
const { locale, t } = useI18n();
|
||||
const posts = ref<LifePost[]>([]);
|
||||
const lifeCategories = ref<LifeCategory[]>([]);
|
||||
const gameVersions = ref<GameVersion[]>([]);
|
||||
const languages = ref<Language[]>([]);
|
||||
const currentUser = ref<AuthUser | null>(null);
|
||||
@@ -78,14 +75,11 @@ const authReady = ref(false);
|
||||
const busy = ref(false);
|
||||
const searchDraft = ref('');
|
||||
const submittedSearch = ref('');
|
||||
const activeCategoryId = ref('all');
|
||||
const activeLanguageCode = ref('all');
|
||||
const activeGameVersionId = ref('all');
|
||||
const activeRateableFilter = ref('all');
|
||||
const activeSort = ref<LifePostSort>('latest');
|
||||
const activeFeedScope = ref<LifeFeedScope>('all');
|
||||
const body = ref('');
|
||||
const selectedCategoryId = ref('');
|
||||
const selectedGameVersionId = ref('');
|
||||
const editingPostId = ref<number | null>(null);
|
||||
const postModalOpen = ref(false);
|
||||
@@ -102,8 +96,6 @@ const commentErrors = ref<Record<string, string>>({});
|
||||
const reactionPickerPostId = ref<number | null>(null);
|
||||
const reactionBusyPostId = ref<number | null>(null);
|
||||
const reactionErrors = ref<Record<number, string>>({});
|
||||
const ratingBusyPostId = ref<number | null>(null);
|
||||
const ratingErrors = ref<Record<number, string>>({});
|
||||
const moderationBusyPostId = ref<number | null>(null);
|
||||
const moderationErrors = ref<Record<number, string>>({});
|
||||
const reactionUsersModal = ref<{ postId: number; reactionType: LifeReactionType | null } | null>(null);
|
||||
@@ -123,7 +115,6 @@ let postsRequestId = 0;
|
||||
const nextCursor = ref<string | null>(null);
|
||||
const hasMorePosts = ref(false);
|
||||
const loadMorePaused = ref(false);
|
||||
const allCategoryValue = 'all';
|
||||
const allLanguageValue = 'all';
|
||||
const allGameVersionValue = 'all';
|
||||
const deleteConfirmTitle = computed(() =>
|
||||
@@ -134,7 +125,7 @@ const deleteConfirmMessage = computed(() =>
|
||||
);
|
||||
|
||||
type LifeInitialData = {
|
||||
options: { lifeCategories: LifeCategory[]; gameVersions: GameVersion[] } | null;
|
||||
options: { gameVersions: GameVersion[] } | null;
|
||||
languages: Language[] | null;
|
||||
posts: LifePostsPage | null;
|
||||
};
|
||||
@@ -154,7 +145,7 @@ const { data: initialData } = await useAsyncData<LifeInitialData>(
|
||||
return {
|
||||
options:
|
||||
optionsResult.status === 'fulfilled'
|
||||
? { lifeCategories: optionsResult.value.lifeCategories, gameVersions: optionsResult.value.gameVersions }
|
||||
? { gameVersions: optionsResult.value.gameVersions }
|
||||
: null,
|
||||
languages: languagesResult.status === 'fulfilled' ? languagesResult.value.filter((language) => language.enabled) : null,
|
||||
posts: postsResult.status === 'fulfilled' ? postsResult.value : null
|
||||
@@ -163,7 +154,6 @@ const { data: initialData } = await useAsyncData<LifeInitialData>(
|
||||
{ default: () => ({ options: null, languages: null, posts: null }) }
|
||||
);
|
||||
|
||||
lifeCategories.value = initialData.value.options?.lifeCategories ?? [];
|
||||
gameVersions.value = initialData.value.options?.gameVersions ?? [];
|
||||
languages.value = initialData.value.languages ?? [];
|
||||
posts.value = initialData.value.posts?.items ?? [];
|
||||
@@ -189,14 +179,9 @@ const canPost = computed(() => can('life.posts.create'));
|
||||
const canComment = computed(() => can('life.comments.create'));
|
||||
const canLikeComments = computed(() => can('life.comments.like'));
|
||||
const canReact = computed(() => can('life.reactions.set'));
|
||||
const canRate = computed(() => can('life.ratings.set'));
|
||||
const charactersLeft = computed(() => Math.max(0, bodyMaxLength - body.value.length));
|
||||
const isEditing = computed(() => editingPostId.value !== null);
|
||||
const searchQuery = computed(() => submittedSearch.value.trim());
|
||||
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
|
||||
);
|
||||
@@ -206,19 +191,6 @@ const selectedFeedGameVersionId = computed(() => {
|
||||
? undefined
|
||||
: gameVersionId;
|
||||
});
|
||||
const selectedRateableFilter = computed(() => {
|
||||
if (activeRateableFilter.value === 'rateable') {
|
||||
return true;
|
||||
}
|
||||
if (activeRateableFilter.value === 'not-rateable') {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
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 }))
|
||||
@@ -227,15 +199,9 @@ const gameVersionFilterOptions = computed(() => [
|
||||
{ value: allGameVersionValue, label: t('pages.life.allVersions') },
|
||||
...gameVersions.value.map((version) => ({ value: String(version.id), label: version.name }))
|
||||
]);
|
||||
const rateableFilterOptions = computed(() => [
|
||||
{ value: 'all', label: t('pages.life.allRatingModes') },
|
||||
{ value: 'rateable', label: t('pages.life.rateableOnly') },
|
||||
{ value: 'not-rateable', label: t('pages.life.notRateableOnly') }
|
||||
]);
|
||||
const sortOptions = computed<Array<{ value: LifePostSort; label: string }>>(() => [
|
||||
{ value: 'latest', label: t('pages.life.sortLatest') },
|
||||
{ value: 'oldest', label: t('pages.life.sortOldest') },
|
||||
{ value: 'top-rated', label: t('pages.life.sortTopRated') }
|
||||
{ value: 'oldest', label: t('pages.life.sortOldest') }
|
||||
]);
|
||||
const commentSortOptions = computed<Array<{ value: CommentSort; label: string }>>(() => [
|
||||
{ value: 'oldest', label: t('pages.life.sortOldest') },
|
||||
@@ -247,10 +213,6 @@ const feedScopeOptions = computed<TabOption[]>(() => [
|
||||
{ value: 'all', label: t('pages.life.allFeed') },
|
||||
{ value: 'following', label: t('pages.life.followingFeed') }
|
||||
]);
|
||||
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');
|
||||
@@ -271,27 +233,17 @@ async function loadCurrentUser() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLifeCategories() {
|
||||
async function loadLifeOptions() {
|
||||
try {
|
||||
const options = await api.options();
|
||||
lifeCategories.value = options.lifeCategories;
|
||||
gameVersions.value = options.gameVersions;
|
||||
|
||||
if (activeCategoryId.value !== allCategoryValue && !lifeCategories.value.some((category) => String(category.id) === activeCategoryId.value)) {
|
||||
activeCategoryId.value = allCategoryValue;
|
||||
}
|
||||
if (
|
||||
activeGameVersionId.value !== allGameVersionValue &&
|
||||
!gameVersions.value.some((gameVersion) => String(gameVersion.id) === activeGameVersionId.value)
|
||||
) {
|
||||
activeGameVersionId.value = allGameVersionValue;
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (selectedGameVersionId.value && !gameVersions.value.some((gameVersion) => String(gameVersion.id) === selectedGameVersionId.value)) {
|
||||
selectedGameVersionId.value = '';
|
||||
}
|
||||
@@ -328,10 +280,8 @@ async function loadPosts() {
|
||||
const params = {
|
||||
limit: lifePostPageSize,
|
||||
search: searchQuery.value,
|
||||
categoryId: selectedFeedCategoryId.value,
|
||||
language: selectedFeedLanguageCode.value,
|
||||
gameVersionId: selectedFeedGameVersionId.value,
|
||||
rateable: selectedRateableFilter.value,
|
||||
sort: activeSort.value
|
||||
};
|
||||
const page = activeFeedScope.value === 'following' ? await api.followingLifePosts(params) : await api.lifePosts(params);
|
||||
@@ -374,10 +324,8 @@ async function loadMorePosts() {
|
||||
cursor,
|
||||
limit: lifePostPageSize,
|
||||
search: searchQuery.value,
|
||||
categoryId: selectedFeedCategoryId.value,
|
||||
language: selectedFeedLanguageCode.value,
|
||||
gameVersionId: selectedFeedGameVersionId.value,
|
||||
rateable: selectedRateableFilter.value,
|
||||
sort: activeSort.value
|
||||
};
|
||||
const page = activeFeedScope.value === 'following' ? await api.followingLifePosts(params) : await api.lifePosts(params);
|
||||
@@ -403,7 +351,6 @@ async function loadMorePosts() {
|
||||
|
||||
function resetForm() {
|
||||
body.value = '';
|
||||
selectedCategoryId.value = '';
|
||||
selectedGameVersionId.value = '';
|
||||
editingPostId.value = null;
|
||||
formError.value = '';
|
||||
@@ -412,17 +359,11 @@ function resetForm() {
|
||||
function payload() {
|
||||
return {
|
||||
body: body.value.trim(),
|
||||
categoryId: selectedLifeCategoryId() ?? 0,
|
||||
gameVersionId: selectedGameVersionForPost(),
|
||||
languageCode: selectedFeedLanguageCode.value ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function selectedLifeCategoryId() {
|
||||
const categoryId = Number(selectedCategoryId.value);
|
||||
return Number.isInteger(categoryId) && categoryId > 0 ? categoryId : null;
|
||||
}
|
||||
|
||||
function selectedGameVersionForPost() {
|
||||
const gameVersionId = Number(selectedGameVersionId.value);
|
||||
return Number.isInteger(gameVersionId) && gameVersionId > 0 ? gameVersionId : null;
|
||||
@@ -459,21 +400,16 @@ function retryLoadMore() {
|
||||
|
||||
function matchesCurrentFilters(post: LifePost) {
|
||||
const keyword = searchQuery.value.toLowerCase();
|
||||
const categoryId = selectedFeedCategoryId.value;
|
||||
const gameVersionId = selectedFeedGameVersionId.value;
|
||||
const rateable = selectedRateableFilter.value;
|
||||
const matchesSearch = keyword === '' || post.body.toLowerCase().includes(keyword);
|
||||
const matchesCategory = categoryId === undefined || post.category?.id === categoryId;
|
||||
const matchesGameVersion = gameVersionId === undefined || post.gameVersion?.id === gameVersionId;
|
||||
const matchesRateable = rateable === null || post.category?.isRateable === rateable;
|
||||
const matchesLanguage =
|
||||
selectedFeedLanguageCode.value === undefined || post.moderationLanguageCode === selectedFeedLanguageCode.value;
|
||||
return matchesSearch && matchesCategory && matchesGameVersion && matchesRateable && matchesLanguage;
|
||||
return matchesSearch && matchesGameVersion && matchesLanguage;
|
||||
}
|
||||
|
||||
function openCreatePostModal() {
|
||||
resetForm();
|
||||
selectedCategoryId.value = defaultLifeCategoryId.value;
|
||||
postModalOpen.value = true;
|
||||
void nextTick(() => bodyInput.value?.focus());
|
||||
}
|
||||
@@ -494,12 +430,6 @@ async function submitPost() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedLifeCategoryId() === null) {
|
||||
formError.value = t('pages.life.categoryRequired');
|
||||
document.getElementById('life-post-category')?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
busy.value = true;
|
||||
formError.value = '';
|
||||
|
||||
@@ -886,10 +816,6 @@ function isReactionBusy(postId: number) {
|
||||
return reactionBusyPostId.value === postId;
|
||||
}
|
||||
|
||||
function isRatingBusy(postId: number) {
|
||||
return ratingBusyPostId.value === postId;
|
||||
}
|
||||
|
||||
function commentAuthorName(comment: LifeComment) {
|
||||
return comment.author?.displayName ?? t('pages.life.byUnknown');
|
||||
}
|
||||
@@ -923,16 +849,6 @@ function clearReactionError(postId: number) {
|
||||
reactionErrors.value = nextErrors;
|
||||
}
|
||||
|
||||
function setRatingError(postId: number, message: string) {
|
||||
ratingErrors.value = { ...ratingErrors.value, [postId]: message };
|
||||
}
|
||||
|
||||
function clearRatingError(postId: number) {
|
||||
const nextErrors = { ...ratingErrors.value };
|
||||
delete nextErrors[postId];
|
||||
ratingErrors.value = nextErrors;
|
||||
}
|
||||
|
||||
function canUseReactions() {
|
||||
return canReact.value && reactionBusyPostId.value === null;
|
||||
}
|
||||
@@ -954,10 +870,6 @@ function commentLikeLabel(comment: LifeComment) {
|
||||
return comment.myLiked ? t('pages.life.unlikeComment') : t('pages.life.likeComment');
|
||||
}
|
||||
|
||||
function canUseRatings(post: LifePost) {
|
||||
return canRate.value && ratingBusyPostId.value === null && post.moderationStatus === 'approved' && post.category?.isRateable === true;
|
||||
}
|
||||
|
||||
function closeReactionPicker() {
|
||||
reactionPickerPostId.value = null;
|
||||
}
|
||||
@@ -1027,31 +939,9 @@ async function toggleReaction(post: LifePost, reactionType: LifeReactionType) {
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleRating(post: LifePost, rating: number) {
|
||||
if (!canUseRatings(post)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ratingBusyPostId.value = post.id;
|
||||
clearRatingError(post.id);
|
||||
|
||||
try {
|
||||
const updatedPost = post.myRating === rating ? await api.deleteLifeRating(post.id) : await api.setLifeRating(post.id, rating);
|
||||
replacePost(updatedPost);
|
||||
if (activeSort.value === 'top-rated') {
|
||||
void loadPosts();
|
||||
}
|
||||
} catch (error) {
|
||||
setRatingError(post.id, error instanceof Error && error.message ? error.message : t('pages.life.ratingFailed'));
|
||||
} finally {
|
||||
ratingBusyPostId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit(post: LifePost) {
|
||||
editingPostId.value = post.id;
|
||||
body.value = post.body;
|
||||
selectedCategoryId.value = post.category ? String(post.category.id) : '';
|
||||
selectedGameVersionId.value = post.gameVersion ? String(post.gameVersion.id) : '';
|
||||
formError.value = '';
|
||||
postModalOpen.value = true;
|
||||
@@ -1373,9 +1263,6 @@ function observeLoadMore() {
|
||||
}
|
||||
|
||||
watch([loadMoreSentinel, hasMorePosts, loading, loadingMore, loadMorePaused], observeLoadMore, { flush: 'post' });
|
||||
watch(activeCategoryId, () => {
|
||||
void loadPosts();
|
||||
});
|
||||
watch(activeLanguageCode, () => {
|
||||
expandedComments.value = {};
|
||||
commentPages.value = {};
|
||||
@@ -1384,9 +1271,6 @@ watch(activeLanguageCode, () => {
|
||||
watch(activeGameVersionId, () => {
|
||||
void loadPosts();
|
||||
});
|
||||
watch(activeRateableFilter, () => {
|
||||
void loadPosts();
|
||||
});
|
||||
watch(activeSort, () => {
|
||||
void loadPosts();
|
||||
});
|
||||
@@ -1395,7 +1279,7 @@ watch(activeFeedScope, () => {
|
||||
});
|
||||
watch(locale, () => {
|
||||
void loadLanguages();
|
||||
void loadLifeCategories();
|
||||
void loadLifeOptions();
|
||||
void loadPosts();
|
||||
});
|
||||
|
||||
@@ -1410,7 +1294,7 @@ onMounted(() => {
|
||||
initialLanguagesLoaded.value = true;
|
||||
}
|
||||
if (!initialOptionsLoaded.value) {
|
||||
await loadLifeCategories();
|
||||
await loadLifeOptions();
|
||||
initialOptionsLoaded.value = true;
|
||||
}
|
||||
if (!initialPostsLoaded.value || currentUser.value) {
|
||||
@@ -1473,14 +1357,6 @@ onUnmounted(() => {
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field life-toolbar__select">
|
||||
<label for="life-rateable-filter">{{ t('pages.life.ratingFilter') }}</label>
|
||||
<select id="life-rateable-filter" v-model="activeRateableFilter">
|
||||
<option v-for="option in rateableFilterOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field life-toolbar__select">
|
||||
<label for="life-sort">{{ t('pages.life.sort') }}</label>
|
||||
<select id="life-sort" v-model="activeSort">
|
||||
@@ -1528,19 +1404,6 @@ onUnmounted(() => {
|
||||
<span class="life-form__counter">{{ t('pages.life.charactersLeft', { count: charactersLeft }) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="life-post-category">{{ t('pages.life.category') }}</label>
|
||||
<TagsSelect
|
||||
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>
|
||||
|
||||
<div class="field">
|
||||
<label for="life-post-version">{{ t('pages.life.gameVersion') }}</label>
|
||||
<TagsSelect
|
||||
@@ -1591,7 +1454,6 @@ onUnmounted(() => {
|
||||
:label="t('pages.life.feedScope')"
|
||||
/>
|
||||
<Tabs id="life-language-filter" v-model="activeLanguageCode" :tabs="languageFilterOptions" :label="t('pages.life.languages')" />
|
||||
<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')">
|
||||
@@ -1648,8 +1510,7 @@ onUnmounted(() => {
|
||||
|
||||
<p class="life-post__body">{{ post.body }}</p>
|
||||
|
||||
<div v-if="post.category || post.gameVersion" class="life-post__tags" :aria-label="t('pages.life.postMeta')">
|
||||
<span v-if="post.category" class="life-post__tag">{{ post.category.name }}</span>
|
||||
<div v-if="post.gameVersion" class="life-post__tags" :aria-label="t('pages.life.postMeta')">
|
||||
<span v-if="post.gameVersion" class="life-post__tag life-post__tag--version">
|
||||
<Icon :icon="iconVersion" class="ui-icon" aria-hidden="true" />
|
||||
{{ post.gameVersion.name }}
|
||||
@@ -1662,16 +1523,6 @@ onUnmounted(() => {
|
||||
|
||||
<div class="life-post__engagement">
|
||||
<div class="life-post__engagement-actions">
|
||||
<LifeRatingControl
|
||||
v-if="post.category?.isRateable"
|
||||
:rating-average="post.ratingAverage"
|
||||
:rating-count="post.ratingCount"
|
||||
:my-rating="post.myRating"
|
||||
:disabled="!canUseRatings(post)"
|
||||
:busy="isRatingBusy(post.id)"
|
||||
@rate="toggleRating(post, $event)"
|
||||
/>
|
||||
|
||||
<div class="life-reactions">
|
||||
<div class="life-reaction-control">
|
||||
<button
|
||||
@@ -1810,7 +1661,6 @@ onUnmounted(() => {
|
||||
<span>{{ post.moderationReason }}</span>
|
||||
</p>
|
||||
|
||||
<p v-if="ratingErrors[post.id]" class="life-form__error" role="alert">{{ ratingErrors[post.id] }}</p>
|
||||
<p v-if="moderationErrors[post.id]" class="life-form__error" role="alert">{{ moderationErrors[post.id] }}</p>
|
||||
<p v-if="reactionErrors[post.id]" class="life-form__error" role="alert">{{ reactionErrors[post.id] }}</p>
|
||||
|
||||
|
||||
@@ -683,8 +683,7 @@ function contentTypeLabel(contentType: string): string {
|
||||
environments: t('config.environments'),
|
||||
'favorite-things': t('config.favoriteThings'),
|
||||
'acquisition-methods': t('config.acquisitionMethods'),
|
||||
maps: t('config.maps'),
|
||||
'life-tags': t('config.lifeCategories')
|
||||
maps: t('config.maps')
|
||||
};
|
||||
return labels[contentType] ?? t('pages.profile.otherContributions');
|
||||
}
|
||||
@@ -840,10 +839,6 @@ onMounted(() => {
|
||||
|
||||
<p class="life-post__body">{{ post.body }}</p>
|
||||
|
||||
<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">
|
||||
<button
|
||||
class="profile-reaction-open-button"
|
||||
|
||||
Reference in New Issue
Block a user