StoicQuote.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { createSignal, onMount, onCleanup } from "solid-js";
  2. import { getRefreshInterval } from "../utils/timeUtils";
  3. import type { WidgetSchema, WidgetSettings } from "../types/widget";
  4. interface StoicQuoteProps {
  5. settings: WidgetSettings;
  6. }
  7. interface Quote {
  8. text: string;
  9. author: string;
  10. }
  11. // Cache to prevent spam and persist across re-renders
  12. const quoteCooldowns = new Map<string, number>();
  13. const quoteCache = new Map<string, Quote>();
  14. export function StoicQuote(props: StoicQuoteProps) {
  15. const widgetKey = "stoic-quote";
  16. const COOLDOWN_MS = 10000; // 10 second cooldown
  17. // Initialize quote from cache or default
  18. const initialQuote = quoteCache.get(widgetKey) || {
  19. text: "Loading wisdom...",
  20. author: "",
  21. };
  22. const [quote, setQuote] = createSignal<Quote>(initialQuote);
  23. const [error, setError] = createSignal<string>("");
  24. const [renderKey, setRenderKey] = createSignal<number>(0);
  25. const fetchQuote = async (bypassCooldown: boolean = false) => {
  26. const now = Date.now();
  27. const lastFetch = quoteCooldowns.get(widgetKey) || 0;
  28. // Cooldown check - only for manual refreshes, not interval refreshes
  29. if (!bypassCooldown && now - lastFetch < COOLDOWN_MS) {
  30. console.log(`[StoicQuote] Cooldown active, skipping manual refresh`);
  31. return;
  32. }
  33. quoteCooldowns.set(widgetKey, now);
  34. console.log("[StoicQuote] Fetching quote from API...");
  35. try {
  36. const response = await fetch("https://stoic-quotes.com/api/quote");
  37. if (!response.ok) {
  38. throw new Error("Failed to fetch quote");
  39. }
  40. const data = await response.json();
  41. const newQuote: Quote = {
  42. text: data.text,
  43. author: data.author,
  44. };
  45. console.log("[StoicQuote] ✨ New quote received:", {
  46. author: newQuote.author,
  47. preview: newQuote.text.substring(0, 50) + "...",
  48. timestamp: new Date().toLocaleTimeString(),
  49. });
  50. setQuote(newQuote);
  51. setError("");
  52. quoteCache.set(widgetKey, newQuote);
  53. setRenderKey((prev) => prev + 1); // Force re-render
  54. console.log(
  55. "[StoicQuote] ✅ Quote state updated, render key incremented, forcing re-render",
  56. );
  57. } catch (err) {
  58. console.error("[StoicQuote] Error:", err);
  59. setError("Failed to load quote");
  60. }
  61. };
  62. onMount(() => {
  63. // Load initial quote if not cached
  64. if (!quoteCache.has(widgetKey)) {
  65. fetchQuote(true); // Bypass cooldown on mount
  66. }
  67. // Refresh based on interval setting
  68. const refreshInterval = getRefreshInterval(props.settings, 1, "hours");
  69. console.log(`[StoicQuote] Setting up interval: ${refreshInterval}ms`);
  70. const intervalId = setInterval(() => {
  71. console.log("[StoicQuote] Interval tick - fetching new quote");
  72. fetchQuote(true); // Bypass cooldown for scheduled refreshes
  73. }, refreshInterval);
  74. onCleanup(() => {
  75. console.log("[StoicQuote] Cleaning up interval");
  76. clearInterval(intervalId);
  77. });
  78. });
  79. // Make these reactive by moving them inside the render
  80. const showAuthor = () => props.settings.showAuthor !== false;
  81. const fontSize = () => (props.settings.fontSize as string) || "medium";
  82. const getSizes = () => {
  83. const fontSizeMap = {
  84. small: {
  85. quote: "clamp(0.7rem, 1.5vw, 1.2rem)",
  86. author: "clamp(0.6rem, 1.2vw, 0.9rem)",
  87. },
  88. medium: {
  89. quote: "clamp(0.9rem, 2vw, 1.5rem)",
  90. author: "clamp(0.7rem, 1.5vw, 1.1rem)",
  91. },
  92. large: {
  93. quote: "clamp(1.1rem, 2.5vw, 2rem)",
  94. author: "clamp(0.8rem, 1.8vw, 1.3rem)",
  95. },
  96. };
  97. return (
  98. fontSizeMap[fontSize() as keyof typeof fontSizeMap] || fontSizeMap.medium
  99. );
  100. };
  101. return (
  102. <div
  103. data-render-key={renderKey()}
  104. style={{
  105. border: "1px solid var(--border)",
  106. padding: "1rem",
  107. width: "100%",
  108. height: "100%",
  109. "box-sizing": "border-box",
  110. display: "flex",
  111. "flex-direction": "column",
  112. "justify-content": "center",
  113. "align-items": "center",
  114. background: "var(--bg, #fff)",
  115. position: "relative",
  116. }}
  117. >
  118. {/* Stoic symbol - smaller and positioned absolutely */}
  119. <div
  120. style={{
  121. position: "absolute",
  122. top: "0.5rem",
  123. left: "0.5rem",
  124. "font-size": "1.5rem",
  125. "line-height": "1",
  126. color: "var(--border, #ccc)",
  127. "font-family": "Georgia, serif",
  128. "font-weight": "bold",
  129. opacity: "0.3",
  130. }}
  131. >
  132. Σ
  133. </div>
  134. {/* Quote text - direct child of flex container like Clock */}
  135. <div
  136. style={{
  137. "text-align": "center",
  138. "max-width": "95%",
  139. }}
  140. >
  141. {error() ? (
  142. <div style={{ color: "var(--gray, #999)" }}>{error()}</div>
  143. ) : (
  144. <>
  145. <div
  146. style={{
  147. "font-size": getSizes().quote,
  148. "line-height": "1.4",
  149. "font-style": "italic",
  150. }}
  151. >
  152. "{quote().text}"
  153. </div>
  154. {showAuthor() && quote().author && (
  155. <div
  156. style={{
  157. "margin-top": "0.5rem",
  158. "font-size": getSizes().author,
  159. color: "var(--gray, #666)",
  160. "font-weight": "500",
  161. }}
  162. >
  163. — {quote().author}
  164. </div>
  165. )}
  166. </>
  167. )}
  168. </div>
  169. {/* Stoic badge */}
  170. <div
  171. style={{
  172. position: "absolute",
  173. bottom: "8px",
  174. left: "8px",
  175. "font-size": "0.7rem",
  176. color: "var(--gray, #999)",
  177. "text-transform": "uppercase",
  178. "letter-spacing": "0.05em",
  179. opacity: "0.5",
  180. }}
  181. >
  182. Stoic Wisdom
  183. </div>
  184. </div>
  185. );
  186. }
  187. export const stoicQuoteSchema: WidgetSchema = {
  188. name: "Stoic Quote",
  189. description:
  190. "Display stoic wisdom from Marcus Aurelius, Seneca, and Epictetus",
  191. settingsSchema: {
  192. fontSize: {
  193. type: "select",
  194. label: "Font Size",
  195. options: ["small", "medium", "large"],
  196. default: "medium",
  197. },
  198. showAuthor: {
  199. type: "boolean",
  200. label: "Show Author",
  201. default: true,
  202. },
  203. refreshIntervalValue: {
  204. type: "number",
  205. label: "Refresh Interval",
  206. default: 1,
  207. required: true,
  208. },
  209. refreshIntervalUnit: {
  210. type: "select",
  211. label: "Unit",
  212. options: ["seconds", "minutes", "hours", "days"],
  213. default: "hours",
  214. required: true,
  215. },
  216. },
  217. };