import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { Play, RotateCcw, RotateCw, Trash2, Code, Layout, Plus, X, Folder, Settings, Terminal, Sun, Moon, Zap, Clock, Monitor, Tablet, Smartphone, Download, Upload, Menu, FileText, Trash, AlertTriangle, PanelRight, PanelTop, GripVertical, Keyboard, // --- 新增功能:匯入登入/登出圖示 --- LogIn, LogOut, User } from 'lucide-react'; // --- 新增功能:匯入 Firebase --- import { initializeApp } from "firebase/app"; import { getAuth, onAuthStateChanged, GoogleAuthProvider, signInWithPopup, signOut } from "firebase/auth"; import { getFirestore, collection, doc, setDoc, deleteDoc, onSnapshot, query, where } from "firebase/firestore"; // --- 新增功能:使用您提供的 Firebase 設定 --- const firebaseConfig = { apiKey: "AIzaSyDBJEKcGNpV4kCt81ziW0Z7zfEVKhpvYL0", authDomain: "codebox-pro.firebaseapp.com", projectId: "codebox-pro", storageBucket: "codebox-pro.firebasestorage.app", messagingSenderId: "1073960323519", appId: "1:1073960323519:web:8c36f1d1c1a8a5c645f90a", measurementId: "G-R9P5HNM7VM" }; // --- 新增功能:初始化 Firebase --- const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); // --- 預設程式碼內容 (已簡化) --- const initialContents = { html: ` 範例頁面

歡迎!

這是一個可調整大小的佈局。

`, css: `/* 基本 CSS 樣式 */ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; padding: 20px; background-color: #fff; transition: background-color 0.3s; } body.dark-mode { background-color: #222; color: #eee; } h1 { color: #333; } body.dark-mode h1 { color: #eee; } button { background-color: #007bff; color: white; border: none; padding: 10px 15px; border-radius: 5px; cursor: pointer; } button:hover { background-color: #0056b3; } `, js: `// 基本 JavaScript console.log("腳本已載入!"); document.addEventListener('DOMContentLoaded', () => { const button = document.getElementById('my-button'); if (button) { button.addEventListener('click', () => { console.log('按鈕被點擊了!'); document.body.classList.toggle('dark-mode'); }); } }); ` }; // --- 專案結構與歷史記錄管理 --- const MAX_HISTORY = 50; const createInitialProject = (id, name, content) => ({ id: id || crypto.randomUUID(), name: name, fileContents: content, history: { html: [content.html], css: [content.css], js: [content.js], }, historyIndex: { html: 0, css: 0, js: 0, }, // --- 新增功能:確保新專案有 libraries 屬性 --- libraries: { js: [], css: [] } }); /** * 核心 App 組件 */ const App = () => { const initialProjects = useMemo(() => [ createInitialProject('p1', '專案 1 (範例)', initialContents) ], []); // --- 狀態 --- // --- 新增功能:Firebase 驗證狀態 --- const [user, setUser] = useState(null); const [isAuthReady, setIsAuthReady] = useState(false); // 追蹤驗證是否已初始化 // --- 修改:移除 localStorage 讀取,改為空陣列 --- const [projects, setProjects] = useState([]); // --- 修改:移除 localStorage 讀取 --- const [activeProjectId, setActiveProjectId] = useState(null); const [activeTab, setActiveTab] = useState('html'); const [previewSrc, setPreviewSrc] = useState(''); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [autoRunEnabled, setAutoRunEnabled] = useState(true); const [theme, setTheme] = useState('dark'); const [previewDevice, setPreviewDevice] = useState('desktop'); const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [alertModal, setAlertModal] = useState({ isOpen: false, message: '' }); const [confirmModal, setConfirmModal] = useState({ isOpen: false, title: '', message: '', onConfirm: null }); const [consoleLogs, setConsoleLogs] = useState([]); const [layoutMode, setLayoutMode] = useState('horizontal'); const [mainPaneSize, setMainPaneSize] = useState(50); const [consolePaneSize, setConsolePaneSize] = useState(160); const [isResizingMain, setIsResizingMain] = useState(false); const [isResizingConsole, setIsResizingConsole] = useState(false); const importFileRef = useRef(null); const { activeProject, activeProjectIndex } = useMemo(() => { if (!activeProjectId || projects.length === 0) { return { activeProject: null, activeProjectIndex: -1 }; } const index = projects.findIndex(p => p.id === activeProjectId); if (index === -1) { // 如果找不到,但有專案,則指向第一個 if(projects.length > 0) { setActiveProjectId(projects[0].id); // 觸發 return { activeProject: projects[0], activeProjectIndex: 0 }; } return { activeProject: null, activeProjectIndex: -1 }; } return { activeProject: projects[index], activeProjectIndex: index }; }, [projects, activeProjectId]); // --- 專案管理操作 (在 effects 之前定義) --- const handleNewProject = useCallback(async () => { const newId = crypto.randomUUID(); const blankContent = { html: `\n\n\n 新專案\n\n\n

新專案

\n\n`, css: `/* 新專案 CSS */\nbody {\n padding: 1rem;\n}`, js: `// 新專案 JS\nconsole.log('新專案已載入');` }; const newProject = createInitialProject(newId, `新專案 ${projects.length + 1}`, blankContent); // --- 修改:如果登入,寫入 Firestore --- if (user) { try { const projectDocRef = doc(db, 'userProjects', user.uid, 'projects', newProject.id); await setDoc(projectDocRef, newProject); } catch (error) { console.error("建立 Firestore 專案失敗:", error); setAlertModal({ isOpen: true, message: "建立雲端專案失敗。" }); } } // 本地狀態更新 (Firestore 登入時 onSnapshot 會處理,但登出時需要這個) if (!user) { setProjects(prevProjects => [...prevProjects, newProject]); } setActiveProjectId(newId); setActiveTab('html'); setIsSidebarOpen(false); }, [user, projects.length]); // 依賴 user // --- 特效 (Effects) --- // --- 新增功能:Firebase 驗證監聽 --- useEffect(() => { const unsubscribe = onAuthStateChanged(auth, (user) => { setUser(user); setIsAuthReady(true); }); return () => unsubscribe(); // 清理監聽器 }, []); // --- 新增功能:Firebase 專案讀取 (切換本地/雲端) --- useEffect(() => { if (!isAuthReady) return; // 等待驗證初始化 let unsubscribe = () => {}; if (user) { // --- 登入狀態:從 Firestore 讀取 --- const projectsColRef = collection(db, 'userProjects', user.uid, 'projects'); const q = query(projectsColRef); unsubscribe = onSnapshot(q, (snapshot) => { if (snapshot.empty) { // 如果 Firestore 沒專案,建立一個新的 handleNewProject(); } else { const fetchedProjects = snapshot.docs.map(doc => ({ ...doc.data(), id: doc.id, // 確保舊資料相容 libraries: doc.data().libraries || { js: [], css: [] } })); setProjects(fetchedProjects); // 檢查 activeProjectId 是否有效 const savedId = localStorage.getItem('code-pro-activeProjectId'); if (savedId && fetchedProjects.find(p => p.id === savedId)) { setActiveProjectId(savedId); } else { setActiveProjectId(fetchedProjects[0].id); } } }, (error) => { console.error("讀取 Firestore 失敗:", error); setAlertModal({ isOpen: true, message: "讀取雲端專案失敗。" }); }); } else { // --- 登出狀態:從 localStorage 讀取 --- try { const savedProjects = localStorage.getItem('code-pro-projects'); const parsedProjects = savedProjects ? JSON.parse(savedProjects) : []; if (parsedProjects.length > 0) { // 確保舊資料相容 setProjects(parsedProjects.map(p => ({ ...p, libraries: p.libraries || { js: [], css: [] } }))); } else { setProjects(initialProjects); } const savedId = localStorage.getItem('code-pro-activeProjectId'); if (savedId && parsedProjects.find(p => p.id === savedId)) { setActiveProjectId(savedId); } else { setActiveProjectId(parsedProjects.length > 0 ? parsedProjects[0].id : initialProjects[0].id); } } catch (e) { console.error("Failed to load projects from localStorage", e); setProjects(initialProjects); setActiveProjectId(initialProjects[0].id); } } return () => unsubscribe(); // 清理 onSnapshot 監聽器 }, [user, isAuthReady, handleNewProject, initialProjects]); // 依賴 user 和 isAuthReady // --- 修改:localStorage 儲存 (僅在登出時) --- useEffect(() => { if (isAuthReady && !user && projects.length > 0) { try { localStorage.setItem('code-pro-projects', JSON.stringify(projects)); } catch (e) { console.error("Failed to save projects to localStorage", e); } } }, [projects, user, isAuthReady]); // --- 修改:activeProjectId 儲存 (這個可以一直存在 localStorage) --- useEffect(() => { if (activeProjectId) { localStorage.setItem('code-pro-activeProjectId', activeProjectId); } }, [activeProjectId]); // 主題切換 useEffect(() => { const root = window.document.documentElement; if (theme === 'light') root.classList.remove('dark'); else root.classList.add('dark'); }, [theme]); // 監聽來自 iframe 的 console log useEffect(() => { const handleConsoleMessage = (event) => { if (event.data && event.data.type && event.data.type.startsWith('console-')) { setConsoleLogs(prevLogs => [ ...prevLogs.slice(-100), { type: event.data.type, message: event.data.message, timestamp: new Date().toLocaleTimeString('en-US', { hour12: false }) } ]); } }; window.addEventListener('message', handleConsoleMessage); return () => window.removeEventListener('message', handleConsoleMessage); }, []); // 處理面板大小調整 const handleMouseMove = useCallback((e) => { e.preventDefault(); if (isResizingMain) { if (layoutMode === 'horizontal') { const newSize = (e.clientX / window.innerWidth) * 100; setMainPaneSize(Math.max(20, Math.min(80, newSize))); } else { const newSize = (e.clientY / window.innerHeight) * 100; setMainPaneSize(Math.max(20, Math.min(80, newSize))); } } if (isResizingConsole) { const newHeight = window.innerHeight - e.clientY; setConsolePaneSize(Math.max(40, Math.min(window.innerHeight * 0.6, newHeight))); } }, [isResizingMain, isResizingConsole, layoutMode]); const handleMouseUp = useCallback(() => { setIsResizingMain(false); setIsResizingConsole(false); }, []); useEffect(() => { if (isResizingMain || isResizingConsole) { window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mouseup', handleMouseUp); } else { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); } return () => { window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mouseup', handleMouseUp); }; }, [isResizingMain, isResizingConsole, handleMouseMove, handleMouseUp]); // --- 核心功能:執行/更新預覽 --- const updatePreview = useCallback(() => { if (!activeProject) return; setConsoleLogs([]); // --- 修改:納入 libraries (如果有的話) --- const { fileContents, libraries } = activeProject; const { html, css, js } = fileContents; const safeLibraries = libraries || { js: [], css: [] }; const cssLinks = (safeLibraries.css || []).map(url => ``).join('\n'); const jsScripts = (safeLibraries.js || []).map(url => ``).join('\n'); const fullCSS = ``; const consoleInterceptor = ` `; const userScript = ``; let finalHTML = html; if (finalHTML.includes('')) { finalHTML = finalHTML.replace('', `\n${consoleInterceptor}\n${cssLinks}`); } else { finalHTML = `${consoleInterceptor}\n${cssLinks}\n${finalHTML}`; } if (finalHTML.includes('')) { finalHTML = finalHTML.replace('', `${fullCSS}\n`); } else { finalHTML = `${fullCSS}\n${finalHTML}`; } if (finalHTML.includes('')) { finalHTML = finalHTML.replace('', `${jsScripts}\n${userScript}\n`); } else { finalHTML = `${finalHTML}\n${jsScripts}\n${userScript}`; } setPreviewSrc(finalHTML); }, [activeProject]); const updatePreviewRef = useRef(updatePreview); useEffect(() => { updatePreviewRef.current = updatePreview; }, [updatePreview]); useEffect(() => { if (!autoRunEnabled) return; const timer = setTimeout(() => updatePreviewRef.current(), 500); return () => clearTimeout(timer); }, [activeProject?.fileContents, autoRunEnabled, activeProject?.libraries]); // 依賴 libraries useEffect(() => { const handleKeyDown = (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); if (!autoRunEnabled) { updatePreviewRef.current(); } } }; window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [autoRunEnabled]); // 處理初始載入 (現在由 'Firebase 專案讀取' effect 處理) // --- 專案管理操作 (續) --- const executeDeleteProject = async (idToDelete) => { // --- 修改:如果登入,刪除 Firestore --- if (user) { try { const projectDocRef = doc(db, 'userProjects', user.uid, 'projects', idToDelete); await deleteDoc(projectDocRef); } catch (error) { console.error("刪除 Firestore 專案失敗:", error); setAlertModal({ isOpen: true, message: "刪除雲端專案失敗。" }); } } // 本地狀態更新 (登出時需要) if (!user) { setProjects(prevProjects => { const newProjects = prevProjects.filter(p => p.id !== idToDelete); if (activeProjectId === idToDelete) { setActiveProjectId(newProjects[0]?.id || null); } return newProjects; }); } // 注意:登入時,onSnapshot 會自動處理 setProjects }; const handleDeleteProject = (idToDelete, projectName) => { if (projects.length === 1) { setAlertModal({ isOpen: true, message: "無法刪除最後一個專案。" }); return; } setConfirmModal({ isOpen: true, title: `刪除專案`, message: `您確定要永久刪除專案 "${projectName}" 嗎?此操作無法復原。`, onConfirm: () => { executeDeleteProject(idToDelete); setConfirmModal({ isOpen: false, title: '', message: '', onConfirm: null }); } }); }; const updateActiveProject = async (newProject) => { // --- 修改:如果登入,寫入 Firestore --- if (user) { try { const projectDocRef = doc(db, 'userProjects', user.uid, 'projects', newProject.id); await setDoc(projectDocRef, newProject, { merge: true }); } catch (error) { console.error("更新 Firestore 專案失敗:", error); setAlertModal({ isOpen: true, message: "雲端同步失敗。" }); } } // 本地狀態更新 (登出時需要) if (!user) { setProjects(prevProjects => { const index = prevProjects.findIndex(p => p.id === newProject.id); if (index === -1) return prevProjects; const newProjects = [...prevProjects]; newProjects[index] = newProject; return newProjects; }); } // 注意:登入時,onSnapshot 會自動處理 setProjects,但為了即時反應, // 執行 setDoc 也可以 setProjects(prevProjects => { const index = prevProjects.findIndex(p => p.id === newProject.id); if (index === -1) return prevProjects; // 不太可能發生 const newProjects = [...prevProjects]; newProjects[index] = newProject; return newProjects; }); }; const handleProjectRename = (newName) => { if (!newName.trim() || !activeProject) return; const newProject = { ...activeProject, name: newName }; updateActiveProject(newProject); }; // --- 匯出 / 匯入 功能 --- const handleExport = () => { if (!activeProject) return; // --- 修改:匯出時也包含 libraries --- const { name, fileContents, libraries } = activeProject; const dataToExport = { name, fileContents, libraries, exportedAt: new Date().toISOString() }; const jsonString = JSON.stringify(dataToExport, null, 2); const blob = new Blob([jsonString], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${name.replace(/\s+/g, '_') || 'project'}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const handleImport = async (event) => { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (e) => { try { const importedData = JSON.parse(e.target.result); if (!importedData.name || !importedData.fileContents || !importedData.fileContents.html || typeof importedData.fileContents.css === 'undefined' || typeof importedData.fileContents.js === 'undefined') { throw new Error('無效的專案檔案格式。'); } const newId = crypto.randomUUID(); // --- 修改:匯入時包含 libraries --- const newProject = createInitialProject( newId, `${importedData.name} (已匯入)`, importedData.fileContents, importedData.libraries || { js: [], css: [] } // 確保相容性 ); // --- 修改:如果登入,寫入 Firestore --- if (user) { const projectDocRef = doc(db, 'userProjects', user.uid, 'projects', newProject.id); await setDoc(projectDocRef, newProject); } if (!user) { setProjects(prevProjects => [...prevProjects, newProject]); } // 登入時 onSnapshot 會自動更新 setActiveProjectId(newId); setIsSettingsOpen(false); setAlertModal({ isOpen: true, message: "專案匯入成功!" }); } catch (error) { console.error("匯入失敗:", error); setAlertModal({ isOpen: true, message: `匯入失敗:${error.message}` }); } finally { if (importFileRef.current) importFileRef.current.value = ""; } }; reader.readText(file); }; // --- 編輯器操作 --- const handleEditorChange = (e) => { if (!activeProject) return; const newContent = e.target.value; const { history, historyIndex, fileContents, ...rest } = activeProject; if (fileContents[activeTab] === newContent) { return; } const newFileContents = { ...fileContents, [activeTab]: newContent }; const currentHistory = history[activeTab]; const currentIndex = historyIndex[activeTab]; const newHistory = currentHistory.slice(0, currentIndex + 1); newHistory.push(newContent); const trimmedHistory = newHistory.slice(Math.max(0, newHistory.length - MAX_HISTORY)); const newIndex = trimmedHistory.length - 1; const newProject = { ...rest, fileContents: newFileContents, history: { ...history, [activeTab]: trimmedHistory }, historyIndex: { ...historyIndex, [activeTab]: newIndex } }; // --- 修改:觸發 updateActiveProject 以進行儲存 --- updateActiveProject(newProject); }; const handleHistoryAction = (action) => { if (!activeProject) return; const { history, historyIndex, fileContents, ...rest } = activeProject; const currentIndex = historyIndex[activeTab]; const maxIndex = history[activeTab].length - 1; let newIndex = currentIndex; if (action === 'undo' && currentIndex > 0) newIndex = currentIndex - 1; else if (action === 'redo' && currentIndex < maxIndex) newIndex = currentIndex + 1; else return; const contentToLoad = history[activeTab][newIndex]; const newFileContents = { ...fileContents, [activeTab]: contentToLoad }; const newProject = { ...rest, fileContents: newFileContents, history, historyIndex: { ...historyIndex, [activeTab]: newIndex } }; // --- 修改:觸發 updateActiveProject 以進行儲存 --- updateActiveProject(newProject); }; const handleUndo = () => handleHistoryAction('undo'); const handleRedo = () => handleHistoryAction('redo'); const handleClear = () => { if (!activeProject) return; const emptyContent = activeTab === 'html' ? initialContents.html.split('')[0] + '\n\n\n' : (activeTab === 'css' ? '/* CSS 已清除 */' : '// JavaScript 已清除'); const { history, historyIndex, fileContents, ...rest } = activeProject; const currentIndex = historyIndex[activeTab]; const newHistory = [...history[activeTab].slice(0, currentIndex + 1), emptyContent]; const trimmedHistory = newHistory.slice(Math.max(0, newHistory.length - MAX_HISTORY)); const newProject = { ...rest, fileContents: { ...fileContents, [activeTab]: emptyContent }, history: { ...history, [activeTab]: trimmedHistory }, historyIndex: { ...historyIndex, [activeTab]: trimmedHistory.length - 1 } }; // --- 修改:觸發 updateActiveProject 以進行儲存 --- updateActiveProject(newProject); }; // --- 檢查和加載 (修正 5) --- if (!activeProject) { // 讓 useEffect 處理載入邏輯 return
正在載入專案...
; } // --- 狀態計算 (依賴 activeProject) --- const canUndo = activeProject.historyIndex[activeTab] > 0; const canRedo = activeProject.historyIndex[activeTab] < activeProject.history[activeTab].length - 1; const activeContent = activeProject.fileContents[activeTab]; const charCount = activeContent.length; const lineCount = activeContent.split('\n').length; // 編輯器樣式 const editorStyles = { fontFamily: '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace', lineHeight: 1.6, fontSize: 14, flex: '1', width: '100%', height: '100%', padding: '1rem', backgroundColor: theme === 'dark' ? '#1e293b' : '#f8fafc', color: theme === 'dark' ? '#f8fafc' : '#1e293b', border: 'none', outline: 'none', resize: 'none', overflow: 'auto', }; const getTabClasses = (tabName) => `px-4 py-2 text-sm font-medium transition-colors duration-200 ease-in-out ${ activeTab === tabName ? 'bg-blue-600 text-white shadow-md rounded-t-lg' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-slate-700 rounded-t-lg' }`; // --- Modals (Settings, Alert, Confirm) --- const SettingsModal = ({ isOpen, onClose }) => { const [projectName, setProjectName] = useState(activeProject.name); // --- 新增功能:本地狀態管理 libraries --- const [localLibraries, setLocalLibraries] = useState(activeProject.libraries || { js: [], css: [] }); useEffect(() => { setProjectName(activeProject.name); setLocalLibraries(activeProject.libraries || { js: [], css: [] }); }, [activeProject.name, activeProject.libraries, isOpen]); const handleSave = () => { handleProjectRename(projectName); // --- 新增功能:儲存 libraries --- updateActiveProject({ ...activeProject, name: projectName, libraries: localLibraries }); onClose(); }; const handleLibChange = (type, index, value) => { const newLibs = { ...localLibraries }; newLibs[type][index] = value; setLocalLibraries(newLibs); }; const addLib = (type) => { const newLibs = { ...localLibraries }; newLibs[type] = [...(newLibs[type] || []), ""]; // 新增一個空字串 setLocalLibraries(newLibs); }; const removeLib = (type, index) => { const newLibs = { ...localLibraries }; newLibs[type] = newLibs[type].filter((_, i) => i !== index); setLocalLibraries(newLibs); }; if (!isOpen) return null; return (

