App Introduction

Nothing is more embarrassing than forgetting a birthday or anniversary.
To help myself remember special dates, I created a very simple, browser-based JavaScript web app called Celebrations, and I’m sharing it with you here as a free beginner-friendly project.
Preview a working demo version of the Celebrations web app which was made from the code provided below.
Prerequisites: Just basic computer skills and knowing how to copy, paste, and save a file.
Time Commitment: About 1 hour or less
Difficulty: Easy
App Source Code
To get started, copy the source code below and paste it into a code or text editor to that you can edit, delete and add your own names and birthday dates.
To do so, find this part (located in the lower half of the code):
// —————————–
// Birthdays & Anniversaries
//—————————–
and replace the sample names and dates with your own.
Don’t change anything else.
When done, save the file as celebrations.html
After that, open it in your browser.
Here is the full code for you to copy and edit to your liking:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Celebrations</title>
<meta name="viewport" content="width=1280" />
<style>
:root {
--bg-main: #25282d;
--bg-surface: #34373F;
--text-primary: #abb2ba;
--text-secondary: #abb2ba;
--accent-primary: #4169E1;
--accent-secondary: #45B52D;
--domain-green: darkgreen;
--domain-green-text: #FFFFFF;
--domain-orange: #F7B42C;
--domain-orange-text: #2A2D34;
--domain-red: #F85149;
--domain-red-text: #FFFFFF;
--border-subtle: #40444C;
--border-radius: 6px;
--spacing-unit: 20px;
--font-sans: Inter, system-ui, sans-serif;
--font-mono: Fira Code, monospace;
}
body {
background-color: var(--bg-main);
color: var(--text-primary);
font-family: var(--font-sans);
margin: 0;
padding: 0;
font-size: 1.0625rem; /* 17px */
line-height: 1.6;
}
.dashboard-container {
max-width: 1140px;
margin: 0 auto;
padding: calc(var(--spacing-unit) * 1.5) var(--spacing-unit);
}
h2 {
font-size: 1.6rem;
font-weight: normal;
margin-bottom: 20px;
color: var(--text-primary);
}
footer p {
font-size: 1rem;
font-weight: 500;
color: var(--text-muted);
margin-top: 20px;
text-align: center;
}
/* Apply to payments table as well */
.payments-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 30px;
}
.payments-table th,
.payments-table td {
padding: 12px;
border-bottom: 1px solid var(--border-subtle);
text-align: left;
}
.payments-table th {
background-color: var(--bg-surface);
color: var(--text-secondary);
font-weight: 600;
font-size: 0.95rem;
}
.days-remaining-tag {
font-weight: 600;
padding: 6px 10px;
border-radius: var(--border-radius);
display: inline-block;
font-size: 0.9em;
}
.days-green {
background-color: var(--domain-green);
color: var(--domain-green-text);
}
.days-orange {
background-color: var(--domain-orange);
color: var(--domain-orange-text);
}
.days-red {
background-color: var(--domain-red);
color: var(--domain-red-text);
}
.notes-info {
font-style: italic;
color: var(--text-secondary);
white-space: pre-wrap;
word-break: break-word;
}
.full-cell {
display: block;
width: 60%;
text-align: center;
padding: 8px 20;
border-radius: var(--border-radius);
}
th.sortable {
cursor: pointer;
position: relative;
user-select: none;
}
th.sortable::after {
content: '';
font-size: 0.8em;
margin-left: 6px;
}
th.sortable.asc::after {
content: '▲';
}
th.sortable.desc::after {
content: '▼';
}
</style>
</head>
<body>
<div class="dashboard-container">
<section id="celebration-section">
<h2>Birthdays & Anniversaries</h2>
<div id="celebration-table-wrapper"></div>
</section>
</div>
<script>
// -----------------------------
// Footer Date
// -----------------------------
document.addEventListener("DOMContentLoaded", () => {
const today = new Date();
const dateSpan = document.getElementById("footer-date");
if (dateSpan) {
dateSpan.textContent = today.toLocaleDateString(undefined, {
weekday: "short",
year: "numeric",
month: "short",
day: "numeric"
});
}
if (document.getElementById("celebration-table-wrapper")) {
// Sort by days until by default
celebrationSort.column = "days";
celebrationSort.direction = "asc";
renderCelebrationTable();
}
});
// -----------------------------
// Birthdays & Anniversaries
// -----------------------------
const celebrations = [
{ name: "Alex Johnson", type: "Birthday", date: "1992-03-15T00:00:00", notes: "Friend's birthday" },
{ name: "Maria Garcia", type: "Anniversary", date: "1985-07-22T00:00:00", notes: "15th wedding anniversary" },
{ name: "Robert Chen", type: "Birthday", date: "1978-11-05T00:00:00", notes: "Retirement celebration" },
{ name: "Sarah Williams", type: "Birthday", date: "2001-09-18T00:00:00", notes: "Graduation present" },
{ name: "David Miller", type: "Anniversary", date: "1995-04-30T00:00:00", notes: "10th anniversary" },
{ name: "Emily Davis", type: "Birthday", date: "1989-12-12T00:00:00", notes: "New year gift" }
];
let celebrationSort = { column: null, direction: "asc" };
function getNextCelebrationCountdown(dateStr) {
const today = new Date();
const original = new Date(dateStr);
const thisYear = new Date(today.getFullYear(), original.getMonth(), original.getDate());
const nextYear = new Date(today.getFullYear() + 1, original.getMonth(), original.getDate());
const eventDate = thisYear >= today ? thisYear : nextYear;
const daysUntil = Math.ceil((eventDate - today) / (1000 * 60 * 60 * 24));
let tag = "days-green";
if (daysUntil <= 3) tag = "days-red";
else if (daysUntil <= 7) tag = "days-orange";
return { daysUntil, tag, displayDate: eventDate.toLocaleDateString("en-CA") };
}
function toggleCelebrationSort(key) {
if (celebrationSort.column === key) {
celebrationSort.direction = celebrationSort.direction === "asc" ? "desc" : "asc";
} else {
celebrationSort.column = key;
celebrationSort.direction = "asc";
}
renderCelebrationTable();
}
function renderCelebrationTable() {
let data = [...celebrations];
if (celebrationSort.column) {
data.sort((a, b) => {
let x = a[celebrationSort.column];
let y = b[celebrationSort.column];
if (celebrationSort.column === "days") {
x = getNextCelebrationCountdown(a.date).daysUntil;
y = getNextCelebrationCountdown(b.date).daysUntil;
return celebrationSort.direction === "asc" ? x - y : y - x;
}
return celebrationSort.direction === "asc"
? x.localeCompare(y)
: y.localeCompare(x);
});
}
let html = `<table class="payments-table"><thead><tr>
<th class="sortable ${celebrationSort.column === "name" ? celebrationSort.direction : ""}" onclick="toggleCelebrationSort('name')">Name</th>
<th>Type</th>
<th>Date</th>
<th class="sortable ${celebrationSort.column === "days" ? celebrationSort.direction : ""}" onclick="toggleCelebrationSort('days')">Days Until</th>
<th>Notes</th></tr></thead><tbody>`;
data.forEach(({ name, type, date, notes }) => {
const { daysUntil, tag, displayDate } = getNextCelebrationCountdown(date);
html += `<tr>
<td>${name}</td>
<td>${type}</td>
<td>${displayDate}</td>
<td><span class="days-remaining-tag ${tag} full-cell">${daysUntil} Days</span></td>
<td class="notes-info">${notes}</td>
</tr>`;
});
html += "</tbody></table>";
document.getElementById("celebration-table-wrapper").innerHTML = html;
}
</script>
</body>
</html>
I’ve double-checked the code before publishing to make sure that it is safe. But as a beginner developer, it’s important to verify code before running it locally or in your browser by pasting it into your favorite AI tool.
This blog post provides supplemental information about how the celebrations web app works.