Cezarxil commited on
Commit
9fa64d3
·
verified ·
1 Parent(s): 3cfe1e2

Make a simple app for client appointments, that can run locally on the phone - Follow Up Deployment

Browse files
Files changed (1) hide show
  1. index.html +449 -366
index.html CHANGED
@@ -3,417 +3,500 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Epiliz - Laser Hair Removal</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
  <style>
10
- .fade-in {
11
- animation: fadeIn 0.3s ease-in-out;
 
12
  }
13
- @keyframes fadeIn {
14
- from { opacity: 0; transform: translateY(10px); }
15
- to { opacity: 1; transform: translateY(0); }
16
  }
17
- .sidebar {
18
- transition: all 0.3s ease;
 
19
  }
20
- @media (max-width: 768px) {
21
- .sidebar {
22
- transform: translateX(-100%);
23
- }
24
- .sidebar.open {
25
- transform: translateX(0);
26
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
  </style>
29
  </head>
30
- <body class="bg-gray-50 font-sans">
31
- <!-- Login Page -->
32
- <div id="login-page" class="min-h-screen flex items-center justify-center bg-gradient-to-br from-purple-100 to-pink-100">
33
- <div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
34
- <div class="text-center mb-8">
35
- <h1 class="text-3xl font-bold text-purple-800">Epiliz</h1>
36
- <p class="text-gray-600 mt-2">Laser Hair Removal Management</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  </div>
38
- <form id="login-form" class="space-y-6">
 
39
  <div>
40
- <label for="username" class="block text-sm font-medium text-gray-700 mb-1">Username</label>
41
- <input type="text" id="username" name="username" required
42
- class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500">
43
  </div>
44
  <div>
45
- <label for="password" class="block text-sm font-medium text-gray-700 mb-1">Password</label>
46
- <input type="password" id="password" name="password" required
47
- class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500">
48
  </div>
49
  <div>
50
- <button type="submit"
51
- class="w-full bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 transition duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2">
52
- Sign In
53
- </button>
54
  </div>
55
- </form>
56
- <div id="login-error" class="mt-4 text-red-500 text-sm hidden"></div>
57
- </div>
58
- </div>
59
-
60
- <!-- Main App (hidden initially) -->
61
- <div id="app-container" class="hidden min-h-screen">
62
- <!-- Mobile Header -->
63
- <div class="md:hidden bg-purple-700 text-white p-4 flex justify-between items-center">
64
- <button id="menu-toggle" class="text-white focus:outline-none">
65
- <i class="fas fa-bars text-xl"></i>
66
- </button>
67
- <h1 class="text-xl font-bold">Epiliz</h1>
68
- <div class="w-6"></div> <!-- Spacer for alignment -->
69
- </div>
70
-
71
- <!-- Sidebar -->
72
- <div id="sidebar" class="sidebar fixed inset-y-0 left-0 w-64 bg-purple-800 text-white transform md:translate-x-0 z-10">
73
- <div class="p-4 flex items-center border-b border-purple-700">
74
- <h1 class="text-xl font-bold">Epiliz</h1>
75
- </div>
76
- <nav class="mt-6">
77
- <div class="px-4 py-3 bg-purple-900">
78
- <span class="text-sm font-semibold">Menu</span>
79
- </div>
80
- <a href="#" id="dashboard-link" class="block px-4 py-3 mt-2 text-white hover:bg-purple-700 transition duration-200">
81
- <i class="fas fa-calendar-alt mr-3"></i> Appointments
82
- </a>
83
- <a href="#" id="add-appointment-link" class="block px-4 py-3 mt-2 text-white hover:bg-purple-700 transition duration-200">
84
- <i class="fas fa-plus-circle mr-3"></i> Add Appointment
85
- </a>
86
- <a href="#" id="logout-link" class="block px-4 py-3 mt-2 text-white hover:bg-purple-700 transition duration-200">
87
- <i class="fas fa-sign-out-alt mr-3"></i> Logout
88
- </a>
89
- </nav>
90
- </div>
91
-
92
- <!-- Main Content -->
93
- <div class="md:ml-64 transition-all duration-300">
94
- <!-- Dashboard -->
95
- <div id="dashboard" class="p-6 fade-in">
96
- <div class="flex justify-between items-center mb-6">
97
- <h2 class="text-2xl font-bold text-gray-800">Appointments</h2>
98
- <button id="add-appointment-btn" class="bg-purple-600 text-white px-4 py-2 rounded-md hover:bg-purple-700 transition duration-200 md:hidden">
99
- <i class="fas fa-plus mr-2"></i> Add
100
- </button>
101
  </div>
102
-
103
- <div class="bg-white rounded-lg shadow overflow-hidden">
104
- <div class="overflow-x-auto">
105
- <table class="min-w-full divide-y divide-gray-200">
106
- <thead class="bg-gray-50">
107
- <tr>
108
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Client</th>
109
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
110
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Treatment Area</th>
111
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Contact</th>
112
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
113
- </tr>
114
- </thead>
115
- <tbody id="appointments-list" class="bg-white divide-y divide-gray-200">
116
- <!-- Appointments will be loaded here -->
117
- </tbody>
118
- </table>
119
- </div>
120
  </div>
121
- <div id="no-appointments" class="mt-8 text-center text-gray-500 hidden">
122
- <i class="fas fa-calendar-times text-4xl mb-4"></i>
123
- <p class="text-lg">No appointments scheduled</p>
124
  </div>
125
- </div>
126
-
127
- <!-- Add Appointment Form -->
128
- <div id="add-appointment" class="p-6 hidden fade-in">
129
- <div class="flex items-center mb-6">
130
- <button id="back-to-dashboard" class="mr-4 text-purple-600 hover:text-purple-800">
131
- <i class="fas fa-arrow-left text-xl"></i>
132
  </button>
133
- <h2 class="text-2xl font-bold text-gray-800">Add New Appointment</h2>
134
- </div>
135
-
136
- <div class="bg-white rounded-lg shadow p-6 max-w-2xl mx-auto">
137
- <form id="appointment-form">
138
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
139
- <div>
140
- <label for="client-name" class="block text-sm font-medium text-gray-700 mb-1">Client Name*</label>
141
- <input type="text" id="client-name" name="client-name" required
142
- class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500">
143
- </div>
144
- <div>
145
- <label for="appointment-date" class="block text-sm font-medium text-gray-700 mb-1">Appointment Date*</label>
146
- <input type="datetime-local" id="appointment-date" name="appointment-date" required
147
- class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500">
148
- </div>
149
- <div>
150
- <label for="treatment-area" class="block text-sm font-medium text-gray-700 mb-1">Treatment Area*</label>
151
- <select id="treatment-area" name="treatment-area" required
152
- class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500">
153
- <option value="">Select area</option>
154
- <option value="Face">Face</option>
155
- <option value="Underarms">Underarms</option>
156
- <option value="Arms">Arms</option>
157
- <option value="Legs">Legs</option>
158
- <option value="Bikini">Bikini</option>
159
- <option value="Back">Back</option>
160
- <option value="Chest">Chest</option>
161
- <option value="Brazilian">Brazilian</option>
162
- </select>
163
- </div>
164
- <div>
165
- <label for="contact-info" class="block text-sm font-medium text-gray-700 mb-1">Contact Info</label>
166
- <input type="text" id="contact-info" name="contact-info"
167
- class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500">
168
- </div>
169
- </div>
170
- <div class="mt-8">
171
- <button type="submit"
172
- class="w-full md:w-auto bg-purple-600 text-white py-2 px-6 rounded-md hover:bg-purple-700 transition duration-200 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2">
173
- Save Appointment
174
- </button>
175
- </div>
176
- </form>
177
- <div id="form-error" class="mt-4 text-red-500 text-sm hidden"></div>
178
- <div id="form-success" class="mt-4 text-green-500 text-sm hidden"></div>
179
  </div>
180
- </div>
181
  </div>
182
  </div>
 
 
 
 
 
183
 
184
  <script>
185
- // DOM Elements
186
- const loginPage = document.getElementById('login-page');
187
- const appContainer = document.getElementById('app-container');
188
- const loginForm = document.getElementById('login-form');
189
- const loginError = document.getElementById('login-error');
190
- const menuToggle = document.getElementById('menu-toggle');
191
- const sidebar = document.getElementById('sidebar');
192
- const dashboardLink = document.getElementById('dashboard-link');
193
- const addAppointmentLink = document.getElementById('add-appointment-link');
194
- const logoutLink = document.getElementById('logout-link');
195
- const dashboard = document.getElementById('dashboard');
196
- const addAppointment = document.getElementById('add-appointment');
197
- const backToDashboard = document.getElementById('back-to-dashboard');
198
- const addAppointmentBtn = document.getElementById('add-appointment-btn');
199
- const appointmentsList = document.getElementById('appointments-list');
200
- const noAppointments = document.getElementById('no-appointments');
201
- const appointmentForm = document.getElementById('appointment-form');
202
- const formError = document.getElementById('form-error');
203
- const formSuccess = document.getElementById('form-success');
204
-
205
- // Login
206
- loginForm.addEventListener('submit', function(e) {
207
- e.preventDefault();
208
- const username = document.getElementById('username').value;
209
- const password = document.getElementById('password').value;
 
 
 
 
 
 
210
 
211
- if (username === 'Epiliz' && password === 'Epilat2110') {
212
- loginError.classList.add('hidden');
213
- loginPage.classList.add('hidden');
214
- appContainer.classList.remove('hidden');
215
- loadAppointments();
216
- } else {
217
- loginError.textContent = 'Invalid username or password';
218
- loginError.classList.remove('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  }
220
- });
221
-
222
- // Navigation
223
- dashboardLink.addEventListener('click', function(e) {
224
- e.preventDefault();
225
- showDashboard();
226
- });
227
-
228
- addAppointmentLink.addEventListener('click', function(e) {
229
- e.preventDefault();
230
- showAddAppointment();
231
- });
232
-
233
- addAppointmentBtn.addEventListener('click', function(e) {
234
- e.preventDefault();
235
- showAddAppointment();
236
- });
237
-
238
- backToDashboard.addEventListener('click', function(e) {
239
- e.preventDefault();
240
- showDashboard();
241
- });
242
-
243
- logoutLink.addEventListener('click', function(e) {
244
- e.preventDefault();
245
- appContainer.classList.add('hidden');
246
- loginPage.classList.remove('hidden');
247
- loginForm.reset();
248
- });
249
-
250
- // Mobile menu toggle
251
- menuToggle.addEventListener('click', function() {
252
- sidebar.classList.toggle('open');
253
- });
254
-
255
- // Show dashboard view
256
- function showDashboard() {
257
- dashboard.classList.remove('hidden');
258
- addAppointment.classList.add('hidden');
259
- if (window.innerWidth < 768) {
260
- sidebar.classList.remove('open');
261
  }
262
- loadAppointments();
263
- }
264
-
265
- // Show add appointment view
266
- function showAddAppointment() {
267
- dashboard.classList.add('hidden');
268
- addAppointment.classList.remove('hidden');
269
- appointmentForm.reset();
270
- formError.classList.add('hidden');
271
- formSuccess.classList.add('hidden');
272
- if (window.innerWidth < 768) {
273
- sidebar.classList.remove('open');
274
  }
275
- }
276
-
277
- // Load appointments from database
278
- function loadAppointments() {
279
- // In a real app, this would fetch from your PHP backend
280
- // For demo purposes, we'll use mock data
281
- const mockAppointments = [
282
- {
283
- id: 1,
284
- client_name: "Sarah Johnson",
285
- appointment_date: "2023-06-15 14:30:00",
286
- treatment_area: "Legs",
287
- contact_info: "sarah@example.com"
288
- },
289
- {
290
- id: 2,
291
- client_name: "Michael Chen",
292
- appointment_date: "2023-06-16 10:00:00",
293
- treatment_area: "Back",
294
- contact_info: "555-123-4567"
295
- },
296
- {
297
- id: 3,
298
- client_name: "Emma Williams",
299
- appointment_date: "2023-06-17 15:45:00",
300
- treatment_area: "Bikini",
301
- contact_info: "emma.w@example.com"
 
 
 
 
 
 
302
  }
303
- ];
304
-
305
- appointmentsList.innerHTML = '';
 
 
 
 
 
 
 
306
 
307
- if (mockAppointments.length === 0) {
308
- noAppointments.classList.remove('hidden');
309
- } else {
310
- noAppointments.classList.add('hidden');
311
- mockAppointments.forEach(appointment => {
312
- const row = document.createElement('tr');
313
- row.className = 'hover:bg-gray-50';
314
 
315
- const formattedDate = new Date(appointment.appointment_date).toLocaleString('en-US', {
316
- month: 'short',
317
- day: 'numeric',
318
- year: 'numeric',
319
- hour: '2-digit',
320
- minute: '2-digit'
321
- });
322
 
323
- row.innerHTML = `
324
- <td class="px-6 py-4 whitespace-nowrap">
325
- <div class="font-medium text-gray-900">${appointment.client_name}</div>
326
- </td>
327
- <td class="px-6 py-4 whitespace-nowrap">
328
- <div class="text-gray-700">${formattedDate}</div>
329
- </td>
330
- <td class="px-6 py-4 whitespace-nowrap">
331
- <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-purple-100 text-purple-800">
332
- ${appointment.treatment_area}
333
- </span>
334
- </td>
335
- <td class="px-6 py-4 whitespace-nowrap text-gray-700">
336
- ${appointment.contact_info || 'N/A'}
337
- </td>
338
- <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
339
- <button class="text-red-600 hover:text-red-900 delete-btn" data-id="${appointment.id}">
340
- <i class="fas fa-trash-alt"></i>
341
- </button>
342
- </td>
343
- `;
344
 
345
- appointmentsList.appendChild(row);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  });
347
 
348
- // Add event listeners to delete buttons
349
- document.querySelectorAll('.delete-btn').forEach(btn => {
350
- btn.addEventListener('click', function() {
351
- const id = this.getAttribute('data-id');
352
- deleteAppointment(id);
353
- });
354
  });
355
- }
356
- }
357
-
358
- // Delete appointment
359
- function deleteAppointment(id) {
360
- if (confirm('Are you sure you want to delete this appointment?')) {
361
- // In a real app, this would send a request to your PHP backend
362
- console.log(`Deleting appointment with ID: ${id}`);
363
 
364
- // Simulate successful deletion
365
- setTimeout(() => {
366
- loadAppointments();
367
- }, 300);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  }
369
- }
370
-
371
- // Add new appointment
372
- appointmentForm.addEventListener('submit', function(e) {
373
- e.preventDefault();
374
-
375
- const clientName = document.getElementById('client-name').value;
376
- const appointmentDate = document.getElementById('appointment-date').value;
377
- const treatmentArea = document.getElementById('treatment-area').value;
378
- const contactInfo = document.getElementById('contact-info').value;
379
 
380
- // Simple validation
381
- if (!clientName || !appointmentDate || !treatmentArea) {
382
- formError.textContent = 'Please fill in all required fields';
383
- formError.classList.remove('hidden');
384
- formSuccess.classList.add('hidden');
385
- return;
 
 
 
 
 
 
 
386
  }
387
 
388
- // In a real app, this would send data to your PHP backend
389
- console.log('Saving appointment:', {
390
- clientName,
391
- appointmentDate,
392
- treatmentArea,
393
- contactInfo
394
- });
 
 
395
 
396
- // Simulate successful save
397
- formError.classList.add('hidden');
398
- formSuccess.textContent = 'Appointment saved successfully!';
399
- formSuccess.classList.remove('hidden');
 
 
 
400
 
401
- // Reset form and show success message
402
- setTimeout(() => {
403
- appointmentForm.reset();
404
- showDashboard();
405
- }, 1500);
406
- });
407
-
408
- // Initialize
409
- document.addEventListener('DOMContentLoaded', function() {
410
- // Set min date for appointment date input to today
411
- const today = new Date();
412
- const tomorrow = new Date(today);
413
- tomorrow.setDate(tomorrow.getDate() + 1);
414
 
415
- const minDate = tomorrow.toISOString().slice(0, 16);
416
- document.getElementById('appointment-date').min = minDate;
 
 
 
417
  });
418
  </script>
419
  <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/awesome-app" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Client Appointments</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
  <style>
10
+ /* Custom scrollbar */
11
+ ::-webkit-scrollbar {
12
+ width: 8px;
13
  }
14
+ ::-webkit-scrollbar-track {
15
+ background: #f1f1f1;
 
16
  }
17
+ ::-webkit-scrollbar-thumb {
18
+ background: #888;
19
+ border-radius: 4px;
20
  }
21
+ ::-webkit-scrollbar-thumb:hover {
22
+ background: #555;
23
+ }
24
+
25
+ /* Animation for notifications */
26
+ @keyframes slideIn {
27
+ from { transform: translateX(100%); }
28
+ to { transform: translateX(0); }
29
+ }
30
+
31
+ @keyframes fadeOut {
32
+ from { opacity: 1; }
33
+ to { opacity: 0; }
34
+ }
35
+
36
+ .notification {
37
+ animation: slideIn 0.3s forwards, fadeOut 0.5s 2.5s forwards;
38
+ }
39
+
40
+ /* Custom checkbox */
41
+ .custom-checkbox {
42
+ appearance: none;
43
+ -webkit-appearance: none;
44
+ width: 20px;
45
+ height: 20px;
46
+ border: 2px solid #4f46e5;
47
+ border-radius: 4px;
48
+ outline: none;
49
+ cursor: pointer;
50
+ position: relative;
51
+ }
52
+
53
+ .custom-checkbox:checked {
54
+ background-color: #4f46e5;
55
+ }
56
+
57
+ .custom-checkbox:checked::after {
58
+ content: '\2713';
59
+ font-size: 14px;
60
+ color: white;
61
+ position: absolute;
62
+ top: 50%;
63
+ left: 50%;
64
+ transform: translate(-50%, -50%);
65
  }
66
  </style>
67
  </head>
68
+ <body class="bg-gray-100 min-h-screen font-sans">
69
+ <div class="container mx-auto px-4 py-6 max-w-md">
70
+ <!-- Header -->
71
+ <header class="flex justify-between items-center mb-8">
72
+ <h1 class="text-2xl font-bold text-indigo-700">
73
+ <i class="fas fa-calendar-check mr-2"></i> Appointments
74
+ </h1>
75
+ <button id="addBtn" class="bg-indigo-600 text-white p-2 rounded-full hover:bg-indigo-700 transition">
76
+ <i class="fas fa-plus"></i>
77
+ </button>
78
+ </header>
79
+
80
+ <!-- Stats Cards -->
81
+ <div class="grid grid-cols-2 gap-4 mb-6">
82
+ <div class="bg-white p-4 rounded-lg shadow">
83
+ <p class="text-gray-500 text-sm">Today</p>
84
+ <h3 class="text-xl font-bold" id="todayCount">0</h3>
85
+ </div>
86
+ <div class="bg-white p-4 rounded-lg shadow">
87
+ <p class="text-gray-500 text-sm">Upcoming</p>
88
+ <h3 class="text-xl font-bold" id="upcomingCount">0</h3>
89
+ </div>
90
+ </div>
91
+
92
+ <!-- Filter Tabs -->
93
+ <div class="flex mb-4 bg-white rounded-lg shadow overflow-hidden">
94
+ <button class="filter-tab flex-1 py-2 px-4 text-center font-medium" data-filter="all">All</button>
95
+ <button class="filter-tab flex-1 py-2 px-4 text-center font-medium" data-filter="upcoming">Upcoming</button>
96
+ <button class="filter-tab flex-1 py-2 px-4 text-center font-medium" data-filter="completed">Completed</button>
97
+ </div>
98
+
99
+ <!-- Search -->
100
+ <div class="relative mb-6">
101
+ <input type="text" id="searchInput" placeholder="Search clients..."
102
+ class="w-full p-3 pl-10 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500">
103
+ <i class="fas fa-search absolute left-3 top-3.5 text-gray-400"></i>
104
+ </div>
105
+
106
+ <!-- Appointments List -->
107
+ <div id="appointmentsList" class="space-y-3 max-h-[60vh] overflow-y-auto">
108
+ <!-- Appointments will be loaded here -->
109
+ </div>
110
+
111
+ <!-- Empty State -->
112
+ <div id="emptyState" class="text-center py-10 hidden">
113
+ <i class="fas fa-calendar-times text-4xl text-gray-300 mb-4"></i>
114
+ <h3 class="text-lg font-medium text-gray-500">No appointments found</h3>
115
+ <p class="text-gray-400">Add a new appointment by clicking the + button</p>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Add/Edit Appointment Modal -->
120
+ <div id="appointmentModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 hidden">
121
+ <div class="bg-white rounded-lg w-full max-w-md max-h-[90vh] overflow-y-auto">
122
+ <div class="p-4 border-b border-gray-200 flex justify-between items-center">
123
+ <h3 class="text-lg font-medium" id="modalTitle">Add New Appointment</h3>
124
+ <button id="closeModal" class="text-gray-500 hover:text-gray-700">
125
+ <i class="fas fa-times"></i>
126
+ </button>
127
  </div>
128
+ <form id="appointmentForm" class="p-4 space-y-4">
129
+ <input type="hidden" id="appointmentId">
130
  <div>
131
+ <label for="clientName" class="block text-sm font-medium text-gray-700 mb-1">Client Name</label>
132
+ <input type="text" id="clientName" required
133
+ class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500">
134
  </div>
135
  <div>
136
+ <label for="clientPhone" class="block text-sm font-medium text-gray-700 mb-1">Phone Number</label>
137
+ <input type="tel" id="clientPhone"
138
+ class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500">
139
  </div>
140
  <div>
141
+ <label for="appointmentDate" class="block text-sm font-medium text-gray-700 mb-1">Date</label>
142
+ <input type="date" id="appointmentDate" required
143
+ class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500">
 
144
  </div>
145
+ <div>
146
+ <label for="appointmentTime" class="block text-sm font-medium text-gray-700 mb-1">Time</label>
147
+ <input type="time" id="appointmentTime" required
148
+ class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </div>
150
+ <div>
151
+ <label for="appointmentNotes" class="block text-sm font-medium text-gray-700 mb-1">Notes</label>
152
+ <textarea id="appointmentNotes" rows="3"
153
+ class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  </div>
155
+ <div class="flex items-center">
156
+ <input type="checkbox" id="isCompleted" class="custom-checkbox mr-2">
157
+ <label for="isCompleted" class="text-sm font-medium text-gray-700">Completed</label>
158
  </div>
159
+ <div class="flex space-x-3 pt-2">
160
+ <button type="submit" class="flex-1 bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 transition">
161
+ Save
162
+ </button>
163
+ <button type="button" id="cancelBtn" class="flex-1 bg-gray-200 text-gray-700 py-2 px-4 rounded-md hover:bg-gray-300 transition">
164
+ Cancel
 
165
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  </div>
167
+ </form>
168
  </div>
169
  </div>
170
+
171
+ <!-- Notification -->
172
+ <div id="notification" class="fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg shadow-lg hidden notification">
173
+ <span id="notificationText">Appointment saved successfully!</span>
174
+ </div>
175
 
176
  <script>
177
+ document.addEventListener('DOMContentLoaded', function() {
178
+ // DOM Elements
179
+ const addBtn = document.getElementById('addBtn');
180
+ const appointmentModal = document.getElementById('appointmentModal');
181
+ const closeModal = document.getElementById('closeModal');
182
+ const cancelBtn = document.getElementById('cancelBtn');
183
+ const appointmentForm = document.getElementById('appointmentForm');
184
+ const appointmentsList = document.getElementById('appointmentsList');
185
+ const emptyState = document.getElementById('emptyState');
186
+ const searchInput = document.getElementById('searchInput');
187
+ const filterTabs = document.querySelectorAll('.filter-tab');
188
+ const todayCountEl = document.getElementById('todayCount');
189
+ const upcomingCountEl = document.getElementById('upcomingCount');
190
+ const notification = document.getElementById('notification');
191
+
192
+ // Current filter state
193
+ let currentFilter = 'all';
194
+ let currentSearch = '';
195
+
196
+ // Initialize the app
197
+ initApp();
198
+
199
+ // Event Listeners
200
+ addBtn.addEventListener('click', openAddModal);
201
+ closeModal.addEventListener('click', closeModalFunc);
202
+ cancelBtn.addEventListener('click', closeModalFunc);
203
+ appointmentForm.addEventListener('submit', saveAppointment);
204
+ searchInput.addEventListener('input', function(e) {
205
+ currentSearch = e.target.value.toLowerCase();
206
+ renderAppointments();
207
+ });
208
 
209
+ filterTabs.forEach(tab => {
210
+ tab.addEventListener('click', function() {
211
+ filterTabs.forEach(t => t.classList.remove('bg-indigo-600', 'text-white'));
212
+ this.classList.add('bg-indigo-600', 'text-white');
213
+ currentFilter = this.dataset.filter;
214
+ renderAppointments();
215
+ });
216
+ });
217
+
218
+ // Set first tab as active
219
+ filterTabs[0].classList.add('bg-indigo-600', 'text-white');
220
+
221
+ // Functions
222
+ function initApp() {
223
+ // Check if appointments exist in localStorage
224
+ if (!localStorage.getItem('appointments')) {
225
+ // Add some sample data if empty
226
+ const sampleAppointments = [
227
+ {
228
+ id: Date.now().toString(),
229
+ clientName: 'John Doe',
230
+ clientPhone: '555-1234',
231
+ date: getFormattedDate(new Date()),
232
+ time: '10:00',
233
+ notes: 'First consultation',
234
+ completed: false,
235
+ createdAt: new Date().toISOString()
236
+ },
237
+ {
238
+ id: (Date.now() + 1).toString(),
239
+ clientName: 'Jane Smith',
240
+ clientPhone: '555-5678',
241
+ date: getFormattedDate(new Date(new Date().setDate(new Date().getDate() + 1))),
242
+ time: '14:30',
243
+ notes: 'Follow-up appointment',
244
+ completed: false,
245
+ createdAt: new Date().toISOString()
246
+ }
247
+ ];
248
+ localStorage.setItem('appointments', JSON.stringify(sampleAppointments));
249
+ }
250
+
251
+ renderAppointments();
252
+ updateStats();
253
  }
254
+
255
+ function openAddModal() {
256
+ document.getElementById('modalTitle').textContent = 'Add New Appointment';
257
+ document.getElementById('appointmentId').value = '';
258
+ document.getElementById('appointmentForm').reset();
259
+ document.getElementById('isCompleted').checked = false;
260
+
261
+ // Set default date to today
262
+ const today = new Date();
263
+ document.getElementById('appointmentDate').value = getFormattedDate(today);
264
+
265
+ // Set default time to next hour
266
+ const nextHour = today.getHours() + 1;
267
+ document.getElementById('appointmentTime').value = `${nextHour.toString().padStart(2, '0')}:00`;
268
+
269
+ appointmentModal.classList.remove('hidden');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  }
271
+
272
+ function openEditModal(appointment) {
273
+ document.getElementById('modalTitle').textContent = 'Edit Appointment';
274
+ document.getElementById('appointmentId').value = appointment.id;
275
+ document.getElementById('clientName').value = appointment.clientName;
276
+ document.getElementById('clientPhone').value = appointment.clientPhone;
277
+ document.getElementById('appointmentDate').value = appointment.date;
278
+ document.getElementById('appointmentTime').value = appointment.time;
279
+ document.getElementById('appointmentNotes').value = appointment.notes;
280
+ document.getElementById('isCompleted').checked = appointment.completed;
281
+
282
+ appointmentModal.classList.remove('hidden');
283
  }
284
+
285
+ function closeModalFunc() {
286
+ appointmentModal.classList.add('hidden');
287
+ }
288
+
289
+ function saveAppointment(e) {
290
+ e.preventDefault();
291
+
292
+ const appointments = JSON.parse(localStorage.getItem('appointments')) || [];
293
+ const appointmentId = document.getElementById('appointmentId').value;
294
+
295
+ const appointment = {
296
+ id: appointmentId || Date.now().toString(),
297
+ clientName: document.getElementById('clientName').value,
298
+ clientPhone: document.getElementById('clientPhone').value,
299
+ date: document.getElementById('appointmentDate').value,
300
+ time: document.getElementById('appointmentTime').value,
301
+ notes: document.getElementById('appointmentNotes').value,
302
+ completed: document.getElementById('isCompleted').checked,
303
+ createdAt: appointmentId ?
304
+ (appointments.find(a => a.id === appointmentId)?.createdAt || new Date().toISOString()) :
305
+ new Date().toISOString()
306
+ };
307
+
308
+ if (appointmentId) {
309
+ // Update existing appointment
310
+ const index = appointments.findIndex(a => a.id === appointmentId);
311
+ if (index !== -1) {
312
+ appointments[index] = appointment;
313
+ }
314
+ } else {
315
+ // Add new appointment
316
+ appointments.push(appointment);
317
  }
318
+
319
+ localStorage.setItem('appointments', JSON.stringify(appointments));
320
+
321
+ closeModalFunc();
322
+ renderAppointments();
323
+ updateStats();
324
+
325
+ // Show notification
326
+ showNotification('Appointment saved successfully!');
327
+ }
328
 
329
+ function deleteAppointment(id) {
330
+ if (confirm('Are you sure you want to delete this appointment?')) {
331
+ const appointments = JSON.parse(localStorage.getItem('appointments')) || [];
332
+ const updatedAppointments = appointments.filter(a => a.id !== id);
333
+ localStorage.setItem('appointments', JSON.stringify(updatedAppointments));
 
 
334
 
335
+ renderAppointments();
336
+ updateStats();
 
 
 
 
 
337
 
338
+ // Show notification
339
+ showNotification('Appointment deleted!');
340
+ }
341
+ }
342
+
343
+ function toggleCompleted(id) {
344
+ const appointments = JSON.parse(localStorage.getItem('appointments')) || [];
345
+ const appointment = appointments.find(a => a.id === id);
346
+ if (appointment) {
347
+ appointment.completed = !appointment.completed;
348
+ localStorage.setItem('appointments', JSON.stringify(appointments));
349
+
350
+ renderAppointments();
351
+ updateStats();
 
 
 
 
 
 
 
352
 
353
+ // Show notification
354
+ showNotification(`Appointment marked as ${appointment.completed ? 'completed' : 'pending'}!`);
355
+ }
356
+ }
357
+
358
+ function renderAppointments() {
359
+ const appointments = JSON.parse(localStorage.getItem('appointments')) || [];
360
+
361
+ // Filter appointments based on current filter and search
362
+ let filteredAppointments = appointments.filter(appointment => {
363
+ // Apply search filter
364
+ const matchesSearch = currentSearch === '' ||
365
+ appointment.clientName.toLowerCase().includes(currentSearch) ||
366
+ appointment.clientPhone.includes(currentSearch) ||
367
+ appointment.notes.toLowerCase().includes(currentSearch);
368
+
369
+ // Apply status filter
370
+ let matchesFilter = true;
371
+ if (currentFilter === 'upcoming') {
372
+ matchesFilter = !appointment.completed;
373
+ } else if (currentFilter === 'completed') {
374
+ matchesFilter = appointment.completed;
375
+ }
376
+
377
+ return matchesSearch && matchesFilter;
378
  });
379
 
380
+ // Sort appointments by date and time (upcoming first)
381
+ filteredAppointments.sort((a, b) => {
382
+ const dateA = new Date(`${a.date}T${a.time}`);
383
+ const dateB = new Date(`${b.date}T${b.time}`);
384
+ return dateA - dateB;
 
385
  });
 
 
 
 
 
 
 
 
386
 
387
+ // Clear the list
388
+ appointmentsList.innerHTML = '';
389
+
390
+ if (filteredAppointments.length === 0) {
391
+ emptyState.classList.remove('hidden');
392
+ } else {
393
+ emptyState.classList.add('hidden');
394
+
395
+ filteredAppointments.forEach(appointment => {
396
+ const appointmentDate = new Date(`${appointment.date}T${appointment.time}`);
397
+ const isToday = isSameDay(new Date(), appointmentDate);
398
+ const isPast = appointmentDate < new Date() && !isToday;
399
+
400
+ const appointmentEl = document.createElement('div');
401
+ appointmentEl.className = `bg-white rounded-lg shadow p-4 ${appointment.completed ? 'opacity-70' : ''} ${isPast && !appointment.completed ? 'border-l-4 border-red-500' : ''}`;
402
+
403
+ appointmentEl.innerHTML = `
404
+ <div class="flex justify-between items-start mb-2">
405
+ <div>
406
+ <h3 class="font-bold text-lg ${appointment.completed ? 'line-through' : ''}">${appointment.clientName}</h3>
407
+ <p class="text-gray-500 text-sm">${formatDate(appointment.date)} at ${appointment.time}</p>
408
+ </div>
409
+ <div class="flex space-x-2">
410
+ <button class="toggle-complete p-1 text-${appointment.completed ? 'green' : 'gray'}-500" data-id="${appointment.id}">
411
+ <i class="fas fa-${appointment.completed ? 'check-circle' : 'circle'}"></i>
412
+ </button>
413
+ <button class="edit-btn p-1 text-blue-500" data-id="${appointment.id}">
414
+ <i class="fas fa-edit"></i>
415
+ </button>
416
+ <button class="delete-btn p-1 text-red-500" data-id="${appointment.id}">
417
+ <i class="fas fa-trash"></i>
418
+ </button>
419
+ </div>
420
+ </div>
421
+ ${appointment.clientPhone ? `<p class="text-gray-600 mb-1"><i class="fas fa-phone mr-2"></i>${appointment.clientPhone}</p>` : ''}
422
+ ${appointment.notes ? `<p class="text-gray-600"><i class="fas fa-sticky-note mr-2"></i>${appointment.notes}</p>` : ''}
423
+ ${isToday ? `<span class="inline-block mt-2 px-2 py-1 bg-indigo-100 text-indigo-800 text-xs rounded-full">Today</span>` : ''}
424
+ `;
425
+
426
+ appointmentsList.appendChild(appointmentEl);
427
+ });
428
+
429
+ // Add event listeners to dynamically created buttons
430
+ document.querySelectorAll('.edit-btn').forEach(btn => {
431
+ btn.addEventListener('click', function() {
432
+ const id = this.dataset.id;
433
+ const appointments = JSON.parse(localStorage.getItem('appointments')) || [];
434
+ const appointment = appointments.find(a => a.id === id);
435
+ if (appointment) {
436
+ openEditModal(appointment);
437
+ }
438
+ });
439
+ });
440
+
441
+ document.querySelectorAll('.delete-btn').forEach(btn => {
442
+ btn.addEventListener('click', function() {
443
+ const id = this.dataset.id;
444
+ deleteAppointment(id);
445
+ });
446
+ });
447
+
448
+ document.querySelectorAll('.toggle-complete').forEach(btn => {
449
+ btn.addEventListener('click', function() {
450
+ const id = this.dataset.id;
451
+ toggleCompleted(id);
452
+ });
453
+ });
454
+ }
455
  }
 
 
 
 
 
 
 
 
 
 
456
 
457
+ function updateStats() {
458
+ const appointments = JSON.parse(localStorage.getItem('appointments')) || [];
459
+ const today = new Date().toISOString().split('T')[0];
460
+
461
+ const todayAppointments = appointments.filter(a => a.date === today && !a.completed).length;
462
+ const upcomingAppointments = appointments.filter(a => {
463
+ const appointmentDate = new Date(`${a.date}T${a.time}`);
464
+ return !a.completed && (new Date(a.date) > new Date() ||
465
+ (isSameDay(new Date(), appointmentDate) && appointmentDate > new Date()));
466
+ }).length;
467
+
468
+ todayCountEl.textContent = todayAppointments;
469
+ upcomingCountEl.textContent = upcomingAppointments;
470
  }
471
 
472
+ function showNotification(message) {
473
+ notificationText.textContent = message;
474
+ notification.classList.remove('hidden');
475
+
476
+ // Hide after 3 seconds
477
+ setTimeout(() => {
478
+ notification.classList.add('hidden');
479
+ }, 3000);
480
+ }
481
 
482
+ // Helper functions
483
+ function getFormattedDate(date) {
484
+ const year = date.getFullYear();
485
+ const month = String(date.getMonth() + 1).padStart(2, '0');
486
+ const day = String(date.getDate()).padStart(2, '0');
487
+ return `${year}-${month}-${day}`;
488
+ }
489
 
490
+ function formatDate(dateString) {
491
+ const options = { weekday: 'short', year: 'numeric', month: 'short', day: 'numeric' };
492
+ return new Date(dateString).toLocaleDateString(undefined, options);
493
+ }
 
 
 
 
 
 
 
 
 
494
 
495
+ function isSameDay(date1, date2) {
496
+ return date1.getFullYear() === date2.getFullYear() &&
497
+ date1.getMonth() === date2.getMonth() &&
498
+ date1.getDate() === date2.getDate();
499
+ }
500
  });
501
  </script>
502
  <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/awesome-app" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>