feat(life): add search and move post composer to modal
Support searching life posts by content Move post creation and editing to a modal dialog Add search toolbar and update empty states
This commit is contained in:
@@ -377,6 +377,7 @@ Life Post 可配置:
|
|||||||
- 每条 Life Post 默认只展示评论入口与评论数量;评论列表、回复和评论输入默认折叠,用户点击后展开。
|
- 每条 Life Post 默认只展示评论入口与评论数量;评论列表、回复和评论输入默认折叠,用户点击后展开。
|
||||||
- 已注册并完成邮箱验证的用户可以对每条 Life Post 选择一个 Reaction;普通点击默认设置 `like`,再次点击 `like` 会取消,当前为其他 Reaction 时普通点击会替换为 `like`。
|
- 已注册并完成邮箱验证的用户可以对每条 Life Post 选择一个 Reaction;普通点击默认设置 `like`,再次点击 `like` 会取消,当前为其他 Reaction 时普通点击会替换为 `like`。
|
||||||
- Life Reaction 的其他类型通过右键 / context menu 打开 Popup 选择;再次选择当前 Reaction 会取消,选择其他 Reaction 会替换原 Reaction。
|
- Life Reaction 的其他类型通过右键 / context menu 打开 Popup 选择;再次选择当前 Reaction 会取消,选择其他 Reaction 会替换原 Reaction。
|
||||||
|
- 支持按 Life Post 正文搜索;用户按 Enter 或点击 Search 按钮后提交搜索,不随输入实时请求;搜索结果仍按创建时间倒序展示并分页加载。
|
||||||
- 信息流分页加载,初始展示最新一页,滚动到底部自动加载更多。
|
- 信息流分页加载,初始展示最新一页,滚动到底部自动加载更多。
|
||||||
- 当前没有图片上传、转发、置顶或单独审核流程。
|
- 当前没有图片上传、转发、置顶或单独审核流程。
|
||||||
- Life Post 是用户生成内容,正文按作者输入展示,不进入 `entity_translations`。
|
- Life Post 是用户生成内容,正文按作者输入展示,不进入 `entity_translations`。
|
||||||
@@ -409,7 +410,7 @@ API 暴露边界:
|
|||||||
- `/items/:id/edit`
|
- `/items/:id/edit`
|
||||||
- `/recipes/new`
|
- `/recipes/new`
|
||||||
- `/recipes/:id/edit`
|
- `/recipes/:id/edit`
|
||||||
- Life 使用信息流内联发布与编辑,不使用路由驱动 Modal。
|
- Life 使用信息流顶部 New Post / 编辑按钮打开普通 Modal 发布与编辑,不使用路由驱动 Modal。
|
||||||
- 进入或关闭编辑 Modal 时应保留底层页面上下文,不进行不必要的滚动跳转。
|
- 进入或关闭编辑 Modal 时应保留底层页面上下文,不进行不必要的滚动跳转。
|
||||||
- 用户界面不得展示内部字段名、调试数据、计划说明或“已修改某字段”一类实现说明。
|
- 用户界面不得展示内部字段名、调试数据、计划说明或“已修改某字段”一类实现说明。
|
||||||
|
|
||||||
@@ -428,7 +429,7 @@ API 暴露边界:
|
|||||||
- `GET /api/items/:id`
|
- `GET /api/items/:id`
|
||||||
- `GET /api/recipes`
|
- `GET /api/recipes`
|
||||||
- `GET /api/recipes/:id`
|
- `GET /api/recipes/:id`
|
||||||
- `GET /api/life-posts`:支持 `cursor` / `limit` 分页读取。
|
- `GET /api/life-posts`:支持 `cursor` / `limit` 分页读取;支持 `search` 按 Life Post 正文搜索。
|
||||||
|
|
||||||
认证 API:
|
认证 API:
|
||||||
|
|
||||||
|
|||||||
@@ -1482,21 +1482,26 @@ async function getLifeCommentById(id: number): Promise<LifeComment | null> {
|
|||||||
export async function listLifePosts(paramsQuery: QueryParams = {}, userId: number | null = null): Promise<LifePostsPage> {
|
export async function listLifePosts(paramsQuery: QueryParams = {}, userId: number | null = null): Promise<LifePostsPage> {
|
||||||
const cursor = decodeLifePostCursor(paramsQuery.cursor);
|
const cursor = decodeLifePostCursor(paramsQuery.cursor);
|
||||||
const limit = cleanLifePostLimit(paramsQuery.limit);
|
const limit = cleanLifePostLimit(paramsQuery.limit);
|
||||||
|
const search = asString(paramsQuery.search)?.trim();
|
||||||
const params: unknown[] = [];
|
const params: unknown[] = [];
|
||||||
let cursorClause = '';
|
const conditions: string[] = [];
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
params.push(`%${search}%`);
|
||||||
|
conditions.push(`lp.body ILIKE $${params.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
params.push(cursor.createdAt, cursor.id);
|
params.push(cursor.createdAt, cursor.id);
|
||||||
cursorClause = `
|
conditions.push(`(lp.created_at, lp.id) < ($${params.length - 1}::timestamptz, $${params.length}::integer)`);
|
||||||
WHERE (lp.created_at, lp.id) < ($${params.length - 1}::timestamptz, $${params.length}::integer)
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
||||||
params.push(limit + 1);
|
params.push(limit + 1);
|
||||||
const rows = await query<LifePostRow>(
|
const rows = await query<LifePostRow>(
|
||||||
`
|
`
|
||||||
${lifePostProjection()}
|
${lifePostProjection()}
|
||||||
${cursorClause}
|
${whereClause}
|
||||||
ORDER BY lp.created_at DESC, lp.id DESC
|
ORDER BY lp.created_at DESC, lp.id DESC
|
||||||
LIMIT $${params.length}
|
LIMIT $${params.length}
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -229,6 +229,10 @@ const messages = {
|
|||||||
composerPrompt: 'What would you like to share?',
|
composerPrompt: 'What would you like to share?',
|
||||||
bodyLabel: 'Post',
|
bodyLabel: 'Post',
|
||||||
bodyPlaceholder: 'Share a thought, tip, or discovery...',
|
bodyPlaceholder: 'Share a thought, tip, or discovery...',
|
||||||
|
newPost: 'New Post',
|
||||||
|
search: 'Search Life',
|
||||||
|
searchPlaceholder: 'Search post content...',
|
||||||
|
searchEmpty: 'No posts match your search',
|
||||||
comments: 'Comments',
|
comments: 'Comments',
|
||||||
commentsCount: '{count} comments',
|
commentsCount: '{count} comments',
|
||||||
comment: 'Comment',
|
comment: 'Comment',
|
||||||
@@ -572,6 +576,10 @@ const messages = {
|
|||||||
composerPrompt: '想分享什么?',
|
composerPrompt: '想分享什么?',
|
||||||
bodyLabel: '动态内容',
|
bodyLabel: '动态内容',
|
||||||
bodyPlaceholder: '分享一段想法、心得或发现……',
|
bodyPlaceholder: '分享一段想法、心得或发现……',
|
||||||
|
newPost: 'New Post',
|
||||||
|
search: '搜索动态',
|
||||||
|
searchPlaceholder: '搜索动态内容……',
|
||||||
|
searchEmpty: '没有匹配的动态',
|
||||||
comments: '评论',
|
comments: '评论',
|
||||||
commentsCount: '{count} 条评论',
|
commentsCount: '{count} 条评论',
|
||||||
comment: '评论',
|
comment: '评论',
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const iconReactionHelpful: AppIcon = 'mdi:lightbulb-on-outline';
|
|||||||
export const iconReactionLike: AppIcon = 'mdi:thumb-up-outline';
|
export const iconReactionLike: AppIcon = 'mdi:thumb-up-outline';
|
||||||
export const iconReactionThanks: AppIcon = 'mdi:hand-heart-outline';
|
export const iconReactionThanks: AppIcon = 'mdi:hand-heart-outline';
|
||||||
export const iconSave: AppIcon = 'mdi:content-save-outline';
|
export const iconSave: AppIcon = 'mdi:content-save-outline';
|
||||||
|
export const iconSearch: AppIcon = 'mdi:magnify';
|
||||||
export const iconSuccess: AppIcon = 'mdi:check-circle-outline';
|
export const iconSuccess: AppIcon = 'mdi:check-circle-outline';
|
||||||
export const iconTranslate: AppIcon = 'mdi:translate';
|
export const iconTranslate: AppIcon = 'mdi:translate';
|
||||||
export const iconWarning: AppIcon = 'mdi:alert-outline';
|
export const iconWarning: AppIcon = 'mdi:alert-outline';
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export interface LifePostsPage {
|
|||||||
export interface LifePostsParams {
|
export interface LifePostsParams {
|
||||||
cursor?: string | null;
|
cursor?: string | null;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LifeComment {
|
export interface LifeComment {
|
||||||
@@ -465,7 +466,7 @@ export const api = {
|
|||||||
dailyChecklist: () => getJson<DailyChecklistItem[]>('/api/daily-checklist'),
|
dailyChecklist: () => getJson<DailyChecklistItem[]>('/api/daily-checklist'),
|
||||||
lifePosts: (params: LifePostsParams = {}) =>
|
lifePosts: (params: LifePostsParams = {}) =>
|
||||||
getJson<LifePostsPage>(
|
getJson<LifePostsPage>(
|
||||||
`/api/life-posts${buildQuery({ cursor: params.cursor ?? undefined, limit: params.limit })}`
|
`/api/life-posts${buildQuery({ cursor: params.cursor ?? undefined, limit: params.limit, search: params.search?.trim() })}`
|
||||||
),
|
),
|
||||||
createLifePost: (payload: LifePostPayload) => sendJson<LifePost>('/api/life-posts', 'POST', payload),
|
createLifePost: (payload: LifePostPayload) => sendJson<LifePost>('/api/life-posts', 'POST', payload),
|
||||||
updateLifePost: (id: string | number, payload: LifePostPayload) =>
|
updateLifePost: (id: string | number, payload: LifePostPayload) =>
|
||||||
|
|||||||
@@ -1179,6 +1179,27 @@ button:disabled,
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.life-toolbar {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.life-toolbar__search {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.life-toolbar__actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.life-toolbar .ui-button {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
.life-composer,
|
.life-composer,
|
||||||
.life-post {
|
.life-post {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -2844,6 +2865,16 @@ button:disabled,
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.life-toolbar,
|
||||||
|
.life-toolbar__search {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.life-toolbar__actions,
|
||||||
|
.life-toolbar .ui-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.life-post__metrics {
|
.life-post__metrics {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
import { Icon } from '@iconify/vue';
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import FilterPanel from '../components/FilterPanel.vue';
|
||||||
|
import Modal from '../components/Modal.vue';
|
||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import Skeleton from '../components/Skeleton.vue';
|
import Skeleton from '../components/Skeleton.vue';
|
||||||
import StatusMessage from '../components/StatusMessage.vue';
|
import StatusMessage from '../components/StatusMessage.vue';
|
||||||
import {
|
import {
|
||||||
|
iconAdd,
|
||||||
iconCancel,
|
iconCancel,
|
||||||
iconComment,
|
iconComment,
|
||||||
iconDelete,
|
iconDelete,
|
||||||
@@ -16,7 +19,8 @@ import {
|
|||||||
iconReactionLike,
|
iconReactionLike,
|
||||||
iconReactionThanks,
|
iconReactionThanks,
|
||||||
iconReply,
|
iconReply,
|
||||||
iconSave
|
iconSave,
|
||||||
|
iconSearch
|
||||||
} from '../icons';
|
} from '../icons';
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
@@ -36,8 +40,11 @@ const loading = ref(true);
|
|||||||
const loadingMore = ref(false);
|
const loadingMore = ref(false);
|
||||||
const authReady = ref(false);
|
const authReady = ref(false);
|
||||||
const busy = ref(false);
|
const busy = ref(false);
|
||||||
|
const searchDraft = ref('');
|
||||||
|
const submittedSearch = ref('');
|
||||||
const body = ref('');
|
const body = ref('');
|
||||||
const editingPostId = ref<number | null>(null);
|
const editingPostId = ref<number | null>(null);
|
||||||
|
const postModalOpen = ref(false);
|
||||||
const formError = ref('');
|
const formError = ref('');
|
||||||
const loadError = ref('');
|
const loadError = ref('');
|
||||||
const commentBodies = ref<Record<number, string>>({});
|
const commentBodies = ref<Record<number, string>>({});
|
||||||
@@ -71,6 +78,8 @@ const reactionOptions = [
|
|||||||
const canPost = computed(() => currentUser.value?.emailVerified === true);
|
const canPost = computed(() => currentUser.value?.emailVerified === true);
|
||||||
const charactersLeft = computed(() => Math.max(0, 2000 - body.value.length));
|
const charactersLeft = computed(() => Math.max(0, 2000 - body.value.length));
|
||||||
const isEditing = computed(() => editingPostId.value !== null);
|
const isEditing = computed(() => editingPostId.value !== null);
|
||||||
|
const searchQuery = computed(() => submittedSearch.value.trim());
|
||||||
|
const postModalTitle = computed(() => (isEditing.value ? t('pages.life.editPost') : t('pages.life.newPost')));
|
||||||
const submitLabel = computed(() => {
|
const submitLabel = computed(() => {
|
||||||
if (busy.value) return isEditing.value ? t('pages.life.updating') : t('pages.life.publishing');
|
if (busy.value) return isEditing.value ? t('pages.life.updating') : t('pages.life.publishing');
|
||||||
return isEditing.value ? t('pages.life.update') : t('pages.life.publish');
|
return isEditing.value ? t('pages.life.update') : t('pages.life.publish');
|
||||||
@@ -99,13 +108,14 @@ async function loadCurrentUser() {
|
|||||||
async function loadPosts() {
|
async function loadPosts() {
|
||||||
const requestId = ++postsRequestId;
|
const requestId = ++postsRequestId;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
loadingMore.value = false;
|
||||||
loadError.value = '';
|
loadError.value = '';
|
||||||
nextCursor.value = null;
|
nextCursor.value = null;
|
||||||
hasMorePosts.value = false;
|
hasMorePosts.value = false;
|
||||||
loadMorePaused.value = false;
|
loadMorePaused.value = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const page = await api.lifePosts({ limit: lifePostPageSize });
|
const page = await api.lifePosts({ limit: lifePostPageSize, search: searchQuery.value });
|
||||||
if (requestId !== postsRequestId) {
|
if (requestId !== postsRequestId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -139,7 +149,7 @@ async function loadMorePosts() {
|
|||||||
loadError.value = '';
|
loadError.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const page = await api.lifePosts({ cursor, limit: lifePostPageSize });
|
const page = await api.lifePosts({ cursor, limit: lifePostPageSize, search: searchQuery.value });
|
||||||
if (requestId !== postsRequestId) {
|
if (requestId !== postsRequestId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -172,6 +182,36 @@ function payload() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submitSearch() {
|
||||||
|
const nextSearch = searchDraft.value.trim();
|
||||||
|
if (nextSearch === submittedSearch.value && !loadError.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submittedSearch.value = nextSearch;
|
||||||
|
void loadPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesCurrentSearch(post: LifePost) {
|
||||||
|
const keyword = searchQuery.value.toLowerCase();
|
||||||
|
return keyword === '' || post.body.toLowerCase().includes(keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreatePostModal() {
|
||||||
|
resetForm();
|
||||||
|
postModalOpen.value = true;
|
||||||
|
void nextTick(() => bodyInput.value?.focus());
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePostModal() {
|
||||||
|
if (busy.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
postModalOpen.value = false;
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
|
||||||
async function submitPost() {
|
async function submitPost() {
|
||||||
if (!body.value.trim()) {
|
if (!body.value.trim()) {
|
||||||
formError.value = t('pages.life.bodyRequired');
|
formError.value = t('pages.life.bodyRequired');
|
||||||
@@ -188,9 +228,12 @@ async function submitPost() {
|
|||||||
replacePost(updated);
|
replacePost(updated);
|
||||||
} else {
|
} else {
|
||||||
const created = await api.createLifePost(payload());
|
const created = await api.createLifePost(payload());
|
||||||
|
if (matchesCurrentSearch(created)) {
|
||||||
posts.value = [created, ...posts.value];
|
posts.value = [created, ...posts.value];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
resetForm();
|
resetForm();
|
||||||
|
postModalOpen.value = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
formError.value =
|
formError.value =
|
||||||
error instanceof Error && error.message
|
error instanceof Error && error.message
|
||||||
@@ -251,6 +294,11 @@ function reactionCountLabel(post: LifePost, type: LifeReactionType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function replacePost(updatedPost: LifePost) {
|
function replacePost(updatedPost: LifePost) {
|
||||||
|
if (!matchesCurrentSearch(updatedPost)) {
|
||||||
|
posts.value = posts.value.filter((post) => post.id !== updatedPost.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
posts.value = posts.value.map((post) => (post.id === updatedPost.id ? updatedPost : post));
|
posts.value = posts.value.map((post) => (post.id === updatedPost.id ? updatedPost : post));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +411,7 @@ function startEdit(post: LifePost) {
|
|||||||
editingPostId.value = post.id;
|
editingPostId.value = post.id;
|
||||||
body.value = post.body;
|
body.value = post.body;
|
||||||
formError.value = '';
|
formError.value = '';
|
||||||
|
postModalOpen.value = true;
|
||||||
void nextTick(() => bodyInput.value?.focus());
|
void nextTick(() => bodyInput.value?.focus());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,16 +420,17 @@ async function deletePost(post: LifePost) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
formError.value = '';
|
loadError.value = '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.deleteLifePost(post.id);
|
await api.deleteLifePost(post.id);
|
||||||
posts.value = posts.value.filter((item) => item.id !== post.id);
|
posts.value = posts.value.filter((item) => item.id !== post.id);
|
||||||
if (editingPostId.value === post.id) {
|
if (editingPostId.value === post.id) {
|
||||||
resetForm();
|
resetForm();
|
||||||
|
postModalOpen.value = false;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
formError.value = error instanceof Error && error.message ? error.message : t('pages.life.deleteFailed');
|
loadError.value = error instanceof Error && error.message ? error.message : t('pages.life.deleteFailed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,14 +595,35 @@ onUnmounted(() => {
|
|||||||
<template #kicker>{{ t('pages.life.kicker') }}</template>
|
<template #kicker>{{ t('pages.life.kicker') }}</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
<FilterPanel class="life-toolbar">
|
||||||
|
<form class="life-toolbar__search" @submit.prevent="submitSearch">
|
||||||
|
<div class="field">
|
||||||
|
<label for="life-search">{{ t('pages.life.search') }}</label>
|
||||||
|
<input id="life-search" v-model="searchDraft" type="search" :placeholder="t('pages.life.searchPlaceholder')" />
|
||||||
|
</div>
|
||||||
|
<button class="ui-button ui-button--ghost" type="submit">
|
||||||
|
<Icon :icon="iconSearch" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.search') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="life-toolbar__actions">
|
||||||
|
<button class="ui-button ui-button--primary" :disabled="!authReady" type="button" @click="openCreatePostModal">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('pages.life.newPost') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</FilterPanel>
|
||||||
|
|
||||||
<StatusMessage v-if="loadError" variant="danger" :duration="0">{{ loadError }}</StatusMessage>
|
<StatusMessage v-if="loadError" variant="danger" :duration="0">{{ loadError }}</StatusMessage>
|
||||||
|
|
||||||
<section class="life-composer" :aria-busy="!authReady || busy">
|
<Modal
|
||||||
<div class="life-composer__header">
|
:open="postModalOpen"
|
||||||
<h2>{{ t('pages.life.composerTitle') }}</h2>
|
:title="postModalTitle"
|
||||||
<p>{{ t('pages.life.composerPrompt') }}</p>
|
:subtitle="t('pages.life.composerPrompt')"
|
||||||
</div>
|
:close-label="t('common.close')"
|
||||||
|
@close="closePostModal"
|
||||||
|
>
|
||||||
<div v-if="!authReady" class="life-composer__auth-skeleton" aria-hidden="true">
|
<div v-if="!authReady" class="life-composer__auth-skeleton" aria-hidden="true">
|
||||||
<Skeleton variant="box" height="112px" />
|
<Skeleton variant="box" height="112px" />
|
||||||
<Skeleton width="42%" />
|
<Skeleton width="42%" />
|
||||||
@@ -579,9 +650,9 @@ onUnmounted(() => {
|
|||||||
<Icon :icon="isEditing ? iconSave : iconLife" class="ui-icon" aria-hidden="true" />
|
<Icon :icon="isEditing ? iconSave : iconLife" class="ui-icon" aria-hidden="true" />
|
||||||
{{ submitLabel }}
|
{{ submitLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button v-if="isEditing" class="ui-button ui-button--ghost" :disabled="busy" type="button" @click="resetForm">
|
<button class="ui-button ui-button--ghost" :disabled="busy" type="button" @click="closePostModal">
|
||||||
<Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
|
<Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
|
||||||
{{ t('pages.life.cancelEdit') }}
|
{{ t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -592,7 +663,7 @@ onUnmounted(() => {
|
|||||||
{{ t('nav.login') }}
|
{{ t('nav.login') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</Modal>
|
||||||
|
|
||||||
<section class="life-feed" :aria-busy="loading || loadingMore" :aria-label="t('pages.life.kicker')">
|
<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')">
|
<div v-if="loading" class="life-feed__list" :aria-label="t('pages.life.loading')">
|
||||||
@@ -885,7 +956,7 @@ onUnmounted(() => {
|
|||||||
<div v-if="hasMorePosts" ref="loadMoreSentinel" class="life-feed__sentinel" aria-hidden="true"></div>
|
<div v-if="hasMorePosts" ref="loadMoreSentinel" class="life-feed__sentinel" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-else class="status">{{ t('pages.life.empty') }}</p>
|
<p v-else class="status">{{ searchQuery ? t('pages.life.searchEmpty') : t('pages.life.empty') }}</p>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user