Squash commmits
This commit is contained in:
4
src/app.css
Normal file
4
src/app.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import 'tailwindcss';
|
||||
@plugin "daisyui" {
|
||||
themes: cmyk --default, dracula --prefersdark;
|
||||
}
|
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
12
src/app.html
Normal file
12
src/app.html
Normal file
@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
7
src/demo.spec.ts
Normal file
7
src/demo.spec.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('sum test', () => {
|
||||
it('adds 1 + 2 to equal 3', () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
});
|
79
src/lib/components/ClickSpark.svelte
Normal file
79
src/lib/components/ClickSpark.svelte
Normal file
@ -0,0 +1,79 @@
|
||||
<!-- https://codepen.io/hexagoncircle/pen/bGZdWyw -->
|
||||
|
||||
<script>
|
||||
let svgEl;
|
||||
let sparkEl;
|
||||
|
||||
export function triggerSpark(e) {
|
||||
setPosition(e);
|
||||
animate();
|
||||
}
|
||||
|
||||
function setPosition(e) {
|
||||
sparkEl.style.left = `${e.pageX - sparkEl.clientWidth / 2}px`;
|
||||
sparkEl.style.top = `${e.pageY - sparkEl.clientHeight / 2}px`;
|
||||
}
|
||||
|
||||
function animate() {
|
||||
const sparks = [...svgEl.children];
|
||||
const size = parseInt(sparks[0].getAttribute('y1'));
|
||||
const offset = size / 2 + 'px';
|
||||
|
||||
const options = {
|
||||
duration: 660,
|
||||
easing: 'cubic-bezier(0.25, 1, 0.5, 1)',
|
||||
fill: 'forwards'
|
||||
};
|
||||
|
||||
sparks.forEach((spark, i) => {
|
||||
const deg = `calc(${i} * (360deg / ${sparks.length}))`;
|
||||
const keyframes = [
|
||||
{
|
||||
strokeDashoffset: size * 3,
|
||||
transform: `rotate(${deg}) translateY(${offset})`
|
||||
},
|
||||
{
|
||||
strokeDashoffset: size,
|
||||
transform: `rotate(${deg}) translateY(0)`
|
||||
}
|
||||
];
|
||||
spark.animate(keyframes, options);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={sparkEl} class="spark">
|
||||
<svg
|
||||
bind:this={svgEl}
|
||||
viewBox="0 0 100 100"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="4"
|
||||
stroke="var(--color-accent, currentcolor)"
|
||||
>
|
||||
{#each Array(8) as _}
|
||||
<line
|
||||
x1="50"
|
||||
y1="30"
|
||||
x2="50"
|
||||
y2="4"
|
||||
stroke-dasharray="30"
|
||||
stroke-dashoffset="30"
|
||||
style="transform-origin: center"
|
||||
/>
|
||||
{/each}
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spark {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
svg {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
transform: rotate(-20deg);
|
||||
}
|
||||
</style>
|
17
src/lib/crabfitData.ts
Normal file
17
src/lib/crabfitData.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { Member } from '$lib/meeting';
|
||||
import { fetchCrabfitData } from '$lib/meeting';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
export let crabfitData: Member[] | null = null;
|
||||
let lastFetchTime = 0;
|
||||
const TTL = 1000 * 60 * 60 * 12; // 12 hours in ms
|
||||
|
||||
export async function getCrabfitData(): Promise<Member[]> {
|
||||
const now = Date.now();
|
||||
if (!crabfitData || now - lastFetchTime > TTL) {
|
||||
crabfitData = await fetchCrabfitData(env.CRABFIT_API_URL);
|
||||
lastFetchTime = now;
|
||||
console.log(`${now} cached new crabifit data`);
|
||||
}
|
||||
return crabfitData;
|
||||
}
|
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
108
src/lib/meeting.ts
Normal file
108
src/lib/meeting.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import fetch from 'node-fetch';
|
||||
import { env } from '$env/dynamic/private';
|
||||
|
||||
export type Member = {
|
||||
name: string;
|
||||
availability: string[];
|
||||
};
|
||||
|
||||
export type Options = {
|
||||
members: Member[];
|
||||
days: string[];
|
||||
number: number;
|
||||
};
|
||||
|
||||
const dayMap: Record<string, string> = {
|
||||
Monday: '1',
|
||||
Tuesday: '2',
|
||||
Wednesday: '3',
|
||||
Thursday: '4',
|
||||
Friday: '5',
|
||||
Saturday: '6',
|
||||
Sunday: '7'
|
||||
};
|
||||
|
||||
const dayReverseMap: Record<string, string> = {
|
||||
'1': 'Monday',
|
||||
'2': 'Tuesday',
|
||||
'3': 'Wednesday',
|
||||
'4': 'Thursday',
|
||||
'5': 'Friday',
|
||||
'6': 'Saturday',
|
||||
'7': 'Sunday'
|
||||
};
|
||||
|
||||
export function findMeetingOptions(
|
||||
members: Member[],
|
||||
{ days, number }: Omit<Options, 'members'>
|
||||
): { [key: string]: [string, string] }[] {
|
||||
const data: Record<string, string[]> = {};
|
||||
const selectedDays = days.map((d) => dayMap[d]);
|
||||
|
||||
for (const member of members) {
|
||||
for (const slot of member.availability) {
|
||||
const [time, day] = slot.split('-');
|
||||
if (!time.endsWith('00')) continue;
|
||||
if (!selectedDays.includes(day)) continue;
|
||||
if (!data[slot]) data[slot] = [];
|
||||
data[slot].push(member.name);
|
||||
}
|
||||
}
|
||||
|
||||
const membersCount = members.length;
|
||||
const slots = Object.keys(data);
|
||||
const combinations = getCombinations(slots, number);
|
||||
const seen: Set<string> = new Set();
|
||||
const options: { [key: string]: [string, string] }[] = [];
|
||||
let counter = 1;
|
||||
|
||||
for (const combo of combinations) {
|
||||
const unique = new Set(combo);
|
||||
if (unique.size < number) continue;
|
||||
const key = [...unique].sort().join(',');
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
|
||||
const attendeesPerSlot = combo.map((slot) => data[slot] ?? []);
|
||||
const attendees = new Set(attendeesPerSlot.flat());
|
||||
|
||||
if (
|
||||
attendees.size === membersCount &&
|
||||
attendeesPerSlot.every((s) => s.length >= Math.floor(membersCount / number)) &&
|
||||
attendeesPerSlot.every((s) => s.length < Math.floor(membersCount / number) + 2)
|
||||
) {
|
||||
for (const slot of combo) {
|
||||
const [time, day] = slot.split('-');
|
||||
options.push({
|
||||
[`Option ${counter}`]: [dayReverseMap[day], time]
|
||||
});
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function getCombinations(arr: string[], size: number): string[][] {
|
||||
if (size === 0) return [[]];
|
||||
if (size > arr.length) return [];
|
||||
if (size === 1) return arr.map((v) => [v]);
|
||||
const combos: string[][] = [];
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const rest = getCombinations(arr.slice(i + 1), size - 1);
|
||||
for (const r of rest) {
|
||||
combos.push([arr[i], ...r]);
|
||||
}
|
||||
}
|
||||
return combos;
|
||||
}
|
||||
|
||||
export async function fetchCrabfitData(apiUrl): Promise<Member[]> {
|
||||
const res = await fetch(`${apiUrl}/people`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function filterMemberData(members: string[], memberData: Member[]): Promise<Member[]> {
|
||||
return members.map((name: string) => memberData.find((m) => m.name === name)).filter(Boolean);
|
||||
}
|
7
src/routes/+layout.svelte
Normal file
7
src/routes/+layout.svelte
Normal file
@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
38
src/routes/+page.server.ts
Normal file
38
src/routes/+page.server.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { fail } from '@sveltejs/kit';
|
||||
// import type { Actions } from './$types';
|
||||
import { getCrabfitData } from '$lib/crabfitData';
|
||||
import { findMeetingOptions, filterMemberData } from '$lib/meeting';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch }) => {
|
||||
const res = await fetch('/api/members');
|
||||
const data = await res.json();
|
||||
return { members: data.names };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
getMeetings: async ({ request }) => {
|
||||
const form = await request.formData();
|
||||
|
||||
const selectedMembers = form.getAll('selectedMembers');
|
||||
const days = form.getAll('selectedDays');
|
||||
// const number = form.get('number');
|
||||
const number = 1;
|
||||
|
||||
if (!selectedMembers) return fail(500, { success: false, error: 'Select a member' });
|
||||
if (!days) return fail(500, { success: false, error: 'Select a day' });
|
||||
|
||||
try {
|
||||
const crabfit = await getCrabfitData();
|
||||
|
||||
const members = await filterMemberData(selectedMembers, crabfit);
|
||||
|
||||
const options = findMeetingOptions(members, { days, number });
|
||||
|
||||
return { success: true, options };
|
||||
} catch (err) {
|
||||
console.error('findMeetingOptions error:', err);
|
||||
return fail(500, { success: false, error: 'Failed to fetch meetings' });
|
||||
}
|
||||
}
|
||||
};
|
206
src/routes/+page.svelte
Normal file
206
src/routes/+page.svelte
Normal file
@ -0,0 +1,206 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { Copy } from '@lucide/svelte';
|
||||
import type { PageProps } from './$types';
|
||||
import ClickSpark from '$lib/components/ClickSpark.svelte';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
|
||||
// let members: string[] = $state(data.members);
|
||||
let selectedDays: string[] = $state([]);
|
||||
let lastSelectedDays = $state([]);
|
||||
let selectedMembers: string[] = $state([]);
|
||||
let number = $state(1);
|
||||
let form: HTMLFormElement;
|
||||
let noMeetings = $state(false);
|
||||
let meetingOptions: { [optionName: string]: [string, string] }[] = $state([]);
|
||||
|
||||
let sparkRef;
|
||||
|
||||
const daysOfTheWeek = [
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday',
|
||||
'Sunday'
|
||||
];
|
||||
|
||||
const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
|
||||
|
||||
function updateMeetings() {
|
||||
noMeetings = false;
|
||||
if (selectedDays.length > 0 && selectedMembers.length > 0) {
|
||||
if (form) form.requestSubmit();
|
||||
} else {
|
||||
meetingOptions = [];
|
||||
}
|
||||
}
|
||||
|
||||
function sortByDay(options) {
|
||||
const grouped = {};
|
||||
|
||||
for (const option of options) {
|
||||
for (const [optName, [day, time]] of Object.entries(option)) {
|
||||
if (!grouped[day]) grouped[day] = [];
|
||||
grouped[day].push(time);
|
||||
}
|
||||
}
|
||||
|
||||
for (const day in grouped) {
|
||||
grouped[day].sort();
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function formatTime(time: string) {
|
||||
const t = time.split('');
|
||||
return `${t[0]}${t[1]}:${t[2]}${t[3]}`;
|
||||
}
|
||||
|
||||
function copyOptions(e) {
|
||||
const membs = selectedMembers.reduce((prev, curr) => `${prev}, ${curr}`);
|
||||
let text = `Meeting times for ${membs}\n`;
|
||||
meetingOptions.forEach((opt, i) => {
|
||||
const [day, time] = Object.values(opt)[0];
|
||||
text += `${day} at ${formatTime(time)}\n`;
|
||||
});
|
||||
|
||||
navigator.clipboard.writeText(text);
|
||||
sparkRef.triggerSpark(e);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center gap-8 px-8 pt-8 pb-20">
|
||||
<div>
|
||||
<form
|
||||
class="flex flex-wrap gap-8 md:gap-24"
|
||||
bind:this={form}
|
||||
id="meeting-form"
|
||||
method="POST"
|
||||
action="?/getMeetings"
|
||||
use:enhance={({ formElement, formData, action, cancel, submitter }) => {
|
||||
return async ({ result, update }) => {
|
||||
if (result.type === 'success') {
|
||||
meetingOptions = result.data.options;
|
||||
if (meetingOptions.length === 0) noMeetings = true;
|
||||
}
|
||||
|
||||
if (result.type === 'error' || result.type === 'failure') {
|
||||
meetingOptions = [];
|
||||
}
|
||||
};
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<ul class="menu-horizontal rounded-box bg-base-200">
|
||||
<li class="menu-title">Select</li>
|
||||
<li>
|
||||
<button
|
||||
onclick={() => {
|
||||
if (selectedDays.toString() === weekdays.toString()) {
|
||||
selectedDays = [...lastSelectedDays];
|
||||
} else {
|
||||
lastSelectedDays = [...selectedDays];
|
||||
selectedDays = [...weekdays];
|
||||
}
|
||||
}}
|
||||
class="btn">Weekdays</button
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
onclick={() => {
|
||||
if (selectedDays.toString() === daysOfTheWeek.toString()) {
|
||||
selectedDays = [...lastSelectedDays];
|
||||
} else {
|
||||
lastSelectedDays = [...selectedDays];
|
||||
selectedDays = [...daysOfTheWeek];
|
||||
}
|
||||
}}
|
||||
class="btn">All</button
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
onclick={() => {
|
||||
if (selectedDays.toString() === '') {
|
||||
selectedDays = [...lastSelectedDays];
|
||||
} else {
|
||||
lastSelectedDays = [...selectedDays];
|
||||
selectedDays = [];
|
||||
}
|
||||
}}
|
||||
class="btn">None</button
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<fieldset id="day-select" class="fieldset flex flex-col gap-3">
|
||||
<legend class="fieldset-legend text-xl">Which days</legend>
|
||||
{#each daysOfTheWeek as day}
|
||||
<label class="label gap-3 text-xl text-base-content select-none"
|
||||
><input
|
||||
class="checkbox checkbox-lg checkbox-secondary"
|
||||
type="checkbox"
|
||||
bind:group={selectedDays}
|
||||
onchange={updateMeetings}
|
||||
name="selectedDays"
|
||||
value={day}
|
||||
/>{day}</label
|
||||
>
|
||||
{/each}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<fieldset id="member-select" class="fieldset flex flex-col gap-3">
|
||||
<legend class="fieldset-legend text-xl">Which members</legend>
|
||||
|
||||
{#each data.members as member}
|
||||
<label class="label gap-3 text-xl text-base-content select-none"
|
||||
><input
|
||||
class="checkbox checkbox-lg checkbox-primary"
|
||||
type="checkbox"
|
||||
bind:group={selectedMembers}
|
||||
onchange={updateMeetings}
|
||||
value={member}
|
||||
name="selectedMembers"
|
||||
/>{member}</label
|
||||
>
|
||||
{/each}
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="divider"><h2 class="text-2xl">Possible meeting times</h2></div>
|
||||
|
||||
<div>
|
||||
{#if meetingOptions.length > 0}
|
||||
<button class="btn absolute right-5 sm:right-20" onclick={copyOptions}><Copy /> </button>
|
||||
<div class="flex flex-wrap justify-center gap-8 md:gap-4">
|
||||
{#each Object.entries(sortByDay(meetingOptions)) as [day, times]}
|
||||
<ul class="list h-fit w-56 rounded-box bg-base-200 shadow-md sm:w-fit">
|
||||
<h3 class="divide-y-2 rounded-t-box bg-base-300 px-8 py-4 text-center text-2xl">
|
||||
{day}
|
||||
</h3>
|
||||
{#each times as time}
|
||||
<li
|
||||
class="list-row flex justify-center divide-y-2 rounded-box text-2xl font-extralight tabular-nums"
|
||||
>
|
||||
{formatTime(time)}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if noMeetings}
|
||||
<p class="text-lg font-semibold">No meeting options found</p>
|
||||
{:else}
|
||||
<p class="text-lg font-semibold">Select days and members first</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ClickSpark bind:this={sparkRef} />
|
12
src/routes/api/members/+server.ts
Normal file
12
src/routes/api/members/+server.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { getCrabfitData } from '$lib/crabfitData';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const crabfitData = await getCrabfitData();
|
||||
|
||||
const names = crabfitData.map((m) => m.name);
|
||||
|
||||
return new Response(JSON.stringify({ names }), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
};
|
96
src/routes/page.html
Normal file
96
src/routes/page.html
Normal file
@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { options } from '../../.svelte-kit/generated/server/internal.js';
|
||||
import { enhance } from '$app/forms';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let members: string[] = $state([]);
|
||||
let selectedDay = $state('Mon');
|
||||
let selectedMembers: string[] = $state([]);
|
||||
let number = $state(1);
|
||||
let form: HTMLFormElement;
|
||||
|
||||
let meetingOptions: { [optionName: string]: [string, string] }[] = $state([]);
|
||||
|
||||
// Fetch members on mount
|
||||
onMount(async () => {
|
||||
const res = await fetch('/api/members');
|
||||
const data = await res.json();
|
||||
members = data.names;
|
||||
});
|
||||
|
||||
function updateMeetings() {
|
||||
if (form) form.requestSubmit();
|
||||
}
|
||||
</script>
|
||||
|
||||
<form
|
||||
bind:this="{form}"
|
||||
id="meeting-form"
|
||||
method="POST"
|
||||
action="?/getMeetings"
|
||||
use:enhance="{({"
|
||||
formElement,
|
||||
formData,
|
||||
action,
|
||||
cancel,
|
||||
submitter
|
||||
})=""
|
||||
>
|
||||
{ return async ({ result, update }) => { if (result.type === 'success') { meetingOptions =
|
||||
result.data; } if (result.type === 'error' || result.type === 'failure') { meetingOptions = []; }
|
||||
}; }} >
|
||||
<div>
|
||||
<label for="day-select">Select day of week:</label>
|
||||
<select id="day-select" name="days" bind:value="{selectedDay}" onchange="{updateMeetings}">
|
||||
<option>Mon</option>
|
||||
<option>Tue</option>
|
||||
<option>Wed</option>
|
||||
<option>Thu</option>
|
||||
<option>Fri</option>
|
||||
<option>Sat</option>
|
||||
<option>Sun</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>Select members:</label>
|
||||
{#if members.length === 0}
|
||||
<p>Loading members...</p>
|
||||
{:else}
|
||||
<select multiple bind:value="{members}">
|
||||
{#each members as member}
|
||||
<option value="{member}">{member}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="number-input">Number of meetings:</label>
|
||||
<input
|
||||
id="number-input"
|
||||
type="number"
|
||||
name="number"
|
||||
min="1"
|
||||
bind:value="{number}"
|
||||
onchange="{updateMeetings}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Hidden inputs for comma-separated values -->
|
||||
<!-- <input type="hidden" name="members" value={selectedMembers.join(',')} />
|
||||
<input type="hidden" name="days" value={selectedDay} /> -->
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<h2>Possible meeting times:</h2>
|
||||
{#if meetingOptions.length > 0}
|
||||
<ul>
|
||||
{#each meetingOptions as option, i} {#each Object.entries(option) as [optName, [day, time]]}
|
||||
<li><strong>{optName}</strong>: {day} at {time}</li>
|
||||
{/each} {/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p>No meeting options found.</p>
|
||||
{/if}
|
||||
</div>
|
107
src/test/meeting.test.ts
Normal file
107
src/test/meeting.test.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { fetchCrabfitData, filterMemberData, findMeetingOptions } from '$lib/meeting';
|
||||
|
||||
// vi.mock('node-fetch', async () => ({
|
||||
// default: vi.fn(() =>
|
||||
// Promise.resolve({
|
||||
// json: () => Promise.resolve(mockData)
|
||||
// })
|
||||
// )
|
||||
// }));
|
||||
const mockData = [
|
||||
{
|
||||
name: 'Alice',
|
||||
availability: ['1100-1', '1115-1', '1130-1', '1100-2'],
|
||||
created_at: 1000000000
|
||||
},
|
||||
{
|
||||
name: 'Bob',
|
||||
availability: ['1100-1', '1400-1', '1500-1'],
|
||||
created_at: 1000000001
|
||||
},
|
||||
// Charlie has no times on Monday, but does on Tuesdays
|
||||
{
|
||||
name: 'Charlie',
|
||||
availability: ['2000-1', '1100-2'],
|
||||
created_at: 1000000002
|
||||
}
|
||||
];
|
||||
|
||||
const testData = [
|
||||
{
|
||||
name: 'B',
|
||||
availability: ['0900-1', '0915-1', '0930-1', '0945-1', '1000-1', '1015-1', '1030-1', '1045-1'],
|
||||
created_at: 1753176466
|
||||
},
|
||||
{
|
||||
name: 'A',
|
||||
availability: ['0800-1', '0815-1', '0830-1', '0845-1', '0900-1', '0915-1', '0930-1', '0945-1'],
|
||||
created_at: 1753176450
|
||||
}
|
||||
];
|
||||
|
||||
const CRABFIT_TEST_API_URL = 'https://api.crab.fit/event/schedulertestdata-850919';
|
||||
|
||||
describe('findMeetingOptions', () => {
|
||||
it('returns options correctly', async () => {
|
||||
const options = await findMeetingOptions([mockData[0], mockData[1]], {
|
||||
days: ['Monday'],
|
||||
number: 1
|
||||
});
|
||||
expect(options).toEqual([{ 'Option 1': ['Monday', '1100'] }]);
|
||||
});
|
||||
|
||||
it('returns options correctly', async () => {
|
||||
const options = await findMeetingOptions([mockData[0], mockData[2]], {
|
||||
days: ['Tuesday'],
|
||||
number: 1
|
||||
});
|
||||
expect(options).toEqual([{ 'Option 1': ['Tuesday', '1100'] }]);
|
||||
});
|
||||
|
||||
it('returns no options for impossible constraints', async () => {
|
||||
const options = await findMeetingOptions([mockData[0], mockData[2]], {
|
||||
days: ['Monday'],
|
||||
number: 1
|
||||
});
|
||||
expect(options).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterMemberData', () => {
|
||||
it('filters member data', async () => {
|
||||
const members = ['A', 'B'];
|
||||
const result = await filterMemberData(members, testData);
|
||||
expect(result).toEqual(expect.arrayContaining(testData));
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchCrabfitData', () => {
|
||||
it('gets member data', async () => {
|
||||
expect(await fetchCrabfitData(CRABFIT_TEST_API_URL)).toEqual(testData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration', () => {
|
||||
it('finds meetings with fetched data', async () => {
|
||||
const crabfitData = await fetchCrabfitData(CRABFIT_TEST_API_URL);
|
||||
const members = ['A', 'B'];
|
||||
const memberData = await filterMemberData(members, crabfitData);
|
||||
|
||||
const options = await findMeetingOptions(memberData, {
|
||||
days: ['Monday'],
|
||||
number: 1
|
||||
});
|
||||
expect(options).toEqual([{ 'Option 1': ['Monday', '0900'] }]);
|
||||
});
|
||||
it('finds meetings with test data', async () => {
|
||||
const members = ['A', 'B'];
|
||||
const memberData = await filterMemberData(members, testData);
|
||||
|
||||
const options = await findMeetingOptions(memberData, {
|
||||
days: ['Monday'],
|
||||
number: 1
|
||||
});
|
||||
expect(options).toEqual([{ 'Option 1': ['Monday', '0900'] }]);
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user