Timeline
This prompt asks for a single HTML file creating an interactive timeline web app where students can add and view chronological events. It also includes a PDF download of the timeline and event details.
Prompt
I am a teacher and want to create an interactive web app for my lessons. I want a cauldron simulator in which:
- The cauldron shows the current temperature.
- A 'liquid' in the cauldron changes visually (in height and color) based on the temperature.
- Two blocks are present: one for increasing (plus) and one for decreasing (minus) the temperature. You can drag these blocks to the cauldron to adjust the temperature.
- When dragging, animations (emojis) appear and a funny comment is shown depending on the temperature.
- There is also a reset button to reset the simulator to the starting values.
Design an interactive timeline in a single index.html file using React, CSS, JavaScript and HTML. The timeline should contain the following functionalities and properties:
1. **Enter events:**
- The timeline starts empty; students should be able to add their own historical events via an input form.
- The form contains the fields: date (numeric), title, description and optionally an image URL.
- After filling in, the event will automatically appear on the timeline in the correct chronological position.
2. **Chronological order:**
- The timeline is displayed horizontally with sufficient padding (left and right) so that the labels on the edges are clearly visible.
- The arrangement should be such that the youngest year (e.g. 1) is on the left and the oldest year (e.g. 1000) is on the right.
3. **Display of the timeline:**
- The timeline shows a visual marker and a label with the title and date for each event.
- When clicking on an event, a pop-up appears with extended information (title, date, description and if available the image).
4. **Download function:**
- Add a “Download PDF” button that generates a PDF of a screenshot of the timeline and a list with details of all events.
- The list of details (event details) is below the timeline, but is hidden in the web page (display: none) so that it is not visible on the website. It must be included in the PDF.
- During the generation of the PDF, the images must be temporarily hidden so that there is no unnecessary white space in the overview of events.
5. **Technical specifications:**
- Use html2canvas and jsPDF to generate the PDF.
- Make sure that the entire code (HTML, CSS, JavaScript/React) is in one index.html file.
- No zoom function is needed.
Create the code in such a way that it exactly meets the above specifications.Example Output
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Interactive Timeline - Add and Download Events</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, sans-serif;
background: #f0f0f0;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h2,
h3 {
text-align: center;
}
/* Timeline styling */
.timeline-container {
position: relative;
overflow-x: auto;
white-space: nowrap;
background: #fff;
border: 1px solid #ccc;
border-radius: 8px;
padding: 20px 50px; /* extra left/right padding so labels are fully visible */
margin: 20px 0;
min-height: 120px;
}
.timeline {
position: relative;
height: 100px;
border-top: 2px solid #333;
}
.event {
position: absolute;
top: -10px;
cursor: pointer;
transition: transform 0.2s;
}
.event:hover {
transform: scale(1.2);
}
.event-marker {
width: 20px;
height: 20px;
background: #0074d9;
border: 2px solid #fff;
border-radius: 50%;
}
.event-label {
position: absolute;
top: 25px;
left: -40px;
width: 100px;
text-align: center;
font-size: 12px;
pointer-events: none;
}
.no-events {
text-align: center;
color: #777;
padding-top: 40px;
}
/* Timeline details: hidden on screen, but temporarily visible for PDF download */
#timeline-details {
display: none;
}
.timeline-details .event-detail {
background: #fff;
border: 1px solid #ccc;
border-radius: 8px;
padding: 10px;
margin-bottom: 10px;
}
.timeline-details .event-detail img {
max-width: 100%;
border-radius: 4px;
margin-top: 10px;
}
/* Add Event Form */
.add-event-form {
background: #fff;
padding: 20px;
border-radius: 8px;
border: 1px solid #ccc;
margin-bottom: 20px;
}
.add-event-form h3 {
margin-top: 0;
}
.add-event-form label {
display: block;
margin: 10px 0 5px;
}
.add-event-form input[type='text'],
.add-event-form input[type='number'],
.add-event-form textarea {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.add-event-form textarea {
resize: vertical;
min-height: 60px;
}
.add-event-form .error {
color: red;
font-size: 12px;
}
.add-event-form button {
margin-top: 10px;
padding: 10px 20px;
cursor: pointer;
}
/* Download Button */
.download-button {
display: block;
margin: 20px auto;
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
}
/* Popup styles */
.popup-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.popup {
background: #fff;
border-radius: 8px;
padding: 20px;
width: 90%;
max-width: 500px;
position: relative;
animation: fadeIn 0.3s ease;
}
.popup img {
width: 100%;
border-radius: 4px;
margin-bottom: 10px;
}
.popup h3 {
margin-top: 0;
}
.popup .close-btn {
position: absolute;
top: 10px;
right: 10px;
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
</head>
<body>
<div id="root"></div>
<script
crossorigin
src="https://unpkg.com/react@18/umd/react.development.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"
></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
const { useState } = React;
/**************** Popup Component ****************/
function Popup({ event, onClose }) {
return (
<div className="popup-overlay" onClick={onClose}>
<div className="popup" onClick={(e) => e.stopPropagation()}>
<button className="close-btn" onClick={onClose}>
×
</button>
{event.image && <img src={event.image} alt={event.title} />}
<h3>{event.title}</h3>
<p>
<strong>Date:</strong> {event.date}
</p>
<p>{event.description}</p>
</div>
</div>
);
}
/**************** Timeline Component ****************/
function Timeline({ events, onEventClick }) {
if (events.length === 0) {
return (
<div className="timeline-container">
<div className="no-events">No events added.</div>
</div>
);
}
// Determine the minimum and maximum date.
// Currently: the youngest year (lowest value, e.g., 1) should be on the left and the oldest (highest value, e.g., 1000) on the right.
const dates = events.map((e) => Number(e.date));
const minDate = Math.min(...dates);
const maxDate = Math.max(...dates);
const range = maxDate - minDate || 1;
return (
<div className="timeline-container">
<div className="timeline">
{events.map((event) => {
// If event.date is equal to minDate, the position is 0% (left) and if equal to maxDate, 100% (right).
const posPercent = ((event.date - minDate) / range) * 100;
return (
<div
key={event.id}
className="event"
style={{ left: `${posPercent}%` }}
onClick={(e) => {
e.stopPropagation();
onEventClick(event);
}}
>
<div className="event-marker"></div>
<div className="event-label">
<div>{event.title}</div>
<div style={{ fontSize: '10px', color: '#666' }}>
{event.date}
</div>
</div>
</div>
);
})}
</div>
</div>
);
}
/**************** Timeline Details Component ****************/
function TimelineDetails({ events }) {
if (events.length === 0) {
return null;
}
// Sort events in ascending order: youngest (lowest year) first, oldest (highest year) last.
const sortedEvents = [...events].sort((a, b) => a.date - b.date);
return (
<div id="timeline-details" className="timeline-details">
{sortedEvents.map((event) => (
<div key={event.id} className="event-detail">
<h4>
{event.title} ({event.date})
</h4>
<p>{event.description}</p>
{event.image && <img src={event.image} alt={event.title} />}
</div>
))}
</div>
);
}
/**************** Add Event Form Component ****************/
function AddEventForm({ onAddEvent }) {
const [date, setDate] = useState('');
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [image, setImage] = useState('');
const [errors, setErrors] = useState({});
const validate = () => {
const errs = {};
if (!date) errs.date = 'Date is required.';
else if (isNaN(Number(date)))
errs.date = 'Date must be a number.';
if (!title.trim()) errs.title = 'Title is required.';
if (!description.trim())
errs.description = 'Description is required.';
return errs;
};
const handleSubmit = (e) => {
e.preventDefault();
const errs = validate();
if (Object.keys(errs).length > 0) {
setErrors(errs);
return;
}
const newEvent = {
id: Date.now(),
date: Number(date),
title: title.trim(),
description: description.trim(),
image: image.trim() || null,
};
onAddEvent(newEvent);
setErrors({});
setDate('');
setTitle('');
setDescription('');
setImage('');
};
return (
<form className="add-event-form" onSubmit={handleSubmit}>
<h3>Add a new event</h3>
<label>Date (e.g., 1945):</label>
<input
type="number"
value={date}
onChange={(e) => setDate(e.target.value)}
placeholder="Year"
/>
{errors.date && <div className="error">{errors.date}</div>}
<label>Title:</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title of the event"
/>
{errors.title && <div className="error">{errors.title}</div>}
<label>Description:</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Describe the event"
/>
{errors.description && (
<div className="error">{errors.description}</div>
)}
<label>Image URL (optional):</label>
<input
type="text"
value={image}
onChange={(e) => setImage(e.target.value)}
placeholder="http://..."
/>
<button type="submit">Add event</button>
</form>
);
}
/**************** Main App Component ****************/
function App() {
// Start with an empty list of events.
const [events, setEvents] = useState([]);
const [selectedEvent, setSelectedEvent] = useState(null);
// Add a new event and sort in ascending order (youngest first).
const handleAddEvent = (newEvent) => {
const updated = [...events, newEvent].sort((a, b) => a.date - b.date);
setEvents(updated);
};
const handleEventClick = (event) => {
setSelectedEvent(event);
};
// Download function: Takes a screenshot of the 'downloadable-area' (timeline and hidden details) and downloads it as PDF.
// All images are temporarily hidden so that no unnecessary white space is created.
const downloadPDF = () => {
const area = document.getElementById('downloadable-area');
// Temporarily hide all <img> elements within the downloadable area
const images = area.querySelectorAll('img');
const originalDisplays = [];
images.forEach((img, index) => {
originalDisplays[index] = img.style.display;
img.style.display = 'none';
});
// Make the timeline details temporarily visible for the screenshot
const details = document.getElementById('timeline-details');
const originalDetailsDisplay = details.style.display;
details.style.display = 'block';
html2canvas(area, { scale: 2 }).then((canvas) => {
const imgData = canvas.toDataURL('image/png');
const pdf = new jspdf.jsPDF('p', 'pt', 'a4');
const pdfWidth = pdf.internal.pageSize.getWidth();
const pdfHeight = (canvas.height * pdfWidth) / canvas.width;
pdf.addImage(imgData, 'PNG', 0, 0, pdfWidth, pdfHeight);
pdf.save('timeline.pdf');
// Restore the original display of images and details
images.forEach((img, index) => {
img.style.display = originalDisplays[index];
});
details.style.display = originalDetailsDisplay;
});
};
return (
<div className="container">
<h2>Interactive Timeline</h2>
{/* Downloadable content: timeline and details (hidden on screen, but visible in PDF) */}
<div id="downloadable-area">
<Timeline events={events} onEventClick={handleEventClick} />
<TimelineDetails events={events} />
</div>
<AddEventForm onAddEvent={handleAddEvent} />
<button className="download-button" onClick={downloadPDF}>
Download PDF
</button>
{selectedEvent && (
<Popup
event={selectedEvent}
onClose={() => setSelectedEvent(null)}
/>
)}
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>