@waline/client 2.10.0 → 2.11.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.
Files changed (48) hide show
  1. package/dist/component.mjs +1 -1
  2. package/dist/component.mjs.map +1 -1
  3. package/dist/legacy.umd.d.ts +14 -0
  4. package/dist/legacy.umd.js +1 -1
  5. package/dist/legacy.umd.js.map +1 -1
  6. package/dist/pageview.cjs +1 -1
  7. package/dist/pageview.cjs.map +1 -1
  8. package/dist/pageview.js +1 -1
  9. package/dist/pageview.js.map +1 -1
  10. package/dist/pageview.mjs +1 -1
  11. package/dist/pageview.mjs.map +1 -1
  12. package/dist/shim.cjs +1 -1
  13. package/dist/shim.cjs.map +1 -1
  14. package/dist/shim.d.cts +14 -0
  15. package/dist/shim.d.mts +14 -0
  16. package/dist/shim.mjs +1 -1
  17. package/dist/shim.mjs.map +1 -1
  18. package/dist/waline.cjs +1 -1
  19. package/dist/waline.cjs.map +1 -1
  20. package/dist/waline.css +1 -1
  21. package/dist/waline.css.map +1 -1
  22. package/dist/waline.d.cts +14 -0
  23. package/dist/waline.d.mts +14 -0
  24. package/dist/waline.d.ts +14 -0
  25. package/dist/waline.js +1 -1
  26. package/dist/waline.js.map +1 -1
  27. package/dist/waline.mjs +1 -1
  28. package/dist/waline.mjs.map +1 -1
  29. package/package.json +5 -5
  30. package/src/components/ArticleReaction.vue +141 -0
  31. package/src/components/Waline.vue +11 -0
  32. package/src/composables/vote.ts +17 -0
  33. package/src/config/default.ts +9 -0
  34. package/src/config/i18n/en.ts +1 -0
  35. package/src/config/i18n/generate.ts +1 -0
  36. package/src/config/i18n/jp.ts +1 -0
  37. package/src/config/i18n/pt-BR.ts +1 -0
  38. package/src/config/i18n/ru.ts +1 -0
  39. package/src/config/i18n/vi-VN.ts +1 -0
  40. package/src/config/i18n/zh-CN.ts +1 -0
  41. package/src/config/i18n/zh-TW.ts +1 -0
  42. package/src/styles/card.scss +3 -1
  43. package/src/styles/index.scss +1 -0
  44. package/src/styles/reaction.scss +66 -0
  45. package/src/typings/locale.ts +10 -0
  46. package/src/typings/waline.ts +6 -1
  47. package/src/utils/config.ts +5 -1
  48. package/src/utils/fetch.ts +48 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waline/client",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "client for waline comment system",
5
5
  "keywords": [
6
6
  "valine",
@@ -111,16 +111,16 @@
111
111
  "@rollup/plugin-replace": "4.0.0",
112
112
  "@types/autosize": "4.0.1",
113
113
  "@types/marked": "4.0.7",
114
- "@types/node": "18.7.18",
114
+ "@types/node": "18.7.21",
115
115
  "@vitejs/plugin-vue": "3.1.0",
116
- "recaptcha-v3": "^1.10.0",
116
+ "recaptcha-v3": "1.10.0",
117
117
  "rimraf": "3.0.2",
118
- "rollup": "2.79.0",
118
+ "rollup": "2.79.1",
119
119
  "rollup-plugin-dts": "4.2.2",
120
120
  "rollup-plugin-terser": "7.0.2",
121
121
  "rollup-plugin-ts": "3.0.2",
122
122
  "typescript": "4.8.3",
123
- "vite": "3.1.2"
123
+ "vite": "3.1.3"
124
124
  },
