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
12 changes: 12 additions & 0 deletions src/containers/career-guides/branch-meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Branch, BranchShort } from "./types";

// Distinct branch colors (handoff §Branch colors), tuned for use on our navy ground.
export const BRANCH_META: Record<Branch, { short: BranchShort; color: string }> = {
Army: { short: "ARMY", color: "#6b8050" },
Navy: { short: "NAVY", color: "#5b87c4" },
"Air Force": { short: "USAF", color: "#8eb4d8" },
"Marine Corps": { short: "USMC", color: "#d9514a" },
"Coast Guard": { short: "USCG", color: "#e89149" },
};

export const BRANCH_ORDER: Branch[] = ["Army", "Navy", "Air Force", "Marine Corps", "Coast Guard"];
96 changes: 96 additions & 0 deletions src/containers/career-guides/category-showcase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import clsx from "clsx";
import type { Family, GuideEntry } from "./types";

interface Props {
guides: GuideEntry[];
onPick: (family: Family) => void;
}

const FEATURED_FAMILIES: Array<{
family: Family;
number: string;
blurb: string;
}> = [
{
family: "Cyber",
number: "/ 01",
blurb: "Signal warfare and cryptologic operators translate directly to security engineering.",
},
{
family: "IT / Comms",
number: "/ 02",
blurb: "Network and signal techs become DevOps, SRE, and platform engineers.",
},
{
family: "Aviation",
number: "/ 03",
blurb: "Avionics and aircrew specialties bridge to embedded systems and aerospace software.",
},
{
family: "Intelligence",
number: "/ 04",
blurb: "Intel analysts move into data engineering, threat hunting, and ML for defense.",
},
];

const medianSalary = (rows: GuideEntry[]): string => {
if (rows.length === 0) return "—";
const lows = rows.map((r) => r.salaryLow).sort((a, b) => a - b);
const highs = rows.map((r) => r.salaryHigh).sort((a, b) => a - b);
const lo = lows[Math.floor(lows.length / 2)];
const hi = highs[Math.floor(highs.length / 2)];
return `$${lo}–${hi}K typical`;
};

const CategoryShowcase = ({ guides, onPick }: Props) => {
return (
<section className="tw-bg-secondary tw-py-16 md:tw-py-20">
<div className="tw-container">
<div className="tw-mb-10 tw-flex tw-items-center tw-gap-3">
<span className="tw-inline-block tw-h-[2px] tw-w-4 tw-bg-primary" />
<span className="tw-font-mono tw-text-[11px] tw-uppercase tw-tracking-[0.14em] tw-text-[#8590a6]">
Pathways that ship in civilian roles
</span>
</div>

<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">
{FEATURED_FAMILIES.map(({ family, number, blurb }, idx) => {
const rows = guides.filter((g) => g.family === family);
return (
<button
type="button"
key={family}
onClick={() => onPick(family)}
className={clsx(
"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",
"hover:tw-bg-[#0c2549]",
idx > 0 && "lg:tw-border-l lg:tw-border-cream/10",
idx > 0 && idx < FEATURED_FAMILIES.length && "sm:tw-border-l sm:tw-border-cream/10",
)}
>
<span className="tw-font-mono tw-text-[11px] tw-uppercase tw-tracking-[0.14em] tw-text-[#5a6478] group-hover:tw-text-accent">
{number}
</span>
<span className="tw-font-heading tw-text-[22px] tw-font-medium tw-uppercase tw-text-cream [letter-spacing:-0.01em]">
{family}
</span>
<span className="tw-font-body tw-text-[14px] tw-leading-[1.55] tw-text-[#c4cad6]">
{blurb}
</span>
<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]">
<span>
<span className="tw-text-cream">{rows.length.toLocaleString()}</span>{" "}
guides
</span>
<span>{medianSalary(rows)}</span>
</span>
</button>
);
})}
</div>
</div>
</section>
);
};

export default CategoryShowcase;
44 changes: 44 additions & 0 deletions src/containers/career-guides/cta-band.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Link from "next/link";

const CtaBand = () => (
<section className="tw-border-t tw-border-cream/10 tw-bg-secondary tw-py-20 md:tw-py-24">
<div className="tw-container">
<div className="tw-mb-6 tw-flex tw-items-center tw-gap-3">
<span className="tw-inline-block tw-h-[2px] tw-w-4 tw-bg-primary" />
<span className="tw-font-mono tw-text-[11px] tw-uppercase tw-tracking-[0.14em] tw-text-[#8590a6]">
Cohort 2027 · Intake Open
</span>
</div>

<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)]">
Found your translation?
<br />
<span className="tw-text-[#8590a6]">
Now build the skills to land it.
</span>
</h2>

<p className="tw-mt-7 tw-max-w-[680px] tw-font-body tw-text-[17px] tw-leading-[1.55] tw-text-[#c4cad6]">
Vets Who Code is a free, full-time software engineering accelerator for active duty,
veterans, and military spouses. 17 weeks, 25 modules, and a portfolio that ships.
</p>

<div className="tw-mt-9 tw-flex tw-flex-wrap tw-gap-4">
<Link
href="/apply"
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]"
>
Apply for Cohort 2027 →
</Link>
<Link
href="/programs/core-curriculum"
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]"
>
Read the curriculum
</Link>
</div>
</div>
</section>
);

export default CtaBand;
194 changes: 194 additions & 0 deletions src/containers/career-guides/filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import clsx from "clsx";
import { BRANCH_META, BRANCH_ORDER } from "./branch-meta";
import { FAMILIES } from "@/lib/career-guides";
import type { Branch, Family, Rank, SortKey } from "./types";

