@esershnr/artalk-sidebar 1.0.3

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.
Files changed (75) hide show
  1. package/README.md +16 -0
  2. package/auto-imports.d.ts +76 -0
  3. package/components.d.ts +31 -0
  4. package/env.d.ts +2 -0
  5. package/index.html +15 -0
  6. package/package.json +32 -0
  7. package/public/favicon.png +0 -0
  8. package/public/robots.txt +2 -0
  9. package/src/App.vue +89 -0
  10. package/src/artalk.ts +82 -0
  11. package/src/assets/favicon.png +0 -0
  12. package/src/assets/icon-darkmode-off.svg +1 -0
  13. package/src/assets/icon-darkmode-on.svg +1 -0
  14. package/src/assets/icon-eye-off.svg +1 -0
  15. package/src/assets/icon-eye-on.svg +1 -0
  16. package/src/assets/nav-icon-comments.svg +1 -0
  17. package/src/assets/nav-icon-pages.svg +1 -0
  18. package/src/assets/nav-icon-search.svg +1 -0
  19. package/src/assets/nav-icon-settings.svg +1 -0
  20. package/src/assets/nav-icon-sites.svg +1 -0
  21. package/src/assets/nav-icon-transfer.svg +1 -0
  22. package/src/assets/nav-icon-users.svg +1 -0
  23. package/src/components/AppHeader.vue +235 -0
  24. package/src/components/AppNavigation.vue +11 -0
  25. package/src/components/AppNavigationDesktop.vue +176 -0
  26. package/src/components/AppNavigationMenu.ts +152 -0
  27. package/src/components/AppNavigationMobile.vue +187 -0
  28. package/src/components/AppNavigationSearch.vue +137 -0
  29. package/src/components/FileUploader.vue +149 -0
  30. package/src/components/ItemTextEditor.vue +130 -0
  31. package/src/components/LoadingLayer.vue +37 -0
  32. package/src/components/LogTerminal.vue +89 -0
  33. package/src/components/PageEditor.vue +171 -0
  34. package/src/components/Pagination.vue +253 -0
  35. package/src/components/PreferenceArr.vue +105 -0
  36. package/src/components/PreferenceGrp.vue +153 -0
  37. package/src/components/PreferenceItem.vue +159 -0
  38. package/src/components/SiteCreate.vue +96 -0
  39. package/src/components/SiteEditor.vue +138 -0
  40. package/src/components/SiteSwitcher.vue +184 -0
  41. package/src/components/UserEditor.vue +229 -0
  42. package/src/global.ts +62 -0
  43. package/src/hooks/MobileWidth.ts +27 -0
  44. package/src/i18n/fr.ts +103 -0
  45. package/src/i18n/ja.ts +100 -0
  46. package/src/i18n/ko.ts +99 -0
  47. package/src/i18n/ru.ts +102 -0
  48. package/src/i18n/tr.ts +102 -0
  49. package/src/i18n/zh-CN.ts +97 -0
  50. package/src/i18n/zh-TW.ts +97 -0
  51. package/src/i18n-en.ts +99 -0
  52. package/src/i18n.ts +37 -0
  53. package/src/lib/promise-polyfill.ts +9 -0
  54. package/src/lib/settings-option.ts +186 -0
  55. package/src/lib/settings-sensitive.ts +44 -0
  56. package/src/lib/settings.ts +94 -0
  57. package/src/main.ts +65 -0
  58. package/src/pages/comments.vue +110 -0
  59. package/src/pages/index.vue +33 -0
  60. package/src/pages/login.vue +245 -0
  61. package/src/pages/pages.vue +309 -0
  62. package/src/pages/settings.vue +181 -0
  63. package/src/pages/sites.vue +353 -0
  64. package/src/pages/transfer.vue +204 -0
  65. package/src/pages/users.vue +271 -0
  66. package/src/stores/nav.ts +114 -0
  67. package/src/stores/user.ts +48 -0
  68. package/src/style/_extends.scss +100 -0
  69. package/src/style/_variables.scss +18 -0
  70. package/src/style.scss +245 -0
  71. package/src/vue-i18n.d.ts +7 -0
  72. package/tsconfig.json +40 -0
  73. package/tsconfig.node.json +11 -0
  74. package/typed-router.d.ts +30 -0
  75. package/vite.config.ts +71 -0