編輯器設定

setProjectName(e.target.value)} className="w-full bg-gray-100 dark:bg-slate-700 border border-gray-300 dark:border-slate-600 rounded-lg p-3 text-gray-900 dark:text-white focus:ring-blue-500 focus:border-blue-500" placeholder="輸入專案名稱" />
{/* --- 新增功能:外部資源庫管理 --- */}
{/* CSS 資源 */}
{(localLibraries.css || []).map((lib, index) => (
handleLibChange('css', index, e.target.value)} className="flex-1 bg-gray-100 dark:bg-slate-700 border border-gray-300 dark:border-slate-600 rounded-lg p-2 text-sm text-gray-900 dark:text-white focus:ring-blue-500 focus:border-blue-500" placeholder="https://.../style.css" />
))}
{/* JS 資源 */}
{(localLibraries.js || []).map((lib, index) => (
handleLibChange('js', index, e.target.value)} className="flex-1 bg-gray-100 dark:bg-slate-700 border border-gray-300 dark:border-slate-600 rounded-lg p-2 text-sm text-gray-900 dark:text-white focus:ring-blue-500 focus:border-blue-500" placeholder="https://.../script.js" />
))}
{autoRunEnabled ? : }
); }; const AlertModal = () => { if (!alertModal.isOpen) return null; return (

提示

{alertModal.message}

); }; const ConfirmDeleteModal = () => { if (!confirmModal.isOpen) return null; return (

{confirmModal.title}

{confirmModal.message}

); }; // --- 裝置預覽相關 --- const devicePreviewStyles = { desktop: { width: '100%', height: '100%', borderRadius: '0', border: 'none', boxShadow: 'none' }, tablet: { width: '768px', height: '1024px', borderRadius: '20px', border: '10px solid #333', boxShadow: '0 0 20px rgba(0,0,0,0.3)', marginTop: '2rem' }, mobile: { width: '375px', height: '667px', borderRadius: '20px', border: '8px solid #333', boxShadow: '0 0 20px rgba(0,0,0,0.3)', marginTop: '2rem' } }; const getDeviceBtnClass = (device) => `p-2 rounded-lg transition-colors ${ (previewDevice === device) || (device === 'horizontal-layout' && layoutMode === 'horizontal') || (device === 'vertical-layout' && layoutMode === 'vertical') ? 'bg-blue-600 text-white' : 'text-gray-500 dark:text-slate-400 hover:bg-gray-200 dark:hover:bg-slate-600' }`; // --- 側邊欄組件 (Sidebar) --- const SidebarComponent = () => (

