memi-board 0.44.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -3
- package/dist/ar-BCQdRw_S.js +622 -0
- package/dist/ar-BCQdRw_S.js.map +1 -0
- package/dist/composables/fetchPublicSeo.d.ts +4 -1
- package/dist/composables/useMemiBoardAutoTranslate.d.ts +33 -0
- package/dist/composables/useMemiBoardModeration.d.ts +5 -1
- package/dist/composables/useMemiBoardReports.d.ts +6 -1
- package/dist/composables/useMemiBoardSections.d.ts +2 -1
- package/dist/composables/useMemiBoardUsers.d.ts +4 -0
- package/dist/composables/useMemiBoardWritingAssistant.d.ts +9 -1
- package/dist/config.d.ts +5 -0
- package/dist/de-ddWQNaqs.js +622 -0
- package/dist/de-ddWQNaqs.js.map +1 -0
- package/dist/en-BVUBLkxc.js +648 -0
- package/dist/en-BVUBLkxc.js.map +1 -0
- package/dist/es-7JHjGVlM.js +622 -0
- package/dist/es-7JHjGVlM.js.map +1 -0
- package/dist/fr-C3NCMNoy.js +622 -0
- package/dist/fr-C3NCMNoy.js.map +1 -0
- package/dist/id-C8hCAMPJ.js +622 -0
- package/dist/id-C8hCAMPJ.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +1218 -1487
- package/dist/index.js.map +1 -1
- package/dist/ja-oTWri4hY.js +622 -0
- package/dist/ja-oTWri4hY.js.map +1 -0
- package/dist/module.mjs +55 -31
- package/dist/module.mjs.map +1 -1
- package/dist/pt-B9EH4scA.js +622 -0
- package/dist/pt-B9EH4scA.js.map +1 -0
- package/dist/runtime/components/Attachments.vue +4 -2
- package/dist/runtime/components/AuthorMenu.vue +14 -12
- package/dist/runtime/components/AvatarCropModal.vue +8 -5
- package/dist/runtime/components/CategorySettings.vue +65 -43
- package/dist/runtime/components/CommentFeed.vue +9 -7
- package/dist/runtime/components/CommentForm.vue +16 -10
- package/dist/runtime/components/CommentItem.vue +25 -26
- package/dist/runtime/components/CommentList.vue +7 -4
- package/dist/runtime/components/CommentThread.vue +7 -5
- package/dist/runtime/components/Detail.vue +20 -15
- package/dist/runtime/components/Editor.vue +145 -135
- package/dist/runtime/components/Footer.vue +14 -13
- package/dist/runtime/components/List.vue +31 -29
- package/dist/runtime/components/ListDefault.vue +4 -3
- package/dist/runtime/components/ListDense.vue +6 -5
- package/dist/runtime/components/ListImage.vue +4 -2
- package/dist/runtime/components/ListVideo.vue +3 -2
- package/dist/runtime/components/ListViewSwitch.vue +10 -6
- package/dist/runtime/components/MiniCard.vue +3 -2
- package/dist/runtime/components/MiniComments.vue +5 -4
- package/dist/runtime/components/MiniList.vue +3 -2
- package/dist/runtime/components/MiniPicker.vue +10 -9
- package/dist/runtime/components/MiniPost.vue +3 -2
- package/dist/runtime/components/MiniPosts.vue +5 -4
- package/dist/runtime/components/Profile.vue +12 -7
- package/dist/runtime/components/ProfileEditModal.vue +14 -13
- package/dist/runtime/components/ReportButton.vue +20 -10
- package/dist/runtime/components/ReportList.vue +11 -10
- package/dist/runtime/components/Section.vue +3 -2
- package/dist/runtime/components/Settings.vue +84 -62
- package/dist/runtime/components/ShareButton.vue +3 -1
- package/dist/runtime/components/SignIn.vue +14 -8
- package/dist/runtime/components/SwipeHint.vue +5 -3
- package/dist/runtime/components/UserCommentList.vue +8 -6
- package/dist/runtime/components/UserPostList.vue +6 -4
- package/dist/runtime/components/Users.vue +16 -10
- package/dist/runtime/composables/useMemiBoardSeo.ts +16 -10
- package/dist/slugify-6bG6GSO_.js +942 -0
- package/dist/slugify-6bG6GSO_.js.map +1 -0
- package/dist/storage.js +72 -71
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/utils/boardSeo.d.ts +5 -5
- package/dist/utils/compressImage.d.ts +2 -1
- package/dist/utils/formatDate.d.ts +6 -4
- package/dist/utils/imeSafeSubmit.d.ts +2 -0
- package/dist/utils/moderation-prompt.d.ts +18 -1
- package/dist/utils/moderation-strike.d.ts +2 -1
- package/dist/utils/section.d.ts +15 -2
- package/dist/zh-D5rDYV6E.js +622 -0
- package/dist/zh-D5rDYV6E.js.map +1 -0
- package/package.json +2 -3
- package/dist/slugify-Cm_pi2al.js +0 -124
- package/dist/slugify-Cm_pi2al.js.map +0 -1
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { imeSafeSubmitClick, imeSafeSubmitPointerDown } from 'memi-board/runtime'
|
|
2
3
|
import { computed, ref } from 'vue'
|
|
3
4
|
import { canWriteCommentByRole, useMemiBoardAuth } from 'memi-board/runtime'
|
|
4
5
|
import { COMMENT_BODY_MAX_LENGTH, useMemiBoardComments } from 'memi-board/runtime'
|
|
5
6
|
import { useMemiBoardModeration } from 'memi-board/runtime'
|
|
6
7
|
import { useMemiBoardSettings } from 'memi-board/runtime'
|
|
8
|
+
import { useMemiBoardI18n } from 'memi-board/runtime'
|
|
7
9
|
import type { CommentModel } from 'memi-board/runtime'
|
|
8
10
|
|
|
9
11
|
const props = defineProps<{ boardId: string, postId: string, parent?: CommentModel | null }>()
|
|
@@ -17,6 +19,7 @@ const { addComment, addReply } = useMemiBoardComments(
|
|
|
17
19
|
{ subscribe: false },
|
|
18
20
|
)
|
|
19
21
|
const { checkText } = useMemiBoardModeration()
|
|
22
|
+
const { t, formatNumber } = useMemiBoardI18n()
|
|
20
23
|
const { getBoard } = useMemiBoardSettings()
|
|
21
24
|
|
|
22
25
|
const board = computed(() => getBoard(props.boardId))
|
|
@@ -26,8 +29,8 @@ const canComment = computed(() => {
|
|
|
26
29
|
return canWriteCommentByRole(user.value.uid, isAdmin.value, isStaff.value, board.value)
|
|
27
30
|
})
|
|
28
31
|
const deniedMessage = computed(() => {
|
|
29
|
-
if (isHiddenBoard.value) return '
|
|
30
|
-
return '
|
|
32
|
+
if (isHiddenBoard.value) return t('commentForm.hiddenBoardDenied')
|
|
33
|
+
return t('commentForm.denied')
|
|
31
34
|
})
|
|
32
35
|
|
|
33
36
|
const body = ref('')
|
|
@@ -35,10 +38,11 @@ const submitting = ref(false)
|
|
|
35
38
|
const error = ref('')
|
|
36
39
|
|
|
37
40
|
async function handleSubmit() {
|
|
41
|
+
if (submitting.value) return
|
|
38
42
|
error.value = ''
|
|
39
43
|
if (!body.value.trim()) return
|
|
40
44
|
if (!user.value) {
|
|
41
|
-
error.value = '
|
|
45
|
+
error.value = t('commentForm.signInRequired')
|
|
42
46
|
return
|
|
43
47
|
}
|
|
44
48
|
if (!canComment.value) {
|
|
@@ -47,7 +51,7 @@ async function handleSubmit() {
|
|
|
47
51
|
}
|
|
48
52
|
if (isWriteRestricted.value) {
|
|
49
53
|
error.value = restrictedMessage.value
|
|
50
|
-
|| '
|
|
54
|
+
|| t('commentForm.writeRestricted')
|
|
51
55
|
return
|
|
52
56
|
}
|
|
53
57
|
|
|
@@ -55,7 +59,7 @@ async function handleSubmit() {
|
|
|
55
59
|
try {
|
|
56
60
|
const moderation = await checkText(body.value)
|
|
57
61
|
if (moderation.flagged) {
|
|
58
|
-
error.value = moderation.reason || '
|
|
62
|
+
error.value = moderation.reason || t('commentForm.moderationBlocked')
|
|
59
63
|
return
|
|
60
64
|
}
|
|
61
65
|
const input = {
|
|
@@ -86,7 +90,7 @@ async function handleSubmit() {
|
|
|
86
90
|
>
|
|
87
91
|
<UTextarea
|
|
88
92
|
v-model="body"
|
|
89
|
-
:placeholder="parent ?
|
|
93
|
+
:placeholder="parent ? t('commentForm.replyTo', { name: parent.authorName || t('common.label.user') }) : t('commentForm.placeholder')"
|
|
90
94
|
:rows="2"
|
|
91
95
|
:maxlength="COMMENT_BODY_MAX_LENGTH"
|
|
92
96
|
/>
|
|
@@ -97,7 +101,7 @@ async function handleSubmit() {
|
|
|
97
101
|
{{ restrictedMessage }}
|
|
98
102
|
</p>
|
|
99
103
|
<div class="flex justify-between items-center">
|
|
100
|
-
<span class="text-xs text-dimmed">{{ body.length
|
|
104
|
+
<span class="text-xs text-dimmed">{{ formatNumber(body.length) }} / {{ formatNumber(COMMENT_BODY_MAX_LENGTH) }}</span>
|
|
101
105
|
<p
|
|
102
106
|
v-if="error"
|
|
103
107
|
class="text-xs text-error"
|
|
@@ -109,15 +113,17 @@ async function handleSubmit() {
|
|
|
109
113
|
v-if="parent"
|
|
110
114
|
type="button"
|
|
111
115
|
size="sm"
|
|
112
|
-
label="
|
|
116
|
+
:label="t('common.action.cancel')"
|
|
113
117
|
color="neutral"
|
|
114
118
|
variant="ghost"
|
|
115
119
|
@click="emit('cancel')"
|
|
116
120
|
/>
|
|
117
121
|
<UButton
|
|
118
122
|
type="submit"
|
|
123
|
+
@pointerdown="imeSafeSubmitPointerDown"
|
|
124
|
+
@click="imeSafeSubmitClick"
|
|
119
125
|
size="sm"
|
|
120
|
-
:label="parent ? '
|
|
126
|
+
:label="parent ? t('commentForm.submitReply') : t('commentForm.submit')"
|
|
121
127
|
:loading="submitting"
|
|
122
128
|
:disabled="!body.trim() || isWriteRestricted"
|
|
123
129
|
/>
|
|
@@ -127,7 +133,7 @@ async function handleSubmit() {
|
|
|
127
133
|
v-else-if="!isSignedIn"
|
|
128
134
|
class="text-sm text-muted"
|
|
129
135
|
>
|
|
130
|
-
|
|
136
|
+
{{ t('commentForm.signInToComment') }}
|
|
131
137
|
</p>
|
|
132
138
|
<p
|
|
133
139
|
v-else
|
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { imeSafeSubmitClick, imeSafeSubmitPointerDown } from 'memi-board/runtime'
|
|
2
3
|
import { computed, inject, ref, watch } from 'vue'
|
|
3
4
|
import { Timestamp } from 'firebase/firestore'
|
|
4
|
-
import dayjs from 'dayjs'
|
|
5
|
-
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
6
|
-
import 'dayjs/locale/ko'
|
|
7
5
|
import type { CommentModel } from 'memi-board/runtime'
|
|
8
6
|
import {
|
|
9
7
|
COMMENT_BODY_MAX_LENGTH,
|
|
10
|
-
formatTimestampDetails,
|
|
11
8
|
memiBoardCommentLikesKey,
|
|
12
9
|
useMemiBoardAuth,
|
|
13
10
|
useMemiBoardComments,
|
|
11
|
+
useMemiBoardI18n,
|
|
14
12
|
useMemiBoardModeration,
|
|
15
13
|
} from 'memi-board/runtime'
|
|
16
14
|
|
|
17
|
-
dayjs.extend(relativeTime)
|
|
18
|
-
dayjs.locale('ko')
|
|
19
|
-
|
|
20
15
|
const props = defineProps<{
|
|
21
16
|
comment: CommentModel
|
|
22
17
|
boardId: string
|
|
@@ -37,6 +32,7 @@ const { updateComment, setCommentBlinded } = useMemiBoardComments(
|
|
|
37
32
|
{ subscribe: false },
|
|
38
33
|
)
|
|
39
34
|
const { checkText } = useMemiBoardModeration()
|
|
35
|
+
const { t, formatRelativeDate, formatTimestampDetails } = useMemiBoardI18n()
|
|
40
36
|
const commentLikes = inject(memiBoardCommentLikesKey, null)
|
|
41
37
|
|
|
42
38
|
const editing = ref(false)
|
|
@@ -63,7 +59,7 @@ watch(() => props.comment.likeCount, (likeCount) => {
|
|
|
63
59
|
})
|
|
64
60
|
|
|
65
61
|
const date = computed(() => props.comment.createdAt?.toDate?.())
|
|
66
|
-
const relativeDate = computed(() =>
|
|
62
|
+
const relativeDate = computed(() => formatRelativeDate(props.comment.createdAt, props.now))
|
|
67
63
|
const timestampDetails = computed(() => formatTimestampDetails(props.comment.createdAt, localUpdatedAt.value).join('\n'))
|
|
68
64
|
const blindLines = computed(() => Math.min(3, Math.max(1, Math.ceil(localBody.value.length / 45))))
|
|
69
65
|
|
|
@@ -80,13 +76,14 @@ function cancelEdit() {
|
|
|
80
76
|
}
|
|
81
77
|
|
|
82
78
|
async function saveEdit() {
|
|
79
|
+
if (saving.value) return
|
|
83
80
|
const body = editBody.value.trim()
|
|
84
81
|
if (!props.comment.id || !body || body === localBody.value) {
|
|
85
82
|
if (body === localBody.value) editing.value = false
|
|
86
83
|
return
|
|
87
84
|
}
|
|
88
85
|
if (isWriteRestricted.value) {
|
|
89
|
-
editError.value = restrictedMessage.value || '
|
|
86
|
+
editError.value = restrictedMessage.value || t('commentItem.editRestricted')
|
|
90
87
|
return
|
|
91
88
|
}
|
|
92
89
|
|
|
@@ -95,7 +92,7 @@ async function saveEdit() {
|
|
|
95
92
|
try {
|
|
96
93
|
const moderation = await checkText(body)
|
|
97
94
|
if (moderation.flagged) {
|
|
98
|
-
editError.value = moderation.reason || '
|
|
95
|
+
editError.value = moderation.reason || t('commentItem.moderationBlocked')
|
|
99
96
|
return
|
|
100
97
|
}
|
|
101
98
|
await updateComment(props.comment.id, body)
|
|
@@ -104,7 +101,7 @@ async function saveEdit() {
|
|
|
104
101
|
editing.value = false
|
|
105
102
|
}
|
|
106
103
|
catch (cause) {
|
|
107
|
-
editError.value = cause instanceof Error ? cause.message : '
|
|
104
|
+
editError.value = cause instanceof Error ? cause.message : t('commentItem.editFailed')
|
|
108
105
|
}
|
|
109
106
|
finally {
|
|
110
107
|
saving.value = false
|
|
@@ -115,8 +112,8 @@ async function toggleBlind() {
|
|
|
115
112
|
if (!props.comment.id || !user.value || !canManageBoard(props.comment.boardId)) return
|
|
116
113
|
const next = !localBlinded.value
|
|
117
114
|
const confirmed = window.confirm(next
|
|
118
|
-
? '
|
|
119
|
-
: '
|
|
115
|
+
? t('commentItem.blindConfirm')
|
|
116
|
+
: t('commentItem.unblindConfirm'))
|
|
120
117
|
if (!confirmed) return
|
|
121
118
|
|
|
122
119
|
blindSaving.value = true
|
|
@@ -127,7 +124,7 @@ async function toggleBlind() {
|
|
|
127
124
|
if (next) cancelEdit()
|
|
128
125
|
}
|
|
129
126
|
catch (cause) {
|
|
130
|
-
editError.value = cause instanceof Error ? cause.message : '
|
|
127
|
+
editError.value = cause instanceof Error ? cause.message : t('commentItem.blindFailed')
|
|
131
128
|
}
|
|
132
129
|
finally {
|
|
133
130
|
blindSaving.value = false
|
|
@@ -143,7 +140,7 @@ async function toggleLike() {
|
|
|
143
140
|
if (next !== wasLiked) localLikeCount.value = Math.max(0, localLikeCount.value + (next ? 1 : -1))
|
|
144
141
|
}
|
|
145
142
|
catch (cause) {
|
|
146
|
-
likeError.value = cause instanceof Error ? cause.message : '
|
|
143
|
+
likeError.value = cause instanceof Error ? cause.message : t('commentItem.likeFailed')
|
|
147
144
|
}
|
|
148
145
|
}
|
|
149
146
|
</script>
|
|
@@ -152,12 +149,12 @@ async function toggleLike() {
|
|
|
152
149
|
<div class="flex items-start gap-3">
|
|
153
150
|
<UAvatar
|
|
154
151
|
:src="comment.authorPhoto ?? undefined"
|
|
155
|
-
:alt="comment.authorName ?? '
|
|
152
|
+
:alt="comment.authorName ?? t('common.label.anonymous')"
|
|
156
153
|
size="sm"
|
|
157
154
|
/>
|
|
158
155
|
<div class="min-w-0 flex-1">
|
|
159
156
|
<div class="flex items-center gap-2">
|
|
160
|
-
<span class="text-sm font-medium">{{ comment.authorName ?? '
|
|
157
|
+
<span class="text-sm font-medium">{{ comment.authorName ?? t('common.label.anonymous') }}</span>
|
|
161
158
|
<UPopover :content="{ side: 'top' }" :ui="{ content: 'h-auto w-max' }">
|
|
162
159
|
<time
|
|
163
160
|
:datetime="date?.toISOString()"
|
|
@@ -192,7 +189,7 @@ async function toggleLike() {
|
|
|
192
189
|
<div class="flex justify-end gap-1">
|
|
193
190
|
<UButton
|
|
194
191
|
type="button"
|
|
195
|
-
label="
|
|
192
|
+
:label="t('common.action.cancel')"
|
|
196
193
|
size="xs"
|
|
197
194
|
color="neutral"
|
|
198
195
|
variant="ghost"
|
|
@@ -201,7 +198,9 @@ async function toggleLike() {
|
|
|
201
198
|
/>
|
|
202
199
|
<UButton
|
|
203
200
|
type="submit"
|
|
204
|
-
|
|
201
|
+
@pointerdown="imeSafeSubmitPointerDown"
|
|
202
|
+
@click="imeSafeSubmitClick"
|
|
203
|
+
:label="t('common.action.save')"
|
|
205
204
|
size="xs"
|
|
206
205
|
:loading="saving"
|
|
207
206
|
:disabled="!editBody.trim() || editBody.trim() === localBody"
|
|
@@ -221,7 +220,7 @@ async function toggleLike() {
|
|
|
221
220
|
/>
|
|
222
221
|
</div>
|
|
223
222
|
<div class="absolute inset-0 flex items-center justify-center px-3 text-center text-xs font-medium text-muted">
|
|
224
|
-
|
|
223
|
+
{{ t('commentItem.blinded') }}
|
|
225
224
|
</div>
|
|
226
225
|
</div>
|
|
227
226
|
<p v-else class="whitespace-pre-wrap break-words text-sm">
|
|
@@ -241,13 +240,13 @@ async function toggleLike() {
|
|
|
241
240
|
:color="isLiked ? 'error' : 'neutral'"
|
|
242
241
|
:label="String(localLikeCount)"
|
|
243
242
|
:loading="likePending"
|
|
244
|
-
:aria-label="isLiked ? '
|
|
243
|
+
:aria-label="isLiked ? t('common.action.unlike') : t('common.action.like')"
|
|
245
244
|
@click="toggleLike"
|
|
246
245
|
/>
|
|
247
246
|
<span
|
|
248
247
|
v-else
|
|
249
248
|
class="flex items-center gap-0.5 px-1 text-xs text-muted"
|
|
250
|
-
aria-label="
|
|
249
|
+
:aria-label="t('common.action.like')"
|
|
251
250
|
>
|
|
252
251
|
<UIcon name="i-lucide-heart" class="size-3.5" />
|
|
253
252
|
{{ localLikeCount }}
|
|
@@ -258,7 +257,7 @@ async function toggleLike() {
|
|
|
258
257
|
size="xs"
|
|
259
258
|
color="neutral"
|
|
260
259
|
variant="ghost"
|
|
261
|
-
aria-label="
|
|
260
|
+
:aria-label="t('commentItem.edit')"
|
|
262
261
|
:disabled="editing"
|
|
263
262
|
@click="startEdit"
|
|
264
263
|
/>
|
|
@@ -269,7 +268,7 @@ async function toggleLike() {
|
|
|
269
268
|
:color="localBlinded ? 'neutral' : 'warning'"
|
|
270
269
|
variant="ghost"
|
|
271
270
|
:loading="blindSaving"
|
|
272
|
-
:aria-label="localBlinded ? '
|
|
271
|
+
:aria-label="localBlinded ? t('commentItem.unblind') : t('commentItem.blind')"
|
|
273
272
|
@click="toggleBlind"
|
|
274
273
|
/>
|
|
275
274
|
<UButton
|
|
@@ -277,7 +276,7 @@ async function toggleLike() {
|
|
|
277
276
|
size="xs"
|
|
278
277
|
color="neutral"
|
|
279
278
|
variant="ghost"
|
|
280
|
-
aria-label="
|
|
279
|
+
:aria-label="t('common.action.reply')"
|
|
281
280
|
@click="emit('reply', comment)"
|
|
282
281
|
/>
|
|
283
282
|
<UButton
|
|
@@ -287,7 +286,7 @@ async function toggleLike() {
|
|
|
287
286
|
variant="ghost"
|
|
288
287
|
color="error"
|
|
289
288
|
:loading="deleting"
|
|
290
|
-
aria-label="
|
|
289
|
+
:aria-label="t('commentItem.delete')"
|
|
291
290
|
@click="emit('delete', comment.id)"
|
|
292
291
|
/>
|
|
293
292
|
</div>
|
|
@@ -4,12 +4,15 @@ import {
|
|
|
4
4
|
memiBoardCommentLikesKey,
|
|
5
5
|
useMemiBoardCommentLikes,
|
|
6
6
|
useMemiBoardComments,
|
|
7
|
+
useMemiBoardI18n,
|
|
7
8
|
} from 'memi-board/runtime'
|
|
8
9
|
import MemiBoardCommentSkeleton from './CommentSkeleton.vue'
|
|
9
10
|
import MemiBoardCommentThread from './CommentThread.vue'
|
|
10
11
|
|
|
11
12
|
const props = defineProps<{ boardId: string, postId: string }>()
|
|
12
13
|
|
|
14
|
+
const { t } = useMemiBoardI18n()
|
|
15
|
+
|
|
13
16
|
const { comments, commentsPending, hasMore, loadingMore, loadMore } = useMemiBoardComments(
|
|
14
17
|
toRef(props, 'boardId'),
|
|
15
18
|
toRef(props, 'postId'),
|
|
@@ -79,14 +82,14 @@ onUnmounted(() => {
|
|
|
79
82
|
|
|
80
83
|
<template>
|
|
81
84
|
<div class="flex flex-col gap-3">
|
|
82
|
-
<div v-if="commentsPending" class="flex flex-col gap-3" aria-label="
|
|
85
|
+
<div v-if="commentsPending" class="flex flex-col gap-3" :aria-label="t('commentList.loading')">
|
|
83
86
|
<MemiBoardCommentSkeleton v-for="index in 10" :key="`initial-${index}`" />
|
|
84
87
|
</div>
|
|
85
88
|
<p
|
|
86
89
|
v-else-if="!comments.length"
|
|
87
90
|
class="text-sm text-muted"
|
|
88
91
|
>
|
|
89
|
-
|
|
92
|
+
{{ t('commentList.empty') }}
|
|
90
93
|
</p>
|
|
91
94
|
|
|
92
95
|
<MemiBoardCommentThread
|
|
@@ -104,7 +107,7 @@ onUnmounted(() => {
|
|
|
104
107
|
class="flex justify-center py-3"
|
|
105
108
|
>
|
|
106
109
|
<UButton
|
|
107
|
-
label="
|
|
110
|
+
:label="t('commentList.loadMore')"
|
|
108
111
|
icon="i-lucide-chevron-down"
|
|
109
112
|
color="neutral"
|
|
110
113
|
variant="soft"
|
|
@@ -114,7 +117,7 @@ onUnmounted(() => {
|
|
|
114
117
|
/>
|
|
115
118
|
</div>
|
|
116
119
|
|
|
117
|
-
<div v-if="morePending" class="flex flex-col gap-3" aria-label="
|
|
120
|
+
<div v-if="morePending" class="flex flex-col gap-3" :aria-label="t('commentList.loadingMore')">
|
|
118
121
|
<MemiBoardCommentSkeleton v-for="index in 10" :key="`more-${index}`" />
|
|
119
122
|
</div>
|
|
120
123
|
</div>
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
3
|
import type { CommentModel } from 'memi-board/runtime'
|
|
4
|
-
import { useMemiBoardComments, useMemiBoardReplies } from 'memi-board/runtime'
|
|
4
|
+
import { useMemiBoardComments, useMemiBoardI18n, useMemiBoardReplies } from 'memi-board/runtime'
|
|
5
5
|
import MemiBoardCommentForm from './CommentForm.vue'
|
|
6
6
|
import MemiBoardCommentItem from './CommentItem.vue'
|
|
7
7
|
import MemiBoardCommentSkeleton from './CommentSkeleton.vue'
|
|
8
8
|
|
|
9
9
|
const props = defineProps<{ boardId: string, postId: string, root: CommentModel, now: number }>()
|
|
10
10
|
|
|
11
|
+
const { t } = useMemiBoardI18n()
|
|
12
|
+
|
|
11
13
|
const { deleteComment } = useMemiBoardComments(
|
|
12
14
|
props.boardId,
|
|
13
15
|
props.postId,
|
|
@@ -56,7 +58,7 @@ async function handleSaved() {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
async function handleDelete(comment: CommentModel) {
|
|
59
|
-
if (!window.confirm('
|
|
61
|
+
if (!window.confirm(t('commentThread.deleteConfirm'))) return
|
|
60
62
|
deletingId.value = comment.id ?? null
|
|
61
63
|
error.value = ''
|
|
62
64
|
try {
|
|
@@ -64,7 +66,7 @@ async function handleDelete(comment: CommentModel) {
|
|
|
64
66
|
if (comment.parentId) await refresh()
|
|
65
67
|
}
|
|
66
68
|
catch (cause) {
|
|
67
|
-
error.value = cause instanceof Error ? cause.message : '
|
|
69
|
+
error.value = cause instanceof Error ? cause.message : t('commentThread.deleteFailed')
|
|
68
70
|
}
|
|
69
71
|
finally {
|
|
70
72
|
deletingId.value = null
|
|
@@ -86,7 +88,7 @@ async function handleDelete(comment: CommentModel) {
|
|
|
86
88
|
|
|
87
89
|
<UButton
|
|
88
90
|
v-if="(root.replyCount ?? 0) > 0"
|
|
89
|
-
:label="expanded ? '
|
|
91
|
+
:label="expanded ? t('commentThread.hideReplies') : t('commentThread.showReplies', { count: root.replyCount ?? 0 })"
|
|
90
92
|
:icon="expanded ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
|
91
93
|
color="neutral"
|
|
92
94
|
variant="link"
|
|
@@ -111,7 +113,7 @@ async function handleDelete(comment: CommentModel) {
|
|
|
111
113
|
/>
|
|
112
114
|
<UButton
|
|
113
115
|
v-if="needsMoreReplies"
|
|
114
|
-
label="
|
|
116
|
+
:label="t('commentThread.loadMoreReplies')"
|
|
115
117
|
color="neutral"
|
|
116
118
|
variant="soft"
|
|
117
119
|
size="sm"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { ref, computed, onBeforeUnmount, onMounted, watch } from 'vue'
|
|
3
3
|
import Youtube from '@tiptap/extension-youtube'
|
|
4
4
|
import type { PostModel } from 'memi-board/runtime'
|
|
5
|
-
import {
|
|
5
|
+
import { renderMarkdownToHtml, useMemiBoardI18n } from 'memi-board/runtime'
|
|
6
6
|
import { useMemiBoardPost, useMemiBoardPosts } from 'memi-board/runtime'
|
|
7
7
|
import { useMemiBoardAuth } from 'memi-board/runtime'
|
|
8
8
|
import { useMemiBoardSettings } from 'memi-board/runtime'
|
|
@@ -36,7 +36,9 @@ const emit = defineEmits<{
|
|
|
36
36
|
|
|
37
37
|
const { getAdjacentPosts, deletePost, publishPost } = useMemiBoardPosts(() => props.boardId)
|
|
38
38
|
const { canEdit, canDelete } = useMemiBoardAuth()
|
|
39
|
-
const { boardLabel } = useMemiBoardSettings()
|
|
39
|
+
const { boardLabel, getBoard } = useMemiBoardSettings()
|
|
40
|
+
// 보드 설정 언어 → 호스트 언어 순. 댓글 등 자식 컴포넌트도 이 언어를 따른다.
|
|
41
|
+
const { t, dir, formatRelativeDate, formatTimestampDetails } = useMemiBoardI18n({ boardLocale: () => getBoard(props.boardId)?.locale })
|
|
40
42
|
const { recordView } = useMemiBoardViews()
|
|
41
43
|
|
|
42
44
|
// 실시간 구독 — 다른 사람의 좋아요·댓글 수 변경이 화면에 바로 반영된다.
|
|
@@ -124,7 +126,7 @@ async function handlePublish() {
|
|
|
124
126
|
|
|
125
127
|
async function handleDelete() {
|
|
126
128
|
if (!post.value) return
|
|
127
|
-
if (!window.confirm('
|
|
129
|
+
if (!window.confirm(t('detail.deleteConfirm'))) return
|
|
128
130
|
deleting.value = true
|
|
129
131
|
try {
|
|
130
132
|
await deletePost(props.postId)
|
|
@@ -152,6 +154,7 @@ const contentHtml = computed(() => {
|
|
|
152
154
|
<template>
|
|
153
155
|
<div
|
|
154
156
|
v-if="loading"
|
|
157
|
+
:dir="dir"
|
|
155
158
|
class="flex flex-col gap-3"
|
|
156
159
|
>
|
|
157
160
|
<USkeleton class="h-8 w-2/3" />
|
|
@@ -160,13 +163,15 @@ const contentHtml = computed(() => {
|
|
|
160
163
|
|
|
161
164
|
<p
|
|
162
165
|
v-else-if="notFound"
|
|
166
|
+
:dir="dir"
|
|
163
167
|
class="text-sm text-muted"
|
|
164
168
|
>
|
|
165
|
-
|
|
169
|
+
{{ t('detail.notFound') }}
|
|
166
170
|
</p>
|
|
167
171
|
|
|
168
172
|
<div
|
|
169
173
|
v-else-if="post"
|
|
174
|
+
:dir="dir"
|
|
170
175
|
class="flex flex-col gap-6 touch-pan-y"
|
|
171
176
|
@touchstart.passive="onTouchStart"
|
|
172
177
|
@touchend.passive="onTouchEnd"
|
|
@@ -175,13 +180,13 @@ const contentHtml = computed(() => {
|
|
|
175
180
|
<div class="flex items-start gap-2">
|
|
176
181
|
<UIcon name="i-lucide-eye-off" class="mt-0.5 size-4 shrink-0 text-warning" />
|
|
177
182
|
<div>
|
|
178
|
-
<p class="text-sm font-medium text-highlighted"
|
|
179
|
-
<p class="text-xs text-muted"
|
|
183
|
+
<p class="text-sm font-medium text-highlighted">{{ t('detail.draftPreview') }}</p>
|
|
184
|
+
<p class="text-xs text-muted">{{ t('detail.draftNotice') }}</p>
|
|
180
185
|
</div>
|
|
181
186
|
</div>
|
|
182
187
|
<UButton
|
|
183
188
|
v-if="canEdit(post)"
|
|
184
|
-
label="
|
|
189
|
+
:label="t('detail.publish')"
|
|
185
190
|
icon="i-lucide-send"
|
|
186
191
|
size="sm"
|
|
187
192
|
class="shrink-0"
|
|
@@ -207,7 +212,7 @@ const contentHtml = computed(() => {
|
|
|
207
212
|
</div>
|
|
208
213
|
<UButton
|
|
209
214
|
icon="i-lucide-arrow-left"
|
|
210
|
-
aria-label="
|
|
215
|
+
:aria-label="t('detail.backToList')"
|
|
211
216
|
color="neutral"
|
|
212
217
|
variant="ghost"
|
|
213
218
|
size="sm"
|
|
@@ -283,7 +288,7 @@ const contentHtml = computed(() => {
|
|
|
283
288
|
size="sm"
|
|
284
289
|
variant="ghost"
|
|
285
290
|
color="neutral"
|
|
286
|
-
label="
|
|
291
|
+
:label="t('common.action.edit')"
|
|
287
292
|
@click="emit('edit', postId)"
|
|
288
293
|
/>
|
|
289
294
|
<UButton
|
|
@@ -292,7 +297,7 @@ const contentHtml = computed(() => {
|
|
|
292
297
|
size="sm"
|
|
293
298
|
variant="ghost"
|
|
294
299
|
color="error"
|
|
295
|
-
label="
|
|
300
|
+
:label="t('common.action.delete')"
|
|
296
301
|
:loading="deleting"
|
|
297
302
|
@click="handleDelete"
|
|
298
303
|
/>
|
|
@@ -315,12 +320,12 @@ const contentHtml = computed(() => {
|
|
|
315
320
|
/>
|
|
316
321
|
</div>
|
|
317
322
|
|
|
318
|
-
<nav class="grid grid-cols-3 items-center py-3" aria-label="
|
|
323
|
+
<nav class="grid grid-cols-3 items-center py-3" :aria-label="t('detail.postNav')">
|
|
319
324
|
<UButton
|
|
320
325
|
variant="ghost"
|
|
321
326
|
color="neutral"
|
|
322
327
|
icon="i-lucide-chevron-left"
|
|
323
|
-
label="
|
|
328
|
+
:label="t('common.action.prev')"
|
|
324
329
|
class="justify-self-start"
|
|
325
330
|
:disabled="!previousPost?.id"
|
|
326
331
|
@click="previousPost?.id && emit('navigate', previousPost)"
|
|
@@ -329,7 +334,7 @@ const contentHtml = computed(() => {
|
|
|
329
334
|
variant="ghost"
|
|
330
335
|
color="neutral"
|
|
331
336
|
icon="i-lucide-list"
|
|
332
|
-
label="
|
|
337
|
+
:label="t('detail.list')"
|
|
333
338
|
class="justify-self-center"
|
|
334
339
|
@click="emit('list')"
|
|
335
340
|
/>
|
|
@@ -337,7 +342,7 @@ const contentHtml = computed(() => {
|
|
|
337
342
|
variant="ghost"
|
|
338
343
|
color="neutral"
|
|
339
344
|
trailing-icon="i-lucide-chevron-right"
|
|
340
|
-
label="
|
|
345
|
+
:label="t('common.action.next')"
|
|
341
346
|
class="justify-self-end"
|
|
342
347
|
:disabled="!nextPost?.id"
|
|
343
348
|
@click="nextPost?.id && emit('navigate', nextPost)"
|
|
@@ -348,7 +353,7 @@ const contentHtml = computed(() => {
|
|
|
348
353
|
|
|
349
354
|
<section v-if="!isDraft" class="flex flex-col gap-4 border-t border-default pt-4">
|
|
350
355
|
<h2 class="text-sm font-medium text-muted">
|
|
351
|
-
|
|
356
|
+
{{ t('detail.comments') }}
|
|
352
357
|
</h2>
|
|
353
358
|
<MemiBoardCommentList :board-id="boardId" :post-id="postId" />
|
|
354
359
|
<MemiBoardCommentForm :board-id="boardId" :post-id="postId" />
|