interface Props {
branchCounts: Record<Branch, number>;
branch: "all" | Branch;
onBranch: (v: "all" | Branch) => void;
rank: "all" | Rank;
onRank: (v: "all" | Rank) => void;
family: "all" | Family;
onFamily: (v: "all" | Family) => void;
sort: SortKey;
onSort: (v: SortKey) => void;
showing: number;
total: number;
}

const RANK_OPTS: Array<{ key: "all" | Rank; label: string }> = [
{ key: "all", label: "All" },
{ key: "Enlisted", label: "Enlisted" },
{ key: "Warrant", label: "Warrant" },
{ key: "Officer", label: "Officer" },
];

const SORT_OPTS: Array<{ key: SortKey; label: string }> = [
{ key: "code", label: "Code (A→Z)" },
{ key: "title", label: "Title (A→Z)" },
{ key: "salaryHigh", label: "Salary (High→Low)" },
{ key: "salaryLow", label: "Salary (Low→High)" },
{ key: "demand", label: "Demand" },
];

const RAINBOW =
"linear-gradient(90deg, #6b8050 0%, #6b8050 20%, #5b87c4 20%, #5b87c4 40%, #8eb4d8 40%, #8eb4d8 60%, #d9514a 60%, #d9514a 80%, #e89149 80%, #e89149 100%)";

const Filters = ({
branchCounts,
branch,
onBranch,
rank,
onRank,
family,
onFamily,
sort,
onSort,
showing,
total,
}: Props) => {
const allCount = BRANCH_ORDER.reduce((sum, b) => sum + branchCounts[b], 0);

return (
<div className="tw-flex tw-flex-col tw-gap-6">
{/* Branch chips */}
<div className="tw-flex tw-flex-wrap tw-gap-2">
<button
type="button"
onClick={() => onBranch("all")}
className={clsx(
"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",
branch === "all"
? "tw-border-accent tw-bg-accent tw-text-secondary"
: "tw-border-cream/[0.18] tw-bg-secondary tw-text-[#c4cad6] hover:tw-border-[#8590a6] hover:tw-text-cream",
)}
>
<span
aria-hidden
className="tw-h-2 tw-w-2"
style={{ backgroundImage: RAINBOW }}
/>
All
<span
className={clsx(
"tw-tabular-nums",
branch === "all" ? "tw-text-secondary/60" : "tw-text-[#8590a6]",
)}
>
{allCount.toLocaleString()}
</span>
</button>
{BRANCH_ORDER.map((b) => {
const active = branch === b;
return (
<button
type="button"
key={b}
onClick={() => onBranch(b)}
className={clsx(
"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",
active
? "tw-border-accent tw-bg-accent tw-text-secondary"
: "tw-border-cream/[0.18] tw-bg-secondary tw-text-[#c4cad6] hover:tw-border-[#8590a6] hover:tw-text-cream",
)}
>
<span
aria-hidden
className="tw-h-2 tw-w-2"
style={{ backgroundColor: BRANCH_META[b].color }}
/>
{BRANCH_META[b].short}
<span
className={clsx(
"tw-tabular-nums",
active ? "tw-text-secondary/60" : "tw-text-[#8590a6]",
)}
>
{branchCounts[b].toLocaleString()}
</span>
</button>
);
})}
</div>

{/* Facet bar */}
<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">
{/* Rank segmented */}
<div className="tw-flex tw-items-center tw-gap-3">
<span className="tw-font-mono tw-text-[10.5px] tw-uppercase tw-tracking-[0.14em] tw-text-[#8590a6]">
Rank
</span>
<div className="tw-flex tw-border tw-border-cream/[0.18]">
{RANK_OPTS.map((r) => (
<button
key={r.key}
type="button"
onClick={() => onRank(r.key)}
className={clsx(
"tw-px-3 tw-py-1.5 tw-font-mono tw-text-[11px] tw-uppercase tw-tracking-[0.1em] tw-transition-colors",
rank === r.key
? "tw-bg-accent tw-text-secondary"
: "tw-bg-secondary tw-text-[#c4cad6] hover:tw-text-cream",
)}
>
{r.label}
</button>
))}
</div>
</div>

{/* Family */}
<label className="tw-flex tw-items-center tw-gap-3">
<span className="tw-font-mono tw-text-[10.5px] tw-uppercase tw-tracking-[0.14em] tw-text-[#8590a6]">
Family
</span>
<select
value={family}
onChange={(e) => onFamily(e.target.value as Family | "all")}
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"
>
<option value="all">All families</option>
{FAMILIES.map((f) => (
<option key={f} value={f}>
{f}
</option>
))}
</select>
</label>

{/* Sort */}
<label className="tw-flex tw-items-center tw-gap-3">
<span className="tw-font-mono tw-text-[10.5px] tw-uppercase tw-tracking-[0.14em] tw-text-[#8590a6]">
Sort
</span>
<select
value={sort}
onChange={(e) => onSort(e.target.value as SortKey)}
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"
>
{SORT_OPTS.map((s) => (
<option key={s.key} value={s.key}>
{s.label}
</option>
))}
</select>
</label>

<span className="tw-ml-auto tw-font-mono tw-text-[10.5px] tw-uppercase tw-tracking-[0.12em] tw-text-[#8590a6]">
Showing{" "}
<span className="tw-font-bold tw-text-cream tw-tabular-nums">
{showing.toLocaleString()}
</span>{" "}
of{" "}
<span className="tw-font-bold tw-text-cream tw-tabular-nums">
{total.toLocaleString()}
</span>{" "}
Guides
</span>
</div>
</div>
);
};

export default Filters;
Loading
Loading