refac
This commit is contained in:
@@ -71,7 +71,7 @@
|
|||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<!-- Mini Month Calendar -->
|
<!-- Mini Month Calendar -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between px-1 mb-1.5 mt-2">
|
<div class="flex items-center justify-between px-1 mb-1.5 mt-1.5">
|
||||||
<div class="text-[11px] font-medium">{miniMonthNames[miniMonth]} {miniYear}</div>
|
<div class="text-[11px] font-medium">{miniMonthNames[miniMonth]} {miniYear}</div>
|
||||||
<div class="flex items-center gap-0.5">
|
<div class="flex items-center gap-0.5">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher, getContext } from 'svelte';
|
import { createEventDispatcher, getContext } from 'svelte';
|
||||||
import { mobile, showSidebar } from '$lib/stores';
|
|
||||||
import type { CalendarEventModel, CalendarModel } from '$lib/apis/calendar';
|
import type { CalendarEventModel, CalendarModel } from '$lib/apis/calendar';
|
||||||
import CalendarEventChip from './CalendarEventChip.svelte';
|
import CalendarEventChip from './CalendarEventChip.svelte';
|
||||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
||||||
import SidebarIcon from '$lib/components/icons/Sidebar.svelte';
|
|
||||||
import Select from '$lib/components/common/Select.svelte';
|
|
||||||
import Check from '$lib/components/icons/Check.svelte';
|
|
||||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
@@ -20,20 +14,6 @@
|
|||||||
|
|
||||||
const NS = 1_000_000;
|
const NS = 1_000_000;
|
||||||
const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
const MONTH_NAMES = [
|
|
||||||
'January',
|
|
||||||
'February',
|
|
||||||
'March',
|
|
||||||
'April',
|
|
||||||
'May',
|
|
||||||
'June',
|
|
||||||
'July',
|
|
||||||
'August',
|
|
||||||
'September',
|
|
||||||
'October',
|
|
||||||
'November',
|
|
||||||
'December'
|
|
||||||
];
|
|
||||||
|
|
||||||
$: calColorMap = calendars.reduce(
|
$: calColorMap = calendars.reduce(
|
||||||
(acc, c) => ({ ...acc, [c.id]: c.color }),
|
(acc, c) => ({ ...acc, [c.id]: c.color }),
|
||||||
@@ -135,22 +115,6 @@
|
|||||||
return `${h - 12} PM`;
|
return `${h - 12} PM`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigate(delta: number) {
|
|
||||||
const d = new Date(currentDate);
|
|
||||||
if (view === 'month') {
|
|
||||||
d.setDate(1);
|
|
||||||
d.setMonth(d.getMonth() + delta);
|
|
||||||
} else if (view === 'week') d.setDate(d.getDate() + delta * 7);
|
|
||||||
else d.setDate(d.getDate() + delta);
|
|
||||||
currentDate = d;
|
|
||||||
dispatch('navigate', { date: currentDate });
|
|
||||||
}
|
|
||||||
|
|
||||||
function goToToday() {
|
|
||||||
currentDate = new Date();
|
|
||||||
dispatch('navigate', { date: currentDate });
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDayClick(day: Date) {
|
function handleDayClick(day: Date) {
|
||||||
currentDate = day;
|
currentDate = day;
|
||||||
const ms = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9).getTime();
|
const ms = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9).getTime();
|
||||||
@@ -173,137 +137,9 @@
|
|||||||
function handleEventClick(event: CalendarEventModel) {
|
function handleEventClick(event: CalendarEventModel) {
|
||||||
dispatch('eventClick', event);
|
dispatch('eventClick', event);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: headerText =
|
|
||||||
view === 'day'
|
|
||||||
? `${DAY_NAMES[currentDate.getDay()]}, ${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getDate()}, ${currentDate.getFullYear()}`
|
|
||||||
: `${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getFullYear()}`;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full w-full min-h-0 min-w-0">
|
<div class="flex flex-col h-full w-full min-h-0 min-w-0">
|
||||||
<!-- Navbar — matches admin/workspace/notes pattern -->
|
|
||||||
<nav class="px-2.5 pt-1.5 pb-2 backdrop-blur-xl drag-region select-none shrink-0">
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
{#if $mobile}
|
|
||||||
<div class="{$showSidebar ? 'md:hidden' : ''} flex flex-none items-center">
|
|
||||||
<Tooltip
|
|
||||||
content={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
|
|
||||||
interactive={true}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
id="sidebar-toggle-button"
|
|
||||||
class="cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
|
||||||
on:click={() => showSidebar.set(!$showSidebar)}
|
|
||||||
>
|
|
||||||
<div class="self-center p-1.5">
|
|
||||||
<SidebarIcon />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex w-full items-center">
|
|
||||||
<div class="flex items-center gap-0.5 py-1">
|
|
||||||
<span class="min-w-fit px-1 text-sm select-none">{headerText}</span>
|
|
||||||
<button
|
|
||||||
class="p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
|
||||||
on:click={() => navigate(-1)}
|
|
||||||
aria-label="Previous"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-3.5 text-gray-400"
|
|
||||||
><path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M15.75 19.5 8.25 12l7.5-7.5"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
|
||||||
on:click={() => navigate(1)}
|
|
||||||
aria-label="Next"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-3.5 text-gray-400"
|
|
||||||
><path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="m8.25 4.5 7.5 7.5-7.5 7.5"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-1">
|
|
||||||
<button
|
|
||||||
class="text-xs px-2 py-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-850 transition text-gray-500 hover:text-gray-700 dark:hover:text-white"
|
|
||||||
on:click={goToToday}
|
|
||||||
>
|
|
||||||
{$i18n.t('Today')}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
bind:value={view}
|
|
||||||
items={[
|
|
||||||
{ value: 'day', label: $i18n.t('Day') },
|
|
||||||
{ value: 'week', label: $i18n.t('Week') },
|
|
||||||
{ value: 'month', label: $i18n.t('Month') }
|
|
||||||
]}
|
|
||||||
onChange={() => dispatch('viewChange', view)}
|
|
||||||
triggerClass="relative flex items-center gap-1.5 px-3 py-1.5 bg-gray-50 dark:bg-gray-850 rounded-xl text-xs"
|
|
||||||
contentClass="rounded-2xl w-40 p-1 border border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
|
||||||
align="end"
|
|
||||||
>
|
|
||||||
<svelte:fragment slot="trigger" let:selectedLabel>
|
|
||||||
<span class="inline-flex h-input px-0.5 outline-hidden bg-transparent">
|
|
||||||
{selectedLabel}
|
|
||||||
</span>
|
|
||||||
<ChevronDown className="size-3.5" strokeWidth="2.5" />
|
|
||||||
</svelte:fragment>
|
|
||||||
|
|
||||||
<svelte:fragment slot="item" let:item let:selected>
|
|
||||||
{item.label}
|
|
||||||
<div class="ml-auto {selected ? '' : 'invisible'}">
|
|
||||||
<Check />
|
|
||||||
</div>
|
|
||||||
</svelte:fragment>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="md:hidden px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition text-sm flex items-center"
|
|
||||||
on:click={() => dispatch('newEvent')}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="size-3"
|
|
||||||
><path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M12 4.5v15m7.5-7.5h-15"
|
|
||||||
/></svg
|
|
||||||
>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Month View -->
|
<!-- Month View -->
|
||||||
{#if view === 'month'}
|
{#if view === 'month'}
|
||||||
<div class="flex-1 flex flex-col min-h-0 px-3 pb-3">
|
<div class="flex-1 flex flex-col min-h-0 px-3 pb-3">
|
||||||
|
|||||||
@@ -14,6 +14,11 @@
|
|||||||
import CalendarEventModal from '$lib/components/calendar/CalendarEventModal.svelte';
|
import CalendarEventModal from '$lib/components/calendar/CalendarEventModal.svelte';
|
||||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||||
import Plus from '$lib/components/icons/Plus.svelte';
|
import Plus from '$lib/components/icons/Plus.svelte';
|
||||||
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||||
|
import SidebarIcon from '$lib/components/icons/Sidebar.svelte';
|
||||||
|
import Select from '$lib/components/common/Select.svelte';
|
||||||
|
import Check from '$lib/components/icons/Check.svelte';
|
||||||
|
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@@ -29,6 +34,22 @@
|
|||||||
let editEvent: CalendarEventModel | null = null;
|
let editEvent: CalendarEventModel | null = null;
|
||||||
let defaultStartAt: number | null = null;
|
let defaultStartAt: number | null = null;
|
||||||
|
|
||||||
|
const MONTH_NAMES = [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December'
|
||||||
|
];
|
||||||
|
const DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
|
||||||
function getVisibleRange(): { start: string; end: string } {
|
function getVisibleRange(): { start: string; end: string } {
|
||||||
const d = new Date(currentDate);
|
const d = new Date(currentDate);
|
||||||
let start: Date;
|
let start: Date;
|
||||||
@@ -128,8 +149,29 @@
|
|||||||
showEventModal = true;
|
showEventModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function navigateCalendar(delta: number) {
|
||||||
|
const d = new Date(currentDate);
|
||||||
|
if (view === 'month') {
|
||||||
|
d.setDate(1);
|
||||||
|
d.setMonth(d.getMonth() + delta);
|
||||||
|
} else if (view === 'week') d.setDate(d.getDate() + delta * 7);
|
||||||
|
else d.setDate(d.getDate() + delta);
|
||||||
|
currentDate = d;
|
||||||
|
handleNavigate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToToday() {
|
||||||
|
currentDate = new Date();
|
||||||
|
handleNavigate();
|
||||||
|
}
|
||||||
|
|
||||||
$: defaultCalendarId = calendars.find((c) => c.is_default)?.id || calendars[0]?.id || '';
|
$: defaultCalendarId = calendars.find((c) => c.is_default)?.id || calendars[0]?.id || '';
|
||||||
|
|
||||||
|
$: headerText =
|
||||||
|
view === 'day'
|
||||||
|
? `${DAY_NAMES[currentDate.getDay()]}, ${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getDate()}, ${currentDate.getFullYear()}`
|
||||||
|
: `${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getFullYear()}`;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await loadCalendars();
|
await loadCalendars();
|
||||||
await refresh();
|
await refresh();
|
||||||
@@ -157,17 +199,134 @@
|
|||||||
: ''} max-w-full"
|
: ''} max-w-full"
|
||||||
>
|
>
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
|
<!-- Top Navbar — spans above sidebar and calendar -->
|
||||||
|
<nav class="px-3 pt-2 pb-2 backdrop-blur-xl drag-region select-none shrink-0">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
{#if $mobile}
|
||||||
|
<div class="{$showSidebar ? 'md:hidden' : ''} flex flex-none items-center">
|
||||||
|
<Tooltip
|
||||||
|
content={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
|
||||||
|
interactive={true}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
id="sidebar-toggle-button"
|
||||||
|
class="cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
||||||
|
on:click={() => showSidebar.set(!$showSidebar)}
|
||||||
|
>
|
||||||
|
<div class="self-center p-1.5">
|
||||||
|
<SidebarIcon />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex w-full items-center">
|
||||||
|
<div class="flex items-center gap-0.5 py-1">
|
||||||
|
<span class="min-w-fit px-1 text-sm select-none">{headerText}</span>
|
||||||
|
<button
|
||||||
|
class="p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||||
|
on:click={() => navigateCalendar(-1)}
|
||||||
|
aria-label="Previous"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-3.5 text-gray-400"
|
||||||
|
><path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15.75 19.5 8.25 12l7.5-7.5"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||||
|
on:click={() => navigateCalendar(1)}
|
||||||
|
aria-label="Next"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-3.5 text-gray-400"
|
||||||
|
><path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m8.25 4.5 7.5 7.5-7.5 7.5"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ml-auto flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
class="text-xs px-2 py-1 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-850 transition text-gray-500 hover:text-gray-700 dark:hover:text-white"
|
||||||
|
on:click={goToToday}
|
||||||
|
>
|
||||||
|
{$i18n.t('Today')}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
bind:value={view}
|
||||||
|
items={[
|
||||||
|
{ value: 'day', label: $i18n.t('Day') },
|
||||||
|
{ value: 'week', label: $i18n.t('Week') },
|
||||||
|
{ value: 'month', label: $i18n.t('Month') }
|
||||||
|
]}
|
||||||
|
onChange={() => handleNavigate()}
|
||||||
|
triggerClass="relative flex items-center gap-1.5 px-3 py-1.5 bg-gray-50 dark:bg-gray-850 rounded-xl text-xs"
|
||||||
|
contentClass="rounded-2xl w-40 p-1 border border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="trigger" let:selectedLabel>
|
||||||
|
<span class="inline-flex h-input px-0.5 outline-hidden bg-transparent">
|
||||||
|
{selectedLabel}
|
||||||
|
</span>
|
||||||
|
<ChevronDown className="size-3.5" strokeWidth="2.5" />
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="item" let:item let:selected>
|
||||||
|
{item.label}
|
||||||
|
<div class="ml-auto {selected ? '' : 'invisible'}">
|
||||||
|
<Check />
|
||||||
|
</div>
|
||||||
|
</svelte:fragment>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="ml-1 px-2 py-1.5 text-xs gap-1 rounded-xl bg-black text-white dark:bg-white dark:text-black transition text-sm flex items-center"
|
||||||
|
on:click={handleNewEvent}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-3"
|
||||||
|
><path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 4.5v15m7.5-7.5h-15"
|
||||||
|
/></svg
|
||||||
|
>
|
||||||
|
|
||||||
|
<span class="hidden sm:inline">{$i18n.t('New Event')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="flex flex-1 min-h-0">
|
<div class="flex flex-1 min-h-0">
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="hidden md:flex flex-col w-56 shrink-0 pr-1.5 pl-3 pt-3 overflow-y-auto">
|
<div class="hidden md:flex flex-col w-56 shrink-0 pr-1.5 pl-3 overflow-y-auto">
|
||||||
<button
|
|
||||||
class="px-2 py-1.5 mt-0.5 mb-2.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition text-sm flex items-center justify-center gap-1"
|
|
||||||
on:click={handleNewEvent}
|
|
||||||
>
|
|
||||||
<Plus className="size-3" strokeWidth="2.5" />
|
|
||||||
<span class="text-xs">{$i18n.t('New Event')}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<CalendarSidebar
|
<CalendarSidebar
|
||||||
{calendars}
|
{calendars}
|
||||||
{visibleCalendarIds}
|
{visibleCalendarIds}
|
||||||
@@ -178,7 +337,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Calendar -->
|
<!-- Calendar -->
|
||||||
<div class="flex-1 flex flex-col min-h-0 pt-0.5">
|
<div class="flex-1 flex flex-col min-h-0">
|
||||||
<CalendarView
|
<CalendarView
|
||||||
{events}
|
{events}
|
||||||
{calendars}
|
{calendars}
|
||||||
@@ -189,7 +348,6 @@
|
|||||||
on:eventClick={handleEventClick}
|
on:eventClick={handleEventClick}
|
||||||
on:navigate={handleNavigate}
|
on:navigate={handleNavigate}
|
||||||
on:viewChange={handleNavigate}
|
on:viewChange={handleNavigate}
|
||||||
on:newEvent={handleNewEvent}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user