Skip to content
Merged

ical #20

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
13 changes: 13 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { browsers, features } from './data.js';
import { browserIcons } from './browser-icons.js';
import { downloadICal } from './ical-generator.js';

class TimelineApp {
constructor() {
Expand Down Expand Up @@ -710,6 +711,18 @@ class TimelineApp {
}

init() {
// Add event listeners for iCal download buttons
const downloadTopBtn = document.getElementById('download-ical-top');
const downloadBottomBtn = document.getElementById('download-ical-bottom');

if (downloadTopBtn) {
downloadTopBtn.addEventListener('click', downloadICal);
}

if (downloadBottomBtn) {
downloadBottomBtn.addEventListener('click', downloadICal);
}

const groups = this.groupFeaturesByDate();

// Find the current month for scrolling
Expand Down
155 changes: 155 additions & 0 deletions ical-generator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { browsers, features } from './data.js';

export function generateICal() {
const ical = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Web Features Timeline//Baseline Timeline//EN',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
'X-WR-CALNAME:Web Features Timeline',
'X-WR-CALDESC:Timeline of web features and browser support',
''
];

// Process features similar to the main app
const processedFeatures = [];

Object.entries(features)
.forEach(([id, data]) => {
// Get all ship dates from browsers
const shipDates = Object.entries(data.status?.support || {})
.map(([browser, version]) => {
if (typeof version !== 'string') {
return null;
}
const cleanVersion = version.replace('≤', '');
const browserData = browsers[browser];
if (!browserData?.releases) {
return null;
}
const release = browserData.releases.find(r => r.version === cleanVersion);
if (!release) {
return null;
}
return release.date ? { date: parseLocalDate(release.date), browser, version: cleanVersion } : null;
})
.filter(item => item !== null);

if (!shipDates.length) return;

// Sort ship dates chronologically
shipDates.sort((a, b) => a.date - b.date);

// The newly available date is when the last browser adds support
const newlyAvailableDate = shipDates[shipDates.length - 1].date;

// Calculate widely available date (30 months after newly available)
const widelyAvailableDate = new Date(newlyAvailableDate);
widelyAvailableDate.setMonth(widelyAvailableDate.getMonth() + 30);

// Get current date for comparison
const now = new Date();
now.setHours(0, 0, 0, 0);

// Always add the newly available entry
processedFeatures.push({
id,
name: data.name || id,
description: data.description,
date: newlyAvailableDate,
displayType: 'newly-available',
shipDates
});

// Only add widely available entry if the feature is already available and has full browser support
if (newlyAvailableDate <= now && data.status?.baseline !== false) {
processedFeatures.push({
id,
name: data.name || id,
description: data.description,
date: widelyAvailableDate,
displayType: 'widely-available',
shipDates
});
}
});

// Sort features by date in ascending order (earliest first) for chronological order in the calendar
const sortedFeatures = processedFeatures
.filter(feature => feature && feature.date && !isNaN(feature.date))
.sort((a, b) => a.date - b.date);

// Generate calendar events
sortedFeatures.forEach(feature => {
const eventDate = feature.date;
const formattedDate = formatDateForICal(eventDate);

// Create event title
const eventType = feature.displayType === 'newly-available' ? '🆕 Newly Available' : '✅ Widely Available';
const title = `${eventType}: ${feature.name}`;

// Create event description
const browserInfo = feature.shipDates
.map(sd => `${sd.browser} ${sd.version}`)
.join(', ');

const description = `${feature.description || ''}\n\nBrowser Support: ${browserInfo}`;

// Create event
ical.push(
'BEGIN:VEVENT',
`UID:${feature.id}-${feature.displayType}-${formattedDate}@web-features-timeline`,
`DTSTART;VALUE=DATE:${formattedDate}`,
`DTEND;VALUE=DATE:${formatDateForICal(new Date(eventDate.getTime() + 24 * 60 * 60 * 1000))}`,
`SUMMARY:${escapeICalText(title)}`,
`DESCRIPTION:${escapeICalText(description)}`,
'CLASS:PUBLIC',
'STATUS:CONFIRMED',
'TRANSP:TRANSPARENT',
'END:VEVENT'
);
});

ical.push('END:VCALENDAR');

return ical.join('\r\n');
}

function parseLocalDate(dateString) {
const [year, month, day] = dateString.split('-').map(Number);
return new Date(year, month - 1, day);
}

function formatDateForICal(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}${month}${day}`;
}

function escapeICalText(text) {
return text
.replace(/\\/g, '\\\\')
.replace(/;/g, '\\;')
.replace(/,/g, '\\,')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r');
}

export function downloadICal() {
const icalContent = generateICal();
const blob = new Blob([icalContent], { type: 'text/calendar;charset=utf-8' });
const url = URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = 'web-features-timeline.ics';
link.style.display = 'none';

document.body.appendChild(link);
link.click();
document.body.removeChild(link);

URL.revokeObjectURL(url);
}
19 changes: 19 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,29 @@
<link rel="stylesheet" href="styles.css">
</head>
<body>
<header class="download-header">
<div class="download-container">
<h1>Web Features Timeline</h1>
<p>Timeline of web features and browser support</p>
<button id="download-ical-top" class="download-btn" type="button">
📅 Download iCal Calendar
</button>
</div>
</header>

<main class="timeline-container">
<div class="timeline-content"></div>
</main>

<footer class="download-footer">
<div class="download-container">
<button id="download-ical-bottom" class="download-btn">
📅 Download iCal Calendar
</button>
<p>Add web features timeline to your calendar</p>
</div>
</footer>

<script type="module" src="app.js"></script>
</body>
</html>
125 changes: 124 additions & 1 deletion styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -848,4 +848,127 @@ body {
width: 100%;
max-width: none; /* Allow full width on mobile */
}
}
}

@media (max-width: 768px) {
.browser-support-table {
font-size: 0.875rem;
}
}

/* Download header and footer styles */
.download-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem 0;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.download-footer {
background: #f8f9fa;
border-top: 1px solid #e9ecef;
padding: 2rem 0;
text-align: center;
margin-top: 3rem;
}

.download-container {
max-width: 1000px;
margin: 0 auto;
padding: 0 20px;
}

.download-header h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.download-header p {
font-size: 1.1rem;
opacity: 0.9;
margin-bottom: 1.5rem;
}

.download-footer p {
font-size: 0.9rem;
color: #6c757d;
margin-top: 0.5rem;
}

.download-btn {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
display: inline-flex;
align-items: center;
gap: 0.5rem;
}

.download-btn:hover {
background: linear-gradient(135deg, #218838 0%, #1ea085 100%);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4);
}

.download-btn:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(40, 167, 69, 0.3);
}

.download-btn:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(40, 167, 69, 0.3), 0 4px 15px rgba(40, 167, 69, 0.3);
}

/* Adjust timeline container margin for header */
.timeline-container {
max-width: 1000px;
margin: 0 auto;
padding: 2rem 20px;
}

/* Mobile responsive styles for download sections */
@media (max-width: 768px) {
Comment thread
rviscomi marked this conversation as resolved.
.browser-support-table {
width: 100%;
font-size: 0.9rem;
}

.download-header h1 {
font-size: 2rem;
}

.download-header p {
font-size: 1rem;
}

.download-btn {
padding: 0.625rem 1.25rem;
font-size: 0.9rem;
}

.download-container {
padding: 0 15px;
}
}

@media (max-width: 480px) {
.download-header h1 {
font-size: 1.75rem;
}

.download-btn {
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
}