Last active
April 25, 2023 03:58
-
-
Save DarkMatterMatt/5f45b7e0d84dc1ce535edf2b0750f1d7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Usage: add a bookmarklet to the following link. Then, open Calven and click the bookmark to load the extension. | |
* javascript:fetch('https://gist.githubusercontent.com/DarkMatterMatt/5f45b7e0d84dc1ce535edf2b0750f1d7/raw').then(r => r.text()).then(eval) | |
*/ | |
(async () => { | |
if (!window.location.hostname.endsWith('.calven.com')) { | |
alert('Please run this bookmarklet on *.calven.com'); | |
return; | |
} | |
const { fb } = window; | |
const userId = fb.apps[1].auth().currentUser.uid; | |
const getToken = async () => fb.apps[1].auth().currentUser.getIdToken(); | |
const queryApi = async (url, postData) => { | |
const res = await fetch(`https://api.calven.app${url}`, { | |
method: postData && 'POST', | |
headers: { | |
Authorization: `Bearer ${await getToken()}`, | |
'Content-Type': postData && 'application/json', | |
}, | |
body: postData && JSON.stringify(postData), | |
}); | |
if (!res.ok) { | |
throw new Error(`Failed fetching: ${url}, [${res.status}]`); | |
} | |
return res.json(); | |
}; | |
// State loading/saving | |
const DAY = 1000 * 60 * 60 * 24; | |
const STATE_KEY = '__MM_Calven'; | |
const log = (...args) => console.log('!!MM', ...args); | |
const loadState = () => JSON.parse(localStorage.getItem(STATE_KEY)) || { | |
version: 1, | |
}; | |
const state = loadState(); | |
const saveState = () => localStorage.setItem(STATE_KEY, JSON.stringify(state)); | |
// Load user details | |
const userDetails = await queryApi(`/users/${userId}`); | |
log(`Loaded userDetails for userId: ${userId}`, userDetails); | |
// Element creation | |
const $header = document.createElement('div'); | |
$header.style.position = 'fixed'; | |
$header.style.top = '0'; | |
$header.style.left = '0'; | |
$header.style.width = '100%'; | |
$header.style.zIndex = '9999999999'; | |
$header.style.backgroundColor = '#ffe6ff'; | |
$header.style.display = 'flex'; | |
$header.style.justifyContent = 'center'; | |
$header.style.alignItems = 'center'; | |
$header.style.gap = '8px'; | |
$header.innerText = 'Loading'; | |
document.body.prepend($header); | |
const $locationSelect = document.createElement('select'); | |
const $levelSelect = document.createElement('select'); | |
const $placeSelect = document.createElement('select'); | |
const $button = document.createElement('button'); | |
// Loaders | |
const populateDropdown = ($select, options) => { | |
$select.children.length = 0; | |
for (const option of [{ name: 'SELECT', value: '' }, ...options]) { | |
const $option = document.createElement('option'); | |
$option.innerText = option.name; | |
$option.value = option.value; | |
$select.appendChild($option); | |
} | |
}; | |
const populateLocations = async () => { | |
const locations = await queryApi('/locations') | |
.then((j) => j.data) | |
.then((locations) => locations.sort((a, b) => { | |
if (a.countryCode !== b.countryCode) { | |
return a.countryCode.localeCompare(b.countryCode); | |
} | |
return a.address.localeCompare(b.address); | |
})); | |
log('Loaded locations', locations); | |
populateDropdown($locationSelect, locations.map((l) => ({ name: l.name, value: l.locationId }))); | |
saveState(); | |
}; | |
const populateLevels = async (locationId) => { | |
state.locationId = locationId; | |
const levels = await queryApi(`/locations/${state.locationId}/levels`) | |
.then((j) => j.data) | |
.then((levels) => levels.sort((a, b) => a.name.localeCompare(b.name))); | |
log(`Loaded levels for location: ${state.locationId}`, levels); | |
populateDropdown($levelSelect, levels.map((l) => ({ name: l.name, value: l.levelId }))); | |
saveState(); | |
}; | |
const populatePlaces = async (levelId) => { | |
state.levelId = levelId; | |
const zones = await queryApi(`/locations/${state.locationId}/levels/${state.levelId}/zones`) | |
.then((j) => j.data) | |
.then((zones) => zones.sort((a, b) => a.name.localeCompare(b.name))); | |
log(`Loaded zones for level: ${state.levelId}`, zones); | |
const places = []; | |
for (const zone of zones) { | |
const { zoneId } = zone; | |
const zonePlaces = await queryApi(`/locations/${state.locationId}/levels/${state.levelId}/zones/${zoneId}/places?limit=1000`) | |
.then((j) => j.data) | |
.then((place) => place.sort((a, b) => a.name.localeCompare(b.name))); | |
log(`Loaded places for zone: ${zone.zoneId}`, zonePlaces); | |
places.push(...zonePlaces.map((p) => ({ ...p, zoneId }))); | |
} | |
populateDropdown($placeSelect, places.map((p) => ({ name: p.name, value: `${p.zoneId}:${p.placeId}` }))); | |
saveState(); | |
}; | |
const setPlaceId = (zoneId, placeId) => { | |
state.zoneId = zoneId; | |
state.placeId = placeId; | |
saveState(); | |
}; | |
const toISOString = (date) => { | |
date = `${date}`; | |
return `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)}T00:00:00.000Z`; | |
}; | |
const toCalvenString = (date) => new Date(date).toISOString().split('T')[0].replace(/-/g, ''); | |
const queryCurrentBookings = async () => { | |
const params = new URLSearchParams({ | |
start: toCalvenString(Date.now() - DAY), | |
end: toCalvenString(Date.now() + 365 * DAY), | |
}); | |
const results = await queryApi(`/users/${userId}/plans?${params}`); | |
return results.data.map(({ day, ...rest }) => ({ | |
day: new Date(toISOString(day)), | |
...rest, | |
})); | |
}; | |
const makeBooking = async (locationId, zoneId, placeId, date) => { | |
await queryApi(`/locations/${locationId}/locationPlans/${toCalvenString(date)}/zonePlans/${zoneId}/placeBookings`, { | |
day: toCalvenString(date), | |
placeId, | |
uid: userId, | |
}); | |
}; | |
const makeBookings = async (locationId, zoneId, placeId, date) => { | |
let lastSuccessful = date; | |
while (+date < +lastSuccessful + 7 * DAY) { | |
try { | |
await makeBooking(locationId, zoneId, placeId, date); | |
log('Made booking for', date); | |
lastSuccessful = date; | |
} catch { | |
// Ignore errors. | |
} | |
date = new Date(+date + DAY); | |
} | |
log('Finished making bookings until', lastSuccessful); | |
}; | |
// Initialization | |
await populateLocations(); | |
if (state.locationId) { | |
$locationSelect.value = state.locationId; | |
await populateLevels(state.locationId); | |
if (state.levelId) { | |
$levelSelect.value = state.levelId; | |
await populatePlaces(state.levelId); | |
if (state.zoneId && state.placeId) { | |
$placeSelect.value = `${state.zoneId}:${state.placeId}`; | |
setPlaceId(state.zoneId, state.placeId); | |
} | |
} | |
} | |
const currentBookings = await queryCurrentBookings(); | |
log('Loaded current bookings', currentBookings); | |
const nextBookingDate = new Date(Math.max(...currentBookings.map((b) => +b.day)) + DAY); | |
log('Next booking date', nextBookingDate); | |
$button.innerText = `Book from ${nextBookingDate.toISOString().split('T')[0]}`; | |
// Event listeners | |
$locationSelect.addEventListener('change', (ev) => populateLevels(ev.target.value)); | |
$levelSelect.addEventListener('change', (ev) => populatePlaces(ev.target.value)); | |
$placeSelect.addEventListener('change', (ev) => setPlaceId(...ev.target.value.split(':'))); | |
$button.addEventListener( | |
'click', | |
() => { | |
$button.disabled = true; | |
makeBookings(state.locationId, state.zoneId, state.placeId, nextBookingDate); | |
}, | |
{ once: true }, | |
); | |
$header.innerText = ''; | |
$header.appendChild($locationSelect); | |
$header.appendChild($levelSelect); | |
$header.appendChild($placeSelect); | |
$header.appendChild($button); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment