Compare commits

..

2 Commits

Author SHA1 Message Date
f015a90791 Init commit 2025-02-21 15:20:12 -05:00
2732529030 Init commit 2025-02-21 15:20:02 -05:00
15 changed files with 4891 additions and 1 deletions

32
.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Build outputs
dist/
build/
.next/
# IDE and editor files
.idea/
.vscode/
*.swp
*.swo
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

View File

@ -1,2 +1,95 @@
# freesched
# Quick Start
`npm install`
`node server.js`
Browse to http://localhost:3000/admin for admin interface.
Create a UID and follow link in admin interface for frontend.
# Introducing FreeSched: A Simpler Alternative to Cal.com
Youve asked for a much simpler, easy-to-use version of Cal.com, named "FreeSched," that retains the clean and intuitive UI/UX of Cal.com and mirrors the simplicity of Calendly.com. Below, Ill outline what FreeSched is, its core features, how it works, and why its a streamlined alternative to Cal.com—all while keeping the design aesthetic you love.
What is FreeSched?
FreeSched is a lightweight, open-source scheduling tool designed to make booking meetings as simple as possible. Inspired by Cal.com (an open-source scheduling infrastructure) and Calendly.com (a popular scheduling platform), FreeSched strips away complexity to focus on the essentials. Its perfect for individuals—like freelancers, consultants, or anyone needing quick one-on-one scheduling—while maintaining a modern, minimalistic interface similar to Cal.com and Calendly.
Key Features of FreeSched
FreeSched focuses on simplicity without sacrificing usability. Heres what it offers:
Single Event Type
Users set one type of event with a fixed duration (e.g., 30 minutes).
No need to juggle multiple event types—just one straightforward option.
Basic Availability Settings
Set your available days and hours using a simple weekly grid (e.g., Monday to Friday, 9 AM to 5 PM).
Availability is divided into 30-minute slots for easy selection.
Google Calendar Integration
Connects to your Google Calendar to check existing events and prevent double-booking.
Only shows time slots when youre truly free.
Simple Booking Page
A clean, public page (e.g., freesched.example.com/yourname) where others can pick a date and time.
Invitees provide their name and email to confirm the booking.
Email Notifications
Sends confirmation emails to you and the invitee with all the details.
Keeps everyone in the loop without extra effort.
Time Zone Support
Automatically adjusts available times to the invitees time zone.
No more confusion for international scheduling.
How FreeSched Works
FreeSched is designed to be intuitive for both the user (you) and the invitee (the person booking with you). Heres the process:
For You (The User):
Sign Up and Set Up
Log in with Google (which also links your Google Calendar).
Set your availability (e.g., weekdays from 9 AM to 5 PM) and event duration (e.g., 30 minutes).
Share Your Booking Link
Get a unique URL (e.g., freesched.example.com/yourname) to share via email, social media, or anywhere.
Manage Bookings
View upcoming bookings in a simple dashboard.
Cancel bookings if needed, with automatic notifications sent to both parties.
For Invitees (The Booker):
Visit Your Booking Page
They open your link and see a calendar with your available dates.
Pick a Time
They select a date and choose from your available 30-minute slots, adjusted to their time zone.
Confirm the Booking
They enter their name and email, hit confirm, and receive an email with the details.
Once booked, the event is added to your Google Calendar, and both of you get a confirmation email. Thats it—no fuss, no extra steps.
UI/UX: Inspired by Cal.com and Calendly
FreeSched keeps the polished, user-friendly design of Cal.com and Calendly:
Clean Layout: A minimalistic interface with plenty of whitespace, clear fonts, and a simple color scheme.
Intuitive Navigation: Easy-to-use calendar for picking dates and a straightforward list of time slots.
Responsive Design: Works seamlessly on desktops, tablets, and phones.
Booking Flow: Select a date → pick a time → enter details → confirm—all in a few clicks.
Think of Calendlys booking page or Cal.coms sleek dashboard, but pared down to the essentials. The focus is on clarity and ease, avoiding overwhelming options or cluttered screens.
How Its Simpler Than Cal.com
Cal.com is a powerful scheduling infrastructure with tons of features, but that can make it overkill for simple needs. FreeSched simplifies things:
Feature Cal.com FreeSched
Event Types Multiple (15-min, 30-min, etc.) One fixed duration (e.g., 30 min)
Availability Detailed rules and buffers Basic weekly grid
Integrations Multiple calendars, payments, etc. Google Calendar only
Team Support Yes (shared schedules, etc.) Individual use only
Customization Highly customizable Simple, uniform design
FreeSched cuts out the extras to deliver a tool thats fast to set up and easy to use, while still looking and feeling like its more complex cousins.
Technical Overview (For the Curious)
FreeSched is built with modern, open-source tools:
Frontend: Next.js (a React framework for fast, server-rendered pages).
Backend: Node.js with Express.js for handling bookings and logic.
Database: PostgreSQL with Prisma to store user data and bookings.
Authentication: NextAuth.js with Google OAuth for login and calendar access.
Emails: SendGrid or Nodemailer for notifications.
This keeps FreeSched lightweight, secure, and easy to maintain, with a tech stack similar to Cal.com but scaled back for simplicity.
Why Use FreeSched?
FreeSched is for anyone who wants a free, no-nonsense scheduling tool:
Perfect For: Freelancers, tutors, consultants, or anyone needing simple one-on-one bookings.
Benefits: Quick setup, no learning curve, and a professional look inspired by Cal.com and Calendly.
Open-Source: Like Cal.com, its free to use and customizable if youre a developer.
Its scheduling made easy—just share a link and let others book you, all wrapped in a familiar, user-friendly package.

BIN
db/freesched.db Normal file

Binary file not shown.

19
db/init.js Normal file
View File

