Skip to content

Commit ee4660d

Browse files
committed
[Docs Site] Refactor changelog collection handling
1 parent b63fa0f commit ee4660d

File tree

11 files changed

+429
-504
lines changed

11 files changed

+429
-504
lines changed

package-lock.json

Lines changed: 11 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
"@cloudflare/puppeteer": "0.0.14",
3737
"@cloudflare/vitest-pool-workers": "0.6.0",
3838
"@cloudflare/workers-types": "4.20250121.0",
39-
"@codingheads/sticky-header": "1.0.2",
4039
"@expressive-code/plugin-collapsible-sections": "0.40.1",
4140
"@iarna/toml": "2.2.5",
4241
"@iconify-json/material-symbols": "1.2.13",
@@ -70,6 +69,7 @@
7069
"jsonc-parser": "3.3.1",
7170
"lz-string": "1.5.0",
7271
"marked": "15.0.6",
72+
"marked-base-url": "1.1.6",
7373
"mdast-util-mdx-expression": "2.0.1",
7474
"mermaid": "11.4.1",
7575
"node-html-parser": "7.0.1",

public/_redirects

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/dashboard-landing/ / 301
55
/tutorials/ /search/?content_type%5B0%5D=Tutorial 301
66
/sitemap.xml /sitemap-index.xml
7+
/deprecations/ /fundamentals/api/reference/deprecations/ 301
78

89
# 1dot1_redirect
910
/1.1.1.1/1.1.1.1-for-families/ /1.1.1.1/setup/ 301

