use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[cfg(feature = "postgres")]
use sqlx::{FromRow, PgPool};
#[cfg_attr(feature = "postgres", derive(FromRow))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LearningTrack {
pub id: Uuid,
pub slug: String,
pub title: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub body: Option<String>,
pub is_published: bool,
pub sort_order: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[cfg_attr(feature = "postgres", derive(FromRow))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LearningLesson {
pub id: Uuid,
pub track_id: Uuid,
pub slug: String,
pub title: String,
pub body: String,
pub sort_order: i32,
}
#[cfg_attr(feature = "postgres", derive(FromRow))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LearningRecipe {
pub id: Uuid,
pub slug: String,
pub title: String,
#[serde(default)]
pub description: Option<String>,
pub body: String,
pub tags: Vec<String>,
pub is_published: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[cfg_attr(feature = "postgres", derive(FromRow))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LearningProgress {
pub user_id: Uuid,
pub lesson_id: Uuid,
pub completed_at: DateTime<Utc>,
}
#[cfg(feature = "postgres")]
impl LearningTrack {
pub async fn list_published(pool: &PgPool) -> sqlx::Result<Vec<Self>> {
sqlx::query_as::<_, Self>(
"SELECT * FROM learning_tracks WHERE is_published = TRUE ORDER BY sort_order ASC",
)
.fetch_all(pool)
.await
}
pub async fn find_by_slug(pool: &PgPool, slug: &str) -> sqlx::Result<Option<Self>> {
sqlx::query_as::<_, Self>("SELECT * FROM learning_tracks WHERE slug = $1")
.bind(slug)
.fetch_optional(pool)
.await
}
}
#[cfg(feature = "postgres")]
impl LearningLesson {
pub async fn list_by_track(pool: &PgPool, track_id: Uuid) -> sqlx::Result<Vec<Self>> {
sqlx::query_as::<_, Self>(
"SELECT * FROM learning_lessons WHERE track_id = $1 ORDER BY sort_order ASC",
)
.bind(track_id)
.fetch_all(pool)
.await
}
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> sqlx::Result<Option<Self>> {
sqlx::query_as::<_, Self>("SELECT * FROM learning_lessons WHERE id = $1")
.bind(id)
.fetch_optional(pool)
.await
}
}
#[cfg(feature = "postgres")]
impl LearningRecipe {
pub async fn list_published(pool: &PgPool, tag: Option<&str>) -> sqlx::Result<Vec<Self>> {
match tag {
Some(t) => {
sqlx::query_as::<_, Self>(
"SELECT * FROM learning_recipes WHERE is_published = TRUE AND $1 = ANY(tags) \
ORDER BY created_at DESC",
)
.bind(t)
.fetch_all(pool)
.await
}
None => sqlx::query_as::<_, Self>(
"SELECT * FROM learning_recipes WHERE is_published = TRUE ORDER BY created_at DESC",
)
.fetch_all(pool)
.await,
}
}
pub async fn find_by_slug(pool: &PgPool, slug: &str) -> sqlx::Result<Option<Self>> {
sqlx::query_as::<_, Self>("SELECT * FROM learning_recipes WHERE slug = $1")
.bind(slug)
.fetch_optional(pool)
.await
}
}
#[cfg(feature = "postgres")]
impl LearningProgress {
pub async fn mark_completed(pool: &PgPool, user_id: Uuid, lesson_id: Uuid) -> sqlx::Result<()> {
sqlx::query(
"INSERT INTO learning_progress (user_id, lesson_id) VALUES ($1, $2) \
ON CONFLICT DO NOTHING",
)
.bind(user_id)
.bind(lesson_id)
.execute(pool)
.await?;
Ok(())
}
pub async fn list_for_user(pool: &PgPool, user_id: Uuid) -> sqlx::Result<Vec<Self>> {
sqlx::query_as::<_, Self>(
"SELECT * FROM learning_progress WHERE user_id = $1 ORDER BY completed_at DESC",
)
.bind(user_id)
.fetch_all(pool)
.await
}
}