專案列表

{projects.map(project => (
{ setActiveProjectId(project.id); setIsSidebarOpen(false); }} className={`flex items-center justify-between p-3 text-sm cursor-pointer ${ activeProjectId === project.id ? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300' : 'text-gray-700 dark:text-slate-300 hover:bg-gray-200 dark:hover:bg-slate-700' }`} >
{project.name}
{projects.length > 1 && ( )}
))}
); // --- 新增功能:Firebase 登入/登出處理 --- const signInWithGoogle = async () => { const provider = new GoogleAuthProvider(); try { await signInWithPopup(auth, provider); } catch (error) { console.error("Google 登入失敗:", error); setAlertModal({ isOpen: true, message: `登入失敗: ${error.message}` }); } }; const logOut = async () => { try { await signOut(auth); // 登出後,狀態會自動更新 (via onAuthStateChanged) // 這會觸發讀取 localStorage 的 effect } catch (error) { console.error("登出失敗:", error); setAlertModal({ isOpen: true, message: `登出失敗: ${error.message}` }); } }; // --- 新增功能:登入 UI 元件 --- const AuthButton = () => { if (!isAuthReady) { return (
正在載入用戶...
); } if (user) { return (
{user.displayName}
); } return ( ); }; return (
{/* Modals 渲染在頂層 */} setIsSettingsOpen(false)} /> {/* --- 主要內容區 (包含頂部 Header) --- */}
{/* 頂部導覽列 */}

Code Pro IDE

{/* --- 新增功能:顯示同步狀態 --- */} {isAuthReady && user && (
已同步
)} {isAuthReady && !user && (
本地儲存
)}
{/* --- 新增功能:登入按鈕 --- */}
{/* --- 佈局更新:可調整大小的主編輯區 --- */}
{/* 編輯器面板 */}