From e512de1cf619b4462a7ed463052930e9b5557ec5 Mon Sep 17 00:00:00 2001 From: vipulchavan47 Date: Tue, 14 Oct 2025 18:29:56 +0530 Subject: [PATCH] fixes and improvements --- src/App.jsx | 258 ++++++++++++++++++++++++++++++------- src/components/Editor.jsx | 11 +- src/components/Sidebar.jsx | 97 +++++++++++--- src/css/style.css | 106 ++++++++++++++- src/firebase.js | 10 +- 5 files changed, 409 insertions(+), 73 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index beb20a1..5ff3aad 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,91 +1,259 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback } from "react"; import Split from "react-split"; -import { nanoid } from "nanoid"; -import { onSnapshot, addDoc, doc, deleteDoc, setDoc } from "firebase/firestore"; -import { notesCollection, db } from "./firebase"; import { Sidebar, Editor } from "./components"; +import { notesCollection, db } from "./firebase"; +import { + addDoc, + doc, + deleteDoc, + setDoc, + onSnapshot, + query, + orderBy, + collection, +} from "firebase/firestore"; + export default function App() { const [notes, setNotes] = useState([]); const [currentNoteId, setCurrentNoteId] = useState(""); const [tempNoteText, setTempNoteText] = useState(""); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); // indicator for saves + const [darkMode, setDarkMode] = useState(false); + const [searchText, setSearchText] = useState(""); - const currentNote = - notes.find((note) => note.id === currentNoteId) || notes[0]; - const sortedNotes = notes.sort((a, b) => b.updatedAt - a.updatedAt); + const getNotesCollection = useCallback(() => { + return notesCollection; + }, []); + // ---------- FETCH NOTES ---------- useEffect(() => { - const unsubscribe = onSnapshot(notesCollection, function (snapshot) { - const notesArr = snapshot.docs.map((doc) => ({ - ...doc.data(), - id: doc.id, - })); - setNotes(notesArr); - }); + setLoading(true); + const q = query(getNotesCollection(), orderBy("updatedAt", "desc")); + const unsubscribe = onSnapshot( + q, + (snapshot) => { + const notesArr = snapshot.docs.map((docSnap) => ({ + id: docSnap.id, + ...docSnap.data(), + })); + setNotes(notesArr); + setLoading(false); + }, + (error) => { + console.error("Failed to fetch notes:", error); + alert("Failed to load notes. Check your connection."); + setLoading(false); + } + ); return unsubscribe; - }, []); + }, [getNotesCollection]); + + const currentNote = notes.find((note) => note.id === currentNoteId) || notes[0]; + useEffect(() => { - if (!currentNoteId) { - setCurrentNoteId(notes[0]?.id); + if (currentNoteId && !notes.find((n) => n.id === currentNoteId)) { + setCurrentNoteId(notes[0]?.id || ""); } - }, [notes]); + }, [notes, currentNoteId]); + useEffect(() => { - if (currentNote) { - setTempNoteText(currentNote.body); - } + if (!currentNoteId && notes.length > 0) setCurrentNoteId(notes[0].id); + }, [notes, currentNoteId]); + + useEffect(() => { + if (currentNote) setTempNoteText(currentNote.body); }, [currentNote]); + useEffect(() => { + if (!currentNote) return; const timeoutId = setTimeout(() => { if (tempNoteText !== currentNote.body) { updateNote(tempNoteText); } }, 500); return () => clearTimeout(timeoutId); - }, [tempNoteText]); + }, [tempNoteText, currentNote]); + + + const createNoteInDB = async (note) => { + return addDoc(getNotesCollection(), note); + }; + const updateNoteInDB = async (noteId, data) => { + return setDoc(doc(getNotesCollection(), noteId), data, { merge: true }); + }; + const deleteNoteInDB = async (noteId) => { + return deleteDoc(doc(getNotesCollection(), noteId)); + }; + + // - Prevents creating multiple blank/empty notes and trims whitespace checks. + const createNewNote = useCallback(async () => { + const defaultText = "# Type your markdown note's title here"; + + if (notes.some((note) => (note.body || "").trim() === "" || note.body === defaultText)) { + alert("You already have a blank/empty note. Please edit it before creating a new one."); + return; + } - async function createNewNote() { const newNote = { - body: "# Type your markdown note's title here", + body: defaultText, createdAt: Date.now(), updatedAt: Date.now(), + pinned: false, + tags: [], }; - const newNoteRef = await addDoc(notesCollection, newNote); - setCurrentNoteId(newNoteRef.id); - } - - async function updateNote(text) { - const docRef = doc(db, "notes", currentNoteId); - await setDoc( - docRef, - { body: text, updatedAt: Date.now() }, - { merge: true } - ); - } - async function deleteNote(noteId) { - const docRef = doc(db, "notes", noteId); - await deleteDoc(docRef); - } + try { + setSaving(true); + const newNoteRef = await createNoteInDB(newNote); + setCurrentNoteId(newNoteRef.id); + } catch (error) { + console.error("Failed to create note:", error); + alert("Failed to create note. Check console for details."); + } finally { + setSaving(false); + } + }, [notes, createNoteInDB]); + + + const updateNote = useCallback( + async (text) => { + if (!currentNoteId) return; + + // Prevent saving empty content (trim check) + if ((text || "").trim() === "") { + console.warn("Skipped saving empty note."); + return; + } + + try { + setSaving(true); + await updateNoteInDB(currentNoteId, { body: text, updatedAt: Date.now() }); + } catch (error) { + console.error("Failed to update note:", error); + alert("Failed to update note. Check your connection or permissions."); + } finally { + setSaving(false); + } + }, + [currentNoteId, updateNoteInDB] + ); + + + const deleteNote = useCallback( + async (noteId) => { + const noteToDelete = notes.find((n) => n.id === noteId); + // added confirmation before deletion + const confirmDelete = window.confirm( + `Are you sure you want to delete "${noteToDelete?.body?.slice(0, 30) || "this note"}"?` + ); + if (!confirmDelete) return; + + try { + setSaving(true); + await deleteNoteInDB(noteId); + + + if (noteId === currentNoteId) { + setCurrentNoteId(notes[0]?.id || ""); + } + } catch (error) { + console.error("Failed to delete note:", error); + alert("Failed to delete note. Try again."); + } finally { + setSaving(false); + } + }, + [currentNoteId, notes, deleteNoteInDB] + ); + + // ---------- TOGGLE PIN ---------- + const togglePin = useCallback( + async (noteId) => { + try { + setSaving(true); + const note = notes.find((n) => n.id === noteId); + if (!note) return; + await updateNoteInDB(noteId, { pinned: !note.pinned, updatedAt: Date.now() }); + } catch (error) { + console.error("Failed to toggle pin:", error); + alert("Failed to pin/unpin. Try again."); + } finally { + setSaving(false); + } + }, + [notes, updateNoteInDB] + ); + + + // create a shallow copy before sorting so original state isn't mutated. + const sortedNotes = [...notes].sort((a, b) => { + if (a.pinned && !b.pinned) return -1; + if (!a.pinned && b.pinned) return 1; + return b.updatedAt - a.updatedAt; + }); + + // ---------- DARK MODE ---------- + useEffect(() => { + document.body.className = darkMode ? "dark" : "light"; + }, [darkMode]); + + + const [isMobile, setIsMobile] = useState( + typeof window !== "undefined" ? window.innerWidth <= 768 : false + ); + useEffect(() => { + const onResize = () => setIsMobile(window.innerWidth <= 768); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, []); + + if (loading) return
Loading notes...
; return (
+ { } +
+ + {/* Loading indicator for save operations */} + {saving ? "Saving…" : ""} +
+ {notes.length > 0 ? ( - + - + ) : (
diff --git a/src/components/Editor.jsx b/src/components/Editor.jsx index 8d04eee..f2d729b 100644 --- a/src/components/Editor.jsx +++ b/src/components/Editor.jsx @@ -1,9 +1,8 @@ import { useState } from "react"; import ReactMde from "react-mde"; -// const ReactMde = R.default import Showdown from "showdown"; -export default function Editor({ tempNoteText, setTempNoteText }) { +export default function Editor({ tempNoteText, setTempNoteText, darkMode }) { const [selectedTab, setSelectedTab] = useState("write"); const converter = new Showdown.Converter({ @@ -14,16 +13,14 @@ export default function Editor({ tempNoteText, setTempNoteText }) { }); return ( -
+
- Promise.resolve(converter.makeHtml(markdown)) - } - minEditorHeight={80} + generateMarkdownPreview={(markdown) => Promise.resolve(converter.makeHtml(markdown))} + minEditorHeight={100} heightUnits="vh" />
diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index b7d772a..8161ab4 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -1,31 +1,94 @@ -// export default function Sidebar(props) { - const noteElements = props.notes.map((note, index) => ( -
+ // Filter notes based on search input + const filteredNotes = props.notes.filter((note) => { + const search = props.searchText.toLowerCase(); + return note.body.toLowerCase().includes(search); + }); + + const noteElements = filteredNotes.map((note) => ( +
props.setCurrentNoteId(note.id)} + style={{ display: "flex", flexDirection: "column", width: "100%" }} > -

{note.body.split("\n")[0]}

- + + {/* Delete Button */} + +
+
+ + {/* Last Updated Timestamp */} +

- - + {note.updatedAt ? new Date(note.updatedAt).toLocaleString() : ""} +

)); return (
-
-

Notes

- +
+
+

Notes

+ +
+ + {/* Search Input */} + props.setSearchText(e.target.value)} + style={{ + padding: "4px 6px", + borderRadius: "4px", + border: "1px solid #ccc", + width: "100%", + boxSizing: "border-box", + }} + />
{noteElements}
diff --git a/src/css/style.css b/src/css/style.css index c6a3623..0a7086b 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -145,9 +145,7 @@ button:focus { border: none; } -.title:hover > .delete-btn { - display: block; -} + .trash-icon { cursor: pointer; @@ -194,3 +192,105 @@ button:focus { top: -7px; left: -2px; } + +/* ---------- Pin Button Fix ---------- */ +.pin-btn { + background: none; + border: none; + cursor: pointer; + font-size: 1rem; +} + +/* Always visible */ +.pin-btn { + display: inline-block; + margin-right: 5px; +} + +/* Align buttons in flex container */ +.title-buttons { + display: flex; + gap: 5px; +} + +/* ---------- Dark Mode ---------- */ +body.dark { + background-color: #1e1e2f; + color: #f5f5f5; +} + +body.dark .sidebar { + background-color: #2b2b3a; +} + +body.dark .editor { + background-color: #2b2b3a; + color: #f5f5f5; +} + +body.dark .text-snippet { + color: #f5f5f5; +} + +body.dark .selected-note { + background-color: #4a4e74; +} + +body.dark .new-note, +body.dark .first-note { + background-color: #4a4e74; + color: white; +} + +.title-buttons .delete-btn { + display: inline-block; /* always show */ + background: none; + border: none; + cursor: pointer; +} + +.title-buttons .pin-btn { + display: inline-block; + background: none; + border: none; + cursor: pointer; +} + + +/* dark mode new code */ +.editor-dark .mde-textarea { + background-color: #2b2b3a; + color: #f5f5f5; +} + +.editor-dark .mde-preview { + background-color: #2b2b3a; + color: #f5f5f5; +} + + + +body.dark { + background-color: #1e1e2f; + color: #f5f5f5; +} + +body.dark .sidebar { + background-color: #2b2b3a; +} + +body.dark .editor { + background-color: #1c1c28; + color: #f5f5f5; +} + +.editor-dark .mde-textarea { + background-color: #2b2b3a; + color: #f5f5f5; +} + +.editor-dark .mde-preview { + background-color: #2b2b3a; + color: #f5f5f5; +} + diff --git a/src/firebase.js b/src/firebase.js index 33901af..4c18783 100644 --- a/src/firebase.js +++ b/src/firebase.js @@ -1,7 +1,8 @@ import { initializeApp } from "firebase/app"; import { getFirestore, collection } from "firebase/firestore"; +import { getAuth } from "firebase/auth"; -// Your web app's Firebase configuration +// Firebase configuration const firebaseConfig = { apiKey: "AIzaSyCz1vI32TWBxaeQ2d_bgvCKPycKHDlYtKo", authDomain: "notes-app-6296.firebaseapp.com", @@ -13,5 +14,12 @@ const firebaseConfig = { // Initialize Firebase const app = initializeApp(firebaseConfig); + +// Firestore DB export const db = getFirestore(app); + +// Firebase Auth → Issue-1: No authentication previously +export const auth = getAuth(app); + +// Notes collection export const notesCollection = collection(db, "notes");