design nosepass template, add tests, add template previews

This commit is contained in:
Amruth Pillai
2023-11-17 08:31:12 +01:00
parent 0b4cb71320
commit 34247f13b6
92 changed files with 24440 additions and 35518 deletions

View File

@ -1,10 +1,9 @@
export * from "./namespaces/array";
export * from "./namespaces/cefr";
export * from "./namespaces/csv";
export * from "./namespaces/date";
export * from "./namespaces/error";
export * from "./namespaces/fonts";
export * from "./namespaces/languages";
export * from "./namespaces/language";
export * from "./namespaces/number";
export * from "./namespaces/object";
export * from "./namespaces/page";

View File

@ -1,33 +1,5 @@
import { LayoutLocator } from "./types";
type CombinationsInput<T> = {
[K in keyof T]: T[K][];
};
export const generateCombinations = <T>(obj: CombinationsInput<T>): Array<T> => {
const keys = Object.keys(obj) as (keyof T)[];
const results: Array<T> = [];
function backtrack(combination: Partial<T>, index: number): void {
if (index === keys.length) {
results.push(combination as T);
return;
}
const key = keys[index];
const values = obj[key];
for (const value of values) {
backtrack({ ...combination, [key]: value }, index + 1);
}
}
backtrack({}, 0);
return results;
};
// Function to find a specific item in a layout
export const findItemInLayout = (item: string, layout: string[][][]): LayoutLocator | null => {
for (let page = 0; page < layout.length; page++) {
@ -60,17 +32,21 @@ export const moveItemInLayout = (
target: LayoutLocator,
layout: string[][][],
): string[][][] => {
// Create a deep copy of the layout to avoid mutating the original array
const newLayout = JSON.parse(JSON.stringify(layout)) as string[][][];
try {
// Create a deep copy of the layout to avoid mutating the original array
const newLayout = JSON.parse(JSON.stringify(layout)) as string[][][];
// Get the item from the current location
const item = newLayout[current.page][current.column][current.section];
// Get the item from the current location
const item = newLayout[current.page][current.column][current.section];
// Remove the item from the current location
newLayout[current.page][current.column].splice(current.section, 1);
// Remove the item from the current location
newLayout[current.page][current.column].splice(current.section, 1);
// Insert the item at the target location
newLayout[target.page][target.column].splice(target.section, 0, item);
// Insert the item at the target location
newLayout[target.page][target.column].splice(target.section, 0, item);
return newLayout;
return newLayout;
} catch (error) {
return layout;
}
};

View File

@ -1,13 +0,0 @@
// CEFR Levels
const cefrMap = {
1: "A1",
2: "A2",
3: "B1",
4: "B2",
5: "C1",
6: "C2",
};
export const getCEFRLevel = (level: number) => {
return cefrMap[level as keyof typeof cefrMap];
};

View File

@ -4,6 +4,7 @@ export const sortByDate = <T>(a: T, b: T, key: keyof T, desc = true) => {
if (!a[key] || !b[key]) return 0;
if (!(a[key] instanceof Date) || !(b[key] instanceof Date)) return 0;
if (dayjs(a[key] as Date).isSame(dayjs(b[key] as Date))) return 0;
if (desc) return dayjs(a[key] as Date).isBefore(dayjs(b[key] as Date)) ? 1 : -1;
else return dayjs(a[key] as Date).isBefore(dayjs(b[key] as Date)) ? -1 : 1;
};

View File

@ -1,3 +1,4 @@
// Languages
export type Language = {
id: string;
name: string;

View File

@ -4,4 +4,9 @@ export const linearTransform = (
inMax: number,
outMin: number,
outMax: number,
) => ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
) => {
if (inMax === inMin) return value === inMax ? outMin : NaN;
return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
};
// Handle this case: returns output minimum if input maximum equals input minimum

View File

@ -1,11 +1 @@
export const delay = (time: number) => new Promise((resolve) => setTimeout(resolve, time));
export const withTimeout = async <T>(promise: Promise<T>, time: number): Promise<T> => {
const timeout = new Promise((_, reject) =>
setTimeout(() => {
return reject(new Error(`Operation timed out after ${time}ms`));
}, time),
);
return Promise.race([promise, timeout]) as T;
};

View File

@ -1,46 +1,13 @@
export type TemplateKey =
| "onyx"
| "kakuna"
| "rhyhorn"
| "azurill"
| "ditto"
| "chikorita"
| "bronzor"
| "pikachu";
export const templatesList = [
"azurill",
"bronzor",
"chikorita",
"ditto",
"kakuna",
"nosepass",
"onyx",
"pikachu",
"rhyhorn",
] as const;
export type Template = { id: TemplateKey; name: string };
export const templatesList: Template[] = [
{
id: "onyx",
name: "Onyx",
},
{
id: "kakuna",
name: "Kakuna",
},
{
id: "rhyhorn",
name: "Rhyhorn",
},
{
id: "azurill",
name: "Azurill",
},
{
id: "ditto",
name: "Ditto",
},
{
id: "chikorita",
name: "Chikorita",
},
{
id: "bronzor",
name: "Bronzor",
},
{
id: "pikachu",
name: "Pikachu",
},
];
export type Template = (typeof templatesList)[number];

View File

@ -0,0 +1,162 @@
import { describe, expect, it } from "vitest";
import { findItemInLayout, moveItemInLayout, removeItemInLayout } from "../array";
describe("findItemInLayout", () => {
it("should find the correct location of an item", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const item = "item3";
const expectedLocation = { page: 1, column: 0, section: 0 };
const location = findItemInLayout(item, layout);
expect(location).toEqual(expectedLocation);
});
it("should return null if the item is not found", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const item = "item5";
const location = findItemInLayout(item, layout);
expect(location).toBeNull();
});
});
describe("removeItemInLayout", () => {
it("should remove the item and return its location", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const item = "item3";
const expectedLocation = { page: 1, column: 0, section: 0 };
const location = removeItemInLayout(item, layout);
expect(location).toEqual(expectedLocation);
expect(layout[1][0]).not.toContain(item);
});
it("should return null and not modify layout if the item is not found", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const item = "item5";
const location = removeItemInLayout(item, layout);
expect(location).toBeNull();
expect(layout).toEqual([
[["item1"], ["item2"]],
[["item3"], ["item4"]],
]);
});
});
describe("moveItemInLayout", () => {
it("should move an item from the current location to the target location", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const current = { page: 0, column: 1, section: 0 };
const target = { page: 1, column: 0, section: 1 };
const expectedLayout = [
[["item1"], []],
[["item3", "item2"], ["item4"]],
];
const newLayout = moveItemInLayout(current, target, layout);
expect(newLayout).toEqual(expectedLayout);
});
it("should not mutate the original layout array", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const layoutCopy = JSON.parse(JSON.stringify(layout));
const current = { page: 0, column: 1, section: 0 };
const target = { page: 1, column: 0, section: 1 };
moveItemInLayout(current, target, layout);
expect(layout).toEqual(layoutCopy);
});
it("should handle the case where the current and target locations are the same", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const current = { page: 1, column: 0, section: 0 };
const target = { page: 1, column: 0, section: 0 };
const newLayout = moveItemInLayout(current, target, layout);
expect(newLayout).toEqual(layout);
});
it("moves an item to the specified target location", () => {
const layout = [
[["A", "B"], ["C"]],
[["D"], ["E", "F"]],
];
const current = { page: 0, column: 0, section: 1 };
const target = { page: 1, column: 1, section: 1 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual([
[["A"], ["C"]],
[["D"], ["E", "B", "F"]],
]);
});
it("handles moving an item within the same column", () => {
const layout = [[["A", "B"]], [["C", "D"]]];
const current = { page: 0, column: 0, section: 0 };
const target = { page: 0, column: 0, section: 1 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual([[["B", "A"]], [["C", "D"]]]);
});
it("handles moving an item to the beginning of a column", () => {
const layout = [[["A"], ["B", "C"]], [["D"]]];
const current = { page: 1, column: 0, section: 0 };
const target = { page: 0, column: 1, section: 0 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual([[["A"], ["D", "B", "C"]], [[]]]);
});
it("handles moving an item to an empty column", () => {
const layout = [[["A"], []], [["B"]]];
const current = { page: 0, column: 0, section: 0 };
const target = { page: 0, column: 1, section: 0 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual([[[], ["A"]], [["B"]]]);
});
it("returns the same layout if the current location is invalid", () => {
const layout = [[["A"], ["B"]]];
const current = { page: 2, column: 0, section: 0 };
const target = { page: 0, column: 1, section: 0 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual(layout);
});
it("returns the same layout if the target location is invalid", () => {
const layout = [[["A"], ["B"]]];
const current = { page: 0, column: 0, section: 0 };
const target = { page: 2, column: 0, section: 0 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual(layout);
});
});

View File

@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";
import { deepSearchAndParseDates, sortByDate } from "../date";
type TestType = { date?: Date };
describe("sortByDate", () => {
it("sorts by date in descending order when desc is true", () => {
const a: TestType = { date: new Date("2023-01-01") };
const b: TestType = { date: new Date("2023-01-02") };
expect(sortByDate(a, b, "date")).toBe(1);
expect(sortByDate(b, a, "date")).toBe(-1);
});
it("sorts by date in ascending order when desc is false", () => {
const a: TestType = { date: new Date("2023-01-01") };
const b: TestType = { date: new Date("2023-01-02") };
expect(sortByDate(a, b, "date", false)).toBe(-1);
expect(sortByDate(b, a, "date", false)).toBe(1);
});
it("returns 0 if one of the dates is missing", () => {
const a: TestType = { date: new Date("2023-01-01") };
const b: TestType = {};
expect(sortByDate(a, b, "date")).toBe(0);
});
it("returns 0 if one of the values is not a date", () => {
const a: TestType = { date: new Date("2023-01-01") };
const b: TestType = { date: "2023-01-02" as unknown as Date };
expect(sortByDate(a, b, "date")).toBe(0);
});
it("handles equal dates", () => {
const a: TestType = { date: new Date("2023-01-01") };
const b: TestType = { date: new Date("2023-01-01") };
expect(sortByDate(a, b, "date")).toBe(0);
expect(sortByDate(a, b, "date", false)).toBe(0);
});
});
describe("deepSearchAndParseDates", () => {
it("parses dates at various nesting levels", () => {
const input = {
level1: {
date: "2021-08-17T00:00:00Z",
nested: {
date: "2022-08-17T00:00:00Z",
},
},
otherKey: "value",
};
const dateKeys = ["date"];
const output = deepSearchAndParseDates(input, dateKeys);
expect(output.level1.date).toBeInstanceOf(Date);
expect(output.level1.nested.date).toBeInstanceOf(Date);
expect(output.otherKey).toBe("value");
});
it("does not parse invalid date strings", () => {
const input = {
date: "not a date",
};
const dateKeys = ["date"];
const output = deepSearchAndParseDates(input, dateKeys);
expect(output.date).toBe("not a date");
});
it("does not modify non-object input", () => {
const input = "2021-08-17T00:00:00Z";
const dateKeys = ["date"];
const output = deepSearchAndParseDates(input, dateKeys);
expect(output).toBe(input);
});
it("returns null for null input", () => {
const input = null;
const dateKeys = ["date"];
const output = deepSearchAndParseDates(input, dateKeys);
expect(output).toBeNull();
});
it("handles arrays with date strings", () => {
const input = ["2021-08-17T00:00:00Z", "2022-08-17"];
const dateKeys = ["0", "1"]; // Assuming the keys are stringified indices
const output = deepSearchAndParseDates(input, dateKeys);
expect(output[0]).toBeInstanceOf(Date);
expect(output[1]).toBeInstanceOf(Date);
});
it("ignores keys that are not in the dateKeys", () => {
const input = {
date: "2021-08-17T00:00:00Z",
notADate: "2021-08-17T00:00:00Z",
};
const dateKeys = ["date"];
const output = deepSearchAndParseDates(input, dateKeys);
expect(output.date).toBeInstanceOf(Date);
expect(output.notADate).toBe("2021-08-17T00:00:00Z");
});
});

View File

@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { linearTransform } from "../number";
describe("linearTransform", () => {
it("transforms values from one range to another", () => {
const value = 5;
const result = linearTransform(value, 0, 10, 0, 100);
expect(result).toBe(50);
});
it("handles negative ranges", () => {
const value = -5;
const result = linearTransform(value, -10, 0, 0, 100);
expect(result).toBe(50);
});
it("correctly transforms the minimum input value to the minimum output value", () => {
const value = 0;
const result = linearTransform(value, 0, 10, 0, 100);
expect(result).toBe(0);
});
it("correctly transforms the maximum input value to the maximum output value", () => {
const value = 10;
const result = linearTransform(value, 0, 10, 0, 100);
expect(result).toBe(100);
});
it("transforms values outside the input range", () => {
const value = 15;
const result = linearTransform(value, 0, 10, 0, 100);
expect(result).toBe(150);
});
it("handles inverted output ranges", () => {
const value = 5;
const result = linearTransform(value, 0, 10, 100, 0);
expect(result).toBe(50);
});
it("returns NaN if input maximum equals input minimum", () => {
const value = 5;
const result = linearTransform(value, 0, 0, 0, 100);
expect(result).toBe(NaN);
});
it("returns NaN if input range is zero (avoids division by zero)", () => {
const value = 5;
const result = linearTransform(value, 10, 10, 0, 100);
expect(result).toBeNaN();
});
});

View File

@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest";
import { exclude } from "../object";
describe("exclude", () => {
type TestObject = {
id: number;
name: string;
age: number;
email: string;
};
it("excludes specified keys from the object", () => {
const object: TestObject = {
id: 1,
name: "Alice",
age: 30,
email: "alice@example.com",
};
const result = exclude(object, ["age", "email"]);
expect(result).toEqual({ id: 1, name: "Alice" });
expect(result).not.toHaveProperty("age");
expect(result).not.toHaveProperty("email");
});
it("returns the same object if no keys are specified", () => {
const object: TestObject = {
id: 1,
name: "Alice",
age: 30,
email: "alice@example.com",
};
const keysToExclude: Array<keyof TestObject> = [];
const result = exclude(object, keysToExclude);
expect(result).toEqual(object);
});
it("does not modify the original object", () => {
const object: TestObject = {
id: 1,
name: "Alice",
age: 30,
email: "alice@example.com",
};
exclude(object, ["age", "email"]);
expect(object).toHaveProperty("age");
expect(object).toHaveProperty("email");
});
it("handles cases where keys to exclude are not present in the object", () => {
const object: TestObject = {
id: 1,
name: "Alice",
age: 30,
email: "alice@example.com",
};
const keysToExclude = ["nonExistentKey" as keyof TestObject];
const result = exclude(object, keysToExclude);
expect(result).toEqual(object);
});
it("returns the input if it is not an object", () => {
const object: unknown = null;
const keysToExclude = ["id"];
// @ts-expect-error passing invalid input type for tests
const result = exclude(object, keysToExclude);
expect(result).toBeNull();
});
});

View File

@ -0,0 +1,66 @@
import { describe, expect, it } from "vitest";
import {
extractUrl,
generateRandomName,
getInitials,
isEmptyString,
isUrl,
kebabCase,
processUsername,
} from "../string";
describe("getInitials", () => {
it("returns the initials of a name", () => {
expect(getInitials("John Doe")).toBe("JD");
expect(getInitials("Mary Jane Watson")).toBe("MW");
});
});
describe("isUrl", () => {
it("checks if a string is a URL", () => {
expect(isUrl("https://example.com")).toBe(true);
expect(isUrl("not a url")).toBe(false);
});
});
describe("isEmptyString", () => {
it("checks if a string is empty or only contains whitespace", () => {
expect(isEmptyString("")).toBe(true);
expect(isEmptyString(" ")).toBe(true);
expect(isEmptyString("<p></p>")).toBe(true);
expect(isEmptyString("not empty")).toBe(false);
});
});
describe("extractUrl", () => {
it("extracts a URL from a string", () => {
expect(extractUrl("Visit https://example.com today!")).toBe("https://example.com");
expect(extractUrl("No URL here.")).toBeNull();
});
});
describe("kebabCase", () => {
it("converts a string to kebab-case", () => {
expect(kebabCase("fooBar")).toBe("foo-bar");
expect(kebabCase("Foo Bar")).toBe("foo-bar");
expect(kebabCase("foo_bar")).toBe("foo-bar");
expect(kebabCase("")).toBe("");
expect(kebabCase(null)).toBe("");
});
});
describe("generateRandomName", () => {
it("generates a random name", () => {
const name = generateRandomName();
expect(name).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+ [A-Z][a-z]+$/);
});
});
describe("processUsername", () => {
it("processes a username by removing non-alphanumeric characters", () => {
expect(processUsername("User@Name!")).toBe("username");
expect(processUsername("")).toBe("");
expect(processUsername(null)).toBe("");
});
});