125
125
  "engines": {
126
126
  "node": ">=14"
@@ -0,0 +1,141 @@
1
+ <template>
2
+ <div v-if="reaction && reaction.length" class="wl-reaction">
3
+ <h4>{{ locale.reactionTitle }}</h4>
4
+ <ul>
5
+ <li
6
+ v-for="(item, index) in reaction"
7
+ :key="index"
8
+ :class="item.active ? 'active' : ''"
9
+ @click="onVote(index)"
10
+ >
11
+ <div class="wl-reaction__img">
12
+ <img :src="item.icon" :alt="item.desc" />
13
+ <div class="wl-reaction__votes">{{ item.vote }}</div>
14
+ </div>
15
+ <div class="wl-reaction__text">{{ item.desc }}</div>
16
+ </li>
17
+ </ul>
18
+ </div>
19
+ </template>
20
+
21
+ <script lang="ts">
22
+ import {
23
+ defineComponent,
24
+ inject,
25
+ ComputedRef,
26
+ computed,
27
+ onMounted,
28
+ onUnmounted,
29
+ ref,
30
+ } from 'vue';
31
+ import {
32
+ fetchArticleCounter,
33
+ updateArticleCounter,
34
+ WalineConfig,
35
+ } from '../utils';
36
+ import { useVoteStorage } from '../composables/vote';
37
+
38
+ interface ReactionItem {
39
+ icon: string;
40
+ vote: number;
41
+ desc: string;
42
+ active?: boolean;
43
+ }
44
+
45
+ export default defineComponent({
46
+ setup() {
47
+ const votes = ref<ReactionItem['vote'][]>([]);
48
+ const voteStorage = useVoteStorage();
49
+ const config = inject<ComputedRef<WalineConfig>>(
50
+ 'config'
51
+ ) as ComputedRef<WalineConfig>;
52
+ const locale = computed(() => config.value.locale);
53
+ const reaction = computed((): ReactionItem[] => {
54
+ const { path } = config.value;
55
+
56
+ if (!Array.isArray(config.value.reaction)) {
57
+ return [];
58
+ }
59
+
60
+ return config.value.reaction.map((icon, index) => ({
61
+ icon,
62
+ vote: votes.value[index] || 0,
63
+ desc: locale.value[`reaction${index}` as `reaction0`],
64
+ active: Boolean(
65
+ voteStorage.value.find(({ u, i }) => u === path && i === index)
66
+ ),
67
+ }));
68
+ });
69
+
70
+ const controller = new AbortController();
71
+
72
+ const fetchCounter = async (): Promise<void> => {
73
+ const { serverURL, lang, path, reaction } = config.value;
74
+
75
+ if (!Array.isArray(reaction)) {
76
+ return;
77
+ }
78
+
79
+ const resp = await fetchArticleCounter({
80
+ serverURL,
81
+ lang,
82
+ paths: [path],
83
+ type: reaction.map((_, k) => `reaction${k}`),
84
+ signal: controller.signal,
85
+ });
86
+
87
+ if (Array.isArray(resp) || typeof resp === 'number') {
88
+ return;
89
+ }
90
+
91
+ votes.value = reaction.map((_, k) => resp[`reaction${k}`]);
92
+ };
93
+
94
+ const onVote = async (index: number): Promise<void> => {
95
+ const { serverURL, lang, path } = config.value;
96
+ const hasVoted = voteStorage.value.find(({ u }) => u === path);
97
+ const hasVotedTheReaction = hasVoted && hasVoted.i === index;
98
+
99
+ if (hasVotedTheReaction) {
100
+ return;
101
+ }
102
+
103
+ await updateArticleCounter({
104
+ serverURL,
105
+ lang,
106
+ path,
107
+ type: `reaction${index}`,
108
+ });
109
+
110
+ votes.value[index] = (votes.value[index] || 0) + 1;
111
+ if (hasVoted) {
112
+ votes.value[hasVoted.i] = Math.max(votes.value[hasVoted.i] - 1, 0);
113
+ updateArticleCounter({
114
+ serverURL,
115
+ lang,
116
+ path,
117
+ type: `reaction${hasVoted.i}`,
118
+ action: 'desc',
119
+ });
120
+
121
+ hasVoted.i = index;
122
+ voteStorage.value = Array.from(voteStorage.value);
123
+ } else {
124
+ voteStorage.value = [...voteStorage.value, { u: path, i: index }];
125
+ }
126
+
127
+ if (voteStorage.value.length > 50)
128
+ voteStorage.value = voteStorage.value.slice(-50);
129
+ };
130
+
131
+ onMounted(() => fetchCounter());
132
+ onUnmounted(() => controller.abort());
133
+
134
+ return {
135
+ reaction,
136
+ locale,
137
+ onVote,
138
+ };
139
+ },
140
+ });
141
+ </script>
@@ -1,5 +1,6 @@
1
1
  <template>
2
2
  <div data-waline>
3
+ <Reaction />
3
4
  <CommentBox v-if="!reply" @submit="onSubmit" />
4
5
  <div class="wl-meta-head">
5
6
  <div class="wl-count">
@@ -81,6 +82,7 @@
81
82
  <script lang="ts">
82
83
  import { useStyleTag } from '@vueuse/core';
83
84
  import { computed, defineComponent, onMounted, provide, ref, watch } from 'vue';
85
+ import Reaction from './ArticleReaction.vue';
84
86
  import CommentBox from './CommentBox.vue';
85
87
  import CommentCard from './CommentCard.vue';
86
88
  import { LoadingIcon } from './Icons';
@@ -237,12 +239,21 @@ const propsWithValidate = {
237
239
  },
238
240
 
239
241
  copyright: { type: Boolean, default: true },
242
+
243
+ recaptchav3key: {
244
+ type: String,
245
+ },
246
+
247
+ reaction: {
248
+ type: [Array, Boolean] as PropType<string[] | false>,
249
+ },
240
250
  };
