PageSourceSearch

https://wmtc.info/assets/StudentTimeline-AeIpXRLt.js

js wmtc.info collected 2026-10-04 05:37:35 UTC 6,356 bytes, 156 lines download raw bytes

1import { a as __toESM } from "./rolldown-runtime-_TIqcEvS.js";
2import { C as require_jsx_runtime, u as RefreshCw, w as require_react } from "./admin-attendance-Devs9Lzj.js";
3import { C as ChevronDown, S as ChevronUp } from "./admin-audit-CGi9m8Nk.js";
4import { Et as MessageSquare } from "./admin-components-CkAH8kJs.js";
5import { f as DollarSign, g as Award } from "./admin-budget-1k-XVOid.js";
6import { r as UserPlus } from "./admin-broadcast-Dm1KhKbJ.js";
7import { i as CalendarCheck } from "./enroll-page-CX5lPbth.js";
8//#region src/components/StudentTimeline.tsx
9var import_react = /* @__PURE__ */ __toESM(require_react(), 1);
10var import_jsx_runtime = require_jsx_runtime();
11var TYPE_CONFIG = {
12	enrollment: {
13		icon: UserPlus,
14		color: "text-primary",
15		dot: "bg-primary"
16	},
17	note: {
18		icon: MessageSquare,
19		color: "text-blue-600",
20		dot: "bg-blue-500"
21	},
22	payment: {
23		icon: DollarSign,
24		color: "text-green-600",
25		dot: "bg-green-500"
26	},
27	attendance: {
28		icon: CalendarCheck,
29		color: "text-purple-600",
30		dot: "bg-purple-500"
31	},
32	status: {
33		icon: Award,
34		color: "text-orange-600",
35		dot: "bg-orange-500"
36	}
37};
38var STATUS_COLORS = {
39	present: "bg-green-100 text-green-700",
40	late: "bg-yellow-100 text-yellow-700",
41	excused: "bg-blue-100 text-blue-700",
42	absent: "bg-red-100 text-red-700",
43	completed: "bg-green-100 text-green-700",
44	pending: "bg-yellow-100 text-yellow-700",
45	failed: "bg-red-100 text-red-700"
46};
47function StudentTimeline({ studentId }) {
48	const [events, setEvents] = (0, import_react.useState)([]);
49	const [loading, setLoading] = (0, import_react.useState)(true);
50	const [expanded, setExpanded] = (0, import_react.useState)(false);
51	const PREVIEW_COUNT = 8;
52	const load = async () => {
53		setLoading(true);
54		try {
55			const res = await fetch(`/api/students/${studentId}/timeline`);
56			if (res.ok) setEvents((await res.json()).events ?? []);
57		} finally {
58			setLoading(false);
59		}
60	};
61	(0, import_react.useEffect)(() => {
62		load();
63	}, [studentId]);
64	const visible = expanded ? events : events.slice(0, PREVIEW_COUNT);
65	if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
66		className: "text-xs text-gray-400 py-3 animate-pulse",
67		children: "Loading timeline…"
68	});
69	if (events.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
70		className: "text-xs text-gray-400 py-3",
71		children: "No activity recorded yet."
72	});
73	return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
74		/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
75			className: "flex items-center justify-between mb-3",
76			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
77				className: "text-xs font-semibold text-gray-500 uppercase tracking-wide",
78				children: "Activity Timeline"
79			}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
80				onClick: () => void load(),
81				className: "text-gray-400 hover:text-primary",
82				children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCw, { size: 12 })
83			})]
84		}),
85		/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
86			className: "relative",
87			children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute left-[7px] top-2 bottom-2 w-px bg-gray-200" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
88				className: "space-y-3",
89				children: visible.map((event) => {
90					const cfg = TYPE_CONFIG[event.type] ?? TYPE_CONFIG.note;
91					const Icon = cfg.icon;
92					return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
93						className: "flex gap-3 relative",
94						children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `w-3.5 h-3.5 rounded-full shrink-0 mt-0.5 z-10 ring-2 ring-white ${cfg.dot}` }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
95							className: "flex-1 min-w-0 pb-1",
96							children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
97								className: "flex items-start justify-between gap-2",
98								children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
99									className: "flex items-center gap-1.5 min-w-0",
100									children: [
101										/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, {
102											size: 11,
103											className: `shrink-0 ${cfg.color}`
104										}),
105										/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
106											className: "text-xs font-semibold text-gray-800 truncate",
107											children: event.title
108										}),
109										event.meta && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
110											className: `text-xs px-1.5 py-0.5 rounded font-medium ${STATUS_COLORS[event.meta.toLowerCase()] ?? "bg-gray-100 text-gray-500"}`,
111											children: event.meta
112										})
113									]
114								}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
115									className: "text-xs text-gray-400 whitespace-nowrap shrink-0",
116									children: formatRelative(event.timestamp)
117								})]
118							}), event.detail && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
119								className: "text-xs text-gray-500 mt-0.5 leading-relaxed line-clamp-2",
120								children: event.detail
121							})]
122						})]
123					}, event.id);
124				})
125			})]
126		}),
127		events.length > PREVIEW_COUNT && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
128			onClick: () => setExpanded(!expanded),
129			className: "mt-3 flex items-center gap-1 text-xs text-primary hover:underline",
130			children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronUp, { size: 12 }), " Show less"] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
131				/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronDown, { size: 12 }),
132				" Show ",
133				events.length - PREVIEW_COUNT,
134				" more events"
135			] })
136		})
137	] });
138}
139function formatRelative(timestamp) {
140	const date = new Date(timestamp);
141	const diff = Date.now() - date.getTime();
142	const mins = Math.floor(diff / 6e4);
143	const hours = Math.floor(diff / 36e5);
144	const days = Math.floor(diff / 864e5);
145	if (mins < 1) return "just now";
146	if (mins < 60) return `${mins}m ago`;
147	if (hours < 24) return `${hours}h ago`;
148	if (days < 7) return `${days}d ago`;
149	return date.toLocaleDateString("en-US", {
150		month: "short",
151		day: "numeric",
152		year: days > 365 ? "numeric" : void 0
153	});
154}
155//#endregion
156export { StudentTimeline as default };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.