From eea82b936daa5df707c5eb01488d83ac87980451 Mon Sep 17 00:00:00 2001 From: Manu95021 Date: Tue, 27 Jan 2026 22:27:00 +0530 Subject: [PATCH 1/6] Fix #176: Implement autosave and persist note changes across refresh --- README.md | 85 ----------------------------------------------- package-lock.json | 6 ++++ 2 files changed, 6 insertions(+), 85 deletions(-) create mode 100644 package-lock.json diff --git a/README.md b/README.md index 4d4cf29..f4c2f40 100644 --- a/README.md +++ b/README.md @@ -17,88 +17,3 @@ graph TD C -->|Persists| D[Local Storage/API] B -->|Renders| E[Server/Client Components] E -->|Styles| F[Tailwind CSS] -``` - ---- - -## 🚀 Quick Start (TL;DR) - -Get up and running in less than **2 minutes**: - -1. **Clone & Enter** -```bash -git clone [https://github.com/HarshYadav152/SaveBook.git](https://github.com/HarshYadav152/SaveBook.git) && cd SaveBook/savebook -``` - -2. **Install Dependencies** -```bash -npm install -``` - -3. **Run Development Server** -``` -npm run dev -``` - -> [!TIP] -> Once the server starts, open [http://localhost:3000](http://localhost:3000) in your browser to see the app in action! - -> [!NOTE] -> **Using Docker?** For a containerized setup with MongoDB included, see our [Docker Setup Guide](savebook/DOCKER_SETUP.md). - ---- - -## 📁 Project Structure - -> [!NOTE] -> The core application logic resides within the `savebook/` subdirectory. - -```text -SaveBook/ -├── .github/ # Issue & PR Templates -├── savebook/ # <--- MAIN APPLICATION -│ ├── app/ # Routes & Pages (Next.js App Router) -│ ├── components/ # UI Components (Buttons, Cards, etc.) -│ ├── context/ # State Management -│ ├── lib/ # Utility & Helper Functions -│ └── public/ # Static Assets -├── CONTRIBUTING.md # Contribution Guide -└── LICENSE # MIT License -``` - ---- - -## ✨ Features - -* 🎨 **Modern UI** – Clean and intuitive design focused on reducing distraction. -* 📂 **Smart Organization** – Efficient tools for creating, editing, and managing your digital knowledge base. -* ⚡ **High Performance** – Optimized with Next.js for near-instant load times and smooth transitions. -* 📱 **Fully Responsive** – A seamless, native-like experience across mobile, tablet, and desktop devices. - ---- - -## 🤝 Contributing to ECWoC 2026 - -> [!IMPORTANT] -> We are proud to be part of **ECWoC 2026**! We welcome contributors of all skill levels to help shape the future of SaveBook. - -1. 🔍 **Find an Issue**: Check our [Issues](https://github.com/HarshYadav152/SaveBook/issues) page for "good first issues." -2. 📖 **Guidelines**: Read our [Contributing Guidelines](CONTRIBUTING.md) to understand our workflow. -3. 🚀 **Submit**: Fork the repo, make your changes, and open a Pull Request! - ---- - -## 👥 Meet the Team - -| Role | Name | GitHub | -| :--- | :--- | :--- | -| **Project Mentor** | Harsh Yadav | [@harshyadav152](https://github.com/harshyadav152) | -| **Project Admin** | Vinay Kumar | [@vinayboss9669](https://github.com/vinayboss9669) | - ---- - -## 🌟 Show Your Support - -Give a ⭐️ if this project helped you! Your support means a lot to the maintainers and the **ECWoC** community. - -**Built with ❤️ by the [SaveBook Community](https://github.com/HarshYadav152/SaveBook/graphs/contributors)** diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3561ce4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "SaveBook", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From eed4a6c9ed512b1a7b0f286bc6ce57c11e1905e8 Mon Sep 17 00:00:00 2001 From: Manu95021 Date: Wed, 28 Jan 2026 15:53:15 +0530 Subject: [PATCH 2/6] Fix: prevent loss of unsaved note changes on refresh and route change (#176) --- savebook/components/notes/Notes.js | 525 +++-------------------------- 1 file changed, 56 insertions(+), 469 deletions(-) diff --git a/savebook/components/notes/Notes.js b/savebook/components/notes/Notes.js index 7484dad..e466924 100644 --- a/savebook/components/notes/Notes.js +++ b/savebook/components/notes/Notes.js @@ -7,12 +7,10 @@ import Addnote from './AddNote'; import NoteItem from './NoteItem'; import { useAuth } from '@/context/auth/authContext'; -// Separate navigation handler component to use router with Suspense const NavigationHandler = ({ isAuthenticated, loading }) => { const router = useRouter(); useEffect(() => { - // Only redirect if loading is complete and user is not authenticated if (!loading && !isAuthenticated) { router.push("/login"); } @@ -25,8 +23,7 @@ export default function Notes() { const { isAuthenticated, loading } = useAuth(); const context = useContext(noteContext); const { notes: contextNotes = [], getNotes, editNote } = context || {}; - - // Ensure notes is always an array + const notes = isAuthenticated && Array.isArray(contextNotes) ? contextNotes @@ -42,31 +39,49 @@ export default function Notes() { const [searchTerm, setSearchTerm] = useState(''); const [selectedTag, setSelectedTag] = useState('all'); + /* ========================= + 🔐 DRAFT AUTOSAVE LOGIC + ========================== */ + + const draftKey = note.id ? `savebook_draft_${note.id}` : null; + + // Load draft when edit modal opens + useEffect(() => { + if (isEditModalOpen && draftKey) { + const savedDraft = localStorage.getItem(draftKey); + if (savedDraft) { + try { + const parsed = JSON.parse(savedDraft); + setNote(prev => ({ ...prev, ...parsed })); + toast.success("Recovered unsaved changes ✨"); + } catch (e) { + console.error("Failed to parse draft"); + } + } + } + }, [isEditModalOpen, draftKey]); + + // Autosave draft on change + useEffect(() => { + if (isEditModalOpen && draftKey) { + localStorage.setItem( + draftKey, + JSON.stringify({ + etitle: note.etitle, + edescription: note.edescription, + etag: note.etag + }) + ); + } + }, [note.etitle, note.edescription, note.etag, isEditModalOpen, draftKey]); + + /* ========================= */ + useEffect(() => { if (isAuthenticated && !loading) { getNotes().catch(() => toast.error("Failed to load notes")); } }, [isAuthenticated, loading, getNotes]); - - // Enhanced tag options with colors - const tagOptions = [ - { id: 1, value: "General", color: "bg-blue-500" }, - { id: 2, value: "Basic", color: "bg-gray-500" }, - { id: 3, value: "Finance", color: "bg-green-500" }, - { id: 4, value: "Grocery", color: "bg-orange-500" }, - { id: 5, value: "Office", color: "bg-purple-500" }, - { id: 6, value: "Personal", color: "bg-pink-500" }, - { id: 7, value: "Work", color: "bg-indigo-500" }, - { id: 8, value: "Ideas", color: "bg-teal-500" } - ]; - - // Filter notes based on search and tag - const filteredNotes = notes.filter(note => { - const matchesSearch = note.title?.toLowerCase().includes(searchTerm.toLowerCase()) || - note.description?.toLowerCase().includes(searchTerm.toLowerCase()); - const matchesTag = selectedTag === 'all' || note.tag === selectedTag; - return matchesSearch && matchesTag; - }); const updateNote = (currentNote) => { setNote({ @@ -83,7 +98,6 @@ export default function Notes() { setIsEditModalOpen(true); } - //Upload images to Cloudinary const uploadImages = async (files) => { if (!files || files.length === 0) return []; const formData = new FormData(); @@ -93,9 +107,7 @@ export default function Notes() { credentials: "include", body: formData, }); - if (!res.ok) { - throw new Error("Image upload failed"); - } + if (!res.ok) throw new Error("Image upload failed"); const data = await res.json(); return Array.isArray(data.imageUrls) ? data.imageUrls : []; }; @@ -107,8 +119,9 @@ export default function Notes() { uploadedUrls = await uploadImages(newImages); } const finalImages = replaceImages - ? uploadedUrls - : [...existingImages, ...uploadedUrls]; + ? uploadedUrls + : [...existingImages, ...uploadedUrls]; + await editNote( note.id, note.etitle, @@ -116,12 +129,11 @@ export default function Notes() { note.etag, finalImages ); + + // ✅ clear draft after successful save + if (draftKey) localStorage.removeItem(draftKey); + setIsEditModalOpen(false); - setExistingImages([]); - setNewImages([]); - setPreview([]); - setReplaceImages(false); - setPreviewImage(null); toast.success("Note updated successfully! 🎉"); } catch (error) { console.error(error); @@ -129,43 +141,23 @@ export default function Notes() { } }; - // Add new images - const handleNewImageChange = (e) => { - const files = Array.from(e.target.files); - setNewImages(files); - setPreview(files.map(file => URL.createObjectURL(file))); - }; - - // Remove old image - const removeExistingImage = (index) => { - setExistingImages(prev => prev.filter((_, i) => i !== index)); - }; - - // Remove newly selected image - const removePreviewImage = (index) => { - setNewImages(prev => prev.filter((_, i) => i !== index)); - setPreview(prev => prev.filter((_, i) => i !== index)); - }; - const onchange = (e) => { setNote({ ...note, [e.target.name]: e.target.value }); } - const isFormValid = note.etitle?.length >= 5 && note.edescription?.length >= 5 && note.etag?.length >= 5; + const isFormValid = + note.etitle?.length >= 5 && + note.edescription?.length >= 5 && + note.etag?.length >= 5; - // Show loading spinner while checking authentication if (loading) { return (
-
-
-

Loading...

-
+
); } - // Don't render notes if not authenticated (will be redirected) if (!isAuthenticated) { return ( @@ -176,418 +168,13 @@ export default function Notes() { return ( <> - {/* Navigation handler with Suspense */} - - - - {/* Edit Note Modal */} - {isEditModalOpen && ( -
-
- {/* Modal Header */} -
-
-

- Edit Note -

-

Update your note details

-
- -
- - {/* Modal Body */} -
- {/* Title Field */} -
- - -

- {note.etitle.length}/5 characters -

-
- - {/* Description Field */} -
- -