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
10 changes: 4 additions & 6 deletions app/timeline/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { Metadata } from "next";
import { ParchmentLayout } from "@/components/ParchmentLayout";
import { PageHeading } from "@/components/PageHeading";
import { TimelineChart } from "@/components/TimelineChart";
import { TimelineMinimap } from "@/components/TimelineMinimap";
import { TimelineExplorer } from "@/components/TimelineExplorer";
import { sectionGlyphs } from "@/components/SectionGlyphs/SectionGlyphs";
import { loadAllBattles, loadAllEvents } from "@/lib/content";
import { buildTimeline } from "@/lib/timeline";
import { prepareTimeline } from "@/lib/timeline";
import styles from "@/app/timeline/page.module.scss";

export const metadata: Metadata = {
Expand All @@ -21,7 +20,7 @@ export default async function TimelinePage() {
]);
const published = battles.filter((b) => !b.frontmatter.draft);
const publishedEvents = events.filter((e) => !e.frontmatter.draft);
const model = buildTimeline({
const source = prepareTimeline({
battles: published.map((b) => b.frontmatter),
events: publishedEvents.map((e) => e.frontmatter),
});
Expand All @@ -37,9 +36,8 @@ export default async function TimelinePage() {
subtitle="Trace the centuries: the battles and great events of the Known World, laid out in time and place."
/>
<div className={styles.chartBleed}>
<TimelineChart model={model} bodyId="timeline-chart" />
<TimelineExplorer source={source} />
</div>
<TimelineMinimap ticks={model.ticks} targetId="timeline-chart" />
{hasApproximate && (
<p className={styles.legend}>* approximate or legendary date</p>
)}
Expand Down
9 changes: 9 additions & 0 deletions components/FilteredHouseList/FilteredHouseList.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
font-size: 0.85rem;
color: var(--ink-faded);
letter-spacing: 0.5px;

// Match the 50px height of the sibling `SortToggle` / `ViewToggle`
// buttons and enlarge the option text. Scoped here so the shared
// `listSearch.pageSizeSelect` (used by pagination) stays compact.
.rankSelect {
height: 50px;
font-size: 1.1rem;
padding: 0 1.9rem 0 0.85rem;
}
}

.rankLabel {
Expand Down
121 changes: 0 additions & 121 deletions components/FilteredHouseList/FilteredHouseList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -617,116 +617,6 @@ describe("FilteredHouseList page persistence", () => {
});
});

describe("FilteredHouseList status filter", () => {
const mixed: HouseItem[] = [
{ slug: "stark", name: "Stark", region: "north", regionLabel: "The North" },
{
slug: "reyne",
name: "Reyne",
region: "westerlands",
regionLabel: "The Westerlands",
extinct: true,
},
{
slug: "gardener",
name: "Gardener",
region: "reach",
regionLabel: "The Reach",
extinct: true,
},
];

it("exposes a house-status filter group", () => {
renderWithNuqs(<FilteredHouseList items={mixed} />);
expect(screen.getByRole("group", { name: /house status/i })).toBeDefined();
});

it("shows every house regardless of status by default", () => {
renderWithNuqs(<FilteredHouseList items={mixed} />);
expect(screen.getAllByRole("link").length).toBe(3);
});

it("shows only extinct houses when Extinct is selected", () => {
renderWithNuqs(<FilteredHouseList items={mixed} />);
fireEvent.click(screen.getByRole("button", { name: /extinct houses/i }));
const cards = screen.getAllByRole("link");
expect(cards.map((c) => c.textContent)).toEqual(
expect.arrayContaining([
expect.stringContaining("Reyne"),
expect.stringContaining("Gardener"),
]),
);
expect(cards.length).toBe(2);
});

it("shows only standing houses when Standing is selected", () => {
renderWithNuqs(<FilteredHouseList items={mixed} />);
fireEvent.click(screen.getByRole("button", { name: /standing houses/i }));
const cards = screen.getAllByRole("link");
expect(cards.length).toBe(1);
expect(cards[0].textContent).toContain("Stark");
});

it("reflects the status filter in the total count", () => {
renderWithNuqs(<FilteredHouseList items={mixed} />);
expect(screen.getByText("3 houses")).toBeDefined();
fireEvent.click(screen.getByRole("button", { name: /extinct houses/i }));
expect(screen.getByText("2 houses")).toBeDefined();
});

it("hydrates the filter from ?status=extinct on mount", () => {
renderWithNuqs(<FilteredHouseList items={mixed} />, {
searchParams: "?status=extinct",
});
expect(screen.getAllByRole("link").length).toBe(2);
expect(
screen
.getByRole("button", { name: /extinct houses/i })
.getAttribute("aria-pressed"),
).toBe("true");
});

it("writes ?status=extinct when Extinct is selected", async () => {
const { onUrlUpdate } = renderWithNuqs(<FilteredHouseList items={mixed} />);
fireEvent.click(screen.getByRole("button", { name: /extinct houses/i }));
await flushNuqs();
expect(lastQueryString(onUrlUpdate)).toBe("?status=extinct");
});

it("removes ?status= when the filter returns to Any status", async () => {
const { onUrlUpdate } = renderWithNuqs(
<FilteredHouseList items={mixed} />,
{
searchParams: "?status=extinct",
},
);
fireEvent.click(screen.getByRole("button", { name: /any status/i }));
await flushNuqs();
expect(lastQueryString(onUrlUpdate)).toBe("");
});

it("resets ?page= when the status filter changes", async () => {
const { onUrlUpdate } = renderWithNuqs(
<FilteredHouseList items={manyItems(70)} pageSize={24} />,
{ searchParams: "?page=2" },
);
fireEvent.click(screen.getByRole("button", { name: /standing houses/i }));
await flushNuqs();
expect(lastQueryString(onUrlUpdate)).toBe("?status=standing");
});

it("applies the status filter inside region grouping", () => {
renderWithNuqs(<FilteredHouseList items={mixed} />);
fireEvent.click(screen.getByRole("button", { name: /group by region/i }));
fireEvent.click(screen.getByRole("button", { name: /extinct houses/i }));
expect(
screen.getByRole("button", { name: /the westerlands/i }),
).toBeDefined();
expect(screen.getByRole("button", { name: /the reach/i })).toBeDefined();
expect(screen.queryByRole("button", { name: /the north/i })).toBeNull();
});
});

describe("FilteredHouseList rank filter", () => {
const ranked: HouseItem[] = [
{
Expand Down Expand Up @@ -778,17 +668,6 @@ describe("FilteredHouseList rank filter", () => {
expect(cards.length).toBe(2);
});

it("combines the rank filter with the status filter", () => {
renderWithNuqs(<FilteredHouseList items={ranked} />);
fireEvent.change(screen.getByRole("combobox", { name: /house rank/i }), {
target: { value: "lordly" },
});
fireEvent.click(screen.getByRole("button", { name: /extinct houses/i }));
const cards = screen.getAllByRole("link");
expect(cards.length).toBe(1);
expect(cards[0].textContent).toContain("Reyne");
});

it("reflects the rank filter in the total count", () => {
renderWithNuqs(<FilteredHouseList items={ranked} />);
fireEvent.change(screen.getByRole("combobox", { name: /house rank/i }), {
Expand Down
115 changes: 4 additions & 111 deletions components/FilteredHouseList/FilteredHouseList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,6 @@ const GROUP_OPTIONS = [
},
];

type StatusFilter = "all" | "standing" | "extinct";

const STATUS_FILTERS = ["all", "standing", "extinct"] as const;

const STATUS_OPTIONS = [
{ value: "all" as const, label: "Any status", icon: <AllStatusIcon /> },
{
value: "standing" as const,
label: "Standing houses",
icon: <StandingIcon />,
},
{ value: "extinct" as const, label: "Extinct houses", icon: <ExtinctIcon /> },
];

type RankFilter =
"all" | "royal" | "lordly" | "knightly" | "other" | "exiled" | "extinct";

Expand Down Expand Up @@ -133,10 +119,6 @@ export function FilteredHouseList({
const [{ search, dir, size, page: rawPage }, setParams] =
useQueryStates(parsers);
const page = Math.max(1, rawPage);
const [status, setStatus] = useQueryState(
"status",
parseAsStringLiteral(STATUS_FILTERS).withDefault("all"),
);
const [rank, setRank] = useQueryState(
"rank",
parseAsStringLiteral(RANK_FILTERS).withDefault("all"),
Expand Down Expand Up @@ -209,16 +191,10 @@ export function FilteredHouseList({
}, [items, dir]);

const facetFiltered = useMemo(() => {
const byStatus =
status === "all"
? sorted
: sorted.filter((item) =>
status === "extinct" ? !!item.extinct : !item.extinct,
);
return rank === "all"
? byStatus
: byStatus.filter((item) => item.rank === rank);
}, [sorted, status, rank]);
? sorted
: sorted.filter((item) => item.rank === rank);
}, [sorted, rank]);

const filtered = filterByName(facetFiltered, debounced);
const total = facetFiltered.length;
Expand Down Expand Up @@ -261,11 +237,6 @@ export function FilteredHouseList({
setParams({ dir: next, page: 1 });
};

const handleStatusChange = (next: StatusFilter) => {
setStatus(next === "all" ? null : next);
setParams({ page: 1 });
};

const handleRankChange = (next: string) => {
if (!isRankFilter(next)) return;
setRank(next === "all" ? null : next);
Expand Down Expand Up @@ -379,7 +350,7 @@ export function FilteredHouseList({
<label className={styles.rankFilter}>
<span className={styles.rankLabel}>Rank</span>
<select
className={listSearch.pageSizeSelect}
className={cx(listSearch.pageSizeSelect, styles.rankSelect)}
value={rank}
onChange={(e) => handleRankChange(e.target.value)}
aria-label="House rank"
Expand All @@ -391,12 +362,6 @@ export function FilteredHouseList({
))}
</select>
</label>
<ViewToggle
options={STATUS_OPTIONS}
value={status}
onChange={handleStatusChange}
ariaLabel="House status"
/>
<SortToggle value={dir} onChange={handleDirChange} />
<ViewToggle
options={VIEW_OPTIONS}
Expand Down Expand Up @@ -499,75 +464,3 @@ function RegionAccordion({
</Accordion>
);
}

function AllStatusIcon() {
return (
<svg
viewBox="0 0 16 16"
width="16"
height="16"
aria-hidden
focusable="false"
>
<path
d="M8 1.5 L13.5 3.5 V8 C13.5 11.3 8 14.5 8 14.5 C8 14.5 2.5 11.3 2.5 8 V3.5 Z"
fill="none"
stroke="currentColor"
strokeWidth="1.2"
strokeLinejoin="round"
/>
</svg>
);
}

function StandingIcon() {
return (
<svg
viewBox="0 0 16 16"
width="16"
height="16"
aria-hidden
focusable="false"
>
<path
d="M4 2 V14"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
/>
<path d="M5 2.5 L13 4.5 L5 6.5 Z" fill="currentColor" />
</svg>
);
}

function ExtinctIcon() {
return (
<svg
viewBox="0 0 16 16"
width="16"
height="16"
aria-hidden
focusable="false"
>
<path
d="M4 2 V14"
stroke="currentColor"
strokeWidth="1.3"
strokeLinecap="round"
/>
<path
d="M5 2.5 L13 4.5 L5 6.5 Z"
fill="none"
stroke="currentColor"
strokeWidth="1.2"
strokeLinejoin="round"
/>
<path
d="M2 2 L14 14"
stroke="currentColor"
strokeWidth="1.4"
strokeLinecap="round"
/>
</svg>
);
}
Loading
Loading