Use modal

This commit is contained in:
Mork Swork 2026-05-12 18:33:59 -07:00
parent 1a5ece3f7d
commit 269813c12d
6 changed files with 288 additions and 25 deletions

View file

@ -0,0 +1,39 @@
holidays:
- date: '2026-01-01'
name: "New Year's Day"
- date: '2026-01-19'
name: 'Martin Luther King Jr. Day'
- date: '2026-02-14'
name: "Valentine's Day"
- date: '2026-02-16'
name: "Presidents' Day"
- date: '2026-03-17'
name: "St. Patrick's Day"
- date: '2026-04-05'
name: 'Easter Sunday'
- date: '2026-05-10'
name: "Mother's Day"
- date: '2026-05-25'
name: 'Memorial Day'
- date: '2026-06-19'
name: 'Juneteenth'
- date: '2026-06-21'
name: "Father's Day"
- date: '2026-07-04'
name: 'Independence Day'
- date: '2026-08-03'
name: 'Summer Bank Holiday'
- date: '2026-09-07'
name: 'Labor Day'
- date: '2026-10-12'
name: 'Columbus Day'
- date: '2026-10-31'
name: 'Halloween'
- date: '2026-11-11'
name: "Veterans Day"
- date: '2026-11-26'
name: 'Thanksgiving'
- date: '2026-12-25'
name: 'Christmas'
- date: '2026-12-31'
name: "New Year's Eve"

View file

@ -0,0 +1,17 @@
riverside_pt.settings:
type: config_object
label: 'Riverside PT Settings'
mapping:
holidays:
type: sequence
label: 'Holidays'
sequence:
type: mapping
label: 'Holiday'
mapping:
date:
type: string
label: 'Date (YYYY-MM-DD)'
name:
type: string
label: 'Holiday name'

View file

@ -0,0 +1,89 @@
#riverside-calendar {
max-width: 480px;
font-size: 0.8rem;
}
#riverside-booking-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 999;
}
#riverside-booking-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 340px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 1rem;
z-index: 1000;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
}
.riverside-booking-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
font-weight: 600;
}
#riverside-booking-close {
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
line-height: 1;
padding: 0;
color: #6b7280;
}
#riverside-booking-slots {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
#riverside-booking-slots li {
padding: 0.5rem 0.75rem;
border: 1px solid #3b82f6;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
color: #1d4ed8;
}
#riverside-booking-slots li:hover {
background: #eff6ff;
}
#riverside-calendar .fc-more-popover {
display: none;
}
#riverside-calendar .is-holiday .fc-more-link {
display: none;
}
#riverside-calendar .fc-day-other .riverside-holiday-label,
#riverside-calendar .fc-day-other .fc-more-link {
opacity: 0.4;
}
#riverside-calendar .riverside-holiday-label {
font-size: 0.65rem;
color: #b45309;
text-align: center;
padding-bottom: 2px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

View file

@ -4,8 +4,73 @@
if (!el) return; if (!el) return;
const calendar = new FullCalendar.Calendar(el, { const calendar = new FullCalendar.Calendar(el, {
initialView: 'timeGridWeek', initialView: 'dayGridMonth',
headerToolbar: { left: 'prev', center: 'title', right: 'next' },
validRange: function (now) {
return {
start: new Date(now.getFullYear(), now.getMonth(), 1),
end: new Date(now.getFullYear(), now.getMonth() + 7, 1),
};
},
fixedWeekCount: false,
height: 'auto',
events: drupalSettings.riversidePt.eventsUrl, events: drupalSettings.riversidePt.eventsUrl,
eventBackgroundColor: '#3b82f6',
eventBorderColor: '#3b82f6',
dayMaxEvents: 0,
moreLinkContent: function (arg) {
return arg.num + ' slot' + (arg.num !== 1 ? 's' : '');
},
dayCellClassNames: function (arg) {
const date = arg.date.toISOString().substring(0, 10);
if (drupalSettings.riversidePt.holidays[date]) return ['is-holiday'];
},
dayCellDidMount: function (arg) {
const date = arg.date.toISOString().substring(0, 10);
const holiday = drupalSettings.riversidePt.holidays[date];
if (!holiday) return;
const label = document.createElement('div');
label.className = 'riverside-holiday-label';
label.textContent = holiday;
arg.el.appendChild(label);
},
moreLinkClick: function (arg) {
arg.jsEvent.preventDefault();
arg.jsEvent.stopPropagation();
const date = arg.date.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
panelDate.textContent = date;
panelSlots.innerHTML = '';
arg.allSegs.forEach(function (seg) {
const li = document.createElement('li');
const start = seg.event.start.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
const end = seg.event.end.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
li.textContent = start + ' ' + end;
panelSlots.appendChild(li);
});
openPanel();
return false;
},
});
const panel = document.getElementById('riverside-booking-panel');
const backdrop = document.getElementById('riverside-booking-backdrop');
const panelDate = document.getElementById('riverside-booking-date');
const panelSlots = document.getElementById('riverside-booking-slots');
function closePanel() {
panel.hidden = true;
backdrop.hidden = true;
}
function openPanel() {
backdrop.hidden = false;
panel.hidden = false;
}
document.getElementById('riverside-booking-close').addEventListener('click', closePanel);
backdrop.addEventListener('click', closePanel);
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closePanel();
}); });
calendar.render(); calendar.render();

