Module 7: Notes CRUD – Update & Delete

අපේ CRUD (Create, Read, Update, Delete) ක්‍රියාවලිය සම්පූර්ණ කිරීමට අවශ්‍ය Update සහ Delete functionalities ගොඩනගමු.

1. Dynamic API Routes (`/api/notes/[id]`)

නිශ්චිත සටහනක් (note) update කිරීමට හෝ delete කිරීමට අපට එම සටහනේ ID එක අවශ්‍ය වේ. Next.js වලදී, URL එකේ dynamic segments (වෙනස් වන කොටස්) හසුරුවා ගැනීමට square brackets [ ] භාවිතා කරයි. උදාහරණයක් ලෙස, /api/notes/1, /api/notes/45 වැනි requests handle කිරීමට අපි [id] නමින් dynamic parameter එකක් සහිත route එකක් සාදමු.

app/api/notes/[id]/route.ts යන path එකට අලුත් file එකක් සාදා පහත කේතය ඇතුළත් කරන්න. මෙම file එක PUT (Update) සහ DELETE requests දෙකටම handle කරයි.

import { NextRequest, NextResponse } from 'next/server';
import db from '@/lib/db';
import { cookies } from 'next/headers';
import jwt from 'jsonwebtoken';

interface UserPayload {
  userId: number;
}

// Helper to get user and check ownership of the note
function verifyUserAndNote(noteId: number) {
  const token = cookies().get('session_token')?.value;
  if (!token) return null;

  try {
    const user = jwt.verify(token, process.env.JWT_SECRET!) as UserPayload;
    const note = db.prepare('SELECT user_id FROM notes WHERE id = ?').get(noteId) as { user_id: number };

    // Check if the note exists and belongs to the logged-in user
    if (note && note.user_id === user.userId) {
      return user;
    }
    return null;
  } catch (error) {
    return null;
  }
}

// PUT: Update an existing note
export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
  const noteId = parseInt(params.id, 10);
  const user = verifyUserAndNote(noteId);

  if (!user) {
    return NextResponse.json({ error: 'Unauthorized or Note not found' }, { status: 403 });
  }

  try {
    const { title, content } = await request.json();
    if (!title || !content) {
      return NextResponse.json({ error: 'Title and content are required' }, { status: 400 });
    }

    const stmt = db.prepare('UPDATE notes SET title = ?, content = ? WHERE id = ?');
    stmt.run(title, content, noteId);

    return NextResponse.json({ message: 'Note updated successfully' });
  } catch (error) {
    return NextResponse.json({ error: 'Failed to update note' }, { status: 500 });
  }
}

// DELETE: Delete a note
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
  const noteId = parseInt(params.id, 10);
  const user = verifyUserAndNote(noteId);

  if (!user) {
    return NextResponse.json({ error: 'Unauthorized or Note not found' }, { status: 403 });
  }

  const stmt = db.prepare('DELETE FROM notes WHERE id = ?');
  stmt.run(noteId);

  return NextResponse.json({ message: 'Note deleted successfully' });
}

කේතය පැහැදිලි කිරීම:


2. Frontend එකට Edit සහ Delete Buttons එකතු කිරීම

දැන් අපි Module 6 හි සෑදූ notes list එකට, එක් එක් note එකට අදාළව Edit සහ Delete buttons එකතු කරමු. මේ සඳහා, අපිට notes list එක client component එකක් බවට පත් කිරීමට සිදුවේ, මන්ද user interactions (button clicks) handle කළ යුත්තේ client-side එකේදීය.

➡️ NotesList.tsx Component එක සෑදීම

components folder එක තුළ NotesList.tsx නමින් අලුත් file එකක් සාදන්න. /notes/page.tsx එකේ තිබූ list එකට අදාළ logic එක අපි මෙතැනට ගෙන එමු.

'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

// Define the type for a single note
interface Note {
  id: number;
  title: string;
  content: string;
  created_at: string;
}

// Define the props for the component
interface NotesListProps {
  initialNotes: Note[];
}

export default function NotesList({ initialNotes }: NotesListProps) {
  const [notes, setNotes] = useState(initialNotes);
  const router = useRouter();

  const handleDelete = async (id: number) => {
    // Optional: Add a confirmation dialog
    if (confirm('Are you sure you want to delete this note?')) {
      await fetch(`/api/notes/${id}`, { method: 'DELETE' });
      // Remove the note from the state to update the UI instantly
      setNotes(notes.filter(note => note.id !== id));
    }
  };

  // We will add Edit functionality in the next step
  const handleEdit = (note: Note) => {
    // For now, just log to console
    console.log('Editing note:', note);
    alert('Edit functionality will be implemented with a modal.');
  };

  return (
    <div className="list-group">
      {notes.length > 0 ? (
        notes.map(note => (
          <div key={note.id} className="list-group-item list-group-item-action">
            <div className="d-flex w-100 justify-content-between">
              <h5 className="mb-1">{note.title}</h5>
              <small>{new Date(note.created_at).toLocaleDateString()}</small>
            </div>
            <p className="mb-1">{note.content}</p>
            <div className="mt-2">
              <button className="btn btn-sm btn-secondary me-2" onClick={() => handleEdit(note)}>Edit</button>
              <button className="btn btn-sm btn-danger" onClick={() => handleDelete(note.id)}>Delete</button>
            </div>
          </div>
        ))
      ) : (
        <p>You haven't created any notes yet.</p>
      )}
    </div>
  );
}

