From 2e627ce72fcec2f255804013a06650fe5fa67782 Mon Sep 17 00:00:00 2001 From: abhyuday Date: Wed, 29 Oct 2025 20:16:08 +0530 Subject: [PATCH] added note sharing --- note-sharing/App.jsx | 521 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 521 insertions(+) create mode 100644 note-sharing/App.jsx diff --git a/note-sharing/App.jsx b/note-sharing/App.jsx new file mode 100644 index 0000000..8ebad5c --- /dev/null +++ b/note-sharing/App.jsx @@ -0,0 +1,521 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { initializeApp, setLogLevel } from 'firebase/app'; +import { + getAuth, + signInAnonymously, + signInWithCustomToken, + onAuthStateChanged +} from 'firebase/auth'; +import { + getFirestore, + doc, + getDoc, + setDoc, + addDoc, + collection, + onSnapshot, + serverTimestamp, + deleteDoc, + query, +} from 'firebase/firestore'; +import { Plus, Trash2, Share2, Clipboard, X, Loader2, Save } from 'lucide-react'; + +// --- Firebase Configuration --- +// These global variables are expected to be injected by the environment. +const firebaseConfig = typeof __firebase_config !== 'undefined' + ? JSON.parse(__firebase_config) + : { apiKey: "DEFAULT_API_KEY", authDomain: "DEFAULT_AUTH_DOMAIN", projectId: "DEFAULT_PROJECT_ID" }; + +const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; +const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : undefined; + +// --- Firebase Initialization --- +let app; +let auth; +let db; + +try { + app = initializeApp(firebaseConfig); + auth = getAuth(app); + db = getFirestore(app); + setLogLevel('debug'); +} catch (e) { + console.error("Firebase initialization error:", e); +} + +// --- Main App Component --- +export default function App() { + const [isAuthReady, setIsAuthReady] = useState(false); + const [userId, setUserId] = useState(null); + + // 'personal' for user's own notes, 'shared' for viewing a shared link + const [viewMode, setViewMode] = useState('personal'); + + // State for personal notes + const [notes, setNotes] = useState([]); + const [selectedNoteId, setSelectedNoteId] = useState(null); + + // State for viewing a single shared note + const [sharedNote, setSharedNote] = useState(null); + const [isSharedNoteLoading, setIsSharedNoteLoading] = useState(true); + + // State for the share modal + const [showShareModal, setShowShareModal] = useState(false); + const [shareLink, setShareLink] = useState(''); + const [isLoadingShare, setIsLoadingShare] = useState(false); + + // Effect 1: Handle Authentication + useEffect(() => { + if (!auth) return; + + const unsubscribe = onAuthStateChanged(auth, async (user) => { + if (user) { + setUserId(user.uid); + setIsAuthReady(true); + } else { + try { + if (initialAuthToken) { + await signInWithCustomToken(auth, initialAuthToken); + } else { + await signInAnonymously(auth); + } + } catch (error) { + console.error("Authentication error:", error); + setIsAuthReady(true); // Still ready, even if auth failed + } + } + }); + + return () => unsubscribe(); + }, []); + + // Effect 2: Check for Shared Note URL (?view=...) + useEffect(() => { + // This effect runs once on load to check the URL + const urlParams = new URLSearchParams(window.location.search); + const viewId = urlParams.get('view'); + + if (viewId) { + // If a 'view' ID is in the URL, switch to shared mode + setViewMode('shared'); + loadSharedNote(viewId); + } else { + // Otherwise, load personal notes + setViewMode('personal'); + } + }, [isAuthReady]); // Depend on isAuthReady to ensure db is ready + + // Effect 3: Listen for Personal Notes (if in 'personal' mode) + useEffect(() => { + if (viewMode === 'personal' && isAuthReady && db && userId) { + const notesCollectionPath = `artifacts/${appId}/users/${userId}/notes`; + const q = query(collection(db, notesCollectionPath)); + + const unsubscribe = onSnapshot(q, (snapshot) => { + const notesData = snapshot.docs.map(doc => ({ + id: doc.id, + ...doc.data(), + })); + setNotes(notesData); + + // If no note is selected, or selected note was deleted, select the first one + if (!selectedNoteId || !notesData.find(n => n.id === selectedNoteId)) { + setSelectedNoteId(notesData[0]?.id || null); + } + }, (error) => { + console.error("Error listening to notes:", error); + }); + + return () => unsubscribe(); + } + }, [viewMode, isAuthReady, userId, db]); // Re-run if any of these change + + // --- Data Functions --- + + // Load a single shared note from the public collection + const loadSharedNote = async (viewId) => { + if (!db) return; + setIsSharedNoteLoading(true); + try { + const noteRef = doc(db, `artifacts/${appId}/public/data/sharedNotes`, viewId); + const docSnap = await getDoc(noteRef); + + if (docSnap.exists()) { + setSharedNote(docSnap.data()); + } else { + setSharedNote({ title: "Note Not Found", content: "This note may have been deleted or the link is incorrect." }); + } + } catch (error) { + console.error("Error fetching shared note:", error); + setSharedNote({ title: "Error", content: "Could not load this note." }); + } finally { + setIsSharedNoteLoading(false); + } + }; + + const selectedNote = useMemo(() => { + return notes.find(note => note.id === selectedNoteId); + }, [notes, selectedNoteId]); + + const handleNewNote = async () => { + if (!db || !userId) return; + const newNote = { + title: "New Note", + content: "Start writing...", + createdAt: serverTimestamp(), + shareId: null, // No share link initially + }; + try { + const notesCollectionPath = `artifacts/${appId}/users/${userId}/notes`; + const docRef = await addDoc(collection(db, notesCollectionPath), newNote); + setSelectedNoteId(docRef.id); + } catch (error) { + console.error("Error creating new note:", error); + } + }; + + const handleSelectNote = (id) => { + setSelectedNoteId(id); + }; + + const handleUpdateNote = async (id, title, content) => { + if (!db || !userId) return; + const noteRef = doc(db, `artifacts/${appId}/users/${userId}/notes`, id); + try { + await setDoc(noteRef, { title, content }, { merge: true }); + } catch (error) { + console.error("Error updating note:", error); + } + }; + + const handleDeleteNote = async (id) => { + if (!db || !userId) return; + // We won't delete the public note, but we could by grabbing the shareId first. + // For now, deleting the private note just orphans the public link. + const noteRef = doc(db, `artifacts/${appId}/users/${userId}/notes`, id); + try { + await deleteDoc(noteRef); + setSelectedNoteId(null); // Deselect + } catch (error) { + console.error("Error deleting note:", error); + } + }; + + // --- Sharing Logic --- + const handleShareNote = async () => { + if (!selectedNote || !db || !userId) return; + + setIsLoadingShare(true); + + const publicNoteData = { + title: selectedNote.title, + content: selectedNote.content, + ownerId: userId, + originalNoteId: selectedNote.id, + sharedAt: serverTimestamp(), + }; + + try { + let shareId = selectedNote.shareId; + + if (shareId) { + // This note is already shared, just update the public copy + const publicNoteRef = doc(db, `artifacts/${appId}/public/data/sharedNotes`, shareId); + await setDoc(publicNoteRef, publicNoteData, { merge: true }); // Update existing + } else { + // This is the first time sharing this note + const publicCollectionRef = collection(db, `artifacts/${appId}/public/data/sharedNotes`); + const newPublicDoc = await addDoc(publicCollectionRef, publicNoteData); + shareId = newPublicDoc.id; + + // Save the new shareId back to the private note + const privateNoteRef = doc(db, `artifacts/${appId}/users/${userId}/notes`, selectedNote.id); + await setDoc(privateNoteRef, { shareId: shareId }, { merge: true }); + } + + // Generate and show the link + const link = `${window.location.origin}${window.location.pathname}?view=${shareId}`; + setShareLink(link); + setShowShareModal(true); + + } catch (error) { + console.error("Error sharing note:", error); + } finally { + setIsLoadingShare(false); + } + }; + + // --- Render Logic --- + + if (!isAuthReady) { + return ( +
+ + Loading... +
+ ); + } + + // Render Mode 1: Viewing a Shared Note + if (viewMode === 'shared') { + return ( + + ); + } + + // Render Mode 2: Personal Notes Dashboard + return ( +
+ + + {showShareModal && ( + setShowShareModal(false)} + /> + )} +
+ ); +} + +// --- Sub-Components --- + +function NoteList({ notes, selectedNoteId, onSelectNote, onNewNote, onDeleteNote }) { + return ( +
+
+

