client-app / index.html
Cezarxil's picture
Make a small minimalist app for keeping track of clients for a hair removal salon. I want to be able to delete clients and add them, I want a section with a calendar. Make it simple and functional. Have a function that can send a message to the client a day before the appointment. - Follow Up Deployment
63028e1 verified
raw
history blame
31.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Silky Smooth | Client Management</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.calendar-day:hover:not(.empty-day) {
background-color: #f3f4f6;
cursor: pointer;
}
.appointment-day {
background-color: #f0fdf4;
border: 1px solid #bbf7d0;
}
.today {
background-color: #eff6ff;
border: 1px solid #bfdbfe;
}
/* Custom scrollbar */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-6xl">
<!-- Header -->
<header class="flex flex-col md:flex-row justify-between items-center mb-8">
<div class="flex items-center mb-4 md:mb-0">
<i class="fas fa-spa text-pink-500 text-3xl mr-3"></i>
<h1 class="text-2xl font-bold text-gray-800">Silky Smooth Client Manager</h1>
</div>
<div class="flex space-x-2">
<button id="showClientsBtn" class="px-4 py-2 bg-pink-500 text-white rounded-lg hover:bg-pink-600 transition">
<i class="fas fa-users mr-2"></i>Clients
</button>
<button id="showCalendarBtn" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition">
<i class="far fa-calendar-alt mr-2"></i>Calendar
</button>
</div>
</header>
<!-- Main Content -->
<main class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Clients Section -->
<div id="clientsSection" class="lg:col-span-2 bg-white rounded-xl shadow-md overflow-hidden">
<div class="p-5 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-800">Client Management</h2>
</div>
<!-- Add Client Form -->
<div class="p-5 border-b border-gray-200">
<form id="clientForm" class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="clientName" class="block text-sm font-medium text-gray-700 mb-1">Full Name</label>
<input type="text" id="clientName" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500" required>
</div>
<div>
<label for="clientPhone" class="block text-sm font-medium text-gray-700 mb-1">Phone Number</label>
<input type="tel" id="clientPhone" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500" required>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="clientService" class="block text-sm font-medium text-gray-700 mb-1">Service</label>
<select id="clientService" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500">
<option value="Brazilian Wax">Brazilian Wax</option>
<option value="Bikini Wax">Bikini Wax</option>
<option value="Leg Wax">Leg Wax</option>
<option value="Arm Wax">Arm Wax</option>
<option value="Underarm Wax">Underarm Wax</option>
<option value="Facial Wax">Facial Wax</option>
</select>
</div>
<div>
<label for="clientDate" class="block text-sm font-medium text-gray-700 mb-1">Appointment Date</label>
<input type="date" id="clientDate" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500" required>
</div>
<div>
<label for="clientTime" class="block text-sm font-medium text-gray-700 mb-1">Time</label>
<input type="time" id="clientTime" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500" required>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="px-4 py-2 bg-pink-500 text-white rounded-md hover:bg-pink-600 transition flex items-center">
<i class="fas fa-plus mr-2"></i> Add Client
</button>
</div>
</form>
</div>
<!-- Clients List -->
<div class="overflow-y-auto custom-scrollbar" style="max-height: 500px;">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Phone</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Service</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Appointment</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody id="clientsList" class="bg-white divide-y divide-gray-200">
<!-- Clients will be added here dynamically -->
</tbody>
</table>
</div>
</div>
<!-- Calendar Section -->
<div id="calendarSection" class="hidden lg:block bg-white rounded-xl shadow-md overflow-hidden">
<div class="p-5 border-b border-gray-200">
<h2 class="text-xl font-semibold text-gray-800">Appointment Calendar</h2>
</div>
<div class="p-4">
<div class="flex justify-between items-center mb-4">
<button id="prevMonth" class="p-2 rounded-full hover:bg-gray-100">
<i class="fas fa-chevron-left text-gray-600"></i>
</button>
<h3 id="currentMonthYear" class="text-lg font-medium text-gray-800">June 2023</h3>
<button id="nextMonth" class="p-2 rounded-full hover:bg-gray-100">
<i class="fas fa-chevron-right text-gray-600"></i>
</button>
</div>
<div class="grid grid-cols-7 gap-1 mb-2">
<div class="text-center text-xs font-medium text-gray-500 py-1">Sun</div>
<div class="text-center text-xs font-medium text-gray-500 py-1">Mon</div>
<div class="text-center text-xs font-medium text-gray-500 py-1">Tue</div>
<div class="text-center text-xs font-medium text-gray-500 py-1">Wed</div>
<div class="text-center text-xs font-medium text-gray-500 py-1">Thu</div>
<div class="text-center text-xs font-medium text-gray-500 py-1">Fri</div>
<div class="text-center text-xs font-medium text-gray-500 py-1">Sat</div>
</div>
<div id="calendarDays" class="grid grid-cols-7 gap-1">
<!-- Calendar days will be generated here -->
</div>
</div>
<!-- Day Appointments -->
<div class="p-4 border-t border-gray-200">
<h3 id="selectedDayTitle" class="text-md font-medium text-gray-800 mb-3">Today's Appointments</h3>
<div id="dayAppointments" class="space-y-3 max-h-64 overflow-y-auto custom-scrollbar">
<!-- Appointments for selected day will appear here -->
</div>
</div>
<!-- SMS Reminder Button -->
<div class="p-4 border-t border-gray-200">
<button id="sendRemindersBtn" class="w-full py-2 bg-green-500 text-white rounded-md hover:bg-green-600 transition flex items-center justify-center">
<i class="fas fa-sms mr-2"></i> Send Tomorrow's Reminders
</button>
<div id="smsStatus" class="mt-2 text-sm text-center hidden"></div>
</div>
</div>
</main>
</div>
<!-- Edit Client Modal -->
<div id="editModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg p-6 w-full max-w-md">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Edit Client</h3>
<button id="closeEditModal" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<form id="editClientForm" class="space-y-4">
<input type="hidden" id="editClientId">
<div>
<label for="editClientName" class="block text-sm font-medium text-gray-700 mb-1">Full Name</label>
<input type="text" id="editClientName" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500" required>
</div>
<div>
<label for="editClientPhone" class="block text-sm font-medium text-gray-700 mb-1">Phone Number</label>
<input type="tel" id="editClientPhone" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500" required>
</div>
<div>
<label for="editClientService" class="block text-sm font-medium text-gray-700 mb-1">Service</label>
<select id="editClientService" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500">
<option value="Brazilian Wax">Brazilian Wax</option>
<option value="Bikini Wax">Bikini Wax</option>
<option value="Leg Wax">Leg Wax</option>
<option value="Arm Wax">Arm Wax</option>
<option value="Underarm Wax">Underarm Wax</option>
<option value="Facial Wax">Facial Wax</option>
</select>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="editClientDate" class="block text-sm font-medium text-gray-700 mb-1">Appointment Date</label>
<input type="date" id="editClientDate" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500" required>
</div>
<div>
<label for="editClientTime" class="block text-sm font-medium text-gray-700 mb-1">Time</label>
<input type="time" id="editClientTime" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-pink-500" required>
</div>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" id="cancelEdit" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition">
Cancel
</button>
<button type="submit" class="px-4 py-2 bg-pink-500 text-white rounded-md hover:bg-pink-600 transition">
Save Changes
</button>
</div>
</form>
</div>
</div>
<script>
// Client data storage
let clients = JSON.parse(localStorage.getItem('clients')) || [];
// Current date for calendar
let currentDate = new Date();
let selectedDate = new Date();
// DOM Elements
const clientsSection = document.getElementById('clientsSection');
const calendarSection = document.getElementById('calendarSection');
const showClientsBtn = document.getElementById('showClientsBtn');
const showCalendarBtn = document.getElementById('showCalendarBtn');
const clientForm = document.getElementById('clientForm');
const clientsList = document.getElementById('clientsList');
const currentMonthYear = document.getElementById('currentMonthYear');
const calendarDays = document.getElementById('calendarDays');
const prevMonthBtn = document.getElementById('prevMonth');
const nextMonthBtn = document.getElementById('nextMonth');
const dayAppointments = document.getElementById('dayAppointments');
const selectedDayTitle = document.getElementById('selectedDayTitle');
const sendRemindersBtn = document.getElementById('sendRemindersBtn');
const smsStatus = document.getElementById('smsStatus');
const editModal = document.getElementById('editModal');
const editClientForm = document.getElementById('editClientForm');
const closeEditModal = document.getElementById('closeEditModal');
const cancelEdit = document.getElementById('cancelEdit');
// Initialize the app
document.addEventListener('DOMContentLoaded', () => {
renderClients();
generateCalendar();
showTodaysAppointments();
// Set default date to today in the form
const today = new Date().toISOString().split('T')[0];
document.getElementById('clientDate').value = today;
// Set default time to next hour
const nextHour = new Date();
nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0);
const timeString = nextHour.toTimeString().substring(0, 5);
document.getElementById('clientTime').value = timeString;
});
// Toggle between clients and calendar on mobile
showClientsBtn.addEventListener('click', () => {
clientsSection.classList.remove('hidden');
calendarSection.classList.add('hidden');
showClientsBtn.classList.remove('bg-gray-200', 'text-gray-700');
showClientsBtn.classList.add('bg-pink-500', 'text-white');
showCalendarBtn.classList.remove('bg-pink-500', 'text-white');
showCalendarBtn.classList.add('bg-gray-200', 'text-gray-700');
});
showCalendarBtn.addEventListener('click', () => {
clientsSection.classList.add('hidden');
calendarSection.classList.remove('hidden');
showCalendarBtn.classList.remove('bg-gray-200', 'text-gray-700');
showCalendarBtn.classList.add('bg-pink-500', 'text-white');
showClientsBtn.classList.remove('bg-pink-500', 'text-white');
showClientsBtn.classList.add('bg-gray-200', 'text-gray-700');
});
// Add new client
clientForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = document.getElementById('clientName').value;
const phone = document.getElementById('clientPhone').value;
const service = document.getElementById('clientService').value;
const date = document.getElementById('clientDate').value;
const time = document.getElementById('clientTime').value;
const newClient = {
id: Date.now().toString(),
name,
phone,
service,
date,
time,
status: 'scheduled'
};
clients.push(newClient);
saveClients();
renderClients();
generateCalendar();
showTodaysAppointments();
// Reset form
clientForm.reset();
// Set default date to today
document.getElementById('clientDate').value = new Date().toISOString().split('T')[0];
// Set default time to next hour
const nextHour = new Date();
nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0);
const timeString = nextHour.toTimeString().substring(0, 5);
document.getElementById('clientTime').value = timeString;
});
// Edit client modal
function openEditModal(client) {
document.getElementById('editClientId').value = client.id;
document.getElementById('editClientName').value = client.name;
document.getElementById('editClientPhone').value = client.phone;
document.getElementById('editClientService').value = client.service;
document.getElementById('editClientDate').value = client.date;
document.getElementById('editClientTime').value = client.time;
editModal.classList.remove('hidden');
}
// Close edit modal
closeEditModal.addEventListener('click', () => {
editModal.classList.add('hidden');
});
cancelEdit.addEventListener('click', () => {
editModal.classList.add('hidden');
});
// Save edited client
editClientForm.addEventListener('submit', (e) => {
e.preventDefault();
const id = document.getElementById('editClientId').value;
const name = document.getElementById('editClientName').value;
const phone = document.getElementById('editClientPhone').value;
const service = document.getElementById('editClientService').value;
const date = document.getElementById('editClientDate').value;
const time = document.getElementById('editClientTime').value;
const clientIndex = clients.findIndex(client => client.id === id);
if (clientIndex !== -1) {
clients[clientIndex] = {
...clients[clientIndex],
name,
phone,
service,
date,
time
};
saveClients();
renderClients();
generateCalendar();
showTodaysAppointments();
editModal.classList.add('hidden');
}
});
// Delete client
function deleteClient(id) {
if (confirm('Are you sure you want to delete this client?')) {
clients = clients.filter(client => client.id !== id);
saveClients();
renderClients();
generateCalendar();
showTodaysAppointments();
}
}
// Render clients list
function renderClients() {
if (clients.length === 0) {
clientsList.innerHTML = `
<tr>
<td colspan="5" class="px-6 py-4 text-center text-gray-500">No clients found. Add your first client above.</td>
</tr>
`;
return;
}
// Sort clients by date and time
const sortedClients = [...clients].sort((a, b) => {
const dateA = new Date(`${a.date}T${a.time}`);
const dateB = new Date(`${b.date}T${b.time}`);
return dateA - dateB;
});
clientsList.innerHTML = sortedClients.map(client => `
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-gray-900">${client.name}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500">${formatPhoneNumber(client.phone)}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500">${client.service}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-500">${formatDate(client.date)} at ${client.time}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button onclick="openEditModal(${JSON.stringify(client).replace(/"/g, '&quot;')})" class="text-pink-600 hover:text-pink-900 mr-3">
<i class="fas fa-edit"></i>
</button>
<button onclick="deleteClient('${client.id}')" class="text-red-600 hover:text-red-900">
<i class="fas fa-trash-alt"></i>
</button>
</td>
</tr>
`).join('');
}
// Generate calendar
function generateCalendar() {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
currentMonthYear.textContent = new Date(year, month).toLocaleDateString('en-US', {
month: 'long',
year: 'numeric'
});
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const daysInPrevMonth = new Date(year, month, 0).getDate();
calendarDays.innerHTML = '';
// Previous month's days
for (let i = firstDay; i > 0; i--) {
const day = daysInPrevMonth - i + 1;
calendarDays.innerHTML += `
<div class="h-12 flex items-center justify-center text-gray-400 text-sm empty-day">
${day}
</div>
`;
}
// Current month's days
for (let i = 1; i <= daysInMonth; i++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(i).padStart(2, '0')}`;
const hasAppointment = clients.some(client => client.date === dateStr);
const isToday = isSameDay(new Date(dateStr), new Date());
let dayClass = 'h-12 flex items-center justify-center text-sm calendar-day';
if (hasAppointment) dayClass += ' appointment-day';
if (isToday) dayClass += ' today';
calendarDays.innerHTML += `
<div class="${dayClass}" data-date="${dateStr}">
${i}
${hasAppointment ? '<span class="absolute bottom-1 w-1 h-1 bg-pink-500 rounded-full"></span>' : ''}
</div>
`;
}
// Next month's days
const daysToAdd = 42 - (firstDay + daysInMonth); // 6 rows x 7 days = 42 cells
for (let i = 1; i <= daysToAdd; i++) {
calendarDays.innerHTML += `
<div class="h-12 flex items-center justify-center text-gray-400 text-sm empty-day">
${i}
</div>
`;
}
// Add click event to days
document.querySelectorAll('.calendar-day').forEach(day => {
day.addEventListener('click', () => {
const dateStr = day.getAttribute('data-date');
selectedDate = new Date(dateStr);
showDayAppointments(dateStr);
});
});
}
// Show today's appointments
function showTodaysAppointments() {
const today = new Date().toISOString().split('T')[0];
selectedDayTitle.textContent = "Today's Appointments";
showDayAppointments(today);
}
// Show appointments for a specific day
function showDayAppointments(dateStr) {
const dayAppointmentsList = clients.filter(client => client.date === dateStr)
.sort((a, b) => a.time.localeCompare(b.time));
if (dayAppointmentsList.length === 0) {
dayAppointments.innerHTML = `
<div class="text-center text-gray-500 py-4">
No appointments scheduled for ${formatDate(dateStr)}
</div>
`;
} else {
dayAppointments.innerHTML = dayAppointmentsList.map(client => `
<div class="bg-white border border-gray-200 rounded-lg p-3 shadow-sm">
<div class="flex justify-between items-start">
<div>
<h4 class="font-medium text-gray-800">${client.name}</h4>
<p class="text-sm text-gray-600">${client.service}</p>
</div>
<span class="text-sm font-medium">${client.time}</span>
</div>
<div class="mt-2 flex justify-between items-center">
<span class="text-xs text-gray-500">${formatPhoneNumber(client.phone)}</span>
<button onclick="deleteClient('${client.id}')" class="text-red-500 hover:text-red-700 text-xs">
<i class="fas fa-times mr-1"></i>Cancel
</button>
</div>
</div>
`).join('');
}
// Update selected day title if not today
if (!isSameDay(new Date(dateStr), new Date())) {
selectedDayTitle.textContent = `Appointments for ${formatDate(dateStr)}`;
}
}
// Navigate calendar months
prevMonthBtn.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() - 1);
generateCalendar();
});
nextMonthBtn.addEventListener('click', () => {
currentDate.setMonth(currentDate.getMonth() + 1);
generateCalendar();
});
// Send SMS reminders for tomorrow's appointments
sendRemindersBtn.addEventListener('click', () => {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const tomorrowStr = tomorrow.toISOString().split('T')[0];
const tomorrowsAppointments = clients.filter(client => client.date === tomorrowStr);
if (tomorrowsAppointments.length === 0) {
smsStatus.textContent = "No appointments tomorrow to send reminders for.";
smsStatus.className = "mt-2 text-sm text-center text-gray-600";
smsStatus.classList.remove('hidden');
return;
}
// Simulate sending SMS (in a real app, you would integrate with an SMS API)
smsStatus.textContent = `Sending ${tomorrowsAppointments.length} reminder(s)...`;
smsStatus.className = "mt-2 text-sm text-center text-blue-600";
smsStatus.classList.remove('hidden');
setTimeout(() => {
smsStatus.textContent = `Successfully sent ${tomorrowsAppointments.length} reminder(s) for tomorrow's appointments!`;
smsStatus.className = "mt-2 text-sm text-center text-green-600";
// In a real app, you would track which reminders were sent
tomorrowsAppointments.forEach(appointment => {
console.log(`Sent SMS to ${appointment.phone}: Reminder: Your ${appointment.service} appointment is tomorrow at ${appointment.time}.`);
});
}, 2000);
});
// Helper functions
function saveClients() {
localStorage.setItem('clients', JSON.stringify(clients));
}
function formatDate(dateStr) {
const options = { weekday: 'short', month: 'short', day: 'numeric' };
return new Date(dateStr).toLocaleDateString('en-US', options);
}
function formatPhoneNumber(phone) {
// Simple formatting for display
return phone.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
}
function isSameDay(date1, date2) {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
}
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Cezarxil/client-app" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>