Render overnight blocks across midnight in the day view
This commit is contained in:
@@ -2,7 +2,13 @@
|
||||
import { enhance } from '$app/forms';
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
import ProclamationBrowser from './ProclamationBrowser.svelte';
|
||||
import { MINUTE_STEP, formatTime, validateBlock, type BlockSibling } from '$lib/schedule-utils';
|
||||
import {
|
||||
MINUTE_STEP,
|
||||
formatTime,
|
||||
isOvernight,
|
||||
validateBlock,
|
||||
type BlockSibling
|
||||
} from '$lib/schedule-utils';
|
||||
|
||||
type BrowserCategory = { id: number; name: string };
|
||||
type BrowserGroup = { id: number; name: string };
|
||||
@@ -77,6 +83,9 @@
|
||||
|
||||
const startMinutes = $derived((Number(startHour) * 60 + Number(startMinute)) | 0);
|
||||
const endMinutes = $derived((Number(endHour) * 60 + Number(endMinute)) | 0);
|
||||
const overnight = $derived(
|
||||
startMinutes !== endMinutes && isOvernight({ startMinutes, endMinutes })
|
||||
);
|
||||
|
||||
const siblings = $derived(existingBlocks.filter((block) => block.id !== blockId));
|
||||
|
||||
@@ -276,7 +285,9 @@
|
||||
>
|
||||
<h3 class="text-sm font-semibold text-slate-800 dark:text-slate-200">Review</h3>
|
||||
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
|
||||
{formatTime(startMinutes)} – {formatTime(endMinutes)}
|
||||
{formatTime(startMinutes)} – {formatTime(endMinutes)}{overnight
|
||||
? ' (next day)'
|
||||
: ''}
|
||||
</p>
|
||||
{#if selectedTitles.length === 0}
|
||||
<p class="mt-2 text-sm text-slate-500 dark:text-slate-400">
|
||||
|
||||
@@ -74,6 +74,12 @@ describe('BlockModal', () => {
|
||||
await expect.element(page.getByText('Peace')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('marks an overnight block in the review', async () => {
|
||||
mount({ initialStartMinutes: 21 * 60, initialEndMinutes: 9 * 60 });
|
||||
|
||||
await expect.element(page.getByText('9:00 PM – 9:00 AM (next day)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('rejects an overlapping time range with the specific error', async () => {
|
||||
mount({
|
||||
existingBlocks: [{ id: 'b1', startMinutes: 6 * 60, endMinutes: 8 * 60 }],
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { MINUTES_PER_DAY, SNAP_MINUTES, formatTime, snapMinutes } from '$lib/schedule-utils';
|
||||
import {
|
||||
MINUTES_PER_DAY,
|
||||
SNAP_MINUTES,
|
||||
blockSegments,
|
||||
formatTime,
|
||||
isOvernight,
|
||||
snapMinutes
|
||||
} from '$lib/schedule-utils';
|
||||
|
||||
type ScheduleBlockView = {
|
||||
id: string;
|
||||
@@ -75,6 +82,18 @@
|
||||
height: ((end - start) / MINUTES_PER_DAY) * 100
|
||||
};
|
||||
});
|
||||
|
||||
// Overnight blocks render as two segments: start→midnight and midnight→end.
|
||||
const blockViews = $derived(
|
||||
blocks.map((block) => ({
|
||||
block,
|
||||
overnight: isOvernight(block),
|
||||
segments: blockSegments(block).map(([segmentStart, segmentEnd]) => ({
|
||||
top: (segmentStart / MINUTES_PER_DAY) * 100,
|
||||
height: ((segmentEnd - segmentStart) / MINUTES_PER_DAY) * 100
|
||||
}))
|
||||
}))
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2">
|
||||
@@ -113,28 +132,34 @@
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
{#each blocks as block (block.id)}
|
||||
{@const top = (block.startMinutes / MINUTES_PER_DAY) * 100}
|
||||
{@const height = ((block.endMinutes - block.startMinutes) / MINUTES_PER_DAY) * 100}
|
||||
{#each blockViews as view (view.block.id)}
|
||||
{#each view.segments as segment, segmentIndex (`${view.block.id}-${segmentIndex}`)}
|
||||
<button
|
||||
type="button"
|
||||
onpointerdown={(event) => event.stopPropagation()}
|
||||
onclick={(event) => {
|
||||
event.stopPropagation();
|
||||
onEdit?.(block);
|
||||
onEdit?.(view.block);
|
||||
}}
|
||||
aria-label={`Block ${formatTime(block.startMinutes)} to ${formatTime(block.endMinutes)}`}
|
||||
aria-label={`Block ${formatTime(view.block.startMinutes)} to ${formatTime(view.block.endMinutes)}`}
|
||||
class="absolute inset-x-1 overflow-hidden rounded-md border border-indigo-300 bg-indigo-50 px-2 py-1 text-left text-xs text-indigo-900 shadow-sm transition hover:bg-indigo-100 dark:border-indigo-800 dark:bg-indigo-950/60 dark:text-indigo-200 dark:hover:bg-indigo-900"
|
||||
style={`top:${top}%;height:${height}%`}
|
||||
style={`top:${segment.top}%;height:${segment.height}%`}
|
||||
>
|
||||
{#if segmentIndex === 0}
|
||||
<span class="block font-semibold">
|
||||
{formatTime(block.startMinutes)} – {formatTime(block.endMinutes)}
|
||||
{formatTime(view.block.startMinutes)} – {formatTime(
|
||||
view.block.endMinutes
|
||||
)}{view.overnight ? ' (next day)' : ''}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="block font-semibold">↳ continues</span>
|
||||
{/if}
|
||||
<span class="block truncate">
|
||||
{block.proclamations.map((p) => p.name).join(', ') || 'No proclamations'}
|
||||
{view.block.proclamations.map((p) => p.name).join(', ') || 'No proclamations'}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
{/each}
|
||||
|
||||
{#if blocks.length === 0}
|
||||
<p
|
||||
|
||||
Reference in New Issue
Block a user