Skip to content

Commit 41f2146

Browse files
feat(career-guides): Redesign as searchable hashflag index (#1125)
Replaces the flat list of 4,201 job codes with an editorial database: status bar, hero stat strip, category showcase, search with autocomplete and Cmd-K, branch/rank/family/sort filters, paginated grid view, and cohort CTA. Brand-aligned (navy/red/gold tokens, sharp edges, GothamPro, red eyebrow bars). Merges training-pipeline + career-pathways data at build time into the GuideEntry shape.
1 parent 7e9e208 commit 41f2146

12 files changed

Lines changed: 1178 additions & 182 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Branch, BranchShort } from "./types";
2+
3+
// Distinct branch colors (handoff §Branch colors), tuned for use on our navy ground.
4+
export const BRANCH_META: Record<Branch, { short: BranchShort; color: string }> = {
5+
Army: { short: "ARMY", color: "#6b8050" },
6+
Navy: { short: "NAVY", color: "#5b87c4" },
7+
"Air Force": { short: "USAF", color: "#8eb4d8" },
8+
"Marine Corps": { short: "USMC", color: "#d9514a" },
9+
"Coast Guard": { short: "USCG", color: "#e89149" },
10+
};
11+
12+
export const BRANCH_ORDER: Branch[] = ["Army", "Navy", "Air Force", "Marine Corps", "Coast Guard"];
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import clsx from "clsx";
2+
import type { Family, GuideEntry } from "./types";
3+
4+
interface Props {
5+
guides: GuideEntry[];
6+
onPick: (family: Family) => void;
7+
}
8+
9+
const FEATURED_FAMILIES: Array<{
10+
family: Family;
11+
number: string;
12+
blurb: string;
13+
}> = [
14+
{
15+
family: "Cyber",
16+
number: "/ 01",
17+
blurb: "Signal warfare and cryptologic operators translate directly to security engineering.",
18+
},
19+
{
20+
family: "IT / Comms",
21+
number: "/ 02",
22+
blurb: "Network and signal techs become DevOps, SRE, and platform engineers.",
23+
},
24+
{
25+
family: "Aviation",
26+
number: "/ 03",
27+
blurb: "Avionics and aircrew specialties bridge to embedded systems and aerospace software.",
28+
},
29+
{
30+
family: "Intelligence",
31+
number: "/ 04",
32+
blurb: "Intel analysts move into data engineering, threat hunting, and ML for defense.",
33+
},
34+
];
35+
36+
const medianSalary = (rows: GuideEntry[]): string => {
37+
if (rows.length === 0) return "—";
38+
const lows = rows.map((r) => r.salaryLow).sort((a, b) => a - b);
39+
const highs = rows.map((r) => r.salaryHigh).sort((a, b) => a - b);
40+
const lo = lows[Math.floor(lows.length / 2)];
41+
const hi = highs[Math.floor(highs.length / 2)];
42+
return `$${lo}${hi}K typical`;
43+
};
44+
45+
const CategoryShowcase = ({ guides, onPick }: Props) => {
46+
return (
47+
<section className="tw-bg-secondary tw-py-16 md:tw-py-20">
48+
<div className="tw-container">
49+
<div className="tw-mb-10 tw-flex tw-items-center tw-gap-3">
50+
<span className="tw-inline-block tw-h-[2px] tw-w-4 tw-bg-primary" />
51+
<span className="tw-font-mono tw-text-[11px] tw-uppercase tw-tracking-[0.14em] tw-text-[#8590a6]">
52+
Pathways that ship in civilian roles
53+
</span>
54+
</div>
55+
56+
<div className="tw-grid tw-grid-cols-1 sm:tw-grid-cols-2 lg:tw-grid-cols-4 tw-border-t tw-border-cream/10">
57+
{FEATURED_FAMILIES.map(({ family, number, blurb }, idx) => {
58+
const rows = guides.filter((g) => g.family === family);
59+
return (
60+
<button
61+
type="button"
62+
key={family}
63+
onClick={() => onPick(family)}
64+
className={clsx(
65+
"tw-group tw-flex tw-flex-col tw-gap-5 tw-px-6 tw-py-8 tw-text-left tw-transition-colors tw-duration-150",
66+
"hover:tw-bg-[#0c2549]",
67+
idx > 0 && "lg:tw-border-l lg:tw-border-cream/10",
68+
idx > 0 && idx < FEATURED_FAMILIES.length && "sm:tw-border-l sm:tw-border-cream/10",
69+
)}
70+
>
71+
<span className="tw-font-mono tw-text-[11px] tw-uppercase tw-tracking-[0.14em] tw-text-[#5a6478] group-hover:tw-text-accent">
72+
{number}
73+
</span>
74+
<span className="tw-font-heading tw-text-[22px] tw-font-medium tw-uppercase tw-text-cream [letter-spacing:-0.01em]">
75+
{family}
76+
</span>
77+
<span className="tw-font-body tw-text-[14px] tw-leading-[1.55] tw-text-[#c4cad6]">
78+
{blurb}
79+
</span>
80+
<span className="tw-mt-auto tw-flex tw-flex-col tw-gap-1 tw-font-mono tw-text-[11px] tw-uppercase tw-tracking-[0.08em] tw-text-[#8590a6]">
81+
<span>
82+
<span className="tw-text-cream">{rows.length.toLocaleString()}</span>{" "}
83+
guides
84+
</span>
85+
<span>{medianSalary(rows)}</span>
86+
</span>
87+
</button>
88+
);
89+
})}
90+
</div>
91+
</div>
92+
</section>
93+
);
94+
};
95+
96+
export default CategoryShowcase;
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import Link from "next/link";
2+
3+
const CtaBand = () => (
4+
<section className="tw-border-t tw-border-cream/10 tw-bg-secondary tw-py-20 md:tw-py-24">
5+
<div className="tw-container">
6+
<div className="tw-mb-6 tw-flex tw-items-center tw-gap-3">
7+
<span className="tw-inline-block tw-h-[2px] tw-w-4 tw-bg-primary" />
8+
<span className="tw-font-mono tw-text-[11px] tw-uppercase tw-tracking-[0.14em] tw-text-[#8590a6]">
9+
Cohort 2027 · Intake Open
10+
</span>
11+
</div>
12+
13+
<h2 className="tw-font-heading tw-font-semibold tw-uppercase tw-text-cream [letter-spacing:-0.02em] [line-height:1] [font-size:clamp(32px,4.5vw,56px)]">
14+
Found your translation?
15+
<br />
16+
<span className="tw-text-[#8590a6]">
17+
Now build the skills to land it.
18+
</span>
19+
</h2>
20+
21+
<p className="tw-mt-7 tw-max-w-[680px] tw-font-body tw-text-[17px] tw-leading-[1.55] tw-text-[#c4cad6]">
22+
Vets Who Code is a free, full-time software engineering accelerator for active duty,
23+
veterans, and military spouses. 17 weeks, 25 modules, and a portfolio that ships.
24+
</p>
25+
26+
<div className="tw-mt-9 tw-flex tw-flex-wrap tw-gap-4">
27+
<Link
28+
href="/apply"
29+
className="tw-inline-flex tw-items-center tw-gap-2 tw-bg-accent tw-px-7 tw-py-4 tw-font-mono tw-text-[12px] tw-font-bold tw-uppercase tw-tracking-[0.1em] tw-text-secondary tw-transition-colors hover:tw-bg-gold-bright active:tw-scale-[0.97]"
30+
>
31+
Apply for Cohort 2027 →
32+
</Link>
33+
<Link
34+
href="/programs/core-curriculum"
35+
className="tw-inline-flex tw-items-center tw-gap-2 tw-border tw-border-cream/[0.18] tw-px-7 tw-py-4 tw-font-mono tw-text-[12px] tw-font-bold tw-uppercase tw-tracking-[0.1em] tw-text-cream tw-transition-colors hover:tw-border-cream hover:tw-bg-cream/5 active:tw-scale-[0.97]"
36+
>
37+
Read the curriculum
38+
</Link>
39+
</div>
40+
</div>
41+
</section>
42+
);
43+
44+
export default CtaBand;
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import clsx from "clsx";
2+
import { BRANCH_META, BRANCH_ORDER } from "./branch-meta";
3+
import { FAMILIES } from "@/lib/career-guides";
4+
import type { Branch, Family, Rank, SortKey } from "./types";
5+
6+
interface Props {
7+
branchCounts: Record<Branch, number>;
8+
branch: "all" | Branch;
9+
onBranch: (v: "all" | Branch) => void;
10+
rank: "all" | Rank;
11+
onRank: (v: "all" | Rank) => void;
12+
family: "all" | Family;
13+
onFamily: (v: "all" | Family) => void;
14+
sort: SortKey;
15+
onSort: (v: SortKey) => void;
16+
showing: number;
17+
total: number;
18+
}
19+
20+
const RANK_OPTS: Array<{ key: "all" | Rank; label: string }> = [
21+
{ key: "all", label: "All" },
22+
{ key: "Enlisted", label: "Enlisted" },
23+
{ key: "Warrant", label: "Warrant" },
24+
{ key: "Officer", label: "Officer" },
25+
];
26+
27+
const SORT_OPTS: Array<{ key: SortKey; label: string }> = [
28+
{ key: "code", label: "Code (A→Z)" },
29+
{ key: "title", label: "Title (A→Z)" },
30+
{ key: "salaryHigh", label: "Salary (High→Low)" },
31+
{ key: "salaryLow", label: "Salary (Low→High)" },
32+
{ key: "demand", label: "Demand" },
33+
];
34+
35+
const RAINBOW =
36+
"linear-gradient(90deg, #6b8050 0%, #6b8050 20%, #5b87c4 20%, #5b87c4 40%, #8eb4d8 40%, #8eb4d8 60%, #d9514a 60%, #d9514a 80%, #e89149 80%, #e89149 100%)";
37+
38+
const Filters = ({
39+
branchCounts,
40+
branch,
41+
onBranch,
42+
rank,
43+
onRank,
44+
family,
45+
onFamily,
46+
sort,
47+
onSort,
48+
showing,
49+
total,
50+
}: Props) => {
51+
const allCount = BRANCH_ORDER.reduce((sum, b) => sum + branchCounts[b], 0);
52+
53+
return (
54+
<div className="tw-flex tw-flex-col tw-gap-6">
55+
{/* Branch chips */}
56+
<div className="tw-flex tw-flex-wrap tw-gap-2">
57+
<button
58+
type="button"
59+
onClick={() => onBranch("all")}
60+
className={clsx(
61+
"tw-flex tw-items-center tw-gap-2.5 tw-border tw-px-3.5 tw-py-2 tw-font-mono tw-text-[11.5px] tw-uppercase tw-tracking-[0.08em] tw-transition-colors",
62+
branch === "all"
63+
? "tw-border-accent tw-bg-accent tw-text-secondary"
64+
: "tw-border-cream/[0.18] tw-bg-secondary tw-text-[#c4cad6] hover:tw-border-[#8590a6] hover:tw-text-cream",
65+
)}
66+
>
67+
<span
68+
aria-hidden
69+
className="tw-h-2 tw-w-2"
70+
style={{ backgroundImage: RAINBOW }}
71+
/>
72+
All
73+
<span
74+
className={clsx(
75+
"tw-tabular-nums",
76+
branch === "all" ? "tw-text-secondary/60" : "tw-text-[#8590a6]",
77+
)}
78+
>
79+
{allCount.toLocaleString()}
80+
</span>
81+
</button>
82+
{BRANCH_ORDER.map((b) => {
83+
const active = branch === b;
84+
return (
85+
<button
86+
type="button"
87+
key={b}
88+
onClick={() => onBranch(b)}
89+
className={clsx(
90+
"tw-flex tw-items-center tw-gap-2.5 tw-border tw-px-3.5 tw-py-2 tw-font-mono tw-text-[11.5px] tw-uppercase tw-tracking-[0.08em] tw-transition-colors",
91+
active
92+
? "tw-border-accent tw-bg-accent tw-text-secondary"
93+
: "tw-border-cream/[0.18] tw-bg-secondary tw-text-[#c4cad6] hover:tw-border-[#8590a6] hover:tw-text-cream",
94+
)}
95+
>
96+
<span
97+
aria-hidden
98+
className="tw-h-2 tw-w-2"
99+
style={{ backgroundColor: BRANCH_META[b].color }}
100+
/>
101+
{BRANCH_META[b].short}
102+
<span
103+
className={clsx(
104+
"tw-tabular-nums",
105+
active ? "tw-text-secondary/60" : "tw-text-[#8590a6]",
106+
)}
107+
>
108+
{branchCounts[b].toLocaleString()}
109+
</span>
110+
</button>
111+
);
112+
})}
113+
</div>
114+
115+
{/* Facet bar */}
116+
<div className="tw-flex tw-flex-col tw-gap-6 tw-border-t tw-border-cream/10 tw-border-b tw-py-5 lg:tw-flex-row lg:tw-flex-wrap lg:tw-items-center">
117+
{/* Rank segmented */}
118+
<div className="tw-flex tw-items-center tw-gap-3">
119+
<span className="tw-font-mono tw-text-[10.5px] tw-uppercase tw-tracking-[0.14em] tw-text-[#8590a6]">
120+
Rank
121+
</span>
122+
<div className="tw-flex tw-border tw-border-cream/[0.18]">
123+
{RANK_OPTS.map((r) => (
124+
<button
125+
key={r.key}
126+
type="button"
127+
onClick={() => onRank(r.key)}
128+
className={clsx(
129+
"tw-px-3 tw-py-1.5 tw-font-mono tw-text-[11px] tw-uppercase tw-tracking-[0.1em] tw-transition-colors",
130+
rank === r.key
131+
? "tw-bg-accent tw-text-secondary"
132+
: "tw-bg-secondary tw-text-[#c4cad6] hover:tw-text-cream",
133+
)}
134+
>
135+
{r.label}
136+
</button>
137+
))}
138+
</div>
139+
</div>
140+
141+
{/* Family */}
142+
<label className="tw-flex tw-items-center tw-gap-3">
143+
<span className="tw-font-mono tw-text-[10.5px] tw-uppercase tw-tracking-[0.14em] tw-text-[#8590a6]">
144+
Family
145+
</span>
146+
<select
147+
value={family}
148+
onChange={(e) => onFamily(e.target.value as Family | "all")}
149+
className="tw-border tw-border-cream/[0.18] tw-bg-secondary tw-px-3 tw-py-1.5 tw-font-mono tw-text-[11px] tw-uppercase tw-tracking-[0.1em] tw-text-cream tw-outline-none focus:tw-border-accent"
150+
>
151+
<option value="all">All families</option>
152+
{FAMILIES.map((f) => (
153+
<option key={f} value={f}>
154+
{f}
155+
</option>
156+
))}
157+
</select>
158+
</label>
159+
160+
{/* Sort */}
161+
<label className="tw-flex tw-items-center tw-gap-3">
162+
<span className="tw-font-mono tw-text-[10.5px] tw-uppercase tw-tracking-[0.14em] tw-text-[#8590a6]">
163+
Sort
164+
</span>
165+
<select
166+
value={sort}
167+
onChange={(e) => onSort(e.target.value as SortKey)}
168+
className="tw-border tw-border-cream/[0.18] tw-bg-secondary tw-px-3 tw-py-1.5 tw-font-mono tw-text-[11px] tw-uppercase tw-tracking-[0.1em] tw-text-cream tw-outline-none focus:tw-border-accent"
169+
>
170+
{SORT_OPTS.map((s) => (
171+
<option key={s.key} value={s.key}>
172+
{s.label}
173+
</option>
174+
))}
175+
</select>
176+
</label>
177+
178+
<span className="tw-ml-auto tw-font-mono tw-text-[10.5px] tw-uppercase tw-tracking-[0.12em] tw-text-[#8590a6]">
179+
Showing{" "}
180+
<span className="tw-font-bold tw-text-cream tw-tabular-nums">
181+
{showing.toLocaleString()}
182+
</span>{" "}
183+
of{" "}
184+
<span className="tw-font-bold tw-text-cream tw-tabular-nums">
185+
{total.toLocaleString()}
186+
</span>{" "}
187+
Guides
188+
</span>
189+
</div>
190+
</div>
191+
);
192+
};
193+
194+
export default Filters;

0 commit comments

Comments
 (0)