Squash commmits
This commit is contained in:
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>
|
Reference in New Issue
Block a user