- Delete old Vite+Svelte frontend - Initialize new SvelteKit project with TypeScript - Configure Tailwind CSS v4 + DaisyUI - Implement JWT authentication with auto-refresh - Create login page with form validation (Zod) - Add protected route guards - Update Docker configuration for single-stage build - Add E2E tests with Playwright (6/11 passing) - Fix Svelte 5 reactivity with $state() runes Known issues: - 5 E2E tests failing (timing/async issues) - Token refresh implementation needs debugging - Validation error display timing
148 lines
5.1 KiB
JavaScript
148 lines
5.1 KiB
JavaScript
/**
|
|
* Extracts duplicated elements and their indices from an array, returning them.
|
|
*
|
|
* Note that given `a === b && b === c`, then `c === a` must be `true` for this to give accurate results.
|
|
*
|
|
* @param arr The array to extract duplicate elements from.
|
|
*/ export const getDuplicatesOf = (arr, opts) => {
|
|
const isEqual = opts?.isEqual ?? ((l, r) => l === r);
|
|
const elementFirstSeenIndx = new Map();
|
|
const duplicates = [];
|
|
for (const [indx, element] of arr.entries()) {
|
|
const duplicatesIndx = duplicates.findIndex(duplicate => isEqual(duplicate.element, element));
|
|
if (duplicatesIndx !== -1) {
|
|
// This is at least the third occurrence of an item equal to `element`,
|
|
// so add this index to the list of indices where the element is duplicated.
|
|
duplicates[duplicatesIndx].indices.push(indx);
|
|
continue;
|
|
}
|
|
// At this point, we know this is either the first
|
|
// or second occurrence of an item equal to `element`...
|
|
let found = false;
|
|
for (const [existingElement, firstSeenIndx] of elementFirstSeenIndx) {
|
|
if (isEqual(element, existingElement)) {
|
|
// This is the second occurrence of an item equal to `element`,
|
|
// so store it as a duplicate.
|
|
found = true;
|
|
duplicates.push({
|
|
element: existingElement,
|
|
indices: [firstSeenIndx, indx]
|
|
});
|
|
}
|
|
}
|
|
if (!found) {
|
|
// We haven't seen this element before,
|
|
// so just store the index it was first seen
|
|
elementFirstSeenIndx.set(element, indx);
|
|
}
|
|
}
|
|
return duplicates;
|
|
};
|
|
export const join = (segments, delimiter) => segments.join(delimiter);
|
|
export const getPath = (root, path) => {
|
|
let result = root;
|
|
for (const segment of path) {
|
|
if (typeof result !== "object" || result === null)
|
|
return undefined;
|
|
result = result[segment];
|
|
}
|
|
return result;
|
|
};
|
|
export const intersectUniqueLists = (l, r) => {
|
|
const intersection = [...l];
|
|
for (const item of r)
|
|
if (!l.includes(item))
|
|
intersection.push(item);
|
|
return intersection;
|
|
};
|
|
export const liftArray = (data) => (Array.isArray(data) ? data : [data]);
|
|
/**
|
|
* Splits an array into two arrays based on the result of a predicate
|
|
*
|
|
* @param predicate - The guard function used to determine which items to include.
|
|
* @returns A tuple containing two arrays:
|
|
* - the first includes items for which `predicate` returns true
|
|
* - the second includes items for which `predicate` returns false
|
|
*
|
|
* @example
|
|
* const list = [1, "2", "3", 4, 5];
|
|
* const [numbers, strings] = spliterate(list, (x) => typeof x === "number");
|
|
* // Type: number[]
|
|
* // Output: [1, 4, 5]
|
|
* console.log(evens);
|
|
* // Type: string[]
|
|
* // Output: ["2", "3"]
|
|
* console.log(odds);
|
|
*/
|
|
export const spliterate = (arr, predicate) => {
|
|
const result = [[], []];
|
|
for (const item of arr) {
|
|
if (predicate(item))
|
|
result[0].push(item);
|
|
else
|
|
result[1].push(item);
|
|
}
|
|
return result;
|
|
};
|
|
export const ReadonlyArray = Array;
|
|
export const includes = (array, element) => array.includes(element);
|
|
export const range = (length, offset = 0) => [...new Array(length)].map((_, i) => i + offset);
|
|
/**
|
|
* Adds a value or array to an array, returning the concatenated result
|
|
*/
|
|
export const append = (to, value, opts) => {
|
|
if (to === undefined) {
|
|
return (value === undefined ? []
|
|
: Array.isArray(value) ? value
|
|
: [value]);
|
|
}
|
|
if (opts?.prepend) {
|
|
if (Array.isArray(value))
|
|
to.unshift(...value);
|
|
else
|
|
to.unshift(value);
|
|
}
|
|
else {
|
|
if (Array.isArray(value))
|
|
to.push(...value);
|
|
else
|
|
to.push(value);
|
|
}
|
|
return to;
|
|
};
|
|
/**
|
|
* Concatenates an element or list with a readonly list
|
|
*/
|
|
export const conflatenate = (to, elementOrList) => {
|
|
if (elementOrList === undefined || elementOrList === null)
|
|
return to ?? [];
|
|
if (to === undefined || to === null)
|
|
return liftArray(elementOrList);
|
|
return to.concat(elementOrList);
|
|
};
|
|
/**
|
|
* Concatenates a variadic list of elements or lists with a readonly list
|
|
*/
|
|
export const conflatenateAll = (...elementsOrLists) => elementsOrLists.reduce(conflatenate, []);
|
|
/**
|
|
* Appends a value or concatenates an array to an array if it is not already included, returning the array
|
|
*/
|
|
export const appendUnique = (to, value, opts) => {
|
|
if (to === undefined)
|
|
return Array.isArray(value) ? value : [value];
|
|
const isEqual = opts?.isEqual ?? ((l, r) => l === r);
|
|
for (const v of liftArray(value))
|
|
if (!to.some(existing => isEqual(existing, v)))
|
|
to.push(v);
|
|
return to;
|
|
};
|
|
export const groupBy = (array, discriminant) => array.reduce((result, item) => {
|
|
const key = item[discriminant];
|
|
result[key] = append(result[key], item);
|
|
return result;
|
|
}, {});
|
|
export const arrayEquals = (l, r, opts) => l.length === r.length &&
|
|
l.every(opts?.isEqual ?
|
|
(lItem, i) => opts.isEqual(lItem, r[i])
|
|
: (lItem, i) => lItem === r[i]);
|