Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 0 additions & 160 deletions src-tauri/src/features/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,167 +372,7 @@ pub fn parse_exam_schedule(html: &str) -> Result<Vec<ExamScheduleEntry>, String>
Ok(entries)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_exam_schedule() {
let html = r#"
<table class="customTable">
<tr class="tableHeader">
<td>S.No.</td><td>Course Code</td><td>Course Title</td><td>Course Type</td>
<td>Class ID</td><td>Slot</td><td>Exam Date</td><td>Exam Session</td>
<td>Reporting Time</td><td>Exam Time</td><td>Venue</td><td>Seat Location</td><td>Seat No.</td>
</tr>
<tr class="tableContent">
<td class="panelHead-secondary" colspan="13" align="center">FAT</td>
</tr>
<tr class="tableContent">
<td>1</td>
<td>BCSE302L</td>
<td>Database Systems</td>
<td>TH</td>
<td>CH2025260501233</td>
<td>A1+TA1</td>
<td>20-Apr-2026</td>
<td>FN1</td>
<td>09:45 AM</td>
<td>10:00 AM - 01:00 PM</td>
<td><span>AB3-607</span></td>
<td><span><span>-</span></span></td>
<td><span>27</span></td>
</tr>
<tr class="tableContent">
<td class="panelHead-secondary" colspan="13" align="center">CAT2</td>
</tr>
<tr class="tableContent">
<td>1</td>
<td>BCSE302L</td>
<td>Database Systems</td>
<td>TH</td>
<td>CH2025260501233</td>
<td>A1+TA1</td>
<td>14-Mar-2026</td>
<td>FN2</td>
<td>11:45 AM</td>
<td>12:00 PM - 01:30 PM</td>
<td><span>AB3-106</span></td>
<td><span><span>R5C3</span></span></td>
<td><span>27</span></td>
</tr>
</table>
"#;

let entries = parse_exam_schedule(html).unwrap();
assert_eq!(entries.len(), 2);

assert_eq!(entries[0].exam_type, "FAT");
assert_eq!(entries[0].course_code, "BCSE302L");
assert_eq!(entries[0].course_title, "Database Systems");
assert_eq!(entries[0].venue, "AB3-607");
assert_eq!(entries[0].seat_location, "-");
assert_eq!(entries[0].seat_no, "27");

assert_eq!(entries[1].exam_type, "CAT2");
assert_eq!(entries[1].venue, "AB3-106");
assert_eq!(entries[1].seat_location, "R5C3");
assert_eq!(entries[1].seat_no, "27");
}

#[test]
fn test_parse_hod_dean_details() {
let html = r#"
<div class="box">
<div class="box-header with-border text-center">
<h3 class="box-title"><b>DEAN</b></h3>
</div>
<table class="table">
<tr>
<td><b>Name of the Faculty </b></td>
<td>Dr. VISWANATHAN V</td>
<td rowspan="5"><img src="data:jpg;base64,12345" /></td>
</tr>
<tr>
<td><b>School </b></td>
<td>SCOPE</td>
</tr>
<tr>
<td><b>Cabin No </b></td>
<td>AB3-329</td>
</tr>
<tr>
<td><b>Email ID </b></td>
<td>dean.scope@vit.ac.in</td>
</tr>
<tr>
<td><b>Intercom </b></td>
<td>1234</td>
</tr>
</table>
</div>
"#;

let details = parse_hod_dean_details(html).unwrap();
assert_eq!(details.len(), 1);
assert_eq!(details[0].role, "DEAN");
assert_eq!(details[0].name, "Dr. VISWANATHAN V");
assert_eq!(details[0].school, "SCOPE");
assert_eq!(details[0].cabin, "AB3-329");
assert_eq!(details[0].email, "dean.scope@vit.ac.in");
assert_eq!(details[0].intercom, "1234");
assert_eq!(details[0].photo, "data:jpg;base64,12345");
}

#[test]
fn test_parse_hod_dean_details_multiple_in_single_box() {
let html = r#"
<div class="box">
<div class="table-responsive">
<div class="box-header with-border text-center">
<h3 class="box-title"><b>DEAN</b></h3>
</div>
<table class="table">
<tr>
<td><b>Name of the Faculty </b></td>
<td>Dr. VISWANATHAN V</td>
<td rowspan="5"><img src="data:jpg;base64,123" /></td>
</tr>
<tr>
<td><b>School </b></td>
<td>SCOPE</td>
</tr>
</table>
</div>
<div class="table-responsive">
<div class="box-header with-border text-center">
<h3 class="box-title"><b>HOD</b></h3>
</div>
<table class="table">
<tr>
<td><b>Name of the Faculty </b></td>
<td>Dr. HOD NAME</td>
<td rowspan="5"><img src="data:jpg;base64,456" /></td>
</tr>
<tr>
<td><b>School </b></td>
<td>SCOPE</td>
</tr>
</table>
</div>
</div>
"#;

let details = parse_hod_dean_details(html).unwrap();
assert_eq!(details.len(), 2);
assert_eq!(details[0].role, "DEAN");
assert_eq!(details[0].name, "Dr. VISWANATHAN V");
assert_eq!(details[0].photo, "data:jpg;base64,123");
assert_eq!(details[1].role, "HOD");
assert_eq!(details[1].name, "Dr. HOD NAME");
assert_eq!(details[1].photo, "data:jpg;base64,456");
}
}