241
251
 
242
252
  export default defineComponent({
243
253
  name: 'WalineRoot',
244
254
 
245
255
  components: {
256
+ Reaction,
246
257
  CommentBox,
247
258
  CommentCard,
248
259
  LoadingIcon,
@@ -0,0 +1,17 @@
1
+ import { useStorage } from '@vueuse/core';
2
+
3
+ import type { Ref } from 'vue';
4
+
5
+ const VOTE_KEY = 'WALINE_VOTE';
6
+
7
+ export interface VoteLogItem {
8
+ u: string;
9
+ i: number;
10
+ }
11
+
12
+ export type VoteRef = Ref<VoteLogItem[]>;
13
+
14
+ let voteStorage: VoteRef | null = null;
15
+
16
+ export const useVoteStorage = (): VoteRef =>
17
+ voteStorage || (voteStorage = useStorage<VoteLogItem[]>(VOTE_KEY, []));
@@ -115,3 +115,12 @@ export const getDefaultSearchOptions = (): WalineSearchOptions => {
115
115
  }),
116
116
  };
117
117
  };
118
+
119
+ export const defaultReaction = [
120
+ '//unpkg.com/@waline/emojis/tieba/tieba_agree.png',
121
+ '//unpkg.com/@waline/emojis/tieba/tieba_look_down.png',
122
+ '//unpkg.com/@waline/emojis/tieba/tieba_sunglasses.png',
123
+ '//unpkg.com/@waline/emojis/tieba/tieba_pick_nose.png',
124
+ '//unpkg.com/@waline/emojis/tieba/tieba_awkward.png',
125
+ '//unpkg.com/@waline/emojis/tieba/tieba_sleep.png',
126
+ ];
@@ -49,4 +49,5 @@ export default generateLocale([
49
49
  'Oldest',
50
50
  'Latest',
51
51
  'Hottest',
52
+ 'What do you think?',
52
53
  ]);
@@ -49,6 +49,7 @@ const localeKeys = [
49
49
  'oldest',
50
50
  'latest',
51
51
  'hottest',
52
+ 'reactionTitle',
52
53
  ];
53
54
 
54
55
  export const generateLocale = (locale: string[]): WalineLocale =>
@@ -49,4 +49,5 @@ export default generateLocale([
49
49
  '逆順',
50
50
  '正順',
51
51
  '人気順',
52
+ 'どう思いますか?',
52
53
  ]);
@@ -49,4 +49,5 @@ export default generateLocale([
49
49
  'Mais velho',
50
50
  'Mais recentes',
51
51
  'Mais quente',
52
+ 'O que você acha?',
52
53
  ]);
@@ -49,4 +49,5 @@ export default generateLocale([
49
49
  'самый старый',
50
50
  'последний',
51
51
  'самый горячий',
52
+ 'Что вы думаете?',
52
53
  ]);
@@ -49,4 +49,5 @@ export default generateLocale([
49
49
  'lâu đời nhất',
50
50
  'muộn nhất',
51
51
  'nóng nhất',
52
+ 'What do you think?',
52
53
  ]);
@@ -49,4 +49,5 @@ export default generateLocale([
49
49
  '按倒序',
50
50
  '按正序',
51
51
  '按热度',
52
+ '你认为这篇文章怎么样?',
52
53
  ]);
@@ -49,4 +49,5 @@ export default generateLocale([
49
49
  '按倒序',
50
50
  '按正序',
51
51
  '按熱度',
52
+ '你認為這篇文章怎麼樣?',
52
53
  ]);
@@ -57,6 +57,7 @@
57
57
 
58
58
  .wl-head {
59
59
  line-height: 1.5;
60
+ overflow: hidden; // bfc to fix https://github.com/walinejs/waline/issues/1415
60
61
  }
