Phase 1: Staff Client & OPAC Pop-ups (On-Screen Greetings)
This step uses simple JavaScript and CSS to read a member's birth date right from the screen. It shows a beautiful birthday message instantly without slowing down your library server. Both setups use browser storage logic so the message only appears once per day for each member.
1. The Staff Client Setup
When a member comes to the desk, a bright animated box pops up on the screen to tell your staff to wish them a happy birthday live.
📄 Where to paste the CSS:
Go to Koha Administration ➔ System Preferences ➔ Search for IntranetUserCSS and paste this style code at the bottom:
/* --- PATRON BIRTHDAY SPLASH SCREEN --- */
/* The dark background overlay */
#koha-bday-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
z-index: 9998;
display: none;
backdrop-filter: blur(2px);
}
/* The birthday card box */
#koha-bday-splash-box {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
background: #ffffff;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0,0,0,0.3);
z-index: 9999;
text-align: center;
border-top: 8px solid #E6A23C;
display: none;
flex-direction: column;
align-items: center;
gap: 15px;
}
/* The pop-in animation */
.koha-splash-animate {
animation: kohaBdayPopIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
@keyframes kohaBdayPopIn {
0% { transform: translate(-50%, -50%) scale(0); opacity: 0; }
100% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
}
/* Text and Button styling */
.koha-bday-title {
font-size: 28px;
font-weight: bold;
color: #333;
margin: 0;
}
.koha-bday-subtitle {
font-size: 16px;
color: #666;
}
#koha-close-bday-splash {
background: #E6A23C;
color: white;
border: none;
padding: 10px 24px;
font-size: 16px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s;
margin-top: 10px;
}
#koha-close-bday-splash:hover {
background: #cf8e2e;
}
Where to paste the JavaScript:
Go to Koha Administration ➔ System Preferences ➔ Search for IntranetUserJS and paste this logic code at the bottom:
/* --- PATRON BIRTHDAY SPLASH SCREEN (SHOW ONCE PER DAY) --- */
$(document).ready(function() {
if ($('.patrondateofbirth').length > 0) {
// 1. Get the patron's Borrowernumber to use as a unique ID
var urlParams = new URLSearchParams(window.location.search);
var borrowerNum = urlParams.get('borrowernumber');
// Fallback: Try to get it from the page text if it is not in the URL
if (!borrowerNum) {
var textMatch = $('.patronborrowernumber').text().match(/\d+/);
if (textMatch) borrowerNum = textMatch[0];
}
var dobText = $('.patrondateofbirth').text();
var dateMatch = dobText.match(/(\d{2,4})[\.\/\-](\d{2})[\.\/\-](\d{2,4})/);
if (dateMatch && borrowerNum) {
var today = new Date();
var currentDay = today.getDate();
var currentMonth = today.getMonth() + 1;
var currentYear = today.getFullYear();
// 2. Create a unique memory key for today
var storageKey = 'bday_ack_' + borrowerNum + '_' + currentYear + '-' + currentMonth + '-' + currentDay;
// 3. STOP: If the browser remembers showing this today, do not show it again!
if (localStorage.getItem(storageKey)) {
return;
}
var matchDay, matchMonth;
var isBirthday = false;
if (dateMatch[1].length === 4) {
matchMonth = parseInt(dateMatch[2]);
matchDay = parseInt(dateMatch[3]);
} else if (dateMatch[3].length === 4) {
var val1 = parseInt(dateMatch[1]);
var val2 = parseInt(dateMatch[2]);
if ((val1 === currentDay && val2 === currentMonth) || (val1 === currentMonth && val2 === currentDay)) {
isBirthday = true;
}
}
if ((matchDay === currentDay && matchMonth === currentMonth) || isBirthday) {
triggerBirthdaySplash(storageKey);
}
}
}
function triggerBirthdaySplash(storageKey) {
if ($('#koha-bday-overlay').length) return;
var overlay = $('<div id="koha-bday-overlay"></div>');
var splashBox = $(
'<div id="koha-bday-splash-box" role="dialog" aria-modal="true">' +
'<p class="koha-bday-title">🎉 Happy Birthday! 🎉</p>' +
'<p class="koha-bday-subtitle" style="margin-bottom: 20px;">Today is this patron\'s birthday!</p>' +
'<button id="koha-close-bday-splash">Close</button>' +
'</div>'
);
$('body').append(overlay).append(splashBox);
$('#koha-bday-overlay').fadeIn(200);
$('#koha-bday-splash-box').css('display', 'flex').addClass('koha-splash-animate');
// 4. When they click Close, save it to the browser memory
$('#koha-close-bday-splash, #koha-bday-overlay').click(function(e) {
if (e.target !== this) return;
localStorage.setItem(storageKey, 'true');
$('#koha-bday-overlay').fadeOut(200);
$('#koha-bday-splash-box').removeClass('koha-splash-animate').fadeOut(200, function() {
$('#koha-bday-overlay').remove();
$(this).remove();
});
});
}
});
2. The OPAC Setup
The exact moment a member logs into their online library account on their birthday, they see a clean greeting box right in the center of the dashboard display.
📄 Where to paste the CSS:
Go to Koha Administration ➔ System Preferences ➔ Search for OpacUserCSS and paste this style code at the bottom:
/* --- OPAC PATRON BIRTHDAY SPLASH SCREEN --- */
#koha-opac-bday-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 99998; display: none; backdrop-filter: blur(3px); }
#koha-opac-bday-box { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0); background: #ffffff; padding: 40px; border-radius: 15px; box-shadow: 0 15px 30px rgba(0,0,0,0.4); z-index: 99999; text-align: center; border-top: 8px solid #005b9f; display: none; flex-direction: column; align-items: center; min-width: 300px; }
.koha-opac-splash-animate { animation: kohaOpacBdayPopIn 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards; }
@keyframes kohaOpacBdayPopIn { 0% { transform: translate(-50%, -50%) scale(0); opacity: 0; } 100% { transform: translate(-50%, -50%) scale(1); opacity: 1; } }
.koha-opac-bday-title { font-size: 28px; font-weight: bold; color: #333; margin: 0 0 10px 0; }
.koha-opac-bday-subtitle { font-size: 16px; color: #555; margin: 0 0 25px 0; }
#koha-close-opac-bday { background: #005b9f; color: white; border: none; padding: 12px 30px; font-size: 16px; border-radius: 8px; cursor: pointer; transition: background 0.3s; }
#koha-close-opac-bday:hover { background: #00437a; }
Where to paste the JavaScript:
Go to Koha Administration ➔ System Preferences ➔ Search for opacuserjs and paste this logic code at the bottom:
/* --- OPAC BIRTHDAY SPLASH SCREEN (BACKGROUND FETCH METHOD) --- */
$(document).ready(function() {
// 1. Only run this when the patron is on their main account dashboard
if (window.location.href.indexOf("opac-user.pl") > -1) {
// 2. Fetch the personal details page using relative URLs for environment compatibility
$.get('/cgi-bin/koha/opac-memberentry.pl?op=edit', function(data) {
// 3. Extract the DOB from the hidden page code
var dob = $(data).find('#borrower_dateofbirth').val();
// Extract Borrowernumber for local storage tracking, fallback to 'opac_patron'
var borrowerNum = $(data).find('input[name="borrowernumber"]').val() || "opac_patron";
if (dob) {
var dateMatch = dob.match(/(\d{2,4})[\.\/\-](\d{2})[\.\/\-](\d{2,4})/);
if (dateMatch) {
var today = new Date();
var currentDay = today.getDate();
var currentMonth = today.getMonth() + 1;
var currentYear = today.getFullYear();
var storageKey = 'opac_bday_ack_' + borrowerNum + '_' + currentYear + '-' + currentMonth + '-' + currentDay;
// Stop if they already saw the popup today
if (localStorage.getItem(storageKey)) {
return;
}
var matchDay, matchMonth;
var isBirthday = false;
// Figure out date format
if (dateMatch[1].length === 4) {
matchMonth = parseInt(dateMatch[2]);
matchDay = parseInt(dateMatch[3]);
} else if (dateMatch[3].length === 4) {
var val1 = parseInt(dateMatch[1]);
var val2 = parseInt(dateMatch[2]);
if ((val1 === currentDay && val2 === currentMonth) || (val1 === currentMonth && val2 === currentDay)) {
isBirthday = true;
}
}
// Trigger the popup if it's a match!
if ((matchDay === currentDay && matchMonth === currentMonth) || isBirthday) {
triggerOpacBirthdaySplash(storageKey);
}
}
}
});
}
// 4. Build and animate the popup with dynamic library profile name discovery
function triggerOpacBirthdaySplash(storageKey) {
if ($('#koha-opac-bday-overlay').length) return;
// Attempt to detect the logged-in library name dynamically from the active OPAC theme
var libraryName = $(".loggedin-branch-text").text().trim();
if (libraryName.indexOf("Library:") > -1) {
libraryName = libraryName.replace("Library:", "").trim();
}
if (!libraryName) {
libraryName = "our Library";
} else {
libraryName = libraryName + " Library";
}
var overlay = $('<div id="koha-opac-bday-overlay"></div>');
var splashBox = $(
'<div id="koha-opac-bday-box" role="dialog" aria-modal="true">' +
'<h2 class="koha-opac-bday-title">🎉 Happy Birthday! 🎉</h2>' +
'<p class="koha-opac-bday-subtitle">Wishing you a fantastic day from ' + libraryName + '!</p>' +
'<button id="koha-close-opac-bday">Thank You!</button>' +
'</div>'
);
$('body').append(overlay).append(splashBox);
$('#koha-opac-bday-overlay').fadeIn(200);
$('#koha-opac-bday-box').css('display', 'flex').addClass('koha-opac-splash-animate');
$('#koha-close-opac-bday, #koha-opac-bday-overlay').click(function(e) {
if (e.target !== this) return;
localStorage.setItem(storageKey, 'true');
$('#koha-opac-bday-overlay').fadeOut(200);
$('#koha-opac-bday-box').removeClass('koha-opac-splash-animate').fadeOut(200, function() {
$('#koha-opac-bday-overlay').remove();
$(this).remove();
});
});
}
});