header.tsx 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. 'use client';
  2. import {
  3. Settings,
  4. Sun,
  5. Moon,
  6. Monitor,
  7. ArrowLeft,
  8. Loader2,
  9. Download,
  10. FileDown,
  11. Package,
  12. } from 'lucide-react';
  13. import { useI18n } from '@/lib/hooks/use-i18n';
  14. import { useTheme } from '@/lib/hooks/use-theme';
  15. import { LanguageSwitcher } from './language-switcher';
  16. import { useState, useEffect, useRef, useCallback } from 'react';
  17. import { useRouter } from 'next/navigation';
  18. import { SettingsDialog } from './settings';
  19. import { cn } from '@/lib/utils';
  20. import { useStageStore } from '@/lib/store/stage';
  21. import { useMediaGenerationStore } from '@/lib/store/media-generation';
  22. import { useExportPPTX } from '@/lib/export/use-export-pptx';
  23. interface HeaderProps {
  24. readonly currentSceneTitle: string;
  25. }
  26. export function Header({ currentSceneTitle }: HeaderProps) {
  27. const { t } = useI18n();
  28. const { theme, setTheme } = useTheme();
  29. const router = useRouter();
  30. const [settingsOpen, setSettingsOpen] = useState(false);
  31. const [themeOpen, setThemeOpen] = useState(false);
  32. // Export
  33. const { exporting: isExporting, exportPPTX, exportResourcePack } = useExportPPTX();
  34. const [exportMenuOpen, setExportMenuOpen] = useState(false);
  35. const exportRef = useRef<HTMLDivElement>(null);
  36. const scenes = useStageStore((s) => s.scenes);
  37. const generatingOutlines = useStageStore((s) => s.generatingOutlines);
  38. const failedOutlines = useStageStore((s) => s.failedOutlines);
  39. const mediaTasks = useMediaGenerationStore((s) => s.tasks);
  40. const canExport =
  41. scenes.length > 0 &&
  42. generatingOutlines.length === 0 &&
  43. failedOutlines.length === 0 &&
  44. Object.values(mediaTasks).every((task) => task.status === 'done' || task.status === 'failed');
  45. const themeRef = useRef<HTMLDivElement>(null);
  46. // Close dropdown when clicking outside
  47. const handleClickOutside = useCallback(
  48. (e: MouseEvent) => {
  49. if (themeOpen && themeRef.current && !themeRef.current.contains(e.target as Node)) {
  50. setThemeOpen(false);
  51. }
  52. if (exportMenuOpen && exportRef.current && !exportRef.current.contains(e.target as Node)) {
  53. setExportMenuOpen(false);
  54. }
  55. },
  56. [themeOpen, exportMenuOpen],
  57. );
  58. useEffect(() => {
  59. if (themeOpen || exportMenuOpen) {
  60. document.addEventListener('mousedown', handleClickOutside);
  61. return () => document.removeEventListener('mousedown', handleClickOutside);
  62. }
  63. }, [themeOpen, exportMenuOpen, handleClickOutside]);
  64. return (
  65. <>
  66. <header className="h-20 px-8 flex items-center justify-between z-10 bg-transparent gap-4">
  67. <div className="flex items-center gap-3 min-w-0 flex-1">
  68. <button
  69. onClick={() => router.push('/')}
  70. className="shrink-0 p-2 rounded-lg text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
  71. title={t('generation.backToHome')}
  72. >
  73. <ArrowLeft className="w-5 h-5" />
  74. </button>
  75. <div className="flex flex-col min-w-0">
  76. <span className="text-[10px] uppercase tracking-widest font-bold text-gray-400 dark:text-gray-500 mb-0.5">
  77. {t('stage.currentScene')}
  78. </span>
  79. <h1
  80. className="text-xl font-bold text-gray-800 dark:text-gray-200 tracking-tight truncate"
  81. suppressHydrationWarning
  82. >
  83. {currentSceneTitle || t('common.loading')}
  84. </h1>
  85. </div>
  86. </div>
  87. <div className="flex items-center gap-4 bg-white/60 dark:bg-gray-800/60 backdrop-blur-md px-2 py-1.5 rounded-full border border-gray-100/50 dark:border-gray-700/50 shadow-sm shrink-0">
  88. {/* Language Selector */}
  89. <LanguageSwitcher onOpen={() => setThemeOpen(false)} />
  90. <div className="w-[1px] h-4 bg-gray-200 dark:bg-gray-700" />
  91. {/* Theme Selector */}
  92. <div className="relative" ref={themeRef}>
  93. <button
  94. onClick={() => {
  95. setThemeOpen(!themeOpen);
  96. }}
  97. className="p-2 rounded-full text-gray-400 dark:text-gray-500 hover:bg-white dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:shadow-sm transition-all group"
  98. >
  99. {theme === 'light' && <Sun className="w-4 h-4" />}
  100. {theme === 'dark' && <Moon className="w-4 h-4" />}
  101. {theme === 'system' && <Monitor className="w-4 h-4" />}
  102. </button>
  103. {themeOpen && (
  104. <div className="absolute top-full mt-2 right-0 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden z-50 min-w-[140px]">
  105. <button
  106. onClick={() => {
  107. setTheme('light');
  108. setThemeOpen(false);
  109. }}
  110. className={cn(
  111. 'w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2',
  112. theme === 'light' &&
  113. 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
  114. )}
  115. >
  116. <Sun className="w-4 h-4" />
  117. {t('settings.themeOptions.light')}
  118. </button>
  119. <button
  120. onClick={() => {
  121. setTheme('dark');
  122. setThemeOpen(false);
  123. }}
  124. className={cn(
  125. 'w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2',
  126. theme === 'dark' &&
  127. 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
  128. )}
  129. >
  130. <Moon className="w-4 h-4" />
  131. {t('settings.themeOptions.dark')}
  132. </button>
  133. <button
  134. onClick={() => {
  135. setTheme('system');
  136. setThemeOpen(false);
  137. }}
  138. className={cn(
  139. 'w-full px-4 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2',
  140. theme === 'system' &&
  141. 'bg-purple-50 dark:bg-purple-900/20 text-purple-600 dark:text-purple-400',
  142. )}
  143. >
  144. <Monitor className="w-4 h-4" />
  145. {t('settings.themeOptions.system')}
  146. </button>
  147. </div>
  148. )}
  149. </div>
  150. <div className="w-[1px] h-4 bg-gray-200 dark:bg-gray-700" />
  151. {/* Settings Button */}
  152. <div className="relative">
  153. <button
  154. onClick={() => setSettingsOpen(true)}
  155. className="p-2 rounded-full text-gray-400 dark:text-gray-500 hover:bg-white dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:shadow-sm transition-all group"
  156. >
  157. <Settings className="w-4 h-4 group-hover:rotate-90 transition-transform duration-500" />
  158. </button>
  159. </div>
  160. </div>
  161. {/* Export Dropdown */}
  162. <div className="relative" ref={exportRef}>
  163. <button
  164. onClick={() => {
  165. if (canExport && !isExporting) setExportMenuOpen(!exportMenuOpen);
  166. }}
  167. disabled={!canExport || isExporting}
  168. title={
  169. canExport
  170. ? isExporting
  171. ? t('export.exporting')
  172. : t('export.pptx')
  173. : t('share.notReady')
  174. }
  175. className={cn(
  176. 'shrink-0 p-2 rounded-full transition-all',
  177. canExport && !isExporting
  178. ? 'text-gray-400 dark:text-gray-500 hover:bg-white dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-200 hover:shadow-sm'
  179. : 'text-gray-300 dark:text-gray-600 cursor-not-allowed opacity-50',
  180. )}
  181. >
  182. {isExporting ? (
  183. <Loader2 className="w-4 h-4 animate-spin" />
  184. ) : (
  185. <Download className="w-4 h-4" />
  186. )}
  187. </button>
  188. {exportMenuOpen && (
  189. <div className="absolute top-full mt-2 right-0 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden z-50 min-w-[200px]">
  190. <button
  191. onClick={() => {
  192. setExportMenuOpen(false);
  193. exportPPTX();
  194. }}
  195. className="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2.5"
  196. >
  197. <FileDown className="w-4 h-4 text-gray-400 shrink-0" />
  198. <span>{t('export.pptx')}</span>
  199. </button>
  200. <button
  201. onClick={() => {
  202. setExportMenuOpen(false);
  203. exportResourcePack();
  204. }}
  205. className="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors flex items-center gap-2.5"
  206. >
  207. <Package className="w-4 h-4 text-gray-400 shrink-0" />
  208. <div>
  209. <div>{t('export.resourcePack')}</div>
  210. <div className="text-[11px] text-gray-400 dark:text-gray-500">
  211. {t('export.resourcePackDesc')}
  212. </div>
  213. </div>
  214. </button>
  215. </div>
  216. )}
  217. </div>
  218. </header>
  219. <SettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
  220. </>
  221. );
  222. }