master
1import { ArtalkProvider } from './recent-comments/Artalk';
2import { TwikooProvider } from './recent-comments/Twikoo';
3import { WalineProvider } from './recent-comments/Waline';
4import type { CommentData } from './recent-comments/types';
5import { asideConfig, commentConfig, siteConfig } from '@/config';
6import { t } from '@utils/i18n';
7import type { Component } from 'solid-js';
8import { For, Show, createSignal, onMount } from 'solid-js';
9
10const cacheKey = 'recent-comments-cache';
11const cacheExpireTime = 30 * 60 * 1000; // 30 min
12
13interface CacheInfo {
14 data: CommentData[];
15 timestamp: number;
16 provider: string;
17}
18
19function getFromCache(): CommentData[] | null {
20 try {
21 const cached = localStorage.getItem(cacheKey);
22 if (!cached) return null;
23
24 const tmp: CacheInfo = JSON.parse(cached);
25
26 const expired = Date.now() - tmp.timestamp > cacheExpireTime;
27 if (expired || tmp.provider !== commentConfig.provider) {
28 localStorage.removeItem(cacheKey);
29 return null;
30 }
31
32 return tmp.data.map((c) => ({ ...c, time: new Date(c.time) }));
33 } catch {
34 localStorage.removeItem(cacheKey);
35 return null;
36 }
37}
38
39function saveToCache(list: CommentData[]): void {
40 try {
41 const cacheObj: CacheInfo = {
42 data: list,
43 timestamp: Date.now(),
44 provider: commentConfig.provider,
45 };
46 localStorage.setItem(cacheKey, JSON.stringify(cacheObj));
47 } catch {
48 /* ignore */
49 }
50}
51
52const RecentComments: Component = () => {
53 const [comments, setComments] = createSignal<CommentData[]>([]);
54 const [loading, setLoading] = createSignal(true);
55
56 const load = async () => {
57 const cached = getFromCache();
58 if (cached) {
59 setComments(cached);
60 setLoading(false);
61 return;
62 }
63
64 const provider = (() => {
65 switch (commentConfig.provider) {
66 case 'twikoo':
67 return TwikooProvider;
68 case 'waline':
69 return WalineProvider;
70 case 'artalk':
71 return ArtalkProvider;
72 default:
73 throw new Error(
74 `Unsupported comment provider: '${commentConfig.provider}' for recent comments`
75 );
76 }
77 })();
78
79 try {
80 const raw = await provider.setup();
81 setComments(raw);
82 saveToCache(raw);
83 } catch (e) {
84 console.error('Failed to load recent comments:', e);
85 setComments([]);
86 } finally {
87 setLoading(false);
88 }
89 };
90
91 onMount(load);
92
93 const refresh = () => {
94 localStorage.removeItem(cacheKey);
95 setLoading(true);
96 load();
97 };
98
99 return (
100 <div id="recent-comments-card" class="card border-base-300 bg-base-200 border">
101 <div class="card-body px-4 py-2">
102 <div class="card-title flex justify-between">
103 <span>{t.info.recentComments()}</span>
104 <button
105 class="btn btn-ghost btn-sm btn-square text-base"
106 disabled={loading()}
107 title={t.common.refresh()}
108 aria-label={t.common.refresh()}
109 onClick={refresh}
110 >
111 ↻
112 </button>
113 </div>
114
115 <ul class="list">
116 <Show when={loading()}>
117 <For each={Array.from({ length: asideConfig.recentComment.count })}>
118 {(_, _idx) => (
119 <li class="list-row comment-placeholder px-0">
120 <div class="avatar">
121 <div class="skeleton w-16 min-w-16 rounded-md" />
122 </div>
123 <div class="flex w-full flex-col justify-between">
124 <div class="flex flex-col gap-2">
125 <div class="skeleton h-4 w-full" />
126 <div class="skeleton h-4 w-[100%-2rem]" />
127 </div>
128 <div class="skeleton h-4 w-10" />
129 </div>
130 </li>
131 )}
132 </For>
133 </Show>
134
135 <Show when={!loading()}>
136 <For each={comments()}>
137 {(item) => (
138 <li class="list-row px-0">
139 <a class="avatar" href={item.commentUrl}>
140 <div class="w-16 min-w-16 rounded-md">
141 <img src={item.avatarUrl} alt={item.author} />
142 </div>
143 </a>
144
145 <div class="flex w-full flex-col justify-between">
146 <a
147 href={item.commentUrl}
148 class="hover:text-primary line-clamp-2 w-full overflow-clip"
149 innerHTML={item.commentContent}
150 />
151 <time
152 datetime={item.time.toISOString()}
153 class="text-base-content/60 text-xs"
154 textContent={item.time.toLocaleDateString(
155 siteConfig.lang.replace('_', '-'),
156 {
157 year: 'numeric',
158 month: '2-digit',
159 day: '2-digit',
160 }
161 )}
162 />
163 </div>
164 </li>
165 )}
166 </For>
167 </Show>
168 </ul>
169 </div>
170 </div>
171 );
172};
173
174export default RecentComments;