Skip to content

Commit 59323c6

Browse files
authored
mdn-timelines
I didn't save all the prompts for this one (I'm using a new tool).
1 parent e62bd12 commit 59323c6

File tree

1 file changed

+324
-0
lines changed

1 file changed

+324
-0
lines changed

mdn-timelines.html

+324
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta name="viewport" content="width=device-width, initial-scale=1">
5+
<style>
6+
* {
7+
box-sizing: border-box;
8+
}
9+
body {
10+
font-family: Helvetica, Arial, sans-serif;
11+
font-size: 16px;
12+
margin: 2rem 1rem;
13+
max-width: 800px;
14+
margin: 0 auto;
15+
}
16+
#autocomplete-container {
17+
position: relative;
18+
width: 100%;
19+
}
20+
#search-input {
21+
width: 100%;
22+
padding: 10px;
23+
font-size: 16px;
24+
}
25+
#suggestions {
26+
position: absolute;
27+
top: 100%;
28+
left: 0;
29+
right: 0;
30+
max-height: 300px;
31+
overflow-y: auto;
32+
border: 1px solid #ddd;
33+
background: white;
34+
z-index: 10;
35+
margin: 0 15px;
36+
}
37+
.suggestion-item {
38+
padding: 10px;
39+
cursor: pointer;
40+
}
41+
.suggestion-item:hover, .suggestion-item.selected {
42+
background-color: #f0f0f0;
43+
}
44+
#timeline {
45+
position: relative;
46+
border-left: 2px solid #ccc;
47+
margin-left: 20px;
48+
padding-left: 20px;
49+
margin-top: 2em;
50+
}
51+
.event {
52+
margin-bottom: 20px;
53+
position: relative;
54+
}
55+
.event::before {
56+
content: '';
57+
position: absolute;
58+
left: -26px;
59+
top: 5px;
60+
width: 10px;
61+
height: 10px;
62+
border-radius: 50%;
63+
background: #3b82f6;
64+
}
65+
.event-date {
66+
font-size: 0.875rem;
67+
color: #666;
68+
}
69+
.event-browser {
70+
font-weight: 600;
71+
color: #1f2937;
72+
}
73+
.event-version {
74+
color: #059669;
75+
margin-left: 4px;
76+
}
77+
.error {
78+
color: #dc2626;
79+
margin-top: 8px;
80+
display: none;
81+
padding: 8px;
82+
background: #fef2f2;
83+
border-radius: 4px;
84+
}
85+
.not-supported {
86+
margin-top: 20px;
87+
padding: 12px;
88+
background: #f3f4f6;
89+
border-radius: 4px;
90+
}
91+
@media (max-width: 600px) {
92+
body {
93+
margin: 1rem;
94+
font-size: 14px;
95+
}
96+
#timeline {
97+
margin-left: 10px;
98+
padding-left: 10px;
99+
}
100+
.event::before {
101+
left: -16px;
102+
}
103+
}
104+
</style>
105+
</head>
106+
<body>
107+
<body>
108+
<h1>MDN Browser Support Timelines</h1>
109+
110+
<div id="autocomplete-container">
111+
<input type="text" id="search-input" placeholder="Search files...">
112+
<div id="suggestions"></div>
113+
</div>
114+
115+
<div id="timeline-section">
116+
<div id="timeline"></div>
117+
</div>
118+
119+
<script>
120+
let allFiles = [];
121+
let selectedFiles = [];
122+
123+
async function fetchAllFiles(repo, path = 'api', page = 1, allFiles = []) {
124+
const token = ''; // Optional: Add GitHub Personal Access Token for higher rate limits
125+
const headers = token ? { 'Authorization': `token ${token}` } : {};
126+
127+
try {
128+
const url = `https://api.github.com/repos/${repo}/contents/${path}?page=${page}&per_page=100`;
129+
const response = await fetch(url, { headers });
130+
131+
if (!response.ok) {
132+
throw new Error(`HTTP error! status: ${response.status}`);
133+
}
134+
135+
const files = await response.json();
136+
137+
// Add files to our collection
138+
allFiles.push(...files.map(file => file.path));
139+
140+
// If we got 100 files, there might be more pages
141+
if (files.length === 100) {
142+
return fetchAllFiles(repo, path, page + 1, allFiles);
143+
}
144+
145+
return allFiles;
146+
} catch (error) {
147+
console.error('Error fetching files:', error);
148+
return allFiles;
149+
}
150+
}
151+
152+
function setupAutocomplete() {
153+
const searchInput = document.getElementById('search-input');
154+
const suggestionsContainer = document.getElementById('suggestions');
155+
156+
searchInput.addEventListener('input', () => {
157+
const searchTerm = searchInput.value.toLowerCase();
158+
const filteredFiles = allFiles.filter(file =>
159+
file.toLowerCase().includes(searchTerm)
160+
).slice(0, 20); // Limit to 20 suggestions
161+
162+
// Clear previous suggestions
163+
suggestionsContainer.innerHTML = '';
164+
165+
// Create suggestion items
166+
filteredFiles.forEach((file, index) => {
167+
const suggestionItem = document.createElement('div');
168+
suggestionItem.textContent = file;
169+
suggestionItem.className = 'suggestion-item';
170+
suggestionItem.setAttribute('data-index', index);
171+
172+
suggestionItem.addEventListener('click', () => {
173+
searchInput.value = file;
174+
suggestionsContainer.innerHTML = '';
175+
fetchBrowserCompatData(file);
176+
});
177+
178+
suggestionsContainer.appendChild(suggestionItem);
179+
});
180+
});
181+
182+
// Keyboard navigation
183+
searchInput.addEventListener('keydown', (e) => {
184+
const suggestions = suggestionsContainer.children;
185+
186+
if (e.key === 'ArrowDown') {
187+
e.preventDefault();
188+
if (suggestions.length > 0) {
189+
const currentSelected = suggestionsContainer.querySelector('.selected');
190+
const nextIndex = currentSelected
191+
? Math.min(parseInt(currentSelected.getAttribute('data-index')) + 1, suggestions.length - 1)
192+
: 0;
193+
194+
if (currentSelected) currentSelected.classList.remove('selected');
195+
suggestions[nextIndex].classList.add('selected');
196+
}
197+
} else if (e.key === 'ArrowUp') {
198+
e.preventDefault();
199+
if (suggestions.length > 0) {
200+
const currentSelected = suggestionsContainer.querySelector('.selected');
201+
const prevIndex = currentSelected
202+
? Math.max(parseInt(currentSelected.getAttribute('data-index')) - 1, 0)
203+
: suggestions.length - 1;
204+
205+
if (currentSelected) currentSelected.classList.remove('selected');
206+
suggestions[prevIndex].classList.add('selected');
207+
}
208+
} else if (e.key === 'Enter') {
209+
const selectedItem = suggestionsContainer.querySelector('.selected') ||
210+
suggestionsContainer.children[0];
211+
212+
if (selectedItem) {
213+
searchInput.value = selectedItem.textContent;
214+
suggestionsContainer.innerHTML = '';
215+
fetchBrowserCompatData(selectedItem.textContent);
216+
}
217+
}
218+
});
219+
}
220+
221+
async function fetchBrowserCompatData(filePath) {
222+
try {
223+
const url = `https://bcd.developer.mozilla.org/bcd/api/v0/current/${filePath.replace('/', '.')}`;
224+
const response = await fetch(url);
225+
226+
if (!response.ok) {
227+
throw new Error(`HTTP error! status: ${response.status}`);
228+
}
229+
230+
const data = await response.json();
231+
renderTimeline(extractSupportData(data));
232+
} catch (error) {
233+
console.error('Error fetching browser compat data:', error);
234+
renderErrorMessage(error.message);
235+
}
236+
}
237+
238+
function extractSupportData(data) {
239+
const browsers = data.browsers;
240+
const support = data.data.__compat.support;
241+
242+
const supportData = [];
243+
const notSupported = [];
244+
245+
for (const [browserName, supportInfo] of Object.entries(support)) {
246+
if (!supportInfo[0]) continue;
247+
248+
if (supportInfo[0].version_added === false) {
249+
notSupported.push(browsers[browserName]?.name || browserName);
250+
continue;
251+
}
252+
253+
if (!supportInfo[0].version_added || !supportInfo[0].release_date) continue;
254+
255+
supportData.push({
256+
browser: browsers[browserName]?.name || browserName,
257+
version: supportInfo[0].version_added,
258+
date: supportInfo[0].release_date
259+
});
260+
}
261+
262+
return {
263+
supported: supportData.sort((a, b) => new Date(a.date) - new Date(b.date)),
264+
notSupported
265+
};
266+
}
267+
268+
function formatDate(dateStr) {
269+
return new Date(dateStr).toLocaleDateString('en-US', {
270+
year: 'numeric',
271+
month: 'long',
272+
day: 'numeric'
273+
});
274+
}
275+
276+
function renderTimeline(data) {
277+
const timeline = document.getElementById('timeline');
278+
timeline.innerHTML = '';
279+
280+
// Add supported browsers timeline
281+
data.supported.forEach(item => {
282+
const event = document.createElement('div');
283+
event.className = 'event';
284+
event.innerHTML = `
285+
<div class="event-date">${formatDate(item.date)}</div>
286+
<div>
287+
<span class="event-browser">${item.browser}</span>
288+
<span class="event-version">v${item.version}</span>
289+
</div>
290+
`;
291+
timeline.appendChild(event);
292+
});
293+
294+
// Add not supported browsers section if any
295+
if (data.notSupported.length > 0) {
296+
const notSupportedDiv = document.createElement('div');
297+
notSupportedDiv.className = 'not-supported';
298+
notSupportedDiv.innerHTML = `
299+
<strong>Not Supported:</strong> ${data.notSupported.join(', ')}
300+
`;
301+
timeline.appendChild(notSupportedDiv);
302+
}
303+
}
304+
305+
function renderErrorMessage(message) {
306+
const timeline = document.getElementById('timeline');
307+
timeline.innerHTML = `
308+
<div class="error">
309+
Error: ${message}
310+
</div>
311+
`;
312+
}
313+
314+
// Initialize the app
315+
async function init() {
316+
allFiles = await fetchAllFiles('mdn/browser-compat-data', 'api');
317+
setupAutocomplete();
318+
}
319+
320+
init();
321+
</script>
322+
</body>
323+
</body>
324+
</html>

0 commit comments

Comments
 (0)