@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,149 @@
1
+ <script setup lang="ts">
2
+ import { useUserStore } from '../stores/user'
3
+
4
+ const { t } = useI18n()
5
+ const user = useUserStore()
6
+
7
+ const props = defineProps<{
8
+ apiUrl: string
9
+ }>()
10
+
11
+ const emit = defineEmits<{
12
+ /** Upload done event */
13
+ (evt: 'done', filename: string): void
14
+ }>()
15
+
16
+ const { apiUrl } = toRefs(props)
17
+
18
+ let xhr: XMLHttpRequest | null = null
19
+ const fileInputEl = ref<HTMLInputElement | null>(null)
20
+ const remoteFilename = ref('')
21
+ const isUploading = ref(false)
22
+ const progress = ref(0)
23
+ const isDone = ref(false)
24
+
25
+ function reset() {
26
+ remoteFilename.value = ''
27
+ isUploading.value = false
28
+ progress.value = 0
29
+ isDone.value = false
30
+ if (fileInputEl.value) fileInputEl.value.value = ''
31
+ }
32
+
33
+ async function startUploadFile(file: File) {
34
+ remoteFilename.value = ''
35
+
36
+ xhr = new XMLHttpRequest()
37
+
38
+ // Progress bar
39
+ xhr.upload.addEventListener('progress', (evt) => {
40
+ if (evt.loaded === evt.total) {
41
+ // Upload done
42
+ progress.value = 100
43
+ return
44
+ }
45
+
46
+ const fileSize = file.size
47
+ if (evt.loaded <= fileSize) {
48
+ // Uploading
49
+ progress.value = Math.round((evt.loaded / fileSize) * 100)
50
+ }
51
+ })
52
+
53
+ // Create form data for upload
54
+ const formData = new FormData()
55
+ formData.append('file', file)
56
+ formData.append('token', user.token)
57
+
58
+ // Start upload
59
+ xhr.open('post', apiUrl.value)
60
+ xhr.send(formData)
61
+
62
+ // Update finished event
63
+ xhr.onload = () => {
64
+ const setErr = (msg: string): void => {
65
+ reset()
66
+ isUploading.value = false
67
+ alert(`File upload failed: ${msg}`)
68
+ }
69
+
70
+ if (!xhr) {
71
+ setErr('xhr instance is null')
72
+ return
73
+ }
74
+
75
+ const ok = xhr.status >= 200 && xhr.status <= 299
76
+ if (!ok) {
77
+ setErr(`Response HTTP Code: ${xhr.status}, Body: ${xhr.response}`)
78
+ return
79
+ }
80
+
81
+ let json: any
82
+ try {
83
+ json = JSON.parse(xhr.response)
84
+ } catch (err) {
85
+ console.error(err)
86
+ setErr(`JSON parse error: ${err}`)
87
+ return
88
+ }
89
+
90
+ if (!json.filename) {
91
+ setErr(`Response filename is empty: ${xhr.response}`)
92
+ return
93
+ }
94
+
95
+ isDone.value = true
96
+ remoteFilename.value = json.filename
97
+ isUploading.value = false
98
+ emit('done', remoteFilename.value)
99
+ }
100
+ }
101
+
102
+ function onFileInputChange() {
103
+ const files = fileInputEl.value?.files
104
+ if (!files || files.length === 0) return
105
+
106
+ isUploading.value = true
107
+ setTimeout(async () => {
108
+ await startUploadFile(files[0])
109
+ isUploading.value = false
110
+ }, 80)
111
+ }
112
+
113
+ function abortUpload() {
114
+ xhr?.abort()
115
+ reset()
116
+ isUploading.value = false
117
+ }
118
+ </script>
119
+
120
+ <template>
121
+ <div class="atk-file-upload-group">
122
+ <div v-show="!isUploading" class="atk-file-input-wrap atk-fade-in">
123
+ <input
124
+ ref="fileInputEl"
125
+ type="file"
126
+ name="AtkDataFile"
127
+ accept=".artrans"
128
+ @change="onFileInputChange()"
129
+ />
130
+ <div class="atk-desc">
131
+ <slot v-if="!isDone" name="tip"></slot>
132
+ <slot v-if="isDone" name="done-msg"></slot>
133
+ </div>
134
+ </div>
135
+ <div v-show="isUploading" class="atk-uploading-wrap atk-fade-in">
136
+ <div class="atk-progress">
137
+ <div class="atk-bar" :style="{ width: `${progress}%` }"></div>
138
+ </div>
139
+ <div class="atk-status">
140
+ {{ t('uploading') }}
141
+ <span class="atk-curt">{{ progress }}%</span>
142
+ ...
143
+ <span class="atk-abort" @click="abortUpload()">{{ t('cancel') }}</span>
144
+ </div>
145
+ </div>
146
+ </div>
147
+ </template>
148
+
149
+ <style scoped lang="scss"></style>
@@ -0,0 +1,130 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ initValue?: string
4
+ placeholder?: string
5
+ validator?: (value: string) => boolean
6
+ }>()
7
+
8
+ const emit = defineEmits<{
9
+ (evt: 'yes', value: string): boolean | void | Promise<boolean | void>
10
+ (evt: 'no', value: string): boolean | void | Promise<boolean | void>
11
+ (evt: 'close'): void
12
+ }>()
13
+
14
+ const inputEl = ref<HTMLInputElement | null>(null)
15
+ const inputVal = ref('')
16
+ const inputInvalid = ref(false)
17
+
18
+ const { t } = useI18n()
19
+
20
+ onMounted(() => {
21
+ inputVal.value = props.initValue || ''
22
+ window.setTimeout(() => inputEl.value?.focus(), 80)
23
+ })
24
+
25
+ function onInput() {
26
+ // Input value validator
27
+ if (props.validator) {
28
+ inputInvalid.value = props.validator(inputVal.value)
29
+ }
30
+ }
31
+
32
+ function onKeyUp(evt: KeyboardEvent) {
33
+ if (evt.key === 'Enter' || evt.keyCode === 13) {
34
+ // Press Enter to submit
35
+ evt.preventDefault()
36
+ submit('yes')
37
+ }
38
+ }
39
+
40
+ async function submit(type: 'yes' | 'no') {
41
+ if (type == 'yes' && inputInvalid.value) return
42
+
43
+ let isContinue: any = undefined
44
+
45
+ const callback = emit(type as any, inputVal.value)
46
+ if (callback instanceof (async () => {}).constructor) {
47
+ isContinue = await callback
48
+ } else {
49
+ isContinue = callback
50
+ }
51
+
52
+ if (isContinue === undefined || isContinue === true) {
53
+ emit('close')
54
+ }
55
+ }
56
+ </script>
57
+
58
+ <template>
59
+ <div class="atk-item-text-editor-layer">
60
+ <div class="atk-edit-form">
61
+ <input
62
+ ref="inputEl"
63
+ v-model="inputVal"
64
+ class="atk-main-input"
65
+ type="text"
66
+ :placeholder="props.placeholder || t('inputHint')"
67
+ autocomplete="off"
68
+ :class="{ 'atk-invalid': inputInvalid }"
69
+ @input="onInput()"
70
+ @keyup="onKeyUp"
71
+ />
72
+ </div>
73
+ <div class="atk-actions">
74
+ <div
75
+ class="atk-item atk-yes-btn"
76
+ :class="{ 'atk-disabled': inputInvalid }"
77
+ @click="submit('yes')"
78
+ >
79
+ <i class="atk-icon atk-icon-yes"></i>
80
+ </div>
81
+ <div class="atk-item atk-no-btn" @click="submit('no')">
82
+ <i class="atk-icon atk-icon-no"></i>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </template>
87
+
88
+ <style scoped lang="scss">
89
+ .atk-item-text-editor-layer {
90
+ z-index: 999;
91
+ background: var(--at-color-bg);
92
+ position: absolute;
93
+ left: 0;
94
+ top: 0;
95
+ width: 100%;
96
+ height: 100%;
97
+ display: flex;
98
+ flex-direction: row;
99
+ align-items: center;
100
+
101
+ .atk-edit-form {
102
+ flex: auto;
103
+ padding-left: 20px;
104
+
105
+ input {
106
+ font-size: 17px;
107
+ width: 100%;
108
+ padding: 3px 5px;
109
+ border: 0;
110
+ border-bottom: 1px solid var(--at-color-border);
111
+ outline: none;
112
+ background: transparent;
113
+
114
+ &.atk-invalid {
115
+ }
116
+
117
+ &:focus {
118
+ border-bottom-color: var(--at-color-main);
119
+ }
120
+ }
121
+ }
122
+
123
+ .atk-actions {
124
+ @extend .atk-list-btn-actions;
125
+
126
+ .atk-yes-btn.atk-disabled {
127
+ }
128
+ }
129
+ }
130
+ </style>
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ transparentBg?: boolean
4
+ timeout?: number
5
+ }>()
6
+
7
+ const showSpinner = ref(false)
8
+ let timer: number | undefined
9
+
10
+ onMounted(() => {
11
+ // spinner delay to prevent flash
12
+ // (no need to show spinner if loading is fast)
13
+ timer = window.setTimeout(() => {
14
+ showSpinner.value = true
15
+ timer = undefined
16
+ }, props.timeout || 700)
17
+ })
18
+
19
+ onUnmounted(() => {
20
+ window.clearTimeout(timer)
21
+ })
22
+ </script>
23
+
24
+ <template>
25
+ <div
26
+ class="atk-loading atk-fade-in"
27
+ :style="{ background: props.transparentBg ? 'transparent' : undefined }"
28
+ >
29
+ <div v-if="showSpinner" class="atk-loading-spinner">
30
+ <svg viewBox="25 25 50 50">
31
+ <circle cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10"></circle>
32
+ </svg>
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <style scoped lang="scss"></style>
@@ -0,0 +1,89 @@
1
+ <script setup lang="ts">
2
+ import { useNavStore } from '@/stores/nav'
3
+
4
+ const props = defineProps<{
5
+ apiUrl: string
6
+ reqParams: { [k: string]: string }
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ (evt: 'back'): void
11
+ }>()
12
+
13
+ const { t } = useI18n()
14
+
15
+ const logWrapEl = ref<HTMLElement | null>(null)
16
+
17
+ onMounted(() => {
18
+ // Create iframe element
19
+ const frameName = `f_${+new Date()}`
20
+ const $frame = document.createElement('iframe')
21
+ $frame.className = 'atk-iframe'
22
+ $frame.name = frameName
23
+ logWrapEl.value!.append($frame)
24
+
25
+ // on iframe done
26
+ $frame.onload = () => {
27
+ useNavStore().refreshSites()
28
+ }
29
+
30
+ // Crate temporary form for submitting and load iframe page
31
+ const $formTmp = document.createElement('form')
32
+ $formTmp.style.display = 'none'
33
+ $formTmp.setAttribute('method', 'post')
34
+ $formTmp.setAttribute('action', props.apiUrl)
35
+ $formTmp.setAttribute('target', frameName)
36
+
37
+ Object.entries(props.reqParams).forEach(([key, val]) => {
38
+ const $inputTmp = document.createElement('input')
39
+ $inputTmp.setAttribute('type', 'hidden')
40
+ $inputTmp.setAttribute('name', key)
41
+ $inputTmp.value = val
42
+ $formTmp.appendChild($inputTmp)
43
+ })
44
+
45
+ logWrapEl.value!.append($formTmp)
46
+ $formTmp.submit()
47
+ $formTmp.remove()
48
+ })
49
+
50
+ function back() {
51
+ emit('back')
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <div class="atk-log-wrap">
57
+ <div class="atk-log-back-btn" @click="back()">{{ t('back') }}</div>
58
+ <div ref="logWrapEl" class="atk-log"></div>
59
+ </div>
60
+ </template>
61
+
62
+ <style scoped lang="scss">
63
+ .atk-log-wrap {
64
+ margin-bottom: -40px;
65
+
66
+ .atk-log-back-btn {
67
+ display: inline-block;
68
+ padding: 5px 33px;
69
+ cursor: pointer;
70
+ user-select: none;
71
+ border-right: 1px solid var(--at-color-border);
72
+ border-left: 1px solid transparent;
73
+ &:hover {
74
+ background: var(--at-color-bg-grey);
75
+ }
76
+ }
77
+
78
+ .atk-log {
79
+ }
80
+
81
+ .atk-iframe {
82
+ width: 100%;
83
+ height: calc(100vh - 150px);
84
+ border: 0;
85
+ background: var(--at-color-bg-grey);
86
+ border: 3px solid #eee;
87
+ }
88
+ }
89
+ </style>
@@ -0,0 +1,171 @@
1
+ <script setup lang="ts">
2
+ import type { ArtalkType } from '@esershnr/artalk'
3
+ import { artalk } from '../global'
4
+
5
+ const props = defineProps<{
6
+ page: ArtalkType.PageData
7
+ }>()
8
+
9
+ const emit = defineEmits<{
10
+ (evt: 'close'): void
11
+ (evt: 'update', page: ArtalkType.PageData): void
12
+ (evt: 'remove', id: number): void
13
+ }>()
14
+
15
+ const { page } = toRefs(props)
16
+ const editFieldKey = ref<keyof ArtalkType.PageData | null>(null)
17
+ const editFieldVal = computed(() =>
18
+ String(editFieldKey.value ? page.value[editFieldKey.value!] || '' : ''),
19
+ )
20
+ const isLoading = ref(false)
21
+ const { t } = useI18n()
22
+
23
+ function editTitle() {
24
+ editFieldKey.value = 'title'
25
+ }
26
+
27
+ function editKey() {
28
+ editFieldKey.value = 'key'
29
+ }
30
+
31
+ async function editAdminOnly() {
32
+ isLoading.value = true
33
+ let p: ArtalkType.PageData
34
+ try {
35
+ p = (
36
+ await artalk!.ctx.getApi().pages.updatePage(page.value.id, {
37
+ ...page.value,
38
+ admin_only: !page.value.admin_only,
39
+ })
40
+ ).data
41
+ } catch (err: any) {
42
+ alert(err.message)
43
+ console.error(err)
44
+ return
45
+ } finally {
46
+ isLoading.value = false
47
+ }
48
+ emit('update', p)
49
+ }
50
+
51
+ async function sync() {
52
+ isLoading.value = true
53
+ let p: ArtalkType.PageData
54
+ try {
55
+ p = (await artalk!.ctx.getApi().pages.fetchPage(page.value.id)).data
56
+ } catch (err: any) {
57
+ alert(err.message)
58
+ console.error(err)
59
+ return
60
+ } finally {
61
+ isLoading.value = false
62
+ }
63
+ emit('update', p)
64
+ }
65
+
66
+ function del() {
67
+ const del = async () => {
68
+ isLoading.value = true
69
+ try {
70
+ await artalk!.ctx.getApi().pages.deletePage(page.value.id)
71
+ } catch (err: any) {
72
+ alert(err.message)
73
+ console.error(err)
74
+ return
75
+ } finally {
76
+ isLoading.value = false
77
+ }
78
+ emit('remove', page.value.id)
79
+ }
80
+ if (window.confirm(t('pageDeleteConfirm', { title: page.value.title || page.value.key }))) del()
81
+ }
82
+
83
+ function close() {
84
+ emit('close')
85
+ }
86
+
87
+ async function onFieldEditorYes(val: string) {
88
+ if (editFieldVal.value !== val) {
89
+ isLoading.value = true
90
+ let p: ArtalkType.PageData
91
+ try {
92
+ p = (
93
+ await artalk!.ctx.getApi().pages.updatePage(page.value.id, {
94
+ ...page.value,
95
+ [editFieldKey.value as any]: val,
96
+ })
97
+ ).data
98
+ } catch (err: any) {
99
+ alert(err.message)
100
+ console.error(err)
101
+ return false
102
+ } finally {
103
+ isLoading.value = false
104
+ }
105
+ emit('update', p)
106
+ }
107
+
108
+ editFieldKey.value = null
109
+ close()
110
+ return true
111
+ }
112
+
113
+ function onFiledEditorNo() {
114
+ editFieldKey.value = null
115
+ }
116
+ </script>
117
+
118
+ <template>
119
+ <div class="atk-page-edit-layer">
120
+ <div class="atk-page-main-actions">
121
+ <div class="atk-item atk-title-edit-btn" @click="editTitle()">
122
+ {{ t('editTitle') }}
123
+ </div>
124
+ <div class="atk-item atk-key-edit-btn" @click="editKey()">
125
+ {{ t('switchKey') }}
126
+ </div>
127
+ <div
128
+ class="atk-item atk-admin-only-btn"
129
+ :class="!page.admin_only ? 'atk-green' : 'atk-yellow'"
130
+ @click="editAdminOnly()"
131
+ >
132
+ {{ !page.admin_only ? t('commentAllowAll') : t('commentOnlyAdmin') }}
133
+ </div>
134
+ </div>
135
+ <div class="atk-page-actions">
136
+ <div class="atk-item atk-sync-btn" @click="sync()">
137
+ <i class="atk-icon atk-icon-sync"></i>
138
+ </div>
139
+ <div class="atk-item atk-del-btn" @click="del()">
140
+ <i class="atk-icon atk-icon-del"></i>
141
+ </div>
142
+ <div class="atk-item atk-close-btn" @click="close()">
143
+ <i class="atk-icon atk-icon-close"></i>
144
+ </div>
145
+ </div>
146
+ <LoadingLayer v-if="isLoading" style="z-index: 1000" />
147
+ <ItemTextEditor
148
+ v-if="!!editFieldKey"
149
+ :init-value="editFieldVal"
150
+ @yes="onFieldEditorYes"
151
+ @no="onFiledEditorNo"
152
+ />
153
+ </div>
154
+ </template>
155
+
156
+ <style scoped lang="scss">
157
+ .atk-page-edit-layer {
158
+ z-index: 9;
159
+ background: var(--at-color-bg);
160
+ position: absolute;
161
+ width: 100%;
162
+ height: 100%;
163
+ display: flex;
164
+ flex-direction: row;
165
+ align-items: center;
166
+ }
167
+
168
+ .atk-page-main-actions {
169
+ @extend .atk-list-text-actions;
170
+ }
171
+ </style>