3. /notes/page.tsx යාවත්කාලීන කිරීම

දැන් අපේ NotesPage (Server Component) එක සරල කරමු. එහිදී server එකෙන් notes fetch කර, අලුතින් සෑදූ NotesList (Client Component) එකට props ලෙස pass කරමු. මේ ක්‍රමය "Passing Server Components to Client Components" ලෙස හැඳින්වේ.

app/notes/page.tsx file එක පහත පරිදි update කරන්න.

import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import jwt from 'jsonwebtoken';
import db from '@/lib/db';
import AddNoteForm from '@/components/AddNoteForm';
import NotesList from '@/components/NotesList'; // Import the new client component

// Types remain the same
interface Note {
  id: number;
  title: string;
  content: string;
  created_at: string;
}

interface UserPayload {
  userId: number;
}

async function getNotesForUser(): Promise<Note[]> {
  const token = cookies().get('session_token')?.value;
  if (!token) redirect('/auth/login');

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!) as UserPayload;
    return db.prepare('SELECT * FROM notes WHERE user_id = ? ORDER BY created_at DESC').all(decoded.userId) as Note[];
  } catch (error) {
    redirect('/auth/login');
  }
}

export default async function NotesPage() {
  const initialNotes = await getNotesForUser();

  return (
    <div>
      <h1>My Notes</h1>
      <AddNoteForm />
      {/* Pass the server-fetched notes to the client component */}
      <NotesList initialNotes={initialNotes} />
    </div>
  );
}

දැන් Delete button එක click කළ විට, note එක UI එකෙන් සහ database එකෙන් ඉවත් විය යුතුය!


4. Edit Modal එක නිර්මාණය කිරීම

Note එකක් edit කිරීමට වෙනම පිටුවකට යනවාට වඩා, එම පිටුවේම pop-up වන modal එකක් භාවිතා කිරීම user experience එකට වඩා හොඳයි. අපි NotesList.tsx component එකටම Edit Modal එකට අදාළ logic එක එකතු කරමු.

components/NotesList.tsx file එක නැවතත් පහත පරිදි update කරන්න.

'use client';

import { useState, useEffect } from 'react';
// ... other imports

// (Note and NotesListProps interfaces remain the same)

export default function NotesList({ initialNotes }: NotesListProps) {
  const [notes, setNotes] = useState(initialNotes);
  const [editingNote, setEditingNote] = useState<Note | null>(null);
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');
  
  // When editingNote changes, update the form fields
  useEffect(() => {
    if (editingNote) {
      setTitle(editingNote.title);
      setContent(editingNote.content);
    }
  }, [editingNote]);

  const handleDelete = async (id: number) => { /* ... same as before ... */ };

  const handleEdit = (note: Note) => {
    setEditingNote(note);
  };
  
  const handleUpdate = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!editingNote) return;

    const res = await fetch(`/api/notes/${editingNote.id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title, content }),
    });

    if (res.ok) {
      // Update the note in the local state
      setNotes(notes.map(n => (n.id === editingNote.id ? { ...n, title, content } : n)));
      // Close the modal
      setEditingNote(null);
    }
  };

  return (
    <>
      {/* The Notes List (same as before) */}
      <div className="list-group">
        {/* ... map function ... */}
      </div>

      {/* Edit Modal */}
      {editingNote && (
        <div className="modal" style={{ display: 'block', backgroundColor: 'rgba(0,0,0,0.5)' }}>
          <div className="modal-dialog">
            <div className="modal-content">
              <form onSubmit={handleUpdate}>
                <div className="modal-header">
                  <h5 className="modal-title">Edit Note</h5>
                  <button type="button" className="btn-close" onClick={() => setEditingNote(null)}></button>
                </div>
                <div className="modal-body">
                  <div className="mb-3">
                    <label className="form-label">Title</label>
                    <input type="text" className="form-control" value={title} onChange={e => setTitle(e.target.value)} required />
                  </div>
                  <div className="mb-3">
                    <label className="form-label">Content</label>
                    <textarea className="form-control" value={content} onChange={e => setContent(e.target.value)} required />
                  </div>
                </div>
                <div className="modal-footer">
                  <button type="button" className="btn btn-secondary" onClick={() => setEditingNote(null)}>Close</button>
                  <button type="submit" className="btn btn-primary">Save changes</button>
                </div>
              </form>
            </div>
          </div>
        </div>
      )}
    </>
  );
}

නව වෙනස්කම් පැහැදිලි කිරීම: