feat: enhance calendar loading experience with skeleton UI
This commit is contained in:
@@ -22,6 +22,14 @@ from Web.modules.database.settings import MongoClient
|
|||||||
from bson.objectid import ObjectId
|
from bson.objectid import ObjectId
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def _get_tenant_db(client):
|
||||||
|
try:
|
||||||
|
from tenant import get_tenant_db
|
||||||
|
return get_tenant_db(client)
|
||||||
|
except Exception:
|
||||||
|
return client[cfg.MONGODB_DB]
|
||||||
|
|
||||||
def _active_record_query(extra_query=None):
|
def _active_record_query(extra_query=None):
|
||||||
"""Build a query that excludes logically deleted records."""
|
"""Build a query that excludes logically deleted records."""
|
||||||
base_query = {'Deleted': {'$ne': True}}
|
base_query = {'Deleted': {'$ne': True}}
|
||||||
@@ -33,7 +41,7 @@ def _active_record_query(extra_query=None):
|
|||||||
def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False):
|
def add(date_start: str, date_end: str, time_span: list, slots: int, slot_lenght: int, user: str, mail: list=[], note:str="", calendar_enabled: bool=False):
|
||||||
try:
|
try:
|
||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
db = client[cfg.MONGODB_DB]
|
db = _get_tenant_db(client)
|
||||||
items = db['appointments']
|
items = db['appointments']
|
||||||
|
|
||||||
item = {
|
item = {
|
||||||
@@ -68,7 +76,7 @@ def get_item(id):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
db = client[cfg.MONGODB_DB]
|
db = _get_tenant_db(client)
|
||||||
items = db['appointments']
|
items = db['appointments']
|
||||||
item = items.find_one(_active_record_query({'_id': ObjectId(id)}))
|
item = items.find_one(_active_record_query({'_id': ObjectId(id)}))
|
||||||
client.close()
|
client.close()
|
||||||
@@ -90,7 +98,7 @@ def update(id,slots_used: list):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
db = client[cfg.MONGODB_DB]
|
db = _get_tenant_db(client)
|
||||||
items = db['appointments']
|
items = db['appointments']
|
||||||
|
|
||||||
update_data = {
|
update_data = {
|
||||||
@@ -124,7 +132,7 @@ def remove_slot(id, date_start_time, name):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
db = client[cfg.MONGODB_DB]
|
db = _get_tenant_db(client)
|
||||||
items = db['appointments']
|
items = db['appointments']
|
||||||
|
|
||||||
# Attempt to pull the exact element (stored as an array/tuple)
|
# Attempt to pull the exact element (stored as an array/tuple)
|
||||||
@@ -152,7 +160,7 @@ def remove(id):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||||
db = client[cfg.MONGODB_DB]
|
db = _get_tenant_db(client)
|
||||||
items = db['appointments']
|
items = db['appointments']
|
||||||
|
|
||||||
result = items.delete_one({'_id': ObjectId(id)})
|
result = items.delete_one({'_id': ObjectId(id)})
|
||||||
|
|||||||
@@ -41,7 +41,37 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="calendar"></div>
|
<div class="calendar-stage">
|
||||||
|
<div id="calendar-skeleton" class="calendar-skeleton" aria-hidden="true">
|
||||||
|
<div class="skeleton-toolbar">
|
||||||
|
<span class="skeleton-pill"></span>
|
||||||
|
<span class="skeleton-pill"></span>
|
||||||
|
<span class="skeleton-pill"></span>
|
||||||
|
</div>
|
||||||
|
<div class="skeleton-grid">
|
||||||
|
<div class="skeleton-day-header"></div>
|
||||||
|
<div class="skeleton-day-header"></div>
|
||||||
|
<div class="skeleton-day-header"></div>
|
||||||
|
<div class="skeleton-day-header"></div>
|
||||||
|
<div class="skeleton-day-header"></div>
|
||||||
|
<div class="skeleton-day-header"></div>
|
||||||
|
<div class="skeleton-day-header"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
<div class="skeleton-slot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="calendar"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Modal for new bookings -->
|
<!-- Modal for new bookings -->
|
||||||
<div id="booking-modal" class="modal">
|
<div id="booking-modal" class="modal">
|
||||||
@@ -254,7 +284,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Initialize calendar
|
// Initialize calendar
|
||||||
calendar = new FullCalendar.Calendar(calendarEl, {
|
calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
plugins: [FullCalendar.dayGridPlugin, FullCalendar.timeGridPlugin],
|
|
||||||
initialView: 'timeGridDay',
|
initialView: 'timeGridDay',
|
||||||
locale: 'de',
|
locale: 'de',
|
||||||
headerToolbar: false, // We're using our custom header
|
headerToolbar: false, // We're using our custom header
|
||||||
@@ -281,6 +310,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
hour12: false
|
hour12: false
|
||||||
},
|
},
|
||||||
|
loading: function(isLoading) {
|
||||||
|
const skeleton = document.getElementById('calendar-skeleton');
|
||||||
|
if (skeleton) {
|
||||||
|
skeleton.classList.toggle('is-hidden', !isLoading);
|
||||||
|
}
|
||||||
|
},
|
||||||
selectAllow: function(info) {
|
selectAllow: function(info) {
|
||||||
const startHour = info.start.getHours();
|
const startHour = info.start.getHours();
|
||||||
const endHour = info.end.getHours();
|
const endHour = info.end.getHours();
|
||||||
@@ -452,6 +487,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
// Render calendar
|
// Render calendar
|
||||||
try {
|
try {
|
||||||
|
const calendarSkeleton = document.getElementById('calendar-skeleton');
|
||||||
|
if (calendarSkeleton) {
|
||||||
|
calendarSkeleton.classList.remove('is-hidden');
|
||||||
|
}
|
||||||
calendar.render();
|
calendar.render();
|
||||||
setCalendarView(activeCalendarView);
|
setCalendarView(activeCalendarView);
|
||||||
|
|
||||||
@@ -1259,6 +1298,74 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-stage {
|
||||||
|
position: relative;
|
||||||
|
min-height: 620px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-skeleton {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 5;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(247, 250, 252, 0.96));
|
||||||
|
padding: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-skeleton.is-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-pill,
|
||||||
|
.skeleton-day-header,
|
||||||
|
.skeleton-slot {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(90deg, #e9eef5 0%, #f7f9fc 50%, #e9eef5 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeleton-shimmer 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-pill {
|
||||||
|
width: 90px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-day-header {
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-slot {
|
||||||
|
height: 72px;
|
||||||
|
border-radius: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes skeleton-shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-header {
|
.calendar-header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user