评论阶段一(前端):评论区 + 登录引导卡 + dev mock,经典头像留白修复
评论区(按已批准的设计文档,阶段一=前端;后端与多用户为阶段二/三)
- classic / vivid 共用同一份 DOM:CommentSection + CommentItem(回复一层、
自引用渲染,回复/编辑框模板只写一份)+ CommentMarkdown(唯一 v-html 出口,
必过 sanitizeHtml)。比计划里的两套组件少一半文件,也避免两边结构漂移
- 数据层:api.js 追加 readerApi(me/providers/logout/comments/thread/create/
edit/remove);src/reader.js 读者身份 reactive 单例(仿 site.js,
挂 one:unauthorized 清 user);composables/useComments 按文章实例化
(刻意不做单例,换文章必须重置),乐观插入按当前排序落位
- 交互:排序(默认/最新/最早)、发表、回复(一层 + 加载更多回复)、
10 分钟内编辑、软删、分页(显示更多 N 条)、骨架屏、空态、错误态、审核中徽章
- 登录引导卡:三种 provider(GitHub/Google/Telegram);redirect 类整页跳转,
widget 类(Telegram)点击时内联官方脚本(无跳转、无回调,官方按钮落位)
- 样式:styles.css 追加 .cm-* 基础段(经典 token,vivid 靠别名继承同一份规则);
ui/yohaku/comments.css 只写 vivid 差异(全前缀、特指度盖过基础版),
自己的窄屏/触屏规则写在文件内,不塞 mobile.css
- main.ts:comments.css 插在 pages 之后 mobile 之前(层叠契约不变);
dev mock 用 ?mock 门控动态 import,await 装完再挂载,否则首屏请求会打到真接口
修三处真问题
- vivid 的 a{color:inherit} 重置(特指度 0,1,2)把作者链接名拉回 body 色,
暗色下暗底暗字 —— 用 a.cm-name-a 压回
- classic 文章页整页掉进 236px 左列:PostView import 了 LeftNav/RightRail
却没渲染,补上(与 HomeView 三栏一致),文章页回到 640px
- classic 信息流头像贴死列边框:.row-feed 的负 margin 正好抵光左右内边距,
改 padding 14/16 -> 14/30(30 = 16 出血 + 14 留白,hover 底色与分隔线仍铺满整列)
mock 假数据
- 8 位作者(三种登录方式、不同色相头像、两个走首字母兜底)、
默认一屏 8 条各测一个排版点(代码块/引用/列表/已删除/审核中/已编辑/站长徽章/
加载更多回复),长列表态 26 条测分页;右下角浮层切 9 个状态(?mock&state=),
?ui=classic 可强制 classic 壳(平时渲染不出来,没法做探针)
验证(headless Chromium + CDP 真实时间截图;virtual-time 对含动态 import 的页面
截图时机不稳,会截出全白,已换 CDP 方案)
- 头部计数 13px/tabular-nums、.seg 3 选 1;头像 50% 圆、36/24px;
气泡圆角 3,20,20,20(缺角);回复缩进 36px 且无二层嵌套;360 宽无横向溢出
- 暗色:气泡/输入框/名字全部适配(量的时候被 a 的 150ms 颜色 transition 骗过,
t0 与 t400 两次测量值不同才看清,稳态正确)
- 产物:CSS 顺序 styles→pages→comments→mobile;49 条评论规则全带
html[data-ui=vivid] 前缀;grep __COMMENT_MOCK__ 为空、无 mock chunk —— 生产零 mock
This commit is contained in:
@@ -71,7 +71,9 @@ const initial = computed(() => (site.author_name || 'O').trim().slice(0, 1).toUp
|
||||
display: grid;
|
||||
grid-template-columns: 40px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
/* 左右 30 = 16(出血到列边)+ 14(内容离边框的呼吸位)。
|
||||
之前是 16/16,负 margin 正好把内边距抵光,头像贴死在边框上。 */
|
||||
padding: 14px 30px;
|
||||
margin: 0 -16px;
|
||||
border-bottom: 1px solid var(--line-soft);
|
||||
transition: background 0.15s ease;
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { reader } from '../../reader'
|
||||
import CommentMarkdown from './CommentMarkdown.vue'
|
||||
import { BRANDS, PROVIDER_LABEL } from './brands'
|
||||
import { relativeDate } from '../../utils'
|
||||
|
||||
// 单条评论。回复在同一个组件里自引用渲染(depth 0 → 1),
|
||||
// 这样「回复框 / 编辑框」的模板只写一遍,两层不会各写一份再漂移。
|
||||
// 只渲染一层:depth >= 1 时不再显示它自己的 replies 与「加载更多」。
|
||||
const props = defineProps({
|
||||
c: { type: Object, required: true },
|
||||
api: { type: Object, required: true },
|
||||
depth: { type: Number, default: 0 },
|
||||
maxLen: { type: Number, default: 500 }
|
||||
})
|
||||
|
||||
const s = props.api.s
|
||||
const EDIT_WINDOW_MS = 10 * 60 * 1000
|
||||
|
||||
const name = computed(() => (props.c.author && props.c.author.name) || '已注销用户')
|
||||
const relativeTime = computed(() => relativeDate(props.c.created_at))
|
||||
const isOwner = computed(() => !!(props.c.author && props.c.author.is_owner))
|
||||
const isMine = computed(() => !!(reader.user && props.c.author && reader.user.id === props.c.author.id))
|
||||
|
||||
// 外链:优先作者自填的 url,其次 GitHub 主页
|
||||
const authorHref = computed(() => {
|
||||
const a = props.c.author
|
||||
if (!a) return ''
|
||||
if (a.url) return a.url
|
||||
if (a.provider === 'github' && a.handle) return `https://github.com/${a.handle}`
|
||||
return ''
|
||||
})
|
||||
|
||||
const canEdit = computed(() => {
|
||||
if (props.c.is_deleted) return false
|
||||
if (isOwner.value) return true
|
||||
if (!isMine.value) return false
|
||||
const t = new Date(props.c.created_at).getTime()
|
||||
return Number.isFinite(t) && Date.now() - t <= EDIT_WINDOW_MS
|
||||
})
|
||||
const canDelete = computed(() => !props.c.is_deleted && (isMine.value || isOwner.value))
|
||||
|
||||
const replying = computed(() => s.replyTo && s.replyTo.id === props.c.id)
|
||||
const editing = computed(() => s.editing && s.editing.id === props.c.id)
|
||||
|
||||
const replyDraft = ref('')
|
||||
const editDraft = ref('')
|
||||
|
||||
function startReply() {
|
||||
replyDraft.value = ''
|
||||
props.api.startReply(props.c)
|
||||
}
|
||||
function startEdit() {
|
||||
editDraft.value = props.c.body_md || ''
|
||||
props.api.startEdit(props.c)
|
||||
}
|
||||
async function onReply() {
|
||||
const text = replyDraft.value.trim()
|
||||
if (!text) return
|
||||
const ok = await props.api.submit(text, props.c)
|
||||
if (ok) {
|
||||
replyDraft.value = ''
|
||||
props.api.cancel()
|
||||
}
|
||||
}
|
||||
async function onSave() {
|
||||
const text = editDraft.value.trim()
|
||||
if (!text) return
|
||||
await props.api.saveEdit(props.c, text)
|
||||
}
|
||||
async function onRemove() {
|
||||
if (!window.confirm('删掉这条评论?')) return
|
||||
await props.api.remove(props.c)
|
||||
}
|
||||
|
||||
const moreReplies = computed(() =>
|
||||
props.depth === 0 ? Math.max(0, (props.c.reply_count || 0) - ((props.c.replies || []).length)) : 0
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="cm-item" :class="{ 'is-deleted': c.is_deleted, 'is-reply': depth > 0 }">
|
||||
<div class="cm-av" :class="{ 'is-owner': isOwner }">
|
||||
<img
|
||||
v-if="c.author && c.author.avatar_url"
|
||||
:src="c.author.avatar_url"
|
||||
:alt="`${name} 的头像`"
|
||||
loading="lazy"
|
||||
>
|
||||
<span v-else class="cm-av-fb">{{ name.slice(0, 1) }}</span>
|
||||
<i v-if="c.author && c.author.provider" class="cm-pv" :title="PROVIDER_LABEL[c.author.provider] || c.author.provider">
|
||||
<svg v-if="BRANDS[c.author.provider]" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path :d="BRANDS[c.author.provider]" />
|
||||
</svg>
|
||||
</i>
|
||||
</div>
|
||||
|
||||
<div class="cm-main">
|
||||
<div class="cm-name">
|
||||
<a
|
||||
v-if="authorHref"
|
||||
class="cm-name-a"
|
||||
:href="authorHref"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>{{ name }}</a>
|
||||
<span v-else class="cm-name-a">{{ name }}</span>
|
||||
<span v-if="isOwner" class="cm-owner">站长</span>
|
||||
<time class="cm-time" :datetime="c.created_at">{{ relativeTime }}</time>
|
||||
<span v-if="c.state === 'pending'" class="cm-pending">审核中</span>
|
||||
<span v-if="c.edited_at" class="cm-edited-tip">已编辑</span>
|
||||
</div>
|
||||
|
||||
<!-- 编辑态:把气泡换成输入框 -->
|
||||
<div v-if="editing" class="cm-field cm-field-inline">
|
||||
<p class="cm-field-tip">正在改定此评:</p>
|
||||
<textarea v-model="editDraft" class="cm-input" rows="3" :maxlength="maxLen" aria-label="编辑评论" />
|
||||
<div class="cm-bar">
|
||||
<span class="cm-bar-l" />
|
||||
<span class="cm-bar-r">
|
||||
<button type="button" class="cm-link" @click="api.cancel">取消</button>
|
||||
<button type="button" class="cm-send" :disabled="!editDraft.trim() || s.submitting" @click="onSave">
|
||||
{{ s.submitting ? '保存中…' : '保存' }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="cm-bubble">
|
||||
<span v-if="c.is_deleted" class="cm-gone">该评论已删除</span>
|
||||
<CommentMarkdown v-else :html="c.body_html" />
|
||||
|
||||
<div v-if="!c.is_deleted" class="cm-acts">
|
||||
<button v-if="canEdit" type="button" class="cm-act" aria-label="编辑" title="编辑" @click="startEdit">✎</button>
|
||||
<button v-if="canDelete" type="button" class="cm-act" aria-label="删除" title="删除" @click="onRemove">✕</button>
|
||||
<button v-if="reader.user" type="button" class="cm-act" aria-label="回复" title="回复" @click="startReply">↩</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回复框 -->
|
||||
<div v-if="replying" class="cm-field cm-field-inline">
|
||||
<textarea
|
||||
v-model="replyDraft"
|
||||
class="cm-input"
|
||||
rows="3"
|
||||
:maxlength="maxLen"
|
||||
:placeholder="`回复 ${name}…`"
|
||||
:aria-label="`回复 ${name}`"
|
||||
/>
|
||||
<div class="cm-bar">
|
||||
<span class="cm-bar-l">回应 <b>{{ name }}</b></span>
|
||||
<span class="cm-bar-r">
|
||||
<button type="button" class="cm-link" @click="api.cancel">取消</button>
|
||||
<button type="button" class="cm-send" :disabled="!replyDraft.trim() || s.submitting" @click="onReply">
|
||||
{{ s.submitting ? '发送中…' : '发送' }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回复:只有一层,depth 1 不再往下展开 -->
|
||||
<ul v-if="depth === 0 && c.replies && c.replies.length" class="cm-replies">
|
||||
<CommentItem
|
||||
v-for="r in c.replies"
|
||||
:key="r.id"
|
||||
:c="r"
|
||||
:api="api"
|
||||
:depth="depth + 1"
|
||||
:max-len="maxLen"
|
||||
/>
|
||||
</ul>
|
||||
<button v-if="moreReplies > 0" type="button" class="cm-more-r" @click="api.loadMoreReplies(c)">
|
||||
加载更多回复(剩 {{ moreReplies }} 条)
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { sanitizeHtml } from '../../utils'
|
||||
|
||||
// 评论正文的唯一 v-html 出口。
|
||||
// 后端已经用 goldmark 渲染并转义过一遍(不开启 Unsafe),这里再过一次
|
||||
// DOMPurify —— 和文章正文同一条规矩:任何 v-html 都要走 sanitizeHtml。
|
||||
//
|
||||
// 刻意不给这个组件加 <style>:它被 classic 和 vivid 两边共用,
|
||||
// 而 v-html 出来的内部元素选不到作用域样式(只给子组件根元素加 data-v)。
|
||||
// 正文排版由两边各自的全局规则负责(vivid: comments.css,classic: styles.css)。
|
||||
const props = defineProps({
|
||||
html: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const safe = computed(() => sanitizeHtml(props.html || ''))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cm-md" v-html="safe" />
|
||||
</template>
|
||||
@@ -0,0 +1,179 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { reader, loadReader, signIn, signOut } from '../../reader'
|
||||
import { readerApi } from '../../api'
|
||||
import { useComments } from '../../composables/useComments'
|
||||
import CommentItem from './CommentItem.vue'
|
||||
import { BRANDS } from './brands'
|
||||
|
||||
// 文章评论区。classic 和 vivid 共用这一份 DOM —— 两边只是 CSS 不同
|
||||
// (基础样式在 styles.css 的 .cm-*,vivid 的细节在 ui/yohaku/comments.css)。
|
||||
// 这和左栏/工具栏那种「两套组件」不同:评论的结构完全相同,共用一份才不会漂移。
|
||||
//
|
||||
// 刻意不写 <style>:vivid 组件的既有约定,且这里要同时服务两套 UI。
|
||||
|
||||
const props = defineProps({
|
||||
postId: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const api = useComments(props.postId)
|
||||
const { s, remaining, load, setSort, submit, saveEdit, remove, loadMoreReplies, startReply, startEdit, cancel } = api
|
||||
|
||||
const sorts = [
|
||||
{ value: 'default', label: '默认' },
|
||||
{ value: 'newest', label: '最新' },
|
||||
{ value: 'oldest', label: '最早' }
|
||||
]
|
||||
|
||||
const MAX_LEN = 500
|
||||
const draft = ref('')
|
||||
const canSend = computed(() => draft.value.trim().length > 0 && !s.submitting && draft.value.length <= MAX_LEN)
|
||||
|
||||
async function onSend() {
|
||||
if (!canSend.value) return
|
||||
const ok = await submit(draft.value.trim(), s.replyTo)
|
||||
if (ok) {
|
||||
draft.value = ''
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
function onSignIn(p) {
|
||||
if (p.kind === 'widget') {
|
||||
mountTelegram(p)
|
||||
return
|
||||
}
|
||||
signIn(p)
|
||||
}
|
||||
|
||||
// Telegram 的 Login Widget 没有跳转:只能把官方脚本插进来,它会把自己换成
|
||||
// 官方按钮(iframe,样式改不动)。所以它和另外两个自绘圆钮长得不一样 ——
|
||||
// 这是 Telegram 的限制,不是没做。
|
||||
const tgBox = ref(null)
|
||||
function mountTelegram(p) {
|
||||
const box = tgBox.value
|
||||
if (!box || box.dataset.mounted || !p.login) return
|
||||
box.dataset.mounted = '1'
|
||||
const sc = document.createElement('script')
|
||||
sc.async = true
|
||||
sc.src = 'https://telegram.org/js/telegram-widget.js?22'
|
||||
sc.setAttribute('data-telegram-login', p.login)
|
||||
sc.setAttribute('data-size', 'medium')
|
||||
sc.setAttribute('data-radius', '10')
|
||||
sc.setAttribute('data-request-access', 'write')
|
||||
sc.setAttribute('data-onauth', 'oneTelegramAuth(user)')
|
||||
box.appendChild(sc)
|
||||
}
|
||||
// 官方脚本要求回调是全局函数名
|
||||
if (typeof window !== 'undefined') {
|
||||
window.oneTelegramAuth = async (user) => {
|
||||
try {
|
||||
await readerApi.telegram(user)
|
||||
await loadReader()
|
||||
} catch (e) {
|
||||
s.error = e.message || 'Telegram 登录失败'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!reader.loaded) loadReader()
|
||||
if (props.postId) load(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section id="comments" class="cm-sec">
|
||||
<div class="cm-head">
|
||||
<span class="cm-count">共 {{ s.total }} 条评论</span>
|
||||
<div class="seg cm-sort" role="tablist" aria-label="排序方式">
|
||||
<button
|
||||
v-for="o in sorts"
|
||||
:key="o.value"
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="s.sort === o.value"
|
||||
:class="{ on: s.sort === o.value }"
|
||||
@click="setSort(o.value)"
|
||||
>{{ o.label }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录卡 / 输入框 -->
|
||||
<div v-if="!reader.loaded" class="cm-gate cm-gate-skel" aria-hidden="true" />
|
||||
<div v-else-if="!reader.user" class="cm-gate">
|
||||
<p class="cm-gate-t">使用社交账号登录</p>
|
||||
<ul v-if="reader.providers.length" class="cm-providers">
|
||||
<li v-for="p in reader.providers" :key="p.id">
|
||||
<button
|
||||
type="button"
|
||||
class="cm-provider"
|
||||
:title="`使用 ${p.label} 登录`"
|
||||
:aria-label="`使用 ${p.label} 登录`"
|
||||
@click="onSignIn(p)"
|
||||
>
|
||||
<svg v-if="BRANDS[p.id]" class="cm-brand" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path :d="BRANDS[p.id]" />
|
||||
</svg>
|
||||
<span v-else class="cm-brand-t">{{ p.label.slice(0, 1) }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="cm-gate-hint">站点还没配置登录方式。</p>
|
||||
<!-- Telegram 官方按钮落到这里 -->
|
||||
<div ref="tgBox" class="cm-tg" />
|
||||
</div>
|
||||
<div v-else class="cm-compose">
|
||||
<div class="cm-av cm-av-me">
|
||||
<img v-if="reader.user.avatar_url" :src="reader.user.avatar_url" alt="" loading="lazy">
|
||||
<span v-else class="cm-av-fb">{{ (reader.user.name || '?').slice(0, 1) }}</span>
|
||||
</div>
|
||||
<div class="cm-field">
|
||||
<textarea
|
||||
v-model="draft"
|
||||
class="cm-input"
|
||||
rows="3"
|
||||
:maxlength="MAX_LEN"
|
||||
placeholder="说点什么…(支持 Markdown)"
|
||||
aria-label="写评论"
|
||||
/>
|
||||
<div class="cm-bar">
|
||||
<span class="cm-bar-l">
|
||||
<b>{{ reader.user.name }}</b>
|
||||
<button type="button" class="cm-link" @click="signOut">退出</button>
|
||||
</span>
|
||||
<span class="cm-bar-r">
|
||||
<span class="cm-len" :class="{ over: draft.length > MAX_LEN }">{{ draft.length }}/{{ MAX_LEN }}</span>
|
||||
<button type="button" class="cm-send" :disabled="!canSend" @click="onSend">
|
||||
{{ s.submitting ? '发送中…' : '发送' }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="s.error" class="cm-err">{{ s.error }}</p>
|
||||
|
||||
<!-- 骨架 / 空态 -->
|
||||
<ul v-if="s.loading" class="cm-list cm-skel" aria-hidden="true">
|
||||
<li v-for="i in 3" :key="i" class="cm-skel-row">
|
||||
<span class="cm-skel-av" />
|
||||
<span class="cm-skel-lines"><i /><i /></span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else-if="!s.items.length" class="empty cm-empty">
|
||||
<h3>这里还静悄悄</h3>
|
||||
<p>第一条评论留给你。</p>
|
||||
</div>
|
||||
|
||||
<ul v-else class="cm-list">
|
||||
<CommentItem v-for="c in s.items" :key="c.id" :c="c" :api="api" :depth="0" :max-len="MAX_LEN" />
|
||||
</ul>
|
||||
|
||||
<div v-if="remaining > 0 && !s.loading" class="cm-more">
|
||||
<button type="button" class="chip" :disabled="s.loadingMore" @click="load(false)">
|
||||
{{ s.loadingMore ? '载入中…' : `显示更多(剩 ${remaining} 条)` }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
// 登录方式的品牌图标(24×24,单色,靠 currentColor 上色)。
|
||||
// 只放了站点真正会启用的三种;换成文字首字母兜底,见 CommentSection 的 v-else。
|
||||
export const BRANDS = {
|
||||
github:
|
||||
'M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.56 9.56 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z',
|
||||
google:
|
||||
'M12.24 10.285V14.4h6.806c-.275 1.765-2.056 5.174-6.806 5.174-4.095 0-7.439-3.389-7.439-7.574s3.344-7.574 7.439-7.574c2.33 0 3.891.989 4.785 1.849l3.254-3.138C18.189 1.186 15.479 0 12.24 0c-6.635 0-12 5.365-12 12s5.365 12 12 12c6.926 0 11.52-4.869 11.52-11.726 0-.788-.085-1.39-.189-1.989H12.24z',
|
||||
telegram:
|
||||
'M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z'
|
||||
}
|
||||
|
||||
export const PROVIDER_LABEL = { github: 'GitHub', google: 'Google', telegram: 'Telegram' }
|
||||
Reference in New Issue
Block a user