@ -0,0 +1,19 @@
// db/init.js
const sqlite3 = require('sqlite3').verbose();
const db = new sqlite3.Database('db/freesched.db');
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS availability (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT NOT NULL UNIQUE,
times TEXT NOT NULL
)`);
// Example initial data for April 2021 (matching screenshot)
db.run(`INSERT OR IGNORE INTO availability (date, times) VALUES (?, ?)`,
['2025-02-21', '9:30am,10:00am,10:30am,11:00am,11:30am,2:00pm']);
db.run(`INSERT OR IGNORE INTO availability (date, times) VALUES (?, ?)`,
['2025-02-22', '10:00am,11:00am,2:00pm']);
});
db.close();

38
favicon.ico Normal file
View File

@ -0,0 +1,38 @@
<svg width="64" height="64" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<!-- Martian background (reddish-orange) -->
<rect width="64" height="64" fill="#B22222" />
<!-- Calendar grid (white, tilted 15 degrees) -->
<g transform="rotate(15 32 32)">
<rect x="20" y="15" width="24" height="24" fill="none" stroke="#FFFFFF" stroke-width="2" />
<!-- Horizontal lines -->
<line x1="20" y1="21" x2="44" y2="21" stroke="#FFFFFF" stroke-width="1" />
<line x1="20" y1="27" x2="44" y2="27" stroke="#FFFFFF" stroke-width="1" />
<line x1="20" y1="33" x2="44" y2="33" stroke="#FFFFFF" stroke-width="1" />
<line x1="20" y1="39" x2="44" y2="39" stroke="#FFFFFF" stroke-width="1" />
<!-- Vertical lines -->
<line x1="26" y1="15" x2="26" y2="39" stroke="#FFFFFF" stroke-width="1" />
<line x1="32" y1="15" x2="32" y2="39" stroke="#FFFFFF" stroke-width="1" />
<line x1="38" y1="15" x2="38" y2="39" stroke="#FFFFFF" stroke-width="1" />
<!-- Date "21" in the center square -->
<text x="32" y="34" fill="#000000" font-family="Arial" font-size="12" font-weight="bold" text-anchor="middle" dominant-baseline="middle">21</text>
</g>
<!-- Mars silhouette (red circle with subtle craters) -->
<circle cx="50" cy="14" r="7" fill="#FF4500" />
<circle cx="48" cy="12" r="1" fill="#8B0000" /> <!-- Crater 1 -->
<circle cx="50" cy="15" r="1" fill="#8B0000" /> <!-- Crater 2 -->
<!-- FreeSched branding ("FS" in white, bottom-left) -->
<text x="8" y="58" fill="#FFFFFF" font-family="Roboto" font-size="8" font-weight="bold">FS</text>
<!-- Optional subtle glow around calendar for visibility -->
<filter id="glow">
<feGaussianBlur stdDeviation="1" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<rect x="20" y="15" width="24" height="24" fill="none" stroke="#FFFFFF" stroke-width="2" filter="url(#glow)" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

2322
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "freesched-app",
"version": "1.0.0",
"description": "FreeSched Meeting Scheduler",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"sqlite3": "^5.1.6"
},
"keywords": [],
"author": "",
"license": "ISC"
}

826
public/admin.js Normal file
View File

@ -0,0 +1,826 @@
// public/admin.js (for admin.html)
// Global variables
let currentAdminDate = new Date();
let currentAdminMonth = currentAdminDate.getMonth();
let currentAdminYear = currentAdminDate.getFullYear();
let selectedAdminDate = null;
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
// Generate all possible time slots in chronological order (8am to 5pm)
const timeSlotsData = [
'8:00am', '9:00am', '10:00am', '11:00am', '12:00pm',
'1:00pm', '2:00pm', '3:00pm', '4:00pm', '5:00pm'
];
// Helper function to convert time string to minutes since midnight
function timeToMinutes(timeStr) {
const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i);
if (!match) return 0;
let hours = parseInt(match[1]);
const minutes = parseInt(match[2] || '0');
const meridian = match[3].toLowerCase();
if (meridian === 'pm' && hours !== 12) {
hours += 12;
} else if (meridian === 'am' && hours === 12) {
hours = 0;
}
return hours * 60 + minutes;
}
// Helper function to ensure consistent time format
function formatTimeSlot(hour) {
const meridian = hour >= 12 ? 'pm' : 'am';
const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);
return `${displayHour}:00${meridian}`;
}
// Render the admin calendar with available dates highlighted
async function renderAdminCalendar(month, year) {
const adminCalendarDates = document.getElementById('adminCalendarDates');
const adminMonthYear = document.getElementById('adminMonthYear');
if (!adminCalendarDates || !adminMonthYear) {
console.error('Required calendar elements not found');
return;
}
// Clear existing calendar
adminCalendarDates.innerHTML = '';
adminMonthYear.textContent = `${months[month]} ${year}`;
// Get current UID and availability
const selectedUid = document.getElementById('uidSelect').value;
if (!selectedUid) {
// If no UID selected, just show empty calendar
return;
}
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const today = new Date();
const availability = await fetchAvailability();
// Start of today (midnight)
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
// Create blank cells for days before the first day of the month
for (let i = 0; i < firstDay; i++) {
const blank = document.createElement('div');
blank.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center', 'text-muted');
adminCalendarDates.appendChild(blank);
}
// Clear any existing selected date
const existingSelected = adminCalendarDates.querySelector('.selected');
if (existingSelected) {
existingSelected.classList.remove('selected');
}
// Populate the calendar with days
let todayElement = null;
for (let i = 1; i <= daysInMonth; i++) {
const day = document.createElement('div');
day.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center');
day.textContent = i;
const date = new Date(year, month, i);
const dateStr = date.toISOString().split('T')[0];
// Check if date is in the past
const isPastDate = date < startOfToday;
if (date.toDateString() === today.toDateString()) {
day.classList.add('current');
todayElement = day;
}
if (availability[dateStr]) {
day.classList.add('available');
}
if (isPastDate) {
day.classList.add('disabled');
} else {
day.addEventListener('click', () => selectAdminDate(date, firstDay));
}
adminCalendarDates.appendChild(day);
}
// If a date was previously selected, re-select it
if (selectedAdminDate &&
selectedAdminDate.getMonth() === month &&
selectedAdminDate.getFullYear() === year) {
selectAdminDate(selectedAdminDate, firstDay);
}
}
// Handle admin date selection
async function selectAdminDate(date, firstDay) {
selectedAdminDate = date;
const calendarDates = document.getElementById('adminCalendarDates');
if (!calendarDates) return;
// Remove selected class from all dates
const allDates = calendarDates.querySelectorAll('.date-item');
allDates.forEach(item => item.classList.remove('selected'));
// Add selected class to the clicked date
const selectedDay = Array.from(allDates).find(item => {
if (!item.textContent) return false;
const dayText = item.textContent;
const itemDate = new Date(currentAdminYear, currentAdminMonth, parseInt(dayText));
return itemDate.toDateString() === date.toDateString();
});
if (selectedDay) selectedDay.classList.add('selected');
const selectedDateDisplay = document.getElementById('selectedDateDisplay');
if (selectedDateDisplay) {
selectedDateDisplay.textContent = `Selected Date: ${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}`;
}
// Update time slots for the selected date
await updateTimeSlots(date);
}
// Update time slots based on selected date
async function updateTimeSlots(date) {
const availability = await fetchAvailability();
const dateStr = date.toISOString().split('T')[0];
let availableTimes = [];
// Handle case where availability might be undefined or null
if (availability && availability[dateStr]) {
if (typeof availability[dateStr] === 'string') {
availableTimes = availability[dateStr].split(',').map(t => t.trim());
} else if (Array.isArray(availability[dateStr])) {
availableTimes = availability[dateStr].map(t => t.trim());
}
}
// Sort available times chronologically
availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b));
const timeSlots = document.getElementById('timeSlots');
if (!timeSlots) return;
timeSlots.innerHTML = '';
// Get current time in minutes for comparison
const now = new Date();
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const isToday = date.toDateString() === now.toDateString();
// Create a map of all time slots with their status
const allSlots = new Map();
timeSlotsData.forEach(time => {
const timeInMinutes = timeToMinutes(time);
// Skip times that are in the past on the current day
if (isToday && timeInMinutes <= currentMinutes) {
return;
}
allSlots.set(time, availableTimes.includes(time));
});
// Sort all slots chronologically
const sortedSlots = Array.from(allSlots.entries())
.sort((a, b) => timeToMinutes(a[0]) - timeToMinutes(b[0]));
// Display all slots in chronological order
sortedSlots.forEach(([time, isAvailable]) => {
const button = document.createElement('button');
button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2', 'py-2');
button.textContent = time;
if (isAvailable) {
button.classList.add('btn-primary', 'available');
button.addEventListener('click', () => {
button.classList.remove('btn-primary', 'available');
button.classList.add('btn-outline-secondary');
removeTime(dateStr, time);
});
} else {
button.classList.add('btn-outline-secondary');
button.addEventListener('click', () => {
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-primary', 'available');
addSingleTime(dateStr, time);
});
}
timeSlots.appendChild(button);
});
}
// Fetch available dates from the API
async function fetchAvailability() {
try {
const selectedUid = document.getElementById('uidSelect').value;
if (!selectedUid) return {};
const response = await fetch(`/api/availability/${selectedUid}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const availability = await response.json();
return availability;
} catch (error) {
console.error('Error fetching availability:', error);
return {};
}
}
// Helper function to update calendar UI after availability changes
async function updateAvailabilityUI() {
const availability = await fetchAvailability();
if (selectedAdminDate) {
await updateTimeSlots(selectedAdminDate);
}
const calendarDates = document.getElementById('adminCalendarDates');
if (calendarDates) {
const allDateElements = calendarDates.querySelectorAll('.date-item');
allDateElements.forEach(dateElement => {
if (!dateElement.textContent) return; // Skip empty cells
const day = parseInt(dateElement.textContent);
const date = new Date(currentAdminYear, currentAdminMonth, day);
const dateStr = date.toISOString().split('T')[0];
if (availability[dateStr]) {
dateElement.classList.add('available');
} else {
dateElement.classList.remove('available');
}
});
}
}
// UID management functions
async function loadUids() {
try {
const response = await fetch('/api/uids');
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch UIDs');
}
const uids = await response.json();
const select = document.getElementById('uidSelect');
select.innerHTML = '<option value="">Select a Schedule UID...</option>';
uids.forEach(uidObj => {
const option = document.createElement('option');
option.value = uidObj.uid;
option.textContent = uidObj.uid;
select.appendChild(option);
});
} catch (error) {
console.error('Error loading UIDs:', error);
}
}
async function createUid(uid) {
try {
const response = await fetch('/api/uids', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ uid })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to create UID');
}
// Reload UIDs and select the new one
await loadUids();
// Select the new UID and trigger change event
const select = document.getElementById('uidSelect');
select.value = uid;
select.dispatchEvent(new Event('change'));
return true;
} catch (error) {
console.error('Error creating UID:', error);
return false;
}
}
async function deleteUid(uid) {
try {
const response = await fetch(`/api/uids/${uid}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete UID');
await loadUids();
return true;
} catch (error) {
console.error('Error deleting UID:', error);
return false;
}
}
// Add a single time for the selected date
async function addSingleTime(dateStr, time) {
const selectedUid = document.getElementById('uidSelect').value;
if (!selectedUid) {
alert('Please select a UID first');
return;
}
try {
const availability = await fetchAvailability();
let times = [];
if (availability && availability[dateStr]) {
if (typeof availability[dateStr] === 'string') {
times = availability[dateStr].split(',').map(t => t.trim());
} else if (Array.isArray(availability[dateStr])) {
times = [...availability[dateStr]];
}
}
times.push(time);
const response = await fetch(`/api/availability/${selectedUid}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ date: dateStr, times: [...new Set(times)] }) // Remove duplicates
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log(result.message);
await updateAvailabilityUI();
} catch (error) {
console.error('Error adding time slot:', error);
alert('Failed to add time slot. Please try again.');
}
}
// Remove a single time for the selected date
async function removeTime(dateStr, time) {
const selectedUid = document.getElementById('uidSelect').value;
if (!selectedUid) {
alert('Please select a UID first');
return;
}
try {
const availability = await fetchAvailability();
let times = [];
if (availability && availability[dateStr]) {
if (typeof availability[dateStr] === 'string') {
times = availability[dateStr].split(',').filter(t => t.trim() !== time);
} else if (Array.isArray(availability[dateStr])) {
times = availability[dateStr].filter(t => t.trim() !== time);
}
}
const response = await fetch(`/api/availability/${selectedUid}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ date: dateStr, times })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
await updateAvailabilityUI();
} catch (error) {
console.error('Error removing time slot:', error);
alert('Failed to remove time slot. Please try again.');
}
}
// Helper function to convert time string to minutes since midnight
function timeToMinutes(timeStr) {
const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i);
if (!match) return 0;
let [_, hours, minutes, period] = match;
hours = parseInt(hours);
minutes = minutes ? parseInt(minutes) : 0;
period = period.toLowerCase();
if (period === 'pm' && hours !== 12) hours += 12;
if (period === 'am' && hours === 12) hours = 0;
return hours * 60 + minutes;
}
// Helper function to ensure consistent time format
function formatTimeSlot(hour) {
const period = hour < 12 ? 'am' : 'pm';
const displayHour = hour === 0 ? 12 : (hour > 12 ? hour - 12 : hour);
return `${displayHour}:00${period}`;
}
// Handle admin date selection
async function selectAdminDate(date, firstDay) {
// Check if the date is in the past
const today = new Date();
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
if (date < startOfToday) {
console.warn('Attempted to select a past date');
return;
}
// Update selected date and time slots
selectedAdminDate = date;
await updateTimeSlots(date);
const dateItems = document.querySelectorAll('#adminCalendarDates .date-item');
dateItems.forEach(item => item.classList.remove('selected')); // Ensure only one date is selected
// Find the exact date item for the clicked date
const selectedDay = Array.from(dateItems).find(item => {
const dayText = item.textContent;
const itemDate = new Date(currentAdminYear, currentAdminMonth, parseInt(dayText));
return itemDate.toDateString() === date.toDateString();
});
if (selectedDay) selectedDay.classList.add('selected');
const selectedDateDisplay = document.getElementById('selectedDateDisplay');
if (selectedDateDisplay) {
selectedDateDisplay.textContent = `Selected Date: ${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}`;
}
}
// Update time slots based on selected date
async function updateTimeSlots(date) {
const availability = await fetchAvailability();
const dateStr = date.toISOString().split('T')[0];
let availableTimes = [];
// Handle case where availability might be undefined or null
if (availability && availability[dateStr]) {
if (typeof availability[dateStr] === 'string') {
availableTimes = availability[dateStr].split(',').map(t => t.trim());
} else if (Array.isArray(availability[dateStr])) {
availableTimes = availability[dateStr].map(t => t.trim());
}
}
// Sort available times chronologically
availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b));
const timeSlots = document.getElementById('timeSlots');
if (!timeSlots) return;
timeSlots.innerHTML = '';
// Get current time in minutes for comparison
const now = new Date();
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const isToday = date.toDateString() === now.toDateString();
// Create a map of all time slots with their status
const allSlots = new Map();
timeSlotsData.forEach(time => {
const timeInMinutes = timeToMinutes(time);
// Skip times that are in the past on the current day
if (isToday && timeInMinutes <= currentMinutes) {
return;
}
allSlots.set(time, availableTimes.includes(time));
});
// Sort all slots chronologically
const sortedSlots = Array.from(allSlots.entries())
.sort((a, b) => timeToMinutes(a[0]) - timeToMinutes(b[0]));
// Display all slots in chronological order
sortedSlots.forEach(([time, isAvailable]) => {
const button = document.createElement('button');
button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2', 'py-2');
button.textContent = time;
if (isAvailable) {
button.classList.add('btn-primary', 'available');
button.addEventListener('click', () => {
button.classList.remove('btn-primary', 'available');
button.classList.add('btn-outline-secondary');
removeTime(dateStr, time);
});
} else {
button.classList.add('btn-outline-secondary');
button.addEventListener('click', () => {
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-primary', 'available');
addSingleTime(dateStr, time);
});
}
timeSlots.appendChild(button);
});
}
// Fetch available dates from the API
async function fetchAvailability() {
try {
const selectedUid = document.getElementById('uidSelect').value;
if (!selectedUid) return {};
const response = await fetch(`/api/availability/${selectedUid}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const availability = await response.json();
return availability;
} catch (error) {
console.error('Error fetching availability:', error);
return {};
}
}
// Helper function to update calendar UI after availability changes
async function updateAvailabilityUI() {
const availability = await fetchAvailability();
if (selectedAdminDate) {
await updateTimeSlots(selectedAdminDate);
}
const calendarDates = document.getElementById('adminCalendarDates');
if (calendarDates) {
const allDateElements = calendarDates.querySelectorAll('.date-item');
allDateElements.forEach(dateElement => {
if (!dateElement.textContent) return; // Skip empty cells
const day = parseInt(dateElement.textContent);
const date = new Date(currentAdminYear, currentAdminMonth, day);
const dateStr = date.toISOString().split('T')[0];
if (availability[dateStr]) {
dateElement.classList.add('available');
} else {
dateElement.classList.remove('available');
}
});
}
}
document.addEventListener('DOMContentLoaded', async () => {
// Load existing UIDs
await loadUids();
// Handle UID selection
const uidSelect = document.getElementById('uidSelect');
const deleteUidBtn = document.getElementById('deleteUid');
const uidUrl = document.getElementById('uidUrl');
uidSelect.addEventListener('change', async () => {
const selectedUid = uidSelect.value;
deleteUidBtn.disabled = !selectedUid;
flushDatabaseBtn.disabled = !selectedUid;
// Clear current selections
selectedAdminDate = null;
const selectedDateElem = document.getElementById('selectedDateDisplay');
const timeSlotsElem = document.getElementById('timeSlots');
const calendarDatesElem = document.getElementById('adminCalendarDates');
const adminMonthYear = document.getElementById('adminMonthYear');
// Clear all UI elements
if (selectedDateElem) selectedDateElem.textContent = '';
if (timeSlotsElem) timeSlotsElem.innerHTML = '';
if (calendarDatesElem) calendarDatesElem.innerHTML = '';
if (adminMonthYear) adminMonthYear.textContent = '';
if (selectedUid) {
const url = `${window.location.origin}/${selectedUid}`;
uidUrl.textContent = url;
uidUrl.href = url;
// Refresh calendar with new UID's data
await renderAdminCalendar(currentAdminMonth, currentAdminYear);
// Show availability for current date
if (selectedAdminDate) {
await updateTimeSlots(selectedAdminDate);
}
} else {
uidUrl.textContent = 'Select a UID first';
uidUrl.href = '#';
// Add placeholder text
if (selectedDateElem) selectedDateElem.textContent = 'Please select a UID to view the calendar';
await renderAdminCalendar(currentAdminMonth, currentAdminYear);
}
});
// Handle new UID creation
const newUidInput = document.getElementById('newUid');
const createUidBtn = document.getElementById('createUid');
createUidBtn.addEventListener('click', async () => {
const uid = newUidInput.value.trim().toLowerCase();
if (!/^[a-z0-9-]+$/.test(uid)) {
alert('UID can only contain lowercase letters, numbers, and hyphens');
return;
}
if (await createUid(uid)) {
newUidInput.value = '';
} else {
alert('Failed to create UID. Please try again.');
}
});
// Handle UID deletion
deleteUidBtn.addEventListener('click', async () => {
const uid = uidSelect.value;
if (!uid) return;
if (!confirm(`Are you sure you want to delete UID: ${uid}?\nThis will remove all associated availability data.`)) {
return;
}
if (await deleteUid(uid)) {
// Clear selection and trigger change event
uidSelect.value = '';
uidSelect.dispatchEvent(new Event('change'));
} else {
alert('Failed to delete UID. Please try again.');
}
});
// Initialize UI elements
const adminCalendarDates = document.getElementById('adminCalendarDates');
const adminMonthYear = document.getElementById('adminMonthYear');
const adminPrevMonthBtn = document.getElementById('adminPrevMonth');
const adminNextMonthBtn = document.getElementById('adminNextMonth');
const selectedDateDisplay = document.getElementById('selectedDateDisplay');
const timeSlots = document.getElementById('timeSlots');
const flushDatabaseBtn = document.getElementById('flushDatabase');
const resetDatabaseBtn = document.getElementById('resetDatabase');
// Handle database reset
resetDatabaseBtn.addEventListener('click', async () => {
if (!confirm('⚠️ WARNING: This will permanently delete ALL UIDs and their associated availability data. This action cannot be undone.\n\nAre you absolutely sure you want to proceed?')) {
return;
}
// Double confirm for dangerous action
if (!confirm('Please confirm one more time that you want to reset the entire database. All data will be lost.')) {
return;
}
try {
const response = await fetch('/api/reset', {
method: 'POST'
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to reset database');
}
// Reload the page to reset all state
window.location.reload();
} catch (error) {
console.error('Error resetting database:', error);
alert('Failed to reset database. Please try again.');
}
});
// Check if a date has availability
async function checkAvailabilityForDate(date) {
const availability = await fetchAvailability();
const dateStr = date.toISOString().split('T')[0];
return !!availability[dateStr];
}
// Flush (delete) all entries from the database
flushDatabaseBtn.addEventListener('click', async () => {
const selectedUid = document.getElementById('uidSelect').value;
if (!selectedUid) {
alert('Please select a UID first');
return;
}
if (!confirm(`Are you sure you want to delete ALL availability entries for UID: ${selectedUid}?`)) {
return;
}
try {
console.log(`Attempting to delete all availability for UID: ${selectedUid}...`);
const response = await fetch(`/api/availability/${selectedUid}/flush`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('Delete availability response:', result);
// Refresh data
await fetchAvailability(); // Refresh availability data
// Reset UI
selectedAdminDate = null; // Clear selected date
selectedDateDisplay.textContent = '';
timeSlots.innerHTML = ''; // Clear time slots
await renderAdminCalendar(currentAdminMonth, currentAdminYear);
} catch (error) {
console.error('Error deleting availability:', error);
alert('Failed to delete availability. Please try again.');
}
});
// Navigate to previous month (admin)
adminPrevMonthBtn.addEventListener('click', () => {
currentAdminMonth--;
if (currentAdminMonth < 0) {
currentAdminMonth = 11;
currentAdminYear--;
}
renderAdminCalendar(currentAdminMonth, currentAdminYear);
});
// Navigate to next month (admin)
adminNextMonthBtn.addEventListener('click', () => {
currentAdminMonth++;
if (currentAdminMonth > 11) {
currentAdminMonth = 0;
currentAdminYear++;
}
renderAdminCalendar(currentAdminMonth, currentAdminYear);
});
// Handle global database flush
const flushGlobalDatabaseBtn = document.getElementById('flushGlobalDatabase');
if (flushGlobalDatabaseBtn) {
flushGlobalDatabaseBtn.addEventListener('click', async () => {
if (!confirm('WARNING: This will delete ALL UIDs and their availability data. This action cannot be undone. Are you sure you want to proceed?')) {
return;
}
try {
const response = await fetch('/api/flush-global', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('Global database flush response:', result);
alert(result.message || 'Database flushed successfully!');
// Reset everything
selectedAdminDate = null;
const selectedDateElem = document.getElementById('selectedDateDisplay');
const timeSlotsElem = document.getElementById('timeSlots');
const calendarDatesElem = document.getElementById('adminCalendarDates');
if (selectedDateElem) selectedDateElem.textContent = '';
if (timeSlotsElem) timeSlotsElem.innerHTML = '';
if (calendarDatesElem) calendarDatesElem.innerHTML = '';
// Reload UIDs
await loadUids();
// Update UID URL display
const uidUrl = document.getElementById('uidUrl');
if (uidUrl) uidUrl.textContent = 'Select a UID first';
// Disable delete button
const deleteUidBtn = document.getElementById('deleteUid');
if (deleteUidBtn) deleteUidBtn.disabled = true;
} catch (error) {
console.error('Error flushing database:', error);
alert('Failed to flush database. Please try again.');
}
});
}
// Initial render with current date
renderAdminCalendar(currentAdminMonth, currentAdminYear);
if (currentAdminDate.getDate() <= new Date(currentAdminYear, currentAdminMonth, 0).getDate()) {
const hasAvailability = await checkAvailabilityForDate(currentAdminDate);
if (hasAvailability) {
selectAdminDate(currentAdminDate, new Date(currentAdminYear, currentAdminMonth, 1).getDay());
}
}
});

BIN
public/avatar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

425
public/public.js Normal file
View File

@ -0,0 +1,425 @@
// public/public.js (for index.html)
// Convert time string (e.g., '9:30am' or '2:00pm') to minutes since midnight
function timeToMinutes(timeStr) {
const match = timeStr.match(/([0-9]{1,2}):([0-9]{2})(am|pm)/i);
if (!match) {
console.error('Invalid time format:', timeStr);
return 0;
}
const [_, hours, minutes, period] = match;
let totalHours = parseInt(hours);
if (period.toLowerCase() === 'pm' && totalHours !== 12) totalHours += 12;
if (period.toLowerCase() === 'am' && totalHours === 12) totalHours = 0;
return totalHours * 60 + parseInt(minutes);
}
// Get UID from URL path
function getUidFromPath() {
const path = window.location.pathname.substring(1); // Remove leading slash
return path || null;
}
// Validate UID format
function isValidUid(uid) {
return /^[a-z0-9-]+$/.test(uid);
}
document.addEventListener('DOMContentLoaded', async () => {
const uid = getUidFromPath();
const mainContent = document.getElementById('mainContent');
const uidError = document.getElementById('uidError');
// Check if we have a valid UID
if (!uid || !isValidUid(uid)) {
mainContent.style.display = 'none';
uidError.style.display = 'block';
return;
}
// Hide error, show main content
uidError.style.display = 'none';
mainContent.style.display = 'block';
const calendarDates = document.getElementById('calendarDates');
const monthYear = document.getElementById('monthYear');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');
const currentTimeElement = document.getElementById('currentTime');
const timeSlotsContainer = document.getElementById('timeSlots');
// Check if navigation buttons exist before adding event listeners
if (!prevMonthBtn || !nextMonthBtn) {
console.error('Navigation buttons (prevMonth or nextMonth) not found in the DOM');
return;
}
let currentDate = new Date();
let currentMonth = currentDate.getMonth();
let currentYear = currentDate.getFullYear();
let selectedDate = null; // Track selected date
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
// Fetch available dates from the API
async function fetchAvailability() {
try {
const response = await fetch(`/api/availability/${uid}`);
if (response.status === 404) {
// UID doesn't exist, show error page
mainContent.style.display = 'none';
uidError.style.display = 'block';
return null;
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const availability = await response.json();
console.log('Fetched availability for UID:', uid, availability);
return availability;
} catch (error) {
console.error('Error fetching availability:', error);
return {};
}
}
// Render the calendar with available dates highlighted and automatically select/display the current date
async function renderCalendar(month, year) {
const availability = await fetchAvailability();
if (availability === null) return; // UID doesn't exist
calendarDates.innerHTML = '';
monthYear.textContent = `${months[month]} ${year}`;
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const today = new Date();
// Create blank cells for days before the first day of the month
for (let i = 0; i < firstDay; i++) {
const blank = document.createElement('div');
blank.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center', 'text-muted');
calendarDates.appendChild(blank);
}
// Populate the calendar with days
for (let i = 1; i <= daysInMonth; i++) {
const day = document.createElement('div');
day.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center');
day.textContent = i;
const date = new Date(year, month, i);
const dateStr = date.toISOString().split('T')[0];
// Start of today (midnight)
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
// Check if date is in the past
const isPastDate = date < startOfToday;
if (date.toDateString() === today.toDateString()) {
day.classList.add('current');
}
if (availability[dateStr]) {
day.classList.add('available');
}
if (isPastDate) {
day.classList.add('disabled');
} else {
day.addEventListener('click', () => selectDate(date, firstDay));
}
calendarDates.appendChild(day);
}
// Auto-select today if we're in the current month/year
if (today.getMonth() === month && today.getFullYear() === year) {
await selectDate(today, firstDay);
// Also update time slots directly since we're on today's date
await updateTimeSlots(today);
}
}
// Handle date selection
async function selectDate(date, firstDay) {
// Check if the date is in the past
const today = new Date();
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
if (date < startOfToday) {
console.warn('Attempted to select a past date');
return;
}
selectedDate = date;
const dateItems = document.querySelectorAll('#calendarDates .date-item');
dateItems.forEach(item => item.classList.remove('selected')); // Ensure only one date is selected
// Find the exact date item for the clicked or auto-selected date
const selectedDay = Array.from(dateItems).find(item => {
const dayText = item.textContent;
const itemDate = new Date(currentYear, currentMonth, parseInt(dayText));
return itemDate.toDateString() === date.toDateString();
});
if (selectedDay) selectedDay.classList.add('selected');
await updateTimeSlots(date); // Update time slots based on selected date, ensuring async completion
}
// Update time slots based on selected date
async function updateTimeSlots(date) {
const availability = await fetchAvailability();
const dateStr = date.toISOString().split('T')[0];
console.log('Looking for date:', dateStr);
console.log('Available dates:', Object.keys(availability));
let times = [];
if (availability[dateStr]) {
console.log('Found times for date:', availability[dateStr]);
// Ensure availability[dateStr] is a string or array before processing
if (typeof availability[dateStr] === 'string') {
times = availability[dateStr].split(',').map(t => t.trim());
} else if (Array.isArray(availability[dateStr])) {
times = availability[dateStr].map(t => t.trim());
} else {
console.warn(`Unexpected data format for date ${dateStr}:`, availability[dateStr]);
}
// Filter out past times if it's today
const today = new Date();
const isToday = date.toDateString() === today.toDateString();
console.log('Time filtering - Today:', today.toDateString());
console.log('Time filtering - Selected:', date.toDateString());
console.log('Is today?', isToday);
if (isToday) {
const currentMinutes = today.getHours() * 60 + today.getMinutes();
console.log('Current minutes:', currentMinutes);
console.log('Times before filtering:', times);
times = times.filter(time => {
const timeMin = timeToMinutes(time);
console.log(`Time ${time} = ${timeMin} minutes`);
return timeMin > currentMinutes;
});
console.log('Times after filtering:', times);
}
// Sort times chronologically
times.sort((a, b) => timeToMinutes(a) - timeToMinutes(b));
}
// Clear existing time slots or message
timeSlotsContainer.innerHTML = '';
if (times.length === 0) {
// Display "No availability on selected date." if no times are available
const noAvailabilityMsg = document.createElement('p');
noAvailabilityMsg.classList.add('text-muted', 'text-center', 'fw-bold', 'fs-5', 'py-3');
noAvailabilityMsg.textContent = 'No availability on selected date.';
timeSlotsContainer.appendChild(noAvailabilityMsg);
} else {
// Populate time slots dynamically if times are available
times.forEach(time => {
const button = document.createElement('button');
button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2');
button.classList.add('btn-outline-primary', 'available');
button.textContent = time;
button.addEventListener('click', () => {
const allTimeSlots = timeSlotsContainer.querySelectorAll('.time-slot');
allTimeSlots.forEach(s => s.classList.remove('selected'));
button.classList.add('selected');
// Format date as MM/DD/YY
const [year, month, day] = dateStr.split('-');
const formattedDate = `${month}/${day}/${year.slice(2)}`;
// Show booking modal
document.getElementById('selectedTime').value = time;
document.getElementById('selectedDate').value = dateStr;
document.getElementById('bookingModalLabel').textContent = `Confirm Appointment for ${formattedDate} at ${time} EST`;
// Clear previous form data
document.getElementById('bookingForm').reset();
// Show modal
const bookingModal = new bootstrap.Modal(document.getElementById('bookingModal'));
bookingModal.show();
});
timeSlotsContainer.appendChild(button);
});
}
}
// Navigate to previous month
prevMonthBtn.addEventListener('click', () => {
currentMonth--;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
renderCalendar(currentMonth, currentYear);
});
// Navigate to next month
nextMonthBtn.addEventListener('click', () => {
currentMonth++;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
renderCalendar(currentMonth, currentYear);
});
// Update current time in Eastern Time (US & Canada)
function updateCurrentTime() {
const options = {
timeZone: 'America/New_York',
hour: '2-digit',
minute: '2-digit',
hour12: true
};
const time = new Date().toLocaleTimeString('en-US', options);
currentTimeElement.textContent = time;
}
// Update time every second
setInterval(updateCurrentTime, 1000);
updateCurrentTime(); // Initial call
// Form validation and submission
const form = document.getElementById('bookingForm');
const submitBtn = document.getElementById('submitBooking');
// Email validation function
function isValidEmail(email) {
const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return re.test(email);
}
// Phone validation function (optional field)
function isValidPhone(phone) {
if (!phone) return true; // Optional field
const re = /^[0-9]{3}-[0-9]{3}-[0-9]{4}$/;
return re.test(phone);
}
// Format phone number as user types
document.getElementById('phone').addEventListener('input', (e) => {
let value = e.target.value.replace(/\D/g, ''); // Remove non-digits
if (value.length >= 3) {
value = value.slice(0, 3) + '-' + value.slice(3);
if (value.length >= 7) {
value = value.slice(0, 7) + '-' + value.slice(7);
}
}
e.target.value = value.slice(0, 12); // Limit to XXX-XXX-XXXX
});
// Validate fields on blur
document.getElementById('name').addEventListener('blur', (e) => {
if (!e.target.value.trim()) {
e.target.classList.add('is-invalid');
} else {
e.target.classList.remove('is-invalid');
}
});
document.getElementById('email').addEventListener('blur', (e) => {
const email = e.target.value.trim();
if (!email || !isValidEmail(email)) {
e.target.classList.add('is-invalid');
} else {
e.target.classList.remove('is-invalid');
}
});
document.getElementById('phone').addEventListener('blur', (e) => {
const phone = e.target.value.trim();
if (phone && !isValidPhone(phone)) {
e.target.classList.add('is-invalid');
} else {
e.target.classList.remove('is-invalid');
}
});
submitBtn.addEventListener('click', async (e) => {
e.preventDefault();
const name = document.getElementById('name').value.trim();
const email = document.getElementById('email').value.trim();
const phone = document.getElementById('phone').value.trim();
const notes = document.getElementById('notes').value.trim();
const selectedTime = document.getElementById('selectedTime').value;
const selectedDate = document.getElementById('selectedDate').value;
// Clear previous validation state
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
// Validate required fields
let hasErrors = false;
if (!name) {
document.getElementById('name').classList.add('is-invalid');
hasErrors = true;
}
if (!email || !isValidEmail(email)) {
document.getElementById('email').classList.add('is-invalid');
hasErrors = true;
}
// Validate phone if provided
if (phone && !isValidPhone(phone)) {
document.getElementById('phone').classList.add('is-invalid');
hasErrors = true;
}
if (hasErrors) {
return;
}
// Prepare booking data
const bookingData = {
name,
email,
phone: phone || null,
notes: notes || null,
date: selectedDate,
time: selectedTime
};
try {
const response = await fetch('/api/book', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(bookingData)
});
if (!response.ok) {
throw new Error('Booking failed');
}
// Close modal and show success message
const modal = bootstrap.Modal.getInstance(document.getElementById('bookingModal'));
modal.hide();
alert('Appointment scheduled successfully!');
// Refresh the calendar
await renderCalendar(currentMonth, currentYear);
} catch (error) {
console.error('Error booking appointment:', error);
alert('Failed to schedule appointment. Please try again.');
}
});
// Initial render and setup
await renderCalendar(currentMonth, currentYear);
// Force an immediate update of time slots for today's date
const today = new Date();
selectedDate = today; // Ensure selectedDate is set
await updateTimeSlots(today);
// Double-check that time slots are updated
if (!timeSlotsContainer.hasChildNodes()) {
const noAvailabilityMsg = document.createElement('p');
noAvailabilityMsg.classList.add('text-muted', 'text-center', 'fw-bold', 'fs-5', 'py-3');
noAvailabilityMsg.textContent = 'No availability on selected date.';
timeSlotsContainer.appendChild(noAvailabilityMsg);
}
});

245
public/script.js Normal file
View File

@ -0,0 +1,245 @@
// public/public.js (for index.html)
document.addEventListener('DOMContentLoaded', async () => {
const calendarDates = document.getElementById('calendarDates');
const monthYear = document.getElementById('monthYear');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');
const currentTimeElement = document.getElementById('currentTime');
const timeSlotsContainer = document.getElementById('timeSlots');
// Check if navigation buttons exist before adding event listeners
if (!prevMonthBtn || !nextMonthBtn) {
console.error('Navigation buttons (prevMonth or nextMonth) not found in the DOM');
return;
}
let currentDate = new Date();
let currentMonth = currentDate.getMonth();
let currentYear = currentDate.getFullYear();
let selectedDate = null; // Track selected date
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
// Fetch available dates from the API
async function fetchAvailability() {
try {
const response = await fetch('/api/availability');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const availability = await response.json();
return availability;
} catch (error) {
console.error('Error fetching availability:', error);
return {};
}
}
// Render the calendar with available dates highlighted and automatically select/display the current date
async function renderCalendar(month, year) {
// Fetch availability first to ensure data is ready
const availability = await fetchAvailability();
calendarDates.innerHTML = '';
monthYear.textContent = `${months[month]} ${year}`;
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const today = new Date();
// Start of today (midnight)
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
// Create blank cells for days before the first day of the month
for (let i = 0; i < firstDay; i++) {
const blank = document.createElement('div');
blank.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center', 'text-muted');
calendarDates.appendChild(blank);
}
// Populate the calendar with days
for (let i = 1; i <= daysInMonth; i++) {
const day = document.createElement('div');
day.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center');
day.textContent = i;
const date = new Date(year, month, i);
const dateStr = date.toISOString().split('T')[0];
// Check if date is in the past
const isPastDate = date < startOfToday;
if (date.toDateString() === today.toDateString()) {
day.classList.add('current');
}
if (availability[dateStr]) {
day.classList.add('available');
}
if (isPastDate) {
day.classList.add('text-muted');
day.style.cursor = 'not-allowed';
} else {
day.addEventListener('click', () => selectDate(date, firstDay));
day.style.cursor = 'pointer';
}
calendarDates.appendChild(day);
}
// Only try to select today if we're in the current month and year
if (today.getMonth() === month && today.getFullYear() === year) {
await selectDate(today, firstDay); // Use await to ensure selection and display complete
}
}
// Handle date selection
async function selectDate(date, firstDay) {
// Check if the date is in the past
const today = new Date();
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
if (date < startOfToday) {
console.warn('Attempted to select a past date');
return;
}
selectedDate = date;
const dateItems = document.querySelectorAll('#calendarDates .date-item');
dateItems.forEach(item => item.classList.remove('selected')); // Ensure only one date is selected
// Find the exact date item for the clicked or auto-selected date
const selectedDay = Array.from(dateItems).find(item => {
const dayText = item.textContent;
const itemDate = new Date(currentYear, currentMonth, parseInt(dayText));
return itemDate.toDateString() === date.toDateString();
});
if (selectedDay) selectedDay.classList.add('selected');
await updateTimeSlots(date); // Update time slots based on selected date, ensuring async completion
}
// Helper function to convert time string to minutes for sorting
function timeToMinutes(timeStr) {
console.log('Converting time:', timeStr);
const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i);
if (!match) {
console.warn('No match for time:', timeStr);
return 0;
}
let [_, hours, minutes, period] = match;
hours = parseInt(hours);
minutes = minutes ? parseInt(minutes) : 0;
period = period.toLowerCase();
if (period === 'pm' && hours !== 12) hours += 12;
if (period === 'am' && hours === 12) hours = 0;
const totalMinutes = hours * 60 + minutes;
console.log(`${timeStr} -> ${totalMinutes} minutes`);
return totalMinutes;
}
// Update time slots based on selected date
async function updateTimeSlots(date) {
const availability = await fetchAvailability();
const dateStr = date.toISOString().split('T')[0];
let times = [];
if (availability[dateStr]) {
// Ensure availability[dateStr] is a string or array before processing
if (typeof availability[dateStr] === 'string') {
times = availability[dateStr].split(',').map(t => t.trim());
} else if (Array.isArray(availability[dateStr])) {
times = availability[dateStr].map(t => t.trim());
} else {
console.warn(`Unexpected data format for date ${dateStr}:`, availability[dateStr]);
}
}
// Clear existing time slots or message
timeSlotsContainer.innerHTML = '';
// Get current time in minutes for comparison
const now = new Date();
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const isToday = date.toDateString() === now.toDateString();
// Filter out past times if it's today
if (isToday) {
times = times.filter(time => timeToMinutes(time) > currentMinutes);
}
// Sort remaining times chronologically
times.sort((a, b) => timeToMinutes(a) - timeToMinutes(b));
if (times.length === 0) {
// Display appropriate message based on context
const noAvailabilityMsg = document.createElement('p');
noAvailabilityMsg.classList.add('text-muted', 'text-center', 'fw-bold', 'fs-5', 'py-3');
let message;
if (isToday) {
if (currentMinutes > timeToMinutes('5:00pm')) {
message = 'No more availability today.';
} else {
message = 'No available time slots for today.';
}
} else {
message = 'No availability on selected date.';
}
noAvailabilityMsg.textContent = message;
timeSlotsContainer.appendChild(noAvailabilityMsg);
} else {
// Populate time slots dynamically
times.forEach(time => {
const button = document.createElement('button');
button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2');
button.classList.add('btn-outline-primary', 'available');
button.textContent = time;
button.addEventListener('click', () => {
if (button.classList.contains('available') && selectedDate) {
const allTimeSlots = timeSlotsContainer.querySelectorAll('.time-slot');
allTimeSlots.forEach(s => s.classList.remove('selected'));
button.classList.add('selected');
}
});
timeSlotsContainer.appendChild(button);
});
}
}
// Navigate to previous month
prevMonthBtn.addEventListener('click', () => {
currentMonth--;
if (currentMonth < 0) {
currentMonth = 11;
currentYear--;
}
renderCalendar(currentMonth, currentYear);
});
// Navigate to next month
nextMonthBtn.addEventListener('click', () => {
currentMonth++;
if (currentMonth > 11) {
currentMonth = 0;
currentYear++;
}
renderCalendar(currentMonth, currentYear);
});
// Update current time in Eastern Time (US & Canada)
function updateCurrentTime() {
const options = {
timeZone: 'America/New_York',
hour: '2-digit',
minute: '2-digit',
hour12: true
};
const time = new Date().toLocaleTimeString('en-US', options);
currentTimeElement.textContent = time;
}
// Update time every second
setInterval(updateCurrentTime, 1000);
updateCurrentTime(); // Initial call
// Initial render (set to current date, automatically selecting and displaying it)
await renderCalendar(currentMonth, currentYear); // Use await to ensure the initial render completes
});

234
public/styles.css Normal file
View File

@ -0,0 +1,234 @@
/* Custom styles to modernize and align with Calendly-inspired design */
:root {
--primary-color: #007bff; /* Blue from Calendly */
--secondary-color: #6c757d; /* Gray for muted elements */
--light-bg: #f8f9fa; /* Light background for modern look */
--shadow-color: rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Arial', sans-serif; /* Matches Calendly font */
}
.container-fluid {
max-width: 1000px;
margin: 20px auto;
background-color: #fff;
border-radius: 6px;
box-shadow: 0 2px 4px var(--shadow-color);
overflow: hidden;
}
.sidebar {
width: 30%;
padding: 20px;
border-right: 1px solid #e1e8ed;
text-align: center;
background-color: #fff;
}
.profile-pic {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 50%;
margin-bottom: 10px;
border: 2px solid #fff;
box-shadow: 0 2px 4px var(--shadow-color);
}
.sidebar h2 {
font-size: 1.25rem;
color: #333;
margin-bottom: 5px;
font-weight: 600;
}
.sidebar h3 {
font-size: 1rem;
color: #666;
margin-bottom: 5px;
}
.duration {
font-size: 0.875rem;
color: #666;
}
.main-content {
width: 70%;
padding: 20px;
}
.calendar-header h1 {
font-size: 1.5rem;
color: #333;
font-weight: 700;
margin-bottom: 10px;
}
.month-nav {
display: flex;
justify-content: space-between;
font-size: 1rem;
color: #333;
padding: 0.5rem 0;
border-bottom: 1px solid #e1e8ed;
}
.month-nav button {
color: var(--secondary-color);
transition: color 0.3s ease;
}
.month-nav button:hover {
color: var(--primary-color);
}
.days-of-week, .dates {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
text-align: center;
}
.days-of-week span {
font-weight: bold;
color: #666;
padding: 5px;
}
.dates .date-item {
padding: 10px;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.dates .date-item.disabled {
cursor: not-allowed;
color: var(--secondary-color);
opacity: 0.5;
}
.dates .date-item:hover {
background-color: #f0f8ff;
transform: scale(1.05);
}
.dates .date-item.available {
background-color: #e9f2ff;
border: 2px solid var(--primary-color);
font-weight: 600;
}
.dates .date-item.selected {
background-color: #0d6efd !important;
color: #fff !important;
border-radius: 6px;
font-weight: 600;
border: none !important;
}
.dates .date-item.current {
background-color: #e9f2ff;
border: 2px solid var(--primary-color);
font-weight: 600;
}
.time-slot {
font-size: 1rem;
padding: 0.75rem 1.5rem;
transition: background-color 0.3s ease, border-color 0.3s ease, transform 0.2s ease;
}
.time-slot.available {
background-color: #e9f2ff;
border-color: var(--primary-color);
color: var(--primary-color);
}
.time-slots .text-muted {
color: #6c757d; /* Muted text color, matching Calendly */
font-size: 1rem; /* Slightly larger font, e.g., 20px instead of 16px */
font-weight: bold; /* Bold text */
padding: 1rem 0;
text-align: center;
}
.time-slot.available:hover, .time-slot:hover {
background-color: var(--primary-color);
color: #fff;
transform: scale(1.05);
}
.timezone {
font-size: 0.875rem;
color: #666;
}
.powered-by {
font-size: 0.75rem;
color: #666;
text-align: right;
position: relative;
margin-top: 2rem;
opacity: 0.8;
transition: opacity 0.3s ease;
z-index: 1;
}
.powered-by a {
color: inherit;
text-decoration: none;
cursor: pointer;
}
.powered-by:hover {
opacity: 1;
}
/* Admin-specific styles */
#admin .calendar {
margin-bottom: 20px;
}
#admin .form-control, #admin .form-select {
border-radius: 6px;
}
#admin .btn-primary, #admin .btn-danger {
border-radius: 4px;
}
#availabilityList .list-group-item {
border-radius: 4px;
margin-bottom: 5px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.container-fluid {
margin: 10px;
border-radius: 6px;
}
.sidebar, .main-content {
width: 100%;
}
.sidebar {
border-right: none;
border-bottom: 1px solid #e1e8ed;
}
#admin .row {
flex-direction: column;
}
#admin .col-md-6 {
width: 100%;
}
}

