Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 72 additions & 24 deletions src/components/NoteEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useState, useEffect } from "react";
import { Note, formatTimestamp, getRelativeTime } from "@/types/note";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Trash2, Calendar, Clock } from "lucide-react";
import { useState, useEffect } from 'react';
import { Note, formatTimestamp, getRelativeTime } from '@/types/note';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Button } from '@/components/ui/button';
import { Trash2, Calendar, Clock, Save } from 'lucide-react';

import {
AlertDialog,
AlertDialogAction,
Expand All @@ -14,7 +15,9 @@
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
} from '@/components/ui/alert-dialog';
import { toast } from "sonner";


interface NoteEditorProps {
note: Note;
Expand All @@ -28,56 +31,97 @@
export function NoteEditor({ note, onUpdate, onDelete }: NoteEditorProps) {
const [title, setTitle] = useState(note.title);
const [content, setContent] = useState(note.content);
const [hasInteracted, setHasInteracted] = useState(false);


useEffect(() => {
setTitle(note.title);
setContent(note.content);
setHasInteracted(false);
}, [note.id, note.title, note.content]);

useEffect(() => {
const timer = setTimeout(() => {
if (title !== note.title || content !== note.content) {
onUpdate(note.id, { title, content });

const trimmedTitle = title.trim();
const trimmedContent = content.trim();

const isValid =
trimmedTitle &&
trimmedContent;

const hasChanged =
trimmedTitle !== note.title || trimmedContent !== note.content;


if (!isValid && hasInteracted) {
toast.warning('Please use a non-Empty title and non-empty content to save your note.');
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent capitalization: 'non-Empty' should be 'non-empty' to match the style used elsewhere.

Suggested change
toast.warning('Please use a non-Empty title and non-empty content to save your note.');
toast.warning('Please use a non-empty title and non-empty content to save your note.');

Copilot uses AI. Check for mistakes.
return;
}


if (hasInteracted && isValid && hasChanged) {
onUpdate(note.id, {
title: trimmedTitle,
content: trimmedContent,
});
}
}, 500);

return () => clearTimeout(timer);
}, [title, content, note.id, note.title, note.content, onUpdate]);

Check warning on line 72 in src/components/NoteEditor.tsx

View workflow job for this annotation

GitHub Actions / ci (18.x)

React Hook useEffect has a missing dependency: 'hasInteracted'. Either include it or remove the dependency array

Check warning on line 72 in src/components/NoteEditor.tsx

View workflow job for this annotation

GitHub Actions / ci (20.x)

React Hook useEffect has a missing dependency: 'hasInteracted'. Either include it or remove the dependency array

Check warning on line 72 in src/components/NoteEditor.tsx

View workflow job for this annotation

GitHub Actions / ci (22.x)

React Hook useEffect has a missing dependency: 'hasInteracted'. Either include it or remove the dependency array


return (
<div className="flex flex-col h-full">
<div className="flex items-center justify-between">
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
onChange={(e) => {
setTitle(e.target.value);
setHasInteracted(true);
}
}
className="text-2xl font-semibold border-none shadow-none focus-visible:ring-0 px-0"
placeholder="Note title..."
/>
<Button
variant="ghost"
size="icon"
title="Save Note"
className="text-teal-600 hover:text-white hover:bg-teal-600"
onClick={() => {
const trimmedTitle = title.trim();
const trimmedContent = content.trim();

if (!trimmedTitle || !trimmedContent) {
toast.error('Cannot save: Content and Title can not be empty ');
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra space at the end of the error message should be removed, and 'can not' should be 'cannot' for consistency.

Suggested change
toast.error('Cannot save: Content and Title can not be empty ');
toast.error('Cannot save: Content and Title cannot be empty.');

Copilot uses AI. Check for mistakes.
return;
}


onUpdate(note.id, { title: trimmedTitle, content: trimmedContent });
toast.success('Note saved successfully!');
}}
>
<Save className="h-5 w-5" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-destructive hover:text-white"
title="Delete Note"
>
<Button variant="ghost" size="icon" className="text-destructive hover:text-white" title="Delete Note">
<Trash2 className="h-5 w-5" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Note</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this note? This action cannot be
undone.
Are you sure you want to delete this note? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => onDelete(note.id)}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
<AlertDialogAction onClick={() => onDelete(note.id)} className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete
</AlertDialogAction>
</AlertDialogFooter>
Expand All @@ -86,7 +130,7 @@
</div>

{/* Timestamp Display */}
<div className="flex items-center gap-4 text-sm text-muted-foreground pb-2">
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1.5">
<Calendar className="w-4 h-4" />
<span>Created: {formatTimestamp(note.createdAt)}</span>
Expand All @@ -99,7 +143,11 @@
</div>
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
onChange={(e) => {
setContent(e.target.value);
setHasInteracted(true);
}
}
className="flex-1 resize-none border-none shadow-none focus-visible:ring-0 text-base leading-relaxed"
placeholder="Start writing..."
/>
Expand Down
42 changes: 39 additions & 3 deletions src/hooks/useNotes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { useState, useEffect } from 'react';
import { Note } from '@/types/note';

import { toast } from "sonner";
import { title } from 'process';
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import 'title' from 'process' module should be removed as it's not used anywhere in the code.

Suggested change
import { title } from 'process';

Copilot uses AI. Check for mistakes.
const STORAGE_KEY = 'notes-app-data';

//Add validation : Avanish
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed grammatical error in comment from 'Add validation' to 'Added validation'.

Suggested change
//Add validation : Avanish
// Added validation : Avanish

Copilot uses AI. Check for mistakes.


function isValidNote(note: Note): boolean {
const trimmedTitle = note.title.trim().toLowerCase();
const trimmedContent = note.content.trim();
return trimmedTitle !== '' && trimmedContent !== '';
}


export function useNotes() {
const [notes, setNotes] = useState<Note[]>([]);

Expand All @@ -26,7 +37,8 @@ export function useNotes() {

// Save notes to localStorage whenever they change
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(notes));
const validNotes = notes.filter(isValidNote);
localStorage.setItem(STORAGE_KEY, JSON.stringify(validNotes));
Comment on lines +40 to +41
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filtering out invalid notes during localStorage save can cause data loss. Invalid notes that users are actively editing will be permanently removed. Consider saving all notes but marking invalid ones differently.

Suggested change
const validNotes = notes.filter(isValidNote);
localStorage.setItem(STORAGE_KEY, JSON.stringify(validNotes));
// Save all notes, but mark invalid ones with an 'invalid' flag
const notesToSave = notes.map(note => ({
...note,
invalid: !isValidNote(note),
}));
localStorage.setItem(STORAGE_KEY, JSON.stringify(notesToSave));

Copilot uses AI. Check for mistakes.
}, [notes]);

const getUniqueTitle = (baseTitle: string, excludeId?: string) => {
Expand All @@ -52,7 +64,13 @@ export function useNotes() {
return baseTitle;
};

const createNote = () => {
const createNote = (currentNote?: Note) => {

if (currentNote && !isValidNote(currentNote)) {
toast.warning('Please enter a valid title and non-empty content before creating a new note.');
return null; // Prevent creation
}

const title = getUniqueTitle('Untitled Note');
const newNote: Note = {
id: crypto.randomUUID(),
Expand All @@ -66,6 +84,24 @@ export function useNotes() {
};

const updateNote = (id: string, updates: Partial<Pick<Note, 'title' | 'content'>>) => {

//Validate fro the emppty title ans empty content : Avanish
Copy link

Copilot AI Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple spelling errors in comment: 'fro' should be 'for', 'emppty' should be 'empty', and 'ans' should be 'and'.

Suggested change
//Validate fro the emppty title ans empty content : Avanish
// Validate for the empty title and empty content : Avanish

Copilot uses AI. Check for mistakes.

const trimmedTitle = updates.title?.trim();
const trimmedContent = updates.content?.trim();

if ((trimmedTitle !== undefined || trimmedContent !== undefined) && !isValidNote({
id,
title: trimmedTitle ?? '',
content: trimmedContent ?? '',
createdAt: new Date(),
updatedAt: new Date(),
})
) {
toast.warning('Please enter a valid title and non-empty content to update the note.');
return;
}

setNotes((prev) =>
prev.map((note) => {
if (note.id === id) {
Expand Down
12 changes: 12 additions & 0 deletions src/pages/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ const Index = () => {
event.preventDefault();
event.stopPropagation();
const currentNote = notes.find((note) => note.id === activeNoteId);

if (!currentNote) return;

const trimmedTitle = currentNote.title.trim();
const trimmedContent = currentNote.content.trim();

if (!trimmedTitle || !trimmedContent) {
toast.error("Cannot save: Please enter a valid title and non-empty content.");
return;
}


if (currentNote) {
toast.success("Notes are saved automatically");
}
Expand Down
Loading