feat: Implement student notes functionality for lessons, including API endpoints, database schema, and frontend UI.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
-- Migration: Create Student Notes Table
|
||||
-- Description: Allows students to save personal notes for each lesson.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS student_notes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
lesson_id UUID NOT NULL REFERENCES lessons(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE(user_id, lesson_id)
|
||||
);
|
||||
|
||||
-- Index for faster retrieval by user
|
||||
CREATE INDEX IF NOT EXISTS idx_student_notes_user_id ON student_notes(user_id);
|
||||
-- Index for faster retrieval by lesson (useful if we ever want to see all notes for a lesson as an instructor, though not requested yet)
|
||||
CREATE INDEX IF NOT EXISTS idx_student_notes_lesson_id ON student_notes(lesson_id);
|
||||
@@ -0,0 +1,53 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
use common::auth::Claims;
|
||||
use common::models::{SaveNotePayload, StudentNote};
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn get_note(
|
||||
claims: Claims,
|
||||
Path(lesson_id): Path<Uuid>,
|
||||
State(pool): State<PgPool>,
|
||||
) -> Result<Json<Option<StudentNote>>, (StatusCode, String)> {
|
||||
let note = sqlx::query_as::<_, StudentNote>(
|
||||
"SELECT * FROM student_notes WHERE user_id = $1 AND lesson_id = $2",
|
||||
)
|
||||
.bind(claims.sub)
|
||||
.bind(lesson_id)
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(note))
|
||||
}
|
||||
|
||||
pub async fn save_note(
|
||||
claims: Claims,
|
||||
Path(lesson_id): Path<Uuid>,
|
||||
State(pool): State<PgPool>,
|
||||
Json(payload): Json<SaveNotePayload>,
|
||||
) -> Result<Json<StudentNote>, (StatusCode, String)> {
|
||||
let note = sqlx::query_as::<_, StudentNote>(
|
||||
r#"
|
||||
INSERT INTO student_notes (user_id, lesson_id, content)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (user_id, lesson_id)
|
||||
DO UPDATE SET
|
||||
content = EXCLUDED.content,
|
||||
updated_at = NOW()
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(claims.sub)
|
||||
.bind(lesson_id)
|
||||
.bind(payload.content)
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(note))
|
||||
}
|
||||
@@ -2,6 +2,7 @@ mod db_util;
|
||||
mod handlers;
|
||||
mod handlers_announcements;
|
||||
mod handlers_discussions;
|
||||
mod handlers_notes;
|
||||
mod handlers_payments;
|
||||
|
||||
use axum::{
|
||||
@@ -147,6 +148,8 @@ async fn main() {
|
||||
"/announcements/{id}",
|
||||
delete(handlers_announcements::delete_announcement),
|
||||
)
|
||||
.route("/lessons/{id}/notes", get(handlers_notes::get_note))
|
||||
.route("/lessons/{id}/notes", put(handlers_notes::save_note))
|
||||
.route_layer(middleware::from_fn(
|
||||
common::middleware::org_extractor_middleware,
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user