419
server.js Normal file
View File

@ -0,0 +1,419 @@
const express = require('express');
const path = require('path');
const sqlite3 = require('sqlite3').verbose();
const app = express();
const port = 3000;
// Serve static files from the 'public' directory
app.use(express.static(path.join(__dirname, 'public')));
// Serve HTML files from the 'views' directory
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'index.html'));
});
app.get('/admin', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'admin.html'));
});
// Middleware to parse JSON bodies
app.use(express.json());
// Helper function to create database tables
const createDatabaseTables = (callback) => {
db.serialize(() => {
// Enable foreign key support
db.run('PRAGMA foreign_keys = ON', (err) => {
if (err) {
callback(new Error(`Failed to enable foreign keys: ${err.message}`));
return;
}
// Create uids table
db.run(`CREATE TABLE IF NOT EXISTS uids (
uid TEXT PRIMARY KEY,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`, (err) => {
if (err) {
callback(new Error(`Failed to create uids table: ${err.message}`));
return;
}
// Create availability table
db.run(`CREATE TABLE IF NOT EXISTS availability (
uid TEXT,
date TEXT,
times TEXT,
PRIMARY KEY (uid, date),
FOREIGN KEY (uid) REFERENCES uids(uid) ON DELETE CASCADE
)`, (err) => {
if (err) {
callback(new Error(`Failed to create availability table: ${err.message}`));
return;
}
callback(null);
});
});
});
});
};
// Helper function to initialize database
const initializeDatabase = () => {
// Ensure db directory exists
const fs = require('fs');
const dbDir = 'db';
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir);
}
// Connect to SQLite database
db = new sqlite3.Database('db/freesched.db', (err) => {
if (err) {
console.error('Could not connect to database:', err.message);
return;
}
console.log('Connected to SQLite database');
// Create tables
createDatabaseTables((err) => {
if (err) {
console.error('Error creating database tables:', err.message);
return;
}
console.log('Database tables created successfully');
});
});
};
// Initialize database
let db;
initializeDatabase();
// Admin view - must be defined before the catch-all UID route
app.get('/admin', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'admin.html'));
});
// Public view - handle both root and UID-based routes
app.get(['/', '/:uid'], (req, res) => {
// Only validate UID if it's provided (not root path)
if (req.params.uid) {
// Validate UID format
if (!/^[a-z0-9-]+$/.test(req.params.uid)) {
res.sendFile(path.join(__dirname, 'views', 'index.html'));
return;
}
}
res.sendFile(path.join(__dirname, 'views', 'index.html'));
});
// Helper function to convert time string to minutes for sorting
function timeToMinutes(timeStr) {
const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i);
if (!match) return 0;
let [_, hours, minutes, period] = match;
hours = parseInt(hours);
minutes = minutes ? parseInt(minutes) : 0;
period = period.toLowerCase();
if (period === 'pm' && hours !== 12) hours += 12;
if (period === 'am' && hours === 12) hours = 0;
return hours * 60 + minutes;
}
// API endpoint to get available dates and times for a specific UID
app.get('/api/availability/:uid', (req, res) => {
const { uid } = req.params;
// Verify UID exists
db.get('SELECT uid FROM uids WHERE uid = ?', [uid], (err, row) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (!row) {
res.status(404).json({ error: 'UID not found' });
return;
}
// Get availability for this UID
db.all('SELECT date, times FROM availability WHERE uid = ?', [uid], (err, rows) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
const availability = {};
rows.forEach(row => {
// Handle case where times might be null or undefined
if (row.times) {
// Split, trim, and sort times chronologically
const times = row.times.split(',').map(t => t.trim());
times.sort((a, b) => timeToMinutes(a) - timeToMinutes(b));
availability[row.date] = times;
} else {
availability[row.date] = [];
}
});
res.json(availability);
});
});
});
// API endpoint to add/update availability for a specific UID
app.post('/api/availability/:uid', (req, res) => {
const { uid } = req.params;
const { date, times } = req.body;
if (!date) {
res.status(400).json({ error: 'Date is required' });
return;
}
// Verify UID exists
db.get('SELECT uid FROM uids WHERE uid = ?', [uid], (err, row) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (!row) {
res.status(404).json({ error: 'UID not found' });
return;
}
if (times.length === 0) {
// Delete the entry if no times remain
db.run('DELETE FROM availability WHERE uid = ? AND date = ?', [uid, date], (err) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json({ message: 'Availability removed successfully' });
});
} else {
// Sort times before storing
const sortedTimes = [...times].sort((a, b) => timeToMinutes(a) - timeToMinutes(b));
// Insert or replace with the sorted times
db.run('INSERT OR REPLACE INTO availability (uid, date, times) VALUES (?, ?, ?)',
[uid, date, sortedTimes.join(',')],
(err) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
res.json({ message: 'Availability updated successfully' });
}
);
}
});
});
// API endpoint to get all UIDs
app.get('/api/uids', (req, res) => {
createDatabaseTables((err) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
// Get all UIDs
db.all('SELECT uid, created_at FROM uids ORDER BY created_at DESC', (err, rows) => {
if (err) {
res.status(500).json({ error: `Failed to fetch UIDs: ${err.message}` });
return;
}
res.json(rows || []);
});
});
});
// API endpoint to create a new UID
app.post('/api/uids', (req, res) => {
const { uid } = req.body;
// Validate UID format (lowercase letters, numbers, and hyphens only)
if (!uid || !/^[a-z0-9-]+$/.test(uid)) {
res.status(400).json({ error: 'Invalid UID format. Use only lowercase letters, numbers, and hyphens.' });
return;
}
createDatabaseTables((err) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
db.run('INSERT INTO uids (uid) VALUES (?)', [uid], function(err) {
if (err) {
if (err.code === 'SQLITE_CONSTRAINT') {
res.status(409).json({ error: 'UID already exists' });
} else {
res.status(500).json({ error: `Failed to insert UID: ${err.message}` });
}
return;
}
res.status(201).json({ uid, created_at: new Date().toISOString() });
});
});
});
// API endpoint to delete a UID and its availability
app.delete('/api/uids/:uid', (req, res) => {
const { uid } = req.params;
// Begin transaction
db.serialize(() => {
db.run('BEGIN TRANSACTION');
// Delete from availability first due to foreign key constraint
db.run('DELETE FROM availability WHERE uid = ?', [uid], (err) => {
if (err) {
db.run('ROLLBACK');
res.status(500).json({ error: err.message });
return;
}
// Then delete from uids
db.run('DELETE FROM uids WHERE uid = ?', [uid], function(err) {
if (err) {
db.run('ROLLBACK');
res.status(500).json({ error: err.message });
return;
}
if (this.changes === 0) {
db.run('ROLLBACK');
res.status(404).json({ error: 'UID not found' });
return;
}
// Commit transaction
db.run('COMMIT', (err) => {
if (err) {
db.run('ROLLBACK');
res.status(500).json({ error: err.message });
return;
}
res.json({ message: 'UID deleted successfully' });
});
});
});
});
});
// API endpoint to flush (delete) all availability entries for a specific UID
app.delete('/api/availability/:uid/flush', (req, res) => {
const { uid } = req.params;
// Verify UID exists
db.get('SELECT uid FROM uids WHERE uid = ?', [uid], (err, row) => {
if (err) {
res.status(500).json({ error: err.message });
return;
}
if (!row) {
res.status(404).json({ error: 'UID not found' });
return;
}
// Delete all availability for this UID
db.run('DELETE FROM availability WHERE uid = ?', [uid], (err) => {
if (err) {
console.error('Error deleting availability:', err);
res.status(500).json({ error: err.message });
return;
}
console.log(`All availability entries for UID ${uid} deleted successfully`);
res.json({ message: `All availability entries for UID ${uid} deleted successfully` });
});
});
});
// API endpoint to reset the entire database
app.post('/api/reset', async (req, res) => {
const fs = require('fs').promises;
const path = require('path');
const dbDir = 'db';
const dbPath = path.join(dbDir, 'freesched.db');
try {
// Close current database connection
await new Promise((resolve, reject) => {
if (!db) {
resolve();
return;
}
db.close((err) => {
if (err) reject(err);
else resolve();
});
});
// Ensure db directory exists
try {
await fs.access(dbDir);
} catch {
await fs.mkdir(dbDir);
}
// Delete existing database if it exists
try {
await fs.unlink(dbPath);
} catch (err) {
if (err.code !== 'ENOENT') throw err; // Ignore if file doesn't exist
}
// Initialize fresh database
initializeDatabase();
// Wait for database to be ready
await new Promise((resolve, reject) => {
const checkDb = () => {
if (!db) {
setTimeout(checkDb, 100); // Check every 100ms
return;
}
// Test database connection
db.get('SELECT 1', (err) => {
if (err) {
setTimeout(checkDb, 100);
} else {
resolve();
}
});
};
checkDb();
// Set a timeout of 5 seconds
setTimeout(() => {
reject(new Error('Database initialization timeout'));
}, 5000);
});
res.json({ message: 'Database completely wiped and recreated successfully' });
} catch (error) {
console.error('Error in flush-global:', error);
// Try to reinitialize database
try {
initializeDatabase();
} catch (reinitError) {
console.error('Failed to reinitialize database:', reinitError);
}
// Ensure we haven't already sent a response
if (!res.headersSent) {
res.status(500).json({ error: error.message });
}
}
});
app.listen(port, () => {
console.log(`FreeSched app listening at http://localhost:${port}`);
});