61
62
 
62
63
  .wl-nick {
@@ -126,6 +127,7 @@
126
127
 
127
128
  .wl-comment-actions {
128
129
  float: right;
130
+ line-height: 1;
129
131
  }
130
132
 
131
133
  .wl-delete,
@@ -135,7 +137,7 @@
135
137
  display: inline-flex;
136
138
  align-items: center;
137
139
 
138
- padding: 4px;
140
+ // padding: 4px;
139
141
  border: none;
140
142
 
141
143
  background: transparent;
@@ -13,3 +13,4 @@
13
13
  @use 'highlight';
14
14
 
15
15
  @use 'recent';
16
+ @use 'reaction';
@@ -0,0 +1,66 @@
1
+ .wl-reaction {
2
+ text-align: center;
3
+ margin-bottom: 1.75em;
4
+
5
+ ul {
6
+ margin: 0;
7
+ list-style-type: none;
8
+ display: flex;
9
+ flex-direction: row;
10
+ justify-content: center;
11
+ gap: 16px;
12
+ }
13
+
14
+ li {
15
+ cursor: pointer;
16
+ display: flex;
17
+ flex-direction: column;
18
+ align-items: center;
19
+
20
+ &:hover img,
21
+ &.active img {
22
+ transform: scale(1.15);
23
+ }
24
+ }
25
+
26
+ li.active .wl-reaction {
27
+ &__votes {
28
+ color: var(--waline-bgcolor);
29
+ background: var(--waline-theme-color);
30
+ }
31
+ &__text {
32
+ color: var(--waline-theme-color);
33
+ }
34
+ }
35
+
36
+ img {
37
+ width: 100%;
38
+ height: 100%;
39
+ transition: all 250ms ease-in-out;
40
+ }
41
+
42
+ &__img {
43
+ position: relative;
44
+ width: 42px;
45
+ height: 42px;
46
+ }
47
+
48
+ &__votes {
49
+ position: absolute;
50
+ top: -4px;
51
+ right: -5px;
52
+ font-size: 0.75em;
53
+ color: var(--waline-theme-color);
54
+ background: var(--waline-bgcolor);
55
+ border: 1px solid var(--waline-theme-color);
56
+ padding: 2px;
57
+ border-radius: 1em;
58
+ line-height: 1;
59
+ min-width: 1em;
60
+ font-weight: 700;
61
+ }
62
+
63
+ &__text {
64
+ font-size: 0.875em;
65
+ }
66
+ }
@@ -46,4 +46,14 @@ export interface WalineLocale extends WalineDateLocale, WalineLevelLocale {
46
46
  oldest: string;
47
47
  latest: string;
48
48
  hottest: string;
49
+ reactionTitle: string;
50
+ reaction0: string;
51
+ reaction1: string;
52
+ reaction2: string;
53
+ reaction3: string;
54
+ reaction4: string;
55
+ reaction5: string;
56
+ reaction6: string;
57
+ reaction7: string;
58
+ reaction8: string;
49
59
  }
@@ -217,5 +217,10 @@ export interface WalineProps {
217
217
  /**
218
218
  * recaptcha v3 client key
219
219
  */
220
- recaptchaV3Key?: string;
220
+ recaptchaV3Key?: string;
221
+
222
+ /**
223
+ * reaction
224
+ */
225
+ reaction?: string[] | boolean;
221
226
  }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  defaultLang,
3
3
  defaultLocales,
4
+ defaultReaction,
4
5
  defaultUploadImage,
5
6
  defaultHighlighter,
6
7
  defaultTexRenderer,
@@ -22,7 +23,8 @@ export interface WalineEmojiConfig {
22
23
  map: WalineEmojiMaps;
23
24
  }
24
25
 
25
- export interface WalineConfig extends Required<Omit<WalineProps, 'wordLimit' | 'recaptchaV3Key'>> {
26
+ export interface WalineConfig
27
+ extends Required<Omit<WalineProps, 'wordLimit' | 'recaptchaV3Key'>> {
26
28
  locale: WalineLocale;
27
29
  wordLimit: [number, number] | false;
28
30
  // emoji: Promise<EmojiConfig>;
@@ -63,6 +65,7 @@ export const getConfig = ({
63
65
  copyright = true,
64
66
  login = 'enable',
65
67
  search = getDefaultSearchOptions(),
68
+ reaction,
66
69
  ...more
67
70
  }: WalineProps): WalineConfig => ({
68
71
  serverURL: getServerURL(serverURL),
@@ -84,5 +87,6 @@ export const getConfig = ({
84
87
  login,
85
88
  copyright,
86
89
  search,
90
+ reaction: reaction === true ? defaultReaction : reaction || false,
87
91
  ...more,
88
92
  });
@@ -13,8 +13,7 @@ const JSON_HEADERS: Record<string, string> = {
13
13
  const errorCheck = <T = unknown>(data: T | FetchErrorData, name = ''): T => {
14
14
  if (typeof data === 'object' && (data as FetchErrorData).errno)
15
15
  throw new TypeError(
16
- `Fetch ${name} failed with ${(data as FetchErrorData).errno}: ${
17
- (data as FetchErrorData).errmsg
16
+ `Fetch ${name} failed with ${(data as FetchErrorData).errno}: ${(data as FetchErrorData).errmsg
18
17
  }`
19
18
  );
20
19
 
@@ -232,45 +231,80 @@ export const updateComment = ({
232
231
  }).then((resp) => resp.json() as Promise<void>);
233
232
  };
234
233
 
235
- export interface FetchPageviewsOptions {
234
+ export interface FetchArticleCounterOptions {
236
235
  serverURL: string;
237
236
  lang: string;
238
237
  paths: string[];
238
+ type: string[];
239
239
  signal: AbortSignal;
240
240
  }
241
241
 
242
- export const fetchPageviews = ({
242
+ export const fetchArticleCounter = ({
243
243
  serverURL,
244
244
  lang,
245
245
  paths,
246
+ type,
246
247
  signal,
247
- }: FetchPageviewsOptions): Promise<number[]> =>
248
+ }: FetchArticleCounterOptions): Promise<
249
+ Record<string, number>[] | Record<string, number> | number[] | number
250
+ > =>
248
251
  fetch(
249
252
  `${serverURL}/article?path=${encodeURIComponent(
250
253
  paths.join(',')
251
- )}&lang=${lang}`,
254
+ )}&type=${encodeURIComponent(type.join(','))}&lang=${lang}`,
252
255
  { signal }
253
256
  )
254
- .then((resp) => resp.json() as Promise<number[] | number>)
255
- .then((data) => errorCheck(data, 'visit count'))
257
+ .then(
258
+ (resp) =>
259
+ resp.json() as Promise<Record<string, number>[] | number[] | number>
260
+ )
261
+ .then((data) => errorCheck(data, 'article count'));
262
+
263
+ export const fetchPageviews = ({
264
+ serverURL,
265
+ lang,
266
+ paths,
267
+ signal,
268
+ }: Omit<FetchArticleCounterOptions, 'type'>): Promise<number[]> =>
269
+ fetchArticleCounter({
270
+ serverURL,
271
+ lang,
272
+ paths,
273
+ type: ['time'],
274
+ signal,
275
+ })
256
276
  // TODO: Improve this API
257
- .then((counts) => (Array.isArray(counts) ? counts : [counts]));
277
+ .then((counts) => (Array.isArray(counts) ? counts : [counts])) as Promise<
278
+ number[]
279
+ >;
258
280
 
259
- export interface UpdatePageviewsOptions {
281
+ export interface UpdateArticleCounterOptions {
260
282
  serverURL: string;
261
283
  lang: string;
262
284
  path: string;
285
+ type: string;
286
+ action?: 'inc' | 'desc';
263
287
  }
264
288
 
265
- export const updatePageviews = ({
289
+ export const updateArticleCounter = ({
266
290
  serverURL,
267
291
  lang,
268
292
  path,
269
- }: UpdatePageviewsOptions): Promise<number> =>
293
+ type,
294
+ action,
295
+ }: UpdateArticleCounterOptions): Promise<number> =>
270
296
  fetch(`${serverURL}/article?lang=${lang}`, {
271
297
  method: 'POST',
272
298
  headers: JSON_HEADERS,
273
- body: JSON.stringify({ path }),
299
+ body: JSON.stringify({ path, type, action }),
274
300
  })
275
301
  .then((resp) => resp.json() as Promise<number>)
276
- .then((data) => errorCheck(data, 'visit count'));
302
+ .then((data) => errorCheck(data, 'article count'));
303
+
304
+ export const updatePageviews = (
305
+ options: Omit<UpdateArticleCounterOptions, 'type'>
306
+ ): Promise<number> =>
307
+ updateArticleCounter({
308
+ ...options,
309
+ type: 'time',
310
+ });