Initial open source release

This commit is contained in:
Ronni Skansing
2025-08-21 16:14:09 +02:00
commit 11cf01f08e
488 changed files with 97180 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
// max number of items to fetch per request
const global_pagination_max = 1000;
/**
* @callback FetchFn
* @param {TableURLParams} options
* @returns {Promise<import("$lib/api/client").ApiResponse>}
* @async
*/
/**
* TableURLParams is a type that represents the query parameters for a table.
*
* @typedef {object} TableURLParams
* @property {number} currentPage
* @property {number} perPage
* @property {string} sortBy
* @property {string} sortOrder
* @property {string} search
*/
/**
* @param {FetchFn} fetchFn2
* @param {TableURLParams} options
* @returns {Promise<Object[]>}
*/
export const defaultOptions = {
currentPage: 1,
perPage: global_pagination_max,
sortBy: 'name',
sortOrder: 'asc',
search: ''
};
export const fetchAllRows = async (fetchFn2, options = defaultOptions) => {
let items = [];
let res = await fetchFn2(options);
// Add initial rows
if (res.data?.rows) {
items = items.concat(res.data.rows);
}
while (res.data?.hasNextPage) {
// Update the page number
options.currentPage += 1;
// Fetch next page
res = await fetchFn2({ ...options });
// Add new rows
if (res.data?.rows) {
items = items.concat(res.data.rows);
}
}
return items;
};
/**
* @param {Date} date
* @returns {string}
*/
export const utc_yyyy_mm_dd = (date) => {
return date.toISOString().split('T')[0];
};
export const local_yyyy_mm_dd = (date) => {
return (
date.getFullYear() +
'-' +
String(date.getMonth() + 1).padStart(2, '0') +
'-' +
String(date.getDate()).padStart(2, '0')
);
};
// converts a local time formatted as "HH:MM" to a UTC time formatted as "HH:MM"
export function localTimeToUTC(localTime) {
// Split the local time string into hours and minutes
const [hours, minutes] = localTime.split(':').map(Number);
// Get the current date
const now = new Date();
// Create a new Date object with the current date and the local time
const localDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes);
// Get the UTC time components
const utcHours = localDate.getUTCHours();
const utcMinutes = localDate.getUTCMinutes();
// Format the UTC time as a string
const utcTime = `${String(utcHours).padStart(2, '0')}:${String(utcMinutes).padStart(2, '0')}`;
return utcTime;
}
/**
* Converts a UTC time formatted as "HH:MM" to a local time formatted as "HH:MM"
* @param {string} utcTime - The UTC time to convert
* @returns {string} - The local time formatted as "HH:MM"
*/
export function utcTimeToLocal(utcTime) {
if (!utcTime) {
return '';
}
// Split the UTC time string into hours and minutes
const [hours, minutes] = utcTime.split(':').map(Number);
// Get the current date
const now = new Date();
// Create a new Date object with the current date and the UTC time
const utcDate = new Date(
Date.UTC(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes)
);
// Get the local time components
const localHours = utcDate.getHours();
const localMinutes = utcDate.getMinutes();
// Format the local time as a string
const localTime = `${String(localHours).padStart(2, '0')}:${String(localMinutes).padStart(2, '0')}`;
return localTime;
}
/**
* Compares two times formatted as "HH:MM" to determine if the first is larger than the second.
* @param {string} time1 - The first time to compare
* @param {string} time2 - The second time to compare
* @returns {boolean} - True if time1 is larger than time2, false otherwise
*/
export function isTimeLarger(time1, time2) {
// Split the time strings into hours and minutes
const [hours1, minutes1] = time1.split(':').map(Number);
const [hours2, minutes2] = time2.split(':').map(Number);
// Compare hours first
if (hours1 > hours2) {
return true;
} else if (hours1 < hours2) {
return false;
}
// If hours are equal, compare minutes
return minutes1 >= minutes2;
}
+48
View File
@@ -0,0 +1,48 @@
import { addToast } from '$lib/store/toast';
export const getModalText = (name, mode) => {
let t = '';
switch (mode) {
case 'create':
t = `New ${name}`;
break;
case 'copy':
t = `New ${name}`;
break;
case 'update':
t = `Update ${name}`;
break;
}
return t;
};
export const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
func(...args);
}, delay);
};
};
export const debounceTyping = (func) => debounce(func, 350);
export const shouldHideMenuItem = (route) => {
// All menu items are now accessible - no edition restrictions
return false;
};
export const onClickCopy = (text) => {
navigator.clipboard
.writeText(text)
.then(() => {
addToast('Copied to clipboard', 'Success');
})
.catch((err) => {
addToast('Failed to copy to clipboard', 'Error');
console.error('failed to copy to clipboard', err);
});
};
+52
View File
@@ -0,0 +1,52 @@
import papaparse from 'papaparse';
/**
* Parse CSV file to recipients
* @param {File} file - CSV file
* @returns {Promise<Array<*>>}
**/
export const parseCSVToRecipients = async (file) => {
const p = new Promise((resolve, reject) => {
const recipients = {};
papaparse.parse(file, {
header: true,
skipEmptyLines: true,
complete: (results) => {
if (results.errors) {
console.info('CSV import errors', results.errors);
}
if (!results.data) {
reject('No data found in CSV file');
return;
}
// lowercased map of headers
const fieldsMap = {};
for (let i = 0; i < results.meta.fields.length; i++) {
const field = results.meta.fields[i];
fieldsMap[field.toLowerCase()] = field;
}
results.data.forEach((row) => {
const email = row[fieldsMap['email']];
if (!email) {
return;
}
recipients[email] = {
email: email,
phone: row[fieldsMap['phone']] ?? null,
extraIdentifier: row[fieldsMap['extraIdentifier'.toLocaleLowerCase()]] ?? null,
firstName: row[fieldsMap['firstname']] ?? null,
lastName: row[fieldsMap['lastname']] ?? null,
position: row[fieldsMap['position']] ?? null,
department: row[fieldsMap['department']] ?? null,
city: row[fieldsMap['city']] ?? null,
country: row[fieldsMap['country']] ?? null,
misc: row[fieldsMap['misc']] ?? null
};
});
resolve(Object.values(recipients));
}
});
});
return await p;
};
+65
View File
@@ -0,0 +1,65 @@
import { writable } from 'svelte/store';
const DAYS = [
{ short: 'Sun', full: 'Sunday', num: '1' },
{ short: 'Mon', full: 'Monday', num: '2' },
{ short: 'Tue', full: 'Tuesday', num: '3' },
{ short: 'Wed', full: 'Wednesday', num: '4' },
{ short: 'Thu', full: 'Thursday', num: '5' },
{ short: 'Fri', full: 'Friday', num: '6' },
{ short: 'Sat', full: 'Saturday', num: '7' }
];
export const timeFormat = writable(false); // false = 12h, true = 24h
export const formatWeekDays = (binaryDays) => {
const days = DAYS.map((day) => ({
...day,
isActive: !!(binaryDays & (1 << DAYS.indexOf(day)))
}));
const activeDays = days.filter((d) => d.isActive);
const isWeekdaysOnly =
activeDays.length === 5 &&
!days[0].isActive && // Sunday inactive
!days[6].isActive; // Saturday inactive
const isAllDays = activeDays.length === 7;
return {
days,
summary: isAllDays ? 'Every day' : isWeekdaysOnly ? 'Weekdays only' : null
};
};
export const formatTimeConstraint = (time, use24Hour = false) => {
if (!time) return '';
const [hours, minutes] = time.split(':').map(Number);
if (use24Hour) {
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
} else {
const h = hours % 12 || 12;
const ampm = hours >= 12 ? 'PM' : 'AM';
return `${h}:${minutes.toString().padStart(2, '0')} ${ampm}`;
}
};
export const isCurrentlyActive = (startTime, endTime, activeDays) => {
if (!startTime || !endTime || !activeDays) return false;
const now = new Date();
const currentDay = now.getDay(); // 0-6
const currentTime = now.getHours() * 60 + now.getMinutes();
const [startHour, startMin] = startTime.split(':').map(Number);
const [endHour, endMin] = endTime.split(':').map(Number);
const scheduleStart = startHour * 60 + startMin;
const scheduleEnd = endHour * 60 + endMin;
const isDayActive = !!(activeDays & (1 << currentDay));
const isTimeActive = currentTime >= scheduleStart && currentTime <= scheduleEnd;
return isDayActive && isTimeActive;
};
+45
View File
@@ -0,0 +1,45 @@
// map between system event name and a human readable event name
const eventNameMap = {
// campaign recipient events
campaign_recipient_scheduled: { name: 'Scheduled', priority: 10, color: 'bg-scheduled' },
campaign_recipient_cancelled: { name: 'Cancelled', priority: 15, color: 'bg-black' },
campaign_recipient_message_sent: { name: 'Message Sent', priority: 30, color: 'bg-message-sent' },
campaign_recipient_message_failed: {
name: 'Failed Sending',
priority: 80,
color: 'bg-failed-sending'
},
campaign_recipient_message_read: { name: 'Message Read', priority: 40, color: 'bg-message-read' },
campaign_recipient_before_page_visited: {
name: 'Before Page Visited',
priority: 50,
color: 'bg-before-page-visited'
},
campaign_recipient_page_visited: { name: 'Page Visited', priority: 60, color: 'bg-page-visited' },
campaign_recipient_after_page_visited: {
name: 'After Page Visited',
priority: 70,
color: 'bg-after-page-visited'
},
campaign_recipient_submitted_data: {
name: 'Submitted Data',
priority: 90,
color: 'bg-submitted-data'
},
// campaign events
campaign_scheduled: { name: 'Scheduled', priority: 10 },
campaign_active: { name: 'Active', priority: 20 },
campaign_self_managed: { name: 'Self managed', priority: 20 },
campaign_closed: { name: 'Closed', priority: 30, color: 'bg-closed' }
};
/**
* @param {string} systemEventName
* @returns {{name: string, priority: number, color: string}}
*/
export const toEvent = (systemEventName) => {
if (systemEventName === '') {
return { name: '', priority: 0, color: '' };
}
return eventNameMap[systemEventName] ?? { name: 'Unknown Event', priority: 80, color: '' };
};
+104
View File
@@ -0,0 +1,104 @@
/**
* Resets a collection of form elements to their default values
* or empty values.
* @param {{[key: string]: FormElement}} formElementsMap
*/
export const resetForm = (formElementsMap) => {
const elements = Object.values(formElementsMap);
for (let i = 0; i < elements.length; i++) {
const formElement = elements[i];
if (formElement.element.type === 'checkbox') {
if (formElement.default) {
formElement.element.checked = !!formElement.default;
continue;
}
formElement.element.checked = false;
continue;
}
if (
formElement.element.type === 'text' ||
formElement.element.type === 'textarea' ||
formElement.element.type === 'password'
) {
if (formElement.default) {
formElement.element.value = formElement.default.toString();
continue;
}
formElement.element.value = '';
continue;
}
}
};
/**
* @typedef FormElement
* @type {{element: HTMLInputElement, default: string|boolean|null, value: string, checked: boolean, _element: HTMLInputElement }}}
*/
/**
* @typedef FormElementMap
* @type {{[key: string]: FormElement}}
*/
/**
* A wrapper for a form elements that includes the default value
*
* @param {string|boolean|null} defaultValue
* @returns {FormElement}
*/
export const newFormElement = (defaultValue = null) => {
return {
default: defaultValue,
// dont use this directly
_element: null,
// use directly instead
get element() {
return this._element;
},
set element(element) {
this._element = element;
if (this._element.type === 'checkbox') {
this.default = !!this._element.checked;
}
if (
this._element.type === 'text' ||
this._element.type === 'textarea' ||
this._element.type === 'password'
) {
this.default = this._element.value;
}
},
/**
* shortcut for element.value
*
* @returns {string}
*/
get value() {
return this.element.value ?? '';
},
/**
* shortcut for element.checked
* @returns {boolean}
*/
get checked() {
return !!this.element.checked;
}
};
};
export const buttonDisabledAttributes = (element, attribute, reason) => {
if (!element[attribute]) {
return { disabled: true, title: reason };
}
return { disabled: false, title: '' };
};
export const globalButtonDisabledAttributes = (element, context) => {
if (context) {
return buttonDisabledAttributes(element, 'companyID', 'Only available in shared view.');
}
return { disabled: false, title: '' };
};
+103
View File
@@ -0,0 +1,103 @@
export class BiMap {
#keys = [];
#values = [];
/**
* BiMap is a two-way map that allows you to look up values by key, and keys by value.
* Can only accept maps with unique values.
*
* @param {{[key: string]: string}} map
*/
constructor(map) {
// Check for duplicate values
const values = Object.values(map);
if (new Set(values).size !== values.length) {
console.error('BiMap has received a map with duplicate values. This is not allowed.', values);
}
const keys = Object.keys(map);
if (new Set(keys).size !== keys.length) {
console.error('BiMap has received a map with duplicate keys. This is not allowed.', keys);
}
this.#keys = Object.keys(map);
this.#values = Object.values(map);
}
/**
* Transform an array of objects into a BiMap
* Ex. [{key: 'a', value: 'b'}, {key: 'c', value: 'd'}] => BiMap {a: 'b', c: 'd'}
* Default to key = id and value = name
* @param {Object[]} arr
* @param {string} keyName
* @param {string} valueName
* @returns
*/
static FromArrayOfObjects(arr, keyName = 'id', valueName = 'name') {
/**
* @type {{[key: string]: string}} arr
**/
const map = {};
arr.forEach((obj) => {
map[obj[keyName]] = obj[valueName];
});
return new BiMap(map);
}
/**
* @returns {string[]}
*/
keys() {
return this.#keys;
}
/**
* @returns {string[]}
*/
values() {
return this.#values;
}
/**
* Get value by key
* Returns an empty string if the key is not found
*
* @param {string} key
* @returns {any}
*/
byKey(key) {
const v = this.#values[this.#keys.indexOf(key)];
if (v === undefined) {
return "";
}
return v
}
/**
* Get key by value
* Returns an empty string if the value is not found
*
* @param {string} value
* @returns {string}
*/
byValue(value) {
const v = this.#keys[this.#values.indexOf(value)];
if (v === undefined) {
return "";
}
return v
}
/**
* Get value by key or null
*
* @param {string} value
* @param {*} value
* @returns {*}
*/
byValueOrNull(value) {
const v = this.#keys[this.#values.indexOf(value)];
if(!v) {
return null;
}
return v;
}
}
+12
View File
@@ -0,0 +1,12 @@
import { api } from '$lib/api/apiProxy.js';
export let previewQR = async (url = 'https://empty.test', dotSize = 4) => {
const res = await api.utils.qr({
url,
dotSize
});
if (!res.success) {
throw res.error;
}
return res.data;
};
+15
View File
@@ -0,0 +1,15 @@
export const scrollBarClassesHorizontal = `
[&::-webkit-scrollbar]:h-2
[&::-webkit-scrollbar-track]:bg-gray-100
[&::-webkit-scrollbar-thumb]:h-1
[&::-webkit-scrollbar-thumb]:my-1
[&::-webkit-scrollbar-thumb]:rounded-md
[&::-webkit-scrollbar-thumb]:bg-pc-dusty-light-blue
`;
export const scrollBarClassesVertical = `
[&::-webkit-scrollbar]:w-2
[&::-webkit-scrollbar-track]:bg-gray-100
[&::-webkit-scrollbar-thumb]:rounded-md
[&::-webkit-scrollbar-thumb]:bg-pc-dusty-light-blue
`;