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;