mirror of
https://github.com/Xevion/banner.git
synced 2026-01-30 22:23:32 -06:00
feat: add course search UI with ts-rs type bindings
Integrate ts-rs for Rust-to-TypeScript type generation, build course search page with filters, pagination, and expandable detail rows, and refactor theme toggle into a reactive store with view transition animation.
This commit is contained in:
+3
-1
@@ -3,9 +3,11 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// Represents a meeting time stored as JSONB in the courses table.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct DbMeetingTime {
|
||||
pub begin_time: Option<String>,
|
||||
pub end_time: Option<String>,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::data::models::ReferenceData;
|
||||
use crate::error::Result;
|
||||
use html_escape::decode_html_entities;
|
||||
use sqlx::PgPool;
|
||||
|
||||
/// Batch upsert reference data entries.
|
||||
@@ -12,7 +13,10 @@ pub async fn batch_upsert(entries: &[ReferenceData], db_pool: &PgPool) -> Result
|
||||
|
||||
let categories: Vec<&str> = entries.iter().map(|e| e.category.as_str()).collect();
|
||||
let codes: Vec<&str> = entries.iter().map(|e| e.code.as_str()).collect();
|
||||
let descriptions: Vec<&str> = entries.iter().map(|e| e.description.as_str()).collect();
|
||||
let descriptions: Vec<String> = entries
|
||||
.iter()
|
||||
.map(|e| decode_html_entities(&e.description).into_owned())
|
||||
.collect();
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
|
||||
@@ -206,6 +206,19 @@ impl Scheduler {
|
||||
|
||||
let mut all_entries = Vec::new();
|
||||
|
||||
// Terms (fetched via session pool, no active session needed)
|
||||
match banner_api.sessions.get_terms("", 1, 500).await {
|
||||
Ok(terms) => {
|
||||
debug!(count = terms.len(), "Fetched terms");
|
||||
all_entries.extend(terms.into_iter().map(|t| ReferenceData {
|
||||
category: "term".to_string(),
|
||||
code: t.code,
|
||||
description: t.description,
|
||||
}));
|
||||
}
|
||||
Err(e) => warn!(error = ?e, "Failed to fetch terms"),
|
||||
}
|
||||
|
||||
// Subjects
|
||||
match banner_api.get_subjects("", &term, 1, 500).await {
|
||||
Ok(pairs) => {
|
||||
|
||||
+3
-1
@@ -3,10 +3,12 @@ use std::time::Instant;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
/// Health status of a service.
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export)]
|
||||
pub enum ServiceStatus {
|
||||
Starting,
|
||||
Active,
|
||||
|
||||
+25
-18
@@ -18,6 +18,7 @@ use http::header;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use std::{collections::BTreeMap, time::Duration};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::state::AppState;
|
||||
use crate::status::ServiceStatus;
|
||||
@@ -227,14 +228,16 @@ async fn health() -> Json<Value> {
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ServiceInfo {
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct ServiceInfo {
|
||||
name: String,
|
||||
status: ServiceStatus,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StatusResponse {
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export)]
|
||||
pub struct StatusResponse {
|
||||
status: ServiceStatus,
|
||||
version: String,
|
||||
commit: String,
|
||||
@@ -316,9 +319,10 @@ fn default_limit() -> i32 {
|
||||
25
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CourseResponse {
|
||||
#[ts(export)]
|
||||
pub struct CourseResponse {
|
||||
crn: String,
|
||||
subject: String,
|
||||
course_number: String,
|
||||
@@ -340,32 +344,35 @@ struct CourseResponse {
|
||||
link_identifier: Option<String>,
|
||||
is_section_linked: Option<bool>,
|
||||
part_of_term: Option<String>,
|
||||
meeting_times: Value,
|
||||
attributes: Value,
|
||||
meeting_times: Vec<crate::data::models::DbMeetingTime>,
|
||||
attributes: Vec<String>,
|
||||
instructors: Vec<InstructorResponse>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InstructorResponse {
|
||||
#[ts(export)]
|
||||
pub struct InstructorResponse {
|
||||
banner_id: String,
|
||||
display_name: String,
|
||||
email: Option<String>,
|
||||
is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SearchResponse {
|
||||
#[ts(export)]
|
||||
pub struct SearchResponse {
|
||||
courses: Vec<CourseResponse>,
|
||||
total_count: i64,
|
||||
total_count: i32,
|
||||
offset: i32,
|
||||
limit: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CodeDescription {
|
||||
#[ts(export)]
|
||||
pub struct CodeDescription {
|
||||
code: String,
|
||||
description: String,
|
||||
}
|
||||
@@ -411,8 +418,8 @@ async fn build_course_response(
|
||||
link_identifier: course.link_identifier.clone(),
|
||||
is_section_linked: course.is_section_linked,
|
||||
part_of_term: course.part_of_term.clone(),
|
||||
meeting_times: course.meeting_times.clone(),
|
||||
attributes: course.attributes.clone(),
|
||||
meeting_times: serde_json::from_value(course.meeting_times.clone()).unwrap_or_default(),
|
||||
attributes: serde_json::from_value(course.attributes.clone()).unwrap_or_default(),
|
||||
instructors,
|
||||
}
|
||||
}
|
||||
@@ -454,7 +461,7 @@ async fn search_courses(
|
||||
|
||||
Ok(Json(SearchResponse {
|
||||
courses: course_responses,
|
||||
total_count,
|
||||
total_count: total_count as i32,
|
||||
offset,
|
||||
limit,
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user