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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pt-B9EH4scA.js","names":[],"sources":["../src/i18n/messages/pt.ts"],"sourcesContent":["import type { MemiBoardMessages } from '../translate'\n\n/** 짧은 화면 문구만 번역한다. 빠진 키는 en → ko 순으로 폴백된다. */\nconst messages: MemiBoardMessages = {\n attachments: {\n attach: 'Anexar arquivos ({count} / {max})',\n maxFiles: 'Você pode anexar até {max} arquivos.',\n },\n auth: {\n writeRestricted: 'Publicar e comentar está temporariamente restrito por causa de avisos de conteúdo acumulados.',\n strikeSuffix: ' (Aviso {count}/{threshold})',\n strikeRestricted: ' Você atingiu {count} avisos, então publicar e comentar está restrito.',\n notInitialized: '[memi-board] O Firebase Auth não foi inicializado.',\n },\n authorMemo: {\n tooLong: 'As notas podem ter até {max} caracteres.',\n },\n authorMenu: {\n viewProfile: 'Ver perfil',\n viewPosts: 'Ver publicações',\n viewComments: 'Ver comentários',\n memoAriaLabel: 'Nota sobre este autor',\n memoTitle: 'Nota privada',\n memoPlaceholder: 'Anote algo sobre este autor…',\n bad: 'Ruim',\n good: 'Bom',\n },\n avatarCropModal: {\n title: 'Recortar foto do perfil',\n description: 'Exibida em formato quadrado (1:1). Ajuste a área.',\n confirm: 'Recortar e enviar',\n },\n boardSettings: {\n signInRequired: 'Faça login.',\n idAndNameRequired: 'Informe um ID e um nome para o fórum.',\n deleteIdRequired: 'É necessário o ID do fórum para excluí-lo.',\n nameRequired: 'Informe um nome para o fórum.',\n },\n boardUsers: {\n unsupportedRole: 'Função não suportada.',\n roleDescription: {\n admin: 'Gerencia as funções dos usuários, as configurações do fórum e todo o conteúdo.',\n user: 'Pode escrever publicações e comentários, e editar ou excluir apenas o próprio conteúdo.',\n },\n },\n commentFeed: {\n empty: 'Ainda não há comentários.',\n emptyLiked: 'Ainda não há comentários curtidos.',\n loadFailed: 'Não foi possível carregar a lista: {error}',\n blinded: 'Este comentário foi ocultado por um administrador.',\n },\n commentForm: {\n hiddenBoardDenied: 'Somente a equipe responsável pode comentar em fóruns ocultos.',\n denied: 'Você não tem permissão para comentar neste fórum.',\n signInRequired: 'Faça login.',\n writeRestricted: 'Publicar está temporariamente restrito por causa de avisos de conteúdo acumulados.',\n moderationBlocked: 'Há conteúdo que não pode ser publicado.',\n replyTo: 'Responder a {name}',\n placeholder: 'Escreva um comentário',\n submitReply: 'Responder',\n submit: 'Comentar',\n signInToComment: 'Faça login para deixar um comentário.',\n },\n commentItem: {\n editRestricted: 'A edição de comentários está temporariamente restrita.',\n moderationBlocked: 'Há conteúdo que não pode ser publicado.',\n editFailed: 'Não foi possível atualizar o comentário.',\n blindFailed: 'Não foi possível alterar o estado de ocultação.',\n likeFailed: 'Não foi possível processar a curtida.',\n blinded: 'Este comentário foi ocultado por um administrador.',\n edit: 'Editar comentário',\n blind: 'Ocultar comentário',\n unblind: 'Reexibir comentário',\n delete: 'Excluir comentário',\n },\n commentList: {\n loading: 'Carregando comentários',\n empty: 'Ainda não há comentários.',\n loadMore: 'Mais comentários',\n loadingMore: 'Carregando mais comentários',\n },\n commentThread: {\n deleteConfirm: 'Excluir este comentário?',\n deleteFailed: 'Não foi possível excluir o comentário.',\n hideReplies: 'Ocultar respostas',\n showReplies: '{count} respostas',\n loadMoreReplies: 'Mais respostas',\n },\n comments: {\n bodyRequired: 'Escreva um comentário.',\n bodyTooLong: 'Os comentários podem ter até {max} caracteres.',\n replyTargetNotFound: 'O comentário que você está respondendo não foi encontrado.',\n threadNotFound: 'A conversa de comentários não foi encontrada.',\n hasRepliesCannotDelete: 'No momento, não é possível excluir comentários com respostas.',\n },\n common: {\n action: {\n cancel: 'Cancelar',\n save: 'Salvar',\n saved: 'Salvo',\n edit: 'Editar',\n delete: 'Excluir',\n confirm: 'OK',\n close: 'Fechar',\n add: 'Adicionar',\n more: 'Ver mais',\n prev: 'Anterior',\n next: 'Próximo',\n retry: 'Tentar novamente',\n reply: 'Responder',\n like: 'Curtir',\n unlike: 'Descurtir',\n share: 'Compartilhar',\n report: 'Denunciar',\n signIn: 'Entrar',\n signOut: 'Sair',\n write: 'Escrever',\n toTop: 'Topo',\n },\n label: {\n anonymous: 'Anônimo',\n user: 'Usuário',\n image: 'Imagem',\n draft: 'Rascunho',\n loading: 'Carregando…',\n },\n role: {\n admin: 'Administrador',\n staff: 'Equipe',\n user: 'Usuário',\n },\n date: {\n justNow: 'Agora mesmo',\n fewSecondsAgo: 'Há alguns segundos',\n checking: 'Verificando horário',\n createdAt: 'Criado: {time}',\n updatedAt: 'Atualizado: {time}',\n },\n },\n detail: {\n deleteConfirm: 'Excluir esta publicação? Isso não pode ser desfeito.',\n notFound: 'Publicação não encontrada.',\n draftPreview: 'Prévia não publicada',\n draftNotice: 'Esta publicação ainda não foi publicada. Só você pode vê-la até publicá-la.',\n publish: 'Publicar',\n backToList: 'Voltar à lista',\n postNav: 'Navegação entre publicações',\n list: 'Lista',\n comments: 'Comentários',\n },\n editor: {\n titlePlaceholder: 'Título',\n contentPlaceholder: 'Escreva algo… Digite / para abrir o menu de blocos.',\n imageContentPlaceholder: 'Escreva algo…',\n tagsPlaceholder: 'Digite uma tag e pressione Enter',\n skipModeration: 'Ignorar filtro de palavrões (admin)',\n submitEdit: 'Salvar alterações',\n submitNext: 'Próximo: prévia',\n apply: 'Aplicar',\n imageOptimizing: 'Otimizando e enviando a imagem…',\n cover: {\n imageLink: 'Link da imagem',\n addPhoto: 'Adicionar fotos',\n alt: 'Foto {n}',\n representative: 'Capa',\n setRepresentative: 'Usar como capa',\n setRepresentativeAria: 'Usar a foto {n} como capa',\n remove: 'Remover foto',\n add: 'Adicionar',\n uploading: 'Enviando…',\n dropOrClick: 'Solte fotos ou clique para adicionar',\n emptyHint: 'Selecione várias · Escolha a capa após o envio · Arraste para reordenar',\n uploadTitle: 'Enviar',\n },\n uploadFailed: 'Falha no envio',\n aiAssistant: 'Assistente de escrita com AI',\n insertBlock: 'Inserir bloco',\n blockMenu: 'Menu de blocos',\n imageDialog: {\n title: 'Adicionar imagem',\n chooseFile: 'Escolher imagem do dispositivo',\n orLink: 'ou link da imagem',\n urlLabel: 'URL da imagem',\n previewAlt: 'Prévia da imagem externa',\n addLink: 'Adicionar link',\n },\n customDialog: {\n title: 'Reescrever com um estilo personalizado',\n label: 'Como você quer reescrever?',\n help: 'ex.: em tom descontraído / em estilo formal / em tópicos concisos',\n placeholder: 'Em tom descontraído',\n },\n aiPreview: {\n title: 'Revisar resultado da AI',\n currentTitle: 'Título atual',\n newTitle: 'Novo título',\n currentContent: 'Conteúdo atual',\n appendedContent: 'Conteúdo a adicionar',\n before: 'Antes',\n after: 'Depois',\n },\n menu: {\n ai: 'AI',\n aiContinue: 'Continuar com AI',\n style: 'Estilo',\n insert: 'Inserir',\n paragraph: 'Parágrafo',\n heading1: 'Título 1',\n heading2: 'Título 2',\n heading3: 'Título 3',\n bulletList: 'Lista com marcadores',\n orderedList: 'Lista numerada',\n blockquote: 'Citação',\n codeBlock: 'Bloco de código',\n horizontalRule: 'Divisor',\n image: 'Imagem',\n turnInto: 'Transformar em',\n resetFormatting: 'Redefinir formatação',\n duplicate: 'Duplicar',\n moveUp: 'Mover para cima',\n moveDown: 'Mover para baixo',\n delete: 'Excluir',\n },\n toolbar: {\n link: 'Link',\n image: 'Imagem',\n format: 'Formatar',\n bold: 'Negrito',\n italic: 'Itálico',\n underline: 'Sublinhado',\n strike: 'Tachado',\n title: 'Título',\n heading: 'Cabeçalho',\n subheading: 'Subtítulo',\n body: 'Corpo',\n mono: 'Monoespaçado',\n bulletList: 'Lista com marcadores',\n orderedList: 'Lista numerada',\n blockquote: 'Citação em bloco',\n codeBlock: 'Bloco de código',\n horizontalRule: 'Divisor',\n clearFormatting: 'Limpar formatação',\n undo: 'Desfazer',\n redo: 'Refazer',\n },\n ai: {\n proofread: 'Verificar ortografia',\n polish: 'Aprimorar texto',\n continue: 'Continuar escrevendo',\n summarize: 'Resumir brevemente',\n translateEn: 'Traduzir para inglês',\n translateKo: 'Traduzir para coreano',\n title: 'Gerar título a partir do texto',\n custom: 'Reescrever com um estilo personalizado…',\n failed: 'A tarefa de AI falhou. Tente novamente.',\n },\n youtubePrompt: 'Insira um link do YouTube.',\n errors: {\n coverMax: 'Você pode enviar até {max} fotos.',\n imageUrlRequired: 'Insira uma URL de imagem.',\n imageUrlInvalid: 'Insira uma URL de imagem válida.',\n imageUrlHttps: 'Somente URLs de imagem HTTPS são permitidas.',\n imageUrlCredentials: 'URLs com dados de login não são permitidas.',\n imageTimeout: 'A imagem está demorando demais para carregar.',\n imageNotDisplayable: 'Não é possível exibir esta imagem.',\n editorNotReady: 'O editor não está pronto. Tente novamente.',\n imageLinkFailed: 'Não foi possível adicionar o link da imagem.',\n imageOnly: 'Somente arquivos de imagem podem ser enviados.',\n imageTooLarge: 'As imagens originais devem ter {mb}MB ou menos.',\n imageUploadFailed: 'Falha ao enviar a imagem.',\n pasteAsFile: 'Cole ou arraste as fotos como arquivos na área acima.',\n postNotFound: 'Publicação não encontrada.',\n loadPermission: 'Você não tem permissão para carregar esta publicação. Verifique se você fez login.',\n loadFailed: 'Não foi possível carregar a publicação: {message}',\n titleRequired: 'Insira um título.',\n photoRequired: 'Envie pelo menos uma foto.',\n bodyRequired: 'Escreva algum texto ou anexe uma imagem.',\n signInRequired: 'É necessário fazer login.',\n boardMissing: 'Nenhum fórum foi especificado.',\n writeRestricted: 'Publicar e comentar está temporariamente restrito por causa de avisos de conteúdo acumulados.',\n moderationBlocked: 'Há conteúdo que não pode ser publicado.',\n },\n hint: {\n reviewing: 'Revisando o conteúdo…',\n saving: 'Salvando…',\n },\n },\n footer: {\n info: {\n title: 'Informações do fórum',\n about: 'Sobre este fórum',\n history: 'Histórico de versões',\n },\n stack: {\n ui: 'Interface e experiência',\n data: 'Dados e login',\n writing: 'Escrita e praticidade',\n },\n },\n likes: {\n signInRequired: 'Faça login.',\n commentNotFound: 'Comentário não encontrado.',\n },\n list: {\n settings: 'Configurações',\n loadFailed: 'Não foi possível carregar a lista: {error}',\n empty: 'Ainda não há publicações.',\n photo: 'Foto',\n likeCount: 'Curtidas {count}',\n info: {\n title: 'Informações do fórum',\n about: 'Sobre este fórum',\n history: 'Histórico de versões',\n },\n stack: {\n framework: 'Framework e linguagem',\n ui: 'UI e estilos',\n backend: 'Backend e dados',\n },\n tech: {\n nuxt: 'Framework SSR baseado em Vue',\n vue: 'Composition API · script setup',\n typescript: 'Dados e componentes com tipagem segura',\n nuxtUi: 'UI de fórum, formulários e modais',\n tailwind: 'Estilos responsivos · claro/escuro',\n lucide: 'Ícones leves e consistentes',\n dayjs: 'Datas legíveis e tempos relativos',\n firestore: 'Banco de dados em tempo real para publicações e comentários',\n auth: 'Login e permissões seguros',\n storage: 'Armazenamento de imagens e anexos',\n vuefire: 'Conexão em tempo real entre Nuxt e Firebase',\n },\n },\n listViewSwitch: {\n default: 'Padrão',\n dense: 'Compacta',\n video: 'Vídeo',\n },\n miniComments: {\n empty: 'Ainda não há comentários.',\n blinded: 'Este comentário foi ocultado por um administrador.',\n },\n miniList: {\n empty: 'Ainda não há publicações.',\n },\n miniPicker: {\n selectedCount: 'Selecionadas {count} / {max}',\n layoutHint: '2 ou mais aparecem em carrossel; 4, em grade',\n searchPlaceholder: 'Buscar títulos',\n loading: 'Carregando publicações…',\n noMore: 'Não há mais publicações para escolher.',\n },\n miniPost: {\n empty: 'Nenhuma publicação selecionada.',\n },\n moderation: {\n writeRestricted: 'Publicar e comentar está temporariamente restrito por causa de avisos de conteúdo acumulados.',\n },\n moderationPrompt: {\n localHit: 'Expressões como “{hit}” não são permitidas. Reescreva o texto.',\n localGeneric: 'Há palavrões no texto, então não é possível publicar. Reescreva o texto.',\n genericBlocked: 'Há conteúdo que não pode ser publicado. Edite o texto.',\n categories: {\n abuse: 'palavrões ou insultos',\n spam: 'spam ou publicidade',\n adult: 'conteúdo sexual ou adulto',\n violence: 'violência ou ameaças',\n other: 'conteúdo impróprio',\n },\n },\n moderationStrike: {\n liftSoon: 'liberado em breve',\n liftInHours: 'liberado em cerca de {hours} h',\n liftInDays: 'liberado em cerca de {days} d',\n liftInDaysHours: 'liberado em cerca de {days} d {hours} h',\n },\n posts: {\n boardIdRequired: '[memi-board] boardId é obrigatório.',\n photoRequired: 'Adicione uma foto.',\n bodyRequired: 'Escreva algum texto ou anexe uma imagem.',\n titleRequired: 'Insira um título.',\n },\n profile: {\n notFound: 'Usuário não encontrado.',\n noName: 'Sem nome',\n joined: 'Entrou {time}',\n lastVisit: 'Visto por último {time}',\n },\n profileEditModal: {\n imageOnly: 'Somente arquivos de imagem podem ser selecionados.',\n nameRequired: 'Insira um nome.',\n saveFailed: 'Não foi possível salvar o perfil.',\n title: 'Editar perfil',\n changePhoto: 'Alterar foto',\n name: 'Nome',\n bio: 'Bio',\n bioPlaceholder: 'Conte um pouco sobre você',\n },\n reportButton: {\n failed: 'Não foi possível enviar a denúncia.',\n reported: 'Denunciado',\n title: 'Denunciar publicação',\n done: 'Denúncia recebida. A equipe vai analisá-la.',\n reason: 'Motivo',\n detailPlaceholder: 'Detalhes (opcional)',\n submit: 'Denunciar',\n },\n reportList: {\n staffOnly: 'Somente a equipe pode ver as denúncias.',\n loadFailed: 'Não foi possível carregar a lista: {error}',\n empty: 'Nenhuma denúncia pendente.',\n untitled: 'Publicação sem título',\n author: 'por {name}',\n dismiss: 'Descartar',\n confirm: 'Resolver',\n },\n reports: {\n reason: {\n spam: 'Spam / publicidade',\n abuse: 'Ofensa / ódio / assédio',\n adult: 'Conteúdo sexual',\n illegal: 'Conteúdo ilegal',\n other: 'Outro',\n },\n fallbackLabel: 'Denúncia',\n signInRequired: 'Faça login.',\n cannotReportOwn: 'Você não pode denunciar sua própria publicação.',\n reasonRequired: 'Selecione um motivo.',\n detailTooLong: 'Os detalhes podem ter até {max} caracteres.',\n },\n sectionUtil: {\n kind: {\n list: 'Lista de publicações',\n post: 'Publicações',\n comments: 'Comentários',\n },\n sort: {\n latest: 'Mais recentes',\n likes: 'Mais curtidas',\n },\n untitledPost: 'Publicação',\n view: {\n media: 'Mídia',\n list: 'Lista',\n },\n },\n sections: {\n defaultTitle: 'Seção {n}',\n },\n seo: {\n allCategory: 'Tudo',\n boardDescription: 'Fórum {name}',\n allDescription: 'Fórum: avisos e novidades da comunidade.',\n postFallbackTitle: 'Publicação | {site}',\n postFallbackDescription: 'Fórum',\n photo: 'Foto',\n postDescriptionFallback: 'Publicação do fórum',\n listTitle: '{name} | {site}',\n listTitleAll: 'Fórum | {site}',\n listDescription: '{name}: confira as publicações mais recentes.',\n listDescriptionAll: 'Fórum: avisos e histórias.',\n },\n settings: {\n loading: 'Carregando configurações…',\n noAccess: 'Você não tem acesso',\n notFound: {\n title: 'Fórum não encontrado',\n checkAddress: 'Verifique o endereço.',\n checkBoardAddress: 'Verifique o endereço do fórum.',\n },\n newBoard: {\n title: 'Este fórum ainda não existe',\n description: 'Preencha os dados abaixo e salve para criá-lo com este ID.',\n },\n noLabel: 'Sem rótulo',\n option: {\n default: 'Padrão',\n },\n id: {\n label: 'ID da categoria',\n help: 'Usado na URL e nos dados',\n },\n writeRole: {\n label: 'Quem pode publicar',\n help: 'Função mínima necessária para publicar nesta categoria',\n helpShort: 'Função mínima necessária para publicar',\n options: {\n user: 'Usuários ou acima',\n staff: 'Equipe ou acima',\n admin: 'Somente administradores',\n },\n },\n commentWriteRole: {\n label: 'Quem pode comentar',\n help: 'Função mínima necessária para comentar nesta categoria',\n helpShort: 'Função mínima necessária para comentar',\n hiddenFixed: 'Somente a equipe responsável (fixo em fóruns ocultos)',\n },\n staff: {\n label: 'Equipe responsável',\n help: 'Somente a equipe selecionada pode configurar e gerenciar este fórum. Deixe vazio para apenas administradores',\n placeholder: 'Somente administradores (sem equipe definida)',\n },\n label: {\n label: 'Nome de exibição',\n help: 'O nome que os usuários veem',\n },\n description: {\n label: 'Descrição',\n help: 'Exibida no topo do fórum e em outros lugares',\n placeholder: 'Uma breve descrição deste fórum (opcional)',\n },\n visibility: {\n label: 'Visibilidade',\n options: {\n public: {\n label: 'Visível',\n description: 'Exibido na lista completa e nos filtros',\n },\n hidden: {\n label: 'Oculto',\n description: 'Diário ou privado. Somente a equipe responsável pode comentar',\n },\n },\n },\n listView: {\n label: 'Visualização da lista',\n help: 'Como a lista de publicações é exibida',\n options: {\n dense: 'Compacta',\n video: 'Vídeo',\n },\n },\n editorType: {\n label: 'Formulário de publicação',\n help: 'Como as publicações são escritas',\n options: {\n default: 'Escrever um título e um texto',\n image: 'Adicionar várias fotos e uma foto de capa',\n },\n },\n locale: {\n label: 'Idioma do fórum',\n auto: 'Automático (seguir o idioma do site)',\n },\n action: {\n moveDown: 'Para baixo',\n goToBoard: 'Ir para o fórum',\n deleteBoard: 'Excluir fórum',\n create: 'Criar',\n },\n newCategoryPlaceholder: 'Nome da nova categoria',\n savedCategory: 'Categoria “{label}” salva.',\n savedSettings: 'Configurações do fórum salvas.',\n created: 'Fórum criado.',\n error: {\n orderFailed: 'Não foi possível salvar a ordem das categorias.',\n deleteFailed: 'Não foi possível excluir o fórum e seus dados.',\n saveFailed: 'Não foi possível salvar as configurações do fórum.',\n deleteAllFailed: 'Não foi possível excluir os dados do fórum.',\n },\n danger: {\n title: 'Zona de perigo',\n confirmLabel: 'Digite “{phrase}” para confirmar',\n phrase: 'excluir dados do fórum',\n button: 'Excluir todos os dados',\n done: 'Todos os dados do fórum foram excluídos.',\n },\n },\n shareButton: {\n copied: 'Link copiado',\n },\n signIn: {\n withGoogle: 'Entrar com o Google',\n withApple: 'Entrar com a Apple',\n or: 'ou',\n name: 'Nome',\n email: 'E-mail',\n password: 'Senha',\n signUp: 'Cadastrar-se',\n toSignUp: 'Não tem uma conta? Cadastre-se',\n toSignIn: 'Já tem uma conta? Entrar',\n },\n storage: {\n sourceTooLarge: 'A imagem original deve ter {max}MB ou menos.',\n imageOnly: 'Somente arquivos de imagem podem ser enviados.',\n canvasUnavailable: 'Canvas não está disponível',\n compressFailed: 'Falha na compressão',\n loadFailed: 'Falha ao carregar a imagem',\n },\n swipeHint: {\n title: 'Experimente deslizar para os lados',\n description: 'Deslize para a esquerda para ver a próxima publicação e para a direita para ver a anterior.',\n },\n userCommentList: {\n loadFailed: 'Não foi possível carregar a lista: {error}',\n empty: 'Ainda não há comentários.',\n blinded: 'Este comentário foi ocultado por um administrador.',\n likes: '{count} curtidas',\n },\n userPostList: {\n loadFailed: 'Não foi possível carregar a lista: {error}',\n empty: 'Ainda não há publicações.',\n },\n users: {\n changeRoleFailed: 'Não foi possível alterar a função.',\n loading: 'Carregando usuários…',\n noPermission: {\n title: 'Sem permissão',\n description: 'Somente administradores do fórum podem gerenciar as funções dos usuários.',\n },\n empty: 'Nenhum usuário fez login no fórum ainda.',\n noName: 'Sem nome',\n moderationWarnings: '{count} avisos de conteúdo',\n },\n writingAssistant: {\n loginRequired: 'Faça login para usar o assistente de escrita com AI.',\n userMissing: 'Não foi possível verificar suas informações de usuário. Tente novamente.',\n contentRequired: 'Primeiro, escreva algum conteúdo para a AI trabalhar.',\n customRequired: 'Escreva uma instrução de como reescrever.',\n emptyResult: 'A AI não conseguiu gerar um resultado. Tente novamente.',\n failed: 'A tarefa de AI falhou. Tente novamente.',\n actions: {\n proofread: 'Verificar ortografia',\n polish: 'Aprimorar frases',\n continue: 'Continuar escrevendo',\n summarize: 'Resumir',\n translateEn: 'Traduzir para inglês',\n translateKo: 'Traduzir para coreano',\n title: 'Gerar título a partir do texto',\n custom: 'Reescrever com um estilo personalizado…',\n },\n },\n}\n\nexport default messages\n"],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import type { Attachment } from 'memi-board/runtime'
|
|
4
4
|
import { useMemiBoardStorage } from 'memi-board/storage'
|
|
5
|
+
import { useMemiBoardI18n } from 'memi-board/runtime'
|
|
5
6
|
|
|
6
7
|
const props = withDefaults(defineProps<{
|
|
7
8
|
modelValue: Attachment[]
|
|
@@ -17,6 +18,7 @@ const props = withDefaults(defineProps<{
|
|
|
17
18
|
const emit = defineEmits<{ 'update:modelValue': [attachments: Attachment[]] }>()
|
|
18
19
|
|
|
19
20
|
const { uploadAttachment, deleteAttachment } = useMemiBoardStorage()
|
|
21
|
+
const { t } = useMemiBoardI18n()
|
|
20
22
|
|
|
21
23
|
const fileInput = ref<HTMLInputElement | null>(null)
|
|
22
24
|
const uploading = ref(false)
|
|
@@ -145,7 +147,7 @@ function formatSize(bytes: number): string {
|
|
|
145
147
|
>
|
|
146
148
|
<UButton
|
|
147
149
|
icon="i-lucide-paperclip"
|
|
148
|
-
:label="
|
|
150
|
+
:label="t('attachments.attach', { count: modelValue.length, max: maxFiles })"
|
|
149
151
|
variant="outline"
|
|
150
152
|
color="neutral"
|
|
151
153
|
size="sm"
|
|
@@ -167,7 +169,7 @@ function formatSize(bytes: number): string {
|
|
|
167
169
|
v-else-if="editable"
|
|
168
170
|
class="text-xs text-muted"
|
|
169
171
|
>
|
|
170
|
-
|
|
172
|
+
{{ t('attachments.maxFiles', { max: maxFiles }) }}
|
|
171
173
|
</p>
|
|
172
174
|
</div>
|
|
173
175
|
</template>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, watch } from 'vue'
|
|
3
|
-
import { AUTHOR_MEMO_MAX_LENGTH, useMemiBoardAuth, useMemiBoardAuthorMemo } from 'memi-board/runtime'
|
|
3
|
+
import { AUTHOR_MEMO_MAX_LENGTH, useMemiBoardAuth, useMemiBoardAuthorMemo, useMemiBoardI18n } from 'memi-board/runtime'
|
|
4
4
|
|
|
5
5
|
const props = withDefaults(defineProps<{
|
|
6
6
|
authorUid: string
|
|
@@ -20,14 +20,16 @@ const props = withDefaults(defineProps<{
|
|
|
20
20
|
truncate: false,
|
|
21
21
|
})
|
|
22
22
|
|
|
23
|
+
const { t } = useMemiBoardI18n()
|
|
24
|
+
|
|
23
25
|
const authorProfileLink = computed(() => props.authorProfileTo?.(props.authorUid))
|
|
24
26
|
const authorPostsLink = computed(() => props.authorPostsTo?.(props.authorUid))
|
|
25
27
|
const authorCommentsLink = computed(() => props.authorCommentsTo?.(props.authorUid))
|
|
26
28
|
|
|
27
29
|
const items = computed(() => [[
|
|
28
|
-
{ label: '
|
|
29
|
-
{ label: '
|
|
30
|
-
{ label: '
|
|
30
|
+
{ label: t('authorMenu.viewProfile'), icon: 'i-lucide-user-round', to: authorProfileLink.value, disabled: !authorProfileLink.value },
|
|
31
|
+
{ label: t('authorMenu.viewPosts'), icon: 'i-lucide-notebook-text', to: authorPostsLink.value, disabled: !authorPostsLink.value },
|
|
32
|
+
{ label: t('authorMenu.viewComments'), icon: 'i-lucide-message-square-text', to: authorCommentsLink.value, disabled: !authorCommentsLink.value },
|
|
31
33
|
]])
|
|
32
34
|
|
|
33
35
|
// 다른 작성자에 대해 나만 보는 메모 — 로그인 안 했거나 내 카드면 아이콘 자체를 숨긴다.
|
|
@@ -74,8 +76,8 @@ async function onDeleteMemo() {
|
|
|
74
76
|
>
|
|
75
77
|
<UDropdownMenu :items="items" :content="{ align: 'start' }">
|
|
76
78
|
<UButton
|
|
77
|
-
:avatar="showAvatar ? { src: authorPhoto ?? undefined, alt: authorName ?? '
|
|
78
|
-
:label="authorName ?? '
|
|
79
|
+
:avatar="showAvatar ? { src: authorPhoto ?? undefined, alt: authorName ?? t('common.label.anonymous') } : undefined"
|
|
80
|
+
:label="authorName ?? t('common.label.anonymous')"
|
|
79
81
|
variant="ghost"
|
|
80
82
|
color="neutral"
|
|
81
83
|
size="xs"
|
|
@@ -97,20 +99,20 @@ async function onDeleteMemo() {
|
|
|
97
99
|
type="button"
|
|
98
100
|
class="shrink-0 rounded-md p-1 transition-colors hover:bg-elevated/60"
|
|
99
101
|
:class="memoSentiment === 'good' ? 'text-primary' : memoSentiment === 'bad' ? 'text-error' : 'text-muted'"
|
|
100
|
-
aria-label="
|
|
102
|
+
:aria-label="t('authorMenu.memoAriaLabel')"
|
|
101
103
|
>
|
|
102
104
|
<UIcon name="i-lucide-notebook-pen" class="size-3.5" />
|
|
103
105
|
</button>
|
|
104
106
|
<template #content>
|
|
105
107
|
<div class="flex flex-col gap-2 p-3">
|
|
106
108
|
<p class="text-xs text-muted">
|
|
107
|
-
|
|
109
|
+
{{ t('authorMenu.memoTitle') }}
|
|
108
110
|
</p>
|
|
109
111
|
<UTextarea
|
|
110
112
|
v-model="memoText"
|
|
111
113
|
:rows="3"
|
|
112
114
|
:maxlength="AUTHOR_MEMO_MAX_LENGTH"
|
|
113
|
-
placeholder="
|
|
115
|
+
:placeholder="t('authorMenu.memoPlaceholder')"
|
|
114
116
|
class="w-full"
|
|
115
117
|
/>
|
|
116
118
|
<div class="flex items-center justify-between gap-2">
|
|
@@ -124,7 +126,7 @@ async function onDeleteMemo() {
|
|
|
124
126
|
<UButton
|
|
125
127
|
v-if="memoText || memoSentiment"
|
|
126
128
|
size="xs"
|
|
127
|
-
label="
|
|
129
|
+
:label="t('common.action.delete')"
|
|
128
130
|
color="neutral"
|
|
129
131
|
variant="ghost"
|
|
130
132
|
:disabled="memoSaving"
|
|
@@ -132,7 +134,7 @@ async function onDeleteMemo() {
|
|
|
132
134
|
/>
|
|
133
135
|
<UButton
|
|
134
136
|
size="xs"
|
|
135
|
-
label="
|
|
137
|
+
:label="t('authorMenu.bad')"
|
|
136
138
|
color="error"
|
|
137
139
|
:loading="memoPendingSentiment === 'bad'"
|
|
138
140
|
:disabled="memoSaving"
|
|
@@ -140,7 +142,7 @@ async function onDeleteMemo() {
|
|
|
140
142
|
/>
|
|
141
143
|
<UButton
|
|
142
144
|
size="xs"
|
|
143
|
-
label="
|
|
145
|
+
:label="t('authorMenu.good')"
|
|
144
146
|
color="primary"
|
|
145
147
|
:loading="memoPendingSentiment === 'good'"
|
|
146
148
|
:disabled="memoSaving"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import { Cropper } from 'vue-advanced-cropper'
|
|
4
4
|
import 'vue-advanced-cropper/dist/style.css'
|
|
5
|
+
import { useMemiBoardI18n } from 'memi-board/runtime'
|
|
5
6
|
|
|
6
7
|
const open = defineModel<boolean>('open', { default: false })
|
|
7
8
|
|
|
@@ -21,6 +22,8 @@ const emit = defineEmits<{
|
|
|
21
22
|
cancel: []
|
|
22
23
|
}>()
|
|
23
24
|
|
|
25
|
+
const { t } = useMemiBoardI18n()
|
|
26
|
+
|
|
24
27
|
const cropperRef = ref<InstanceType<typeof Cropper> | null>(null)
|
|
25
28
|
|
|
26
29
|
function close() {
|
|
@@ -56,8 +59,8 @@ async function confirm() {
|
|
|
56
59
|
<template>
|
|
57
60
|
<UModal
|
|
58
61
|
v-model:open="open"
|
|
59
|
-
title="
|
|
60
|
-
description="
|
|
62
|
+
:title="t('avatarCropModal.title')"
|
|
63
|
+
:description="t('avatarCropModal.description')"
|
|
61
64
|
:ui="{ content: 'sm:max-w-lg' }"
|
|
62
65
|
>
|
|
63
66
|
<template #body>
|
|
@@ -74,7 +77,7 @@ async function confirm() {
|
|
|
74
77
|
</div>
|
|
75
78
|
</ClientOnly>
|
|
76
79
|
<p class="mt-3 text-xs text-muted">
|
|
77
|
-
|
|
80
|
+
{{ t('avatarCropModal.hint', { size: outputSize }) }}
|
|
78
81
|
</p>
|
|
79
82
|
</template>
|
|
80
83
|
|
|
@@ -86,14 +89,14 @@ async function confirm() {
|
|
|
86
89
|
:disabled="loading"
|
|
87
90
|
@click="close"
|
|
88
91
|
>
|
|
89
|
-
|
|
92
|
+
{{ t('common.action.cancel') }}
|
|
90
93
|
</UButton>
|
|
91
94
|
<UButton
|
|
92
95
|
icon="i-lucide-check"
|
|
93
96
|
:loading="loading"
|
|
94
97
|
@click="confirm"
|
|
95
98
|
>
|
|
96
|
-
|
|
99
|
+
{{ t('avatarCropModal.confirm') }}
|
|
97
100
|
</UButton>
|
|
98
101
|
</div>
|
|
99
102
|
</template>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, ref, watch } from 'vue'
|
|
3
|
-
import { useMemiBoardAuth, useMemiBoardSettings, useMemiBoardUsers } from 'memi-board/runtime'
|
|
3
|
+
import { MEMI_BOARD_LOCALES, 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
|
|
|
@@ -23,25 +23,35 @@ const props = withDefaults(defineProps<{
|
|
|
23
23
|
|
|
24
24
|
const emit = defineEmits<{ saved: [category: BoardCategory] }>()
|
|
25
25
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
{ label: '
|
|
29
|
-
{ label: '
|
|
30
|
-
{ label: '
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{ label: '
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{ label: '
|
|
39
|
-
{ label: '
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{ label: '
|
|
44
|
-
|
|
26
|
+
const { t } = useMemiBoardI18n()
|
|
27
|
+
const listViewOptions = computed<Array<{ label: string, value: BoardListView }>>(() => [
|
|
28
|
+
{ label: t('settings.option.default'), value: 'default' },
|
|
29
|
+
{ label: t('settings.listView.options.dense'), value: 'dense' },
|
|
30
|
+
{ label: t('common.label.image'), value: 'image' },
|
|
31
|
+
{ label: t('settings.listView.options.video'), value: 'video' },
|
|
32
|
+
])
|
|
33
|
+
const editorTypeOptions = computed<Array<{ label: string, value: BoardEditorType, description: string }>>(() => [
|
|
34
|
+
{ label: t('settings.option.default'), value: 'default', description: t('settings.editorType.options.default') },
|
|
35
|
+
{ label: t('common.label.image'), value: 'image', description: t('settings.editorType.options.image') },
|
|
36
|
+
])
|
|
37
|
+
const writeRoleOptions = computed<Array<{ label: string, value: BoardWriteRole }>>(() => [
|
|
38
|
+
{ label: t('settings.writeRole.options.user'), value: 'user' },
|
|
39
|
+
{ label: t('settings.writeRole.options.staff'), value: 'staff' },
|
|
40
|
+
{ label: t('settings.writeRole.options.admin'), value: 'admin' },
|
|
41
|
+
])
|
|
42
|
+
const visibilityOptions = computed<Array<{ label: string, value: BoardVisibility, description?: string }>>(() => [
|
|
43
|
+
{ label: t('settings.visibility.options.public.label'), value: 'public', description: t('settings.visibility.options.public.description') },
|
|
44
|
+
{ label: t('settings.visibility.options.hidden.label'), value: 'hidden', description: t('settings.visibility.options.hidden.description') },
|
|
45
|
+
])
|
|
46
|
+
// reka-ui Select 는 빈 문자열 값을 허용하지 않아 '자동'을 sentinel 로 두고 저장 시 '' 로 바꾼다.
|
|
47
|
+
const LOCALE_AUTO = 'auto'
|
|
48
|
+
const localeOptions = computed(() => [
|
|
49
|
+
{ label: t('settings.locale.auto'), value: LOCALE_AUTO },
|
|
50
|
+
...MEMI_BOARD_LOCALES.map((item: { code: string, name: string }) => ({ label: item.name, value: item.code })),
|
|
51
|
+
])
|
|
52
|
+
function fromLocaleOption(value: string) {
|
|
53
|
+
return value === LOCALE_AUTO ? '' : value
|
|
54
|
+
}
|
|
45
55
|
|
|
46
56
|
const { isAdmin, rolePending } = useMemiBoardAuth()
|
|
47
57
|
const { categories, settingsPending, saveCategory } = useMemiBoardSettings()
|
|
@@ -73,6 +83,7 @@ function emptyDraft(id: string): BoardCategory {
|
|
|
73
83
|
writeRole: 'user',
|
|
74
84
|
commentWriteRole: 'user',
|
|
75
85
|
allowedStaffUids: [],
|
|
86
|
+
locale: '',
|
|
76
87
|
}
|
|
77
88
|
}
|
|
78
89
|
|
|
@@ -92,6 +103,7 @@ function toDraft(category: BoardCategory): BoardCategory {
|
|
|
92
103
|
writeRole: category.writeRole ?? 'user',
|
|
93
104
|
commentWriteRole: category.commentWriteRole ?? 'user',
|
|
94
105
|
allowedStaffUids: category.allowedStaffUids ?? [],
|
|
106
|
+
locale: category.locale ?? '',
|
|
95
107
|
}
|
|
96
108
|
}
|
|
97
109
|
|
|
@@ -144,7 +156,7 @@ async function save() {
|
|
|
144
156
|
emit('saved', snapshot)
|
|
145
157
|
}
|
|
146
158
|
catch (cause) {
|
|
147
|
-
error.value = cause instanceof Error ? cause.message : '
|
|
159
|
+
error.value = cause instanceof Error ? cause.message : t('settings.error.saveFailed')
|
|
148
160
|
}
|
|
149
161
|
finally {
|
|
150
162
|
saving.value = false
|
|
@@ -153,33 +165,33 @@ async function save() {
|
|
|
153
165
|
</script>
|
|
154
166
|
|
|
155
167
|
<template>
|
|
156
|
-
<div v-if="pending" class="py-16 text-center text-sm text-muted"
|
|
157
|
-
<UAlert v-else-if="!canManage" color="neutral" variant="subtle" icon="i-lucide-lock" title="
|
|
158
|
-
<UAlert v-else-if="!draft" color="neutral" variant="subtle" icon="i-lucide-circle-help" title="
|
|
168
|
+
<div v-if="pending" class="py-16 text-center text-sm text-muted">{{ t('settings.loading') }}</div>
|
|
169
|
+
<UAlert v-else-if="!canManage" color="neutral" variant="subtle" icon="i-lucide-lock" :title="t('settings.noAccess')" />
|
|
170
|
+
<UAlert v-else-if="!draft" color="neutral" variant="subtle" icon="i-lucide-circle-help" :title="t('settings.notFound.title')" :description="t('settings.notFound.checkBoardAddress')" />
|
|
159
171
|
<div v-else class="flex flex-col gap-5">
|
|
160
172
|
<UAlert
|
|
161
173
|
v-if="isNew"
|
|
162
174
|
color="neutral"
|
|
163
175
|
variant="subtle"
|
|
164
176
|
icon="i-lucide-circle-plus"
|
|
165
|
-
title="
|
|
166
|
-
description="
|
|
177
|
+
:title="t('settings.newBoard.title')"
|
|
178
|
+
:description="t('settings.newBoard.description')"
|
|
167
179
|
/>
|
|
168
180
|
<div class="grid gap-2 sm:grid-cols-[9rem_1fr] sm:items-center">
|
|
169
|
-
<div><p class="text-sm font-medium"
|
|
181
|
+
<div><p class="text-sm font-medium">{{ t('settings.id.label') }}</p><p class="text-xs text-muted">{{ t('settings.id.help') }}</p></div>
|
|
170
182
|
<UInput :model-value="draft.id" disabled class="font-mono" />
|
|
171
183
|
</div>
|
|
172
184
|
<div class="flex flex-col gap-2">
|
|
173
|
-
<div><p class="text-sm font-medium"
|
|
185
|
+
<div><p class="text-sm font-medium">{{ t('settings.writeRole.label') }}</p><p class="text-xs text-muted">{{ t('settings.writeRole.helpShort') }}</p></div>
|
|
174
186
|
<MemiBoardOptionCards v-model="draft.writeRole" :options="writeRoleOptions" @update:model-value="saved = false" />
|
|
175
187
|
</div>
|
|
176
188
|
<div class="flex flex-col gap-2">
|
|
177
189
|
<div>
|
|
178
|
-
<p class="text-sm font-medium"
|
|
190
|
+
<p class="text-sm font-medium">{{ t('settings.commentWriteRole.label') }}</p>
|
|
179
191
|
<p class="text-xs text-muted">
|
|
180
192
|
{{ draft.visibility === 'hidden'
|
|
181
|
-
? '
|
|
182
|
-
: '
|
|
193
|
+
? t('settings.commentWriteRole.hiddenHelp')
|
|
194
|
+
: t('settings.commentWriteRole.helpShort') }}
|
|
183
195
|
</p>
|
|
184
196
|
</div>
|
|
185
197
|
<MemiBoardOptionCards
|
|
@@ -192,55 +204,65 @@ async function save() {
|
|
|
192
204
|
v-else
|
|
193
205
|
class="rounded-lg border border-default px-3 py-2.5 text-sm text-muted"
|
|
194
206
|
>
|
|
195
|
-
|
|
207
|
+
{{ t('settings.commentWriteRole.hiddenFixed') }}
|
|
196
208
|
</p>
|
|
197
209
|
</div>
|
|
198
210
|
<div v-if="needsStaffPicker" class="grid gap-2 sm:grid-cols-[9rem_1fr] sm:items-center">
|
|
199
|
-
<div><p class="text-sm font-medium"
|
|
211
|
+
<div><p class="text-sm font-medium">{{ t('settings.staff.label') }}</p><p class="text-xs text-muted">{{ t('settings.staff.help') }}</p></div>
|
|
200
212
|
<USelectMenu
|
|
201
213
|
v-model="draft.allowedStaffUids"
|
|
202
214
|
:items="staffOptions"
|
|
203
215
|
value-key="value"
|
|
204
216
|
label-key="label"
|
|
205
217
|
multiple
|
|
206
|
-
placeholder="
|
|
218
|
+
:placeholder="t('settings.staff.placeholder')"
|
|
207
219
|
@update:model-value="saved = false"
|
|
208
220
|
/>
|
|
209
221
|
</div>
|
|
210
222
|
<div class="grid gap-2 sm:grid-cols-[9rem_1fr] sm:items-center">
|
|
211
|
-
<div><p class="text-sm font-medium"
|
|
223
|
+
<div><p class="text-sm font-medium">{{ t('settings.label.label') }}</p><p class="text-xs text-muted">{{ t('settings.label.help') }}</p></div>
|
|
212
224
|
<UInput v-model="draft.label" @update:model-value="saved = false" />
|
|
213
225
|
</div>
|
|
214
226
|
<div class="grid gap-2 sm:grid-cols-[9rem_1fr] sm:items-start">
|
|
215
|
-
<div><p class="text-sm font-medium"
|
|
227
|
+
<div><p class="text-sm font-medium">{{ t('settings.description.label') }}</p><p class="text-xs text-muted">{{ t('settings.description.help') }}</p></div>
|
|
216
228
|
<UTextarea
|
|
217
229
|
v-model="draft.description"
|
|
218
230
|
:rows="3"
|
|
219
231
|
autoresize
|
|
220
232
|
:maxrows="6"
|
|
221
|
-
placeholder="
|
|
233
|
+
:placeholder="t('settings.description.placeholder')"
|
|
222
234
|
@update:model-value="saved = false"
|
|
223
235
|
/>
|
|
224
236
|
</div>
|
|
225
237
|
<div v-if="canManageStaffAssignment" class="flex flex-col gap-2">
|
|
226
238
|
<div>
|
|
227
|
-
<p class="text-sm font-medium"
|
|
228
|
-
<p class="text-xs text-muted"
|
|
239
|
+
<p class="text-sm font-medium">{{ t('settings.visibility.label') }}</p>
|
|
240
|
+
<p class="text-xs text-muted">{{ t('settings.visibility.help') }}</p>
|
|
229
241
|
</div>
|
|
230
242
|
<MemiBoardOptionCards v-model="draft.visibility" :options="visibilityOptions" @update:model-value="saved = false" />
|
|
231
243
|
</div>
|
|
232
244
|
<div class="flex flex-col gap-2">
|
|
233
|
-
<div><p class="text-sm font-medium"
|
|
245
|
+
<div><p class="text-sm font-medium">{{ t('settings.listView.label') }}</p><p class="text-xs text-muted">{{ t('settings.listView.help') }}</p></div>
|
|
234
246
|
<MemiBoardOptionCards v-model="draft.listView" :options="listViewOptions" @update:model-value="saved = false" />
|
|
235
247
|
</div>
|
|
236
248
|
<div class="flex flex-col gap-2">
|
|
237
|
-
<div><p class="text-sm font-medium"
|
|
249
|
+
<div><p class="text-sm font-medium">{{ t('settings.editorType.label') }}</p><p class="text-xs text-muted">{{ t('settings.editorType.help') }}</p></div>
|
|
238
250
|
<MemiBoardOptionCards v-model="draft.editorType" :options="editorTypeOptions" @update:model-value="saved = false" />
|
|
239
251
|
</div>
|
|
252
|
+
<div class="grid gap-2 sm:grid-cols-[9rem_1fr] sm:items-center">
|
|
253
|
+
<div><p class="text-sm font-medium">{{ t('settings.locale.label') }}</p><p class="text-xs text-muted">{{ t('settings.locale.help') }}</p></div>
|
|
254
|
+
<USelect
|
|
255
|
+
:model-value="draft.locale || LOCALE_AUTO"
|
|
256
|
+
:items="localeOptions"
|
|
257
|
+
value-key="value"
|
|
258
|
+
label-key="label"
|
|
259
|
+
@update:model-value="draft.locale = fromLocaleOption($event as string); saved = false"
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
240
262
|
<div class="flex justify-end border-t border-default pt-4">
|
|
241
|
-
<UButton :label="isNew ? '
|
|
263
|
+
<UButton :label="isNew ? t('settings.action.create') : t('common.action.save')" :icon="isNew ? 'i-lucide-plus' : 'i-lucide-save'" :loading="saving" @click="save" />
|
|
242
264
|
</div>
|
|
243
265
|
<UAlert v-if="error" color="error" variant="subtle" :description="error" />
|
|
244
|
-
<UAlert v-if="saved" color="success" variant="subtle" :description="isNew ? '
|
|
266
|
+
<UAlert v-if="saved" color="success" variant="subtle" :description="isNew ? t('settings.created') : t('settings.savedSettings')" />
|
|
245
267
|
</div>
|
|
246
268
|
</template>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, ref, resolveComponent, toRef } from 'vue'
|
|
3
|
-
import {
|
|
3
|
+
import { useMemiBoardCommentFeed, useMemiBoardI18n } from 'memi-board/runtime'
|
|
4
4
|
import type { CommentFeedSort, CommentModel } from 'memi-board/runtime'
|
|
5
5
|
|
|
6
6
|
const NuxtLink = resolveComponent('NuxtLink')
|
|
@@ -18,6 +18,8 @@ const props = withDefaults(defineProps<{
|
|
|
18
18
|
|
|
19
19
|
const emit = defineEmits<{ select: [comment: CommentModel] }>()
|
|
20
20
|
|
|
21
|
+
const { t, formatRelativeDate } = useMemiBoardI18n()
|
|
22
|
+
|
|
21
23
|
const { comments, commentsPending, hasMore, loadingMore, loadError, loadMore } = useMemiBoardCommentFeed(
|
|
22
24
|
toRef(props, 'sort'),
|
|
23
25
|
{ pageSize: props.pageSize },
|
|
@@ -39,7 +41,7 @@ function boardName(comment: CommentModel): string {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
const emptyMessage = computed(() =>
|
|
42
|
-
props.sort === 'likes' ? '
|
|
44
|
+
props.sort === 'likes' ? t('commentFeed.emptyLiked') : t('commentFeed.empty'),
|
|
43
45
|
)
|
|
44
46
|
</script>
|
|
45
47
|
|
|
@@ -57,7 +59,7 @@ const emptyMessage = computed(() =>
|
|
|
57
59
|
v-else-if="loadError"
|
|
58
60
|
class="py-8 text-center text-sm text-error"
|
|
59
61
|
>
|
|
60
|
-
|
|
62
|
+
{{ t('commentFeed.loadFailed', { error: String(loadError) }) }}
|
|
61
63
|
</p>
|
|
62
64
|
|
|
63
65
|
<p
|
|
@@ -83,11 +85,11 @@ const emptyMessage = computed(() =>
|
|
|
83
85
|
class="truncate font-medium text-highlighted"
|
|
84
86
|
:class="authorLink(comment) ? 'hover:underline' : ''"
|
|
85
87
|
>
|
|
86
|
-
{{ comment.authorName || '
|
|
88
|
+
{{ comment.authorName || t('common.label.anonymous') }}
|
|
87
89
|
</component>
|
|
88
90
|
<span v-if="boardName(comment)">· {{ boardName(comment) }}</span>
|
|
89
91
|
<span>· {{ formatRelativeDate(comment.createdAt, now) }}</span>
|
|
90
|
-
<span v-if="comment.parentId || comment.isReply">·
|
|
92
|
+
<span v-if="comment.parentId || comment.isReply">· {{ t('common.action.reply') }}</span>
|
|
91
93
|
</div>
|
|
92
94
|
<component
|
|
93
95
|
:is="commentLink(comment) ? NuxtLink : 'button'"
|
|
@@ -99,7 +101,7 @@ const emptyMessage = computed(() =>
|
|
|
99
101
|
v-if="comment.isBlinded"
|
|
100
102
|
class="text-sm text-muted italic"
|
|
101
103
|
>
|
|
102
|
-
|
|
104
|
+
{{ t('commentFeed.blinded') }}
|
|
103
105
|
</p>
|
|
104
106
|
<p
|
|
105
107
|
v-else
|
|
@@ -129,7 +131,7 @@ const emptyMessage = computed(() =>
|
|
|
129
131
|
<UButton
|
|
130
132
|
variant="outline"
|
|
131
133
|
color="neutral"
|
|
132
|
-
label="
|
|
134
|
+
:label="t('common.action.more')"
|
|
133
135
|
block
|
|
134
136
|
class="w-full"
|
|
135
137
|
:loading="loadingMore"
|