@@ -0,0 +1,96 @@
1
+ <script setup lang="ts">
2
+ import type { ArtalkType } from '@esershnr/artalk'
3
+ import { artalk } from '../global'
4
+
5
+ const { t } = useI18n()
6
+
7
+ const props = defineProps<{
8
+ initVal?: { name: string; urls: string }
9
+ }>()
10
+
11
+ const emit = defineEmits<{
12
+ (evt: 'close'): void
13
+ (evt: 'done', siteNew: ArtalkType.SiteData): void
14
+ }>()
15
+
16
+ const isLoading = ref(false)
17
+ const site = ref<{ name: string; urls: string }>({
18
+ name: '',
19
+ urls: '',
20
+ })
21
+
22
+ onMounted(() => {
23
+ site.value.name = props.initVal?.name || ''
24
+ site.value.urls = props.initVal?.urls || ''
25
+ })
26
+
27
+ async function submit() {
28
+ const siteName = site.value.name.trim()
29
+ const siteUrls = site.value.urls
30
+ .trim()
31
+ .split(',')
32
+ .map((v) => v.trim())
33
+ .filter((v) => !!v)
34
+
35
+ if (siteName === '') {
36
+ alert(t('siteNameInputHint'))
37
+ return
38
+ }
39
+
40
+ isLoading.value = true
41
+ let s: ArtalkType.SiteData
42
+ try {
43
+ s = (
44
+ await artalk!.ctx.getApi().sites.createSite({
45
+ name: siteName,
46
+ urls: siteUrls,
47
+ })
48
+ ).data
49
+ } catch (err: any) {
50
+ window.alert(err.message)
51
+ console.error(err)
52
+ return
53
+ } finally {
54
+ isLoading.value = false
55
+ }
56
+
57
+ emit('done', s)
58
+ }
59
+
60
+ function close() {
61
+ emit('close')
62
+ }
63
+ </script>
64
+
65
+ <template>
66
+ <div class="atk-site-add">
67
+ <div class="atk-header">
68
+ <div class="atk-title">{{ t('createSite') }}</div>
69
+ <div class="atk-close-btn" @click="close()">
70
+ <i class="atk-icon atk-icon-close"></i>
71
+ </div>
72
+ </div>
73
+ <form class="atk-form" @submit.prevent="submit()">
74
+ <input
75
+ v-model="site.name"
76
+ type="text"
77
+ name="AtkSiteName"
78
+ :placeholder="t('siteName')"
79
+ autocomplete="off"
80
+ />
81
+ <input
82
+ v-model="site.urls"
83
+ type="text"
84
+ name="AtkSiteUrls"
85
+ :placeholder="`${t('siteUrls')} (${t('multiSepHint')})`"
86
+ autocomplete="off"
87
+ />
88
+ <button type="submit" class="atk-btn" name="AtkSubmit">
89
+ {{ t('add') }}
90
+ </button>
91
+ </form>
92
+ <LoadingLayer v-if="isLoading" />
93
+ </div>
94
+ </template>
95
+
96
+ <style scoped lang="scss"></style>
@@ -0,0 +1,138 @@
1
+ <script setup lang="ts">
2
+ import type { ArtalkType } from '@esershnr/artalk'
3
+ import { artalk } from '../global'
4
+
5
+ const props = defineProps<{
6
+ site: ArtalkType.SiteData
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ (evt: 'close'): void
11
+ (evt: 'update', page: ArtalkType.SiteData): void
12
+ (evt: 'remove', id: number): void
13
+ }>()
14
+
15
+ const { site } = toRefs(props)
16
+ const isLoading = ref(false)
17
+ const editFieldKey = ref<keyof ArtalkType.SiteData | null>(null)
18
+ const editFieldVal = computed(() => {
19
+ if (editFieldKey.value === 'urls') return site.value.urls_raw || ''
20
+ return String(editFieldKey.value ? site.value[editFieldKey.value!] || '' : '')
21
+ })
22
+
23
+ const { t } = useI18n()
24
+
25
+ function close() {
26
+ emit('close')
27
+ }
28
+
29
+ function openURL(url: string) {
30
+ window.open(url)
31
+ }
32
+
33
+ function rename() {
34
+ editFieldKey.value = 'name'
35
+ }
36
+
37
+ function editURL() {
38
+ editFieldKey.value = 'urls'
39
+ }
40
+
41
+ function del() {
42
+ const del = async () => {
43
+ isLoading.value = true
44
+ try {
45
+ await artalk!.ctx.getApi().sites.deleteSite(site.value.id)
46
+ } catch (err: any) {
47
+ alert(err.message)
48
+ console.error(err)
49
+ return
50
+ } finally {
51
+ isLoading.value = false
52
+ }
53
+ emit('remove', site.value.id)
54
+ }
55
+ if (window.confirm(t('siteDeleteConfirm', { name: site.value.name }))) del()
56
+ }
57
+
58
+ async function onFieldEditorYes(val: string) {
59
+ if (!editFieldKey.value) return
60
+
61
+ if (editFieldVal.value !== val) {
62
+ isLoading.value = true
63
+ let s: ArtalkType.SiteData
64
+ try {
65
+ let finalVal: string | string[] = val
66
+ if (Array.isArray(site.value[editFieldKey.value]))
67
+ finalVal = val
68
+ .split(',')
69
+ .map((v) => v.trim())
70
+ .filter((v) => !!v)
71
+ s = (
72
+ await artalk!.ctx.getApi().sites.updateSite(site.value.id, {
73
+ ...site.value,
74
+ [editFieldKey.value]: finalVal,
75
+ })
76
+ ).data
77
+ } catch (err: any) {
78
+ alert(err.message)
79
+ console.error(err)
80
+ return false
81
+ } finally {
82
+ isLoading.value = false
83
+ }
84
+ emit('update', s)
85
+ }
86
+
87
+ editFieldKey.value = null
88
+ return true
89
+ }
90
+
91
+ function onFiledEditorNo() {
92
+ editFieldKey.value = null
93
+ }
94
+ </script>
95
+
96
+ <template>
97
+ <div class="atk-site-edit">
98
+ <div class="atk-header">
99
+ <div class="atk-site-info">
100
+ <span class="atk-site-name" @click="!site.first_url || openURL(site.first_url)">
101
+ {{ site.name }}
102
+ </span>
103
+ <span class="atk-site-urls">
104
+ <div v-for="(url, i) in site.urls" :key="i" class="atk-url-item" @click="openURL(url)">
105
+ {{ url }}
106
+ </div>
107
+ </span>
108
+ </div>
109
+ <div class="atk-close-btn" @click="close()">
110
+ <i class="atk-icon atk-icon-close"></i>
111
+ </div>
112
+ </div>
113
+ <div class="atk-main">
114
+ <div class="atk-site-text-actions">
115
+ <div class="atk-item atk-rename-btn" @click="rename()">
116
+ {{ t('rename') }}
117
+ </div>
118
+ <div class="atk-item atk-edit-url-btn" @click="editURL()">{{ t('edit') }} URL</div>
119
+ <!--<div class="atk-item atk-export-btn">Export</div>
120
+ <div class="atk-item atk-import-btn">Import</div>-->
121
+ </div>
122
+ <div class="atk-site-btn-actions">
123
+ <div class="atk-item atk-del-btn" @click="del()">
124
+ <i class="atk-icon atk-icon-del"></i>
125
+ </div>
126
+ </div>
127
+ <LoadingLayer v-if="isLoading" style="z-index: 1000" />
128
+ <ItemTextEditor
129
+ v-if="!!editFieldKey"
130
+ :init-value="editFieldVal"
131
+ @yes="onFieldEditorYes"
132
+ @no="onFiledEditorNo"
133
+ />
134
+ </div>
135
+ </div>
136
+ </template>
137
+
138
+ <style scoped lang="scss"></style>
@@ -0,0 +1,184 @@
1
+ <script setup lang="ts">
2
+ import { storeToRefs } from 'pinia'
3
+ import { useNavStore } from '../stores/nav'
4
+ import { useUserStore } from '../stores/user'
5
+ import { isOpenFromSidebar } from '@/global'
6
+
7
+ const el = ref<HTMLElement | null>(null)
8
+
9
+ const nav = useNavStore()
10
+ const { siteSwitcherShow: curtShow, sites } = storeToRefs(nav)
11
+ const { site: curtSite } = storeToRefs(useUserStore())
12
+
13
+ const router = useRouter()
14
+ const { t } = useI18n()
15
+
16
+ onMounted(() => {
17
+ nav.refreshSites()
18
+ })
19
+
20
+ interface IDisplaySite {
21
+ label: string
22
+ name: string
23
+ logoText: string
24
+ }
25
+
26
+ function switchSite(siteName: string) {
27
+ if (siteName === '__SITE_MANAGEMENT__') {
28
+ router.replace('/sites')
29
+ } else {
30
+ curtSite.value = siteName
31
+ }
32
+
33
+ curtShow.value = false
34
+ }
35
+
36
+ const displaySites = computed(() => {
37
+ const displays: IDisplaySite[] = []
38
+ displays.push({ label: t('allSites'), name: '', logoText: '_' })
39
+ sites.value.forEach((site) => {
40
+ displays.push({
41
+ label: site.name,
42
+ name: site.name,
43
+ logoText: site.name.substring(0, 1),
44
+ })
45
+ })
46
+ displays.push({
47
+ label: t('siteManage'),
48
+ name: '__SITE_MANAGEMENT__',
49
+ logoText: '+',
50
+ })
51
+ return displays
52
+ })
53
+
54
+ function outsideChecker(evt: MouseEvent) {
55
+ const isClickInside = el.value?.contains(evt.target as any)
56
+ if (!isClickInside) {
57
+ curtShow.value = false
58
+ }
59
+ }
60
+
61
+ watch(curtShow, (value) => {
62
+ if (value) {
63
+ setTimeout(() => {
64
+ document.addEventListener('click', outsideChecker)
65
+ }, 80)
66
+ } else {
67
+ document.removeEventListener('click', outsideChecker)
68
+ }
69
+ })
70
+
71
+ function logout() {
72
+ if (!window.confirm(t('logoutConfirm'))) return
73
+
74
+ useUserStore().logout()
75
+ nextTick(() => {
76
+ router.replace('/login')
77
+ })
78
+ }
79
+ </script>
80
+
81
+ <template>
82
+ <Transition>
83
+ <div v-show="curtShow" ref="el" class="atk-site-list-floater">
84
+ <div class="atk-sites">
85
+ <div
86
+ v-for="(site, i) in displaySites"
87
+ :key="i"
88
+ class="atk-site-item"
89
+ :class="{ 'atk-active': curtSite === site.name }"
90
+ @click="switchSite(site.name)"
91
+ >
92
+ <div class="atk-site-logo">{{ site.logoText }}</div>
93
+ <div class="atk-site-name">{{ site.label }}</div>
94
+ </div>
95
+
96
+ <!-- Logout Button -->
97
+ <div v-if="!isOpenFromSidebar()" class="atk-site-item" @click="logout()">
98
+ <svg
99
+ class="atk-site-logo"
100
+ stroke="currentColor"
101
+ fill="currentColor"
102
+ stroke-width="0"
103
+ viewBox="-6 -6 36 36"
104
+ xmlns="http://www.w3.org/2000/svg"
105
+ >
106
+ <path
107
+ d="M5 22C4.44772 22 4 21.5523 4 21V3C4 2.44772 4.44772 2 5 2H19C19.5523 2 20 2.44772 20 3V6H18V4H6V20H18V18H20V21C20 21.5523 19.5523 22 19 22H5ZM18 16V13H11V11H18V8L23 12L18 16Z"
108
+ ></path>
109
+ </svg>
110
+ <div class="atk-site-name">{{ t('logout') }}</div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </Transition>
115
+ </template>
116
+
117
+ <style scoped lang="scss">
118
+ .atk-site-list-floater {
119
+ @extend .atk-slim-scrollbar;
120
+
121
+ position: absolute;
122
+ z-index: 9999;
123
+ left: calc((100% - 80%) / 2);
124
+ top: 70px;
125
+ pointer-events: all;
126
+ background: var(--at-color-bg);
127
+ width: 80%;
128
+ max-height: 40%;
129
+ margin: 0 auto;
130
+ border: 1px solid var(--at-color-border);
131
+ border-radius: 4px;
132
+ overflow-y: auto;
133
+ transition: all 0.2s ease;
134
+
135
+ @media (min-width: 1024px) {
136
+ left: 10px;
137
+ max-width: 260px;
138
+ }
139
+
140
+ .atk-sites {
141
+ display: flex;
142
+ flex-direction: column;
143
+ padding: 7px 0;
144
+
145
+ .atk-site-item {
146
+ display: flex;
147
+ flex-direction: row;
148
+ cursor: pointer;
149
+ align-items: center;
150
+ padding: 0 10px;
151
+ margin-bottom: 1px;
152
+
153
+ .atk-site-logo {
154
+ width: 20px;
155
+ height: 20px;
156
+ line-height: 20px;
157
+ background: #5b6f7e;
158
+ margin: 10px;
159
+ border-radius: 3px;
160
+ text-align: center;
161
+ color: #fff;
162
+ font-size: 12px;
163
+ }
164
+
165
+ .atk-site-name {
166
+ color: var(--at-color-deep);
167
+ font-size: 17px;
168
+ margin-left: 7px;
169
+ }
170
+
171
+ &.atk-active,
172
+ &:hover {
173
+ background: var(--at-color-bg-grey);
174
+ }
175
+ }
176
+ }
177
+ }
178
+
179
+ .v-enter-from,
180
+ .v-leave-to {
181
+ opacity: 0;
182
+ transform: scale3d(1.05, 1.05, 1.05);
183
+ }
184
+ </style>
@@ -0,0 +1,229 @@
1
+ <script setup lang="ts">
2
+ import type { ArtalkType } from '@esershnr/artalk'
3
+ import { artalk } from '../global'
4
+
5
+ const { t } = useI18n()
6
+
7
+ interface IUserEditData {
8
+ id: number
9
+ name: string
10
+ email: string
11
+ link: string
12
+ password?: string
13
+ badge_name: string
14
+ badge_color: string
15
+ is_admin: boolean
16
+ receive_email: boolean
17
+ }
18
+
19
+ const props = defineProps<{
20
+ user?: IUserEditData
21
+ }>()
22
+
23
+ const emit = defineEmits<{
24
+ (evt: 'close'): void
25
+ (evt: 'update', user: ArtalkType.UserDataForAdmin): void
26
+ }>()
27
+
28
+ const editUser = ref<IUserEditData>()
29
+
30
+ const isLoading = ref(false)
31
+ const isCreateMode = ref(false)
32
+
33
+ const showFullDetails = ref(false)
34
+
35
+ onBeforeMount(() => {
36
+ if (!props.user) {
37
+ isCreateMode.value = true
38
+ editUser.value = {
39
+ id: 0,
40
+ name: '',
41
+ email: '',
42
+ link: '',
43
+ password: '',
44
+ badge_name: '',
45
+ badge_color: '',
46
+ is_admin: false,
47
+ receive_email: true,
48
+ }
49
+ } else {
50
+ editUser.value = {
51
+ ...props.user,
52
+ password: '',
53
+ }
54
+ }
55
+ })
56
+
57
+ function close() {
58
+ emit('close')
59
+ }
60
+
61
+ function submit() {
62
+ isLoading.value = true
63
+
64
+ // The default badge color
65
+ if (editUser.value!.badge_name !== '' && editUser.value!.badge_color === '') {
66
+ editUser.value!.badge_color = '#0083ff'
67
+ }
68
+
69
+ if (isCreateMode.value) {
70
+ artalk!.ctx
71
+ .getApi()
72
+ .users.createUser({
73
+ ...editUser.value!,
74
+ })
75
+ .then((res) => {
76
+ emit('update', res.data)
77
+ })
78
+ .catch((e: ArtalkType.FetchError) => {
79
+ alert(e.message)
80
+ })
81
+ .finally(() => {
82
+ isLoading.value = false
83
+ })
84
+ } else {
85
+ const user = editUser.value!
86
+ artalk!.ctx
87
+ .getApi()
88
+ .users.updateUser(user.id, {
89
+ ...editUser.value!,
90
+ })
91
+ .then((res) => {
92
+ emit('update', res.data)
93
+ })
94
+ .catch((e: ArtalkType.FetchError) => {
95
+ alert(e.message)
96
+ })
97
+ .finally(() => {
98
+ isLoading.value = false
99
+ })
100
+ }
101
+ }
102
+ </script>
103
+
104
+ <template>
105
+ <div class="user-editor-layer">
106
+ <div class="header">
107
+ <div class="title">
108
+ {{ isCreateMode ? t('userCreate') : t('userEdit') }}
109
+ </div>
110
+ <div v-if="!isCreateMode" class="close-btn" @click="close()">
111
+ <i class="atk-icon atk-icon-close"></i>
112
+ </div>
113
+ </div>
114
+ <div v-if="!isCreateMode" class="user-log">
115
+ <div>
116
+ <span>{{ t('comments') }}</span>
117
+ {{ (editUser as any).comment_count }}
118
+ </div>
119
+ <div>
120
+ <span>{{ t('last') }} IP</span>
121
+ {{ (editUser as any).last_ip || '-' }}
122
+ </div>
123
+ <div>
124
+ <span>{{ t('last') }} UA</span>
125
+ <template v-if="showFullDetails || !(editUser as any).last_ua">
126
+ {{ (editUser as any).last_ua || '-' }}
127
+ </template>
128
+ <template v-else>
129
+ <span
130
+ style="cursor: pointer; color: var(--at-color-main)"
131
+ @click="showFullDetails = true"
132
+ >
133
+ {{ t('Show') }}
134
+ </span>
135
+ </template>
136
+ </div>
137
+ </div>
138
+ <form v-if="editUser" class="atk-form" @submit.prevent="submit()">
139
+ <div class="atk-label required">{{ t('username') }}</div>
140
+ <input v-model="editUser.name" type="text" placeholder="" autocomplete="off" />
141
+ <div class="atk-label required">{{ t('email') }}</div>
142
+ <input v-model="editUser.email" type="text" placeholder="" autocomplete="off" />
143
+ <div class="atk-label">{{ t('link') }}</div>
144
+ <input v-model="editUser.link" type="text" placeholder="" autocomplete="off" />
145
+ <div class="atk-label">{{ t('badgeText') }}</div>
146
+ <input v-model="editUser.badge_name" type="text" placeholder="" autocomplete="off" />
147
+ <div class="atk-label">{{ t('badgeColor') }} (Color Hex)</div>
148
+ <input v-model="editUser.badge_color" type="text" placeholder="" autocomplete="off" />
149
+ <div class="atk-label required">{{ t('role') }}</div>
150
+ <select v-model="editUser.is_admin">
151
+ <option :value="false">{{ t('normal') }}</option>
152
+ <option :value="true">{{ t('admin') }}</option>
153
+ </select>
154
+ <template v-if="editUser.is_admin">
155
+ <div class="atk-label required">{{ t('password') }}</div>
156
+ <input
157
+ v-model="editUser.password"
158
+ type="text"
159
+ :placeholder="isCreateMode ? '' : `(${t('passwordEmptyHint')})`"
160
+ autocomplete="off"
161
+ />
162
+ </template>
163
+ <div class="atk-label required">{{ t('emailNotify') }}</div>
164
+ <select v-model="editUser.receive_email">
165
+ <option :value="true">{{ t('enabled') }}</option>
166
+ <option :value="false">{{ t('disabled') }}</option>
167
+ </select>
168
+ <button type="submit" class="atk-btn">{{ t('save') }}</button>
169
+ </form>
170
+ <LoadingLayer v-if="isLoading" />
171
+ </div>
172
+ </template>
173
+
174
+ <style scoped lang="scss">
175
+ .user-editor-layer {
176
+ z-index: 5;
177
+ position: absolute;
178
+ top: 0;
179
+ left: 0;
180
+ width: 100%;
181
+ height: 100%;
182
+ min-height: calc(100vh - 160px);
183
+ background: var(--at-color-bg);
184
+ overflow-y: auto;
185
+ }
186
+
187
+ .header {
188
+ position: sticky;
189
+ top: 0;
190
+ display: flex;
191
+ flex-direction: row;
192
+ align-items: center;
193
+ padding: 20px 20px 10px 20px;
194
+ background: var(--at-color-bg-transl);
195
+
196
+ .title {
197
+ margin-left: 10px;
198
+ flex: 1;
199
+ font-size: 19px;
200
+ }
201
+ }
202
+
203
+ .close-btn {
204
+ width: 50px;
205
+ height: 50px;
206
+ display: flex;
207
+ justify-content: center;
208
+ align-items: center;
209
+ cursor: pointer;
210
+
211
+ &:hover i::after {
212
+ background-color: var(--at-color-red);
213
+ }
214
+ }
215
+
216
+ .user-log {
217
+ margin: 0 30px;
218
+ padding: 15px 25px;
219
+ border-radius: 2px;
220
+ font-size: 13px;
221
+ background: var(--at-color-bg-grey);
222
+ line-height: 1.8;
223
+
224
+ & > div > span:first-child {
225
+ display: inline-block;
226
+ width: 5em;
227
+ }
228
+ }
229
+ </style>
package/src/global.ts ADDED
@@ -0,0 +1,62 @@
1
+ import Artalk from '@esershnr/artalk'
2
+ import type { LocalUser } from '@esershnr/artalk'
3
+
4
+ export let artalk: Artalk | null = null
5
+
6
+ export function setArtalk(artalkInstance: Artalk) {
7
+ artalk = artalkInstance
8
+ }
9
+
10
+ export function getArtalk() {
11
+ return artalk
12
+ }
13
+
14
+ /**
15
+ * Boot params from URL search params
16
+ *
17
+ * TODO: Refactor to a singleton store
18
+ */
19
+ export const bootParams = getBootParams()
20
+
21
+ function getBootParams() {
22
+ const p = new URLSearchParams(document.location.search)
23
+
24
+ // call history api to clear search params
25
+ // on purpose to prevent the params (e.g. user token)
26
+ // from being leaked like from the referrer header or the browser history
27
+ if (!!p.get('user') && window.history.replaceState) {
28
+ window.history.replaceState({}, '', window.location.pathname)
29
+ }
30
+
31
+ const userFromURL = JSON.parse(p.get('user') || '{}')
32
+ const user: LocalUser = {
33
+ name: userFromURL.name || '',
34
+ email: userFromURL.email || '',
35
+ link: userFromURL.link || '',
36
+ token: userFromURL.token || '',
37
+ is_admin: userFromURL.is_admin || false,
38
+ }
39
+
40
+ let darkMode: boolean
41
+ if (p.get('darkMode') != null) {
42
+ darkMode = p.get('darkMode') == '1'
43
+ } else {
44
+ darkMode =
45
+ localStorage.getItem('ATK_SIDEBAR_DARK_MODE') != null
46
+ ? localStorage.getItem('ATK_SIDEBAR_DARK_MODE') == '1'
47
+ : window.matchMedia('(prefers-color-scheme: dark)').matches
48
+ }
49
+
50
+ return {
51
+ user,
52
+ pageKey: p.get('pageKey') || '',
53
+ site: p.get('site') || '',
54
+ view: p.get('view') || '',
55
+ viewParams: <any>null,
56
+ darkMode,
57
+ }
58
+ }
59
+
60
+ export function isOpenFromSidebar() {
61
+ return !!bootParams.user?.email
62
+ }