How to make a simple collection tracker with HTML, CSS and JavaScript
After figuring out different approaches to making a collection tracker application, I have decided to create a simplified version with just HTML, CSS and JavaScript. Here is the final code with a few adjustments.
HTML structure (index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Collection Tracker</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<header>
<h1>Collection Tracker</h1>
<div class="actions">
<button id="add-item-btn">Add item</button>
<label class="import-label">
Import JSON
<input
type="file"
id="import-file-input"
accept=".json,application/json"
hidden
/>
</label>
<button id="export-btn">Export JSON</button>
</div>
</header>
<main>
<table id="items-table">
<thead>
<tr>
<th>#</th>
<th>Date</th>
<th>Gifted by</th>
<th>Origin</th>
<th>Description</th>
</tr>
</thead>
<tbody id="items-tbody">
<!-- Table contents generated by JS -->
</tbody>
</table>
</main>
<!-- Modal -->
<div id="modal-overlay" class="modal-overlay hidden">
<div class="modal">
<h2>Add new item</h2>
<form id="add-item-form">
<label>
Number
<input type="number" name="number" required />
</label>
<label>
Date
<input type="date" name="date" />
</label>
<label>
Gifted by
<input type="text" name="giftedBy" />
</label>
<label>
Origin
<input type="text" name="origin" />
</label>
<label>
Description
<textarea name="description"></textarea>
</label>
<div class="modal-actions">
<button type="submit">Save</button>
<button type="button" id="cancel-modal-btn">Cancel</button>
</div>
</form>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
Functionality (app.js)
let items = [];
const tbody = document.getElementById("items-tbody");
const addItemBtn = document.getElementById("add-item-btn");
const modalOverlay = document.getElementById("modal-overlay");
const addItemForm = document.getElementById("add-item-form");
const cancelModalBtn = document.getElementById("cancel-modal-btn");
const importFileInput = document.getElementById("import-file-input");
const exportBtn = document.getElementById("export-btn");
function renderItems() {
tbody.innerHTML = "";
items.forEach((item) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${item.number ?? ""}</td>
<td>${item.date ?? ""}</td>
<td>${item.giftedBy ?? ""}</td>
<td>${item.origin ?? ""}</td>
<td>${item.description ?? ""}</td>
`;
tbody.appendChild(tr);
});
}
function openModal() {
modalOverlay.classList.remove("hidden");
}
function closeModal() {
modalOverlay.classList.add("hidden");
addItemForm.reset();
}
addItemForm.addEventListener("submit", (e) => {
e.preventDefault();
const formData = new FormData(addItemForm);
const newItem = {
number: formData.get("number"),
date: formData.get("date"),
giftedBy: formData.get("giftedBy"),
origin: formData.get("origin"),
description: formData.get("description"),
};
items.push(newItem);
renderItems();
closeModal();
});
importFileInput.addEventListener("change", (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const text = event.target.result;
const parsed = JSON.parse(text);
if (!Array.isArray(parsed)) {
alert("Imported JSON is not an array.");
return;
}
items = parsed;
renderItems();
} catch (err) {
console.error("Import error:", err);
alert("Could not import data from this file. Ensure it is valid JSON.");
} finally {
importFileInput.value = "";
}
};
reader.readAsText(file);
});
exportBtn.addEventListener("click", () => {
const json = JSON.stringify(items, null, 2);
const blob = new Blob([json], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "collection.json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
addItemBtn.addEventListener("click", openModal);
cancelModalBtn.addEventListener("click", closeModal);
renderItems();
Basic styling (styles.css)
body {
font-family: system-ui, sans-serif;
margin: 0;
padding: 0;
background: #f4f4f5;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: #18181b;
color: #f9fafb;
}
h1 {
margin: 0;
font-size: 1.25rem;
}
.actions {
display: flex;
gap: 0.5rem;
}
button,
.import-label {
background: #4f46e5;
color: #f9fafb;
border: none;
border-radius: 0.25rem;
padding: 0.4rem 0.7rem;
font-size: 0.9rem;
cursor: pointer;
}
button:hover,
.import-label:hover {
background: #4338ca;
}
.import-label {
display: inline-flex;
align-items: center;
}
main {
padding: 1rem 1.5rem;
}
table {
width: 100%;
border-collapse: collapse;
background: #ffffff;
border-radius: 0.25rem;
overflow: hidden;
}
th,
td {
padding: 0.5rem;
border-bottom: 1px solid #e5e7eb;
font-size: 0.9rem;
}
th {
text-align: left;
background: #f3f4f6;
}
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.6);
display: flex;
align-items: center;
justify-content: center;
}
.modal-overlay.hidden {
display: none;
}
.modal {
background: #ffffff;
padding: 1rem 1.25rem;
border-radius: 0.5rem;
width: 320px;
max-width: 90vw;
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.25);
}
.modal h2 {
margin-top: 0;
margin-bottom: 0.75rem;
}
.modal label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.85rem;
}
.modal input,
.modal textarea {
width: 100%;
padding: 0.3rem;
margin-top: 0.15rem;
margin-bottom: 0.25rem;
font-size: 0.85rem;
border: 1px solid #d4d4d8;
border-radius: 0.25rem;
}
.modal textarea {
resize: vertical;
min-height: 60px;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 0.5rem;
}
If you want to get more details about making this collection tracker, check this post on my Medium blog.
Thank you for reading.
Check my 180+ stories on Medium
I write every Friday and share what I work on and learn.
@Dimterion
Visit my blog