A Chrome extension that exports your Curtin timetable to ICS in one click.
Install: Chrome Web Store (recommended) or load from source.
| Version: 0.4.2 | License: GPL-3.0 |
Option 1 — Chrome Web Store (recommended)
Install directly from the Chrome Web Store.
Option 2 — Manual install
chrome://extensions and enable Developer modePrerequisites:
Setup:
git clone https://github.com/n0x1d3/curtin-calendar.git
cd curtin-calendar
bun install
bun run build
Load in Chrome:
chrome://extensionsbuild/ folderDevelopment mode (with auto-rebuild):
bun run watch
Rebuilds after every file change. Reload the extension in Chrome to see updates.
bun install # Install dependencies
bun run build # Production build → build/
bun run watch # Dev mode with file watching
bun run test # Run test suite (vitest, 14 tests)
bun run test:watch # Watch mode for tests
bun run coverage # Test coverage report
bun run format # Prettier on {config,public,src}/**
bun run pack # Zip build/ for distribution
curtin-calendar/
├── build/ # Compiled output (webpack)
├── config/
│ ├── webpack.config.js # Production webpack config
│ └── webpack.beta.js # Beta build config
├── public/
│ ├── manifest.json # Chrome MV3 manifest
│ ├── popup.html # Extension popup UI
│ └── icons/ # Extension icons
├── src/
│ ├── popup.ts # Popup UI orchestrator (polling, progress, theme)
│ ├── background.ts # MV3 service worker (chrome.downloads)
│ ├── contentScript.ts # Injected into eStudent — initialises scrape session
│ ├── readTable.ts # Runs on each timetable page — scrapes or finalises
│ ├── types.ts # Shared interfaces and enums
│ │
│ └── utils/
│ ├── buttons.ts # DOM refs and date helpers (setDate, readDate, clickForward)
│ ├── scrapData.ts # Reads class slots from the timetable DOM
│ ├── scrapEvents.ts # Converts scraped slots into ICS EventAttributes
│ ├── popupErrors.ts # getSmartErrorMessage, onError
│ │
│ └── format/
│ ├── formatData.ts # Time parsing (convertTime) + MazeMap lookup (getLocation)
│ ├── formatData.test.ts # Tests for time parsing
│ ├── getDates.ts # Semester date arithmetic + knownYearOverrides
│ └── getDates.test.ts # Tests for semester dates
│
├── package.json
├── tsconfig.json
└── vitest.config.ts
Data Model:
The core scrapedDataType interface (in src/types.ts) represents a single scraped class:
interface scrapedDataType {
type: string; // E.g. "Lecture", "Workshop"
location: LocationData | false; // MazeMap result, or false for online/unknown
time: classTimeType; // { start, end, differenceInMinutes }
title: string; // Unit code, e.g. "COMP1005"
date: Date; // Absolute date of this class occurrence
}
Data Flow:
User clicks Download in popup:
popup.ts → sends `click` command → contentScript.ts
contentScript.ts → initialises chrome.storage (events:[], forward:0, semester, totalWeeks)
→ calls setDate() to navigate to semester week 1
For each timetable page load (readTable.ts):
if forward < totalWeeks:
scrapeWeek() → scrapData() (DOM read) → scrapEvents() (ICS build) → clickForward()
if forward === totalWeeks:
finalizeDownload() → createEvents() (ics package)
→ sends `download` command → background.ts
→ background.ts calls chrome.downloads.download()
popup.ts polls chrome.storage every ~500ms → updates progress bar → shows success or error
Key Modules:
| Module | Purpose | Type |
|---|---|---|
src/popup.ts |
Popup UI — polling, progress bar, cancel, theme | DOM + Chrome API |
src/background.ts |
Service worker — triggers file download | Chrome API only |
src/contentScript.ts |
Initialises scrape state, navigates to week 1 | DOM + Chrome storage |
src/readTable.ts |
Scrapes each week and finalises ICS | DOM + Chrome storage |
src/utils/scrapData.ts |
Reads class slots from timetable DOM | DOM |
src/utils/scrapEvents.ts |
Builds EventAttributes objects for ics |
Pure TS |
src/utils/format/formatData.ts |
Time parsing + MazeMap API lookup | Network + Pure TS |
src/utils/format/getDates.ts |
Semester start dates and week counts | Pure TS |
Dependency Rules:
utils/format/ → zero Chrome API; pure TypeScript, fully testableutils/scrapData.ts + utils/scrapEvents.ts → DOM only (content script context)utils/popupErrors.ts → DOM only (popup context)background.ts → Chrome API only, no DOMcontentScript.ts + readTable.ts → DOM + Chrome storagepopup.ts → DOM + Chrome tabs/storage/runtimeSemester start dates live in src/utils/format/getDates.ts inside knownYearOverrides. To add support for a new year, add an entry there — no other files need changing.
bun run test # Run all 14 tests
bun run test:watch # Watch mode
bun run coverage # Coverage report
Tests are in src/utils/format/:
formatData.test.ts – time parsing (convertTime, parseTo24h)getDates.test.ts – semester date arithmetic and knownYearOverridesCoverage: ~80% on pure utility functions. Chrome API and DOM code is not unit tested.
bun run format before committingfeat: add dark mode, fix: handle missing room data)bun run formatfeature/short-descriptionfix/short-descriptionchore/short-descriptionNever commit directly to main.
bun run format
bun run test
bun run build
All tests must pass and the build must succeed.
Q: Is my data shared with anyone? A: No. The extension reads your timetable page directly in the browser. No data is sent to any server — the only external call is to the MazeMap API to resolve room coordinates from a room code.
Q: Why does it take a while to export? A: The extension navigates the timetable week-by-week, one page at a time, to read each class. This is necessary because the eStudent timetable only shows one week at a time.
Q: What if a class has no location? A: Online classes are detected automatically — the event is created with no location data and no broken links.
Q: My semester isn’t in the dropdown / the dates are wrong.
A: Semester dates are hardcoded in src/utils/format/getDates.ts (knownYearOverrides). Open an issue or submit a PR to add the correct dates for your year.
Q: What if I find a bug? A: Open a GitHub issue with your Chrome version, extension version (shown in the popup footer), and steps to reproduce.
Originally created by SetroZ. Redesigned, extended, and maintained by n0x1d3 — with a new interface, improved reliability, and additional features.