Skip to content

Commit 09b68b4

Browse files
committed
Add recruitment status prototype
1 parent d458c86 commit 09b68b4

4 files changed

Lines changed: 661 additions & 0 deletions

File tree

recruitment-status/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# HAAG Recruitment Status
2+
3+
The goal of this page is to give everyone — prospective researchers, faculty, advisors — a
4+
clear, always-current answer to "where is HAAG right now in the recruitment cycle?"
5+
This repository holds the single YAML file that drives that status display on the HAAG
6+
website.
7+
8+
## How it works
9+
10+
- `recruitment.yml` is the single source of truth for the current recruitment phase.
11+
- `index.html` reads `recruitment.yml` at page load and renders the status banner and
12+
timeline. There is no build step and no backend — the page just reads the file directly.
13+
- To change what's shown on the site, you only ever edit `recruitment.yml`. Nothing else
14+
needs to change.
15+
16+
This intentionally does **not** integrate with BuzzMe. The recruitment phase only changes
17+
a handful of times per cycle, so a manual update is simpler and has no dependency on
18+
BuzzMe's system staying accessible to us.
19+
20+
## Template
21+
22+
```yaml
23+
cycle_name: Fall 2026 Recruitment
24+
current_phase: applications_open # applications_open | applications_closed | matching | finalized
25+
last_updated: 2026-07-20 # YYYY-MM-DD
26+
27+
notes: > # Optional context shown under the status banner. Leave as null for none.
28+
Applications are open now through August 15 on BuzzMe.
29+
30+
phases:
31+
- key: applications_open
32+
label: Applications Open
33+
description: Applications are open and being accepted through BuzzMe.
34+
- key: applications_closed
35+
label: Applications Closed
36+
description: The application window has closed and submissions are under review.
37+
- key: matching
38+
label: Matching In Progress
39+
description: Applicants are being matched with projects, faculty, and advisors.
40+
- key: finalized
41+
label: Finalized
42+
description: Matches have been finalized and communicated to applicants.
43+
44+
contact: mailto:haag@gatech.edu
45+
```
46+
47+
## Updating the current phase
48+
49+
1. Open `recruitment.yml`.
50+
2. Change `current_phase` to the key of the phase you're now in.
51+
3. Update `last_updated` to today's date.
52+
4. Optionally update `notes` with anything applicants should know right now.
53+
5. Commit and push (or open a PR, if this gets folded into HAAG's broader review workflow).
54+
55+
The site automatically reflects whatever `current_phase` is set to — phases before it show
56+
as complete, the matching phase shows as current, and phases after it show as upcoming.
57+
58+
## Running locally
59+
60+
Because `index.html` fetches `recruitment.yml` over HTTP, opening the file directly in a
61+
browser (`file://`) will fail due to browser security restrictions. Serve it locally instead:
62+
63+
```bash
64+
python3 -m http.server 8000
65+
# then visit http://localhost:8000
66+
```
67+
68+
Or deploy it via GitHub Pages, same as the Project Explorer repo, and it'll work without
69+
any extra setup.
70+
71+
## Notes for future integration
72+
73+
- This currently stands alone as a prototype. If it gets folded into the main HAAG site
74+
redesign (see the "Understand" / site structure work), this same `recruitment.yml`
75+
pattern can likely be embedded directly rather than rebuilt.
76+
- Phase `key` values should stay stable once in use — the timeline logic depends on
77+
`current_phase` matching one of the `phases[].key` values in order.

recruitment-status/index.html

Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>HAAG Recruitment Status</title>
7+
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
8+
<style>
9+
:root {
10+
/* Georgia Tech official brand colors */
11+
--gt-navy: #003057;
12+
--gt-navy-light: #0C4A7A;
13+
--gt-gold: #B3A369;
14+
--gt-gold-medium: #A4925A; /* accessible for large text */
15+
--gt-gold-dark: #857437; /* accessible for small bold text */
16+
--ink: #22282C;
17+
--mute: #5B6670;
18+
--line: #E1E4E7;
19+
--card-tint: #F5F4F1;
20+
--white: #FFFFFF;
21+
--error-bg: #FBEAEA;
22+
--error-border: #B3242C;
23+
--error-ink: #7A1F1F;
24+
}
25+
* { box-sizing: border-box; }
26+
body {
27+
margin: 0;
28+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
29+
background: var(--white);
30+
color: var(--ink);
31+
}
32+
33+
/* Top bar - mirrors the GT site header treatment */
34+
.gt-topbar {
35+
background: var(--gt-navy);
36+
padding: 14px 24px;
37+
display: flex;
38+
align-items: center;
39+
justify-content: space-between;
40+
}
41+
.gt-topbar .site-name {
42+
color: var(--white);
43+
font-size: 14px;
44+
font-weight: 700;
45+
letter-spacing: 0.02em;
46+
text-transform: uppercase;
47+
}
48+
.gt-topbar .site-tag {
49+
color: var(--gt-gold);
50+
font-size: 12px;
51+
font-weight: 600;
52+
letter-spacing: 0.04em;
53+
}
54+
.gt-goldbar {
55+
height: 4px;
56+
background: var(--gt-gold);
57+
}
58+
59+
.wrap {
60+
max-width: 900px;
61+
margin: 0 auto;
62+
padding: 44px 24px 80px;
63+
}
64+
h1 {
65+
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
66+
color: var(--gt-navy);
67+
font-size: 32px;
68+
font-weight: 800;
69+
margin: 0 0 6px;
70+
letter-spacing: -0.01em;
71+
}
72+
.subtitle {
73+
color: var(--mute);
74+
font-size: 15px;
75+
margin: 0 0 28px;
76+
}
77+
78+
.status-banner {
79+
background: var(--gt-navy);
80+
border-radius: 4px;
81+
padding: 26px 30px;
82+
color: var(--white);
83+
display: flex;
84+
align-items: center;
85+
justify-content: space-between;
86+
flex-wrap: wrap;
87+
gap: 12px;
88+
margin-bottom: 12px;
89+
border-left: 6px solid var(--gt-gold);
90+
}
91+
.status-banner .label {
92+
font-size: 12px;
93+
color: #B7C6D3;
94+
text-transform: uppercase;
95+
letter-spacing: 0.08em;
96+
margin-bottom: 6px;
97+
font-weight: 700;
98+
}
99+
.status-banner .phase {
100+
font-size: 25px;
101+
font-weight: 800;
102+
}
103+
.status-banner .cycle {
104+
font-size: 13px;
105+
color: var(--gt-gold);
106+
font-weight: 700;
107+
letter-spacing: 0.02em;
108+
}
109+
110+
.updated {
111+
font-size: 12px;
112+
color: var(--mute);
113+
margin-bottom: 32px;
114+
}
115+
116+
.notes {
117+
background: var(--card-tint);
118+
border-radius: 4px;
119+
padding: 16px 20px;
120+
font-size: 14px;
121+
color: var(--ink);
122+
margin-bottom: 40px;
123+
border-left: 4px solid var(--gt-gold-dark);
124+
}
125+
126+
.timeline { position: relative; margin: 0 0 40px; }
127+
.phase-row { display: flex; gap: 20px; position: relative; padding-bottom: 32px; }
128+
.phase-row:last-child { padding-bottom: 0; }
129+
.rail { display: flex; flex-direction: column; align-items: center; flex-shrink: 0; width: 32px; }
130+
.dot {
131+
width: 32px;
132+
height: 32px;
133+
border-radius: 50%;
134+
display: flex;
135+
align-items: center;
136+
justify-content: center;
137+
font-size: 14px;
138+
font-weight: 700;
139+
color: var(--white);
140+
background: #C6CCD1;
141+
z-index: 1;
142+
}
143+
.dot.complete { background: var(--gt-gold-dark); }
144+
.dot.current {
145+
background: var(--gt-navy);
146+
box-shadow: 0 0 0 4px var(--card-tint), 0 0 0 6px var(--gt-gold);
147+
}
148+
.line { width: 3px; flex: 1; background: var(--line); margin-top: 2px; }
149+
.line.complete { background: var(--gt-gold-dark); }
150+
151+
.phase-content { padding-top: 2px; }
152+
.phase-label { font-size: 16px; font-weight: 700; color: var(--mute); }
153+
.phase-row.current .phase-label { color: var(--gt-navy); }
154+
.phase-row.complete .phase-label { color: var(--gt-gold-dark); }
155+
.phase-desc { font-size: 13px; color: var(--mute); margin-top: 4px; max-width: 560px; }
156+
157+
.badge {
158+
display: inline-block;
159+
font-size: 10.5px;
160+
font-weight: 700;
161+
padding: 2px 9px;
162+
border-radius: 3px;
163+
margin-left: 8px;
164+
vertical-align: middle;
165+
text-transform: uppercase;
166+
letter-spacing: 0.03em;
167+
}
168+
.badge.current { background: var(--gt-gold); color: var(--gt-navy); }
169+
.badge.complete { background: var(--card-tint); color: var(--gt-gold-dark); }
170+
171+
.contact { border-top: 1px solid var(--line); padding-top: 20px; font-size: 13px; color: var(--mute); }
172+
.contact a { color: var(--gt-navy); font-weight: 700; text-decoration: none; border-bottom: 2px solid var(--gt-gold); }
173+
.contact a:hover { color: var(--gt-navy-light); }
174+
175+
.error {
176+
background: var(--error-bg);
177+
border-left: 4px solid var(--error-border);
178+
padding: 16px 20px;
179+
border-radius: 4px;
180+
font-size: 14px;
181+
color: var(--error-ink);
182+
}
183+
.loading { color: var(--mute); font-size: 14px; }
184+
185+
.footer-tag {
186+
margin-top: 48px;
187+
padding-top: 20px;
188+
border-top: 1px solid var(--line);
189+
font-size: 11.5px;
190+
color: var(--mute);
191+
letter-spacing: 0.02em;
192+
}
193+
</style>
194+
</head>
195+
<body>
196+
<div class="gt-topbar">
197+
<span class="site-name">Human-Augmented Analytics Group</span>
198+
<span class="site-tag">HAAG</span>
199+
</div>
200+
<div class="gt-goldbar"></div>
201+
202+
<div class="wrap">
203+
<h1>Recruitment Status</h1>
204+
<p class="subtitle">Where we currently stand in the recruitment cycle</p>
205+
<div id="content">
206+
<p class="loading">Loading current status&hellip;</p>
207+
</div>
208+
<div class="footer-tag">Georgia Institute of Technology &middot; HAAG</div>
209+
</div>
210+
211+
<script>
212+
async function loadStatus() {
213+
const contentEl = document.getElementById("content");
214+
try {
215+
const res = await fetch("recruitment.yml", { cache: "no-store" });
216+
if (!res.ok) throw new Error("Could not load recruitment.yml (" + res.status + ")");
217+
const text = await res.text();
218+
const data = jsyaml.load(text);
219+
render(data, contentEl);
220+
} catch (err) {
221+
contentEl.innerHTML =
222+
'<div class="error"><strong>Could not load recruitment status.</strong><br>' +
223+
"This page needs to be served over HTTP (e.g. GitHub Pages or a local server) " +
224+
"rather than opened directly as a file, since it fetches <code>recruitment.yml</code>. " +
225+
"<br><br>Details: " + (err.message || err) + "</div>";
226+
}
227+
}
228+
229+
function render(data, el) {
230+
const phases = data.phases || [];
231+
const currentIndex = phases.findIndex((p) => p.key === data.current_phase);
232+
const currentPhase = phases[currentIndex];
233+
234+
let html = "";
235+
236+
html += '<div class="status-banner">';
237+
html += '<div><div class="label">Current Phase</div>';
238+
html += '<div class="phase">' + escapeHtml(currentPhase ? currentPhase.label : data.current_phase) + "</div></div>";
239+
html += '<div class="cycle">' + escapeHtml(data.cycle_name || "") + "</div>";
240+
html += "</div>";
241+
242+
html += '<div class="updated">Last updated: ' + escapeHtml(data.last_updated || "unknown") + "</div>";
243+
244+
if (data.notes) {
245+
html += '<div class="notes">' + escapeHtml(data.notes.trim()) + "</div>";
246+
}
247+
248+
html += '<div class="timeline">';
249+
phases.forEach((p, i) => {
250+
let state = "upcoming";
251+
if (i < currentIndex) state = "complete";
252+
else if (i === currentIndex) state = "current";
253+
254+
const isLast = i === phases.length - 1;
255+
html += '<div class="phase-row ' + state + '">';
256+
html += '<div class="rail">';
257+
html += '<div class="dot ' + state + '">' + (state === "complete" ? "&#10003;" : i + 1) + "</div>";
258+
if (!isLast) html += '<div class="line ' + (state === "complete" ? "complete" : "") + '"></div>';
259+
html += "</div>";
260+
html += '<div class="phase-content">';
261+
html += '<span class="phase-label">' + escapeHtml(p.label) + "</span>";
262+
if (state === "current") html += '<span class="badge current">CURRENT</span>';
263+
if (state === "complete") html += '<span class="badge complete">COMPLETE</span>';
264+
html += '<div class="phase-desc">' + escapeHtml(p.description || "") + "</div>";
265+
html += "</div></div>";
266+
});
267+
html += "</div>";
268+
269+
if (data.contact) {
270+
html += '<div class="contact">Questions about this cycle? <a href="' + escapeAttr(data.contact) + '">Contact HAAG</a></div>';
271+
}
272+
273+
el.innerHTML = html;
274+
}
275+
276+
function escapeHtml(str) {
277+
if (str === null || str === undefined) return "";
278+
return String(str)
279+
.replace(/&/g, "&amp;")
280+
.replace(/</g, "&lt;")
281+
.replace(/>/g, "&gt;");
282+
}
283+
function escapeAttr(str) {
284+
return String(str).replace(/"/g, "&quot;");
285+
}
286+
287+
loadStatus();
288+
</script>
289+
</body>
290+
</html>

0 commit comments

Comments
 (0)