mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 03:02:15 +01:00
Replace fullcalendar calendar header with custom toolbar
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '..';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-vue-next';
|
||||
import { Tabs, TabsList } from '@/Components/ui/tabs';
|
||||
import TabBarItem from '@/Components/Common/TabBar/TabBarItem.vue';
|
||||
import CalendarSettingsPopover from './CalendarSettingsPopover.vue';
|
||||
import type { CalendarSettings } from './calendarSettings';
|
||||
|
||||
defineProps<{
|
||||
viewTitle: string;
|
||||
activeView: string;
|
||||
settings: CalendarSettings;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
prev: [];
|
||||
next: [];
|
||||
today: [];
|
||||
'change-view': [view: string];
|
||||
'update:settings': [value: CalendarSettings];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between bg-background px-2 py-1.5">
|
||||
<!-- Left: Navigation -->
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
aria-label="Previous"
|
||||
@click="emit('prev')">
|
||||
<ChevronLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 w-8 p-0"
|
||||
aria-label="Next"
|
||||
@click="emit('next')">
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" @click="emit('today')"> today </Button>
|
||||
</div>
|
||||
|
||||
<!-- Center: Title -->
|
||||
<span data-testid="calendar-title" class="text-base font-semibold text-foreground">{{
|
||||
viewTitle
|
||||
}}</span>
|
||||
|
||||
<!-- Right: View switcher + Settings -->
|
||||
<div class="flex items-center gap-1">
|
||||
<Tabs
|
||||
:model-value="activeView"
|
||||
@update:model-value="(v) => emit('change-view', String(v))">
|
||||
<TabsList class="flex items-center space-x-0.5 sm:space-x-1">
|
||||
<TabBarItem value="timeGridWeek">week</TabBarItem>
|
||||
<TabBarItem value="timeGridDay">day</TabBarItem>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<CalendarSettingsPopover
|
||||
:settings="settings"
|
||||
@update:settings="(v) => emit('update:settings', v)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -29,7 +29,7 @@ import { getUserTimezone, getWeekStart } from '../utils/settings';
|
||||
import { LoadingSpinner, TimeEntryCreateModal, TimeEntryEditModal } from '..';
|
||||
import FullCalendarEventContent from './FullCalendarEventContent.vue';
|
||||
import FullCalendarDayHeader from './FullCalendarDayHeader.vue';
|
||||
import CalendarSettingsPopover from './CalendarSettingsPopover.vue';
|
||||
import CalendarToolbar from './CalendarToolbar.vue';
|
||||
import type { CalendarSettings } from './calendarSettings';
|
||||
import { useVisualSnap } from './useVisualSnap';
|
||||
import {
|
||||
@@ -123,6 +123,27 @@ function onSettingsUpdate(newSettings: CalendarSettings) {
|
||||
calendarSettings.value = newSettings;
|
||||
}
|
||||
|
||||
// Toolbar state — updated via datesSet callback
|
||||
const viewTitle = ref('');
|
||||
const activeView = ref('timeGridWeek');
|
||||
|
||||
function handlePrev() {
|
||||
calendarRef.value?.getApi().prev();
|
||||
scrollToCurrentTime();
|
||||
}
|
||||
function handleNext() {
|
||||
calendarRef.value?.getApi().next();
|
||||
scrollToCurrentTime();
|
||||
}
|
||||
function handleToday() {
|
||||
calendarRef.value?.getApi().today();
|
||||
scrollToCurrentTime();
|
||||
}
|
||||
function handleChangeView(view: string) {
|
||||
calendarRef.value?.getApi().changeView(view);
|
||||
scrollToCurrentTime();
|
||||
}
|
||||
|
||||
// Reactive "now" for running time entry - updates every minute
|
||||
const currentTime = ref(getDayJsInstance()());
|
||||
let currentTimeInterval: ReturnType<typeof setInterval> | null = null;
|
||||
@@ -240,6 +261,8 @@ const dailyTotals = computed(() => {
|
||||
});
|
||||
|
||||
function emitDatesChange(arg: DatesSetArg) {
|
||||
viewTitle.value = arg.view.title;
|
||||
activeView.value = arg.view.type;
|
||||
emit('dates-change', { start: arg.start, end: arg.end });
|
||||
// Render activity boxes after calendar view has been rendered
|
||||
renderActivityBoxes();
|
||||
@@ -506,11 +529,7 @@ const calendarOptions = computed(() => {
|
||||
return {
|
||||
plugins: [dayGridPlugin, timeGridPlugin, interactionPlugin, activityStatusPlugin],
|
||||
initialView: 'timeGridWeek',
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'timeGridWeek,timeGridDay',
|
||||
},
|
||||
headerToolbar: false as const,
|
||||
height: 'parent',
|
||||
slotMinTime: formatDuration(s.startHour * 3600),
|
||||
slotMaxTime: formatDuration(s.endHour * 3600),
|
||||
@@ -617,7 +636,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full relative h-full flex-1">
|
||||
<div class="w-full relative h-full flex-1 flex flex-col">
|
||||
<div v-if="loading" class="flex items-center justify-center h-full">
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<LoadingSpinner class="h-8 w-8" />
|
||||
@@ -656,13 +675,20 @@ onUnmounted(() => {
|
||||
:clients="clients"
|
||||
:currency="currency"
|
||||
:can-create-project="canCreateProject" />
|
||||
<div class="calendar-settings-trigger">
|
||||
<CalendarSettingsPopover
|
||||
:settings="calendarSettings"
|
||||
@update:settings="onSettingsUpdate" />
|
||||
</div>
|
||||
<CalendarToolbar
|
||||
:view-title="viewTitle"
|
||||
:active-view="activeView"
|
||||
:settings="calendarSettings"
|
||||
@prev="handlePrev"
|
||||
@next="handleNext"
|
||||
@today="handleToday"
|
||||
@change-view="handleChangeView"
|
||||
@update:settings="onSettingsUpdate" />
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger as="div" class="h-full" @contextmenu="handleCalendarContextMenu">
|
||||
<ContextMenuTrigger
|
||||
as="div"
|
||||
class="flex-1 min-h-0"
|
||||
@contextmenu="handleCalendarContextMenu">
|
||||
<FullCalendar ref="calendarRef" class="fullcalendar" :options="calendarOptions">
|
||||
<template #eventContent="arg">
|
||||
<FullCalendarEventContent
|
||||
@@ -743,13 +769,6 @@ onUnmounted(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.calendar-settings-trigger {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.fullcalendar {
|
||||
height: 100%;
|
||||
--fc-border-color: var(--border);
|
||||
@@ -764,49 +783,12 @@ onUnmounted(() => {
|
||||
|
||||
.fullcalendar :deep(.fc-timegrid-slot) {
|
||||
height: 25px;
|
||||
transition: height 0.2s ease;
|
||||
}
|
||||
|
||||
.fullcalendar :deep(.fc-timegrid-slot-label) {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
.fullcalendar :deep(.fc-toolbar) {
|
||||
background-color: var(--background);
|
||||
padding: 0.5rem;
|
||||
padding-right: 2.75rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.fullcalendar :deep(.fc-toolbar-title) {
|
||||
color: var(--foreground);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fullcalendar :deep(.fc-button) {
|
||||
background-color: var(--secondary);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--foreground);
|
||||
font-weight: 500;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.fullcalendar :deep(.fc-button:hover) {
|
||||
background-color: var(--muted);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.fullcalendar :deep(.fc-button:focus) {
|
||||
box-shadow: 0 0 0 2px var(--ring);
|
||||
}
|
||||
|
||||
.fullcalendar :deep(.fc-button-active) {
|
||||
background-color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
.fullcalendar :deep(.fc-col-header) {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ useSelectEvents(
|
||||
v-model="tempDescription"
|
||||
placeholder="What are you working on?"
|
||||
data-testid="time_entry_description"
|
||||
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3.5 border-b border-b-card-background-separator @2xl:px-4 text-lg text-text-primary bg-transparent border-none placeholder-text-secondary font-medium focus:ring-0 transition"
|
||||
class="w-full rounded-l-lg py-4 sm:py-2.5 px-3.5 border-b border-b-card-background-separator @2xl:px-4 text-base text-text-primary bg-transparent border-none placeholder-text-secondary font-medium focus:ring-0 transition"
|
||||
type="text"
|
||||
@keydown.enter="startTimerIfNotActive"
|
||||
@keydown.esc="showDropdown = false"
|
||||
|
||||
@@ -154,7 +154,7 @@ function closeAndFocusInput() {
|
||||
v-model="currentTime"
|
||||
placeholder="00:00:00"
|
||||
data-testid="time_entry_time"
|
||||
class="w-[110px] lg:w-[130px] h-full text-text-primary py-2.5 rounded-lg border-border-secondary border text-center px-4 text-base lg:text-lg font-semibold bg-card-background border-none placeholder-text-tertiary focus:ring-0 transition"
|
||||
class="w-[110px] lg:w-[120px] h-full text-text-primary py-2.5 rounded-lg border-border-secondary border text-center px-4 text-base font-semibold tabular-nums bg-card-background border-none placeholder-text-tertiary focus:ring-0 transition"
|
||||
type="text"
|
||||
@focusin="openModalOnTab"
|
||||
@click="openModalOnClick"
|
||||
|
||||
Reference in New Issue
Block a user