This commit is contained in:
Timothy Jaeryang Baek
2026-04-20 00:12:53 +09:00
parent 24dd5b461e
commit 4e31fa4427
3 changed files with 170 additions and 176 deletions

View File

@@ -71,7 +71,7 @@
<div class="flex flex-col gap-4">
<!-- Mini Month Calendar -->
<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="flex items-center gap-0.5">
<button

View File

@@ -1,13 +1,7 @@
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte';
import { mobile, showSidebar } from '$lib/stores';
import type { CalendarEventModel, CalendarModel } from '$lib/apis/calendar';
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 dispatch = createEventDispatcher();
@@ -20,20 +14,6 @@
const NS = 1_000_000;
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(
(acc, c) => ({ ...acc, [c.id]: c.color }),
@@ -135,22 +115,6 @@
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) {
currentDate = day;
const ms = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 9).getTime();
@@ -173,137 +137,9 @@
function handleEventClick(event: CalendarEventModel) {
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>
<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 -->
{#if view === 'month'}
<div class="flex-1 flex flex-col min-h-0 px-3 pb-3">

View File

@@ -14,6 +14,11 @@
import CalendarEventModal from '$lib/components/calendar/CalendarEventModal.svelte';
import Spinner from '$lib/components/common/Spinner.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');
@@ -29,6 +34,22 @@
let editEvent: CalendarEventModel | 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 } {
const d = new Date(currentDate);
let start: Date;
@@ -128,8 +149,29 @@
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 || '';
$: headerText =
view === 'day'
? `${DAY_NAMES[currentDate.getDay()]}, ${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getDate()}, ${currentDate.getFullYear()}`
: `${MONTH_NAMES[currentDate.getMonth()]} ${currentDate.getFullYear()}`;
onMount(async () => {
await loadCalendars();
await refresh();
@@ -157,17 +199,134 @@
: ''} max-w-full"
>
{#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">
<!-- Sidebar -->
<div class="hidden md:flex flex-col w-56 shrink-0 pr-1.5 pl-3 pt-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>
<div class="hidden md:flex flex-col w-56 shrink-0 pr-1.5 pl-3 overflow-y-auto">
<CalendarSidebar
{calendars}
{visibleCalendarIds}
@@ -178,7 +337,7 @@
</div>
<!-- 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
{events}
{calendars}
@@ -189,7 +348,6 @@
on:eventClick={handleEventClick}
on:navigate={handleNavigate}
on:viewChange={handleNavigate}
on:newEvent={handleNewEvent}
/>
</div>
</div>