fn parse_single_table(
table: &ElementRef<'_>,
Expand Down
143 changes: 143 additions & 0 deletions src/hooks/use-offline-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { fetchWithTimeout } from "@/lib/utils";

export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
[key: string]: unknown;
}

export interface UseOfflineDataOptions<T> {
/** localStorage key to read/write cache */
cacheKey: string;
/** Async function that returns an ApiResponse-shaped object */
fetcher: () => Promise<ApiResponse<T>>;
/** Only fetch when true — e.g. isLoggedIn && !authLoading */
enabled?: boolean;
/** Timeout in ms (default: 15000) */
timeout?: number;
/** Optional: filter or transform raw data before storing */
transform?: (raw: T) => T;
/** Optional: check if cached value is non-empty (default: truthy check) */
isEmpty?: (value: T) => boolean;
}

export interface UseOfflineDataResult<T> {
data: T | null;
loading: boolean;
error: string | null;
/** True when showing cached data but last fetch failed */
isStale: boolean;
/** Manually trigger a re-fetch */
retry: () => void;
}

function readCache<T>(cacheKey: string): T | null {
try {
const raw = localStorage.getItem(cacheKey);
if (raw) return JSON.parse(raw) as T;
} catch {}
return null;
}

function writeCache<T>(cacheKey: string, value: T): void {
try {
localStorage.setItem(cacheKey, JSON.stringify(value));
} catch {}
}

export function useOfflineData<T>({
cacheKey,
fetcher,
enabled = true,
timeout = 15000,
transform,
isEmpty,
}: UseOfflineDataOptions<T>): UseOfflineDataResult<T> {
const fetcherRef = useRef(fetcher);
fetcherRef.current = fetcher;

const transformRef = useRef(transform);
transformRef.current = transform;

const isEmptyRef = useRef(isEmpty);
isEmptyRef.current = isEmpty;

const hasData = useCallback((value: T | null): boolean => {
if (value === null || value === undefined) return false;
if (isEmptyRef.current) return !isEmptyRef.current(value);
if (Array.isArray(value)) return value.length > 0;
if (typeof value === "object") return Object.keys(value as object).length > 0;
return !!value;
}, []);

const [data, setData] = useState<T | null>(() => readCache<T>(cacheKey));
const [loading, setLoading] = useState<boolean>(() => !hasData(readCache<T>(cacheKey)));
const [error, setError] = useState<string | null>(null);
const [isStale, setIsStale] = useState(false);

// Sync cache state when cacheKey changes
useEffect(() => {
const cached = readCache<T>(cacheKey);
setData(cached);
setLoading(!hasData(cached));
setError(null);
setIsStale(false);
}, [cacheKey, hasData]);

// Use ref to avoid stale closure issues in fetch
const dataRef = useRef(data);
dataRef.current = data;

const fetchData = useCallback(async () => {
if (!enabled) return;

const currentData = dataRef.current;
const hasCache = hasData(currentData);

setError(null);
setLoading(!hasCache);

Comment on lines +93 to +101
try {
const res = await fetchWithTimeout(fetcherRef.current(), timeout);

if (res?.success && res.data !== undefined && res.data !== null) {
const processed = transformRef.current ? transformRef.current(res.data) : res.data;
setData(processed);
writeCache(cacheKey, processed);
setIsStale(false);
setError(null);
} else {
const errMsg = res?.error ?? "Failed to fetch data.";
if (!hasCache) {
setError(errMsg);
} else {
// silently mark stale — still showing cached data
setIsStale(true);
}
}
} catch (e) {
const errMsg = e instanceof Error ? e.message : String(e);
if (!hasCache) {
setError(errMsg);
} else {
setIsStale(true);
}
} finally {
setLoading(false);
}
}, [enabled, cacheKey, timeout, hasData]);

useEffect(() => {
if (enabled) {
fetchData();
}
}, [enabled, fetchData]);

const retry = useCallback(() => {
fetchData();
}, [fetchData]);

return { data, loading, error, isStale, retry };
}
Loading
Loading