My Notes

+ +
+
+ {notes.length === 0 && ( +

No notes yet. Create one!

+ )} + {notes.map(note => ( +
onSelectNote(note.id)} + className={`p-4 border-b border-gray-200 cursor-pointer ${ + selectedNoteId === note.id ? 'bg-blue-50' : 'hover:bg-gray-50' + }`} + > +
+

+ {note.title || "Untitled"} +

+ +
+

+ {note.content ? note.content.substring(0, 50).replace(/(\r\n|\n|\r)/gm, " ") + "..." : "No content"} +

+
+ ))} +
+
+ ); +} + +function NoteEditor({ note, onUpdate, onShare, isLoadingShare }) { + const [title, setTitle] = useState(note?.title || ''); + const [content, setContent] = useState(note?.content || ''); + const [isSaving, setIsSaving] = useState(false); + + // Update local state when note prop changes + useEffect(() => { + setTitle(note?.title || ''); + setContent(note?.content || ''); + }, [note]); + + // Debounced save + useEffect(() => { + if (!note) return; // Don't save if no note is selected + + setIsSaving(true); + const handler = setTimeout(() => { + onUpdate(note.id, title, content); + setIsSaving(false); + }, 1000); // Save 1 second after user stops typing + + return () => { + clearTimeout(handler); + }; + }, [title, content, note?.id]); + + + if (!note) { + return ( +
+

Select a note to start editing or create a new one.

+
+ ); + } + + return ( +
+
+
+ {isSaving ? ( + + ) : ( + + )} + setTitle(e.target.value)} + className="text-xl font-bold text-gray-900 w-full focus:outline-none" + placeholder="Note Title" + /> +
+ +
+