general-settings.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. 'use client';
  2. import { useState, useCallback } from 'react';
  3. import { Label } from '@/components/ui/label';
  4. import { Input } from '@/components/ui/input';
  5. import { Button } from '@/components/ui/button';
  6. import {
  7. AlertDialog,
  8. AlertDialogContent,
  9. AlertDialogHeader,
  10. AlertDialogTitle,
  11. AlertDialogDescription,
  12. AlertDialogFooter,
  13. AlertDialogCancel,
  14. } from '@/components/ui/alert-dialog';
  15. import { Loader2, Trash2, AlertTriangle } from 'lucide-react';
  16. import { useI18n } from '@/lib/hooks/use-i18n';
  17. import { clearDatabase } from '@/lib/utils/database';
  18. import { toast } from 'sonner';
  19. import { createLogger } from '@/lib/logger';
  20. const log = createLogger('GeneralSettings');
  21. export function GeneralSettings() {
  22. const { t } = useI18n();
  23. // Clear cache state
  24. const [showClearDialog, setShowClearDialog] = useState(false);
  25. const [confirmInput, setConfirmInput] = useState('');
  26. const [clearing, setClearing] = useState(false);
  27. const confirmPhrase = t('settings.clearCacheConfirmPhrase');
  28. const isConfirmValid = confirmInput === confirmPhrase;
  29. const handleClearCache = useCallback(async () => {
  30. if (!isConfirmValid) return;
  31. setClearing(true);
  32. try {
  33. // 1. Clear IndexedDB
  34. await clearDatabase();
  35. // 2. Clear localStorage
  36. localStorage.clear();
  37. // 3. Clear sessionStorage
  38. sessionStorage.clear();
  39. toast.success(t('settings.clearCacheSuccess'));
  40. // Reload page after a short delay
  41. setTimeout(() => {
  42. window.location.reload();
  43. }, 1000);
  44. } catch (error) {
  45. log.error('Failed to clear cache:', error);
  46. toast.error(t('settings.clearCacheFailed'));
  47. setClearing(false);
  48. }
  49. }, [isConfirmValid, t]);
  50. const clearCacheItems =
  51. t('settings.clearCacheConfirmItems').split('、').length > 1
  52. ? t('settings.clearCacheConfirmItems').split('、')
  53. : t('settings.clearCacheConfirmItems').split(', ');
  54. return (
  55. <div className="flex flex-col gap-8">
  56. {/* Danger Zone - Clear Cache */}
  57. <div className="relative rounded-xl border border-destructive/30 bg-destructive/[0.03] dark:bg-destructive/[0.06] overflow-hidden">
  58. {/* Subtle diagonal stripe pattern for danger emphasis */}
  59. <div
  60. className="absolute inset-0 opacity-[0.015] dark:opacity-[0.03] pointer-events-none"
  61. style={{
  62. backgroundImage: `repeating-linear-gradient(
  63. -45deg,
  64. transparent,
  65. transparent 10px,
  66. currentColor 10px,
  67. currentColor 11px
  68. )`,
  69. }}
  70. />
  71. <div className="relative p-4 space-y-4">
  72. {/* Header */}
  73. <div className="flex items-center gap-2.5">
  74. <div className="p-1.5 rounded-md bg-destructive/10 text-destructive">
  75. <AlertTriangle className="w-4 h-4" />
  76. </div>
  77. <h3 className="text-sm font-semibold text-destructive">{t('settings.dangerZone')}</h3>
  78. </div>
  79. {/* Content */}
  80. <div className="flex items-center justify-between gap-4">
  81. <div className="flex-1 min-w-0">
  82. <p className="text-sm font-medium">{t('settings.clearCache')}</p>
  83. <p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">
  84. {t('settings.clearCacheDescription')}
  85. </p>
  86. </div>
  87. <Button
  88. variant="destructive"
  89. size="sm"
  90. className="shrink-0"
  91. onClick={() => {
  92. setConfirmInput('');
  93. setShowClearDialog(true);
  94. }}
  95. >
  96. <Trash2 className="w-3.5 h-3.5 mr-1.5" />
  97. {t('settings.clearCache')}
  98. </Button>
  99. </div>
  100. </div>
  101. </div>
  102. {/* Clear Cache Confirmation Dialog */}
  103. <AlertDialog
  104. open={showClearDialog}
  105. onOpenChange={(open) => {
  106. if (!clearing) {
  107. setShowClearDialog(open);
  108. if (!open) setConfirmInput('');
  109. }
  110. }}
  111. >
  112. <AlertDialogContent>
  113. <AlertDialogHeader>
  114. <AlertDialogTitle className="flex items-center gap-2 text-destructive">
  115. <AlertTriangle className="w-5 h-5" />
  116. {t('settings.clearCacheConfirmTitle')}
  117. </AlertDialogTitle>
  118. <AlertDialogDescription asChild>
  119. <div className="space-y-3">
  120. <p>{t('settings.clearCacheConfirmDescription')}</p>
  121. <ul className="space-y-1.5 ml-1">
  122. {clearCacheItems.map((item, i) => (
  123. <li key={i} className="flex items-center gap-2 text-sm">
  124. <span className="w-1.5 h-1.5 rounded-full bg-destructive/60 shrink-0" />
  125. {item.trim()}
  126. </li>
  127. ))}
  128. </ul>
  129. <div className="pt-1">
  130. <Label className="text-xs font-medium text-foreground">
  131. {t('settings.clearCacheConfirmInput')}
  132. </Label>
  133. <Input
  134. className="mt-1.5 h-9 text-sm"
  135. placeholder={confirmPhrase}
  136. value={confirmInput}
  137. onChange={(e) => setConfirmInput(e.target.value)}
  138. onKeyDown={(e) => {
  139. if (e.key === 'Enter' && isConfirmValid) {
  140. handleClearCache();
  141. }
  142. }}
  143. autoFocus
  144. />
  145. </div>
  146. </div>
  147. </AlertDialogDescription>
  148. </AlertDialogHeader>
  149. <AlertDialogFooter>
  150. <AlertDialogCancel disabled={clearing}>{t('common.cancel')}</AlertDialogCancel>
  151. <Button
  152. variant="destructive"
  153. disabled={!isConfirmValid || clearing}
  154. onClick={handleClearCache}
  155. >
  156. {clearing ? (
  157. <Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
  158. ) : (
  159. <Trash2 className="w-4 h-4 mr-1.5" />
  160. )}
  161. {t('settings.clearCacheButton')}
  162. </Button>
  163. </AlertDialogFooter>
  164. </AlertDialogContent>
  165. </AlertDialog>
  166. </div>
  167. );
  168. }