mirror of
https://github.com/Xevion/banner.git
synced 2026-01-31 02:23:34 -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:
+68
-16
@@ -1,24 +1,41 @@
|
||||
import type {
|
||||
CodeDescription,
|
||||
CourseResponse,
|
||||
DbMeetingTime,
|
||||
InstructorResponse,
|
||||
SearchResponse as SearchResponseGenerated,
|
||||
ServiceInfo,
|
||||
ServiceStatus,
|
||||
StatusResponse,
|
||||
} from "$lib/bindings";
|
||||
|
||||
const API_BASE_URL = "/api";
|
||||
|
||||
// Re-export generated types under their canonical names
|
||||
export type {
|
||||
CodeDescription,
|
||||
CourseResponse,
|
||||
DbMeetingTime,
|
||||
InstructorResponse,
|
||||
ServiceInfo,
|
||||
ServiceStatus,
|
||||
StatusResponse,
|
||||
};
|
||||
|
||||
// Semantic aliases — these all share the CodeDescription shape
|
||||
export type Term = CodeDescription;
|
||||
export type Subject = CodeDescription;
|
||||
export type ReferenceEntry = CodeDescription;
|
||||
|
||||
// SearchResponse re-exported (aliased to strip the "Generated" suffix)
|
||||
export type SearchResponse = SearchResponseGenerated;
|
||||
|
||||
// Health/metrics endpoints return ad-hoc JSON — keep manual types
|
||||
export interface HealthResponse {
|
||||
status: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export type Status = "starting" | "active" | "connected" | "disabled" | "error";
|
||||
|
||||
export interface ServiceInfo {
|
||||
name: string;
|
||||
status: Status;
|
||||
}
|
||||
|
||||
export interface StatusResponse {
|
||||
status: Status;
|
||||
version: string;
|
||||
commit: string;
|
||||
services: Record<string, ServiceInfo>;
|
||||
}
|
||||
|
||||
export interface MetricsResponse {
|
||||
banner_api: {
|
||||
status: string;
|
||||
@@ -26,15 +43,27 @@ export interface MetricsResponse {
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// Client-side only — not generated from Rust
|
||||
export interface SearchParams {
|
||||
term: string;
|
||||
subject?: string;
|
||||
q?: string;
|
||||
open_only?: boolean;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export class BannerApiClient {
|
||||
private baseUrl: string;
|
||||
private fetchFn: typeof fetch;
|
||||
|
||||
constructor(baseUrl: string = API_BASE_URL) {
|
||||
constructor(baseUrl: string = API_BASE_URL, fetchFn: typeof fetch = fetch) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.fetchFn = fetchFn;
|
||||
}
|
||||
|
||||
private async request<T>(endpoint: string): Promise<T> {
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`);
|
||||
const response = await this.fetchFn(`${this.baseUrl}${endpoint}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
||||
@@ -54,6 +83,29 @@ export class BannerApiClient {
|
||||
async getMetrics(): Promise<MetricsResponse> {
|
||||
return this.request<MetricsResponse>("/metrics");
|
||||
}
|
||||
|
||||
async searchCourses(params: SearchParams): Promise<SearchResponse> {
|
||||
const query = new URLSearchParams();
|
||||
query.set("term", params.term);
|
||||
if (params.subject) query.set("subject", params.subject);
|
||||
if (params.q) query.set("q", params.q);
|
||||
if (params.open_only) query.set("open_only", "true");
|
||||
if (params.limit !== undefined) query.set("limit", String(params.limit));
|
||||
if (params.offset !== undefined) query.set("offset", String(params.offset));
|
||||
return this.request<SearchResponse>(`/courses/search?${query.toString()}`);
|
||||
}
|
||||
|
||||
async getTerms(): Promise<Term[]> {
|
||||
return this.request<Term[]>("/terms");
|
||||
}
|
||||
|
||||
async getSubjects(termCode: string): Promise<Subject[]> {
|
||||
return this.request<Subject[]>(`/subjects?term=${encodeURIComponent(termCode)}`);
|
||||
}
|
||||
|
||||
async getReference(category: string): Promise<ReferenceEntry[]> {
|
||||
return this.request<ReferenceEntry[]>(`/reference/${encodeURIComponent(category)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const client = new BannerApiClient();
|
||||
|
||||
Reference in New Issue
Block a user