import React, { useState, useEffect } from 'react'; import { Edit2, Trash2, Plus, Check, X, ExternalLink, Paperclip, Heart, Image as ImageIcon, LogOut, LogIn } from 'lucide-react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInWithCustomToken, signInAnonymously, onAuthStateChanged, GoogleAuthProvider, signInWithPopup, signOut } from 'firebase/auth'; import { getFirestore, collection, onSnapshot, doc, setDoc, deleteDoc } from 'firebase/firestore'; // === Firebase 初始化設定 === const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; // 定義可用的便利貼顏色 const NOTE_COLORS = [ 'bg-yellow-100', 'bg-pink-100', 'bg-blue-100', 'bg-green-100', 'bg-purple-100', 'bg-orange-100' ]; // 定義些微旋轉的角度,讓便利貼看起來更自然 const ROTATIONS = ['-rotate-2', '-rotate-1', 'rotate-0', 'rotate-1', 'rotate-2', 'rotate-3']; export default function App() { const [notes, setNotes] = useState([]); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); // 注入手寫字體與自訂 CSS useEffect(() => { const link = document.createElement('link'); link.href = 'https://fonts.googleapis.com/css2?family=Klee+One:wght@400;600&display=swap'; link.rel = 'stylesheet'; document.head.appendChild(link); return () => document.head.removeChild(link); }, []); // 1. 處理身份驗證 (Auth) useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (error) { console.error("Auth init error:", error); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, (currentUser) => { setUser(currentUser); }); return () => unsubscribe(); }, []); // 2. 處理資料庫同步 (Firestore) useEffect(() => { if (!user) return; setLoading(true); // 依據使用者 UID 建立專屬的資料路徑 const notesRef = collection(db, 'artifacts', appId, 'users', user.uid, 'notes'); const unsubscribe = onSnapshot(notesRef, (snapshot) => { const fetchedNotes = []; snapshot.forEach((doc) => { fetchedNotes.push({ id: doc.id, ...doc.data() }); }); // 依照建立時間排序 fetchedNotes.sort((a, b) => (a.createdAt || 0) - (b.createdAt || 0)); setNotes(fetchedNotes); setLoading(false); }, (error) => { console.error("Error fetching notes:", error); setLoading(false); } ); return () => unsubscribe(); }, [user]); // Google 登入 const handleGoogleLogin = async () => { const provider = new GoogleAuthProvider(); try { await signInWithPopup(auth, provider); } catch (error) { console.error("Google login failed", error); } }; // 登出 const handleLogout = async () => { try { await signOut(auth); await signInAnonymously(auth); // 登出後退回訪客狀態 } catch (error) { console.error("Logout failed", error); } }; // 新增便利貼 const addNote = async () => { if (!user) return; const randomColor = NOTE_COLORS[Math.floor(Math.random() * NOTE_COLORS.length)]; const randomRotate = ROTATIONS[Math.floor(Math.random() * ROTATIONS.length)]; const newId = Date.now().toString(); const newNote = { title: '新便利貼', content: '在這裡寫下描述...', link: '', color: randomColor, rotate: randomRotate, icon: '📌', iconType: 'emoji', // 'emoji' 或 'image' createdAt: Date.now() }; try { await setDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'notes', newId), newNote); } catch (error) { console.error("Error adding note:", error); } }; // 更新便利貼 const updateNote = async (id, updatedData) => { if (!user) return; try { await setDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'notes', id.toString()), updatedData, { merge: true }); } catch (error) { console.error("Error updating note:", error); } }; // 刪除便利貼 const deleteNote = async (id) => { if (!user) return; try { await deleteDoc(doc(db, 'artifacts', appId, 'users', user.uid, 'notes', id.toString())); } catch (error) { console.error("Error deleting note:", error); } }; return (
{/* 頂部導覽列 / 登入狀態 */}
{user && !user.isAnonymous ? (
{user.email || '已登入'}
) : ( )}
{/* 左側筆記本線圈裝飾 */}
{[...Array(24)].map((_, i) => (
))}
{/* 主要內容區 */}

我的專屬筆記本

{loading ? (
載入筆記中...
) : (
{notes.map(note => ( ))} {/* 新增便利貼按鈕 */}
)}
); } // 獨立的便利貼元件 function NoteCard({ note, onUpdate, onDelete }) { const [isEditing, setIsEditing] = useState(false); const [tempData, setTempData] = useState({ ...note }); const handleEdit = (e) => { e.preventDefault(); e.stopPropagation(); setTempData({ ...note }); setIsEditing(true); }; const handleSave = () => { onUpdate(note.id, tempData); setIsEditing(false); }; const handleCancel = () => { setIsEditing(false); }; const handleCardClick = (e) => { if (isEditing) return; if (e.target.closest('button')) return; if (note.link) { window.open(note.link, '_blank', 'noopener,noreferrer'); } }; // 處理圖片上傳,並將其轉為縮小的 Base64 字串以節省空間 const handleImageUpload = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const img = new Image(); img.onload = () => { // 利用 Canvas 將圖片縮小至最大 80px,避免超過資料庫大小限制 const canvas = document.createElement('canvas'); const MAX_SIZE = 80; let width = img.width; let height = img.height; if (width > height) { if (width > MAX_SIZE) { height *= MAX_SIZE / width; width = MAX_SIZE; } } else { if (height > MAX_SIZE) { width *= MAX_SIZE / height; height = MAX_SIZE; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); const dataUrl = canvas.toDataURL('image/png'); setTempData({ ...tempData, icon: dataUrl, iconType: 'image' }); }; img.src = event.target.result; }; reader.readAsDataURL(file); }; return (
{!isEditing && (
)} {isEditing ? (
e.stopPropagation()}> {/* 圖示與標題編輯區塊 */}
{tempData.iconType === 'image' && tempData.icon ? ( icon ) : ( {tempData.icon || '📌'} )} {/* Emoji 輸入框 */} setTempData({...tempData, icon: e.target.value, iconType: 'emoji'})} className="w-7 bg-transparent text-center text-lg outline-none placeholder-gray-500" placeholder="😀" maxLength={2} title="輸入 Emoji" /> | {/* 圖片上傳按鈕 */}
setTempData({...tempData, title: e.target.value})} className="bg-transparent font-bold text-xl outline-none placeholder-black/40 text-gray-800 flex-1 min-w-0" placeholder="標題..." autoFocus />