110
views/admin.html Normal file
View File

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FreeSched Admin - Manage Availability</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
</head>
<body>
<div class="container-fluid p-0">
<div class="row g-0">
<div class="col-md-4 sidebar bg-light border-end border-gray-300">
<div class="d-flex flex-column align-items-center p-4">
<img src="/avatar.jpg" alt="Doug Masiero" class="profile-pic rounded-circle mb-3 shadow-sm">
<h2 class="fs-5 text-dark fw-bold mb-2">Doug Masiero</h2>
<h3 class="fs-6 text-muted mb-2">30 Minute Meeting</h3>
<p class="text-muted fs-6">30 min</p>
</div>
</div>
<div class="col-md-8 main-content p-4">
<div class="calendar-header d-flex justify-content-between align-items-center text-dark fs-5 mb-3">
<button id="adminPrevMonth" class="btn btn-link p-0 text-decoration-none"><i class="bi bi-chevron-left"></i></button>
<span id="adminMonthYear" class="fw-bold"></span>
<button id="adminNextMonth" class="btn btn-link p-0 text-decoration-none"><i class="bi bi-chevron-right"></i></button>
</div>
<div class="calendar mb-4">
<div class="days-of-week d-grid gap-2 mb-2" style="grid-template-columns: repeat(7, 1fr);">
<span class="text-muted fw-bold text-center p-2">SUN</span>
<span class="text-muted fw-bold text-center p-2">MON</span>
<span class="text-muted fw-bold text-center p-2">TUE</span>
<span class="text-muted fw-bold text-center p-2">WED</span>
<span class="text-muted fw-bold text-center p-2">THU</span>
<span class="text-muted fw-bold text-center p-2">FRI</span>
<span class="text-muted fw-bold text-center p-2">SAT</span>
</div>
<div id="adminCalendarDates" class="dates d-grid gap-2" style="grid-template-columns: repeat(7, 1fr);">
<!-- Dates will be dynamically generated by JavaScript -->
</div>
</div>
<div class="uid-section mb-4">
<h2 class="fs-4 text-dark fw-bold mb-3">Manage Schedule UIDs</h2>
<div class="row g-3 mb-4">
<div class="col-md-6">
<select class="form-select form-select-lg" id="uidSelect">
<option value="">Select a Schedule UID...</option>
</select>
</div>
<div class="col-md-3">
<button class="btn btn-warning btn-lg w-100" id="flushDatabase" disabled>Clear UID</button>
</div>
<div class="col-md-3">
<button class="btn btn-danger btn-lg w-100" id="deleteUid" disabled>Delete UID</button>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-9">
<div class="mb-2">
<div class="input-group input-group-lg gap-2">
<input type="text" class="form-control" id="newUid" placeholder="Enter new UID (e.g., john-1on1)">
<button class="btn btn-success" id="createUid" style="min-width: 120px;">Create UID</button>
</div>
<div class="form-text">UIDs should be lowercase letters, numbers, and hyphens only</div>
</div>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-12">
<div class="form-control-lg bg-light p-3">
<small class="d-block text-muted mb-1">Selected UID URL:</small>
<a id="uidUrl" href="#" target="_blank" class="text-decoration-none text-break">Select a UID first</a>
</div>
</div>
</div>
</div>
<div class="availability-section">
<div id="selectedDateDisplay" class="mb-3 text-muted fs-6"></div>
<div class="time-grid mb-3">
<div class="time-slots" id="timeSlots">
<!-- Time slots will be dynamically populated by JavaScript -->
</div>
</div>
</div>
<div class="powered-by">
<a href="https://github.com/moeny/freesched" target="_blank" class="text-decoration-none text-muted">Powered by FreeSched</a>
</div>
</div>
</div>
</div>
<div class="container-fluid p-0">
<div class="danger-zone border border-danger rounded p-4">
<h2 class="fs-4 text-danger fw-bold mb-3">⚠️ Danger Zone</h2>
<p class="text-muted mb-4">The actions in this section can cause irreversible changes to the database. Please proceed with caution.</p>
<button class="btn btn-outline-danger w-100 mb-2" id="resetDatabase">Reset Global Database</button>
<div class="text-muted">This will permanently delete all UIDs and their associated availability data.</div>
</div>
</div>
<!-- Bootstrap JS and Popper.js -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"></script>
<script src="/admin.js"></script>
</body>
</html>

