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,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
|
-
import { slugify, useMemiBoardAuth, useMemiBoardSettings, useMemiBoardUsers } from 'memi-board/runtime'
|
|
3
|
+
import { MEMI_BOARD_LOCALES, slugify, useMemiBoardAuth, useMemiBoardI18n, useMemiBoardSettings, useMemiBoardUsers } from 'memi-board/runtime'
|
|
4
4
|
import type { BoardCategory, BoardEditorType, BoardListView, BoardVisibility, BoardWriteRole } from 'memi-board/runtime'
|
|
5
5
|
import MemiBoardOptionCards from './OptionCards.vue'
|
|
6
6
|
|
|
@@ -27,25 +27,35 @@ const emit = defineEmits<{
|
|
|
27
27
|
saved: [categories: BoardCategory[]]
|
|
28
28
|
}>()
|
|
29
29
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
{ label: '
|
|
33
|
-
{ label: '
|
|
34
|
-
{ label: '
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{ label: '
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
{ label: '
|
|
43
|
-
{ label: '
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{ label: '
|
|
48
|
-
|
|
30
|
+
const { t } = useMemiBoardI18n()
|
|
31
|
+
const listViewOptions = computed<Array<{ label: string, value: BoardListView }>>(() => [
|
|
32
|
+
{ label: t('settings.option.default'), value: 'default' },
|
|
33
|
+
{ label: t('settings.listView.options.dense'), value: 'dense' },
|
|
34
|
+
{ label: t('common.label.image'), value: 'image' },
|
|
35
|
+
{ label: t('settings.listView.options.video'), value: 'video' },
|
|
36
|
+
])
|
|
37
|
+
const editorTypeOptions = computed<Array<{ label: string, value: BoardEditorType, description: string }>>(() => [
|
|
38
|
+
{ label: t('settings.option.default'), value: 'default', description: t('settings.editorType.options.default') },
|
|
39
|
+
{ label: t('common.label.image'), value: 'image', description: t('settings.editorType.options.image') },
|
|
40
|
+
])
|
|
41
|
+
const writeRoleOptions = computed<Array<{ label: string, value: BoardWriteRole }>>(() => [
|
|
42
|
+
{ label: t('settings.writeRole.options.user'), value: 'user' },
|
|
43
|
+
{ label: t('settings.writeRole.options.staff'), value: 'staff' },
|
|
44
|
+
{ label: t('settings.writeRole.options.admin'), value: 'admin' },
|
|
45
|
+
])
|
|
46
|
+
const visibilityOptions = computed<Array<{ label: string, value: BoardVisibility, description?: string }>>(() => [
|
|
47
|
+
{ label: t('settings.visibility.options.public.label'), value: 'public', description: t('settings.visibility.options.public.description') },
|
|
48
|
+
{ label: t('settings.visibility.options.hidden.label'), value: 'hidden', description: t('settings.visibility.options.hidden.description') },
|
|
49
|
+
])
|
|
50
|
+
// reka-ui Select 는 빈 문자열 값을 허용하지 않아 '자동'을 sentinel 로 두고 저장 시 '' 로 바꾼다.
|
|
51
|
+
const LOCALE_AUTO = 'auto'
|
|
52
|
+
const localeOptions = computed(() => [
|
|
53
|
+
{ label: t('settings.locale.auto'), value: LOCALE_AUTO },
|
|
54
|
+
...MEMI_BOARD_LOCALES.map((item: { code: string, name: string }) => ({ label: item.name, value: item.code })),
|
|
55
|
+
])
|
|
56
|
+
function fromLocaleOption(value: string) {
|
|
57
|
+
return value === LOCALE_AUTO ? '' : value
|
|
58
|
+
}
|
|
49
59
|
|
|
50
60
|
const { isAdmin, rolePending } = useMemiBoardAuth()
|
|
51
61
|
const { categories, settingsPending, saveCategory, saveCategories, deleteCategory } = useMemiBoardSettings()
|
|
@@ -91,6 +101,7 @@ watch([categories, settingsPending], ([list, loading]) => {
|
|
|
91
101
|
writeRole: category.writeRole ?? 'user',
|
|
92
102
|
commentWriteRole: category.commentWriteRole ?? 'user',
|
|
93
103
|
allowedStaffUids: category.allowedStaffUids ?? [],
|
|
104
|
+
locale: category.locale ?? '',
|
|
94
105
|
}))
|
|
95
106
|
}
|
|
96
107
|
}, { immediate: true })
|
|
@@ -112,6 +123,7 @@ function addCategory() {
|
|
|
112
123
|
writeRole: 'user',
|
|
113
124
|
commentWriteRole: 'user',
|
|
114
125
|
allowedStaffUids: [],
|
|
126
|
+
locale: '',
|
|
115
127
|
})
|
|
116
128
|
newLabel.value = ''
|
|
117
129
|
}
|
|
@@ -129,7 +141,7 @@ async function moveCategory(index: number, offset: -1 | 1) {
|
|
|
129
141
|
await saveCategories(draft.value)
|
|
130
142
|
}
|
|
131
143
|
catch (cause) {
|
|
132
|
-
error.value = cause instanceof Error ? cause.message : '
|
|
144
|
+
error.value = cause instanceof Error ? cause.message : t('settings.error.orderFailed')
|
|
133
145
|
}
|
|
134
146
|
finally {
|
|
135
147
|
ordering.value = false
|
|
@@ -139,7 +151,7 @@ async function moveCategory(index: number, offset: -1 | 1) {
|
|
|
139
151
|
async function removeCategory(index: number) {
|
|
140
152
|
const category = draft.value[index]
|
|
141
153
|
if (!category || !window.confirm(
|
|
142
|
-
|
|
154
|
+
t('settings.deleteConfirm', { label: category.label }),
|
|
143
155
|
)) return
|
|
144
156
|
error.value = ''
|
|
145
157
|
deletingId.value = category.id
|
|
@@ -148,7 +160,7 @@ async function removeCategory(index: number) {
|
|
|
148
160
|
draft.value.splice(index, 1)
|
|
149
161
|
}
|
|
150
162
|
catch (cause) {
|
|
151
|
-
error.value = cause instanceof Error ? cause.message : '
|
|
163
|
+
error.value = cause instanceof Error ? cause.message : t('settings.error.deleteFailed')
|
|
152
164
|
}
|
|
153
165
|
finally {
|
|
154
166
|
deletingId.value = null
|
|
@@ -171,7 +183,7 @@ async function save(category: BoardCategory, index: number) {
|
|
|
171
183
|
emit('saved', draft.value)
|
|
172
184
|
}
|
|
173
185
|
catch (cause) {
|
|
174
|
-
error.value = cause instanceof Error ? cause.message : '
|
|
186
|
+
error.value = cause instanceof Error ? cause.message : t('settings.error.saveFailed')
|
|
175
187
|
}
|
|
176
188
|
finally {
|
|
177
189
|
savingId.value = null
|
|
@@ -179,7 +191,7 @@ async function save(category: BoardCategory, index: number) {
|
|
|
179
191
|
}
|
|
180
192
|
|
|
181
193
|
async function runDeleteAll() {
|
|
182
|
-
if (!props.deleteAll || deleteConfirm.value !== '
|
|
194
|
+
if (!props.deleteAll || deleteConfirm.value !== t('settings.danger.phrase')) return
|
|
183
195
|
deletingAll.value = true
|
|
184
196
|
deleteDone.value = false
|
|
185
197
|
error.value = ''
|
|
@@ -189,7 +201,7 @@ async function runDeleteAll() {
|
|
|
189
201
|
deleteDone.value = true
|
|
190
202
|
}
|
|
191
203
|
catch (cause) {
|
|
192
|
-
error.value = cause instanceof Error ? cause.message : '
|
|
204
|
+
error.value = cause instanceof Error ? cause.message : t('settings.error.deleteAllFailed')
|
|
193
205
|
}
|
|
194
206
|
finally {
|
|
195
207
|
deletingAll.value = false
|
|
@@ -198,14 +210,14 @@ async function runDeleteAll() {
|
|
|
198
210
|
</script>
|
|
199
211
|
|
|
200
212
|
<template>
|
|
201
|
-
<div v-if="pending" class="py-16 text-center text-sm text-muted"
|
|
213
|
+
<div v-if="pending" class="py-16 text-center text-sm text-muted">{{ t('settings.loading') }}</div>
|
|
202
214
|
<UAlert
|
|
203
215
|
v-else-if="!canManage"
|
|
204
216
|
color="neutral"
|
|
205
217
|
variant="subtle"
|
|
206
218
|
icon="i-lucide-circle-help"
|
|
207
|
-
title="
|
|
208
|
-
description="
|
|
219
|
+
:title="t('settings.notFound.title')"
|
|
220
|
+
:description="t('settings.notFound.checkAddress')"
|
|
209
221
|
/>
|
|
210
222
|
<div v-else class="flex flex-col gap-4">
|
|
211
223
|
<UAlert
|
|
@@ -213,8 +225,8 @@ async function runDeleteAll() {
|
|
|
213
225
|
color="neutral"
|
|
214
226
|
variant="subtle"
|
|
215
227
|
icon="i-lucide-circle-help"
|
|
216
|
-
title="
|
|
217
|
-
description="
|
|
228
|
+
:title="t('settings.notFound.title')"
|
|
229
|
+
:description="t('settings.notFound.checkBoardAddress')"
|
|
218
230
|
/>
|
|
219
231
|
<details
|
|
220
232
|
v-for="(category, index) in draft"
|
|
@@ -225,18 +237,18 @@ async function runDeleteAll() {
|
|
|
225
237
|
<summary class="flex cursor-pointer list-none items-center justify-between gap-3 px-4 py-3 select-none [&::-webkit-details-marker]:hidden">
|
|
226
238
|
<div class="min-w-0">
|
|
227
239
|
<code class="text-sm font-semibold text-highlighted">{{ category.id }}</code>
|
|
228
|
-
<p class="mt-1 truncate text-xs text-muted">{{ category.label || '
|
|
240
|
+
<p class="mt-1 truncate text-xs text-muted">{{ category.label || t('settings.noLabel') }} · {{ categoryTo(category.id) }}</p>
|
|
229
241
|
</div>
|
|
230
242
|
<UIcon name="i-lucide-chevron-down" class="size-4 text-muted transition-transform group-open:rotate-180" />
|
|
231
243
|
</summary>
|
|
232
244
|
|
|
233
245
|
<div class="flex flex-col gap-5 border-t border-default px-4 py-4">
|
|
234
246
|
<div class="grid gap-2 sm:grid-cols-[9rem_1fr] sm:items-center">
|
|
235
|
-
<div><p class="text-sm font-medium"
|
|
247
|
+
<div><p class="text-sm font-medium">{{ t('settings.id.label') }}</p><p class="text-xs text-muted">{{ t('settings.id.help') }}</p></div>
|
|
236
248
|
<UInput :model-value="category.id" disabled class="font-mono" />
|
|
237
249
|
</div>
|
|
238
250
|
<div class="flex flex-col gap-2">
|
|
239
|
-
<div><p class="text-sm font-medium"
|
|
251
|
+
<div><p class="text-sm font-medium">{{ t('settings.writeRole.label') }}</p><p class="text-xs text-muted">{{ t('settings.writeRole.help') }}</p></div>
|
|
240
252
|
<MemiBoardOptionCards
|
|
241
253
|
v-model="category.writeRole"
|
|
242
254
|
:options="writeRoleOptions"
|
|
@@ -245,11 +257,11 @@ async function runDeleteAll() {
|
|
|
245
257
|
</div>
|
|
246
258
|
<div class="flex flex-col gap-2">
|
|
247
259
|
<div>
|
|
248
|
-
<p class="text-sm font-medium"
|
|
260
|
+
<p class="text-sm font-medium">{{ t('settings.commentWriteRole.label') }}</p>
|
|
249
261
|
<p class="text-xs text-muted">
|
|
250
262
|
{{ category.visibility === 'hidden'
|
|
251
|
-
? '
|
|
252
|
-
: '
|
|
263
|
+
? t('settings.commentWriteRole.hiddenHelp')
|
|
264
|
+
: t('settings.commentWriteRole.help') }}
|
|
253
265
|
</p>
|
|
254
266
|
</div>
|
|
255
267
|
<MemiBoardOptionCards
|
|
@@ -262,40 +274,40 @@ async function runDeleteAll() {
|
|
|
262
274
|
v-else
|
|
263
275
|
class="rounded-lg border border-default px-3 py-2.5 text-sm text-muted"
|
|
264
276
|
>
|
|
265
|
-
|
|
277
|
+
{{ t('settings.commentWriteRole.hiddenFixed') }}
|
|
266
278
|
</p>
|
|
267
279
|
</div>
|
|
268
280
|
<div v-if="needsStaffPicker(category)" class="grid gap-2 sm:grid-cols-[9rem_1fr] sm:items-center">
|
|
269
|
-
<div><p class="text-sm font-medium"
|
|
281
|
+
<div><p class="text-sm font-medium">{{ t('settings.staff.label') }}</p><p class="text-xs text-muted">{{ t('settings.staff.help') }}</p></div>
|
|
270
282
|
<USelectMenu
|
|
271
283
|
v-model="category.allowedStaffUids"
|
|
272
284
|
:items="staffOptions"
|
|
273
285
|
value-key="value"
|
|
274
286
|
label-key="label"
|
|
275
287
|
multiple
|
|
276
|
-
placeholder="
|
|
288
|
+
:placeholder="t('settings.staff.placeholder')"
|
|
277
289
|
@update:model-value="savedId = null"
|
|
278
290
|
/>
|
|
279
291
|
</div>
|
|
280
292
|
<div class="grid gap-2 sm:grid-cols-[9rem_1fr] sm:items-center">
|
|
281
|
-
<div><p class="text-sm font-medium"
|
|
293
|
+
<div><p class="text-sm font-medium">{{ t('settings.label.label') }}</p><p class="text-xs text-muted">{{ t('settings.label.help') }}</p></div>
|
|
282
294
|
<UInput v-model="category.label" @update:model-value="savedId = null" />
|
|
283
295
|
</div>
|
|
284
296
|
<div class="grid gap-2 sm:grid-cols-[9rem_1fr] sm:items-start">
|
|
285
|
-
<div><p class="text-sm font-medium"
|
|
297
|
+
<div><p class="text-sm font-medium">{{ t('settings.description.label') }}</p><p class="text-xs text-muted">{{ t('settings.description.help') }}</p></div>
|
|
286
298
|
<UTextarea
|
|
287
299
|
v-model="category.description"
|
|
288
300
|
:rows="3"
|
|
289
301
|
autoresize
|
|
290
302
|
:maxrows="6"
|
|
291
|
-
placeholder="
|
|
303
|
+
:placeholder="t('settings.description.placeholder')"
|
|
292
304
|
@update:model-value="savedId = null"
|
|
293
305
|
/>
|
|
294
306
|
</div>
|
|
295
307
|
<div v-if="canManageStaffAssignment" class="flex flex-col gap-2">
|
|
296
308
|
<div>
|
|
297
|
-
<p class="text-sm font-medium"
|
|
298
|
-
<p class="text-xs text-muted"
|
|
309
|
+
<p class="text-sm font-medium">{{ t('settings.visibility.label') }}</p>
|
|
310
|
+
<p class="text-xs text-muted">{{ t('settings.visibility.help') }}</p>
|
|
299
311
|
</div>
|
|
300
312
|
<MemiBoardOptionCards
|
|
301
313
|
v-model="category.visibility"
|
|
@@ -304,7 +316,7 @@ async function runDeleteAll() {
|
|
|
304
316
|
/>
|
|
305
317
|
</div>
|
|
306
318
|
<div class="flex flex-col gap-2">
|
|
307
|
-
<div><p class="text-sm font-medium"
|
|
319
|
+
<div><p class="text-sm font-medium">{{ t('settings.listView.label') }}</p><p class="text-xs text-muted">{{ t('settings.listView.help') }}</p></div>
|
|
308
320
|
<MemiBoardOptionCards
|
|
309
321
|
v-model="category.listView"
|
|
310
322
|
:options="listViewOptions"
|
|
@@ -312,21 +324,31 @@ async function runDeleteAll() {
|
|
|
312
324
|
/>
|
|
313
325
|
</div>
|
|
314
326
|
<div class="flex flex-col gap-2">
|
|
315
|
-
<div><p class="text-sm font-medium"
|
|
327
|
+
<div><p class="text-sm font-medium">{{ t('settings.editorType.label') }}</p><p class="text-xs text-muted">{{ t('settings.editorType.help') }}</p></div>
|
|
316
328
|
<MemiBoardOptionCards
|
|
317
329
|
v-model="category.editorType"
|
|
318
330
|
:options="editorTypeOptions"
|
|
319
331
|
@update:model-value="savedId = null"
|
|
320
332
|
/>
|
|
321
333
|
</div>
|
|
334
|
+
<div class="grid gap-2 sm:grid-cols-[9rem_1fr] sm:items-center">
|
|
335
|
+
<div><p class="text-sm font-medium">{{ t('settings.locale.label') }}</p><p class="text-xs text-muted">{{ t('settings.locale.help') }}</p></div>
|
|
336
|
+
<USelect
|
|
337
|
+
:model-value="category.locale || LOCALE_AUTO"
|
|
338
|
+
:items="localeOptions"
|
|
339
|
+
value-key="value"
|
|
340
|
+
label-key="label"
|
|
341
|
+
@update:model-value="category.locale = fromLocaleOption($event as string); savedId = null"
|
|
342
|
+
/>
|
|
343
|
+
</div>
|
|
322
344
|
<div class="flex flex-wrap justify-between gap-2 border-t border-default pt-4">
|
|
323
345
|
<div v-if="!categoryId" class="flex gap-1">
|
|
324
|
-
<UButton label="
|
|
325
|
-
<UButton label="
|
|
346
|
+
<UButton :label="t('common.action.toTop')" icon="i-lucide-arrow-up" color="neutral" variant="outline" size="sm" :disabled="index === 0 || ordering" @click="moveCategory(index, -1)" />
|
|
347
|
+
<UButton :label="t('settings.action.moveDown')" icon="i-lucide-arrow-down" color="neutral" variant="outline" size="sm" :disabled="index === draft.length - 1 || ordering" @click="moveCategory(index, 1)" />
|
|
326
348
|
</div>
|
|
327
349
|
<div class="flex gap-1">
|
|
328
350
|
<UButton
|
|
329
|
-
label="
|
|
351
|
+
:label="t('settings.action.goToBoard')"
|
|
330
352
|
icon="i-lucide-arrow-up-right"
|
|
331
353
|
color="neutral"
|
|
332
354
|
variant="ghost"
|
|
@@ -335,7 +357,7 @@ async function runDeleteAll() {
|
|
|
335
357
|
/>
|
|
336
358
|
<UButton
|
|
337
359
|
v-if="!categoryId"
|
|
338
|
-
label="
|
|
360
|
+
:label="t('settings.action.deleteBoard')"
|
|
339
361
|
icon="i-lucide-trash-2"
|
|
340
362
|
color="error"
|
|
341
363
|
variant="ghost"
|
|
@@ -345,7 +367,7 @@ async function runDeleteAll() {
|
|
|
345
367
|
@click="removeCategory(index)"
|
|
346
368
|
/>
|
|
347
369
|
<UButton
|
|
348
|
-
label="
|
|
370
|
+
:label="t('common.action.save')"
|
|
349
371
|
icon="i-lucide-save"
|
|
350
372
|
size="sm"
|
|
351
373
|
:loading="savingId === category.id"
|
|
@@ -357,33 +379,33 @@ async function runDeleteAll() {
|
|
|
357
379
|
</details>
|
|
358
380
|
|
|
359
381
|
<div v-if="!categoryId" class="flex gap-2 border-t border-default pt-4">
|
|
360
|
-
<UInput v-model="newLabel" class="flex-1" placeholder="
|
|
361
|
-
<UButton label="
|
|
382
|
+
<UInput v-model="newLabel" class="flex-1" :placeholder="t('settings.newCategoryPlaceholder')" @keyup.enter="addCategory" />
|
|
383
|
+
<UButton :label="t('common.action.add')" icon="i-lucide-plus" color="neutral" variant="outline" @click="addCategory" />
|
|
362
384
|
</div>
|
|
363
385
|
<UAlert v-if="error" color="error" variant="subtle" :description="error" />
|
|
364
|
-
<UAlert v-if="savedId" color="success" variant="subtle" :description="
|
|
386
|
+
<UAlert v-if="savedId" color="success" variant="subtle" :description="t('settings.savedCategory', { label: draft.find(item => item.id === savedId)?.label })" />
|
|
365
387
|
|
|
366
388
|
<section v-if="deleteAll && !categoryId" class="mt-4 flex flex-col gap-3 border-t border-error/40 pt-6">
|
|
367
389
|
<div>
|
|
368
|
-
<h2 class="font-semibold text-error"
|
|
390
|
+
<h2 class="font-semibold text-error">{{ t('settings.danger.title') }}</h2>
|
|
369
391
|
<p class="mt-1 text-sm text-muted">
|
|
370
|
-
|
|
392
|
+
{{ t('settings.danger.description') }}
|
|
371
393
|
</p>
|
|
372
394
|
</div>
|
|
373
|
-
<UFormField label="
|
|
374
|
-
<UInput v-model="deleteConfirm" autocomplete="off" placeholder="
|
|
395
|
+
<UFormField :label="t('settings.danger.confirmLabel', { phrase: t('settings.danger.phrase') })">
|
|
396
|
+
<UInput v-model="deleteConfirm" autocomplete="off" :placeholder="t('settings.danger.phrase')" />
|
|
375
397
|
</UFormField>
|
|
376
398
|
<div class="flex justify-end">
|
|
377
399
|
<UButton
|
|
378
|
-
label="
|
|
400
|
+
:label="t('settings.danger.button')"
|
|
379
401
|
icon="i-lucide-bomb"
|
|
380
402
|
color="error"
|
|
381
|
-
:disabled="deleteConfirm !== '
|
|
403
|
+
:disabled="deleteConfirm !== t('settings.danger.phrase')"
|
|
382
404
|
:loading="deletingAll"
|
|
383
405
|
@click="runDeleteAll"
|
|
384
406
|
/>
|
|
385
407
|
</div>
|
|
386
|
-
<UAlert v-if="deleteDone" color="success" variant="subtle" description="
|
|
408
|
+
<UAlert v-if="deleteDone" color="success" variant="subtle" :description="t('settings.danger.done')" />
|
|
387
409
|
</section>
|
|
388
410
|
</div>
|
|
389
411
|
</template>
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onBeforeUnmount, ref } from 'vue'
|
|
3
|
+
import { useMemiBoardI18n } from 'memi-board/runtime'
|
|
3
4
|
|
|
4
5
|
const props = defineProps<{
|
|
5
6
|
title?: string | null
|
|
6
7
|
}>()
|
|
7
8
|
|
|
9
|
+
const { t } = useMemiBoardI18n()
|
|
8
10
|
const copied = ref(false)
|
|
9
11
|
let copiedTimer: ReturnType<typeof setTimeout> | undefined
|
|
10
12
|
|
|
@@ -43,7 +45,7 @@ onBeforeUnmount(() => {
|
|
|
43
45
|
size="sm"
|
|
44
46
|
variant="ghost"
|
|
45
47
|
:color="copied ? 'success' : 'neutral'"
|
|
46
|
-
:label="copied ? '
|
|
48
|
+
:label="copied ? t('shareButton.copied') : t('common.action.share')"
|
|
47
49
|
@click="handleShare"
|
|
48
50
|
/>
|
|
49
51
|
</template>
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { imeSafeSubmitClick, imeSafeSubmitPointerDown } from 'memi-board/runtime'
|
|
2
3
|
import { ref, computed } from 'vue'
|
|
3
4
|
import { useMemiBoardConfig } from 'memi-board/runtime'
|
|
4
5
|
import { useMemiBoardAuth } from 'memi-board/runtime'
|
|
6
|
+
import { useMemiBoardI18n } from 'memi-board/runtime'
|
|
5
7
|
|
|
6
8
|
const config = useMemiBoardConfig()
|
|
9
|
+
const { t } = useMemiBoardI18n()
|
|
7
10
|
const providers = computed(() => config.auth?.providers ?? ['google', 'apple'])
|
|
8
11
|
const hasOAuthProvider = computed(() => providers.value.includes('google') || providers.value.includes('apple'))
|
|
9
12
|
|
|
@@ -45,6 +48,7 @@ async function handleApple() {
|
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
async function handleEmailSubmit() {
|
|
51
|
+
if (loading.value) return
|
|
48
52
|
loading.value = true
|
|
49
53
|
error.value = ''
|
|
50
54
|
try {
|
|
@@ -71,7 +75,7 @@ async function handleEmailSubmit() {
|
|
|
71
75
|
icon="i-simple-icons-google"
|
|
72
76
|
color="neutral"
|
|
73
77
|
variant="outline"
|
|
74
|
-
label="
|
|
78
|
+
:label="t('signIn.withGoogle')"
|
|
75
79
|
block
|
|
76
80
|
:loading="loading"
|
|
77
81
|
@click="handleGoogle"
|
|
@@ -82,7 +86,7 @@ async function handleEmailSubmit() {
|
|
|
82
86
|
icon="i-simple-icons-apple"
|
|
83
87
|
color="neutral"
|
|
84
88
|
variant="outline"
|
|
85
|
-
label="
|
|
89
|
+
:label="t('signIn.withApple')"
|
|
86
90
|
block
|
|
87
91
|
:loading="loading"
|
|
88
92
|
@click="handleApple"
|
|
@@ -94,7 +98,7 @@ async function handleEmailSubmit() {
|
|
|
94
98
|
class="flex items-center gap-2 text-xs text-muted"
|
|
95
99
|
>
|
|
96
100
|
<div class="flex-1 border-t border-default" />
|
|
97
|
-
|
|
101
|
+
{{ t('signIn.or') }}
|
|
98
102
|
<div class="flex-1 border-t border-default" />
|
|
99
103
|
</div>
|
|
100
104
|
|
|
@@ -105,25 +109,27 @@ async function handleEmailSubmit() {
|
|
|
105
109
|
<UInput
|
|
106
110
|
v-if="mode === 'signup'"
|
|
107
111
|
v-model="displayName"
|
|
108
|
-
placeholder="
|
|
112
|
+
:placeholder="t('signIn.name')"
|
|
109
113
|
/>
|
|
110
114
|
<UInput
|
|
111
115
|
v-model="email"
|
|
112
116
|
type="email"
|
|
113
|
-
placeholder="
|
|
117
|
+
:placeholder="t('signIn.email')"
|
|
114
118
|
required
|
|
115
119
|
/>
|
|
116
120
|
<UInput
|
|
117
121
|
v-model="password"
|
|
118
122
|
type="password"
|
|
119
|
-
placeholder="
|
|
123
|
+
:placeholder="t('signIn.password')"
|
|
120
124
|
required
|
|
121
125
|
/>
|
|
122
126
|
<UButton
|
|
123
127
|
type="submit"
|
|
128
|
+
@pointerdown="imeSafeSubmitPointerDown"
|
|
129
|
+
@click="imeSafeSubmitClick"
|
|
124
130
|
block
|
|
125
131
|
:loading="loading"
|
|
126
|
-
:label="mode === 'signin' ? '
|
|
132
|
+
:label="mode === 'signin' ? t('common.action.signIn') : t('signIn.signUp')"
|
|
127
133
|
/>
|
|
128
134
|
</form>
|
|
129
135
|
|
|
@@ -131,7 +137,7 @@ async function handleEmailSubmit() {
|
|
|
131
137
|
variant="link"
|
|
132
138
|
size="sm"
|
|
133
139
|
color="neutral"
|
|
134
|
-
:label="mode === 'signin' ? '
|
|
140
|
+
:label="mode === 'signin' ? t('signIn.toSignUp') : t('signIn.toSignIn')"
|
|
135
141
|
@click="mode = mode === 'signin' ? 'signup' : 'signin'"
|
|
136
142
|
/>
|
|
137
143
|
</template>
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* 닫으면 localStorage에 타임스탬프 저장 → 24시간 지나면 다시 표시.
|
|
5
5
|
*/
|
|
6
6
|
import { onMounted, ref } from 'vue'
|
|
7
|
+
import { useMemiBoardI18n } from 'memi-board/runtime'
|
|
7
8
|
|
|
8
9
|
const STORAGE_KEY = 'memi-board-post-swipe-hint-dismissed-at'
|
|
9
10
|
const DAY_MS = 24 * 60 * 60 * 1000
|
|
10
11
|
|
|
12
|
+
const { t } = useMemiBoardI18n()
|
|
11
13
|
const allowed = ref(false)
|
|
12
14
|
|
|
13
15
|
onMounted(() => {
|
|
@@ -61,10 +63,10 @@ function dismiss() {
|
|
|
61
63
|
</div>
|
|
62
64
|
<div class="min-w-0 flex-1 pt-0.5">
|
|
63
65
|
<p class="text-sm font-medium text-highlighted">
|
|
64
|
-
|
|
66
|
+
{{ t('swipeHint.title') }}
|
|
65
67
|
</p>
|
|
66
68
|
<p class="mt-0.5 text-xs leading-relaxed text-muted">
|
|
67
|
-
|
|
69
|
+
{{ t('swipeHint.description') }}
|
|
68
70
|
</p>
|
|
69
71
|
</div>
|
|
70
72
|
<UButton
|
|
@@ -74,7 +76,7 @@ function dismiss() {
|
|
|
74
76
|
size="sm"
|
|
75
77
|
square
|
|
76
78
|
class="shrink-0"
|
|
77
|
-
aria-label="
|
|
79
|
+
:aria-label="t('common.action.close')"
|
|
78
80
|
@click="dismiss"
|
|
79
81
|
/>
|
|
80
82
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, resolveComponent } from 'vue'
|
|
3
|
-
import {
|
|
3
|
+
import { useMemiBoardI18n, useMemiBoardUserComments } from 'memi-board/runtime'
|
|
4
4
|
import type { CommentModel } from 'memi-board/runtime'
|
|
5
5
|
|
|
6
6
|
const NuxtLink = resolveComponent('NuxtLink')
|
|
@@ -20,6 +20,8 @@ const props = withDefaults(defineProps<{
|
|
|
20
20
|
|
|
21
21
|
const emit = defineEmits<{ select: [comment: CommentModel] }>()
|
|
22
22
|
|
|
23
|
+
const { t, formatRelativeDate } = useMemiBoardI18n()
|
|
24
|
+
|
|
23
25
|
const { comments, commentsPending, hasMore, loadingMore, loadError, loadMore } = useMemiBoardUserComments(
|
|
24
26
|
() => props.uid,
|
|
25
27
|
{ pageSize: props.pageSize },
|
|
@@ -46,14 +48,14 @@ function commentLink(comment: CommentModel): string | undefined {
|
|
|
46
48
|
v-else-if="loadError"
|
|
47
49
|
class="text-sm text-error text-center py-8"
|
|
48
50
|
>
|
|
49
|
-
|
|
51
|
+
{{ t('userCommentList.loadFailed', { error: String(loadError) }) }}
|
|
50
52
|
</p>
|
|
51
53
|
|
|
52
54
|
<p
|
|
53
55
|
v-else-if="!comments.length"
|
|
54
56
|
class="text-sm text-muted text-center py-8"
|
|
55
57
|
>
|
|
56
|
-
|
|
58
|
+
{{ t('userCommentList.empty') }}
|
|
57
59
|
</p>
|
|
58
60
|
|
|
59
61
|
<ul
|
|
@@ -75,7 +77,7 @@ function commentLink(comment: CommentModel): string | undefined {
|
|
|
75
77
|
v-if="comment.isBlinded"
|
|
76
78
|
class="text-sm text-muted italic"
|
|
77
79
|
>
|
|
78
|
-
|
|
80
|
+
{{ t('userCommentList.blinded') }}
|
|
79
81
|
</p>
|
|
80
82
|
<p
|
|
81
83
|
v-else
|
|
@@ -86,7 +88,7 @@ function commentLink(comment: CommentModel): string | undefined {
|
|
|
86
88
|
<p class="mt-1 text-xs text-muted">
|
|
87
89
|
{{ formatRelativeDate(comment.createdAt, now) }}
|
|
88
90
|
<template v-if="(comment.likeCount ?? 0) > 0">
|
|
89
|
-
·
|
|
91
|
+
· {{ t('userCommentList.likes', { count: comment.likeCount ?? 0 }) }}
|
|
90
92
|
</template>
|
|
91
93
|
</p>
|
|
92
94
|
</component>
|
|
@@ -100,7 +102,7 @@ function commentLink(comment: CommentModel): string | undefined {
|
|
|
100
102
|
<UButton
|
|
101
103
|
variant="outline"
|
|
102
104
|
color="neutral"
|
|
103
|
-
label="
|
|
105
|
+
:label="t('common.action.more')"
|
|
104
106
|
block
|
|
105
107
|
class="w-full"
|
|
106
108
|
:loading="loadingMore"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref } from 'vue'
|
|
3
|
-
import { useMemiBoardUserPosts } from 'memi-board/runtime'
|
|
3
|
+
import { useMemiBoardI18n, useMemiBoardUserPosts } from 'memi-board/runtime'
|
|
4
4
|
import type { PostModel, UserPostModel } from 'memi-board/runtime'
|
|
5
5
|
import MemiBoardListDefault from './ListDefault.vue'
|
|
6
6
|
|
|
@@ -25,6 +25,8 @@ const props = withDefaults(defineProps<{
|
|
|
25
25
|
|
|
26
26
|
const emit = defineEmits<{ select: [post: UserPostModel] }>()
|
|
27
27
|
|
|
28
|
+
const { t } = useMemiBoardI18n()
|
|
29
|
+
|
|
28
30
|
const { posts, postsPending, hasMore, loadingMore, loadError, loadMore } = useMemiBoardUserPosts(
|
|
29
31
|
() => props.uid,
|
|
30
32
|
{ pageSize: props.pageSize },
|
|
@@ -51,14 +53,14 @@ function postTo(post: PostModel): string | undefined {
|
|
|
51
53
|
v-else-if="loadError"
|
|
52
54
|
class="text-sm text-error text-center py-8"
|
|
53
55
|
>
|
|
54
|
-
|
|
56
|
+
{{ t('userPostList.loadFailed', { error: String(loadError) }) }}
|
|
55
57
|
</p>
|
|
56
58
|
|
|
57
59
|
<p
|
|
58
60
|
v-else-if="!posts.length"
|
|
59
61
|
class="text-sm text-muted text-center py-8"
|
|
60
62
|
>
|
|
61
|
-
|
|
63
|
+
{{ t('userPostList.empty') }}
|
|
62
64
|
</p>
|
|
63
65
|
|
|
64
66
|
<MemiBoardListDefault
|
|
@@ -80,7 +82,7 @@ function postTo(post: PostModel): string | undefined {
|
|
|
80
82
|
<UButton
|
|
81
83
|
variant="outline"
|
|
82
84
|
color="neutral"
|
|
83
|
-
label="
|
|
85
|
+
:label="t('common.action.more')"
|
|
84
86
|
block
|
|
85
87
|
class="w-full"
|
|
86
88
|
:loading="loadingMore"
|