src/components/Changelogs.tsx

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import { useState } from "react";
2+
import type { Changelogs } from "~/util/changelogs";
3+
4+
type Changelog = Changelogs[0];
5+
type Product = Changelog["product"];
6+
type Area = Product["area"];
7+
8+
function RSSFeed({
9+
selectedProduct,
10+
selectedProductRssUrl,
11+
selectedArea,
12+
selectedAreaRssUrl,
13+
}: {
14+
selectedProduct?: Product;
15+
selectedProductRssUrl: string;
16+
selectedArea?: Area;
17+
selectedAreaRssUrl?: string;
18+
}) {
19+
const name = selectedProduct?.name ?? "changelog";
20+
21+
return (
22+
<>
23+
<p>
24+
Subscribe to all {name} posts via{" "}
25+
<a href={selectedProductRssUrl}>RSS</a>.
26+
</p>
27+
{selectedArea && (
28+
<p>
29+
Subscribe to all {selectedArea.name} posts via{" "}
30+
<a href={selectedAreaRssUrl}>RSS</a>.
31+
</p>
32+
)}
33+
<p>
34+
Unless otherwise noted, all dates refer to the release date of the
35+
change.
36+
</p>
37+
</>
38+
);
39+
}
40+
41+
function Filters({
42+
changelogs,
43+
selectedProduct,
44+
updateSelectedProduct,
45+
selectedArea,
46+
updateSelectedArea,
47+
}: {
48+
changelogs: Changelogs;
49+
selectedProduct?: Product;
50+
updateSelectedProduct: (product: string) => void;
51+
selectedArea?: Area;
52+
updateSelectedArea: (area: string) => void;
53+
}) {
54+
const products = [...new Set(changelogs.flatMap((c) => c.product.name))];
55+
56+
const areas = [...new Set(changelogs.flatMap((c) => c.product.area.name))];
57+
58+
return (
59+
<div className="flex flex-col gap-2 md:flex-row">
60+
<label>
61+
Product
62+
<select
63+
className="ml-2"
64+
autoComplete="off"
65+
value={selectedProduct?.name ?? "all"}
66+
onChange={(e) => updateSelectedProduct(e.target.value)}
67+
>
68+
<option key="all" value="all">
69+
All
70+
</option>
71+
{products.map((product) => (
72+
<option key={product} value={product}>
73+
{product}
74+
</option>
75+
))}
76+
</select>
77+
</label>
78+
<label className="!mt-0">
79+
Product area
80+
<select
81+
className="ml-2"
82+
autoComplete="off"
83+
value={selectedArea?.name ?? "all"}
84+
onChange={(e) => updateSelectedArea(e.target.value)}
85+
>
86+
<option key="all" value="all">
87+
All
88+
</option>
89+
{areas.map((area) => (
90+
<option key={area} value={area}>
91+
{area}
92+
</option>
93+
))}
94+
</select>
95+
</label>
96+
</div>
97+
);
98+
}
99+
100+
function Badge({ product }: { product: Product }) {
101+
return (
102+
<a href={product.link}>
103+
<span className="sl-badge caution">{product.name}</span>
104+
</a>
105+
);
106+
}
107+
108+
function Content({ changelog }: { changelog: Changelog }) {
109+
if (changelog.individual_page) {
110+
return (
111+
<p>
112+
For more details, refer to the dedicated page for{" "}
113+
<a href={changelog.link}>{changelog.product.name}</a>
114+
</p>
115+
);
116+
} else {
117+
return (
118+
<div
119+
dangerouslySetInnerHTML={{ __html: changelog.content }}
120+
suppressHydrationWarning={true}
121+
/>
122+
);
123+
}
124+
}
125+
126+
export default function Changelogs({ changelogs }: { changelogs: Changelogs }) {
127+
const [selectedProduct, setSelectedProduct] = useState<Product>();
128+
const [selectedArea, setSelectedArea] = useState<Area>();
129+
130+
const [selectedProductRssUrl, setSelectedProductRssUrl] = useState(
131+
"/changelog/index.xml",
132+
);
133+
const [selectedAreaRssUrl, setSelectedAreaRssUrl] = useState<string>();
134+
135+
const filtered = changelogs.filter((changelog) => {
136+
if (selectedArea || selectedProduct) {
137+
return (
138+
changelog.product.area.name === selectedArea?.name ||
139+
changelog.product.name === selectedProduct?.name
140+
);
141+
}
142+
143+
return true;
144+
});
145+
146+
const grouped = Object.entries(
147+
Object.groupBy(filtered, (entry) => entry.date),
148+
)
149+
.sort()
150+
.reverse();
151+
152+
function updateSelectedProduct(product: string) {
153+
if (product === "all") {
154+
setSelectedProduct(undefined);
155+
return;
156+
}
157+
158+
const data = changelogs.find((c) => c.product.name === product)!.product;
159+
160+
setSelectedProduct(data);
161+
setSelectedProductRssUrl(data.changelog + "index.xml");
162+
163+
if (selectedArea && selectedArea.name !== data.area.name) {
164+
setSelectedArea(undefined);
165+
}
166+
}
167+
168+
function updateSelectedArea(area: string) {
169+
if (area === "all") {
170+
setSelectedArea(undefined);
171+
return;
172+
}
173+
174+
const data = changelogs.find((c) => c.product.area.name === area)!.product
175+
.area;
176+
177+
setSelectedArea(data);
178+
setSelectedAreaRssUrl(data.changelog + "index.xml");
179+
180+
if (selectedProduct && selectedProduct.name !== data.name) {
181+
setSelectedProduct(undefined);
182+
}
183+
}
184+
185+
return (
186+
<>
187+
<RSSFeed
188+
selectedProduct={selectedProduct}
189+
selectedProductRssUrl={selectedProductRssUrl}
190+
selectedArea={selectedArea}
191+
selectedAreaRssUrl={selectedAreaRssUrl}
192+
/>
193+
<p>
194+
Looking for API deprecations? They can be found on our{" "}
195+
<a href="/fundamentals/api/reference/deprecations/">
196+
dedicated deprecations page
197+
</a>
198+
.
199+
</p>
200+
<hr className="my-4" />
201+
<Filters
202+
changelogs={changelogs}
203+
selectedProduct={selectedProduct}
204+
updateSelectedProduct={updateSelectedProduct}
205+
selectedArea={selectedArea}
206+
updateSelectedArea={updateSelectedArea}
207+
/>
208+
{grouped.map(([date, entries], idx, arr) => (
209+
<div key={date}>
210+
<div className="!mt-8 flex gap-x-4">
211+
<div>
212+
<h4 className="sticky top-[--sl-nav-height] text-nowrap bg-[--sl-color-bg]">
213+
{date}
214+
</h4>
215+
</div>
216+
<div className="!mt-0">
217+
{entries?.map((entry, idx, arr) => (
218+
<div key={idx}>
219+
<div>
220+
<h4 className="sticky top-[--sl-nav-height] bg-[--sl-color-bg]">
221+
{entry.title}
222+
</h4>
223+
{!selectedProduct && <Badge product={entry.product} />}
224+
<div className="mt-4">
225+
<Content changelog={entry} />
226+
</div>
227+
</div>
228+
{idx !== arr.length - 1 && <hr className="my-4" />}
229+
</div>
230+
))}
231+
</div>
232+
</div>
233+
{idx !== arr.length - 1 && <hr className="my-4" />}
234+
</div>
235+
))}
236+
</>
237+
);
238+
}

0 commit comments

Comments
 (0)