111
views/index.html Normal file
View File

@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FreeSched Meeting Scheduler</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css">
</head>
<body>
<div id="uidError" class="container-fluid p-5 text-center" style="display: none;">
<div class="alert alert-warning p-5 shadow-sm" role="alert">
<h4 class="alert-heading mb-4"><i class="bi bi-exclamation-triangle me-2"></i>Invalid Schedule URL</h4>
<p class="mb-0">Please make sure you have the correct scheduling link.<br>The URL should include a valid schedule identifier.</p>
</div>
</div>
<div id="mainContent" class="container-fluid p-0">
<div class="row g-0">
<div class="col-md-4 sidebar bg-light border-end border-gray-300">
<div class="d-flex flex-column align-items-center p-4">
<img src="/avatar.jpg" alt="Doug Masiero" class="profile-pic rounded-circle mb-3 shadow-sm">
<h2 class="fs-5 text-dark fw-bold mb-2">Doug Masiero</h2>
<h3 class="fs-6 text-muted mb-2">30 Minute Meeting</h3>
<p class="text-muted fs-6">30 min</p>
</div>
</div>
<div class="col-md-8 main-content p-4">
<div class="calendar-header">
<h1 class="fs-3 text-dark fw-bold mb-3">Select a Date & Time</h1>
<div class="month-nav d-flex justify-content-between align-items-center text-dark fs-5">
<button id="prevMonth" class="btn btn-link p-0 text-decoration-none"><i class="bi bi-chevron-left"></i></button>
<span id="monthYear">April 2021</span>
<button id="nextMonth" class="btn btn-link p-0 text-decoration-none"><i class="bi bi-chevron-right"></i></button>
</div>
</div>
<div class="calendar mb-4">
<div class="days-of-week d-grid gap-2 mb-2" style="grid-template-columns: repeat(7, 1fr);">
<span class="text-muted fw-bold text-center p-2">SUN</span>
<span class="text-muted fw-bold text-center p-2">MON</span>
<span class="text-muted fw-bold text-center p-2">TUE</span>
<span class="text-muted fw-bold text-center p-2">WED</span>
<span class="text-muted fw-bold text-center p-2">THU</span>
<span class="text-muted fw-bold text-center p-2">FRI</span>
<span class="text-muted fw-bold text-center p-2">SAT</span>
</div>
<div id="calendarDates" class="dates d-grid gap-2" style="grid-template-columns: repeat(7, 1fr);">
<!-- Dates will be dynamically generated by JavaScript -->
</div>
</div>
<div id="timeSlots" class="time-slots mb-4">
<!-- Time slots will be dynamically populated by JavaScript -->
</div>
<p id="timezone" class="text-muted fs-6 mb-3">Eastern Time - US & Canada (<span id="currentTime"></span>)</p>
<div class="powered-by text-muted fs-7 text-end">
<a href="https://github.com/moeny/freesched" target="_blank" class="text-decoration-none text-muted">Powered by FreeSched</a>
</div>
</div>
</div>
</div>
<!-- Booking Modal -->
<div class="modal fade" id="bookingModal" tabindex="-1" aria-labelledby="bookingModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content shadow">
<div class="modal-header border-bottom-0 pb-0">
<h5 class="modal-title fw-bold" id="bookingModalLabel">Confirm Appointment</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pt-2 px-4">
<form id="bookingForm">
<div class="mb-4">
<label for="name" class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
<input type="text" class="form-control form-control-lg" id="name" required>
<div class="invalid-feedback">Please enter your name.</div>
</div>
<div class="mb-4">
<label for="email" class="form-label fw-semibold">Email <span class="text-danger">*</span></label>
<input type="email" class="form-control form-control-lg" id="email" required>
<div class="invalid-feedback">Please enter a valid email address.</div>
</div>
<div class="mb-4">
<label for="phone" class="form-label fw-semibold">Phone Number <small class="text-muted">(optional)</small></label>
<input type="tel" class="form-control form-control-lg" id="phone" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}">
<small class="text-muted">Format: XXX-XXX-XXXX</small>
<div class="invalid-feedback">Please use format: XXX-XXX-XXXX</div>
</div>
<div class="mb-4">
<label for="notes" class="form-label fw-semibold">Notes <small class="text-muted">(optional)</small></label>
<textarea class="form-control" id="notes" rows="3" placeholder="Add any additional information..."></textarea>
</div>
<input type="hidden" id="selectedTime">
<input type="hidden" id="selectedDate">
</form>
</div>
<div class="modal-footer border-top-0 px-4">
<button type="button" class="btn btn-light btn-lg px-4" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-lg px-4" id="submitBooking">Confirm</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS and Popper.js -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"></script>
<script src="/public.js"></script>
</body>
</html>