View file

@ -1,4 +1,7 @@
schedule: schedule:
css:
theme:
css/calendar.css: {}
js: js:
js/fullcalendar.min.js: { minified: true } js/fullcalendar.min.js: { minified: true }
js/calendar.js: {} js/calendar.js: {}

View file

@ -4,7 +4,6 @@ namespace Drupal\riverside_pt\Controller;
use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Url; use Drupal\Core\Url;
use Drupal\node\Entity\Node;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@ -12,47 +11,98 @@ class ScheduleController extends ControllerBase {
public function page(): array { public function page(): array {
return [ return [
'#type' => 'html_tag', '#type' => 'container',
'#tag' => 'div', 'intro' => [
'#attributes' => ['id' => 'riverside-calendar'], '#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->t('View provider availability below. Use the calendar to browse open appointment slots by week.'),
],
'calendar' => [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => ['id' => 'riverside-calendar'],
],
'booking_backdrop' => [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => ['id' => 'riverside-booking-backdrop', 'hidden' => TRUE],
'#value' => '',
],
'booking_panel' => [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => ['id' => 'riverside-booking-panel', 'hidden' => TRUE],
'header' => [
'#type' => 'html_tag',
'#tag' => 'div',
'#attributes' => ['class' => ['riverside-booking-header']],
'title' => [
'#type' => 'html_tag',
'#tag' => 'span',
'#attributes' => ['id' => 'riverside-booking-date'],
'#value' => '',
],
'close' => [
'#type' => 'html_tag',
'#tag' => 'button',
'#attributes' => ['id' => 'riverside-booking-close', 'type' => 'button'],
'#value' => $this->t('✕'),
],
],
'slots' => [
'#type' => 'html_tag',
'#tag' => 'ul',
'#attributes' => ['id' => 'riverside-booking-slots'],
'#value' => '',
],
],
'#attached' => [ '#attached' => [
'library' => ['riverside_pt/schedule'], 'library' => ['riverside_pt/schedule'],
'drupalSettings' => [ 'drupalSettings' => [
'riversidePt' => [ 'riversidePt' => [
'eventsUrl' => Url::fromRoute('riverside_pt.schedule_events')->toString(), 'eventsUrl' => Url::fromRoute('riverside_pt.schedule_events')->toString(),
'holidays' => $this->buildHolidaysMap(),
], ],
], ],
], ],
]; ];
} }
private function buildHolidaysMap(): array {
$holidays = $this->config('riverside_pt.settings')->get('holidays') ?? [];
$map = [];
foreach ($holidays as $holiday) {
$map[$holiday['date']] = $holiday['name'];
}
return $map;
}
public function events(Request $request): JsonResponse { public function events(Request $request): JsonResponse {
$start = $request->query->get('start'); $start = $request->query->get('start');
$end = $request->query->get('end'); $end = $request->query->get('end');
$query = \Drupal::entityQuery('node') $current = new \DateTime($start ?? 'now');
->condition('type', 'provider_availability') $until = new \DateTime($end ?? 'now');
->condition('status', 1) $events = [];
->accessCheck(TRUE); $id = 1;
if ($start) { while ($current < $until) {
$query->condition('field_end_datetime', $start, '>='); $i = (int) floor($current->getTimestamp() / 86400);
} $count = ($i % 5 + $i % 7 + $i % 11) % 6;
if ($end) { for ($n = 0; $n < $count; $n++) {
$query->condition('field_start_datetime', $end, '<='); $slot = clone $current;
$slot->setTime(9 + $n, 0);
$events[] = [
'id' => $id++,
'title' => 'Available',
'start' => $slot->format('Y-m-d\TH:i:s'),
'end' => (clone $slot)->modify('+1 hour')->format('Y-m-d\TH:i:s'),
];
}
$current->modify('+1 day');
} }
$nids = $query->execute(); return new JsonResponse($events);
$nodes = Node::loadMultiple($nids);
$events = array_map(fn(Node $node) => [
'id' => $node->id(),
'title' => $node->field_provider->entity?->getDisplayName() ?? 'Provider',
'start' => $node->field_start_datetime->value,
'end' => $node->field_end_datetime->value,
], $nodes);
return new JsonResponse(array_values($events));
} }
} }